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