Source code for pyatv

"""Library for controlling an Apple TV."""

import asyncio
import logging
import concurrent
from ipaddress import ip_address
from threading import Lock

from zeroconf import ServiceBrowser, Zeroconf
from aiohttp import ClientSession

from pyatv import (conf, exceptions)
from pyatv.airplay import player
from pyatv.airplay.api import AirPlayAPI
from pyatv.const import (PROTOCOL_MRP, PROTOCOL_DMAP)
from pyatv.net import HttpSession

from pyatv.dmap import DmapAppleTV
from pyatv.mrp import MrpAppleTV

_LOGGER = logging.getLogger(__name__)


HOMESHARING_SERVICE = '_appletv-v2._tcp.local.'
DEVICE_SERVICE = '_touch-able._tcp.local.'
MEDIAREMOTE_SERVICE = '_mediaremotetv._tcp.local.'
AIRPLAY_SERVICE = '_airplay._tcp.local.'


class _ServiceListener:

    # pylint: disable=too-many-arguments
    def __init__(self, loop, abort_on_found, device_ip, protocol, semaphore):
        """Initialize a new _ServiceListener."""
        self.loop = loop
        self.abort_on_found = abort_on_found
        self.device_ip = None if not device_ip else ip_address(device_ip)
        self.protocol = protocol
        self.semaphore = semaphore
        self.found_devices = {}
        self.lock = Lock()

    def add_service(self, zeroconf, service_type, name):
        """Handle callback from zeroconf when a service has been discovered."""
        self.lock.acquire()
        try:
            self._internal_add(zeroconf, service_type, name)
        finally:
            self.lock.release()

    def _internal_add(self, zeroconf, service_type, name):
        if self.abort_on_found and len(self.found_devices) == 1:
            return

        info = zeroconf.get_service_info(service_type, name)
        address = ip_address(info.addresses[0])  # TODO: Consider all?

        # We might be looking for a particular device
        if self.device_ip and self.device_ip != address:
            _LOGGER.debug('Ignoring %s (not matching %s)',
                          address, self.device_ip)
            return

        if info.type == HOMESHARING_SERVICE:
            self.add_hs_service(info, address)
        elif info.type == DEVICE_SERVICE:
            self.add_non_hs_service(info, address)
        elif info.type == MEDIAREMOTE_SERVICE:
            self.add_mrp_service(info, address)
        elif info.type == AIRPLAY_SERVICE:
            self.add_airplay_service(info, address)
        else:
            _LOGGER.warning('Discovered unknown device: %s', info)

    def add_hs_service(self, info, address):
        """Add a new device to discovered list."""
        if self.protocol and self.protocol != PROTOCOL_DMAP:
            return

        name = info.properties[b'Name'].decode('utf-8')
        hsgid = info.properties[b'hG'].decode('utf-8')
        self._handle_service(
            address, name, conf.DmapService(hsgid, port=info.port))

    def add_non_hs_service(self, info, address):
        """Add a new device without Home Sharing to discovered list."""
        if self.protocol and self.protocol != PROTOCOL_DMAP:
            return

        name = info.properties[b'CtlN'].decode('utf-8')
        self._handle_service(
            address, name, conf.DmapService(None, port=info.port))

    def add_mrp_service(self, info, address):
        """Add a new MediaRemoteProtocol device to discovered list."""
        if self.protocol and self.protocol != PROTOCOL_MRP:
            return

        name = info.properties[b'Name'].decode('utf-8')
        self._handle_service(address, name, conf.MrpService(info.port))

    def add_airplay_service(self, info, address):
        """Add a new AirPlay device to discovered list."""
        name = info.name.replace('._airplay._tcp.local.', '')
        self._handle_service(address, name, conf.AirPlayService(info.port))

    def _handle_service(self, address, name, service):
        if address not in self.found_devices:
            self.found_devices[address] = conf.AppleTV(address, name)

        _LOGGER.debug('Auto-discovered %s at %s:%d (protocol: %s)',
                      name, address, service.port, service.protocol)

        atv = self.found_devices[address]
        atv.add_service(service)

        # Check if we should continue to run or not
        if self._should_abort(address):
            _LOGGER.debug('Aborting since a device was found')

            # Only return the found device as a convenience to the user
            self.found_devices = {address: atv}

            # zeroconf is run in a different thread so this must be a
            # thread-safe call
            self.loop.call_soon_threadsafe(self.semaphore.release)

    def _should_abort(self, address):
        if not self.abort_on_found:
            return False

        return self.found_devices[address].usable_service()


# pylint: disable=too-many-arguments
[docs]async def scan_for_apple_tvs(loop, timeout=5, abort_on_found=False, device_ip=None, only_usable=True, protocol=None): """Scan for Apple TVs using zeroconf (bonjour) and returns them.""" semaphore = asyncio.Semaphore(value=0, loop=loop) listener = _ServiceListener( loop, abort_on_found, device_ip, protocol, semaphore) zeroconf = Zeroconf() try: ServiceBrowser(zeroconf, HOMESHARING_SERVICE, listener) ServiceBrowser(zeroconf, DEVICE_SERVICE, listener) ServiceBrowser(zeroconf, MEDIAREMOTE_SERVICE, listener) ServiceBrowser(zeroconf, AIRPLAY_SERVICE, listener) _LOGGER.debug('Discovering devices for %d seconds', timeout) await asyncio.wait_for(semaphore.acquire(), timeout, loop=loop) except concurrent.futures.TimeoutError: pass # Will happen when timeout occurs (totally normal) finally: zeroconf.close() def _should_include(atv): if not only_usable: return True return atv.is_usable() return list(filter(_should_include, listener.found_devices.values()))
[docs]async def connect_to_apple_tv(details, loop, protocol=None, session=None): """Connect and logins to an Apple TV.""" service = _get_service_used_to_connect(details, protocol) # If no session is given, create a default one if session is None: session = ClientSession(loop=loop) # AirPlay service is the same for both DMAP and MRP airplay = _setup_airplay(loop, session, details) # Create correct implementation depending on protocol if service.protocol == PROTOCOL_DMAP: return DmapAppleTV(loop, session, details, airplay) return MrpAppleTV(loop, session, details, airplay)
def _get_service_used_to_connect(details, protocol): if not protocol: service = details.usable_service() else: service = details.get_service(protocol) if not service: raise exceptions.NoUsableServiceError( 'no usable service to connect to') return service def _setup_airplay(loop, session, details): airplay_service = details.airplay_service() airplay_player = player.AirPlayPlayer( loop, session, details.address, airplay_service.port) airplay_http = HttpSession( session, 'http://{0}:{1}/'.format( details.address, airplay_service.port)) return AirPlayAPI(airplay_http, airplay_player)