From e185ecf277d6647acb556b4bd7aa9eeb1bfe1102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 13 Sep 2023 14:48:00 +0200 Subject: [PATCH] wip: Support streaming from video services Relates to #2186 --- docs/api/pyatv.exceptions.html | 18 +- docs/api/pyatv.interface.html | 378 ++++++++++++++++-------------- pyatv/exceptions.py | 4 + pyatv/interface.py | 19 ++ pyatv/protocols/airplay/player.py | 3 +- pyatv/support/yt_dlp.py | 63 +++++ tests/core/test_facade.py | 2 +- 7 files changed, 304 insertions(+), 183 deletions(-) create mode 100644 pyatv/support/yt_dlp.py diff --git a/docs/api/pyatv.exceptions.html b/docs/api/pyatv.exceptions.html index 3e95cad95..d868a61e7 100644 --- a/docs/api/pyatv.exceptions.html +++ b/docs/api/pyatv.exceptions.html @@ -54,6 +54,9 @@

InvalidDmapDataError

  • +

    InvalidFormatError

    +
  • +
  • InvalidResponseError

  • @@ -108,7 +111,7 @@

    Module pyatv.exceptions

    Local exceptions used by library.

    - +
    @@ -275,6 +278,19 @@

    Ancestors

  • builtins.BaseException
  • +
    +class InvalidFormatError +(*args, **kwargs) +
    +
    +

    Raised when an unsupported (file) format is encountered.

    + +

    Ancestors

    + +
    class InvalidResponseError (*args, **kwargs) diff --git a/docs/api/pyatv.interface.html b/docs/api/pyatv.interface.html index 814adc59d..953b5858a 100644 --- a/docs/api/pyatv.interface.html +++ b/docs/api/pyatv.interface.html @@ -306,6 +306,7 @@

    Sto

    Stream

    @@ -336,7 +337,7 @@

    Module pyatv.interface

    Public interface exposed by library.

    This module contains all the interfaces that represents a generic Apple TV device and all its features.

    - +
    @@ -352,7 +353,7 @@

    Functions

    Retrieve all commands and help texts from an API object.

    - +
    @@ -366,18 +367,18 @@

    Classes

    Information about an app.

    Initialize a new App instance.

    - +

    Instance variables

    var identifier -> str

    Return a unique bundle id for the app.

    - +
    var name -> Optional[str]

    User friendly name of app.

    - +
    @@ -389,7 +390,7 @@

    Instance variables

    Base class representing an Apple TV.

    Listener interface: pyatv.interfaces.DeviceListener

    Initialize a new StateProducer instance.

    - +

    Ancestors

    • abc.ABC
    • @@ -405,67 +406,67 @@

      Instance variables

      var apps -> Apps

      Return apps interface.

      - +
      var audio -> Audio

      Return audio interface.

      - +
      var device_info -> DeviceInfo

      Return API for device information.

      - +
      var features -> Features

      Return features interface.

      - +
      var keyboard -> Keyboard

      Return keyboard interface.

      - +
      var metadata -> Metadata

      Return API for retrieving metadata from the Apple TV.

      - +
      var power -> Power

      Return API for power management.

      - +
      var push_updater -> PushUpdater

      Return API for handling push update from the Apple TV.

      - +
      var remote_control -> RemoteControl

      Return API for controlling the Apple TV.

      - +
      var service -> BaseService

      Return service used to connect to the Apple TV.

      - +
      var settings -> Settings

      Return device settings used by pyatv.

      - +
      var stream -> Stream

      Return API for streaming media.

      - +
      var user_accounts -> UserAccounts

      Return user accounts interface.

      - +

      Methods

      @@ -477,7 +478,7 @@

      Methods

      Close connection and release allocated resources.

      - +
      @@ -487,7 +488,7 @@

      Methods

      Initiate connection to device.

      No need to call it yourself, it's done automatically.

      - +
      @@ -496,7 +497,7 @@

      Methods

      Base class for app handling.

      - +

      Subclasses

      @@ -528,7 +529,7 @@

      Methods

      Supported by: Protocol.Companion

      Launch an app based on bundle ID or URL.

      - + @@ -538,7 +539,7 @@

      Methods

      Artwork information.

      - +

      Ancestors

      • builtins.tuple
      • @@ -572,7 +573,7 @@

        Instance variables

        Volume level is managed in percent where 0 is muted and 100 is max volume.

        Listener interface: pyatv.interfaces.AudioListener

        Initialize a new StateProducer instance.

        - +

        Ancestors

      var volume -> float
      @@ -606,7 +607,7 @@

      Instance variables

      Return current volume level.

      Range is in percent, i.e. [0.0-100.0].

      - +

      Methods

      @@ -622,7 +623,7 @@

      Methods

      Supported by: Protocol.MRP

      Add output devices.

      - +
      @@ -635,7 +636,7 @@

      Methods

      Supported by: Protocol.MRP

      Remove output devices.

      - +
      @@ -648,7 +649,7 @@

      Methods

      Supported by: Protocol.MRP

      Set output devices.

      - +
      @@ -662,7 +663,7 @@

      Methods

      Change current volume level.

      Range is in percent, i.e. [0.0-100.0].

      - +
      @@ -679,7 +680,7 @@

      Methods

      range. It is not necessarily linear.

      Call will block until volume change has been acknowledged by the device (when possible and supported).

      - +
      @@ -696,7 +697,7 @@

      Methods

      range. It is not necessarily linear.

      Call will block until volume change has been acknowledged by the device (when possible and supported).

      - + @@ -705,7 +706,7 @@

      Methods

      Listener interface for audio updates.

      - +

      Ancestors

      @@ -746,7 +747,7 @@

      Methods

      several services depending on the protocols it supports, e.g. DMAP or AirPlay.

      Initialize a new BaseConfig instance.

      - +

      Ancestors

      • abc.ABC
      • @@ -760,47 +761,47 @@

        Instance variables

        var address -> ipaddress.IPv4Address

        IP address of device.

        - +
        var all_identifiers -> List[str]

        Return all unique identifiers for this device.

        - +
        var deep_sleep -> bool

        If device is in deep sleep.

        - +
        var device_info -> DeviceInfo

        Return general device information.

        - +
        var identifier -> Optional[str]

        Return the main identifier associated with this device.

        - +
        var name -> str

        Name of device.

        - +
        var properties -> Mapping[str, Mapping[str, str]]

        Return Zeroconf properties.

        - +
        var ready -> bool

        Return if configuration is ready, (at least one service with identifier).

        - +
        var services -> List[BaseService]

        Return all supported services.

        - +

        Methods

        @@ -813,7 +814,7 @@

        Methods

        Add a new service.

        If the service already exists, it will be merged.

        - +
        @@ -822,7 +823,7 @@

        Methods

        Apply settings to configuration.

        - +
        @@ -833,7 +834,7 @@

        Methods

        Look up a service based on protocol.

        If a service with the specified protocol is not available, None is returned.

        - +
        @@ -842,7 +843,7 @@

        Methods

        Return suggested service used to establish connection.

        - +
        @@ -851,7 +852,7 @@

        Methods

        Set credentials for a protocol if it exists.

        - +
        @@ -862,7 +863,7 @@

        Methods

        Base class for protocol services.

        Initialize a new BaseService.

        - +

        Ancestors

        @@ -932,7 +933,7 @@

        Methods

        Merge with other service of same type.

        Merge will only include credentials, password and properties.

        - +
        @@ -941,7 +942,7 @@

        Methods

        Return settings and their values.

        - +
        @@ -952,7 +953,7 @@

        Methods

        General information about device.

        Initialize a new DeviceInfo instance.

        - +

        Class variables

        var OUTPUT_DEVICE_ID
        @@ -965,46 +966,46 @@

        Instance variables

        var build_number -> Optional[str]

        Operating system build number, e.g. 17K795.

        - +
        var mac -> Optional[str]

        Device MAC address.

        - +
        var model -> DeviceModel

        Hardware model name, e.g. 3, 4 or 4K.

        - +
        var model_str -> str

        Return model name as string.

        This property will return the model name as a string and fallback to raw_model if it is not available.

        - +
        var operating_system -> OperatingSystem

        Operating system running on device.

        - +
        var output_device_id -> Optional[str]

        Output device identifier.

        - +
        var raw_model -> Optional[str]

        Return raw model description.

        If DeviceInfo.model returns DeviceModel.Unknown then this property contains the raw model string (if any is available).

        - +
        var version -> Optional[str]

        Operating system version.

        - +
        @@ -1013,7 +1014,7 @@

        Instance variables

        Listener interface for generic device updates.

        - +

        Ancestors

        • abc.ABC
        • @@ -1032,7 +1033,7 @@

          Methods

          Device connection was (intentionally) closed.

          - +
          @@ -1041,7 +1042,7 @@

          Methods

          Device was unexpectedly disconnected.

          - +
        @@ -1051,7 +1052,7 @@

        Methods

        Feature state and options.

        - +

        Ancestors

        • builtins.tuple
        • @@ -1073,7 +1074,7 @@

          Instance variables

          Base class for supported feature functionality.

          - +

          Subclasses

          • pyatv.core.facade.FacadeFeatures
          • @@ -1092,7 +1093,7 @@

            Methods

            Return state of all features.

            - +
            @@ -1101,7 +1102,7 @@

            Methods

            Return current state of a feature.

            - +
            @@ -1113,7 +1114,7 @@

            Methods

            This method will return True if all given features are in the state specified by "states". If "states" is a list of states, it is enough for the feature to be in one of the listed states.

            - +
        @@ -1126,7 +1127,7 @@

        Methods

        Listener interface: pyatv.interfaces.KeyboardListener

        Initialize a new StateProducer instance.

        - +

        Ancestors

        • abc.ABC
        • @@ -1147,7 +1148,7 @@

          Instance variables

          Supported by: Protocol.Companion

          Return keyboard focus state.

          - +

          Methods

          @@ -1163,7 +1164,7 @@

          Methods

          Supported by: Protocol.Companion

          Input text into virtual keyboard.

          - +
          @@ -1176,7 +1177,7 @@

          Methods

          Supported by: Protocol.Companion

          Clear virtual keyboard text.

          - +
          @@ -1189,7 +1190,7 @@

          Methods

          Supported by: Protocol.Companion

          Get current virtual keyboard text.

          - +
          @@ -1202,7 +1203,7 @@

          Methods

          Supported by: Protocol.Companion

          Replace text in virtual keyboard.

          - + @@ -1211,7 +1212,7 @@

          Methods

          Listener interface for keyboard updates.

          - +

          Ancestors

          @@ -1239,7 +1240,7 @@

          Methods

          Container for media (e.g. audio or video) metadata.

          - +

          Class variables

          var album -> Optional[str]
          @@ -1269,7 +1270,7 @@

          Class variables

          Base class for retrieving metadata from an Apple TV.

          - +

          Subclasses

          • pyatv.core.facade.FacadeMetadata
          • @@ -1289,17 +1290,17 @@

            Instance variables

            Do note that this property returns which app is currently playing something and not which app is currently active. If nothing is playing, the corresponding feature will be unavailable.

            - +
          var artwork_id -> str

          Return a unique identifier for current artwork.

          - +
          var device_id -> Optional[str]

          Return a unique identifier for current device.

          - +

          Methods

          @@ -1320,7 +1321,7 @@

          Methods

          return artwork of a different size. Set both parameters to None to request default size. Set one of them and let the other one be None to keep original aspect ratio.

          - +
          @@ -1329,7 +1330,7 @@

          Methods

          Return what is currently playing.

          - +
          @@ -1340,18 +1341,18 @@

          Methods

          Information about an output device.

          Initialize a new OutputDevice instance.

          - +

          Instance variables

          var identifier -> str

          Return a unique id for the output device.

          - +
          var name -> Optional[str]

          User friendly name of output device.

          - +
          @@ -1362,7 +1363,7 @@

          Instance variables

          Base class for API used to pair with an Apple TV.

          Initialize a new instance of PairingHandler.

          - +

          Ancestors

          @@ -1440,7 +1441,7 @@

          Methods

          Base class for retrieving what is currently playing.

          Initialize a new Playing instance.

          - +

          Ancestors

          • abc.ABC
          • @@ -1454,7 +1455,7 @@

            Instance variables

            Supported by:

            Album of the currently playing song.

            - +
          var artist -> Optional[str]
          @@ -1463,7 +1464,7 @@

          Instance variables

          Supported by:

          Artist of the currently playing song.

          - +
          var content_identifier -> Optional[str]
          @@ -1472,12 +1473,12 @@

          Instance variables

          Supported by:

          Content identifier (app specific).

          - +
          var device_state -> DeviceState

          Device state, e.g. playing or paused.

          - +
          var episode_number -> Optional[int]
          @@ -1486,7 +1487,7 @@

          Instance variables

          Supported by:

          Episode number of TV series.

          - +
          var genre -> Optional[str]
          @@ -1495,19 +1496,19 @@

          Instance variables

          Supported by:

          Genre of the currently playing song.

          - +
          var hash -> str

          Create a unique hash for what is currently playing.

          The hash is based on title, artist, album and total time. It should always be the same for the same content, but it is not guaranteed.

          - +
          var media_type -> MediaType

          Type of media is currently playing, e.g. video, music.

          - +
          var position -> Optional[int]
          @@ -1516,7 +1517,7 @@

          Instance variables

          Supported by:

          Position in the playing media (seconds).

          - +
          var repeat -> Optional[RepeatState]
          @@ -1525,7 +1526,7 @@

          Instance variables

          Supported by:

          Repeat mode.

          - +
          var season_number -> Optional[int]
          @@ -1534,7 +1535,7 @@

          Instance variables

          Supported by:

          Season number of TV series.

          - +
          var series_name -> Optional[str]
          @@ -1543,7 +1544,7 @@

          Instance variables

          Supported by:

          Title of TV series.

          - +
          var shuffle -> Optional[ShuffleState]
          @@ -1552,7 +1553,7 @@

          Instance variables

          Supported by:

          If shuffle is enabled or not.

          - +
          var title -> Optional[str]
          @@ -1561,7 +1562,7 @@

          Instance variables

          Supported by:

          Title of the current media, e.g. movie or song name.

          - +
          var total_time -> Optional[int]
          @@ -1570,7 +1571,7 @@

          Instance variables

          Supported by:

          Total play time in seconds.

          - +
          @@ -1582,7 +1583,7 @@

          Instance variables

          Base class for retrieving power state from an Apple TV.

          Listener interface: pyatv.interfaces.PowerListener

          Initialize a new StateProducer instance.

          - +

          Ancestors

          • abc.ABC
          • @@ -1604,7 +1605,7 @@

            Instance variables

            Supported by: Protocol.Companion, Protocol.MRP

            Return device power state.

            - +

            Methods

            @@ -1620,7 +1621,7 @@

            Methods

            Supported by: Protocol.Companion, Protocol.MRP

            Turn device off.

            - +
            @@ -1633,7 +1634,7 @@

            Methods

            Supported by: Protocol.Companion, Protocol.MRP

            Turn device on.

            - + @@ -1642,7 +1643,7 @@

            Methods

            Listener interface for power updates.

            - +

            Ancestors

            @@ -1671,7 +1672,7 @@

            Methods

            Listener interface for push updates.

            - +

            Ancestors

            • abc.ABC
            • @@ -1691,7 +1692,7 @@

              Methods

              Inform about an error when updating play status.

              - +
              @@ -1700,7 +1701,7 @@

              Methods

              Inform about changes to what is currently playing.

              - +
            @@ -1714,7 +1715,7 @@

            Methods

            actually changes.

            Listener interface: PushListener.

            Initialize a new StateProducer instance.

            - +

            Ancestors

            • abc.ABC
            • @@ -1731,7 +1732,7 @@

              Instance variables

              var active -> bool

              Return if push updater has been started.

              - +

              Methods

              @@ -1748,7 +1749,7 @@

              Methods

              Begin to listen to updates.

              If an error occurs, start must be called again.

              - +
              @@ -1757,7 +1758,7 @@

              Methods

              No longer forward updates to listener.

              - +
              @@ -1766,7 +1767,7 @@

              Methods

              Base class for API used to control an Apple TV.

              - +

              Subclasses

              @@ -1802,7 +1803,7 @@

              Methods

              Supported by: Protocol.Companion

              Select next channel.

              - +
              @@ -1815,7 +1816,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key down.

              - +
              @@ -1828,7 +1829,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.MRP

              Press key home.

              - +
              @@ -1841,7 +1842,7 @@

              Methods

              Supported by: Protocol.MRP

              Hold key home.

              - +
              @@ -1854,7 +1855,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key left.

              - +
              @@ -1867,7 +1868,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key menu.

              - +
              @@ -1880,7 +1881,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key next.

              - +
              @@ -1893,7 +1894,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP, Protocol.RAOP

              Press key pause.

              - +
              @@ -1906,7 +1907,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key play.

              - +
              @@ -1919,7 +1920,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Toggle between play and pause.

              - +
              @@ -1932,7 +1933,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key previous.

              - +
              @@ -1945,7 +1946,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key right.

              - +
              @@ -1958,7 +1959,7 @@

              Methods

              Supported by: Protocol.Companion

              Activate screen saver..

              - +
              @@ -1971,7 +1972,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key select.

              - +
              @@ -1984,7 +1985,7 @@

              Methods

              Supported by: Protocol.DMAP, Protocol.MRP

              Seek in the current playing media.

              - +
              @@ -1997,7 +1998,7 @@

              Methods

              Supported by: Protocol.DMAP, Protocol.MRP

              Change repeat state.

              - +
              @@ -2010,7 +2011,7 @@

              Methods

              Supported by: Protocol.DMAP, Protocol.MRP

              Change shuffle mode to on or off.

              - +
              @@ -2024,7 +2025,7 @@

              Methods

              Skip backwards a time interval.

              Skip interval is typically 15-30s, but is decided by the app.

              - +
              @@ -2038,7 +2039,7 @@

              Methods

              Skip forward a time interval.

              Skip interval is typically 15-30s, but is decided by the app.

              - +
              @@ -2051,7 +2052,7 @@

              Methods

              Supported by: Protocol.AirPlay, Protocol.DMAP, Protocol.MRP, Protocol.RAOP

              Press key stop.

              - +
              @@ -2065,7 +2066,7 @@

              Methods

              Suspend the device.

              DEPRECATED: Use Power.turn_off() instead.

              - +
              @@ -2078,7 +2079,7 @@

              Methods

              Supported by: Protocol.DMAP, Protocol.MRP

              Go to main menu (long press menu).

              - +
              @@ -2091,7 +2092,7 @@

              Methods

              Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

              Press key up.

              - +
              @@ -2105,7 +2106,7 @@

              Methods

              Press key volume down.

              DEPRECATED: Use Audio.volume_down() instead.

              - +
              @@ -2119,7 +2120,7 @@

              Methods

              Press key volume up.

              DEPRECATED: Use Audio.volume_up() instead.

              - +
              @@ -2133,7 +2134,7 @@

              Methods

              Wake up the device.

              DEPRECATED: Use Power.turn_on() instead.

              - + @@ -2142,7 +2143,7 @@

              Methods

              Base class for storage modules.

              - +

              Ancestors

              • abc.ABC
              • @@ -2156,7 +2157,7 @@

                Instance variables

                var settings -> Sequence[Settings]

                Return settings for all devices.

                - +

                Methods

                @@ -2173,7 +2174,7 @@

                Methods

                If no settings exists for the current configuration, new settings are created automatically and returned. If the configuration does not contain any valid identitiers, DeviceIdMissingError will be raised.

                - +
              @@ -2182,7 +2183,7 @@

              Methods

              Load settings from active storage.

              - +
              @@ -2192,7 +2193,7 @@

              Methods

              Remove settings from storage.

              Returns True if settings were removed, otherwise False.

              - +
              @@ -2201,7 +2202,7 @@

              Methods

              Save settings to active storage.

              - +
              @@ -2212,7 +2213,7 @@

              Methods

              Update settings based on config.

              This method extracts settings from a configuration and writes them back to the storage.

              - + @@ -2221,7 +2222,7 @@

              Methods

              Base class for stream functionality.

              - +

              Subclasses

              • pyatv.core.facade.FacadeStream
              • @@ -2237,7 +2238,24 @@

                Methods

                Close connection and release allocated resources.

                - + +
                +
                + +async def play_service(self, video_url: str) -> None + +
                +
                +

                Play video from a video service, e.g. YouTube.

                +

                This method will try to extract the underlying video URL from various video +hosting services, e.g. YouTube, and play the video using play_url.

                +

                Note 1: For this method to work, yt-dlp must be installed. A NotSupportedError +is thrown otherwise.

                +

                Note 2: By default, pyatv will try to play the video with highest bitrate. It's +not possible to possible to change this at the moment, but will be in the +future.

                +

                INCUBATING METHOD - MIGHT CHANGE IN THE FUTURE!

                +
                @@ -2250,7 +2268,7 @@

                Methods

                Supported by: Protocol.AirPlay

                Play media from an URL on the device.

                - +
              @@ -2265,7 +2283,7 @@

              Methods

              Stream local or remote file to device.

              Supports either local file paths or a HTTP(s) address.

              INCUBATING METHOD - MIGHT CHANGE IN THE FUTURE!

              - + @@ -2276,18 +2294,18 @@

              Methods

              Information about a user account.

              Initialize a new UserAccount instance.

              - +

              Instance variables

              var identifier -> str

              Return a unique id for the account.

              - +
              var name -> Optional[str]

              User name.

              - +
              @@ -2296,7 +2314,7 @@

              Instance variables

              Base class for account handling.

              - +

              Subclasses

              @@ -2328,7 +2346,7 @@

              Methods

              Supported by: Protocol.Companion

              Switch user account by account ID.

              - + diff --git a/pyatv/exceptions.py b/pyatv/exceptions.py index b6f0d2ca2..050048906 100644 --- a/pyatv/exceptions.py +++ b/pyatv/exceptions.py @@ -129,3 +129,7 @@ class OperationTimeoutError(Exception): class SettingsError(Exception): """Raised when an error related to settings happens.""" + + +class InvalidFormatError(Exception): + """Raised when an unsupported (file) format is encountered.""" diff --git a/pyatv/interface.py b/pyatv/interface.py index 910990aa9..928722e4b 100644 --- a/pyatv/interface.py +++ b/pyatv/interface.py @@ -43,6 +43,7 @@ from pyatv.support.device_info import lookup_version from pyatv.support.http import ClientSessionManager from pyatv.support.state_producer import StateProducer +from pyatv.support.yt_dlp import extract_video_url __pdoc__ = { "feature": False, @@ -874,6 +875,24 @@ async def stream_file( """ raise exceptions.NotSupportedError() + async def play_service(self, video_url: str) -> None: + """Play video from a video service, e.g. YouTube. + + This method will try to extract the underlying video URL from various video + hosting services, e.g. YouTube, and play the video using play_url. + + Note 1: For this method to work, yt-dlp must be installed. A NotSupportedError + is thrown otherwise. + + Note 2: By default, pyatv will try to play the video with highest bitrate. It's + not possible to possible to change this at the moment, but will be in the + future. + + INCUBATING METHOD - MIGHT CHANGE IN THE FUTURE! + """ + url = await extract_video_url(video_url) + await self.play_url(url) + class DeviceListener(ABC): """Listener interface for generic device updates.""" diff --git a/pyatv/protocols/airplay/player.py b/pyatv/protocols/airplay/player.py index e2c073cb0..b4da6a906 100644 --- a/pyatv/protocols/airplay/player.py +++ b/pyatv/protocols/airplay/player.py @@ -12,7 +12,8 @@ _LOGGER = logging.getLogger(__name__) PLAY_RETRIES = 3 -WAIT_RETRIES = 5 +WAIT_RETRIES = 10 + HEADERS = { "User-Agent": "AirPlay/550.10", "Content-Type": "application/x-apple-binary-plist", diff --git a/pyatv/support/yt_dlp.py b/pyatv/support/yt_dlp.py new file mode 100644 index 000000000..8c1782200 --- /dev/null +++ b/pyatv/support/yt_dlp.py @@ -0,0 +1,63 @@ +"""Helper methods for working with yt-dlp. + +Currently ytp-dl is used to extract video URLs from various video sites, e.g. YouTube +so they can be streamed via AirPlay. +""" +import asyncio + +from pyatv import exceptions + + +def _extract_video_url(video_link: str) -> str: + # TODO: For now, dynamic support for this feature. User must manually install + # yt-dlp, it will not be pulled in by pyatv. + try: + import yt_dlp # pylint: disable=import-outside-toplevel + except ModuleNotFoundError as ex: + raise exceptions.NotSupportedError("package yt-dlp not installed") from ex + + with yt_dlp.YoutubeDL({"quiet": True, "no_warnings": True}) as ydl: + info = ydl.sanitize_info(ydl.extract_info(video_link, download=False)) + + if "formats" not in info: + raise exceptions.NotSupportedError( + "formats are missing, maybe authentication is needed (not supported)?" + ) + + best = None + best_bitrate = 0 + + # Try to find supported video stream with highest bitrate. No way to customize + # this in any way for now. + for video_format in [ + x for x in info["formats"] if x.get("protocol") == "m3u8_native" + ]: + if video_format["video_ext"] == "none": + continue + if video_format["has_drm"]: + continue + + if video_format["vbr"] > best_bitrate: + best = video_format + best_bitrate = video_format["vbr"] + + if not best or "manifest_url" not in best: + raise exceptions.NotSupportedError("manifest url could not be extracted") + + return best["manifest_url"] + + +async def extract_video_url(video_link: str) -> str: + """Extract video URL from external video service link. + + This method takes a video link from a video service, e.g. YouTube, and extracts the + underlying video URL that (hopefully) can be played via AirPlay. Currently yt-dlp + is used to the extract the URL, thus all services supported by yt-dlp should be + supported. No customization (e.g. resolution) nor authorization is supported at the + moment, putting some restrictions on use case. + """ + loop = asyncio.get_event_loop() + try: + return await loop.run_in_executor(None, _extract_video_url, video_link) + except Exception as ex: + raise exceptions.InvalidFormatError(f"video {video_link} not supported") from ex diff --git a/tests/core/test_facade.py b/tests/core/test_facade.py index 5cd5de27e..810d751a3 100644 --- a/tests/core/test_facade.py +++ b/tests/core/test_facade.py @@ -967,7 +967,7 @@ async def test_base_methods_guarded_after_close(facade_dummy, register_interface (RemoteControl, "remote_control", {}), (Metadata, "metadata", {}), (PushUpdater, "push_updater", {}), - (Stream, "stream", {}), + (Stream, "stream", {"play_service"}), (Power, "power", {}), # in_states is not abstract but uses get_features, will which will raise (Features, "features", {"in_state"}),