A collection of small bugfixes and optimizations (#1092)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 19 Feb 2024 15:04:46 +0000 (16:04 +0100)
committerGitHub <noreply@github.com>
Mon, 19 Feb 2024 15:04:46 +0000 (16:04 +0100)
20 files changed:
music_assistant/common/models/config_entries.py
music_assistant/common/models/player.py
music_assistant/common/models/player_queue.py
music_assistant/common/models/provider.py
music_assistant/server/controllers/config.py
music_assistant/server/controllers/media/playlists.py
music_assistant/server/controllers/metadata.py
music_assistant/server/controllers/music.py
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/players.py
music_assistant/server/controllers/streams.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64
music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64
music_assistant/server/providers/airplay/bin/cliraop-macos-arm64
music_assistant/server/providers/slimproto/manifest.json
music_assistant/server/providers/snapcast/__init__.py
music_assistant/server/providers/ytmusic/__init__.py
music_assistant/server/server.py
pyproject.toml

index d14b4d09e92510f6c0a17340bf5c45f82afbbc6c..abed3c39409a777f004c15c34f50a173bea3b394 100644 (file)
@@ -3,14 +3,14 @@
 from __future__ import annotations
 
 import logging
-from collections.abc import Iterable  # noqa: TCH003
+from collections.abc import Iterable
 from dataclasses import dataclass
 from types import NoneType
 from typing import Any
 
 from mashumaro import DataClassDictMixin
 
-from music_assistant.common.models.enums import ProviderType  # noqa: TCH001
+from music_assistant.common.models.enums import ProviderType
 from music_assistant.constants import (
     CONF_AUTO_PLAY,
     CONF_CROSSFADE,
index e2b8ca7748bfdeea7061270d68d2dcac13823729..ba4e3fabf014f1508ef36f0e609ce073a396b4b5 100644 (file)
@@ -25,7 +25,7 @@ class Player(DataClassDictMixin):
     """Representation of a Player within Music Assistant."""
 
     player_id: str
-    provider: str
+    provider: str  # instance_id of the player provider
     type: PlayerType
     name: str
     available: bool
@@ -51,6 +51,7 @@ class Player(DataClassDictMixin):
     # active_source: return player_id of the active queue for this player
     # if the player is grouped and a group is active, this will be set to the group's player_id
     # otherwise it will be set to the own player_id
+    # can also be an actual different source if the player supports that
     active_source: str | None = None
 
     # current_item_id: return item_id/uri of the current active/loaded item on the player
index ed3e28bb3467a0666e5e9554ddf81fb8272af94d..30b9cae71febd6dd78e96c484a6f0ec640522844 100644 (file)
@@ -7,10 +7,10 @@ from dataclasses import dataclass, field
 
 from mashumaro import DataClassDictMixin
 
-from music_assistant.common.models.media_items import MediaItemType  # noqa: TCH001
+from music_assistant.common.models.media_items import MediaItemType
 
 from .enums import PlayerState, RepeatMode
-from .queue_item import QueueItem  # noqa: TCH001
+from .queue_item import QueueItem
 
 
 @dataclass
index 7327944348234d492305352b81b71deb6804524e..114a09d55ee0481743720edd29450e20a0b12c32 100644 (file)
@@ -1,7 +1,7 @@
 """Models for providers and plugins in the MA ecosystem."""
 from __future__ import annotations
 
-import asyncio  # noqa: TCH003
+import asyncio
 from dataclasses import dataclass, field
 from typing import Any, TypedDict
 
@@ -9,7 +9,7 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
 
 from music_assistant.common.helpers.json import load_json_file
 
-from .enums import MediaType, ProviderFeature, ProviderType  # noqa: TCH001
+from .enums import MediaType, ProviderFeature, ProviderType
 
 
 @dataclass
index 4704e1442431633bb5b3fb1e9854147efee0b279..c0331a8a2a3d3b3ab72780db86b8840518596e62 100644 (file)
@@ -320,13 +320,11 @@ class ConfigController:
     async def get_player_configs(self, provider: str | None = None) -> list[PlayerConfig]:
         """Return all known player configurations, optionally filtered by provider domain."""
         available_providers = {x.instance_id for x in self.mass.providers}
-        # add both domain and instance id
-        available_providers.update({x.domain for x in self.mass.providers})
         return [
             await self.get_player_config(raw_conf["player_id"])
             for raw_conf in list(self.get(CONF_PLAYERS, {}).values())
             # filter out unavailable providers
-            if self.mass.get_provider(raw_conf["provider"])
+            if raw_conf["provider"] in available_providers
             # optional provider filter
             and (provider in (None, raw_conf["provider"]))
         ]
index a7a8016431ef65f5c15244c45dc7c9036297cc31..91eb00adce4c0fbca413014b361621c14dcead28 100644 (file)
@@ -5,7 +5,7 @@ from __future__ import annotations
 import asyncio
 import random
 import time
-from collections.abc import AsyncGenerator  # noqa: TCH003
+from collections.abc import AsyncGenerator
 from typing import Any
 
 from music_assistant.common.helpers.datetime import utc_timestamp
index 072bfb514e363c3bc34747e0d0f6e50271319861..0de9f97ac9ab3d2eaf92fae8e447e96fef51b313 100644 (file)
@@ -329,9 +329,11 @@ class MetaDataController(CoreController):
 
         return None
 
-    def get_image_url(self, image: MediaItemImage, size: int = 0) -> str:
+    def get_image_url(
+        self, image: MediaItemImage, size: int = 0, prefer_proxy: bool = False
+    ) -> str:
         """Get (proxied) URL for MediaItemImage."""
-        if image.provider != "url":
+        if image.provider != "url" 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))
index 86cba133c4d834c7d523c57e9700e30cbd022114..50a274d1f7a8c89da0d56eff2fab5ed0a2a9d94d 100644 (file)
@@ -6,7 +6,7 @@ import asyncio
 import os
 import shutil
 import statistics
-from collections.abc import AsyncGenerator  # noqa: TCH003
+from collections.abc import AsyncGenerator
 from contextlib import suppress
 from itertools import zip_longest
 from typing import TYPE_CHECKING
index e14ad0b8341f072f9f6248968eaccff9b02f6f81..0b0390259b6f832f7ecf47dc8858d2c5ff166229 100644 (file)
@@ -5,7 +5,7 @@ from __future__ import annotations
 import logging
 import random
 import time
-from collections.abc import AsyncGenerator  # noqa: TCH003
+from collections.abc import AsyncGenerator
 from contextlib import suppress
 from typing import TYPE_CHECKING, Any
 
index 04c7ebdd06e7a2760766915b45997270f7bd9d3f..9608742f253894f82907d7f036253ecd144c3db3 100644 (file)
@@ -168,6 +168,12 @@ class PlayerController(CoreController):
             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
@@ -203,6 +209,7 @@ class PlayerController(CoreController):
             return
 
         if player.player_id in self._players:
+            self._players[player.player_id] = player
             self.update(player.player_id)
             return
 
@@ -665,6 +672,12 @@ class PlayerController(CoreController):
         elif child_player.state == PlayerState.PLAYING:
             # stop child player if it is currently playing
             await self.cmd_stop(player_id)
+        if player_id not in parent_player.can_sync_with:
+            raise RuntimeError(
+                "Player %s can not be synced with %s",
+                child_player.display_name,
+                parent_player.display_name,
+            )
         # all checks passed, forward command to the player provider
         player_provider = self.get_player_provider(player_id)
         await player_provider.cmd_sync(player_id, target_player)
@@ -695,6 +708,8 @@ class PlayerController(CoreController):
         # all checks passed, forward command to the player provider
         player_provider = self.get_player_provider(player_id)
         await player_provider.cmd_unsync(player_id)
+        # reset active_source just in case
+        player.active_source = None
 
     @api_command("players/create_group")
     async def create_group(self, provider: str, name: str, members: list[str]) -> Player:
index 4ee75d097156f78d822afba971fc14afff8f44bb..f678a61a55846437e8f4d79911b51ec4ed5e140a 100644 (file)
@@ -12,7 +12,7 @@ import asyncio
 import logging
 import time
 import urllib.parse
-from collections.abc import AsyncGenerator  # noqa: TCH003
+from collections.abc import AsyncGenerator
 from contextlib import suppress
 from typing import TYPE_CHECKING
 
index 9cdd29ea474a1975e933b74ed142ff2ac5aa2f49..000e7f9c1b48ee2da3f4b57ab7a94ce747a6e8ce 100644 (file)
@@ -7,12 +7,10 @@ import os
 import platform
 import socket
 import time
+from collections.abc import AsyncGenerator
 from random import randint, randrange
 from typing import TYPE_CHECKING, cast
 
-import aiofiles
-import shortuuid
-from aiofiles.os import wrap
 from pyatv import connect, exceptions, interface, scan
 from pyatv.conf import AppleTV as ATVConf
 from pyatv.const import DeviceModel, DeviceState, PowerState, Protocol
@@ -40,6 +38,7 @@ from music_assistant.common.models.enums import (
 )
 from music_assistant.common.models.media_items import AudioFormat
 from music_assistant.common.models.player import DeviceInfo, Player
+from music_assistant.common.models.player_queue import PlayerQueue
 from music_assistant.server.helpers.process import AsyncProcess, check_output
 from music_assistant.server.models.player_provider import PlayerProvider
 
@@ -58,6 +57,7 @@ CONF_ENCRYPTION = "encryption"
 CONF_ALAC_ENCODE = "alac_encode"
 CONF_VOLUME_START = "volume_start"
 CONF_SYNC_ADJUST = "sync_adjust"
+CONF_PASSWORD = "password"
 PLAYER_CONFIG_ENTRIES = (
     CONF_ENTRY_CROSSFADE,
     CONF_ENTRY_CROSSFADE_DURATION,
@@ -111,6 +111,15 @@ PLAYER_CONFIG_ENTRIES = (
         "you can shift the audio a bit.",
         advanced=True,
     ),
+    ConfigEntry(
+        key=CONF_PASSWORD,
+        type=ConfigEntryType.STRING,
+        default_value=None,
+        required=False,
+        label="Device password",
+        description="Some devices require a password to connect/play.",
+        advanced=True,
+    ),
 )
 BACKOFF_TIME_LOWER_LIMIT = 15  # seconds
 BACKOFF_TIME_UPPER_LIMIT = 300  # Five minutes
@@ -591,7 +600,7 @@ class AirplayProvider(PlayerProvider):
                 if abs(volume - int(atv_player.atv.audio.volume)) > 2:
                     self.mass.create_task(self.cmd_volume_set(player_id, volume))
             else:
-                self.logger.warning(
+                self.logger.debug(
                     "Unknown DACP request for %s: %s",
                     atv_player.discovery_info.name,
                     path,
@@ -680,14 +689,62 @@ class AirplayProvider(PlayerProvider):
         """
         # stop existing streams first
         await self.cmd_stop(player_id)
+        # power on player if needed
+        # start streaming the queue (pcm) audio in a background task
+        queue = self.mass.player_queues.get_active_queue(player_id)
+        self._stream_tasks[player_id] = asyncio.create_task(
+            self._stream_audio(
+                player_id,
+                queue=queue,
+                audio_iterator=self.mass.streams.get_flow_stream(
+                    queue,
+                    start_queue_item=queue_item,
+                    pcm_format=AudioFormat(
+                        content_type=ContentType.PCM_S16LE,
+                        sample_rate=44100,
+                        bit_depth=16,
+                        channels=2,
+                    ),
+                    seek_position=seek_position,
+                    fade_in=fade_in,
+                ),
+            )
+        )
+
+    async def play_stream(self, player_id: str, stream_job: MultiClientStreamJob) -> None:
+        """Handle PLAY STREAM on given player.
+
+        This is a special feature from the Universal Group provider.
+        """
+        # stop existing streams first
+        await self.cmd_stop(player_id)
+        # power on player if needed
         await self.cmd_power(player_id, True)
-        atv_player = self._atv_players[player_id]
-        player = self.mass.players.get(player_id)
+        if stream_job.pcm_format.bit_depth != 16 or stream_job.pcm_format.sample_rate != 44100:
+            # TODO: resample on the fly here ?
+            raise RuntimeError("Unsupported PCM format")
+        # start streaming the queue (pcm) audio in a background task
+        queue = self.mass.player_queues.get_active_queue(player_id)
+        self._stream_tasks[player_id] = asyncio.create_task(
+            self._stream_audio(
+                player_id,
+                queue=queue,
+                audio_iterator=stream_job.subscribe(player_id),
+            )
+        )
 
+    async def _stream_audio(
+        self, player_id: str, queue: PlayerQueue, audio_iterator: AsyncGenerator[bytes, None]
+    ) -> None:
+        """Handle the actual streaming of audio to Airplay."""
+        player = self.mass.players.get(player_id)
         if player.synced_to:
             # should not happen, but just in case
             raise RuntimeError("Player is synced")
-
+        player.elapsed_time = 0
+        player.elapsed_time_last_updated = time.time()
+        player.state = PlayerState.PLAYING
+        self.mass.players.update(player_id)
         # NOTE: Although the pyatv library is perfectly capable of playback
         # to not only raop targets but also airplay 1 + 2, its not suitable
         # for synced playback to multiple clients at once.
@@ -709,76 +766,42 @@ class AirplayProvider(PlayerProvider):
                     # just in case...
                     await atv_player.connect()
                 tg.create_task(self._init_cliraop(atv_player, ntp))
-
-        async def _streamer() -> None:
-            queue = self.mass.player_queues.get(queue_item.queue_id)
-            player.current_item_id = f"{queue_item.queue_id}.{queue_item.queue_item_id}"
-            player.elapsed_time = 0
-            player.elapsed_time_last_updated = time.time()
-            player.state = PlayerState.PLAYING
-            self.mass.players.register_or_update(player)
-            prev_metadata_checksum: str = ""
-            pcm_format = AudioFormat(
-                content_type=ContentType.PCM_S16LE,
-                sample_rate=44100,
-                bit_depth=16,
-                channels=2,
-            )
-            try:
-                async for pcm_chunk in self.mass.streams.get_flow_stream(
-                    queue,
-                    start_queue_item=queue_item,
-                    pcm_format=pcm_format,
-                    seek_position=seek_position,
-                    fade_in=fade_in,
-                ):
-                    # send metadata to player(s) if needed
-                    # NOTE: this must all be done in separate tasks to not disturb audio
-                    if 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
-                            self.mass.create_task(self._send_metadata(player_id))
-
-                    # send audio chunk to player(s)
-                    async with asyncio.TaskGroup() as tg:
-                        available_clients = 0
+        prev_metadata_checksum: str = ""
+        try:
+            async for pcm_chunk in audio_iterator:
+                # send metadata to player(s) if needed
+                # NOTE: this must all be done in separate tasks to not disturb audio
+                if 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
+                        self.mass.create_task(self._send_metadata(player_id))
+
+                async with asyncio.TaskGroup() as tg:
+                    # send progress metadata
+                    if queue.elapsed_time:
                         for atv_player in self._get_sync_clients(player_id):
-                            if not atv_player.cliraop_proc or atv_player.cliraop_proc.closed:
-                                # this may not happen, but just in case
-                                continue
-                            available_clients += 1
-                            tg.create_task(atv_player.cliraop_proc.write(pcm_chunk))
-                        if not available_clients:
-                            return
-
-                        # send progress metadata
-                        if queue.elapsed_time:
-                            for atv_player in self._get_sync_clients(player_id):
-                                tg.create_task(
-                                    atv_player.send_cli_command(
-                                        f"PROGRESS={int(queue.elapsed_time)}\n"
-                                    )
-                                )
-
-            finally:
-                self.logger.debug("Streamer task ended for player %s", queue.display_name)
-                for atv_player in self._get_sync_clients(player_id):
-                    if atv_player.cliraop_proc and not atv_player.cliraop_proc.closed:
-                        atv_player.cliraop_proc.write_eof()
-
-        # start streaming the queue (pcm) audio in a background task
-        self._stream_tasks[player_id] = asyncio.create_task(_streamer())
-
-    async def play_stream(self, player_id: str, stream_job: MultiClientStreamJob) -> None:
-        """Handle PLAY STREAM on given player.
-
-        This is a special feature from the Universal Group provider.
-        """
-        raise NotImplementedError
+                            tg.create_task(
+                                atv_player.send_cli_command(f"PROGRESS={int(queue.elapsed_time)}\n")
+                            )
+                    # send audio chunk to player(s)
+                    available_clients = 0
+                    for atv_player in self._get_sync_clients(player_id):
+                        if not atv_player.cliraop_proc or atv_player.cliraop_proc.closed:
+                            # this may not happen, but just in case
+                            continue
+                        available_clients += 1
+                        tg.create_task(atv_player.cliraop_proc.write(pcm_chunk))
+                    if not available_clients:
+                        return
+        finally:
+            self.logger.debug("Streaming ended for player %s", player.display_name)
+            for atv_player in self._get_sync_clients(player_id):
+                if atv_player.cliraop_proc and not atv_player.cliraop_proc.closed:
+                    atv_player.cliraop_proc.write_eof()
 
     async def cmd_power(self, player_id: str, powered: bool) -> None:
         """Send POWER command to given player.
@@ -806,7 +829,7 @@ class AirplayProvider(PlayerProvider):
         if atv_player.cliraop_proc:
             # prefer interactive command to our streamer
             await atv_player.send_cli_command(f"VOLUME={volume_level}\n")
-        elif atv := atv_player.atv:
+        if atv := atv_player.atv:
             await atv.audio.set_volume(volume_level)
 
     async def cmd_sync(self, player_id: str, target_player: str) -> None:
@@ -843,8 +866,7 @@ class AirplayProvider(PlayerProvider):
         group_leader = self.mass.players.get(player.synced_to, raise_unavailable=True)
         group_leader.group_childs.remove(player_id)
         player.synced_to = None
-        if player.state == PlayerState.PLAYING:
-            await self.cmd_stop(player_id)
+        await self.cmd_stop(player_id)
         self.mass.players.update(player_id)
 
     async def _run_discovery(self) -> None:
@@ -962,12 +984,16 @@ class AirplayProvider(PlayerProvider):
             logger = self.logger.getChild(atv_player.player_id)
             async for line in cliraop_proc._proc.stderr:
                 line = line.decode().strip()  # noqa: PLW2901
+                if not line:
+                    continue
                 if "set pause" in line:
                     atv_player.optimistic_state = PlayerState.PAUSED
                     atv_player.update_attributes()
-                if "Restarted at" in line:
+                    logger.info(line)
+                elif "Restarted at" in line:
                     atv_player.optimistic_state = PlayerState.PLAYING
                     atv_player.update_attributes()
+                    logger.info(line)
                 elif "after start), played" in line:
                     millis = int(line.split("played ")[1].split(" ")[0])
                     mass_player.elapsed_time = millis / 1000
@@ -1002,6 +1028,10 @@ class AirplayProvider(PlayerProvider):
         sync_adjust = self.mass.config.get_raw_player_config_value(
             atv_player.player_id, CONF_SYNC_ADJUST, 0
         )
+        if device_password := self.mass.config.get_raw_player_config_value(
+            atv_player.player_id, CONF_PASSWORD, None
+        ):
+            extra_args += ["-P", device_password]
         if self.logger.level == logging.DEBUG:
             extra_args += ["-d", "5"]
 
@@ -1070,19 +1100,9 @@ class AirplayProvider(PlayerProvider):
         # get image
         if not queue.current_item.image:
             return
-        temp_image_path = f"/tmp/{shortuuid.random(12)}"  # noqa: S108
-        image_data = await self.mass.metadata.get_thumbnail(
-            queue.current_item.image.path,
-            512,
-            queue.current_item.image.provider,
+
+        image_url = self.mass.metadata.get_image_url(
+            queue.current_item.image, size=512, prefer_proxy=True
         )
-        if not image_data:
-            return
-        async with aiofiles.open(temp_image_path, "wb") as outfile:
-            await outfile.write(image_data)
         for atv_player in self._get_sync_clients(player_id):
-            await atv_player.send_cli_command(f"ARTWORK={temp_image_path}\n")
-        # make sure the temp file gets deleted again
-        await asyncio.sleep(30)
-        rm_func = wrap(os.remove)
-        await rm_func(temp_image_path)
+            await atv_player.send_cli_command(f"ARTWORK={image_url}\n")
index 0bd6d271671e92256bab52c000d073e6be81ba65..ea1044350137dd507bf2f97d3fb4cfc47b3c511c 100755 (executable)
Binary files a/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 and b/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 differ
index eefe426a3caddf2efdc07a0b26d6bf31506fe04e..c6a24fb1f1fc536efe941915578639be2c8c670b 100755 (executable)
Binary files a/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 and b/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 differ
index e04e68da98171d519c931d7d138298ea1e0165a1..09a31d9711b4129144550484d62b36d5558ebc10 100755 (executable)
Binary files a/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 and b/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 differ
index 4b5e29d6f753454c01221a3242681128e1d65e2e..cc0e4aed7cb770f5d7edac0822e764171f2b033f 100644 (file)
@@ -3,10 +3,14 @@
   "domain": "slimproto",
   "name": "Slimproto",
   "description": "Support for slimproto based players (e.g. squeezebox, squeezelite).",
-  "codeowners": ["@music-assistant"],
-  "requirements": ["aioslimproto==2.3.3"],
+  "codeowners": [
+    "@music-assistant"
+  ],
+  "requirements": [
+    "aioslimproto==2.3.3"
+  ],
   "documentation": "https://music-assistant.github.io/player-support/slimproto/",
   "multi_instance": false,
   "builtin": false,
-  "load_by_default": true
+  "load_by_default": false
 }
index c7fcccfa922a4951ffe8f39f550892175a5489d8..edb8df36f5ae99376f07bcfdda73bcb397de6a7d 100644 (file)
@@ -314,13 +314,9 @@ class SnapCastProvider(PlayerProvider):
             raise RuntimeError(msg)
         # stop any existing streams first
         await self.cmd_stop(player_id)
-        # TEMP - TODO - WARNING - ACHTUNG - HACK
-        # override pcm format of streamjob due to issue with snapcast
-        # that seems to only accept a 48000/16 stream somehow ?!
-        stream_job.pcm_format.content_type = ContentType.PCM_S16LE
-        stream_job.pcm_format.sample_rate = 48000
-        stream_job.pcm_format.bit_depth = 16
-        # end of hack
+        if stream_job.pcm_format.bit_depth != 16 or stream_job.pcm_format.sample_rate != 48000:
+            # TODO: resample on the fly here ?
+            raise RuntimeError("Unsupported PCM format")
         stream, port = await self._create_stream()
         stream_job.expected_players.add(player_id)
         snap_group = self._get_snapgroup(player_id)
index 71a4ce58d16b66f3ba7c1fe79fa565e92e3e39c3..9f2508a97982cd165aca21c08a9c713414efe50c 100644 (file)
@@ -5,7 +5,7 @@ from __future__ import annotations
 import asyncio
 import logging
 import re
-from collections.abc import AsyncGenerator  # noqa: TCH003
+from collections.abc import AsyncGenerator
 from operator import itemgetter
 from time import time
 from typing import TYPE_CHECKING
index 43309e6103581e74c3421985dbfe019a954ccdf6..8d3b648ce304aaa7b81863e47ea6734ef99e72c0 100644 (file)
@@ -45,7 +45,7 @@ from music_assistant.server.helpers.util import (
     is_hass_supervisor,
 )
 
-from .models import ProviderInstanceType  # noqa: TCH001
+from .models import ProviderInstanceType
 
 if TYPE_CHECKING:
     from types import TracebackType
index 6ea92cd4803ae547d1b8625b64cec68dcfcaf81c..590ae6bfca3740b67a3a91540b971233df808214 100644 (file)
@@ -191,6 +191,8 @@ ignore = [
   "PLR2004", # Just annoying, not really useful
   "PD011", # Just annoying, not really useful
   "S101", # assert is often used to satisfy type checking
+  "TCH001", # Just annoying, not really useful
+  "TCH003", # Just annoying, not really useful
   "TD002", # Just annoying, not really useful
   "TD003", # Just annoying, not really useful
   "TD004", # Just annoying, not really useful