Add lookup & control of linked Sonos speakers

This commit is contained in:
Jason Lawrence 2020-04-28 11:52:09 -05:00
parent 6eea7cce0f
commit 55b335ee7c
3 changed files with 112 additions and 1 deletions

View file

@ -157,7 +157,7 @@ class PlexClient(PlexObject):
log.debug('%s %s', method.__name__.upper(), url)
headers = self._headers(**headers or {})
response = method(url, headers=headers, timeout=timeout, **kwargs)
if response.status_code not in (200, 201):
if response.status_code not in (200, 201, 204):
codename = codes.get(response.status_code)[0]
errtext = response.text.replace('\n', ' ')
message = '(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext)

View file

@ -12,6 +12,7 @@ from plexapi.client import PlexClient
from plexapi.compat import ElementTree
from plexapi.library import LibrarySection
from plexapi.server import PlexServer
from plexapi.sonos import PlexSonosClient
from plexapi.sync import SyncItem, SyncList
from plexapi.utils import joinArgs
from requests.status_codes import _codes as codes
@ -209,6 +210,12 @@ class MyPlexAccount(PlexObject):
data = self.query(MyPlexResource.key)
return [MyPlexResource(self, elem) for elem in data]
def sonos_speakers(self):
if 'companions_sonos' not in self.subscriptionFeatures:
return []
data = self.query('https://sonos.plex.tv/resources')
return [PlexSonosClient(self, elem) for elem in data]
def inviteFriend(self, user, server, sections=None, allowSync=False, allowCameraUpload=False,
allowChannels=False, filterMovies=None, filterTelevision=None, filterMusic=None):
""" Share library content with the specified user.

104
plexapi/sonos.py Normal file
View file

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
import requests
from plexapi import CONFIG
from plexapi.client import PlexClient
from plexapi.exceptions import BadRequest
from plexapi.playqueue import PlayQueue
class PlexSonosClient(PlexClient):
""" Class for interacting with a Sonos speaker via the Plex API. This class
makes requests to an external Plex API which then forwards the
Sonos-specific commands back to your Plex server & Sonos speakers. Use
of this feature requires an active Plex Pass subscription and Sonos
speakers linked to your Plex account. It also requires remote access to
be working properly.
More details on the Sonos integration are avaialble here:
https://support.plex.tv/articles/218237558-requirements-for-using-plex-for-sonos/
The Sonos API emulates the Plex player control API closely:
https://github.com/plexinc/plex-media-player/wiki/Remote-control-API
Parameters:
server (:class:`~plexapi.myplex.PlexAccount`): PlexAccount instance this
Sonos speaker is associated with.
data (ElementTree): Response from Plex Sonos API used to build this client.
Attributes:
deviceClass (str): "speaker"
lanIP (str): Local IP address of speaker.
machineIdentifier (str): Unique ID for this device.
platform (str): "Sonos"
platformVersion (str): Build version of Sonos speaker firmware.
product (str): "Sonos"
protocol (str): "plex"
protocolCapabilities (list<str>): List of client capabilities (timeline, playback,
playqueues, provider-playback)
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
session (:class:`~requests.Session`): Session object used for connection.
title (str): Name of this Sonos speaker.
token (str): X-Plex-Token used for authenication
_baseurl (str): Address of public Plex Sonos API endpoint.
_commandId (int): Counter for commands sent to Plex API.
_token (str): Token associated with linked Plex account.
_session (obj): Requests session object used to access this client.
"""
def __init__(self, account, data):
self._data = data
self.deviceClass = data.attrib.get("deviceClass")
self.machineIdentifier = data.attrib.get("machineIdentifier")
self.product = data.attrib.get("product")
self.platform = data.attrib.get("platform")
self.platformVersion = data.attrib.get("platformVersion")
self.protocol = data.attrib.get("protocol")
self.protocolCapabilities = data.attrib.get("protocolCapabilities")
self.lanIP = data.attrib.get("lanIP")
self.title = data.attrib.get("title")
self._baseurl = "https://sonos.plex.tv"
self._commandId = 0
self._token = account._token
self._session = account._session or requests.Session()
# Dummy values for PlexClient inheritance
self._last_call = 0
self._proxyThroughServer = False
self._showSecrets = CONFIG.get("log.show_secrets", "").lower() == "true"
def playMedia(self, media, offset=0, **params):
if hasattr(media, "playlistType"):
mediatype = media.playlistType
else:
if isinstance(media, PlayQueue):
mediatype = media.items[0].listType
else:
mediatype = media.listType
if mediatype == "audio":
mediatype = "music"
else:
raise BadRequest("Sonos currently only supports music for playback")
server_protocol, server_address, server_port = media._server._baseurl.split(":")
server_address = server_address.strip("/")
server_port = server_port.strip("/")
playqueue = media if isinstance(media, PlayQueue) else media._server.createPlayQueue(media)
self.sendCommand("playback/playMedia", **dict({
"type": "music",
"providerIdentifier": "com.plexapp.plugins.library",
"containerKey": f"/playQueues/{playqueue.playQueueID}?own=1",
"key": media.key,
"offset": offset,
"machineIdentifier": media._server.machineIdentifier,
"protocol": server_protocol,
"address": server_address,
"port": server_port,
"token": media._server.createToken(),
"commandID": self._nextCommandId(),
#"X-Plex-Client-Identifier": client_id,
"X-Plex-Token": media._server._token,
"X-Plex-Target-Client-Identifier": self.machineIdentifier,
}, **params))