Repository: postlund/pyatv (python)

Issue:
Detection of AirPlay version does not work
### Describe the bug

When I try to stream a file to my Apple TV 3, it is detected as an AirPlay 2 device which it is not. So streaming fails.

### Error log

```log
<place log here>
```


### How to reproduce the bug?

Use `stream_file` with atvremote and it will fail because of authentication.

### What is expected behavior?

It should succeed.

### Operating System

Any

### Python

3.10

### pyatv

0.13.1

### Device

Apple TV 3

### Additional context

No

Pull request:
airplay: Fix AirPlay major version detection
Fixes #2075 

<a href="https://gitpod.io/#https://github.com/postlund/pyatv/pull/2077"><img src="https://gitpod.io/button/open-in-gitpod.svg"/></a>



Patch:
diff --git a/pyatv/protocols/airplay/utils.py b/pyatv/protocols/airplay/utils.py
index 3a56ab48..24a7bf86 100644
--- a/pyatv/protocols/airplay/utils.py
+++ b/pyatv/protocols/airplay/utils.py
@@ -45,6 +45,11 @@ def _get_flags(properties: Mapping[str, str]) -> int:
     return int(flags, 16)
 
 
+# These flags have been imported from here:
+# https://emanuelecozzi.net/docs/airplay2/features/
+# Should be the same as the ones from here:
+# https://openairplay.github.io/airplay-spec/features.html
+# But seems to be some inconsistencies. Worth to keep in mind.
 class AirPlayFlags(IntFlag):
     """Features supported by AirPlay."""
 
@@ -222,17 +227,19 @@ def log_response(logger, response: HttpResponse, message_prefix="") -> None:
 
 # TODO: I don't know how to properly detect if a receiver support AirPlay 2 or not,
 # so I'm guessing until I know better. The basic idea here is simple: the service
-# should have the "features" flag (either "features" or "ft") and have all 64 bits
-# present. Not sure if that is good enough, but will do for now.
+# should have the "features" flag (either "features" or "ft") and either bit 38 or
+# bit 48 should be present.
 def get_protocol_version(service: BaseService) -> AirPlayMajorVersion:
     """Return major AirPlay version supported by a service."""
     features = service.properties.get("ft")
     if not features:
-        features = service.properties.get("features", "")
+        features = service.properties.get("features", "0x0")
 
-    # Ensure feature flag is valid and all flags present (i.e.
-    # 0xAABBCCDD,0xAABBCCDD, thus looking for ",")
-    if features.count(",") == 1:
+    parsed_features = parse_features(features)
+    if (
+        AirPlayFlags.SupportsUnifiedMediaControl in parsed_features
+        or AirPlayFlags.SupportsCoreUtilsPairingAndEncryption in parsed_features
+    ):
         return AirPlayMajorVersion.AirPlayV2
     return AirPlayMajorVersion.AirPlayV1
 


Test patch:
diff --git a/tests/protocols/airplay/test_utils.py b/tests/protocols/airplay/test_utils.py
index f92d2ca8..6374adda 100644
--- a/tests/protocols/airplay/test_utils.py
+++ b/tests/protocols/airplay/test_utils.py
@@ -117,12 +117,20 @@ def test_is_remote_control_supported(props, credentials, expected_supported):
 @pytest.mark.parametrize(
     "props, expected_version",
     [
+        # Fallback
+        ({}, AirPlayMajorVersion.AirPlayV1),
         # Used by RAOP
-        ({"ft": "0xAABBCCDD"}, AirPlayMajorVersion.AirPlayV1),
-        ({"ft": "0xAABBCCDD,0xAABBCCDD"}, AirPlayMajorVersion.AirPlayV2),
-        # User by AirPlay
-        ({"features": "0xAABBCCDD"}, AirPlayMajorVersion.AirPlayV1),
-        ({"features": "0xAABBCCDD,0xAABBCCDD"}, AirPlayMajorVersion.AirPlayV2),
+        ({"ft": "0x5A7FFFF7,0xE"}, AirPlayMajorVersion.AirPlayV1),  # Apple TV 3
+        (
+            {"ft": "0x4A7FCA00,0xBC354BD0"},
+            AirPlayMajorVersion.AirPlayV2,
+        ),  # HomePod Mini
+        # Used by AirPlay
+        ({"features": "0x5A7FFFF7,0xE"}, AirPlayMajorVersion.AirPlayV1),  # Apple TV 3
+        (
+            {"features": "0x4A7FCA00,0xBC354BD0"},
+            AirPlayMajorVersion.AirPlayV2,
+        ),  # HomePod Mini
     ],
 )
 def test_get_protocol_version(props, expected_version):
diff --git a/tests/support/test_metadata.py b/tests/support/test_metadata.py
new file mode 100644
index 00000000..445d95ad
--- /dev/null
+++ b/tests/support/test_metadata.py
@@ -0,0 +1,29 @@
+"""Unit tests for pyatv.support.metadata."""
+import pytest
+
+from pyatv.interface import MediaMetadata
+from pyatv.support.metadata import merge_into
+
+METADATA_FIELDS = list(MediaMetadata.__dataclass_fields__.keys())
+
+
+def test_returns_base_instance():
+    base = MediaMetadata()
+    new_metadata = MediaMetadata()
+    merged = merge_into(base, new_metadata)
+    assert merged is base
+
+
+def test_to_empty_metadata():
+    # This basically sets the title field to "title", artist to "artist" and so on
+    # for all fields and assert that it is correct
+    metadata = merge_into(MediaMetadata(), MediaMetadata(*METADATA_FIELDS))
+    for field in METADATA_FIELDS:
+        assert getattr(metadata, field) == field
+
+
+def test_no_override_set_value():
+    metadata = merge_into(
+        MediaMetadata(title="title"), MediaMetadata(title="new title")
+    )
+    assert metadata.title == "title"