1010import os .path
1111import logging
1212import json
13- import requests
13+ import aiohttp
14+ import asyncio
15+ import async_timeout
1416import voluptuous as vol
1517import homeassistant .helpers .config_validation as cv
1618from datetime import datetime
1719from homeassistant .components .sensor import PLATFORM_SCHEMA
1820from homeassistant .const import CONF_HOST , CONF_NAME , CONF_PORT , CONF_SSL
1921from 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+
2543CONF_DL_IMAGES = 'download_images'
2644DEFAULT_NAME = 'Plex Recently Added'
2745CONF_SERVER = 'server_name'
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
347342def days_since (date , tz ):
0 commit comments