Repository: postlund/pyatv
Pull request #2053 (2023-06-15 09:17:51)

mrp: Fix position if playbackRate is 0.0

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/postlund/pyatv/pull/2053?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) Report
Patch coverage: **`100.00`**% and no project coverage change.
> Comparison is base [(`10e207f`)](https://app.codecov.io/gh/postlund/pyatv/commit/10e207f02b5d863fb6a2791e75f02c0b12d8051d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) 88.78% compared to head [(`332084a`)](https://app.codecov.io/gh/postlund/pyatv/pull/2053?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) 88.78%.

> :exclamation: Current head 332084a differs from pull request most recent head d6c19d2. Consider uploading reports for the commit d6c19d2 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #2053   +/-   ##
=======================================
  Coverage   88.78%   88.78%           
=======================================
  Files         163      163           
  Lines       10780    10781    +1     
=======================================
+ Hits         9571     9572    +1     
  Misses       1209     1209           
```


| [Impacted Files](https://app.codecov.io/gh/postlund/pyatv/pull/2053?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) | Coverage Δ | |
|---|---|---|
| [pyatv/protocols/mrp/\_\_init\_\_.py](https://app.codecov.io/gh/postlund/pyatv/pull/2053?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL21ycC9fX2luaXRfXy5weQ==) | `95.96% <100.00%> (+<0.01%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/postlund/pyatv/pull/2053?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).


Patch:
diff --git a/docs/api/pyatv/interface.html b/docs/api/pyatv/interface.html
index 1121ca24..1d48b6b9 100644
--- a/docs/api/pyatv/interface.html
+++ b/docs/api/pyatv/interface.html
@@ -1188,7 +1188,7 @@ interface: <code>pyatv.interfaces.KeyboardListener</code></p>
 </code></dt>
 <dd>
 <section class="desc"><p>Container for media (e.g. audio or video) metadata.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L70-L77" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L71-L77" class="git-link">Browse git</a></div>
 <h3>Class variables</h3>
 <dl>
 <dt id="pyatv.interface.MediaMetadata.album"><code class="name">var <span class="ident">album</span></code></dt>
diff --git a/pyatv/protocols/mrp/__init__.py b/pyatv/protocols/mrp/__init__.py
index 19746dc2..17e8c284 100644
--- a/pyatv/protocols/mrp/__init__.py
+++ b/pyatv/protocols/mrp/__init__.py
@@ -215,7 +215,10 @@ def build_playing_instance(  # pylint: disable=too-many-locals
             datetime.datetime.now() - _cocoa_to_timestamp(elapsed_timestamp)
         ).total_seconds()
 
-        if device_state() == DeviceState.Playing:
+        playback_rate = state.metadata_field("playbackRate") or 0.0
+        if device_state() == DeviceState.Playing and not math.isclose(
+            playback_rate, 0.0
+        ):
             return int(elapsed_time + diff)
         return int(elapsed_time)
 
@@ -468,7 +471,13 @@ class MrpRemoteControl(RemoteControl):
 class MrpMetadata(Metadata):
     """Implementation of API for retrieving metadata."""
 
-    def __init__(self, protocol, psm, identifier, client_session: ClientSession):
+    def __init__(
+        self,
+        protocol: MrpProtocol,
+        psm: PlayerStateManager,
+        identifier: Optional[str],
+        client_session: ClientSession,
+    ):
         """Initialize a new MrpPlaying."""
         self.protocol = protocol
         self.psm = psm


Test patch:
diff --git a/tests/protocols/mrp/test_mrp_functional.py b/tests/protocols/mrp/test_mrp_functional.py
index 953cd803..6f633456 100644
--- a/tests/protocols/mrp/test_mrp_functional.py
+++ b/tests/protocols/mrp/test_mrp_functional.py
@@ -6,7 +6,6 @@ import math
 from typing import Optional
 
 import pyatv
-from pyatv import exceptions
 from pyatv.conf import AppleTV, ManualService
 from pyatv.const import (
     DeviceModel,
diff --git a/tests/protocols/mrp/test_mrp_interface.py b/tests/protocols/mrp/test_mrp_interface.py
index 21ca7641..b95736f9 100644
--- a/tests/protocols/mrp/test_mrp_interface.py
+++ b/tests/protocols/mrp/test_mrp_interface.py
@@ -1,13 +1,17 @@
 """Unit tests for interface implementations in pyatv.protocols.mrp."""
+import datetime
 import math
-from unittest.mock import Mock
+from typing import Any, Dict
+from unittest.mock import Mock, PropertyMock
 
 import pytest
 
 from pyatv import exceptions
 from pyatv.core import UpdatedState
-from pyatv.interface import OutputDevice
-from pyatv.protocols.mrp import MrpAudio, messages, protobuf
+from pyatv.interface import ClientSessionManager, OutputDevice
+from pyatv.protocols.mrp import MrpAudio, MrpMetadata, messages, player_state, protobuf
+
+from tests.utils import faketime
 
 DEVICE_NAME = "Apple TV"
 DEVICE_UID = "F2204E63-BCAB-4941-80A0-06C46CB71391"
@@ -295,3 +299,73 @@ async def test_audio_set_output_devices(protocol_mock, audio):
         "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
         "FF:GG:HH:II:JJ:KK",
     ]
+
+
+# MrpMetadata
+
+ELAPSED_TIME = 2
+ELAPSED_TIME_TIMESTAMP = 1
+NOW_TIME = 10
+
+
+@pytest.fixture(name="playing_metadata")
+def playing_metadata_fixture() -> Dict[str, Any]:
+    metadata = {}
+
+    # Ok, so this is tricky to get right... We assume time starts at zero. As Apple
+    # uses mach time (starts at 2001 instead of 1970), the elapsed time must be
+    # converted to mach time. Any time can be used here, but one second has been
+    # picked for simplicity. The elapsed time below is used to calculate position
+    # in case media is playing, since "elapsedTime" only reflects time since playback
+    # changed to "playing". So "elapsedTimeTimestamp" must be used as an additional
+    # offset in case media is playing. By hardcoding to one second, we know that the
+    # position essentially will be 1 + (time_now_mach - elapsedTimeTimestamp) IFF
+    # something is playing. Otherwise it's just "elapsedTime".
+    delta = datetime.datetime(1970, 1, 1) - datetime.datetime(2001, 1, 1)
+    metadata["elapsedTimeTimestamp"] = (
+        datetime.timedelta(seconds=ELAPSED_TIME_TIMESTAMP) + delta
+    ).total_seconds()
+
+    metadata["elapsedTime"] = ELAPSED_TIME
+
+    # Start of in paused state
+    metadata["playbackRate"] = 0.0
+    player_state.playback_state = protobuf.PlaybackState.Paused
+
+    yield metadata
+
+
+@pytest.fixture(name="player_state")
+def player_state_fixture(playing_metadata) -> Mock:
+    playing_mock = Mock(name="playing_mock")
+    playing_mock.metadata_field.side_effect = lambda field: playing_metadata.get(field)
+    yield playing_mock
+
+
+@pytest.fixture(name="metadata")
+def metadata_fixture(
+    protocol_mock, player_state, session_manager: ClientSessionManager
+) -> MrpMetadata:
+    psm_mock = Mock(name="metadata_mock")
+    type(psm_mock).playing = PropertyMock(return_value=player_state)
+
+    yield MrpMetadata(protocol_mock, psm_mock, "test", session_manager)
+
+
+async def test_metadata_position_calculation(metadata, playing_metadata, player_state):
+    # See playing_metadata fixture for timing details
+    with faketime("pyatv", NOW_TIME):
+        # Paused state => ELAPSED_TIME
+        assert (await metadata.playing()).position == ELAPSED_TIME
+
+        # Playing state => ELAPSED_TIME + time diff since "elapsedTimeTimestamp"
+        player_state.playback_state = protobuf.PlaybackState.Playing
+        playing_metadata["playbackRate"] = 1.0
+        assert (await metadata.playing()).position == ELAPSED_TIME + (
+            NOW_TIME - ELAPSED_TIME_TIMESTAMP
+        )
+
+        # Playing state but 0.0 playback rate => same as paused
+        player_state.playback_state = protobuf.PlaybackState.Playing
+        playing_metadata["playbackRate"] = 0.0
+        assert (await metadata.playing()).position == ELAPSED_TIME