Fix Playback issues on cast and dlna players (#765)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 10 Jul 2023 18:41:00 +0000 (20:41 +0200)
committerGitHub <noreply@github.com>
Mon, 10 Jul 2023 18:41:00 +0000 (20:41 +0200)
* Fix playback issues on cast and dlna players

* rename api schema

* migrate audio format in provider_mappings column

music_assistant/client/client.py
music_assistant/constants.py
music_assistant/server/controllers/cache.py
music_assistant/server/controllers/music.py
music_assistant/server/controllers/streams.py
music_assistant/server/helpers/api.py
music_assistant/server/helpers/database.py
music_assistant/server/helpers/webserver.py
music_assistant/server/providers/chromecast/__init__.py
music_assistant/server/providers/filesystem_local/base.py
music_assistant/server/server.py

index 032d696069a5d578e068a43e96441f6bb2d33902..1700553dd3a2a3213e5166d997e69d44a4876c32 100644 (file)
@@ -24,7 +24,7 @@ from music_assistant.common.models.enums import EventType
 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 MediaItemImage
-from music_assistant.constants import SCHEMA_VERSION
+from music_assistant.constants import API_SCHEMA_VERSION
 
 from .connection import WebsocketsConnection
 from .music import Music
@@ -116,7 +116,7 @@ class MusicAssistantClient:
         info = ServerInfoMessage.from_dict(result)
 
         # basic check for server schema version compatibility
-        if info.min_supported_schema_version > SCHEMA_VERSION:
+        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()
             raise InvalidServerVersion(
index 4d213fd546688a3ff1c5f0672e264df4f40de4fc..4744a9c7221aef0e9310837572a3161f0fbf1038 100755 (executable)
@@ -3,7 +3,7 @@
 import pathlib
 from typing import Final
 
-SCHEMA_VERSION: Final[int] = 22
+API_SCHEMA_VERSION: Final[int] = 22
 MIN_SCHEMA_VERSION = 22
 
 ROOT_LOGGER_NAME: Final[str] = "music_assistant"
index b002b7f79ffe15076d9f66e92fdc29b26b39ebc7..e1b12653baf65ca3d9071f52ec56eb501494f392 100644 (file)
@@ -8,17 +8,12 @@ import os
 import time
 from collections import OrderedDict
 from collections.abc import Iterator, MutableMapping
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Final
 
 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,
-    ROOT_LOGGER_NAME,
-    SCHEMA_VERSION,
-)
+from music_assistant.constants import DB_TABLE_CACHE, DB_TABLE_SETTINGS, ROOT_LOGGER_NAME
 from music_assistant.server.helpers.database import DatabaseConnection
 from music_assistant.server.models.core_controller import CoreController
 
@@ -27,6 +22,7 @@ if TYPE_CHECKING:
 
 LOGGER = logging.getLogger(f"{ROOT_LOGGER_NAME}.cache")
 CONF_CLEAR_CACHE = "clear_cache"
+DB_SCHEMA_VERSION: Final[int] = 22
 
 
 class CacheController(CoreController):
@@ -182,14 +178,14 @@ class CacheController(CoreController):
         except (KeyError, ValueError):
             prev_version = 0
 
-        if prev_version not in (0, SCHEMA_VERSION):
+        if prev_version not in (0, DB_SCHEMA_VERSION):
             LOGGER.info(
                 "Performing database migration from %s to %s",
                 prev_version,
-                SCHEMA_VERSION,
+                DB_SCHEMA_VERSION,
             )
 
-            if prev_version < 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}")
 
@@ -199,7 +195,7 @@ class CacheController(CoreController):
         # store current schema version
         await self.database.insert_or_replace(
             DB_TABLE_SETTINGS,
-            {"key": "version", "value": str(SCHEMA_VERSION), "type": "str"},
+            {"key": "version", "value": str(DB_SCHEMA_VERSION), "type": "str"},
         )
         # compact db
         await self.database.execute("VACUUM")
index 9acebce1f1656c1af7373442940e3da1e75bb094..c5bc5621ba456b161e0753a2c1d41060f4859294 100755 (executable)
@@ -6,9 +6,10 @@ import logging
 import os
 import statistics
 from itertools import zip_longest
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Final
 
 from music_assistant.common.helpers.datetime import utc_timestamp
+from music_assistant.common.helpers.json import json_dumps, json_loads
 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 (
@@ -32,7 +33,6 @@ from music_assistant.constants import (
     DB_TABLE_TRACK_LOUDNESS,
     DB_TABLE_TRACKS,
     ROOT_LOGGER_NAME,
-    SCHEMA_VERSION,
 )
 from music_assistant.server.helpers.api import api_command
 from music_assistant.server.helpers.database import DatabaseConnection
@@ -51,6 +51,7 @@ if TYPE_CHECKING:
 LOGGER = logging.getLogger(f"{ROOT_LOGGER_NAME}.music")
 DEFAULT_SYNC_INTERVAL = 3 * 60  # default sync interval in minutes
 CONF_SYNC_INTERVAL = "sync_interval"
+DB_SCHEMA_VERSION: Final[int] = 23
 
 
 class MusicController(CoreController):
@@ -636,14 +637,40 @@ class MusicController(CoreController):
         except (KeyError, ValueError):
             prev_version = 0
 
-        if prev_version not in (0, SCHEMA_VERSION):
+        if prev_version not in (0, DB_SCHEMA_VERSION):
             LOGGER.info(
                 "Performing database migration from %s to %s",
                 prev_version,
-                SCHEMA_VERSION,
+                DB_SCHEMA_VERSION,
             )
 
-            if prev_version < 22:
+            if prev_version == 22:
+                # migrate provider_mapping column (audio_format)
+                for table in ("tracks", "albums"):
+                    async for item in self.database.iter_items(table):
+                        prov_mappings = json_loads(item["provider_mappings"])
+                        needs_update = False
+                        for mapping in prov_mappings:
+                            if "content_type" in mapping:
+                                needs_update = True
+                                mapping["audio_format"] = {
+                                    "content_type": mapping.pop("content_type"),
+                                    "sample_rate": mapping.pop("sample_rate"),
+                                    "bit_depth": mapping.pop("bit_depth"),
+                                    "channels": mapping.pop("channels", 2),
+                                    "bit_rate": mapping.pop("bit_rate", 320),
+                                }
+                        if needs_update:
+                            await self.database.update(
+                                table,
+                                {
+                                    "item_id": item["item_id"],
+                                },
+                                {
+                                    "provider_mappings": json_dumps(prov_mappings),
+                                },
+                            )
+            elif prev_version < 22:
                 # for now just keep it simple and just recreate the tables if the schema is too old
                 await self.database.execute(f"DROP TABLE IF EXISTS {DB_TABLE_ARTISTS}")
                 await self.database.execute(f"DROP TABLE IF EXISTS {DB_TABLE_ALBUMS}")
@@ -659,7 +686,7 @@ class MusicController(CoreController):
         # store current schema version
         await self.database.insert_or_replace(
             DB_TABLE_SETTINGS,
-            {"key": "version", "value": str(SCHEMA_VERSION), "type": "str"},
+            {"key": "version", "value": str(DB_SCHEMA_VERSION), "type": "str"},
         )
         # create indexes if needed
         await self.__create_database_indexes()
index eb179353bb8182b9979def30407a4267584d348a..8f18db4198ed1bcc24f7dbd5651fe9a8578f1625 100644 (file)
@@ -348,17 +348,17 @@ class StreamsController(CoreController):
             base_url=f"http://{self.publish_ip}:{self.publish_port}",
             static_routes=[
                 (
-                    "GET",
+                    "*",
                     "/{queue_id}/multi/{job_id}/{player_id}/{queue_item_id}.{fmt}",
                     self.serve_multi_subscriber_stream,
                 ),
                 (
-                    "GET",
+                    "*",
                     "/{queue_id}/flow/{queue_item_id}.{fmt}",
                     self.serve_queue_flow_stream,
                 ),
                 (
-                    "GET",
+                    "*",
                     "/{queue_id}/single/{queue_item_id}.{fmt}",
                     self.serve_queue_item_stream,
                 ),
@@ -923,7 +923,8 @@ class StreamsController(CoreController):
         input_args += ["-metadata", 'title="Music Assistant"']
         # select output args
         if output_format.content_type == ContentType.FLAC:
-            output_args = ["-f", "flac", "-compression_level", "3"]
+            # set compression level to 0 to prevent issues with cast players
+            output_args = ["-f", "flac", "-compression_level", "0"]
         elif output_format.content_type == ContentType.AAC:
             output_args = ["-f", "adts", "-c:a", "aac", "-b:a", "320k"]
         elif output_format.content_type == ContentType.MP3:
@@ -931,16 +932,14 @@ class StreamsController(CoreController):
         else:
             output_args = ["-f", output_format.content_type.value]
 
-        output_args += [
-            # append channels
-            "-ac",
-            str(output_format.channels),
-            # append sample rate
-            "-ar",
-            str(output_format.sample_rate),
-            # output = pipe
-            "-",
-        ]
+        # append channels
+        output_args += ["-ac", str(output_format.channels)]
+        # append sample rate (if codec is lossless)
+        if output_format.content_type.is_lossless():
+            output_args += ["-ar", str(output_format.sample_rate)]
+        # append output = pipe
+        output_args += ["-"]
+
         # collect extra and filter args
         # TODO: add convolution/DSP/roomcorrections here!
         extra_args = []
index 6731b5c80da5656bb3140faa638fabceebf59ebe..31791a9bf396ec9782a45a9c7d7353038563714b 100644 (file)
@@ -14,8 +14,6 @@ if TYPE_CHECKING:
     pass
 
 
-API_SCHEMA_VERSION = 1
-
 LOGGER = logging.getLogger(__name__)
 
 _F = TypeVar("_F", bound=Callable[..., Any])
index e76642c57541e671e8b2c89af80209ec6db5e4cb..c143e57d79221f9ac12d53ef22ff55cc344b71e3 100755 (executable)
@@ -1,7 +1,7 @@
 """Database helpers and logic."""
 from __future__ import annotations
 
-from collections.abc import Mapping
+from collections.abc import AsyncGenerator, Mapping
 from typing import Any
 
 import aiosqlite
@@ -149,3 +149,24 @@ class DatabaseConnection:
     async def execute(self, query: str | str, values: dict = None) -> Any:
         """Execute command on the database."""
         return await self._db.execute(query, values)
+
+    async def iter_items(
+        self,
+        table: str,
+        match: dict = 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
+            offset += limit
index dd60ebd3027d7def117bbc3cc529a11c1be639f5..efa75f61241ebd47244c7b4bbdb41471c35c0109 100644 (file)
@@ -121,7 +121,7 @@ class Webserver:
             if handler := self._dynamic_routes.get(key):
                 return await handler(request)
         # deny all other requests
-        self.logger.debug(
+        self.logger.warning(
             "Received unhandled %s request to %s from %s\nheaders: %s\n",
             request.method,
             request.path,
index 7c935ce8b985402a878f7ff551ecd62158df596f..be7ad9f5bbb9beb80f56f82df513f49e5b4b8e63 100644 (file)
@@ -205,11 +205,6 @@ class ChromecastProvider(PlayerProvider):
                 content_type=f'audio/{url.split(".")[-1].split("?")[0]}',
                 title="Music Assistant",
                 thumb=MASS_LOGO_ONLINE,
-                media_info={
-                    "customData": {
-                        "queue_item_id": "flow",
-                    }
-                },
             )
             return
 
index 9c0f81bf2c33d915ecaff76904d0e8c8ecc1bcf2..ae5bbcd9d46aa4fe0f9fbf7ebed0cd86680f4422 100644 (file)
@@ -39,8 +39,9 @@ from music_assistant.common.models.media_items import (
     StreamDetails,
     Track,
 )
-from music_assistant.constants import SCHEMA_VERSION, VARIOUS_ARTISTS, VARIOUS_ARTISTS_ID
+from music_assistant.constants import VARIOUS_ARTISTS, VARIOUS_ARTISTS_ID
 from music_assistant.server.controllers.cache import use_cache
+from music_assistant.server.controllers.music import DB_SCHEMA_VERSION
 from music_assistant.server.helpers.compare import compare_strings
 from music_assistant.server.helpers.playlists import parse_m3u, parse_pls
 from music_assistant.server.helpers.tags import parse_tags, split_items
@@ -278,7 +279,7 @@ class FileSystemProviderBase(MusicProvider):
         if MediaType.TRACK not in media_types or MediaType.PLAYLIST not in media_types:
             return
         cache_key = f"{self.instance_id}.checksums"
-        prev_checksums = await self.mass.cache.get(cache_key, SCHEMA_VERSION)
+        prev_checksums = await self.mass.cache.get(cache_key, DB_SCHEMA_VERSION)
         save_checksum_interval = 0
         if prev_checksums is None:
             prev_checksums = {}
@@ -337,13 +338,13 @@ class FileSystemProviderBase(MusicProvider):
             # save checksums every 100 processed items
             # this allows us to pickup where we leftoff when initial scan gets interrupted
             if save_checksum_interval == 100:
-                await self.mass.cache.set(cache_key, cur_checksums, SCHEMA_VERSION)
+                await self.mass.cache.set(cache_key, cur_checksums, DB_SCHEMA_VERSION)
                 save_checksum_interval = 0
             else:
                 save_checksum_interval += 1
 
         # store (final) checksums in cache
-        await self.mass.cache.set(cache_key, cur_checksums, SCHEMA_VERSION)
+        await self.mass.cache.set(cache_key, cur_checksums, DB_SCHEMA_VERSION)
 
     async def _process_deletions(self, deleted_files: set[str]) -> None:
         """Process all deletions."""
@@ -414,7 +415,7 @@ class FileSystemProviderBase(MusicProvider):
             )
         )
         playlist.owner = self.name
-        checksum = f"{SCHEMA_VERSION}.{file_item.checksum}"
+        checksum = f"{DB_SCHEMA_VERSION}.{file_item.checksum}"
         playlist.metadata.checksum = checksum
         return playlist
 
index 85c81e8f1324012c014d40d5fcac81360c13e834..078b0e6cd909bf864a3231e3a39e84aac2a02b41 100644 (file)
@@ -21,11 +21,11 @@ from music_assistant.common.models.errors import 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,
     MIN_SCHEMA_VERSION,
     ROOT_LOGGER_NAME,
-    SCHEMA_VERSION,
 )
 from music_assistant.server.controllers.cache import CacheController
 from music_assistant.server.controllers.config import ConfigController
@@ -175,7 +175,7 @@ class MusicAssistant:
         return ServerInfoMessage(
             server_id=self.server_id,
             server_version=self.version,
-            schema_version=SCHEMA_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,