pywiim 2.3.2__tar.gz → 2.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {pywiim-2.3.2/pywiim.egg-info → pywiim-2.3.3}/PKG-INFO +1 -1
  2. {pywiim-2.3.2 → pywiim-2.3.3}/pyproject.toml +1 -1
  3. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/__init__.py +1 -1
  4. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/__init__.py +6 -2
  5. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/base.py +4 -0
  6. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/media.py +75 -0
  7. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/properties.py +31 -20
  8. {pywiim-2.3.2 → pywiim-2.3.3/pywiim.egg-info}/PKG-INFO +1 -1
  9. {pywiim-2.3.2 → pywiim-2.3.3}/LICENSE +0 -0
  10. {pywiim-2.3.2 → pywiim-2.3.3}/README.md +0 -0
  11. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/__init__.py +0 -0
  12. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/audio_pro.py +0 -0
  13. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/audio_settings.py +0 -0
  14. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/base.py +0 -0
  15. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/bluetooth.py +0 -0
  16. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/constants.py +0 -0
  17. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/device.py +0 -0
  18. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/diagnostics.py +0 -0
  19. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/endpoints.py +0 -0
  20. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/eq.py +0 -0
  21. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/firmware.py +0 -0
  22. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/group.py +0 -0
  23. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/lms.py +0 -0
  24. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/loop_mode.py +0 -0
  25. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/misc.py +0 -0
  26. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/parser.py +0 -0
  27. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/peq.py +0 -0
  28. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/playback.py +0 -0
  29. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/preset.py +0 -0
  30. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/ssl.py +0 -0
  31. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/subwoofer.py +0 -0
  32. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/timer.py +0 -0
  33. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/backoff.py +0 -0
  34. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/capabilities.py +0 -0
  35. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/__init__.py +0 -0
  36. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/diagnostics.py +0 -0
  37. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/discovery_cli.py +0 -0
  38. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/group_test_cli.py +0 -0
  39. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/join_test_cli.py +0 -0
  40. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/monitor_cli.py +0 -0
  41. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/verify_cli.py +0 -0
  42. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/client.py +0 -0
  43. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/device_capabilities.py +0 -0
  44. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/discovery.py +0 -0
  45. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/exceptions.py +0 -0
  46. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/group.py +0 -0
  47. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/group_helpers.py +0 -0
  48. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/__init__.py +0 -0
  49. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/__main__.py +0 -0
  50. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/config.example.json +0 -0
  51. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/config.py +0 -0
  52. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/context.py +0 -0
  53. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/server.py +0 -0
  54. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/metadata.py +0 -0
  55. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/model_names.py +0 -0
  56. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/models.py +0 -0
  57. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/normalize.py +0 -0
  58. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/audio.py +0 -0
  59. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/bluetooth.py +0 -0
  60. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/coverart.py +0 -0
  61. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/debounce.py +0 -0
  62. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/diagnostics.py +0 -0
  63. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/groupops.py +0 -0
  64. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/playback.py +0 -0
  65. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/source_capabilities.py +0 -0
  66. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/statemgr.py +0 -0
  67. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/stream.py +0 -0
  68. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/stream_enricher.py +0 -0
  69. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/volume.py +0 -0
  70. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/polling.py +0 -0
  71. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/profiles.py +0 -0
  72. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/py.typed +0 -0
  73. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/role.py +0 -0
  74. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/state.py +0 -0
  75. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/__init__.py +0 -0
  76. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/client.py +0 -0
  77. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/eventer.py +0 -0
  78. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/health.py +0 -0
  79. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/metadata.py +0 -0
  80. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/playqueue.py +0 -0
  81. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/SOURCES.txt +0 -0
  82. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/dependency_links.txt +0 -0
  83. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/entry_points.txt +0 -0
  84. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/requires.txt +0 -0
  85. {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/top_level.txt +0 -0
  86. {pywiim-2.3.2 → pywiim-2.3.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywiim
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: Python library for WiiM/LinkPlay device communication
5
5
  Author-email: Michael Cumming <mjcumming@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pywiim"
7
- version = "2.3.2"
7
+ version = "2.3.3"
8
8
  description = "Python library for WiiM/LinkPlay device communication"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -102,7 +102,7 @@ from .profiles import (
102
102
  from .role import RoleDetectionResult, detect_role
103
103
  from .state import GroupStateSynchronizer, StateSynchronizer
104
104
 
105
- __version__ = "2.3.2"
105
+ __version__ = "2.3.3"
106
106
  __all__ = [
107
107
  # Main client
108
108
  "WiiMClient",
@@ -987,12 +987,16 @@ class Player(PlayerBase):
987
987
 
988
988
  @property
989
989
  def queue_count(self) -> int | None:
990
- """Total number of tracks in queue (from HTTP API plicount field)."""
990
+ """Total tracks in queue (HTTP plicount, or PlayQueue overlay)."""
991
991
  return self._properties.queue_count
992
992
 
993
993
  @property
994
994
  def queue_position(self) -> int | None:
995
- """Current track position in queue (from HTTP API plicurr field)."""
995
+ """Current queue position (HTTP plicurr, or PlayQueue overlay).
996
+
997
+ Overlay is 0-based to match play_queue()/get_queue() item positions.
998
+ HTTP plicount/plicurr stay 0/empty on the PlayQueue path (ADR 004).
999
+ """
996
1000
  return self._properties.queue_position
997
1001
 
998
1002
  @property
@@ -148,6 +148,10 @@ class PlayerBase:
148
148
  # Last played URL tracking (for media_title fallback)
149
149
  self._last_played_url: str | None = None
150
150
 
151
+ # PlayQueue overlay when HTTP plicount/plicurr stay empty (ADR 004)
152
+ self._playqueue_count: int | None = None
153
+ self._playqueue_position: int | None = None
154
+
151
155
  # Cover art cache (in-memory, keyed by URL hash)
152
156
  # Format: {url_hash: (image_bytes, content_type, timestamp)}
153
157
  self._cover_art_cache: dict[str, tuple[bytes, str, float]] = {}
@@ -8,6 +8,7 @@ from html import unescape
8
8
  from typing import TYPE_CHECKING, Any, Literal
9
9
  from xml.etree import ElementTree as ET
10
10
 
11
+ from ..api.loop_mode import resolve_loop_mode_mapping_for_player
11
12
  from ..exceptions import WiiMError
12
13
  from ..upnp.playqueue import (
13
14
  CURRENT_QUEUE,
@@ -507,10 +508,76 @@ class MediaControl:
507
508
  },
508
509
  )
509
510
 
511
+ def _playqueue_loop_mode(self) -> int:
512
+ """PlayQueue LoopMode: 4 = play through once; map shuffle/repeat when set."""
513
+ try:
514
+ mapping = resolve_loop_mode_mapping_for_player(self.player)
515
+ shuffle = bool(self.player.shuffle_state)
516
+ repeat = (self.player.repeat_mode or "off").lower()
517
+ return mapping.to_loop_mode(
518
+ shuffle=shuffle,
519
+ repeat_one=repeat == "one",
520
+ repeat_all=repeat == "all",
521
+ )
522
+ except Exception:
523
+ return 4
524
+
525
+ async def _playqueue_apply_loop_mode(self) -> None:
526
+ """Enable PlayQueue auto-advance via SetQueueLoopMode if advertised."""
527
+ upnp = self.player._upnp_client
528
+ if not upnp or not service_has_action(upnp.play_queue, "SetQueueLoopMode"):
529
+ return
530
+ mode = self._playqueue_loop_mode()
531
+ try:
532
+ await self._playqueue_call("SetQueueLoopMode", {"LoopMode": mode})
533
+ _LOGGER.debug("Set PlayQueue loop mode %s on %s", mode, self.player.host)
534
+ except Exception as err:
535
+ _LOGGER.debug("SetQueueLoopMode failed on %s: %s", self.player.host, err)
536
+
537
+ def _playqueue_set_overlay(self, *, count: int | None = None, position: int | None = None) -> None:
538
+ if count is not None:
539
+ self.player._playqueue_count = count
540
+ if position is not None:
541
+ self.player._playqueue_position = position
542
+
543
+ async def _playqueue_sync_overlay(self, *, items: list[dict[str, Any]] | None = None) -> None:
544
+ """Refresh overlay from GetQueueIndex / BrowseQueue (ADR 004)."""
545
+ if items is not None:
546
+ self._playqueue_set_overlay(count=len(items))
547
+ try:
548
+ result = await self._playqueue_call("GetQueueIndex", {"QueueName": CURRENT_QUEUE})
549
+ except Exception as err:
550
+ _LOGGER.debug("GetQueueIndex overlay failed on %s: %s", self.player.host, err)
551
+ result = None
552
+ if result:
553
+ if items is None:
554
+ track_nums = result.get("TrackNums")
555
+ if track_nums is not None:
556
+ try:
557
+ self._playqueue_set_overlay(count=int(track_nums))
558
+ except (TypeError, ValueError):
559
+ pass
560
+ current = result.get("CurrentIndex")
561
+ if current is not None:
562
+ try:
563
+ idx = int(current)
564
+ # CurrentIndex is 1-based
565
+ self._playqueue_set_overlay(position=max(idx - 1, 0) if idx > 0 else None)
566
+ except (TypeError, ValueError):
567
+ pass
568
+ if items is None and self.player._playqueue_count is None:
569
+ try:
570
+ result = await self._playqueue_call("BrowseQueue", {"QueueName": CURRENT_QUEUE})
571
+ parsed = parse_queue_context((result or {}).get("QueueContext", "") or "")
572
+ self._playqueue_set_overlay(count=len(parsed))
573
+ except Exception as err:
574
+ _LOGGER.debug("BrowseQueue overlay failed on %s: %s", self.player.host, err)
575
+
510
576
  async def _playqueue_add(self, url: str, metadata: str = "") -> None:
511
577
  """Add URL to the end of CurrentQueue without starting playback."""
512
578
  await self._playqueue_ensure()
513
579
  await self._playqueue_append(url, metadata)
580
+ await self._playqueue_sync_overlay()
514
581
  _LOGGER.debug("Added URL to PlayQueue on %s", self.player.host)
515
582
 
516
583
  async def _playqueue_insert_next(self, url: str, metadata: str = "") -> None:
@@ -525,6 +592,7 @@ class MediaControl:
525
592
  except Exception as err:
526
593
  _LOGGER.debug("GetQueueIndex failed on %s, appending after index 1: %s", self.player.host, err)
527
594
  await self._playqueue_append(url, metadata, start_index=start_index)
595
+ await self._playqueue_sync_overlay()
528
596
  _LOGGER.debug("Inserted URL next in PlayQueue on %s", self.player.host)
529
597
 
530
598
  async def _playqueue_get(self) -> list[dict[str, Any]]:
@@ -535,6 +603,7 @@ class MediaControl:
535
603
  _LOGGER.debug("PlayQueue BrowseQueue empty or failed on %s: %s", self.player.host, err)
536
604
  return []
537
605
  items = parse_queue_context((result or {}).get("QueueContext", "") or "")
606
+ await self._playqueue_sync_overlay(items=items)
538
607
  _LOGGER.debug("Retrieved %d PlayQueue items from %s", len(items), self.player.host)
539
608
  return items
540
609
 
@@ -852,6 +921,9 @@ class MediaControl:
852
921
  "PlayQueueWithIndex",
853
922
  {"QueueName": CURRENT_QUEUE, "Index": queue_position + 1},
854
923
  )
924
+ await self._playqueue_apply_loop_mode()
925
+ self._playqueue_set_overlay(position=queue_position)
926
+ await self._playqueue_sync_overlay()
855
927
  _LOGGER.debug(
856
928
  "Started PlayQueue playback at position %d on %s",
857
929
  queue_position,
@@ -925,6 +997,7 @@ class MediaControl:
925
997
  "Action": "",
926
998
  },
927
999
  )
1000
+ await self._playqueue_sync_overlay()
928
1001
  _LOGGER.debug(
929
1002
  "Removed PlayQueue item at position %d from %s",
930
1003
  queue_position,
@@ -984,6 +1057,8 @@ class MediaControl:
984
1057
  if self._use_playqueue_enqueue():
985
1058
  try:
986
1059
  await self._playqueue_call("DeleteQueue", {"QueueName": CURRENT_QUEUE})
1060
+ self.player._playqueue_count = 0
1061
+ self.player._playqueue_position = None
987
1062
  _LOGGER.debug("Cleared PlayQueue on %s", self.player.host)
988
1063
  return
989
1064
  except Exception as err:
@@ -480,31 +480,42 @@ class PlayerProperties:
480
480
 
481
481
  return DEFAULT_WIIM_LOGO_URL
482
482
 
483
- @property
484
- def queue_count(self) -> int | None:
485
- """Total number of tracks in queue (from HTTP API plicount field)."""
483
+ def _http_queue_int(self, field: str) -> int | None:
486
484
  if self.player._status_model is None:
487
485
  return None
488
- count = getattr(self.player._status_model, "queue_count", None)
489
- if count is not None:
490
- try:
491
- return int(count)
492
- except (TypeError, ValueError):
493
- return None
494
- return None
486
+ value = getattr(self.player._status_model, field, None)
487
+ if value is None:
488
+ return None
489
+ try:
490
+ return int(value)
491
+ except (TypeError, ValueError):
492
+ return None
493
+
494
+ @property
495
+ def queue_count(self) -> int | None:
496
+ """Total tracks in queue (HTTP plicount, or PlayQueue overlay)."""
497
+ http = self._http_queue_int("queue_count")
498
+ overlay = self.player._playqueue_count
499
+ if http:
500
+ return http
501
+ if overlay is not None:
502
+ return overlay
503
+ return http
495
504
 
496
505
  @property
497
506
  def queue_position(self) -> int | None:
498
- """Current track position in queue (from HTTP API plicurr field)."""
499
- if self.player._status_model is None:
500
- return None
501
- position = getattr(self.player._status_model, "queue_position", None)
502
- if position is not None:
503
- try:
504
- return int(position)
505
- except (TypeError, ValueError):
506
- return None
507
- return None
507
+ """Current queue position (HTTP plicurr, or PlayQueue overlay).
508
+
509
+ Overlay is 0-based to match play_queue()/get_queue() item positions.
510
+ HTTP plicount/plicurr stay 0/empty on the PlayQueue path (ADR 004).
511
+ """
512
+ http = self._http_queue_int("queue_position")
513
+ overlay = self.player._playqueue_position
514
+ if http:
515
+ return http
516
+ if overlay is not None:
517
+ return overlay
518
+ return http
508
519
 
509
520
  @property
510
521
  def media_sample_rate(self) -> int | None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywiim
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: Python library for WiiM/LinkPlay device communication
5
5
  Author-email: Michael Cumming <mjcumming@gmail.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes