Repository: postlund/pyatv
Pull request #2533 (2024-11-02 08:27:38)

build(deps): Bump types-requests from 2.31.0.2 to 2.32.0.20241016 in /requirements
Bumps [types-requests](https://github.com/python/typeshed) from 2.31.0.2 to 2.32.0.20241016.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/python/typeshed/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=types-requests&package-manager=pip&previous-version=2.31.0.2&new-version=2.32.0.20241016)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bd221707..be5d8873 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,7 +9,7 @@ on:
 
 env:
   REGISTRY: ghcr.io
-  PYTHON_VERSION: 3.9
+  PYTHON_VERSION: 3.12
 
 jobs:
   build_containers:
diff --git a/base_versions.txt b/base_versions.txt
index 585b2ae4..fe97106d 100644
--- a/base_versions.txt
+++ b/base_versions.txt
@@ -7,7 +7,7 @@ mediafile==0.8.1
 miniaudio==1.45
 protobuf==5.28.1
 pydantic==1.10.10
-requests==2.23.0
+requests==2.30.0
 srptools==0.2.0
 tabulate==0.9.0
 tinytag==1.10.0
diff --git a/pyatv/protocols/companion/__init__.py b/pyatv/protocols/companion/__init__.py
index 7d48dda7..1dd28140 100644
--- a/pyatv/protocols/companion/__init__.py
+++ b/pyatv/protocols/companion/__init__.py
@@ -378,12 +378,29 @@ class CompanionRemoteControl(RemoteControl):
         await self._press_button(HidCommand.Screensaver)
 
     async def _press_button(
-        self, command: HidCommand, action: InputAction = InputAction.SingleTap
+        self,
+        command: HidCommand,
+        action: InputAction = InputAction.SingleTap,
+        delay: float = 1,
     ) -> None:
-        if action != InputAction.SingleTap:
-            raise NotImplementedError(f"{action} not supported for {command} (yet)")
-        await self.api.hid_command(True, command)
-        await self.api.hid_command(False, command)
+        if action == InputAction.SingleTap:
+            await self.api.hid_command(True, command)
+            await self.api.hid_command(False, command)
+
+        elif action == InputAction.Hold:
+            await self.api.hid_command(True, command)
+            await asyncio.sleep(delay)
+            await self.api.hid_command(False, command)
+
+        elif action == InputAction.DoubleTap:
+            # First press
+            await self.api.hid_command(True, command)
+            await self.api.hid_command(False, command)
+            # Second press
+            await self.api.hid_command(True, command)
+            await self.api.hid_command(False, command)
+        else:
+            raise exceptions.NotSupportedError(f"unsupported input action: {action}")
 
 
 class CompanionAudio(Audio):
diff --git a/pyatv/scripts/atvremote.py b/pyatv/scripts/atvremote.py
index 15f1a1b5..e6b55769 100644
--- a/pyatv/scripts/atvremote.py
+++ b/pyatv/scripts/atvremote.py
@@ -406,11 +406,11 @@ class DeviceCommands:
                 self.args, command, self.atv, self.storage, self.loop
             )
 
-    async def artwork_save(self, width=None, height=None):
+    async def artwork_save(self, width=None, height=None, file_name="artwork"):
         """Download artwork and save it to artwork.png."""
         artwork = await self.atv.metadata.artwork(width=width, height=height)
         if artwork is not None:
-            with open("artwork.png", "wb") as file:
+            with open(f"{file_name}.png", "wb") as file:
                 file.write(artwork.bytes)
         else:
             print("No artwork is currently available.")
diff --git a/pyatv/support/opack.py b/pyatv/support/opack.py
index e177cfd2..6b4b13eb 100644
--- a/pyatv/support/opack.py
+++ b/pyatv/support/opack.py
@@ -86,17 +86,17 @@ def _pack(data, object_list):
             packed_bytes = (
                 bytes([0x91]) + len(data).to_bytes(1, byteorder="little") + data
             )
-        elif len(data) <= 0xFFFF:
+        elif len(data) <= 0xFFFF:  # 2^16-1
             packed_bytes = (
                 bytes([0x92]) + len(data).to_bytes(2, byteorder="little") + data
             )
-        elif len(data) <= 0xFFFFFF:
+        elif len(data) <= 0xFFFF_FFFF:  # 2^32-1
             packed_bytes = (
-                bytes([0x93]) + len(data).to_bytes(3, byteorder="little") + data
+                bytes([0x93]) + len(data).to_bytes(4, byteorder="little") + data
             )
-        elif len(data) <= 0xFFFFFFFF:
+        elif len(data) <= 0xFFFF_FFFF_FFFF_FFFF:  # 2^64-1
             packed_bytes = (
-                bytes([0x94]) + len(data).to_bytes(4, byteorder="little") + data
+                bytes([0x94]) + len(data).to_bytes(8, byteorder="little") + data
             )
     elif isinstance(data, list):
         packed_bytes = bytes([0xD0 + min(len(data), 0xF)]) + b"".join(
@@ -184,8 +184,8 @@ def _unpack(data, object_list):
     elif 0x70 <= data[0] <= 0x90:
         length = data[0] - 0x70
         value, remaining = data[1 : 1 + length], data[1 + length :]
-    elif 0x90 < data[0] <= 0x94:
-        noof_bytes = data[0] & 0xF
+    elif 0x91 <= data[0] <= 0x94:
+        noof_bytes = 1 << ((data[0] & 0xF) - 1)
         length = int.from_bytes(data[1 : 1 + noof_bytes], byteorder="little")
         value, remaining = (
             data[1 + noof_bytes : 1 + noof_bytes + length],
diff --git a/requirements/requirements_docs.txt b/requirements/requirements_docs.txt
index 468f3765..7a09e096 100644
--- a/requirements/requirements_docs.txt
+++ b/requirements/requirements_docs.txt
@@ -1,2 +1,2 @@
 codespell==2.3.0
-pdoc3==0.11.0
+pdoc3==0.11.1
diff --git a/setup.py b/setup.py
index c4b861b9..c98d3c37 100644
--- a/setup.py
+++ b/setup.py
@@ -46,7 +46,7 @@ setup(
     keywords=["apple", "tv", "airplay", "raop", "companion", "dmap", "dacp"],
     setup_requires=["pytest-runner"],
     tests_require=["pytest==6.2.5", "pytest-xdist==2.4.0"],
-    python_requires=">=3.8.1",
+    python_requires=">=3.9.0",
     entry_points={
         "console_scripts": [
             "atvremote = pyatv.scripts.atvremote:main",
@@ -60,7 +60,6 @@ setup(
         "Intended Audience :: Developers",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5cb5a43d..d2efa3ba 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -27,7 +27,6 @@ jobs:
           - macos-latest
           - windows-latest
         python-version:
-          - "3.8"
           - "3.9"
           - "3.10"
           - "3.11"
@@ -38,7 +37,7 @@ jobs:
           - false
         include:
           - platform: ubuntu-latest
-            python-version: 3.8
+            python-version: 3.9
             chickn-args: "--no-venv -v requirements_file=base_versions.txt"
             regression: true
     steps:
diff --git a/requirements/requirements_test.txt b/requirements/requirements_test.txt
index 74a29f69..567fec6b 100644
--- a/requirements/requirements_test.txt
+++ b/requirements/requirements_test.txt
@@ -17,5 +17,5 @@ mypy==1.13.0
 mypy-protobuf==3.6.0
 typed-ast==1.5.5
 types-protobuf==5.27.0.20240626
-types-requests==2.31.0.2
+types-requests==2.32.0.20241016
 types-tabulate==0.9.0.20240106
diff --git a/tests/protocols/companion/test_companion_functional.py b/tests/protocols/companion/test_companion_functional.py
index a15d34f0..7b1a2763 100644
--- a/tests/protocols/companion/test_companion_functional.py
+++ b/tests/protocols/companion/test_companion_functional.py
@@ -218,6 +218,30 @@ async def test_remote_control_buttons(companion_client, companion_state, button)
     assert companion_state.latest_button == button
 
 
+@pytest.mark.parametrize(
+    "button,action",
+    [
+        ("up", InputAction.DoubleTap),
+        ("up", InputAction.Hold),
+        ("down", InputAction.DoubleTap),
+        ("down", InputAction.Hold),
+        ("left", InputAction.DoubleTap),
+        ("left", InputAction.Hold),
+        ("right", InputAction.DoubleTap),
+        ("right", InputAction.Hold),
+        ("select", InputAction.DoubleTap),
+        ("select", InputAction.Hold),
+        ("menu", InputAction.DoubleTap),
+        ("menu", InputAction.Hold),
+        ("home", InputAction.DoubleTap),
+        ("home", InputAction.Hold),
+    ],
+)
+async def test_button_actions(companion_client, companion_state, button, action):
+    await getattr(companion_client.remote_control, button)(action=action)
+    assert companion_state.latest_button == button
+
+
 async def test_remote_control_skip_forward_backward(companion_client, companion_state):
     duration = companion_state.duration
     await companion_client.remote_control.skip_forward()
diff --git a/tests/support/test_opack.py b/tests/support/test_opack.py
index e337d855..6ffe6b6b 100644
--- a/tests/support/test_opack.py
+++ b/tests/support/test_opack.py
@@ -84,6 +84,7 @@ def test_pack_short_raw_bytes():
 def test_pack_longer_raw_bytes():
     assert pack(33 * b"\x61") == b"\x91\x21" + (33 * b"\x61")
     assert pack(256 * b"\x61") == b"\x92\x00\x01" + (256 * b"\x61")
+    assert pack(65536 * b"\x61") == b"\x93\x00\x00\x01\x00" + (65536 * b"\x61")
 
 
 def test_pack_array():
@@ -275,6 +276,7 @@ def test_unpack_short_raw_bytes():
 def test_unpack_longer_raw_bytes():
     assert unpack(b"\x91\x21" + (33 * b"\x61")) == (33 * b"\x61", b"")
     assert unpack(b"\x92\x00\x01" + (256 * b"\x61")) == (256 * b"\x61", b"")
+    assert unpack(b"\x93\x00\x00\x01\x00" + (65536 * b"\x61")) == (65536 * b"\x61", b"")
 
 
 def test_unpack_array():