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 str
20
21import arrow
22import sqlite3
23from xml.dom import minidom
24
25import plexpy
26if plexpy.PYTHON2:
27    import activity_processor
28    import database
29    import helpers
30    import logger
31    import users
32else:
33    from plexpy import activity_processor
34    from plexpy import database
35    from plexpy import helpers
36    from plexpy import logger
37    from plexpy import users
38
39
40def extract_plexivity_xml(xml=None):
41    output = {}
42    clean_xml = helpers.latinToAscii(xml)
43    try:
44        xml_parse = minidom.parseString(clean_xml)
45    except:
46        logger.warn("Tautulli Importer :: Error parsing XML for Plexivity database.")
47        return None
48
49    # I think Plexivity only tracked videos and not music?
50    xml_head = xml_parse.getElementsByTagName('Video')
51    if not xml_head:
52        logger.warn("Tautulli Importer :: Error parsing XML for Plexivity database.")
53        return None
54
55    for a in xml_head:
56        rating_key = helpers.get_xml_attr(a, 'ratingKey')
57        added_at = helpers.get_xml_attr(a, 'addedAt')
58        art = helpers.get_xml_attr(a, 'art')
59        duration = helpers.get_xml_attr(a, 'duration')
60        grandparent_rating_key = helpers.get_xml_attr(a, 'grandparentRatingKey')
61        grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb')
62        grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle')
63        original_title = helpers.get_xml_attr(a, 'originalTitle')
64        guid = helpers.get_xml_attr(a, 'guid')
65        section_id = helpers.get_xml_attr(a, 'librarySectionID')
66        media_index = helpers.get_xml_attr(a, 'index')
67        originally_available_at = helpers.get_xml_attr(a, 'originallyAvailableAt')
68        last_viewed_at = helpers.get_xml_attr(a, 'lastViewedAt')
69        parent_rating_key = helpers.get_xml_attr(a, 'parentRatingKey')
70        parent_media_index = helpers.get_xml_attr(a, 'parentIndex')
71        parent_thumb = helpers.get_xml_attr(a, 'parentThumb')
72        parent_title = helpers.get_xml_attr(a, 'parentTitle')
73        rating = helpers.get_xml_attr(a, 'rating')
74        thumb = helpers.get_xml_attr(a, 'thumb')
75        media_type = helpers.get_xml_attr(a, 'type')
76        updated_at = helpers.get_xml_attr(a, 'updatedAt')
77        view_offset = helpers.get_xml_attr(a, 'viewOffset')
78        year = helpers.get_xml_attr(a, 'year')
79        studio = helpers.get_xml_attr(a, 'studio')
80        title = helpers.get_xml_attr(a, 'title')
81        tagline = helpers.get_xml_attr(a, 'tagline')
82
83        directors = []
84        if a.getElementsByTagName('Director'):
85            director_elem = a.getElementsByTagName('Director')
86            for b in director_elem:
87                directors.append(helpers.get_xml_attr(b, 'tag'))
88
89        aspect_ratio = ''
90        audio_channels = None
91        audio_codec = ''
92        bitrate = None
93        container = ''
94        height = None
95        video_codec = ''
96        video_framerate = ''
97        video_resolution = ''
98        width = None
99
100        if a.getElementsByTagName('Media'):
101            media_elem = a.getElementsByTagName('Media')
102            for c in media_elem:
103                aspect_ratio = helpers.get_xml_attr(c, 'aspectRatio')
104                audio_channels = helpers.get_xml_attr(c, 'audioChannels')
105                audio_codec = helpers.get_xml_attr(c, 'audioCodec')
106                bitrate = helpers.get_xml_attr(c, 'bitrate')
107                container = helpers.get_xml_attr(c, 'container')
108                height = helpers.get_xml_attr(c, 'height')
109                video_codec = helpers.get_xml_attr(c, 'videoCodec')
110                video_framerate = helpers.get_xml_attr(c, 'videoFrameRate')
111                video_resolution = helpers.get_xml_attr(c, 'videoResolution')
112                width = helpers.get_xml_attr(c, 'width')
113
114        ip_address = ''
115        machine_id = ''
116        platform = ''
117        player = ''
118
119        if a.getElementsByTagName('Player'):
120            player_elem = a.getElementsByTagName('Player')
121            for d in player_elem:
122                ip_address = helpers.get_xml_attr(d, 'address').split('::ffff:')[-1]
123                machine_id = helpers.get_xml_attr(d, 'machineIdentifier')
124                platform = helpers.get_xml_attr(d, 'platform')
125                player = helpers.get_xml_attr(d, 'title')
126
127        transcode_audio_channels = None
128        transcode_audio_codec = ''
129        audio_decision = 'direct play'
130        transcode_container = ''
131        transcode_height = None
132        transcode_protocol = ''
133        transcode_video_codec = ''
134        video_decision = 'direct play'
135        transcode_width = None
136
137        if a.getElementsByTagName('TranscodeSession'):
138            transcode_elem = a.getElementsByTagName('TranscodeSession')
139            for e in transcode_elem:
140                transcode_audio_channels = helpers.get_xml_attr(e, 'audioChannels')
141                transcode_audio_codec = helpers.get_xml_attr(e, 'audioCodec')
142                audio_decision = helpers.get_xml_attr(e, 'audioDecision')
143                transcode_container = helpers.get_xml_attr(e, 'container')
144                transcode_height = helpers.get_xml_attr(e, 'height')
145                transcode_protocol = helpers.get_xml_attr(e, 'protocol')
146                transcode_video_codec = helpers.get_xml_attr(e, 'videoCodec')
147                video_decision = helpers.get_xml_attr(e, 'videoDecision')
148                transcode_width = helpers.get_xml_attr(e, 'width')
149
150        # Generate a combined transcode decision value
151        if video_decision == 'transcode' or audio_decision == 'transcode':
152            transcode_decision = 'transcode'
153        elif video_decision == 'copy' or audio_decision == 'copy':
154            transcode_decision = 'copy'
155        else:
156            transcode_decision = 'direct play'
157
158        user_id = None
159
160        if a.getElementsByTagName('User'):
161            user_elem = a.getElementsByTagName('User')
162            for f in user_elem:
163                user_id = helpers.get_xml_attr(f, 'id')
164
165        writers = []
166        if a.getElementsByTagName('Writer'):
167            writer_elem = a.getElementsByTagName('Writer')
168            for g in writer_elem:
169                writers.append(helpers.get_xml_attr(g, 'tag'))
170
171        actors = []
172        if a.getElementsByTagName('Role'):
173            actor_elem = a.getElementsByTagName('Role')
174            for h in actor_elem:
175                actors.append(helpers.get_xml_attr(h, 'tag'))
176
177        genres = []
178        if a.getElementsByTagName('Genre'):
179            genre_elem = a.getElementsByTagName('Genre')
180            for i in genre_elem:
181                genres.append(helpers.get_xml_attr(i, 'tag'))
182
183        labels = []
184        if a.getElementsByTagName('Lables'):
185            label_elem = a.getElementsByTagName('Lables')
186            for i in label_elem:
187                labels.append(helpers.get_xml_attr(i, 'tag'))
188
189        output = {'rating_key': rating_key,
190                  'added_at': added_at,
191                  'art': art,
192                  'duration': duration,
193                  'grandparent_rating_key': grandparent_rating_key,
194                  'grandparent_thumb': grandparent_thumb,
195                  'title': title,
196                  'parent_title': parent_title,
197                  'grandparent_title': grandparent_title,
198                  'original_title': original_title,
199                  'tagline': tagline,
200                  'guid': guid,
201                  'section_id': section_id,
202                  'media_index': media_index,
203                  'originally_available_at': originally_available_at,
204                  'last_viewed_at': last_viewed_at,
205                  'parent_rating_key': parent_rating_key,
206                  'parent_media_index': parent_media_index,
207                  'parent_thumb': parent_thumb,
208                  'rating': rating,
209                  'thumb': thumb,
210                  'media_type': media_type,
211                  'updated_at': updated_at,
212                  'view_offset': view_offset,
213                  'year': year,
214                  'directors': directors,
215                  'aspect_ratio': aspect_ratio,
216                  'audio_channels': audio_channels,
217                  'audio_codec': audio_codec,
218                  'bitrate': bitrate,
219                  'container': container,
220                  'height': height,
221                  'video_codec': video_codec,
222                  'video_framerate': video_framerate,
223                  'video_resolution': video_resolution,
224                  'width': width,
225                  'ip_address': ip_address,
226                  'machine_id': machine_id,
227                  'platform': platform,
228                  'player': player,
229                  'transcode_audio_channels': transcode_audio_channels,
230                  'transcode_audio_codec': transcode_audio_codec,
231                  'audio_decision': audio_decision,
232                  'transcode_container': transcode_container,
233                  'transcode_height': transcode_height,
234                  'transcode_protocol': transcode_protocol,
235                  'transcode_video_codec': transcode_video_codec,
236                  'video_decision': video_decision,
237                  'transcode_width': transcode_width,
238                  'transcode_decision': transcode_decision,
239                  'user_id': user_id,
240                  'writers': writers,
241                  'actors': actors,
242                  'genres': genres,
243                  'studio': studio,
244                  'labels': labels
245                  }
246
247    return output
248
249
250def validate_database(database_file=None, table_name=None):
251    try:
252        connection = sqlite3.connect(database_file, timeout=20)
253    except sqlite3.OperationalError:
254        logger.error("Tautulli Importer :: Invalid database specified.")
255        return 'Invalid database specified.'
256    except ValueError:
257        logger.error("Tautulli Importer :: Invalid database specified.")
258        return 'Invalid database specified.'
259    except:
260        logger.error("Tautulli Importer :: Uncaught exception.")
261        return 'Uncaught exception.'
262
263    try:
264        connection.execute('SELECT xml from %s' % table_name)
265        connection.close()
266    except sqlite3.OperationalError:
267        logger.error("Tautulli Importer :: Invalid database specified.")
268        return 'Invalid database specified.'
269    except:
270        logger.error("Tautulli Importer :: Uncaught exception.")
271        return 'Uncaught exception.'
272
273    return 'success'
274
275
276def import_from_plexivity(database_file=None, table_name=None, import_ignore_interval=0):
277
278    try:
279        connection = sqlite3.connect(database_file, timeout=20)
280        connection.row_factory = sqlite3.Row
281    except sqlite3.OperationalError:
282        logger.error("Tautulli Importer :: Invalid filename.")
283        return None
284    except ValueError:
285        logger.error("Tautulli Importer :: Invalid filename.")
286        return None
287
288    try:
289        connection.execute('SELECT xml from %s' % table_name)
290    except sqlite3.OperationalError:
291        logger.error("Tautulli Importer :: Database specified does not contain the required fields.")
292        return None
293
294    logger.debug("Tautulli Importer :: Plexivity data import in progress...")
295    database.set_is_importing(True)
296
297    ap = activity_processor.ActivityProcessor()
298    user_data = users.Users()
299
300    # Get the latest friends list so we can pull user id's
301    try:
302        users.refresh_users()
303    except:
304        logger.debug("Tautulli Importer :: Unable to refresh the users list. Aborting import.")
305        return None
306
307    query = 'SELECT id AS id, ' \
308            'time AS started, ' \
309            'stopped, ' \
310            'null AS user_id, ' \
311            'user, ' \
312            'ip_address, ' \
313            'paused_counter, ' \
314            'platform AS player, ' \
315            'null AS platform, ' \
316            'null as machine_id, ' \
317            'null AS media_type, ' \
318            'null AS view_offset, ' \
319            'xml, ' \
320            'rating as content_rating,' \
321            'summary,' \
322            'title AS full_title,' \
323            '(case when orig_title_ep = "n/a" then orig_title else ' \
324            'orig_title_ep end) as title,' \
325            '(case when orig_title_ep != "n/a" then orig_title else ' \
326            'null end) as grandparent_title ' \
327            'FROM ' + table_name + ' ORDER BY id'
328
329    result = connection.execute(query)
330
331    for row in result:
332        # Extract the xml from the Plexivity db xml field.
333        extracted_xml = extract_plexivity_xml(row['xml'])
334
335        # If we get back None from our xml extractor skip over the record and log error.
336        if not extracted_xml:
337            logger.error("Tautulli Importer :: Skipping record with id %s due to malformed xml."
338                         % str(row['id']))
339            continue
340
341        # Skip line if we don't have a ratingKey to work with
342        #if not row['rating_key']:
343        #    logger.error("Tautulli Importer :: Skipping record due to null ratingKey.")
344        #    continue
345
346        # If the user_id no longer exists in the friends list, pull it from the xml.
347        if user_data.get_user_id(user=row['user']):
348            user_id = user_data.get_user_id(user=row['user'])
349        else:
350            user_id = extracted_xml['user_id']
351
352        session_history = {'started': arrow.get(row['started']).timestamp(),
353                           'stopped': arrow.get(row['stopped']).timestamp(),
354                           'rating_key': extracted_xml['rating_key'],
355                           'title': row['title'],
356                           'parent_title': extracted_xml['parent_title'],
357                           'grandparent_title': row['grandparent_title'],
358                           'original_title': extracted_xml['original_title'],
359                           'full_title': row['full_title'],
360                           'user_id': user_id,
361                           'user': row['user'],
362                           'ip_address': row['ip_address'] if row['ip_address'] else extracted_xml['ip_address'],
363                           'paused_counter': row['paused_counter'],
364                           'player': row['player'],
365                           'platform': extracted_xml['platform'],
366                           'machine_id': extracted_xml['machine_id'],
367                           'parent_rating_key': extracted_xml['parent_rating_key'],
368                           'grandparent_rating_key': extracted_xml['grandparent_rating_key'],
369                           'media_type': extracted_xml['media_type'],
370                           'view_offset': extracted_xml['view_offset'],
371                           'section_id': extracted_xml['section_id'],
372                           'video_decision': extracted_xml['video_decision'],
373                           'audio_decision': extracted_xml['audio_decision'],
374                           'transcode_decision': extracted_xml['transcode_decision'],
375                           'duration': extracted_xml['duration'],
376                           'width': extracted_xml['width'],
377                           'height': extracted_xml['height'],
378                           'container': extracted_xml['container'],
379                           'video_codec': extracted_xml['video_codec'],
380                           'audio_codec': extracted_xml['audio_codec'],
381                           'bitrate': extracted_xml['bitrate'],
382                           'video_resolution': extracted_xml['video_resolution'],
383                           'video_framerate': extracted_xml['video_framerate'],
384                           'aspect_ratio': extracted_xml['aspect_ratio'],
385                           'audio_channels': extracted_xml['audio_channels'],
386                           'transcode_protocol': extracted_xml['transcode_protocol'],
387                           'transcode_container': extracted_xml['transcode_container'],
388                           'transcode_video_codec': extracted_xml['transcode_video_codec'],
389                           'transcode_audio_codec': extracted_xml['transcode_audio_codec'],
390                           'transcode_audio_channels': extracted_xml['transcode_audio_channels'],
391                           'transcode_width': extracted_xml['transcode_width'],
392                           'transcode_height': extracted_xml['transcode_height']
393                           }
394
395        session_history_metadata = {'rating_key': extracted_xml['rating_key'],
396                                    'parent_rating_key': extracted_xml['parent_rating_key'],
397                                    'grandparent_rating_key': extracted_xml['grandparent_rating_key'],
398                                    'title': row['title'],
399                                    'parent_title': extracted_xml['parent_title'],
400                                    'grandparent_title': row['grandparent_title'],
401                                    'original_title': extracted_xml['original_title'],
402                                    'media_index': extracted_xml['media_index'],
403                                    'parent_media_index': extracted_xml['parent_media_index'],
404                                    'thumb': extracted_xml['thumb'],
405                                    'parent_thumb': extracted_xml['parent_thumb'],
406                                    'grandparent_thumb': extracted_xml['grandparent_thumb'],
407                                    'art': extracted_xml['art'],
408                                    'media_type': extracted_xml['media_type'],
409                                    'year': extracted_xml['year'],
410                                    'originally_available_at': extracted_xml['originally_available_at'],
411                                    'added_at': extracted_xml['added_at'],
412                                    'updated_at': extracted_xml['updated_at'],
413                                    'last_viewed_at': extracted_xml['last_viewed_at'],
414                                    'content_rating': row['content_rating'],
415                                    'summary': row['summary'],
416                                    'tagline': extracted_xml['tagline'],
417                                    'rating': extracted_xml['rating'],
418                                    'duration': extracted_xml['duration'],
419                                    'guid': extracted_xml['guid'],
420                                    'directors': extracted_xml['directors'],
421                                    'writers': extracted_xml['writers'],
422                                    'actors': extracted_xml['actors'],
423                                    'genres': extracted_xml['genres'],
424                                    'studio': extracted_xml['studio'],
425                                    'labels': extracted_xml['labels'],
426                                    'full_title': row['full_title'],
427                                    'width': extracted_xml['width'],
428                                    'height': extracted_xml['height'],
429                                    'container': extracted_xml['container'],
430                                    'video_codec': extracted_xml['video_codec'],
431                                    'audio_codec': extracted_xml['audio_codec'],
432                                    'bitrate': extracted_xml['bitrate'],
433                                    'video_resolution': extracted_xml['video_resolution'],
434                                    'video_framerate': extracted_xml['video_framerate'],
435                                    'aspect_ratio': extracted_xml['aspect_ratio'],
436                                    'audio_channels': extracted_xml['audio_channels']
437                                    }
438
439        # On older versions of PMS, "clip" items were still classified as "movie" and had bad ratingKey values
440        # Just make sure that the ratingKey is indeed an integer
441        if session_history_metadata['rating_key'].isdigit():
442            ap.write_session_history(session=session_history,
443                                     import_metadata=session_history_metadata,
444                                     is_import=True,
445                                     import_ignore_interval=import_ignore_interval)
446        else:
447            logger.debug("Tautulli Importer :: Item has bad rating_key: %s" % session_history_metadata['rating_key'])
448
449    import_users()
450
451    logger.debug("Tautulli Importer :: Plexivity data import complete.")
452    database.set_is_importing(False)
453
454
455def import_users():
456    logger.debug("Tautulli Importer :: Importing Plexivity Users...")
457    monitor_db = database.MonitorDatabase()
458
459    query = 'INSERT OR IGNORE INTO users (user_id, username) ' \
460            'SELECT user_id, user ' \
461            'FROM session_history WHERE user_id != 1 GROUP BY user_id'
462
463    try:
464        monitor_db.action(query)
465        logger.debug("Tautulli Importer :: Users imported.")
466    except:
467        logger.debug("Tautulli Importer :: Failed to import users.")
468