@bharper/atv-js 0.2.6 → 0.3.4

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 (283) hide show
  1. package/dist/index.d.ts +15 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +89 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/mdns.d.ts.map +1 -1
  6. package/dist/mdns.js +96 -11
  7. package/dist/mdns.js.map +1 -1
  8. package/examples/print-device-json.js +22 -0
  9. package/package.json +2 -3
  10. package/pyatv/.codecov.yml +38 -0
  11. package/pyatv/.github/FUNDING.yml +3 -0
  12. package/pyatv/.github/ISSUE_TEMPLATE/bug_report.yml +80 -0
  13. package/pyatv/.github/ISSUE_TEMPLATE/config.yml +1 -0
  14. package/pyatv/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
  15. package/pyatv/.github/ISSUE_TEMPLATE/implementation-proposal.yml +29 -0
  16. package/pyatv/.github/ISSUE_TEMPLATE/investigation.yml +16 -0
  17. package/pyatv/.github/ISSUE_TEMPLATE/minor-change.yml +10 -0
  18. package/pyatv/.github/ISSUE_TEMPLATE/question-or-idea.yml +11 -0
  19. package/pyatv/.github/dependabot.yml +26 -0
  20. package/pyatv/.github/workflows/codeql-analysis.yml +71 -0
  21. package/pyatv/.github/workflows/release.yml +160 -0
  22. package/pyatv/.github/workflows/tests.yml +104 -0
  23. package/pyatv/.gitpod.yml +23 -0
  24. package/pyatv/CHANGES.md +3708 -0
  25. package/pyatv/CODE_OF_CONDUCT.md +76 -0
  26. package/pyatv/CONTRIBUTING.md +72 -0
  27. package/pyatv/CONTRIBUTORS.md +3 -0
  28. package/pyatv/Dockerfile +15 -0
  29. package/pyatv/LICENSE.md +9 -0
  30. package/pyatv/MANIFEST.in +14 -0
  31. package/pyatv/README.md +111 -0
  32. package/pyatv/base_versions.txt +13 -0
  33. package/pyatv/chickn.yaml +75 -0
  34. package/pyatv/docs/404.html +24 -0
  35. package/pyatv/docs/CNAME +1 -0
  36. package/pyatv/docs/Gemfile +31 -0
  37. package/pyatv/docs/_config.yml +121 -0
  38. package/pyatv/docs/_includes/api +10 -0
  39. package/pyatv/docs/_includes/atvremote_scan +32 -0
  40. package/pyatv/docs/_includes/code +6 -0
  41. package/pyatv/docs/_includes/issue +14 -0
  42. package/pyatv/docs/_includes/pypi +5 -0
  43. package/pyatv/docs/_layouts/template.html +109 -0
  44. package/pyatv/docs/api/pyatv.conf.html +312 -0
  45. package/pyatv/docs/api/pyatv.const.html +974 -0
  46. package/pyatv/docs/api/pyatv.convert.html +106 -0
  47. package/pyatv/docs/api/pyatv.exceptions.html +489 -0
  48. package/pyatv/docs/api/pyatv.helpers.html +102 -0
  49. package/pyatv/docs/api/pyatv.html +120 -0
  50. package/pyatv/docs/api/pyatv.interface.html +2369 -0
  51. package/pyatv/docs/api/pyatv.settings.html +484 -0
  52. package/pyatv/docs/api/pyatv.storage.file_storage.html +102 -0
  53. package/pyatv/docs/api/pyatv.storage.html +186 -0
  54. package/pyatv/docs/api/pyatv.storage.memory_storage.html +83 -0
  55. package/pyatv/docs/assets/css/custom.css +19 -0
  56. package/pyatv/docs/assets/css/hljs.css +1 -0
  57. package/pyatv/docs/assets/css/normalize.css +349 -0
  58. package/pyatv/docs/assets/css/pdoc.css +287 -0
  59. package/pyatv/docs/assets/css/sanitize.css +566 -0
  60. package/pyatv/docs/assets/css/style.scss +9 -0
  61. package/pyatv/docs/assets/img/logo.svg +63 -0
  62. package/pyatv/docs/assets/js/highlight.9.12.0.min.js +3 -0
  63. package/pyatv/docs/assets/js/mermaid.8.9.2.min.js +32 -0
  64. package/pyatv/docs/assets/js/mermaid.min.js.map +1 -0
  65. package/pyatv/docs/development/apps.md +81 -0
  66. package/pyatv/docs/development/audio.md +42 -0
  67. package/pyatv/docs/development/control.md +56 -0
  68. package/pyatv/docs/development/development.md +15 -0
  69. package/pyatv/docs/development/device_info.md +36 -0
  70. package/pyatv/docs/development/examples.md +44 -0
  71. package/pyatv/docs/development/features.md +70 -0
  72. package/pyatv/docs/development/keyboard.md +51 -0
  73. package/pyatv/docs/development/listeners.md +144 -0
  74. package/pyatv/docs/development/logging.md +55 -0
  75. package/pyatv/docs/development/metadata.md +115 -0
  76. package/pyatv/docs/development/power_management.md +53 -0
  77. package/pyatv/docs/development/scan_pair_and_connect.md +331 -0
  78. package/pyatv/docs/development/services.md +9 -0
  79. package/pyatv/docs/development/storage.md +259 -0
  80. package/pyatv/docs/development/stream.md +241 -0
  81. package/pyatv/docs/development/testing.md +9 -0
  82. package/pyatv/docs/documentation/atvlog.md +64 -0
  83. package/pyatv/docs/documentation/atvproxy.md +244 -0
  84. package/pyatv/docs/documentation/atvremote.md +639 -0
  85. package/pyatv/docs/documentation/atvscript.md +275 -0
  86. package/pyatv/docs/documentation/concepts.md +168 -0
  87. package/pyatv/docs/documentation/documentation.md +130 -0
  88. package/pyatv/docs/documentation/getting_started.md +248 -0
  89. package/pyatv/docs/documentation/protocols.md +1959 -0
  90. package/pyatv/docs/documentation/supported_features.md +246 -0
  91. package/pyatv/docs/documentation/tutorial.md +1062 -0
  92. package/pyatv/docs/documentation/workspace.code-workspace +7 -0
  93. package/pyatv/docs/favicon.ico +0 -0
  94. package/pyatv/docs/index.md +109 -0
  95. package/pyatv/docs/internals/design.md +354 -0
  96. package/pyatv/docs/internals/documentation.md +84 -0
  97. package/pyatv/docs/internals/interfaces.md +95 -0
  98. package/pyatv/docs/internals/internals.md +157 -0
  99. package/pyatv/docs/internals/submit_pr.md +56 -0
  100. package/pyatv/docs/internals/testing.md +176 -0
  101. package/pyatv/docs/internals/tools.md +574 -0
  102. package/pyatv/docs/pdoc_templates/config.mako +46 -0
  103. package/pyatv/docs/pdoc_templates/html.mako +454 -0
  104. package/pyatv/docs/support/acknowledgements.md +87 -0
  105. package/pyatv/docs/support/faq.md +214 -0
  106. package/pyatv/docs/support/migration.md +138 -0
  107. package/pyatv/docs/support/scanning_issues.md +110 -0
  108. package/pyatv/docs/support/support.md +18 -0
  109. package/pyatv/docs/support/troubleshooting.md +83 -0
  110. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeMessage.proto +13 -0
  111. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeMessage_pb2.pyi +37 -0
  112. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeResponseMessage.proto +11 -0
  113. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeResponseMessage_pb2.pyi +32 -0
  114. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFormatSettingsMessage.proto +5 -0
  115. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFormatSettingsMessage_pb2.pyi +27 -0
  116. package/pyatv/pyatv/protocols/mrp/protobuf/ClientUpdatesConfigMessage.proto +16 -0
  117. package/pyatv/pyatv/protocols/mrp/protobuf/ClientUpdatesConfigMessage_pb2.pyi +44 -0
  118. package/pyatv/pyatv/protocols/mrp/protobuf/CommandInfo.proto +117 -0
  119. package/pyatv/pyatv/protocols/mrp/protobuf/CommandInfo_pb2.pyi +325 -0
  120. package/pyatv/pyatv/protocols/mrp/protobuf/CommandOptions.proto +36 -0
  121. package/pyatv/pyatv/protocols/mrp/protobuf/CommandOptions_pb2.pyi +115 -0
  122. package/pyatv/pyatv/protocols/mrp/protobuf/Common.proto +79 -0
  123. package/pyatv/pyatv/protocols/mrp/protobuf/Common_pb2.pyi +228 -0
  124. package/pyatv/pyatv/protocols/mrp/protobuf/ConfigureConnectionMessage.proto +11 -0
  125. package/pyatv/pyatv/protocols/mrp/protobuf/ConfigureConnectionMessage_pb2.pyi +32 -0
  126. package/pyatv/pyatv/protocols/mrp/protobuf/ContentItem.proto +27 -0
  127. package/pyatv/pyatv/protocols/mrp/protobuf/ContentItemMetadata.proto +213 -0
  128. package/pyatv/pyatv/protocols/mrp/protobuf/ContentItemMetadata_pb2.pyi +630 -0
  129. package/pyatv/pyatv/protocols/mrp/protobuf/ContentItem_pb2.pyi +94 -0
  130. package/pyatv/pyatv/protocols/mrp/protobuf/CryptoPairingMessage.proto +15 -0
  131. package/pyatv/pyatv/protocols/mrp/protobuf/CryptoPairingMessage_pb2.pyi +46 -0
  132. package/pyatv/pyatv/protocols/mrp/protobuf/DeviceInfoMessage.proto +69 -0
  133. package/pyatv/pyatv/protocols/mrp/protobuf/DeviceInfoMessage_pb2.pyi +226 -0
  134. package/pyatv/pyatv/protocols/mrp/protobuf/GenericMessage.proto +12 -0
  135. package/pyatv/pyatv/protocols/mrp/protobuf/GenericMessage_pb2.pyi +35 -0
  136. package/pyatv/pyatv/protocols/mrp/protobuf/GetKeyboardSessionMessage.proto +11 -0
  137. package/pyatv/pyatv/protocols/mrp/protobuf/GetKeyboardSessionMessage_pb2.pyi +26 -0
  138. package/pyatv/pyatv/protocols/mrp/protobuf/GetRemoteTextInputSessionMessage.proto +10 -0
  139. package/pyatv/pyatv/protocols/mrp/protobuf/GetRemoteTextInputSessionMessage_pb2.pyi +26 -0
  140. package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeMessage.proto +11 -0
  141. package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeMessage_pb2.pyi +32 -0
  142. package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeResultMessage.proto +11 -0
  143. package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeResultMessage_pb2.pyi +32 -0
  144. package/pyatv/pyatv/protocols/mrp/protobuf/KeyboardMessage.proto +88 -0
  145. package/pyatv/pyatv/protocols/mrp/protobuf/KeyboardMessage_pb2.pyi +261 -0
  146. package/pyatv/pyatv/protocols/mrp/protobuf/LanguageOption.proto +9 -0
  147. package/pyatv/pyatv/protocols/mrp/protobuf/LanguageOption_pb2.pyi +42 -0
  148. package/pyatv/pyatv/protocols/mrp/protobuf/ModifyOutputContextRequestMessage.proto +23 -0
  149. package/pyatv/pyatv/protocols/mrp/protobuf/ModifyOutputContextRequestMessage_pb2.pyi +86 -0
  150. package/pyatv/pyatv/protocols/mrp/protobuf/NotificationMessage.proto +12 -0
  151. package/pyatv/pyatv/protocols/mrp/protobuf/NotificationMessage_pb2.pyi +38 -0
  152. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingClient.proto +12 -0
  153. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingClient_pb2.pyi +49 -0
  154. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingInfo.proto +24 -0
  155. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingInfo_pb2.pyi +79 -0
  156. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingPlayer.proto +11 -0
  157. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingPlayer_pb2.pyi +45 -0
  158. package/pyatv/pyatv/protocols/mrp/protobuf/Origin.proto +17 -0
  159. package/pyatv/pyatv/protocols/mrp/protobuf/OriginClientPropertiesMessage.proto +11 -0
  160. package/pyatv/pyatv/protocols/mrp/protobuf/OriginClientPropertiesMessage_pb2.pyi +32 -0
  161. package/pyatv/pyatv/protocols/mrp/protobuf/Origin_pb2.pyi +63 -0
  162. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueue.proto +15 -0
  163. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueCapabilities.proto +7 -0
  164. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueCapabilities_pb2.pyi +33 -0
  165. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueContext.proto +5 -0
  166. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueContext_pb2.pyi +27 -0
  167. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueRequestMessage.proto +29 -0
  168. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueRequestMessage_pb2.pyi +87 -0
  169. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueue_pb2.pyi +53 -0
  170. package/pyatv/pyatv/protocols/mrp/protobuf/PlayerClientPropertiesMessage.proto +13 -0
  171. package/pyatv/pyatv/protocols/mrp/protobuf/PlayerClientPropertiesMessage_pb2.pyi +37 -0
  172. package/pyatv/pyatv/protocols/mrp/protobuf/PlayerPath.proto +11 -0
  173. package/pyatv/pyatv/protocols/mrp/protobuf/PlayerPath_pb2.pyi +39 -0
  174. package/pyatv/pyatv/protocols/mrp/protobuf/ProtocolMessage.proto +171 -0
  175. package/pyatv/pyatv/protocols/mrp/protobuf/ProtocolMessage_pb2.pyi +377 -0
  176. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterForGameControllerEventsMessage.proto +18 -0
  177. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterForGameControllerEventsMessage_pb2.pyi +54 -0
  178. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceMessage.proto +12 -0
  179. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceMessage_pb2.pyi +34 -0
  180. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceResultMessage.proto +12 -0
  181. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceResultMessage_pb2.pyi +35 -0
  182. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceMessage.proto +12 -0
  183. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceMessage_pb2.pyi +34 -0
  184. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceResponseMessage.proto +12 -0
  185. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceResponseMessage_pb2.pyi +35 -0
  186. package/pyatv/pyatv/protocols/mrp/protobuf/RemoteTextInputMessage.proto +13 -0
  187. package/pyatv/pyatv/protocols/mrp/protobuf/RemoteTextInputMessage_pb2.pyi +38 -0
  188. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveClientMessage.proto +12 -0
  189. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveClientMessage_pb2.pyi +34 -0
  190. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveEndpointsMessage.proto +11 -0
  191. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveEndpointsMessage_pb2.pyi +34 -0
  192. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveOutputDevicesMessage.proto +12 -0
  193. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveOutputDevicesMessage_pb2.pyi +38 -0
  194. package/pyatv/pyatv/protocols/mrp/protobuf/RemovePlayerMessage.proto +12 -0
  195. package/pyatv/pyatv/protocols/mrp/protobuf/RemovePlayerMessage_pb2.pyi +34 -0
  196. package/pyatv/pyatv/protocols/mrp/protobuf/SendButtonEventMessage.proto +13 -0
  197. package/pyatv/pyatv/protocols/mrp/protobuf/SendButtonEventMessage_pb2.pyi +38 -0
  198. package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandMessage.proto +16 -0
  199. package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandMessage_pb2.pyi +43 -0
  200. package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandResultMessage.proto +100 -0
  201. package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandResultMessage_pb2.pyi +286 -0
  202. package/pyatv/pyatv/protocols/mrp/protobuf/SendHIDEventMessage.proto +41 -0
  203. package/pyatv/pyatv/protocols/mrp/protobuf/SendHIDEventMessage_pb2.pyi +63 -0
  204. package/pyatv/pyatv/protocols/mrp/protobuf/SendPackedVirtualTouchEventMessage.proto +24 -0
  205. package/pyatv/pyatv/protocols/mrp/protobuf/SendPackedVirtualTouchEventMessage_pb2.pyi +64 -0
  206. package/pyatv/pyatv/protocols/mrp/protobuf/SendVoiceInputMessage.proto +38 -0
  207. package/pyatv/pyatv/protocols/mrp/protobuf/SendVoiceInputMessage_pb2.pyi +134 -0
  208. package/pyatv/pyatv/protocols/mrp/protobuf/SetArtworkMessage.proto +11 -0
  209. package/pyatv/pyatv/protocols/mrp/protobuf/SetArtworkMessage_pb2.pyi +32 -0
  210. package/pyatv/pyatv/protocols/mrp/protobuf/SetConnectionStateMessage.proto +18 -0
  211. package/pyatv/pyatv/protocols/mrp/protobuf/SetConnectionStateMessage_pb2.pyi +54 -0
  212. package/pyatv/pyatv/protocols/mrp/protobuf/SetDefaultSupportedCommandsMessage.proto +28 -0
  213. package/pyatv/pyatv/protocols/mrp/protobuf/SetDefaultSupportedCommandsMessage_pb2.pyi +74 -0
  214. package/pyatv/pyatv/protocols/mrp/protobuf/SetDiscoveryModeMessage.proto +12 -0
  215. package/pyatv/pyatv/protocols/mrp/protobuf/SetDiscoveryModeMessage_pb2.pyi +35 -0
  216. package/pyatv/pyatv/protocols/mrp/protobuf/SetHiliteModeMessage.proto +11 -0
  217. package/pyatv/pyatv/protocols/mrp/protobuf/SetHiliteModeMessage_pb2.pyi +32 -0
  218. package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingClientMessage.proto +12 -0
  219. package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingClientMessage_pb2.pyi +34 -0
  220. package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingPlayerMessage.proto +12 -0
  221. package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingPlayerMessage_pb2.pyi +34 -0
  222. package/pyatv/pyatv/protocols/mrp/protobuf/SetRecordingStateMessage.proto +17 -0
  223. package/pyatv/pyatv/protocols/mrp/protobuf/SetRecordingStateMessage_pb2.pyi +54 -0
  224. package/pyatv/pyatv/protocols/mrp/protobuf/SetStateMessage.proto +27 -0
  225. package/pyatv/pyatv/protocols/mrp/protobuf/SetStateMessage_pb2.pyi +72 -0
  226. package/pyatv/pyatv/protocols/mrp/protobuf/SetVolumeMessage.proto +12 -0
  227. package/pyatv/pyatv/protocols/mrp/protobuf/SetVolumeMessage_pb2.pyi +35 -0
  228. package/pyatv/pyatv/protocols/mrp/protobuf/SupportedCommands.proto +7 -0
  229. package/pyatv/pyatv/protocols/mrp/protobuf/SupportedCommands_pb2.pyi +30 -0
  230. package/pyatv/pyatv/protocols/mrp/protobuf/TextInputMessage.proto +23 -0
  231. package/pyatv/pyatv/protocols/mrp/protobuf/TextInputMessage_pb2.pyi +76 -0
  232. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionKey.proto +6 -0
  233. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionKey_pb2.pyi +30 -0
  234. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionMessage.proto +15 -0
  235. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionMessage_pb2.pyi +42 -0
  236. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPacket.proto +11 -0
  237. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPacket_pb2.pyi +41 -0
  238. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPackets.proto +7 -0
  239. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPackets_pb2.pyi +30 -0
  240. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateClientMessage.proto +12 -0
  241. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateClientMessage_pb2.pyi +34 -0
  242. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemArtworkMessage.proto +14 -0
  243. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemArtworkMessage_pb2.pyi +41 -0
  244. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemMessage.proto +14 -0
  245. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemMessage_pb2.pyi +41 -0
  246. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateEndPointsMessage.proto +25 -0
  247. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateEndPointsMessage_pb2.pyi +74 -0
  248. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateOutputDeviceMessage.proto +88 -0
  249. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateOutputDeviceMessage_pb2.pyi +277 -0
  250. package/pyatv/pyatv/protocols/mrp/protobuf/UpdatePlayerPath.proto +12 -0
  251. package/pyatv/pyatv/protocols/mrp/protobuf/UpdatePlayerPath_pb2.pyi +34 -0
  252. package/pyatv/pyatv/protocols/mrp/protobuf/VirtualTouchDeviceDescriptorMessage.proto +8 -0
  253. package/pyatv/pyatv/protocols/mrp/protobuf/VirtualTouchDeviceDescriptorMessage_pb2.pyi +36 -0
  254. package/pyatv/pyatv/protocols/mrp/protobuf/VoiceInputDeviceDescriptorMessage.proto +8 -0
  255. package/pyatv/pyatv/protocols/mrp/protobuf/VoiceInputDeviceDescriptorMessage_pb2.pyi +35 -0
  256. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlAvailabilityMessage.proto +23 -0
  257. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlAvailabilityMessage_pb2.pyi +71 -0
  258. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlCapabilitiesDidChangeMessage.proto +14 -0
  259. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlCapabilitiesDidChangeMessage_pb2.pyi +40 -0
  260. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeDidChangeMessage.proto +13 -0
  261. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeDidChangeMessage_pb2.pyi +38 -0
  262. package/pyatv/pyatv/protocols/mrp/protobuf/WakeDeviceMessage.proto +11 -0
  263. package/pyatv/pyatv/protocols/mrp/protobuf/WakeDeviceMessage_pb2.pyi +26 -0
  264. package/pyatv/pyatv/py.typed +0 -0
  265. package/pyatv/pylintrc +49 -0
  266. package/pyatv/pyproject.toml +74 -0
  267. package/pyatv/requirements/requirements.txt +14 -0
  268. package/pyatv/requirements/requirements_docs.txt +2 -0
  269. package/pyatv/requirements/requirements_test.txt +20 -0
  270. package/pyatv/scripts/build_docs.sh +17 -0
  271. package/pyatv/scripts/setup_dev_env.sh +83 -0
  272. package/pyatv/setup.cfg +14 -0
  273. package/pyatv/tests/data/README +23 -0
  274. package/pyatv/tests/data/audio_10_frames.wav +0 -0
  275. package/pyatv/tests/data/audio_1_packet_metadata.wav +0 -0
  276. package/pyatv/tests/data/audio_3_packets.wav +0 -0
  277. package/pyatv/tests/data/only_metadata.wav +0 -0
  278. package/pyatv/tests/data/only_title.wav +0 -0
  279. package/pyatv/tests/data/static_3sec.ogg +0 -0
  280. package/pyatv/tests/data/testfile.txt +1 -0
  281. package/pyatv/tests/support/pyatv.code-workspace +14 -0
  282. package/src/index.ts +122 -8
  283. package/src/mdns.ts +64 -11
@@ -0,0 +1,40 @@
1
+ """
2
+ @generated by mypy-protobuf. Do not edit manually!
3
+ isort:skip_file
4
+ """
5
+
6
+ import builtins
7
+ import google.protobuf.descriptor
8
+ import google.protobuf.internal.extension_dict
9
+ import google.protobuf.message
10
+ import pyatv.protocols.mrp.protobuf.ProtocolMessage_pb2
11
+ import pyatv.protocols.mrp.protobuf.VolumeControlAvailabilityMessage_pb2
12
+ import typing
13
+
14
+ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
15
+
16
+ @typing.final
17
+ class VolumeControlCapabilitiesDidChangeMessage(google.protobuf.message.Message):
18
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
19
+
20
+ CAPABILITIES_FIELD_NUMBER: builtins.int
21
+ ENDPOINTUID_FIELD_NUMBER: builtins.int
22
+ OUTPUTDEVICEUID_FIELD_NUMBER: builtins.int
23
+ endpointUID: builtins.str
24
+ outputDeviceUID: builtins.str
25
+ @property
26
+ def capabilities(self) -> pyatv.protocols.mrp.protobuf.VolumeControlAvailabilityMessage_pb2.VolumeControlAvailabilityMessage: ...
27
+ def __init__(
28
+ self,
29
+ *,
30
+ capabilities: pyatv.protocols.mrp.protobuf.VolumeControlAvailabilityMessage_pb2.VolumeControlAvailabilityMessage | None = ...,
31
+ endpointUID: builtins.str | None = ...,
32
+ outputDeviceUID: builtins.str | None = ...,
33
+ ) -> None: ...
34
+ def HasField(self, field_name: typing.Literal["capabilities", b"capabilities", "endpointUID", b"endpointUID", "outputDeviceUID", b"outputDeviceUID"]) -> builtins.bool: ...
35
+ def ClearField(self, field_name: typing.Literal["capabilities", b"capabilities", "endpointUID", b"endpointUID", "outputDeviceUID", b"outputDeviceUID"]) -> None: ...
36
+
37
+ global___VolumeControlCapabilitiesDidChangeMessage = VolumeControlCapabilitiesDidChangeMessage
38
+
39
+ VOLUMECONTROLCAPABILITIESDIDCHANGEMESSAGE_FIELD_NUMBER: builtins.int
40
+ volumeControlCapabilitiesDidChangeMessage: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[pyatv.protocols.mrp.protobuf.ProtocolMessage_pb2.ProtocolMessage, global___VolumeControlCapabilitiesDidChangeMessage]
@@ -0,0 +1,13 @@
1
+ syntax = "proto2";
2
+
3
+ import "pyatv/protocols/mrp/protobuf/ProtocolMessage.proto";
4
+
5
+ extend ProtocolMessage {
6
+ optional VolumeDidChangeMessage volumeDidChangeMessage = 56;
7
+ }
8
+
9
+ message VolumeDidChangeMessage {
10
+ optional float volume = 1;
11
+ optional string endpointUID = 2;
12
+ optional string outputDeviceUID = 3;
13
+ }
@@ -0,0 +1,38 @@
1
+ """
2
+ @generated by mypy-protobuf. Do not edit manually!
3
+ isort:skip_file
4
+ """
5
+
6
+ import builtins
7
+ import google.protobuf.descriptor
8
+ import google.protobuf.internal.extension_dict
9
+ import google.protobuf.message
10
+ import pyatv.protocols.mrp.protobuf.ProtocolMessage_pb2
11
+ import typing
12
+
13
+ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
14
+
15
+ @typing.final
16
+ class VolumeDidChangeMessage(google.protobuf.message.Message):
17
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
18
+
19
+ VOLUME_FIELD_NUMBER: builtins.int
20
+ ENDPOINTUID_FIELD_NUMBER: builtins.int
21
+ OUTPUTDEVICEUID_FIELD_NUMBER: builtins.int
22
+ volume: builtins.float
23
+ endpointUID: builtins.str
24
+ outputDeviceUID: builtins.str
25
+ def __init__(
26
+ self,
27
+ *,
28
+ volume: builtins.float | None = ...,
29
+ endpointUID: builtins.str | None = ...,
30
+ outputDeviceUID: builtins.str | None = ...,
31
+ ) -> None: ...
32
+ def HasField(self, field_name: typing.Literal["endpointUID", b"endpointUID", "outputDeviceUID", b"outputDeviceUID", "volume", b"volume"]) -> builtins.bool: ...
33
+ def ClearField(self, field_name: typing.Literal["endpointUID", b"endpointUID", "outputDeviceUID", b"outputDeviceUID", "volume", b"volume"]) -> None: ...
34
+
35
+ global___VolumeDidChangeMessage = VolumeDidChangeMessage
36
+
37
+ VOLUMEDIDCHANGEMESSAGE_FIELD_NUMBER: builtins.int
38
+ volumeDidChangeMessage: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[pyatv.protocols.mrp.protobuf.ProtocolMessage_pb2.ProtocolMessage, global___VolumeDidChangeMessage]
@@ -0,0 +1,11 @@
1
+ syntax = "proto2";
2
+
3
+ import "pyatv/protocols/mrp/protobuf/ProtocolMessage.proto";
4
+
5
+ extend ProtocolMessage {
6
+ optional WakeDeviceMessage wakeDeviceMessage = 45;
7
+ }
8
+
9
+ message WakeDeviceMessage {
10
+
11
+ }
@@ -0,0 +1,26 @@
1
+ """
2
+ @generated by mypy-protobuf. Do not edit manually!
3
+ isort:skip_file
4
+ """
5
+
6
+ import builtins
7
+ import google.protobuf.descriptor
8
+ import google.protobuf.internal.extension_dict
9
+ import google.protobuf.message
10
+ import pyatv.protocols.mrp.protobuf.ProtocolMessage_pb2
11
+ import typing
12
+
13
+ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
14
+
15
+ @typing.final
16
+ class WakeDeviceMessage(google.protobuf.message.Message):
17
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
18
+
19
+ def __init__(
20
+ self,
21
+ ) -> None: ...
22
+
23
+ global___WakeDeviceMessage = WakeDeviceMessage
24
+
25
+ WAKEDEVICEMESSAGE_FIELD_NUMBER: builtins.int
26
+ wakeDeviceMessage: google.protobuf.internal.extension_dict._ExtensionFieldDescriptor[pyatv.protocols.mrp.protobuf.ProtocolMessage_pb2.ProtocolMessage, global___WakeDeviceMessage]
File without changes
package/pyatv/pylintrc ADDED
@@ -0,0 +1,49 @@
1
+ [MASTER]
2
+ load-plugins=pylint.extensions.no_self_use
3
+ reports=no
4
+
5
+ [BASIC]
6
+ good-names=
7
+ ex,
8
+ rd,
9
+ f,
10
+ i,
11
+ T,
12
+ ws
13
+
14
+ [FORMAT]
15
+ max-line-length=88
16
+
17
+ [TYPECHECK]
18
+ ignore=protobuf
19
+ ignored-modules=pyatv.protocols.mrp.protobuf
20
+ ignored-classes=
21
+ ProtocolMessage,
22
+ SetConnectionStateMessage,
23
+ SetStateMessage,
24
+ ContentItemMetadata,
25
+ CommandInfo,
26
+ PlaybackState,
27
+ ShuffleMode,
28
+ RepeatMode,
29
+ HandlerReturnStatus,
30
+ SendError,
31
+ DeviceClass
32
+
33
+ disable=
34
+ broad-except,
35
+ cyclic-import, # TODO: Bug in pylint?
36
+ locally-disabled,
37
+ duplicate-code,
38
+ fixme,
39
+ unused-argument,
40
+ no-self-use,
41
+ too-few-public-methods,
42
+ too-many-public-methods,
43
+ too-many-arguments,
44
+ too-many-instance-attributes,
45
+ too-many-lines,
46
+ too-many-positional-arguments
47
+
48
+ [EXCEPTIONS]
49
+ overgeneral-exceptions=builtins.Exception
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools>=77.0"]
4
+
5
+ [project]
6
+ name="pyatv"
7
+ license = "MIT"
8
+ license-files = ["LICENSE.md"]
9
+ description = "A client library for Apple TV and AirPlay devices"
10
+ readme = "README.md"
11
+ authors = [{ name = "Pierre Ståhl", email = "pierre.staahl@gmail.com" }]
12
+ requires-python = ">=3.9.0"
13
+ keywords = ["apple", "tv", "airplay", "raop", "companion", "dmap", "dacp"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Software Development :: Libraries",
25
+ "Topic :: Home Automation",
26
+ "Typing :: Typed",
27
+ ]
28
+ dynamic = ["version", "dependencies"]
29
+
30
+ [project.urls]
31
+ "Homepage" = "https://pyatv.dev"
32
+ "Repository" = "https://github.com/postlund/pyatv"
33
+ "Bug Reports" = "https://github.com/postlund/pyatv/issues"
34
+
35
+ [project.scripts]
36
+ atvremote = "pyatv.scripts.atvremote:main"
37
+ atvproxy = "pyatv.scripts.atvproxy:main"
38
+ atvscript = "pyatv.scripts.atvscript:main"
39
+ atvlog = "pyatv.scripts.atvlog:main"
40
+
41
+ [tool.setuptools.dynamic]
42
+ version = { attr = "pyatv.const.__version__" }
43
+
44
+ [tool.setuptools.packages.find]
45
+ include = ["pyatv*"]
46
+
47
+ [tool.black]
48
+ extend-exclude = '(protobuf/(__init__|.*_pb2).py)|__pycache__'
49
+ include = '(pyatv|tests|examples|scripts).*\.py'
50
+
51
+ [tool.isort]
52
+ skip_glob = "pyatv/protocols/mrp/protobuf/*.py"
53
+ profile = "black"
54
+ force_sort_within_sections = true
55
+ known_first_party = [
56
+ "pyatv",
57
+ "tests",
58
+ "scripts",
59
+ ]
60
+ forced_separate = [
61
+ "tests",
62
+ "scripts",
63
+ ]
64
+
65
+ [[tool.mypy.overrides]]
66
+ module = [
67
+ "miniaudio",
68
+ "audio_metadata",
69
+ "srptools",
70
+ ]
71
+ ignore_missing_imports = true
72
+
73
+ [tool.pytest.ini_options]
74
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,14 @@
1
+ aiohttp==3.13.3
2
+ async-timeout==5.0.1;python_version<'3.11'
3
+ cryptography==46.0.3
4
+ chacha20poly1305-reuseable==0.13.2
5
+ ifaddr==0.2.0
6
+ ifaddr==0.2.0
7
+ miniaudio==1.61
8
+ protobuf==6.33.2
9
+ pydantic==2.12.5
10
+ requests==2.32.5
11
+ srptools==1.0.1
12
+ tabulate==0.9.0
13
+ tinytag==2.1.2
14
+ zeroconf==0.148.0
@@ -0,0 +1,2 @@
1
+ codespell==2.4.1
2
+ pdoc3==0.11.6
@@ -0,0 +1,20 @@
1
+ black==25.11.0
2
+ deepdiff==8.6.1
3
+ flake8==7.3.0
4
+ isort==6.0.1
5
+ mutagen==1.47.0
6
+ pyfakefs==5.10.2
7
+ pylint==3.3.9
8
+ pytest==8.4.2
9
+ pytest-asyncio==0.26.0
10
+ pytest-cov==7.0.0
11
+ pytest-timeout==2.4.0
12
+ pytest-aiohttp==1.0.5
13
+ pytest_httpserver==1.1.3
14
+ pytest-xdist==3.8.0
15
+ pydocstyle==6.3.0
16
+ mypy==1.19.0
17
+ mypy-protobuf==3.7.0
18
+ types-protobuf==6.32.1.20251210
19
+ types-requests==2.32.4.20260107
20
+ types-tabulate==0.9.0.20241207
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+
3
+ run()
4
+ {
5
+ docker run -p 4000:4000 --rm \
6
+ -v "$PWD:/srv/jekyll" \
7
+ -v "$PWD/docs/vendor/bundle:/usr/local/bundle" \
8
+ -w /srv/jekyll/docs \
9
+ -it jekyll/jekyll:3.8 "$@"
10
+ }
11
+
12
+ if [[ $GITPOD_INSTANCE_ID ]]; then
13
+ cd docs && bundle install && bundle update github-pages && bundle exec jekyll serve --incremental --watch
14
+ else
15
+ run bundle update github-pages
16
+ run jekyll serve --incremental --watch
17
+ fi
@@ -0,0 +1,83 @@
1
+ #!/bin/bash
2
+
3
+ # Set in GitPod which breaks installation in venv
4
+ unset PIP_USER
5
+
6
+ VERSIONS="3.10 3.9 3.8 3.7 3.6"
7
+
8
+ found_version=
9
+ for p in $VERSIONS
10
+ do
11
+ which python${p} 2>&1 > /dev/null
12
+ if [ $? -eq 0 ]; then
13
+ found_version=python${p}
14
+ break
15
+ fi
16
+ done
17
+
18
+ set -e
19
+
20
+ if [ -z $found_version ]; then
21
+ >&2 echo "no python installation found"
22
+ exit 1
23
+ fi
24
+
25
+ echo "-> Using python: $found_version"
26
+
27
+ echo "-> Creating python virtual environment..."
28
+ $found_version -m venv .
29
+
30
+ echo "-> Activating virtual environment..."
31
+ source bin/activate
32
+ sed -i '' 's/false/true/' pyvenv.cfg
33
+
34
+ echo "-> Upgrading pip..."
35
+ pip install --upgrade pip
36
+
37
+ echo "-> Installing library as develop..."
38
+ python setup.py develop
39
+
40
+ echo "-> Installing protobuf-setuptools..."
41
+ pip install protobuf-setuptools
42
+
43
+ echo "-> Installing dependencies..."
44
+ pip install --upgrade pyyaml -r requirements/requirements_test.txt -r requirements/requirements_docs.txt
45
+
46
+ echo "-> Running tests as verification..."
47
+ python setup.py test
48
+
49
+ cat <<EOF
50
+ ==================================================
51
+
52
+ When starting a new shell, run:
53
+
54
+ source bin/activate
55
+
56
+ To run tests, run any of:
57
+
58
+ python setup.py test
59
+ pytest tests/test_conf.py # Single test
60
+
61
+ Test everything with chickn:
62
+
63
+ ./scripts/chickn.py
64
+
65
+ To re-generate protobuf messages:
66
+
67
+ ./scripts/protobuf.py --download generate
68
+
69
+ The CLI application can be used, e.g. run:
70
+
71
+ atvremote --debug commands
72
+ atvremote --debug playing
73
+
74
+ To preview documentation in docs, run:
75
+
76
+ ./scripts/build_docs.sh
77
+
78
+ and navigate to http://127.0.0.1:4000
79
+
80
+ ==================================================
81
+
82
+ Environment is configured and ready to use!
83
+ EOF
@@ -0,0 +1,14 @@
1
+ [aliases]
2
+ test=pytest
3
+
4
+ [tool:pytest]
5
+ testpaths = tests
6
+ norecursedirs = .git
7
+
8
+ [flake8]
9
+ exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
10
+ max-line-length = 88
11
+ ignore = E203, W503
12
+
13
+ [pydocstyle]
14
+ match_dir = ^((?!\.|www_static).)*$
@@ -0,0 +1,23 @@
1
+ Audio files in this directory have been generated by the
2
+ commands below in case they ever need to be re-generated.
3
+
4
+ Only metadata, no audio:
5
+
6
+ python ../../scripts/audiogen.py --title pyatv --artist postlund --album raop -n 0 -o only_metadata.wav
7
+
8
+ Audio, 10 frames (=20 samples):
9
+
10
+ python ../../scripts/audiogen.py -n 10 -o audio_10_frames.wav
11
+
12
+ Audio, 352 * 3 frames = 3 packets (=1056 samples):
13
+
14
+ python ../../scripts/audiogen.py -n 1056 -o audio_3_packets.wav
15
+
16
+ Audio, 8 seconds static (no sound), ogg for compression:
17
+
18
+ python ../../scripts/audiogen.py -n 132300 -s -o static_3sec.wav
19
+ ffmpeg -i static_3sec.wav static_3sec.ogg
20
+
21
+ Audio, 1 packets (=352 samples), with metadata:
22
+
23
+ python ../../scripts/audiogen.py --title pyatv --artist postlund --album raop -n 352 -o audio_1_packet_metadata.wav
Binary file
Binary file
@@ -0,0 +1 @@
1
+ a file for testing
@@ -0,0 +1,14 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "../.."
5
+ },
6
+ {
7
+ "path": "../../../airplay2-receiver"
8
+ },
9
+ {
10
+ "path": "../../../pyminiaudio"
11
+ }
12
+ ],
13
+ "settings": {}
14
+ }
package/src/index.ts CHANGED
@@ -118,19 +118,68 @@ export interface AppleTVConnection {
118
118
  protocol: CompanionProtocol;
119
119
  device: AppleTVDevice;
120
120
  credentials: Credentials;
121
+ /** Credentials + endpoint used for this connection attempt/result. */
122
+ usedCredentials: ConnectionCredentialsSnapshot;
123
+ /** True when usedCredentials exactly match the values passed to connect(...). */
124
+ usedCredentialsMatchProvided: boolean;
121
125
  /** @internal */
122
126
  _keyboardFocusState: KeyboardFocusState;
123
127
  }
124
128
 
129
+ export interface ConnectionCredentialsSnapshot {
130
+ airplay: string;
131
+ companion: string;
132
+ device: {
133
+ name: string;
134
+ address: string;
135
+ port: number;
136
+ airplayPort: number;
137
+ identifier: string;
138
+ };
139
+ }
140
+
125
141
  function errorMessage(error: unknown): string {
126
142
  if (error instanceof Error) return error.message;
127
143
  return String(error);
128
144
  }
129
145
 
130
- function matchesDevice(target: AppleTVDevice, candidate: AppleTVDevice): boolean {
131
- if (target.identifier && candidate.identifier === target.identifier) return true;
132
- if (candidate.address === target.address) return true;
133
- return candidate.name === target.name;
146
+ const ID_PROPERTY_KEYS = new Set(['rpmrtid', 'deviceid', 'macaddress']);
147
+
148
+ function normalizeIdentifier(value: string): string {
149
+ return value.trim().toLowerCase();
150
+ }
151
+
152
+ function getIdentifiers(device: AppleTVDevice): Set<string> {
153
+ const identifiers = new Set<string>();
154
+
155
+ if (device.identifier) {
156
+ identifiers.add(normalizeIdentifier(device.identifier));
157
+ }
158
+
159
+ for (const [key, value] of Object.entries(device.properties || {})) {
160
+ if (!value || !ID_PROPERTY_KEYS.has(key.toLowerCase())) continue;
161
+ identifiers.add(normalizeIdentifier(value));
162
+ }
163
+
164
+ return identifiers;
165
+ }
166
+
167
+ function sharesIdentifier(target: AppleTVDevice, candidate: AppleTVDevice): boolean {
168
+ const targetIds = getIdentifiers(target);
169
+ if (targetIds.size === 0) return false;
170
+
171
+ for (const identifier of getIdentifiers(candidate)) {
172
+ if (targetIds.has(identifier)) return true;
173
+ }
174
+
175
+ return false;
176
+ }
177
+
178
+ function deviceMatchScore(target: AppleTVDevice, candidate: AppleTVDevice): number {
179
+ if (sharesIdentifier(target, candidate)) return 100;
180
+ if (candidate.address === target.address) return 10;
181
+ if (candidate.name === target.name) return 1;
182
+ return 0;
134
183
  }
135
184
 
136
185
  function mergeDiscoveredDevice(target: AppleTVDevice, discovered: AppleTVDevice): AppleTVDevice {
@@ -141,6 +190,40 @@ function mergeDiscoveredDevice(target: AppleTVDevice, discovered: AppleTVDevice)
141
190
  };
142
191
  }
143
192
 
193
+ function hasCompanionEndpointChanged(current: AppleTVDevice, discovered: AppleTVDevice): boolean {
194
+ return current.address !== discovered.address || current.port !== discovered.port;
195
+ }
196
+
197
+ function toCredentialSnapshot(
198
+ device: AppleTVDevice,
199
+ credentials: Credentials,
200
+ ): ConnectionCredentialsSnapshot {
201
+ return {
202
+ airplay: credentials.airplay,
203
+ companion: credentials.companion,
204
+ device: {
205
+ name: device.name,
206
+ address: device.address,
207
+ port: device.port,
208
+ airplayPort: device.airplayPort,
209
+ identifier: device.identifier,
210
+ },
211
+ };
212
+ }
213
+
214
+ function snapshotsEqual(
215
+ left: ConnectionCredentialsSnapshot,
216
+ right: ConnectionCredentialsSnapshot,
217
+ ): boolean {
218
+ return left.airplay === right.airplay
219
+ && left.companion === right.companion
220
+ && left.device.name === right.device.name
221
+ && left.device.address === right.device.address
222
+ && left.device.port === right.device.port
223
+ && left.device.airplayPort === right.device.airplayPort
224
+ && left.device.identifier === right.device.identifier;
225
+ }
226
+
144
227
  async function connectCompanion(
145
228
  device: AppleTVDevice,
146
229
  companionCredentials: string,
@@ -154,7 +237,29 @@ async function connectCompanion(
154
237
 
155
238
  async function discoverLatestDevice(device: AppleTVDevice): Promise<AppleTVDevice | null> {
156
239
  const discovered = await scanDevices(3000, false);
157
- return discovered.find((candidate) => matchesDevice(device, candidate)) || null;
240
+ let bestMatch: AppleTVDevice | null = null;
241
+ let bestScore = 0;
242
+
243
+ for (const candidate of discovered) {
244
+ const score = deviceMatchScore(device, candidate);
245
+ if (score === 0) continue;
246
+
247
+ if (!bestMatch || score > bestScore) {
248
+ bestMatch = candidate;
249
+ bestScore = score;
250
+ continue;
251
+ }
252
+
253
+ if (score === bestScore) {
254
+ const candidatePortMatches = candidate.port === device.port;
255
+ const currentPortMatches = bestMatch.port === device.port;
256
+ if (candidatePortMatches && !currentPortMatches) {
257
+ bestMatch = candidate;
258
+ }
259
+ }
260
+ }
261
+
262
+ return bestMatch;
158
263
  }
159
264
 
160
265
  /**
@@ -166,6 +271,7 @@ export async function connect(
166
271
  credentials: Credentials,
167
272
  ): Promise<AppleTVConnection> {
168
273
  let activeDevice = device;
274
+ const providedCredentials = toCredentialSnapshot(device, credentials);
169
275
  let protocol: CompanionProtocol;
170
276
 
171
277
  try {
@@ -178,17 +284,22 @@ export async function connect(
178
284
  discovered = null;
179
285
  }
180
286
 
181
- if (!discovered || discovered.port === activeDevice.port) {
287
+ if (!discovered) {
288
+ throw initialError;
289
+ }
290
+
291
+ const mergedDevice = mergeDiscoveredDevice(activeDevice, discovered);
292
+ if (!hasCompanionEndpointChanged(activeDevice, mergedDevice)) {
182
293
  throw initialError;
183
294
  }
184
295
 
185
- activeDevice = mergeDiscoveredDevice(activeDevice, discovered);
296
+ activeDevice = mergedDevice;
186
297
 
187
298
  try {
188
299
  protocol = await connectCompanion(activeDevice, credentials.companion);
189
300
  } catch (retryError) {
190
301
  throw new Error(
191
- `Companion connection failed on saved port ${device.port} and discovered port ${activeDevice.port}: ${errorMessage(retryError)}`,
302
+ `Companion connection failed on saved endpoint ${device.address}:${device.port} and discovered endpoint ${activeDevice.address}:${activeDevice.port}: ${errorMessage(retryError)}`,
192
303
  { cause: initialError instanceof Error ? initialError : undefined },
193
304
  );
194
305
  }
@@ -236,8 +347,11 @@ export async function connect(
236
347
  protocol,
237
348
  device: activeDevice,
238
349
  credentials,
350
+ usedCredentials: toCredentialSnapshot(activeDevice, credentials),
351
+ usedCredentialsMatchProvided: false,
239
352
  _keyboardFocusState: KeyboardFocusState.Unknown,
240
353
  };
354
+ conn.usedCredentialsMatchProvided = snapshotsEqual(conn.usedCredentials, providedCredentials);
241
355
 
242
356
  // Watch keyboard focus state
243
357
  _watchKeyboardFocus(protocol, (state) => {