Some small code quality changes to DLNA Provider
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 21 Feb 2026 22:36:29 +0000 (23:36 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 21 Feb 2026 22:36:29 +0000 (23:36 +0100)
music_assistant/providers/dlna/player.py
music_assistant/providers/dlna/provider.py

index 107426583dd36ca72ac36b5ea08052c886eb8e44..a4c130eb340f45d0a3ac9abd90a665e26ed15296 100644 (file)
@@ -500,13 +500,31 @@ class DLNAPlayer(Player):
             self.last_seen = now if do_ping else self.last_seen
         except UpnpError as err:
             self.logger.debug("Device unavailable: %r", err)
-            if TYPE_CHECKING:
-                assert isinstance(self.provider, DLNAPlayerProvider)  # for type checking
-            await self.provider._device_disconnect(self)
+            await self._device_disconnect()
             raise PlayerUnavailableError from err
         finally:
             self.force_poll = False
 
+    async def on_unload(self) -> None:
+        """Handle logic when the player is unloaded from the Player controller."""
+        await super().on_unload()
+        await self._device_disconnect()
+
+    async def _device_disconnect(self) -> None:
+        """Destroy connections to the device."""
+        async with self.lock:
+            if not self.device:
+                self.logger.debug("Disconnecting from device that's not connected")
+                return
+
+            self.logger.debug("Disconnecting from %s", self.device.name)
+
+            self.device.on_event = None
+            old_device = self.device
+            self.device = None
+            self.set_available(False)
+            await old_device.async_unsubscribe_services()
+
     @staticmethod
     def _extract_mac_from_uuid(uuid_value: str) -> str | None:
         """Try to extract MAC address from UUID.
index 176a8c921f67e9fa6d4118e85933722ba2e3a062..f566c8029664a76d05b6c778a749f1c28670ee2e 100644 (file)
@@ -12,7 +12,6 @@ from async_upnp_client.utils import CaseInsensitiveDict
 from music_assistant_models.player import DeviceInfo
 
 from music_assistant.constants import CONF_PLAYERS, VERBOSE_LOG_LEVEL
-from music_assistant.helpers.util import TaskManager
 from music_assistant.models.player_provider import PlayerProvider
 
 from .constants import CONF_NETWORK_SCAN
@@ -23,8 +22,8 @@ from .player import DLNAPlayer
 class DLNAPlayerProvider(PlayerProvider):
     """DLNA Player provider."""
 
-    dlnaplayers: dict[str, DLNAPlayer] = {}
     _discovery_running: bool = False
+    _ignored_udns: set[str]
 
     lock: asyncio.Lock
     requester: UpnpRequester
@@ -34,6 +33,7 @@ class DLNAPlayerProvider(PlayerProvider):
     async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self.lock = asyncio.Lock()
+        self._ignored_udns = set()
         # silence the async_upnp_client logger
         if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL):
             logging.getLogger("async_upnp_client").setLevel(logging.DEBUG)
@@ -50,10 +50,7 @@ class DLNAPlayerProvider(PlayerProvider):
         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))
+        self._ignored_udns = set()
 
     async def discover_players(self, use_multicast: bool = False) -> None:
         """Discover DLNA players on the network."""
@@ -105,30 +102,16 @@ class DLNAPlayerProvider(PlayerProvider):
             # 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
-            dlna_player.set_available(False)
-            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):
+            # skip devices that we've already determined should be ignored
+            if udn in self._ignored_udns:
+                return
+
+            if dlna_player := self.mass.players.get_player(udn):
                 # existing player
+                assert isinstance(dlna_player, DLNAPlayer)
                 if dlna_player.description_url == description_url and dlna_player.available:
                     # nothing to do, device is already connected
                     return
@@ -153,9 +136,7 @@ class DLNAPlayerProvider(PlayerProvider):
                     model="unknown",
                     manufacturer="unknown",
                 )
-                self.dlnaplayers[udn] = dlna_player
 
             # Setup will return False if the device should be ignored (e.g., passive speaker)
             if not await dlna_player.setup():
-                # Remove from dict if it was just added
-                self.dlnaplayers.pop(udn, None)
+                self._ignored_udns.add(udn)