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.
- {pywiim-2.3.2/pywiim.egg-info → pywiim-2.3.3}/PKG-INFO +1 -1
- {pywiim-2.3.2 → pywiim-2.3.3}/pyproject.toml +1 -1
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/__init__.py +1 -1
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/__init__.py +6 -2
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/base.py +4 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/media.py +75 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/properties.py +31 -20
- {pywiim-2.3.2 → pywiim-2.3.3/pywiim.egg-info}/PKG-INFO +1 -1
- {pywiim-2.3.2 → pywiim-2.3.3}/LICENSE +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/README.md +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/__init__.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/audio_pro.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/audio_settings.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/base.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/bluetooth.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/constants.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/device.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/diagnostics.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/endpoints.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/eq.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/firmware.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/group.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/lms.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/loop_mode.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/misc.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/parser.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/peq.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/playback.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/preset.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/ssl.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/subwoofer.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/api/timer.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/backoff.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/capabilities.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/__init__.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/diagnostics.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/discovery_cli.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/group_test_cli.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/join_test_cli.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/monitor_cli.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/cli/verify_cli.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/client.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/device_capabilities.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/discovery.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/exceptions.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/group.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/group_helpers.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/__init__.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/__main__.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/config.example.json +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/config.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/context.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/mcp/server.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/metadata.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/model_names.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/models.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/normalize.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/audio.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/bluetooth.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/coverart.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/debounce.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/diagnostics.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/groupops.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/playback.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/source_capabilities.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/statemgr.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/stream.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/stream_enricher.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/player/volume.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/polling.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/profiles.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/py.typed +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/role.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/state.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/__init__.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/client.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/eventer.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/health.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/metadata.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim/upnp/playqueue.py +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/SOURCES.txt +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/dependency_links.txt +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/entry_points.txt +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/requires.txt +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/pywiim.egg-info/top_level.txt +0 -0
- {pywiim-2.3.2 → pywiim-2.3.3}/setup.cfg +0 -0
|
@@ -987,12 +987,16 @@ class Player(PlayerBase):
|
|
|
987
987
|
|
|
988
988
|
@property
|
|
989
989
|
def queue_count(self) -> int | None:
|
|
990
|
-
"""Total
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
if
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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:
|
|
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
|
|
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
|