1# -*- coding: utf-8 -*-
2
3# This file is part of Tautulli.
4#
5#  Tautulli is free software: you can redistribute it and/or modify
6#  it under the terms of the GNU General Public License as published by
7#  the Free Software Foundation, either version 3 of the License, or
8#  (at your option) any later version.
9#
10#  Tautulli is distributed in the hope that it will be useful,
11#  but WITHOUT ANY WARRANTY; without even the implied warranty of
12#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#  GNU General Public License for more details.
14#
15#  You should have received a copy of the GNU General Public License
16#  along with Tautulli.  If not, see <http://www.gnu.org/licenses/>.
17
18from __future__ import unicode_literals
19from future.builtins import next
20from future.builtins import str
21from future.builtins import object
22from future.moves.urllib.parse import unquote
23
24import json
25
26import plexpy
27if plexpy.PYTHON2:
28    import common
29    import helpers
30    import http_handler
31    import logger
32    import users
33    import pmsconnect
34    import session
35    from plex import Plex
36else:
37    from plexpy import common
38    from plexpy import helpers
39    from plexpy import http_handler
40    from plexpy import logger
41    from plexpy import users
42    from plexpy import pmsconnect
43    from plexpy import session
44    from plexpy.plex import Plex
45
46
47def get_server_resources(return_presence=False, return_server=False, return_info=False, **kwargs):
48    if not return_presence and not return_info:
49        logger.info("Tautulli PlexTV :: Requesting resources for server...")
50
51    server = {'pms_name': plexpy.CONFIG.PMS_NAME,
52              'pms_version': plexpy.CONFIG.PMS_VERSION,
53              'pms_platform': plexpy.CONFIG.PMS_PLATFORM,
54              'pms_ip': plexpy.CONFIG.PMS_IP,
55              'pms_port': plexpy.CONFIG.PMS_PORT,
56              'pms_ssl': plexpy.CONFIG.PMS_SSL,
57              'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
58              'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
59              'pms_url': plexpy.CONFIG.PMS_URL,
60              'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL,
61              'pms_identifier': plexpy.CONFIG.PMS_IDENTIFIER,
62              'pms_plexpass': plexpy.CONFIG.PMS_PLEXPASS
63              }
64
65    if return_info:
66        return server
67
68    if kwargs:
69        server.update(kwargs)
70        for k in ['pms_ssl', 'pms_is_remote', 'pms_is_cloud', 'pms_url_manual']:
71            server[k] = int(server[k])
72
73    if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']:
74        scheme = 'https'
75    else:
76        scheme = 'http'
77
78    fallback_url = '{scheme}://{hostname}:{port}'.format(scheme=scheme,
79                                                         hostname=server['pms_ip'],
80                                                         port=server['pms_port'])
81
82    plex_tv = PlexTV()
83    result = plex_tv.get_server_connections(pms_identifier=server['pms_identifier'],
84                                            pms_ip=server['pms_ip'],
85                                            pms_port=server['pms_port'],
86                                            include_https=server['pms_ssl'])
87
88    if result:
89        connections = result.pop('connections', [])
90        server.update(result)
91        presence = server.pop('pms_presence', 0)
92    else:
93        connections = []
94        presence = 0
95
96    if return_presence:
97        return presence
98
99    plexpass = plex_tv.get_plexpass_status()
100    server['pms_plexpass'] = int(plexpass)
101
102    # Only need to retrieve PMS_URL if using SSL
103    if not server['pms_url_manual'] and server['pms_ssl']:
104        if connections:
105            if server['pms_is_remote']:
106                # Get all remote connections
107                conns = [c for c in connections if
108                         c['local'] == '0' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])]
109            else:
110                # Get all local connections
111                conns = [c for c in connections if
112                         c['local'] == '1' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])]
113
114            if conns:
115                # Get connection with matching address, otherwise return first connection
116                conn = next((c for c in conns if c['address'] == server['pms_ip']
117                             and c['port'] == str(server['pms_port'])), conns[0])
118                server['pms_url'] = conn['uri']
119                logger.info("Tautulli PlexTV :: Server URL retrieved.")
120
121        # get_server_urls() failed or PMS_URL not found, fallback url doesn't use SSL
122        if not server['pms_url']:
123            server['pms_url'] = fallback_url
124            logger.warn("Tautulli PlexTV :: Unable to retrieve server URLs. Using user-defined value without SSL.")
125
126        # Not using SSL, remote has no effect
127    else:
128        server['pms_url'] = fallback_url
129        logger.info("Tautulli PlexTV :: Using user-defined URL.")
130
131    if return_server:
132        return server
133
134    logger.info("Tautulli PlexTV :: Selected server: %s (%s) (%s - Version %s)",
135                server['pms_name'], server['pms_url'], server['pms_platform'], server['pms_version'])
136
137    plexpy.CONFIG.process_kwargs(server)
138    plexpy.CONFIG.write()
139
140
141class PlexTV(object):
142    """
143    Plex.tv authentication
144    """
145
146    def __init__(self, username=None, password=None, token=None, headers=None):
147        self.username = username
148        self.password = password
149        self.token = token
150
151        self.urls = 'https://plex.tv'
152        self.timeout = plexpy.CONFIG.PMS_TIMEOUT
153        self.ssl_verify = plexpy.CONFIG.VERIFY_SSL_CERT
154
155        if self.username is None and self.password is None:
156            if not self.token:
157                # Check if we should use the admin token, or the guest server token
158                if session.get_session_user_id():
159                    user_data = users.Users()
160                    user_tokens = user_data.get_tokens(user_id=session.get_session_user_id())
161                    self.token = user_tokens['server_token']
162                else:
163                    self.token = plexpy.CONFIG.PMS_TOKEN
164
165            if not self.token:
166                logger.error("Tautulli PlexTV :: PlexTV called, but no token provided.")
167                return
168
169        self.request_handler = http_handler.HTTPHandler(urls=self.urls,
170                                                        token=self.token,
171                                                        timeout=self.timeout,
172                                                        ssl_verify=self.ssl_verify,
173                                                        headers=headers)
174
175    def get_server_token(self):
176        servers = self.get_plextv_resources(output_format='xml')
177        server_token = ''
178
179        try:
180            xml_head = servers.getElementsByTagName('Device')
181        except Exception as e:
182            logger.warn("Tautulli PlexTV :: Unable to parse XML for get_server_token: %s." % e)
183            return None
184
185        for a in xml_head:
186            if helpers.get_xml_attr(a, 'clientIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER \
187                    and 'server' in helpers.get_xml_attr(a, 'provides'):
188                server_token = helpers.get_xml_attr(a, 'accessToken')
189                break
190
191        return server_token
192
193    def get_plextv_pin(self, pin='', output_format=''):
194        if pin:
195            uri = '/api/v2/pins/' + pin
196            request = self.request_handler.make_request(uri=uri,
197                                                        request_type='GET',
198                                                        output_format=output_format,
199                                                        no_token=True)
200        else:
201            uri = '/api/v2/pins?strong=true'
202            request = self.request_handler.make_request(uri=uri,
203                                                        request_type='POST',
204                                                        output_format=output_format,
205                                                        no_token=True)
206        return request
207
208    def get_pin(self, pin=''):
209        plextv_response = self.get_plextv_pin(pin=pin,
210                                              output_format='xml')
211
212        if plextv_response:
213            try:
214                xml_head = plextv_response.getElementsByTagName('pin')
215                if xml_head:
216                    pin = {'id': xml_head[0].getAttribute('id'),
217                           'code': xml_head[0].getAttribute('code'),
218                           'token': xml_head[0].getAttribute('authToken')
219                           }
220                    return pin
221                else:
222                    logger.warn("Tautulli PlexTV :: Could not get Plex authentication pin.")
223                    return None
224
225            except Exception as e:
226                logger.warn("Tautulli PlexTV :: Unable to parse XML for get_pin: %s." % e)
227                return None
228
229        else:
230            return None
231
232    def get_plextv_friends(self, output_format=''):
233        uri = '/api/users'
234        request = self.request_handler.make_request(uri=uri,
235                                                    request_type='GET',
236                                                    output_format=output_format)
237
238        return request
239
240    def get_plextv_user_details(self, output_format=''):
241        uri = '/users/account'
242        request = self.request_handler.make_request(uri=uri,
243                                                    request_type='GET',
244                                                    output_format=output_format)
245
246        return request
247
248    def get_plextv_devices_list(self, output_format=''):
249        uri = '/devices.xml'
250        request = self.request_handler.make_request(uri=uri,
251                                                    request_type='GET',
252                                                    output_format=output_format)
253
254        return request
255
256    def get_plextv_server_list(self, output_format=''):
257        uri = '/pms/servers.xml'
258        request = self.request_handler.make_request(uri=uri,
259                                                    request_type='GET',
260                                                    output_format=output_format)
261
262        return request
263
264    def get_plextv_shared_servers(self, machine_id='', output_format=''):
265        uri = '/api/servers/%s/shared_servers' % machine_id
266        request = self.request_handler.make_request(uri=uri,
267                                                    request_type='GET',
268                                                    output_format=output_format)
269
270        return request
271
272    def get_plextv_sync_lists(self, machine_id='', output_format=''):
273        uri = '/servers/%s/sync_lists' % machine_id
274        request = self.request_handler.make_request(uri=uri,
275                                                    request_type='GET',
276                                                    output_format=output_format)
277
278        return request
279
280    def get_plextv_resources(self, include_https=False, output_format=''):
281        if include_https:
282            uri = '/api/resources?includeHttps=1'
283        else:
284            uri = '/api/resources'
285        request = self.request_handler.make_request(uri=uri,
286                                                    request_type='GET',
287                                                    output_format=output_format)
288
289        return request
290
291    def get_plextv_downloads(self, plexpass=False, output_format=''):
292        if plexpass:
293            uri = '/api/downloads/5.json?channel=plexpass'
294        else:
295            uri = '/api/downloads/1.json'
296        request = self.request_handler.make_request(uri=uri,
297                                                    request_type='GET',
298                                                    output_format=output_format)
299
300        return request
301
302    def delete_plextv_device(self, device_id='', output_format=''):
303        uri = '/devices/%s.xml' % device_id
304        request = self.request_handler.make_request(uri=uri,
305                                                    request_type='DELETE',
306                                                    output_format=output_format)
307
308        return request
309
310    def delete_plextv_device_sync_lists(self, client_id='', output_format=''):
311        uri = '/devices/%s/sync_items' % client_id
312        request = self.request_handler.make_request(uri=uri,
313                                                    request_type='GET',
314                                                    output_format=output_format)
315
316        return request
317
318    def delete_plextv_sync(self, client_id='', sync_id=''):
319        uri = '/devices/%s/sync_items/%s' % (client_id, sync_id)
320        request = self.request_handler.make_request(uri=uri,
321                                                    request_type='DELETE',
322                                                    return_response=True)
323
324        return request
325
326    def cloud_server_status(self, output_format=''):
327        uri = '/api/v2/cloud_server'
328        request = self.request_handler.make_request(uri=uri,
329                                                    request_type='GET',
330                                                    output_format=output_format)
331
332        return request
333
334    def get_plextv_geoip(self, ip_address='', output_format=''):
335        uri = '/api/v2/geoip?ip_address=%s' % ip_address
336        request = self.request_handler.make_request(uri=uri,
337                                                    request_type='GET',
338                                                    output_format=output_format)
339
340        return request
341
342    def get_full_users_list(self):
343        own_account = self.get_plextv_user_details(output_format='xml')
344        friends_list = self.get_plextv_friends(output_format='xml')
345        shared_servers = self.get_plextv_shared_servers(machine_id=plexpy.CONFIG.PMS_IDENTIFIER,
346                                                        output_format='xml')
347
348        users_list = []
349
350        try:
351            xml_head = own_account.getElementsByTagName('user')
352        except Exception as e:
353            logger.warn("Tautulli PlexTV :: Unable to parse own account XML for get_full_users_list: %s." % e)
354            return []
355
356        for a in xml_head:
357            own_details = {"user_id": helpers.get_xml_attr(a, 'id'),
358                           "username": helpers.get_xml_attr(a, 'username'),
359                           "thumb": helpers.get_xml_attr(a, 'thumb'),
360                           "email": helpers.get_xml_attr(a, 'email'),
361                           "is_active": 1,
362                           "is_admin": 1,
363                           "is_home_user": helpers.get_xml_attr(a, 'home'),
364                           "is_allow_sync": 1,
365                           "is_restricted": helpers.get_xml_attr(a, 'restricted'),
366                           "filter_all": helpers.get_xml_attr(a, 'filterAll'),
367                           "filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
368                           "filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
369                           "filter_music": helpers.get_xml_attr(a, 'filterMusic'),
370                           "filter_photos": helpers.get_xml_attr(a, 'filterPhotos'),
371                           "user_token": helpers.get_xml_attr(a, 'authToken'),
372                           "server_token": helpers.get_xml_attr(a, 'authToken'),
373                           "shared_libraries": None,
374                           }
375
376            users_list.append(own_details)
377
378        try:
379            xml_head = friends_list.getElementsByTagName('User')
380        except Exception as e:
381            logger.warn("Tautulli PlexTV :: Unable to parse friends list XML for get_full_users_list: %s." % e)
382            return []
383
384        for a in xml_head:
385            friend = {"user_id": helpers.get_xml_attr(a, 'id'),
386                      "username": helpers.get_xml_attr(a, 'title'),
387                      "thumb": helpers.get_xml_attr(a, 'thumb'),
388                      "email": helpers.get_xml_attr(a, 'email'),
389                      "is_active": 1,
390                      "is_admin": 0,
391                      "is_home_user": helpers.get_xml_attr(a, 'home'),
392                      "is_allow_sync": helpers.get_xml_attr(a, 'allowSync'),
393                      "is_restricted": helpers.get_xml_attr(a, 'restricted'),
394                      "filter_all": helpers.get_xml_attr(a, 'filterAll'),
395                      "filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
396                      "filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
397                      "filter_music": helpers.get_xml_attr(a, 'filterMusic'),
398                      "filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
399                      }
400
401            users_list.append(friend)
402
403        try:
404            xml_head = shared_servers.getElementsByTagName('SharedServer')
405        except Exception as e:
406            logger.warn("Tautulli PlexTV :: Unable to parse shared server list XML for get_full_users_list: %s." % e)
407            return []
408
409        user_map = {}
410        for a in xml_head:
411            user_id = helpers.get_xml_attr(a, 'userID')
412            server_token = helpers.get_xml_attr(a, 'accessToken')
413
414            sections = a.getElementsByTagName('Section')
415            shared_libraries = [helpers.get_xml_attr(s, 'key')
416                                for s in sections if helpers.get_xml_attr(s, 'shared') == '1']
417
418            user_map[user_id] = {'server_token': server_token,
419                                 'shared_libraries': shared_libraries}
420
421        for u in users_list:
422            d = user_map.get(u['user_id'], {})
423            u.update(d)
424
425        return users_list
426
427    def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None,
428                         rating_key_filter=None, sync_id_filter=None):
429
430        if not machine_id:
431            machine_id = plexpy.CONFIG.PMS_IDENTIFIER
432
433        if isinstance(rating_key_filter, list):
434            rating_key_filter = [str(k) for k in rating_key_filter]
435        elif rating_key_filter:
436            rating_key_filter = [str(rating_key_filter)]
437
438        if isinstance(user_id_filter, list):
439            user_id_filter = [str(k) for k in user_id_filter]
440        elif user_id_filter:
441            user_id_filter = [str(user_id_filter)]
442
443        sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml')
444        user_data = users.Users()
445
446        synced_items = []
447
448        try:
449            xml_head = sync_list.getElementsByTagName('SyncList')
450        except Exception as e:
451            logger.warn("Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s." % e)
452            return {}
453
454        for a in xml_head:
455            client_id = helpers.get_xml_attr(a, 'clientIdentifier')
456
457            # Filter by client_id
458            if client_id_filter and str(client_id_filter) != client_id:
459                continue
460
461            sync_list_id = helpers.get_xml_attr(a, 'id')
462            sync_device = a.getElementsByTagName('Device')
463
464            for device in sync_device:
465                device_user_id = helpers.get_xml_attr(device, 'userID')
466                try:
467                    device_username = user_data.get_details(user_id=device_user_id)['username']
468                    device_friendly_name = user_data.get_details(user_id=device_user_id)['friendly_name']
469                except:
470                    device_username = ''
471                    device_friendly_name = ''
472                device_name = helpers.get_xml_attr(device, 'name')
473                device_product = helpers.get_xml_attr(device, 'product')
474                device_product_version = helpers.get_xml_attr(device, 'productVersion')
475                device_platform = helpers.get_xml_attr(device, 'platform')
476                device_platform_version = helpers.get_xml_attr(device, 'platformVersion')
477                device_type = helpers.get_xml_attr(device, 'device')
478                device_model = helpers.get_xml_attr(device, 'model')
479                device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
480
481            # Filter by user_id
482            if user_id_filter and device_user_id not in user_id_filter:
483                continue
484
485            for synced in a.getElementsByTagName('SyncItems'):
486                sync_item = synced.getElementsByTagName('SyncItem')
487                for item in sync_item:
488
489                    sync_media_type = None
490                    rating_key = None
491                    for location in item.getElementsByTagName('Location'):
492                        location_uri = unquote(helpers.get_xml_attr(location, 'uri'))
493
494                        if location_uri.startswith('library://'):
495                            if 'collection' in location_uri:
496                                sync_media_type = 'collection'
497                            clean_uri = location_uri.split('/')
498                            rating_key = next((j for i, j in zip(clean_uri[:-1], clean_uri[1:])
499                                              if i in ('metadata', 'collections')), None)
500
501                        elif location_uri.startswith('playlist://'):
502                            sync_media_type = 'playlist'
503                            tokens = users.Users().get_tokens(user_id=device_user_id)
504                            if tokens['server_token']:
505                                plex = Plex(token=tokens['server_token'])
506                                for playlist in plex.PlexServer.playlists():
507                                    if location_uri.endswith(playlist.guid):
508                                        rating_key = str(playlist.ratingKey)  # String for backwards consistency
509
510                    # Filter by rating_key
511                    if rating_key_filter and rating_key not in rating_key_filter:
512                        continue
513
514                    sync_id = helpers.get_xml_attr(item, 'id')
515
516                    # Filter by sync_id
517                    if sync_id_filter and str(sync_id_filter) != sync_id:
518                        continue
519
520                    sync_version = helpers.get_xml_attr(item, 'version')
521                    sync_root_title = helpers.get_xml_attr(item, 'rootTitle')
522                    sync_title = helpers.get_xml_attr(item, 'title')
523                    sync_metadata_type = helpers.get_xml_attr(item, 'metadataType')
524                    sync_content_type = helpers.get_xml_attr(item, 'contentType')
525
526                    for status in item.getElementsByTagName('Status'):
527                        status_failure_code = helpers.get_xml_attr(status, 'failureCode')
528                        status_failure = helpers.get_xml_attr(status, 'failure')
529                        status_state = helpers.get_xml_attr(status, 'state')
530                        status_item_count = helpers.get_xml_attr(status, 'itemsCount')
531                        status_item_complete_count = helpers.get_xml_attr(status, 'itemsCompleteCount')
532                        status_item_downloaded_count = helpers.get_xml_attr(status, 'itemsDownloadedCount')
533                        status_item_ready_count = helpers.get_xml_attr(status, 'itemsReadyCount')
534                        status_item_successful_count = helpers.get_xml_attr(status, 'itemsSuccessfulCount')
535                        status_total_size = helpers.get_xml_attr(status, 'totalSize')
536                        status_item_download_percent_complete = helpers.get_percent(
537                            status_item_downloaded_count, status_item_count)
538
539                    for settings in item.getElementsByTagName('MediaSettings'):
540                        settings_video_bitrate = helpers.get_xml_attr(settings, 'maxVideoBitrate')
541                        settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
542                        settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
543                        settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
544                        settings_audio_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
545                        settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
546                        settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
547
548                    sync_details = {"device_name": device_name,
549                                    "platform": device_platform,
550                                    "user_id": device_user_id,
551                                    "user": device_friendly_name,
552                                    "username": device_username,
553                                    "root_title": sync_root_title,
554                                    "sync_title": sync_title,
555                                    "metadata_type": sync_metadata_type,
556                                    "content_type": sync_content_type,
557                                    "rating_key": rating_key,
558                                    "state": status_state,
559                                    "item_count": status_item_count,
560                                    "item_complete_count": status_item_complete_count,
561                                    "item_downloaded_count": status_item_downloaded_count,
562                                    "item_downloaded_percent_complete": status_item_download_percent_complete,
563                                    "video_bitrate": settings_video_bitrate,
564                                    "audio_bitrate": settings_audio_bitrate,
565                                    "photo_quality": settings_photo_quality,
566                                    "video_quality": settings_video_quality,
567                                    "total_size": status_total_size,
568                                    "failure": status_failure,
569                                    "client_id": client_id,
570                                    "sync_id": sync_id,
571                                    "sync_media_type": sync_media_type
572                                    }
573
574                    synced_items.append(sync_details)
575
576        return session.filter_session_info(synced_items, filter_key='user_id')
577
578    def delete_sync(self, client_id, sync_id):
579        logger.info("Tautulli PlexTV :: Deleting sync item '%s'." % sync_id)
580        response = self.delete_plextv_sync(client_id=client_id, sync_id=sync_id)
581        return response.ok
582
583    def get_server_connections(self, pms_identifier='', pms_ip='', pms_port=32400, include_https=True):
584
585        if not pms_identifier:
586            logger.error("Tautulli PlexTV :: Unable to retrieve server connections: no pms_identifier provided.")
587            return {}
588
589        plextv_resources = self.get_plextv_resources(include_https=include_https,
590                                                     output_format='xml')
591        try:
592            xml_head = plextv_resources.getElementsByTagName('Device')
593        except Exception as e:
594            logger.warn("Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s." % e)
595            return {}
596
597        # Function to get all connections for a device
598        def get_connections(device):
599            conn = []
600            connections = device.getElementsByTagName('Connection')
601
602            server = {'pms_identifier': helpers.get_xml_attr(device, 'clientIdentifier'),
603                      'pms_name': helpers.get_xml_attr(device, 'name'),
604                      'pms_version': helpers.get_xml_attr(device, 'productVersion'),
605                      'pms_platform': helpers.get_xml_attr(device, 'platform'),
606                      'pms_presence': helpers.get_xml_attr(device, 'presence'),
607                      'pms_is_cloud': 1 if helpers.get_xml_attr(device, 'platform') == 'Cloud' else 0
608                      }
609
610            for c in connections:
611                server_details = {'protocol': helpers.get_xml_attr(c, 'protocol'),
612                                  'address': helpers.get_xml_attr(c, 'address'),
613                                  'port': helpers.get_xml_attr(c, 'port'),
614                                  'uri': helpers.get_xml_attr(c, 'uri'),
615                                  'local': helpers.get_xml_attr(c, 'local')
616                                  }
617                conn.append(server_details)
618
619            server['connections'] = conn
620            return server
621
622        server = {}
623
624        # Try to match the device
625        for a in xml_head:
626            if helpers.get_xml_attr(a, 'clientIdentifier') == pms_identifier:
627                server = get_connections(a)
628                break
629
630        # Else no device match found
631        if not server:
632            # Try to match the PMS_IP and PMS_PORT
633            for a in xml_head:
634                if helpers.get_xml_attr(a, 'provides') == 'server':
635                    connections = a.getElementsByTagName('Connection')
636
637                    for connection in connections:
638                        if helpers.get_xml_attr(connection, 'address') == pms_ip and \
639                                helpers.get_xml_attr(connection, 'port') == str(pms_port):
640                            server = get_connections(a)
641                            break
642
643                    if server.get('connections'):
644                        break
645
646        return server
647
648    def get_server_times(self):
649        servers = self.get_plextv_server_list(output_format='xml')
650        server_times = {}
651
652        try:
653            xml_head = servers.getElementsByTagName('Server')
654        except Exception as e:
655            logger.warn("Tautulli PlexTV :: Unable to parse XML for get_server_times: %s." % e)
656            return {}
657
658        for a in xml_head:
659            if helpers.get_xml_attr(a, 'machineIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER:
660                server_times = {"created_at": helpers.get_xml_attr(a, 'createdAt'),
661                                "updated_at": helpers.get_xml_attr(a, 'updatedAt'),
662                                "version": helpers.get_xml_attr(a, 'version')
663                                }
664                break
665
666        return server_times
667
668    def discover(self, include_cloud=True, all_servers=False):
669        """ Query plex for all servers online. Returns the ones you own in a selectize format """
670
671        # Try to discover localhost server
672        local_machine_identifier = None
673        request_handler = http_handler.HTTPHandler(urls='http://127.0.0.1:32400', timeout=1,
674                                                   ssl_verify=False, silent=True)
675        request = request_handler.make_request(uri='/identity', request_type='GET', output_format='xml')
676        if request:
677            xml_head = request.getElementsByTagName('MediaContainer')[0]
678            local_machine_identifier = xml_head.getAttribute('machineIdentifier')
679
680        local_server = {'httpsRequired': '0',
681                        'clientIdentifier': local_machine_identifier,
682                        'label': 'Local',
683                        'ip': '127.0.0.1',
684                        'port': '32400',
685                        'uri': 'http://127.0.0.1:32400',
686                        'local': '1',
687                        'value': '127.0.0.1:32400',
688                        'is_cloud': False
689                        }
690
691        servers = self.get_plextv_resources(include_https=True, output_format='xml')
692        clean_servers = []
693
694        try:
695            xml_head = servers.getElementsByTagName('MediaContainer')
696        except Exception as e:
697            logger.warn("Tautulli PlexTV :: Failed to get servers from plex: %s." % e)
698            return []
699
700        for a in xml_head:
701            if a.getAttribute('size'):
702                if a.getAttribute('size') == '0':
703                    return []
704
705            if a.getElementsByTagName('Device'):
706                devices = a.getElementsByTagName('Device')
707
708                for d in devices:
709                    if helpers.get_xml_attr(d, 'presence') == '1' and \
710                            helpers.get_xml_attr(d, 'owned') == '1' and \
711                            helpers.get_xml_attr(d, 'provides') == 'server':
712
713                        is_cloud = (helpers.get_xml_attr(d, 'platform').lower() == 'cloud')
714                        if not include_cloud and is_cloud:
715                            continue
716
717                        connections = d.getElementsByTagName('Connection')
718
719                        for c in connections:
720                            if not all_servers:
721                                # If this is a remote server don't show any local IPs.
722                                if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \
723                                        helpers.get_xml_attr(c, 'local') == '1':
724                                    continue
725
726                                # If this is a local server don't show any remote IPs.
727                                if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \
728                                        helpers.get_xml_attr(c, 'local') == '0':
729                                    continue
730
731                            if helpers.get_xml_attr(d, 'clientIdentifier') == local_machine_identifier:
732                                local_server['httpsRequired'] = helpers.get_xml_attr(d, 'httpsRequired')
733                                local_server['label'] = helpers.get_xml_attr(d, 'name')
734                                clean_servers.append(local_server)
735                                local_machine_identifier = None
736
737                            server = {'httpsRequired': '1' if is_cloud else helpers.get_xml_attr(d, 'httpsRequired'),
738                                      'clientIdentifier': helpers.get_xml_attr(d, 'clientIdentifier'),
739                                      'label': helpers.get_xml_attr(d, 'name'),
740                                      'ip': helpers.get_xml_attr(c, 'address'),
741                                      'port': helpers.get_xml_attr(c, 'port'),
742                                      'uri': helpers.get_xml_attr(c, 'uri'),
743                                      'local': helpers.get_xml_attr(c, 'local'),
744                                      'value': helpers.get_xml_attr(c, 'address') + ':' + helpers.get_xml_attr(c, 'port'),
745                                      'is_cloud': is_cloud
746                                      }
747                            clean_servers.append(server)
748
749            if local_machine_identifier:
750                clean_servers.append(local_server)
751
752        clean_servers.sort(key=lambda s: (s['label'], -int(s['local']), s['ip']))
753
754        return clean_servers
755
756    def get_plex_downloads(self):
757        logger.debug("Tautulli PlexTV :: Retrieving current server version.")
758
759        pms_connect = pmsconnect.PmsConnect()
760        pms_connect.set_server_version()
761
762        update_channel = pms_connect.get_server_update_channel()
763
764        logger.debug("Tautulli PlexTV :: Plex update channel is %s." % update_channel)
765        plex_downloads = self.get_plextv_downloads(plexpass=(update_channel == 'beta'))
766
767        try:
768            available_downloads = json.loads(plex_downloads)
769        except Exception as e:
770            logger.warn("Tautulli PlexTV :: Unable to load JSON for get_plex_updates.")
771            return {}
772
773        # Get the updates for the platform
774        pms_platform = common.PMS_PLATFORM_NAME_OVERRIDES.get(plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM)
775        platform_downloads = available_downloads.get('computer').get(pms_platform) or \
776            available_downloads.get('nas').get(pms_platform)
777
778        if not platform_downloads:
779            logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Could not match server platform: %s."
780                         % pms_platform)
781            return {}
782
783        v_old = helpers.cast_to_int("".join(v.zfill(4) for v in plexpy.CONFIG.PMS_VERSION.split('-')[0].split('.')[:4]))
784        v_new = helpers.cast_to_int("".join(v.zfill(4) for v in platform_downloads.get('version', '').split('-')[0].split('.')[:4]))
785
786        if not v_old:
787            logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Invalid current server version: %s."
788                         % plexpy.CONFIG.PMS_VERSION)
789            return {}
790        if not v_new:
791            logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Invalid new server version: %s."
792                         % platform_downloads.get('version'))
793            return {}
794
795        # Get proper download
796        releases = platform_downloads.get('releases', [{}])
797        release = next((r for r in releases if r['distro'] == plexpy.CONFIG.PMS_UPDATE_DISTRO and
798                        r['build'] == plexpy.CONFIG.PMS_UPDATE_DISTRO_BUILD), releases[0])
799
800        download_info = {'update_available': v_new > v_old,
801                         'platform': platform_downloads.get('name'),
802                         'release_date': platform_downloads.get('release_date'),
803                         'version': platform_downloads.get('version'),
804                         'requirements': platform_downloads.get('requirements'),
805                         'extra_info': platform_downloads.get('extra_info'),
806                         'changelog_added': platform_downloads.get('items_added'),
807                         'changelog_fixed': platform_downloads.get('items_fixed'),
808                         'label': release.get('label'),
809                         'distro': release.get('distro'),
810                         'distro_build': release.get('build'),
811                         'download_url': release.get('url'),
812                         }
813
814        return download_info
815
816    def get_plexpass_status(self):
817        account_data = self.get_plextv_user_details(output_format='xml')
818
819        try:
820            subscription = account_data.getElementsByTagName('subscription')
821        except Exception as e:
822            logger.warn("Tautulli PlexTV :: Unable to parse XML for get_plexpass_status: %s." % e)
823            return False
824
825        if subscription and helpers.get_xml_attr(subscription[0], 'active') == '1':
826            plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 1)
827            plexpy.CONFIG.write()
828            return True
829        else:
830            logger.debug("Tautulli PlexTV :: Plex Pass subscription not found.")
831            plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 0)
832            plexpy.CONFIG.write()
833            return False
834
835    def get_devices_list(self):
836        devices = self.get_plextv_devices_list(output_format='xml')
837
838        try:
839            xml_head = devices.getElementsByTagName('Device')
840        except Exception as e:
841            logger.warn("Tautulli PlexTV :: Unable to parse XML for get_devices_list: %s." % e)
842            return []
843
844        devices_list = []
845        for a in xml_head:
846            device = {"device_name": helpers.get_xml_attr(a, 'name'),
847                      "product": helpers.get_xml_attr(a, 'product'),
848                      "product_version": helpers.get_xml_attr(a, 'productVersion'),
849                      "platform": helpers.get_xml_attr(a, 'platform'),
850                      "platform_version": helpers.get_xml_attr(a, 'platformVersion'),
851                      "device": helpers.get_xml_attr(a, 'device'),
852                      "model": helpers.get_xml_attr(a, 'model'),
853                      "vendor": helpers.get_xml_attr(a, 'vendor'),
854                      "provides": helpers.get_xml_attr(a, 'provides'),
855                      "device_identifier": helpers.get_xml_attr(a, 'clientIdentifier'),
856                      "device_id": helpers.get_xml_attr(a, 'id'),
857                      "token": helpers.get_xml_attr(a, 'token')
858                      }
859            devices_list.append(device)
860
861        return devices_list
862
863    def get_cloud_server_status(self):
864        cloud_status = self.cloud_server_status(output_format='xml')
865
866        try:
867            status_info = cloud_status.getElementsByTagName('info')
868        except Exception as e:
869            logger.warn("Tautulli PlexTV :: Unable to parse XML for get_cloud_server_status: %s." % e)
870            return False
871
872        for info in status_info:
873            servers = info.getElementsByTagName('server')
874            for s in servers:
875                if helpers.get_xml_attr(s, 'address') == plexpy.CONFIG.PMS_IP:
876                    if helpers.get_xml_attr(info, 'running') == '1':
877                        return True
878                    else:
879                        return False
880
881    def get_plex_account_details(self):
882        account_data = self.get_plextv_user_details(output_format='xml')
883
884        try:
885            xml_head = account_data.getElementsByTagName('user')
886        except Exception as e:
887            logger.warn("Tautulli PlexTV :: Unable to parse XML for get_plex_account_details: %s." % e)
888            return None
889
890        for a in xml_head:
891            account_details = {"user_id": helpers.get_xml_attr(a, 'id'),
892                               "username": helpers.get_xml_attr(a, 'username'),
893                               "thumb": helpers.get_xml_attr(a, 'thumb'),
894                               "email": helpers.get_xml_attr(a, 'email'),
895                               "is_home_user": helpers.get_xml_attr(a, 'home'),
896                               "is_restricted": helpers.get_xml_attr(a, 'restricted'),
897                               "filter_all": helpers.get_xml_attr(a, 'filterAll'),
898                               "filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
899                               "filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
900                               "filter_music": helpers.get_xml_attr(a, 'filterMusic'),
901                               "filter_photos": helpers.get_xml_attr(a, 'filterPhotos'),
902                               "user_token": helpers.get_xml_attr(a, 'authToken')
903                               }
904            return account_details
905
906    def get_geoip_lookup(self, ip_address=''):
907        if not ip_address or not helpers.is_valid_ip(ip_address):
908            return
909
910        geoip_data = self.get_plextv_geoip(ip_address=ip_address, output_format='xml')
911
912        try:
913            xml_head = geoip_data.getElementsByTagName('location')
914        except Exception as e:
915            logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_geoip_lookup: %s." % e)
916            return None
917
918        for a in xml_head:
919            coordinates = helpers.get_xml_attr(a, 'coordinates').split(',')
920            latitude = longitude = None
921            if len(coordinates) == 2:
922                latitude, longitude = [helpers.cast_to_float(c) for c in coordinates]
923
924            geo_info = {"code": helpers.get_xml_attr(a, 'code') or None,
925                        "country": helpers.get_xml_attr(a, 'country') or None,
926                        "region": helpers.get_xml_attr(a, 'subdivisions') or None,
927                        "city": helpers.get_xml_attr(a, 'city') or None,
928                        "postal_code": helpers.get_xml_attr(a, 'postal_code') or None,
929                        "timezone": helpers.get_xml_attr(a, 'time_zone') or None,
930                        "latitude": latitude,
931                        "longitude": longitude,
932                        "continent": None,  # keep for backwards compatibility with GeoLite2
933                        "accuracy": None   # keep for backwards compatibility with GeoLite2
934                        }
935
936            return geo_info
937