Skip to content

Commit 85b39b2

Browse files
committed
aiohttp and async, remove server_name
1 parent 557d273 commit 85b39b2

File tree

2 files changed

+120
-125
lines changed

2 files changed

+120
-125
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Read through these two resources before posting issues to GitHub or the forums.
3333
| ssl | false | no | Set to true if you use SSL to access Plex.
3434
| max | 5 | no | Max number of items to show in sensor.
3535
| download_images | true | no | Setting this to false will turn off downloading of images, but will require certain Plex settings to work. See below.
36-
| img_dir | '/custom-lovelace/upcoming-media-card/images/plex/' | no | This option allows you to choose a custom directory to store images in if you enable download_images.
36+
| img_dir | '/upcoming-media-card-images/plex/' | no | This option allows you to choose a custom directory to store images in if you enable download_images. Directory must start and end with a `/`.
3737
| ssl_cert | false | no | If you provide your own SSL certificate in Plex's network settings set this to true.
3838
| section_types | movie, show | no | Allows you to specify which section types to consider [movie, show].
3939
| image_resolution | 200 | no | Allows you to change the resolution of the generated images (in px), useful to display higher quality images as a background somewhere.

custom_components/plex_recently_added/sensor.py

Lines changed: 119 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,36 @@
1010
import os.path
1111
import logging
1212
import json
13-
import requests
13+
import aiohttp
14+
import asyncio
15+
import async_timeout
1416
import voluptuous as vol
1517
import homeassistant.helpers.config_validation as cv
1618
from datetime import datetime
1719
from homeassistant.components.sensor import PLATFORM_SCHEMA
1820
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL
1921
from homeassistant.helpers.entity import Entity
2022

21-
__version__ = '0.3.1'
22-
2323
_LOGGER = logging.getLogger(__name__)
2424

25+
26+
async def fetch(session, url, self, ssl, content):
27+
with async_timeout.timeout(10):
28+
async with session.get(
29+
url, ssl=ssl, headers={
30+
"Accept": "application/json", "X-Plex-Token": self.token}
31+
) as response:
32+
if content:
33+
return await response.content.read()
34+
else:
35+
return await response.text()
36+
37+
38+
async def request(url, self, content=False, ssl=False):
39+
async with aiohttp.ClientSession() as session:
40+
return await fetch(session, url, self, ssl, content)
41+
42+
2543
CONF_DL_IMAGES = 'download_images'
2644
DEFAULT_NAME = 'Plex Recently Added'
2745
CONF_SERVER = 'server_name'
@@ -43,10 +61,10 @@
4361
vol.Optional(CONF_HOST, default='localhost'): cv.string,
4462
vol.Optional(CONF_PORT, default=32400): cv.port,
4563
vol.Optional(CONF_SECTION_TYPES,
46-
default=['movie', 'show']): vol.All(cv.ensure_list, [cv.string]),
64+
default=['movie', 'show']): vol.All(cv.ensure_list, [cv.string]),
4765
vol.Optional(CONF_RESOLUTION, default=200): cv.positive_int,
48-
vol.Optional(CONF_IMG_CACHE,
49-
default='/upcoming-media-card-images/plex/'): cv.string
66+
vol.Optional(CONF_IMG_CACHE,
67+
default='/upcoming-media-card-images/plex/'): cv.string
5068
})
5169

5270

@@ -77,8 +95,9 @@ def __init__(self, hass, conf, name):
7795
self.sections = conf.get(CONF_SECTION_TYPES)
7896
self.resolution = conf.get(CONF_RESOLUTION)
7997
if self.server_name:
80-
self.server_ip, self.local_ip, self.port = get_server_ip(
81-
self.server_name, self.token)
98+
_LOGGER.warning(
99+
"Plex Recently Added: The server_name option has been removed. Use host and port options instead.")
100+
return
82101
else:
83102
self.server_ip = conf.get(CONF_HOST)
84103
self.local_ip = conf.get(CONF_HOST)
@@ -97,10 +116,14 @@ def name(self):
97116

98117
@property
99118
def state(self):
119+
if self.server_name:
120+
return "server_name is no longer an option, use host and port."
100121
return self._state
101122

102123
@property
103124
def device_state_attributes(self):
125+
if self.server_name:
126+
return
104127
import math
105128
attributes = {}
106129
if self.change_detected:
@@ -181,23 +204,20 @@ def device_state_attributes(self):
181204
else:
182205
card_item['fanart'] = ''
183206
else:
184-
card_item['poster'] = image_url(self.url_elements,
207+
card_item['poster'] = image_url(self,
185208
False, poster, self.resolution)
186-
card_item['fanart'] = image_url(self.url_elements,
209+
card_item['fanart'] = image_url(self,
187210
False, fanart, self.resolution)
188211
self.card_json.append(card_item)
189212
self.change_detected = False
190213
attributes['data'] = self.card_json
191214
return attributes
192215

193-
def update(self):
194-
import re
216+
async def async_update(self):
195217
import os
196-
plex = requests.Session()
197-
if not self.cert:
198-
"""Default SSL certificate is for plex.tv not our api server"""
199-
plex.verify = False
200-
headers = {"Accept": "application/json", "X-Plex-Token": self.token}
218+
import re
219+
if self.server_name:
220+
return
201221
url_base = 'http{0}://{1}:{2}/library/sections'.format(self.ssl,
202222
self.server_ip,
203223
self.port)
@@ -208,102 +228,100 @@ def update(self):
208228
"""Find the ID of all libraries in Plex."""
209229
sections = []
210230
try:
211-
libraries = plex.get(all_libraries, headers=headers, timeout=10)
212-
for lib_section in libraries.json()['MediaContainer']['Directory']:
231+
libraries = await request(all_libraries, self)
232+
libraries = json.loads(libraries)
233+
for lib_section in libraries['MediaContainer']['Directory']:
213234
if lib_section['type'] in self.sections:
214235
sections.append(lib_section['key'])
215236
except OSError:
216237
_LOGGER.warning("Host %s is not available", self.server_ip)
217238
self._state = '%s cannot be reached' % self.server_ip
218239
return
219-
if libraries.status_code == 200:
220-
self.api_json = []
221-
self._state = 'Online'
222-
"""Get JSON for each library, combine and sort."""
223-
for library in sections:
224-
sub_sec = plex.get(recently_added.format(
225-
library, self.max_items * 2), headers=headers, timeout=10)
226-
try:
227-
self.api_json += sub_sec.json()['MediaContainer']['Metadata']
228-
except:
229-
_LOGGER.warning('No Metadata field for "{}"'.format(sub_sec.json()['MediaContainer']['librarySectionTitle']))
230-
pass
231-
self.api_json = sorted(self.api_json, key=lambda i: i['addedAt'],
232-
reverse=True)[:self.max_items]
233-
234-
"""Update attributes if view count changes"""
235-
if view_count(self.api_json) != view_count(self.data):
236-
self.change_detected = True
237-
self.data = self.api_json
240+
self.api_json = []
241+
self._state = 'Online'
242+
"""Get JSON for each library, combine and sort."""
243+
for library in sections:
244+
sub_sec = await request(recently_added.format(
245+
library, self.max_items * 2), self)
246+
sub_sec = json.loads(sub_sec)
247+
try:
248+
self.api_json += sub_sec['MediaContainer']['Metadata']
249+
except:
250+
_LOGGER.warning('No Metadata field for "{}"'.format(
251+
sub_sec['MediaContainer']['librarySectionTitle']))
252+
pass
253+
self.api_json = sorted(self.api_json, key=lambda i: i['addedAt'],
254+
reverse=True)[:self.max_items]
238255

239-
api_ids = media_ids(self.api_json, True)
240-
data_ids = media_ids(self.data, True)
241-
if self.dl_images:
242-
directory = self.conf_dir + 'www' + self._dir
243-
if not os.path.exists(directory):
244-
os.makedirs(directory, mode=0o777)
245-
246-
"""Make list of images in dir that use our naming scheme"""
247-
dir_re = re.compile(r'[pf]\d+\.jpg') # p1234.jpg or f1234.jpg
248-
dir_images = list(filter(dir_re.search,
249-
os.listdir(directory)))
250-
dir_ids = [file[1:-4] for file in dir_images]
251-
dir_ids.sort(key=int)
252-
253-
"""Update if media items have changed or images are missing"""
254-
if dir_ids != api_ids or data_ids != api_ids:
255-
self.change_detected = True # Tell attributes to update
256-
self.data = self.api_json
257-
"""Remove images not in list"""
258-
for file in dir_images:
259-
if not any(str(ids) in file for ids in data_ids):
260-
os.remove(directory + file)
261-
"""Retrieve image from Plex if it doesn't exist"""
262-
for media in self.data:
263-
if 'type' not in media:
264-
continue
265-
elif media['type'] == 'movie':
266-
poster = media.get('thumb', '')
267-
fanart = media.get('art', '')
268-
elif media['type'] == 'episode':
269-
poster = media.get('grandparentThumb', '')
270-
fanart = media.get('grandparentArt', '')
256+
"""Update attributes if view count changes"""
257+
if view_count(self.api_json) != view_count(self.data):
258+
self.change_detected = True
259+
self.data = self.api_json
260+
261+
api_ids = media_ids(self.api_json, True)
262+
data_ids = media_ids(self.data, True)
263+
if self.dl_images:
264+
directory = self.conf_dir + 'www' + self._dir
265+
if not os.path.exists(directory):
266+
os.makedirs(directory, mode=0o777)
267+
268+
"""Make list of images in dir that use our naming scheme"""
269+
dir_re = re.compile(r'[pf]\d+\.jpg') # p1234.jpg or f1234.jpg
270+
dir_images = list(filter(dir_re.search,
271+
os.listdir(directory)))
272+
dir_ids = [file[1:-4] for file in dir_images]
273+
dir_ids.sort(key=int)
274+
275+
"""Update if media items have changed or images are missing"""
276+
if dir_ids != api_ids or data_ids != api_ids:
277+
self.change_detected = True # Tell attributes to update
278+
self.data = self.api_json
279+
"""Remove images not in list"""
280+
for file in dir_images:
281+
if not any(str(ids) in file for ids in data_ids):
282+
os.remove(directory + file)
283+
"""Retrieve image from Plex if it doesn't exist"""
284+
for media in self.data:
285+
if 'type' not in media:
286+
continue
287+
elif media['type'] == 'movie':
288+
poster = media.get('thumb', '')
289+
fanart = media.get('art', '')
290+
elif media['type'] == 'episode':
291+
poster = media.get('grandparentThumb', '')
292+
fanart = media.get('grandparentArt', '')
293+
else:
294+
_LOGGER.error("Media type: %s", media['type'])
295+
continue
296+
poster_jpg = '{}p{}.jpg'.format(directory,
297+
media['ratingKey'])
298+
fanart_jpg = '{}f{}.jpg'.format(directory,
299+
media['ratingKey'])
300+
if not os.path.isfile(fanart_jpg):
301+
fanart_image = await request(image_url(
302+
self, True, fanart, self.resolution), self, True, True)
303+
if fanart_image:
304+
open(fanart_jpg, 'wb').write(fanart_image)
305+
else:
306+
pass
307+
if not os.path.isfile(poster_jpg):
308+
poster_image = await request(image_url(
309+
self, True, poster, self.resolution), self, True, True)
310+
if poster_image:
311+
open(poster_jpg, 'wb').write(poster_image)
271312
else:
272-
_LOGGER.error("Media type: %s", media['type'])
273313
continue
274-
poster_jpg = '{}p{}.jpg'.format(directory,
275-
media['ratingKey'])
276-
fanart_jpg = '{}f{}.jpg'.format(directory,
277-
media['ratingKey'])
278-
if not os.path.isfile(fanart_jpg):
279-
if image_url(self.url_elements, True, fanart):
280-
image = plex.get(image_url(
281-
self.url_elements, True, fanart, self.resolution),
282-
headers=headers, timeout=10).content
283-
open(fanart_jpg, 'wb').write(image)
284-
else:
285-
pass
286-
if not os.path.isfile(poster_jpg):
287-
if image_url(self.url_elements, True, poster):
288-
image = plex.get(image_url(
289-
self.url_elements, True, poster, self.resolution),
290-
headers=headers, timeout=10).content
291-
open(poster_jpg, 'wb').write(image)
292-
else:
293-
continue
294-
else:
295-
"""Update if media items have changed"""
296-
if api_ids != data_ids:
297-
self.change_detected = True # Tell attributes to update
298-
self.data = self.api_json
299314
else:
300-
self._state = '%s cannot be reached' % self.server_ip
315+
"""Update if media items have changed"""
316+
if api_ids != data_ids:
317+
self.change_detected = True # Tell attributes to update
318+
self.data = self.api_json
301319

302320

303-
def image_url(url_elements, cert_check, img, resolution=200):
321+
def image_url(self, cert_check, img, resolution=200):
304322
"""Plex can resize images with a long & partially % encoded url."""
305323
from urllib.parse import quote
306-
ssl, host, local, port, token, self_cert, dl_images = url_elements
324+
ssl, host, local, port, token, self_cert, dl_images = self.url_elements
307325
if not cert_check and not self_cert:
308326
ssl = ''
309327
if dl_images:
@@ -313,35 +331,12 @@ def image_url(url_elements, cert_check, img, resolution=200):
313331
port,
314332
img,
315333
token),
316-
safe='')
334+
safe='')
317335
url = ('http{0}://{1}:{2}/photo/:/transcode?width={5}&height={5}'
318336
'&minSize=1&url={3}&X-Plex-Token={4}').format(ssl, host, port,
319337
encoded, token,
320338
resolution)
321-
"""Check if image exists"""
322-
if not self_cert:
323-
r = requests.head(url, verify=False)
324-
else:
325-
r = requests.head(url)
326-
if r.status_code == 200:
327-
return url
328-
else:
329-
return False
330-
331-
332-
def get_server_ip(name, token):
333-
"""With a token and server name we get server's ip, local ip, and port"""
334-
import xml.etree.ElementTree as ET
335-
from unicodedata import normalize
336-
plex_tv = requests.get(
337-
'https://plex.tv/api/servers.xml?X-Plex-Token=' + token, timeout=10)
338-
plex_xml = ET.fromstring(plex_tv.content)
339-
for server in plex_xml.findall('Server'):
340-
server_name = server.get('name').casefold()
341-
name = name.casefold()
342-
if normalize('NFKD', server_name) == normalize('NFKD', name):
343-
return (server.get('address'), server.get('localAddresses'),
344-
server.get('port'))
339+
return url
345340

346341

347342
def days_since(date, tz):

0 commit comments

Comments
 (0)