#  This file is part of Tautulli.
#
#  Tautulli is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Tautulli is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with Tautulli.  If not, see <http://www.gnu.org/licenses/>.


import arrow
import bleach
from collections import Counter, defaultdict
import hashlib
from itertools import groupby
import json
from operator import itemgetter
import os
import re
from string import Formatter
import threading
import time

import plexpy
import activity_processor
import common
import database
import datafactory
import libraries
import logger
import helpers
import notifiers
import plextv
import pmsconnect
import request
import users
from newsletter_handler import notify as notify_newsletter


def process_queue():
    queue = plexpy.NOTIFY_QUEUE
    while True:
        params = queue.get()
        
        if params is None:
            break
        elif params:
            try:
                if 'newsletter' in params:
                    notify_newsletter(**params)
                elif 'notification' in params:
                    notify(**params)
                else:
                    add_notifier_each(**params)
            except Exception as e:
                logger.exception(u"Tautulli NotificationHandler :: Notification thread exception: %s" % e)
                
        queue.task_done()

    logger.info(u"Tautulli NotificationHandler :: Notification thread exiting...")


def start_threads(num_threads=1):
    logger.info(u"Tautulli NotificationHandler :: Starting background notification handler ({} threads).".format(num_threads))
    for x in range(num_threads):
        thread = threading.Thread(target=process_queue)
        thread.daemon = True
        thread.start()


def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, manual_trigger=False, **kwargs):
    if not notify_action:
        logger.debug(u"Tautulli NotificationHandler :: Notify called but no action received.")
        return

    if notifier_id:
        # Send to a specific notifier regardless if it is enabled
        notifiers_enabled = notifiers.get_notifiers(notifier_id=notifier_id)
    else:
        # Check if any notification agents have notifications enabled for the action
        notifiers_enabled = notifiers.get_notifiers(notify_action=notify_action)

    if notifiers_enabled and not manual_trigger:
        # Check if notification conditions are satisfied
        conditions = notify_conditions(notify_action=notify_action,
                                       stream_data=stream_data,
                                       timeline_data=timeline_data)
    else:
        conditions = True

    if notifiers_enabled and (manual_trigger or conditions):
        if stream_data or timeline_data:
            # Build the notification parameters
            parameters = build_media_notify_params(notify_action=notify_action,
                                                   session=stream_data,
                                                   timeline=timeline_data,
                                                   manual_trigger=manual_trigger,
                                                   **kwargs)
        else:
            # Build the notification parameters
            parameters = build_server_notify_params(notify_action=notify_action,
                                                    **kwargs)

        if not parameters:
            logger.error(u"Tautulli NotificationHandler :: Failed to build notification parameters.")
            return

        for notifier in notifiers_enabled:
            # Check custom user conditions
            if manual_trigger or notify_custom_conditions(notifier_id=notifier['id'], parameters=parameters):
                # Add each notifier to the queue
                data = {'notification': True,
                        'notifier_id': notifier['id'],
                        'notify_action': notify_action,
                        'stream_data': stream_data,
                        'timeline_data': timeline_data,
                        'parameters': parameters}
                data.update(kwargs)
                plexpy.NOTIFY_QUEUE.put(data)
            else:
                logger.debug(u"Tautulli NotificationHandler :: Custom notification conditions not satisfied, skipping notifier_id %s." % notifier['id'])

    # Add on_concurrent and on_newdevice to queue if action is on_play
    if notify_action == 'on_play':
        plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_concurrent'})
        plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_newdevice'})


def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
    # Activity notifications
    if stream_data:

        # Check if notifications enabled for user and library
        # user_data = users.Users()
        # user_details = user_data.get_details(user_id=stream_data['user_id'])
        #
        # library_data = libraries.Libraries()
        # library_details = library_data.get_details(section_id=stream_data['section_id'])

        # if not user_details['do_notify']:
        #     logger.debug(u"Tautulli NotificationHandler :: Notifications for user '%s' are disabled." % user_details['username'])
        #     return False
        #
        # elif not library_details['do_notify'] and notify_action not in ('on_concurrent', 'on_newdevice'):
        #     logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' are disabled." % library_details['section_name'])
        #     return False

        if notify_action == 'on_concurrent':
            pms_connect = pmsconnect.PmsConnect()
            result = pms_connect.get_current_activity()

            user_sessions = []
            if result:
                user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']]

            if plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP:
                return len(Counter(s['ip_address'] for s in user_sessions)) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
            else:
                return len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD

        elif notify_action == 'on_newdevice':
            data_factory = datafactory.DataFactory()
            user_devices = data_factory.get_user_devices(user_id=stream_data['user_id'])
            return stream_data['machine_id'] not in user_devices

        elif stream_data['media_type'] == 'movie' or stream_data['media_type'] == 'episode':
            progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
            
            if notify_action == 'on_stop':
                return (plexpy.CONFIG.NOTIFY_CONSECUTIVE or
                    (stream_data['media_type'] == 'movie' and progress_percent < plexpy.CONFIG.MOVIE_WATCHED_PERCENT) or 
                    (stream_data['media_type'] == 'episode' and progress_percent < plexpy.CONFIG.TV_WATCHED_PERCENT))
            
            elif notify_action == 'on_resume':
                return plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99

            # All other activity notify actions
            else:
                return True

        elif stream_data['media_type'] == 'track':
            return True

        else:
            return False

    # Recently Added notifications
    elif timeline_data:

        # Check if notifications enabled for library
        # library_data = libraries.Libraries()
        # library_details = library_data.get_details(section_id=timeline_data['section_id'])
        #
        # if not library_details['do_notify_created']:
        #     # logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' is disabled." % library_details['section_name'])
        #     return False

        return True

    # Server notifications
    else:
        return True


def notify_custom_conditions(notifier_id=None, parameters=None):
    notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)

    custom_conditions_logic = notifier_config['custom_conditions_logic']
    custom_conditions = notifier_config['custom_conditions']

    if custom_conditions_logic or any(c for c in custom_conditions if c['value']):
        logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s."
                     % notifier_id)

        logic_groups = None
        if custom_conditions_logic:
            try:
                # Parse and validate the custom conditions logic
                logic_groups = helpers.parse_condition_logic_string(custom_conditions_logic, len(custom_conditions))
            except ValueError as e:
                logger.error(u"Tautulli NotificationHandler :: Unable to parse custom condition logic '%s': %s."
                             % (custom_conditions_logic, e))
                return False

        evaluated_conditions = [None]  # Set condition {0} to None

        for condition in custom_conditions:
            parameter = condition['parameter']
            operator = condition['operator']
            values = condition['value']
            parameter_type = condition['type']
            parameter_value = parameters.get(parameter, "")

            # Set blank conditions to True (skip)
            if not parameter or not operator or not values:
                evaluated_conditions.append(True)
                continue

            # Make sure the condition values is in a list
            if isinstance(values, basestring):
                values = [values]

            # Cast the condition values to the correct type
            try:
                if parameter_type == 'str':
                    values = [unicode(v).lower() for v in values]

                elif parameter_type == 'int':
                    values = [helpers.cast_to_int(v) for v in values]

                elif parameter_type == 'float':
                    values = [helpers.cast_to_float(v) for v in values]
            
            except ValueError as e:
                logger.error(u"Tautulli NotificationHandler :: Unable to cast condition '%s', values '%s', to type '%s'."
                             % (parameter, values, parameter_type))
                return False

            # Cast the parameter value to the correct type
            try:
                if parameter_type == 'str':
                    parameter_value = unicode(parameter_value).lower()

                elif parameter_type == 'int':
                    parameter_value = helpers.cast_to_int(parameter_value)

                elif parameter_type == 'float':
                    parameter_value = helpers.cast_to_float(parameter_value)

            except ValueError as e:
                logger.error(u"Tautulli NotificationHandler :: Unable to cast parameter '%s', value '%s', to type '%s'."
                             % (parameter, parameter_value, parameter_type))
                return False

            # Check each condition
            if operator == 'contains':
                evaluated_conditions.append(any(c in parameter_value for c in values))

            elif operator == 'does not contain':
                evaluated_conditions.append(all(c not in parameter_value for c in values))

            elif operator == 'is':
                evaluated_conditions.append(any(parameter_value == c for c in values))

            elif operator == 'is not':
                evaluated_conditions.append(all(parameter_value != c for c in values))

            elif operator == 'begins with':
                evaluated_conditions.append(parameter_value.startswith(tuple(values)))

            elif operator == 'ends with':
                evaluated_conditions.append(parameter_value.endswith(tuple(values)))

            elif operator == 'is greater than':
                evaluated_conditions.append(any(parameter_value > c for c in values))

            elif operator == 'is less than':
                evaluated_conditions.append(any(parameter_value < c for c in values))

            else:
                logger.warn(u"Tautulli NotificationHandler :: Invalid condition operator '%s'." % operator)
                evaluated_conditions.append(None)

        if logic_groups:
            # Format and evaluate the logic string
            try:
                evaluated_logic = helpers.eval_logic_groups_to_bool(logic_groups, evaluated_conditions)
            except Exception as e:
                logger.error(u"Tautulli NotificationHandler :: Unable to evaluate custom condition logic: %s." % e)
                return False
        else:
            evaluated_logic = all(evaluated_conditions[1:])

        logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic))
        return evaluated_logic

    return True


def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs):
    logger.info(u"Tautulli NotificationHandler :: Preparing notifications for notifier_id %s." % notifier_id)

    notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)

    if not notifier_config:
        return

    if notify_action in ('test', 'api'):
        subject = kwargs.pop('subject', 'Tautulli')
        body = kwargs.pop('body', 'Test Notification')
        script_args = kwargs.pop('script_args', [])
    else:
        # Get the subject and body strings
        subject_string = notifier_config['notify_text'][notify_action]['subject']
        body_string = notifier_config['notify_text'][notify_action]['body']

        # Format the subject and body strings
        subject, body, script_args = build_notify_text(subject=subject_string,
                                                       body=body_string,
                                                       notify_action=notify_action,
                                                       parameters=parameters,
                                                       agent_id=notifier_config['agent_id'])

    # Set the notification state in the db
    notification_id = set_notify_state(session=stream_data or timeline_data,
                                       notifier=notifier_config,
                                       notify_action=notify_action,
                                       subject=subject,
                                       body=body,
                                       script_args=script_args)

    # Send the notification
    success = notifiers.send_notification(notifier_id=notifier_config['id'],
                                          subject=subject,
                                          body=body,
                                          script_args=script_args,
                                          notify_action=notify_action,
                                          notification_id=notification_id,
                                          parameters=parameters or {},
                                          **kwargs)

    if success:
        set_notify_success(notification_id)
        return True


def get_notify_state(session):
    monitor_db = database.MonitorDatabase()
    result = monitor_db.select('SELECT timestamp, notify_action, notifier_id '
                               'FROM notify_log '
                               'WHERE session_key = ? '
                               'AND rating_key = ? '
                               'AND user_id = ? '
                               'ORDER BY id DESC',
                               args=[session['session_key'], session['rating_key'], session['user_id']])
    notify_states = []
    for item in result:
        notify_state = {'timestamp': item['timestamp'],
                        'notify_action': item['notify_action'],
                        'notifier_id': item['notifier_id']}
        notify_states.append(notify_state)

    return notify_states


def get_notify_state_enabled(session, notify_action, notified=True):
    if notified:
        timestamp_where = 'AND timestamp IS NOT NULL'
    else:
        timestamp_where = 'AND timestamp IS NULL'

    monitor_db = database.MonitorDatabase()
    result = monitor_db.select('SELECT id AS notifier_id, timestamp '
                               'FROM notifiers '
                               'LEFT OUTER JOIN ('
                               'SELECT timestamp, notifier_id '
                               'FROM notify_log '
                               'WHERE session_key = ? '
                               'AND rating_key = ? '
                               'AND user_id = ? '
                               'AND notify_action = ?) AS t ON notifiers.id = t.notifier_id '
                               'WHERE %s = 1 %s' % (notify_action, timestamp_where),
                               args=[session['session_key'], session['rating_key'], session['user_id'], notify_action])

    return result


def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):

    if notifier and notify_action:
        monitor_db = database.MonitorDatabase()

        session = session or {}

        script_args = json.dumps(script_args) if script_args else None

        keys = {'timestamp': int(time.time()),
                'session_key': session.get('session_key', None),
                'rating_key': session.get('rating_key', None),
                'user_id': session.get('user_id', None),
                'notifier_id': notifier['id'],
                'agent_id': notifier['agent_id'],
                'notify_action': notify_action}

        values = {'parent_rating_key': session.get('parent_rating_key', None),
                  'grandparent_rating_key': session.get('grandparent_rating_key', None),
                  'user': session.get('user', None),
                  'agent_name': notifier['agent_name'],
                  'subject_text': subject,
                  'body_text': body,
                  'script_args': script_args}

        monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
        return monitor_db.last_insert_id()
    else:
        logger.error(u"Tautulli NotificationHandler :: Unable to set notify state.")


def set_notify_success(notification_id):
    keys = {'id': notification_id}
    values = {'success': 1}

    monitor_db = database.MonitorDatabase()
    monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)


def build_media_notify_params(notify_action=None, session=None, timeline=None, manual_trigger=False, **kwargs):
    # Get time formats
    date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
    time_format = helpers.momentjs_to_arrow(plexpy.CONFIG.TIME_FORMAT)
    duration_format = helpers.momentjs_to_arrow(plexpy.CONFIG.TIME_FORMAT, duration=True)

    # Get metadata for the item
    if session:
        rating_key = session['rating_key']
    elif timeline:
        rating_key = timeline['rating_key']

    notify_params = defaultdict(str)
    if session:
        # Reload json from raw stream info
        if session.get('raw_stream_info'):
            session.update(json.loads(session['raw_stream_info']))
        notify_params.update(session)

    if timeline:
        notify_params.update(timeline)

    ## TODO: Check list of media info items, currently only grabs first item
    media_info = media_part_info = {}
    if 'media_info' in notify_params and len(notify_params['media_info']) > 0:
        media_info = notify_params['media_info'][0]
        if 'parts' in media_info and len(media_info['parts']) > 0:
            media_part_info = media_info.pop('parts')[0]

    stream_video = stream_audio = stream_subtitle = False
    if 'streams' in media_part_info:
        for stream in media_part_info.pop('streams'):
            if not stream_video and stream['type'] == '1':
                media_part_info.update(stream)
                stream_video = True
            if not stream_audio and stream['type'] == '2':
                media_part_info.update(stream)
                stream_audio = True
            if not stream_subtitle and stream['type'] == '3':
                media_part_info.update(stream)
                stream_subtitle = True

    notify_params.update(media_info)
    notify_params.update(media_part_info)

    child_metadata = grandchild_metadata = []
    for key in kwargs.pop('child_keys', []):
        child_metadata.append(pmsconnect.PmsConnect().get_metadata_details(rating_key=key))
    for key in kwargs.pop('grandchild_keys', []):
        grandchild_metadata.append(pmsconnect.PmsConnect().get_metadata_details(rating_key=key))

    # Session values
    session = session or {}

    ap = activity_processor.ActivityProcessor()
    sessions = ap.get_sessions()
    stream_count = len(sessions)
    user_sessions = ap.get_sessions(user_id=session.get('user_id'))
    user_stream_count = len(user_sessions)

    # Generate a combined transcode decision value
    if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision', '') == 'transcode':
        transcode_decision = 'Transcode'
    elif session.get('stream_video_decision', '') == 'copy' or session.get('stream_audio_decision', '') == 'copy':
        transcode_decision = 'Direct Stream'
    else:
        transcode_decision = 'Direct Play'
    
    if notify_action != 'play':
        stream_duration = int((time.time() -
                               helpers.cast_to_int(session.get('started', 0)) -
                               helpers.cast_to_int(session.get('paused_counter', 0))) / 60)
    else:
        stream_duration = 0

    view_offset = helpers.convert_milliseconds_to_minutes(session.get('view_offset', 0))
    duration = helpers.convert_milliseconds_to_minutes(notify_params['duration'])
    remaining_duration = duration - view_offset

    # Build Plex URL
    if notify_params['media_type'] == 'track':
        plex_web_rating_key = notify_params['parent_rating_key']
    else:
        plex_web_rating_key = notify_params['rating_key']

    notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
        web_url=plexpy.CONFIG.PMS_WEB_URL,
        pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
        rating_key=plex_web_rating_key)

    # Get media IDs from guid and build URLs
    if 'imdb://' in notify_params['guid']:
        notify_params['imdb_id'] = notify_params['guid'].split('imdb://')[1].split('?')[0]
        notify_params['imdb_url'] = 'https://www.imdb.com/title/' + notify_params['imdb_id']
        notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']

    if 'thetvdb://' in notify_params['guid']:
        notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0]
        notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
        notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'

    elif 'thetvdbdvdorder://' in notify_params['guid']:
        notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0].split('?')[0]
        notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
        notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'

    if 'themoviedb://' in notify_params['guid']:
        if notify_params['media_type'] == 'movie':
            notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('?')[0]
            notify_params['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + notify_params['themoviedb_id']
            notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=movie'

        elif notify_params['media_type'] in ('show', 'season', 'episode'):
            notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('/')[0].split('?')[0]
            notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id']
            notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=show'

    if 'lastfm://' in notify_params['guid']:
        notify_params['lastfm_id'] = notify_params['guid'].split('lastfm://')[1].rsplit('/', 1)[0]
        notify_params['lastfm_url'] = 'https://www.last.fm/music/' + notify_params['lastfm_id']

    # Get TheMovieDB info
    if plexpy.CONFIG.THEMOVIEDB_LOOKUP:
        if notify_params.get('themoviedb_id'):
            themoveidb_json = get_themoviedb_info(rating_key=rating_key,
                                                  media_type=notify_params['media_type'],
                                                  themoviedb_id=notify_params['themoviedb_id'])

            if themoveidb_json.get('imdb_id'):
                notify_params['imdb_id'] = themoveidb_json['imdb_id']
                notify_params['imdb_url'] = 'https://www.imdb.com/title/' + themoveidb_json['imdb_id']

        elif notify_params.get('thetvdb_id') or notify_params.get('imdb_id'):
            if notify_params['media_type'] in ('episode', 'track'):
                lookup_key = notify_params['grandparent_rating_key']
            elif notify_params['media_type'] in ('season', 'album'):
                lookup_key = notify_params['parent_rating_key']
            else:
                lookup_key = rating_key

            themoviedb_info = lookup_themoviedb_by_id(rating_key=lookup_key,
                                                      thetvdb_id=notify_params.get('thetvdb_id'),
                                                      imdb_id=notify_params.get('imdb_id'))
            notify_params.update(themoviedb_info)

    # Get TVmaze info (for tv shows only)
    if plexpy.CONFIG.TVMAZE_LOOKUP:
        if notify_params['media_type'] in ('show', 'season', 'episode') and (notify_params.get('thetvdb_id') or notify_params.get('imdb_id')):
            if notify_params['media_type'] in ('episode', 'track'):
                lookup_key = notify_params['grandparent_rating_key']
            elif notify_params['media_type'] in ('season', 'album'):
                lookup_key = notify_params['parent_rating_key']
            else:
                lookup_key = rating_key

            tvmaze_info = lookup_tvmaze_by_id(rating_key=lookup_key,
                                              thetvdb_id=notify_params.get('thetvdb_id'),
                                              imdb_id=notify_params.get('imdb_id'))
            notify_params.update(tvmaze_info)

            if tvmaze_info.get('thetvdb_id'):
                notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + str(tvmaze_info['thetvdb_id'])
            if tvmaze_info.get('imdb_id'):
                notify_params['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id']

    if notify_params['media_type'] in ('movie', 'show', 'artist'):
        poster_thumb = notify_params['thumb']
        poster_key = notify_params['rating_key']
        poster_title = notify_params['title']
    elif notify_params['media_type'] in ('season', 'album'):
        poster_thumb = notify_params['thumb'] or notify_params['parent_thumb']
        poster_key = notify_params['rating_key']
        poster_title = '%s - %s' % (notify_params['parent_title'],
                                    notify_params['title'])
    elif notify_params['media_type'] in ('episode', 'track'):
        poster_thumb = notify_params['parent_thumb'] or notify_params['grandparent_thumb']
        poster_key = notify_params['parent_rating_key']
        poster_title = '%s - %s' % (notify_params['grandparent_title'],
                                    notify_params['parent_title'])
    else:
        poster_thumb = ''

    if plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS in (1, 3):
        img_info = get_img_info(img=poster_thumb, rating_key=poster_key, title=poster_title, fallback='poster')
        poster_info = {'poster_title': img_info['img_title'], 'poster_url': img_info['img_url']}
        notify_params.update(poster_info)
    elif plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS == 2 and plexpy.CONFIG.HTTP_BASE_URL:
        img_hash = set_hash_image_info(img=poster_thumb, fallback='poster')
        poster_info = {'poster_title': poster_title,
                       'poster_url': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/' + img_hash}
        notify_params.update(poster_info)

    if ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT)
        and notify_params['media_type'] in ('show', 'artist')):
        show_name = notify_params['title']
        episode_name = ''
        artist_name = notify_params['title']
        album_name = ''
        track_name = ''

        child_num = [helpers.cast_to_int(
            d['media_index']) for d in child_metadata if d['parent_rating_key'] == rating_key]
        num, num00 = format_group_index(child_num)
        season_num, season_num00 = num, num00

        episode_num, episode_num00 = '', ''
        track_num, track_num00 = '', ''

        child_count = len(child_num)
        grandchild_count = ''

    elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
          and notify_params['media_type'] in ('season', 'album')):
        show_name = notify_params['parent_title']
        episode_name = ''
        artist_name = notify_params['parent_title']
        album_name = notify_params['title']
        track_name = ''

        season_num = str(notify_params['media_index']).zfill(1)
        season_num00 = str(notify_params['media_index']).zfill(2)

        grandchild_num = [helpers.cast_to_int(
            d['media_index']) for d in child_metadata if d['parent_rating_key'] == rating_key]
        num, num00 = format_group_index(grandchild_num)
        episode_num, episode_num00 = num, num00
        track_num, track_num00 = num, num00

        child_count = 1
        grandchild_count = len(grandchild_num)

    else:
        show_name = notify_params['grandparent_title']
        episode_name = notify_params['title']
        artist_name = notify_params['grandparent_title']
        album_name = notify_params['parent_title']
        track_name = notify_params['title']
        season_num = str(notify_params['parent_media_index']).zfill(1)
        season_num00 = str(notify_params['parent_media_index']).zfill(2)
        episode_num = str(notify_params['media_index']).zfill(1)
        episode_num00 = str(notify_params['media_index']).zfill(2)
        track_num = str(notify_params['media_index']).zfill(1)
        track_num00 = str(notify_params['media_index']).zfill(2)
        child_count = 1
        grandchild_count = 1

    available_params = {
        # Global paramaters
        'tautulli_version': common.RELEASE,
        'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
        'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
        'tautulli_commit': plexpy.CURRENT_VERSION,
        'server_name': plexpy.CONFIG.PMS_NAME,
        'server_ip': plexpy.CONFIG.PMS_IP,
        'server_port': plexpy.CONFIG.PMS_PORT,
        'server_url': plexpy.CONFIG.PMS_URL,
        'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
        'server_platform': plexpy.CONFIG.PMS_PLATFORM,
        'server_version': plexpy.CONFIG.PMS_VERSION,
        'action': notify_action.split('on_')[-1],
        'week_number': arrow.now().isocalendar()[1],
        'datestamp': arrow.now().format(date_format),
        'timestamp': arrow.now().format(time_format),
        'unixtime': int(time.time()),
        # Stream parameters
        'streams': stream_count,
        'user_streams': user_stream_count,
        'user': notify_params['friendly_name'],
        'username': notify_params['user'],
        'device': notify_params['device'],
        'platform': notify_params['platform'],
        'product': notify_params['product'],
        'player': notify_params['player'],
        'ip_address': notify_params.get('ip_address', 'N/A'),
        'stream_duration': stream_duration,
        'stream_time': arrow.get(stream_duration * 60).format(duration_format),
        'remaining_duration': remaining_duration,
        'remaining_time': arrow.get(remaining_duration * 60).format(duration_format),
        'progress_duration': view_offset,
        'progress_time': arrow.get(view_offset * 60).format(duration_format),
        'progress_percent': helpers.get_percent(view_offset, duration),
        'transcode_decision': transcode_decision,
        'video_decision': notify_params['video_decision'],
        'audio_decision': notify_params['audio_decision'],
        'subtitle_decision': notify_params['subtitle_decision'],
        'quality_profile': notify_params['quality_profile'],
        'optimized_version': notify_params['optimized_version'],
        'optimized_version_profile': notify_params['optimized_version_profile'],
        'synced_version': notify_params['synced_version'],
        'stream_local': notify_params['local'],
        'stream_location': notify_params['location'],
        'stream_bandwidth': notify_params['bandwidth'],
        'stream_container': notify_params['stream_container'],
        'stream_bitrate': notify_params['stream_bitrate'],
        'stream_aspect_ratio': notify_params['stream_aspect_ratio'],
        'stream_video_codec': notify_params['stream_video_codec'],
        'stream_video_codec_level': notify_params['stream_video_codec_level'],
        'stream_video_bitrate': notify_params['stream_video_bitrate'],
        'stream_video_bit_depth': notify_params['stream_video_bit_depth'],
        'stream_video_framerate': notify_params['stream_video_framerate'],
        'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
        'stream_video_resolution': notify_params['stream_video_resolution'],
        'stream_video_height': notify_params['stream_video_height'],
        'stream_video_width': notify_params['stream_video_width'],
        'stream_video_language': notify_params['stream_video_language'],
        'stream_video_language_code': notify_params['stream_video_language_code'],
        'stream_audio_bitrate': notify_params['stream_audio_bitrate'],
        'stream_audio_bitrate_mode': notify_params['stream_audio_bitrate_mode'],
        'stream_audio_codec': notify_params['stream_audio_codec'],
        'stream_audio_channels': notify_params['stream_audio_channels'],
        'stream_audio_channel_layout': notify_params['stream_audio_channel_layout'],
        'stream_audio_sample_rate': notify_params['stream_audio_sample_rate'],
        'stream_audio_language': notify_params['stream_audio_language'],
        'stream_audio_language_code': notify_params['stream_audio_language_code'],
        'stream_subtitle_codec': notify_params['stream_subtitle_codec'],
        'stream_subtitle_container': notify_params['stream_subtitle_container'],
        'stream_subtitle_format': notify_params['stream_subtitle_format'],
        'stream_subtitle_forced': notify_params['stream_subtitle_forced'],
        'stream_subtitle_language': notify_params['stream_subtitle_language'],
        'stream_subtitle_language_code': notify_params['stream_subtitle_language_code'],
        'stream_subtitle_location': notify_params['stream_subtitle_location'],
        'transcode_container': notify_params['transcode_container'],
        'transcode_video_codec': notify_params['transcode_video_codec'],
        'transcode_video_width': notify_params['transcode_width'],
        'transcode_video_height': notify_params['transcode_height'],
        'transcode_audio_codec': notify_params['transcode_audio_codec'],
        'transcode_audio_channels': notify_params['transcode_audio_channels'],
        'transcode_hw_requested': notify_params['transcode_hw_requested'],
        'transcode_hw_decoding': notify_params['transcode_hw_decoding'],
        'transcode_hw_decode_codec': notify_params['transcode_hw_decode'],
        'transcode_hw_decode_title': notify_params['transcode_hw_decode_title'],
        'transcode_hw_encoding': notify_params['transcode_hw_encoding'],
        'transcode_hw_encode_codec': notify_params['transcode_hw_encode'],
        'transcode_hw_encode_title': notify_params['transcode_hw_encode_title'],
        'transcode_hw_full_pipeline': notify_params['transcode_hw_full_pipeline'],
        'session_key': notify_params['session_key'],
        'transcode_key': notify_params['transcode_key'],
        'session_id': notify_params['session_id'],
        'user_id': notify_params['user_id'],
        'machine_id': notify_params['machine_id'],
        # Source metadata parameters
        'media_type': notify_params['media_type'],
        'title': notify_params['full_title'],
        'library_name': notify_params['library_name'],
        'show_name': show_name,
        'episode_name': episode_name,
        'artist_name': artist_name,
        'album_name': album_name,
        'track_name': track_name,
        'season_num': season_num,
        'season_num00': season_num00,
        'episode_num': episode_num,
        'episode_num00': episode_num00,
        'track_num': track_num,
        'track_num00': track_num00,
        'season_count': child_count,
        'episode_count': grandchild_count,
        'album_count': child_count,
        'track_count': grandchild_count,
        'year': notify_params['year'],
        'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
            if notify_params['originally_available_at'] else '',
        'air_date': arrow.get(notify_params['originally_available_at']).format(date_format)
            if notify_params['originally_available_at'] else '',
        'added_date': arrow.get(notify_params['added_at']).format(date_format)
            if notify_params['added_at'] else '',
        'updated_date': arrow.get(notify_params['updated_at']).format(date_format)
            if notify_params['updated_at'] else '',
        'last_viewed_date': arrow.get(notify_params['last_viewed_at']).format(date_format)
            if notify_params['last_viewed_at'] else '',
        'studio': notify_params['studio'],
        'content_rating': notify_params['content_rating'],
        'directors': ', '.join(notify_params['directors']),
        'writers': ', '.join(notify_params['writers']),
        'actors': ', '.join(notify_params['actors']),
        'genres': ', '.join(notify_params['genres']),
        'labels': ', '.join(notify_params['labels']),
        'collections': ', '.join(notify_params['collections']),
        'summary': notify_params['summary'],
        'tagline': notify_params['tagline'],
        'rating': notify_params['rating'],
        'audience_rating': helpers.get_percent(notify_params['audience_rating'], 10) or '',
        'duration': duration,
        'poster_title': notify_params['poster_title'],
        'poster_url': notify_params['poster_url'],
        'plex_url': notify_params['plex_url'],
        'imdb_id': notify_params['imdb_id'],
        'imdb_url': notify_params['imdb_url'],
        'thetvdb_id': notify_params['thetvdb_id'],
        'thetvdb_url': notify_params['thetvdb_url'],
        'themoviedb_id': notify_params['themoviedb_id'],
        'themoviedb_url': notify_params['themoviedb_url'],
        'tvmaze_id': notify_params['tvmaze_id'],
        'tvmaze_url': notify_params['tvmaze_url'],
        'lastfm_url': notify_params['lastfm_url'],
        'trakt_url': notify_params['trakt_url'],
        'container': notify_params['container'],
        'bitrate': notify_params['bitrate'],
        'aspect_ratio': notify_params['aspect_ratio'],
        'video_codec': notify_params['video_codec'],
        'video_codec_level': notify_params['video_codec_level'],
        'video_bitrate': notify_params['video_bitrate'],
        'video_bit_depth': notify_params['video_bit_depth'],
        'video_framerate': notify_params['video_framerate'],
        'video_ref_frames': notify_params['video_ref_frames'],
        'video_resolution': notify_params['video_resolution'],
        'video_height': notify_params['height'],
        'video_width': notify_params['width'],
        'video_language': notify_params['video_language'],
        'video_language_code': notify_params['video_language_code'],
        'audio_bitrate': notify_params['audio_bitrate'],
        'audio_bitrate_mode': notify_params['audio_bitrate_mode'],
        'audio_codec': notify_params['audio_codec'],
        'audio_channels': notify_params['audio_channels'],
        'audio_channel_layout': notify_params['audio_channel_layout'],
        'audio_sample_rate': notify_params['audio_sample_rate'],
        'audio_language': notify_params['audio_language'],
        'audio_language_code': notify_params['audio_language_code'],
        'subtitle_codec': notify_params['subtitle_codec'],
        'subtitle_container': notify_params['subtitle_container'],
        'subtitle_format': notify_params['subtitle_format'],
        'subtitle_forced': notify_params['subtitle_forced'],
        'subtitle_location': notify_params['subtitle_location'],
        'subtitle_language': notify_params['subtitle_language'],
        'subtitle_language_code': notify_params['subtitle_language_code'],
        'file': notify_params['file'],
        'file_size': helpers.humanFileSize(notify_params['file_size']),
        'indexes': notify_params['indexes'],
        'section_id': notify_params['section_id'],
        'rating_key': notify_params['rating_key'],
        'parent_rating_key': notify_params['parent_rating_key'],
        'grandparent_rating_key': notify_params['grandparent_rating_key'],
        'thumb': notify_params['thumb'],
        'parent_thumb': notify_params['parent_thumb'],
        'grandparent_thumb': notify_params['grandparent_thumb'],
        'poster_thumb': poster_thumb
        }

    return available_params


def build_server_notify_params(notify_action=None, **kwargs):
    # Get time formats
    date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
    time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')

    update_channel = pmsconnect.PmsConnect().get_server_update_channel()

    pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
    plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))

    available_params = {
        # Global paramaters
        'tautulli_version': common.RELEASE,
        'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
        'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
        'tautulli_commit': plexpy.CURRENT_VERSION,
        'server_name': plexpy.CONFIG.PMS_NAME,
        'server_ip': plexpy.CONFIG.PMS_IP,
        'server_port': plexpy.CONFIG.PMS_PORT,
        'server_url': plexpy.CONFIG.PMS_URL,
        'server_platform': plexpy.CONFIG.PMS_PLATFORM,
        'server_version': plexpy.CONFIG.PMS_VERSION,
        'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
        'action': notify_action.split('on_')[-1],
        'datestamp': arrow.now().format(date_format),
        'timestamp': arrow.now().format(time_format),
        'unixtime': int(time.time()),
        # Plex Media Server update parameters
        'update_version': pms_download_info['version'],
        'update_url': pms_download_info['download_url'],
        'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
            if pms_download_info['release_date'] else '',
        'update_channel': 'Beta' if update_channel == 'beta' else 'Public',
        'update_platform': pms_download_info['platform'],
        'update_distro': pms_download_info['distro'],
        'update_distro_build': pms_download_info['build'],
        'update_requirements': pms_download_info['requirements'],
        'update_extra_info': pms_download_info['extra_info'],
        'update_changelog_added': pms_download_info['changelog_added'],
        'update_changelog_fixed': pms_download_info['changelog_fixed'],
        # Tautulli update parameters
        'tautulli_update_version': plexpy_download_info['tag_name'],
        'tautulli_update_release_url': plexpy_download_info['html_url'],
        'tautulli_update_tar': plexpy_download_info['tarball_url'],
        'tautulli_update_zip': plexpy_download_info['zipball_url'],
        'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''),
        'tautulli_update_behind': kwargs.pop('plexpy_update_behind', ''),
        'tautulli_update_changelog': plexpy_download_info['body']
        }

    return available_params


def build_notify_text(subject='', body='', notify_action=None, parameters=None, agent_id=None, test=False):
    # Default subject and body text
    if agent_id == 15:
        default_subject = default_body = ''
    else:
        default_action = next((a for a in notifiers.available_notification_actions() if a['name'] == notify_action), {})
        default_subject = default_action.get('subject', '')
        default_body = default_action.get('body', '')

    # Make sure subject and body text are strings
    if not isinstance(subject, basestring):
        logger.error(u"Tautulli NotificationHandler :: Invalid subject text. Using fallback.")
        subject = default_subject
    if not isinstance(body, basestring):
        logger.error(u"Tautulli NotificationHandler :: Invalid body text. Using fallback.")
        body = default_body

    media_type = parameters.get('media_type')

    all_tags = r'<movie>.*?</movie>|' \
        '<show>.*?</show>|<season>.*?</season>|<episode>.*?</episode>|' \
        '<artist>.*?</artist>|<album>.*?</album>|<track>.*?</track>'

    # Check for exclusion tags
    if media_type == 'movie':
        pattern = re.compile(all_tags.replace('<movie>.*?</movie>', '<movie>|</movie>'), re.IGNORECASE | re.DOTALL)
    elif media_type == 'show':
        pattern = re.compile(all_tags.replace('<show>.*?</show>', '<show>|</show>'), re.IGNORECASE | re.DOTALL)
    elif media_type == 'season':
        pattern = re.compile(all_tags.replace('<season>.*?</season>', '<season>|</season>'), re.IGNORECASE | re.DOTALL)
    elif media_type == 'episode':
        pattern = re.compile(all_tags.replace('<episode>.*?</episode>', '<episode>|</episode>'), re.IGNORECASE | re.DOTALL)
    elif media_type == 'artist':
        pattern = re.compile(all_tags.replace('<artist>.*?</artist>', '<artist>|</artist>'), re.IGNORECASE | re.DOTALL)
    elif media_type == 'album':
        pattern = re.compile(all_tags.replace('<album>.*?</album>', '<album>|</album>'), re.IGNORECASE | re.DOTALL)
    elif media_type == 'track':
        pattern = re.compile(all_tags.replace('<track>.*?</track>', '<track>|</track>'), re.IGNORECASE | re.DOTALL)
    else:
        pattern = re.compile(all_tags, re.IGNORECASE | re.DOTALL)

    # Remove the unwanted tags and strip any unmatch tags too.
    subject = strip_tag(re.sub(pattern, '', subject), agent_id).strip(' \t\n\r')
    body = strip_tag(re.sub(pattern, '', body), agent_id).strip(' \t\n\r')

    if test:
        return subject, body

    custom_formatter = CustomFormatter()

    if agent_id == 15:
        try:
            script_args = [custom_formatter.format(unicode(arg), **parameters) for arg in subject.split()]
        except LookupError as e:
            logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e)
            script_args = []
        except Exception as e:
            logger.error(u"Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
            script_args = []
    else:
        script_args = []

    try:
        subject = custom_formatter.format(unicode(subject), **parameters)
    except LookupError as e:
        logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
        subject = unicode(default_subject).format(**parameters)
    except Exception as e:
        logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
        subject = unicode(default_subject).format(**parameters)

    try:
        body = custom_formatter.format(unicode(body), **parameters)
    except LookupError as e:
        logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e)
        body = unicode(default_body).format(**parameters)
    except Exception as e:
        logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
        body = unicode(default_body).format(**parameters)

    return subject, body, script_args


def strip_tag(data, agent_id=None):

    if agent_id == 7:
        # Allow tags b, i, u, a[href], font[color] for Pushover
        whitelist = {'b': [],
                     'i': [],
                     'u': [],
                     'a': ['href'],
                     'font': ['color']}
        return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)

    elif agent_id in (10, 14, 20):
        # Don't remove tags for Email, Slack, and Discord
        return data

    elif agent_id == 13:
        # Allow tags b, i, code, pre, a[href] for Telegram
        whitelist = {'b': [],
                     'i': [],
                     'code': [],
                     'pre': [],
                     'a': ['href']}
        return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)

    else:
        whitelist = {}
        return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)


def format_group_index(group_keys):
    group_keys = sorted(group_keys)

    num = []
    num00 = []

    for k, g in groupby(enumerate(group_keys), lambda (i, x): i-x):
        group = map(itemgetter(1), g)
        g_min, g_max = min(group), max(group)

        if g_min == g_max:
            num.append('{0:01d}'.format(g_min))
            num00.append('{0:02d}'.format(g_min))
        else:
            num.append('{0:01d}-{1:01d}'.format(g_min, g_max))
            num00.append('{0:02d}-{1:02d}'.format(g_min, g_max))

    return ','.join(num) or '0', ','.join(num00) or '00'


def get_img_info(img=None, rating_key=None, title='', width=600, height=1000,
                 opacity=100, background='000000', blur=0, fallback=None):
    img_info = {'img_title': '', 'img_url': ''}

    if not rating_key and not img:
        return img_info

    if rating_key and not img:
        if fallback == 'art':
            img = '/library/metadata/{}/art'.format(rating_key)
        else:
            img = '/library/metadata/{}/thumb'.format(rating_key)

    img_split = img.split('/')
    img = '/'.join(img_split[:5])
    rating_key = rating_key or img_split[3]

    image_info = {'img': img,
                  'rating_key': rating_key,
                  'width': width,
                  'height': height,
                  'opacity': opacity,
                  'background': background,
                  'blur': blur,
                  'fallback': fallback}

    if plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS == 1:
        service = 'imgur'
    elif plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS == 3:
        service = 'cloudinary'
    else:
        service = None

    # Try to retrieve poster info from the database
    data_factory = datafactory.DataFactory()
    database_img_info = data_factory.get_img_info(service=service, **image_info)

    if database_img_info:
        img_info = database_img_info[0]

    elif not database_img_info and img:
        pms_connect = pmsconnect.PmsConnect()
        result = pms_connect.get_image(**image_info)

        if result and result[0]:
            img_url = delete_hash = ''

            if service == 'imgur':
                img_url, delete_hash = helpers.upload_to_imgur(img_data=result[0],
                                                               img_title=title,
                                                               rating_key=rating_key,
                                                               fallback=fallback)
            elif service == 'cloudinary':
                img_url = helpers.upload_to_cloudinary(img_data=result[0],
                                                       img_title=title,
                                                       rating_key=rating_key,
                                                       fallback=fallback)

            if img_url:
                img_hash = set_hash_image_info(**image_info)
                data_factory.set_img_info(img_hash=img_hash,
                                          img_title=title,
                                          img_url=img_url,
                                          delete_hash=delete_hash,
                                          service=service)

                img_info = {'img_title': title, 'img_url': img_url}

    return img_info


def set_hash_image_info(img=None, rating_key=None, width=600, height=1000,
                        opacity=100, background='000000', blur=0, fallback=None):
    if not rating_key and not img:
        return fallback

    if rating_key and not img:
        if fallback == 'art':
            img = '/library/metadata/{}/art'.format(rating_key)
        else:
            img = '/library/metadata/{}/thumb'.format(rating_key)

    img_split = img.split('/')
    img = '/'.join(img_split[:5])
    rating_key = rating_key or img_split[3]

    img_string = '{}.{}.{}.{}.{}.{}.{}.{}'.format(
        plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
    img_hash = hashlib.sha256(img_string).hexdigest()

    keys = {'img_hash': img_hash}
    values = {'img': img,
              'rating_key': rating_key,
              'width': width,
              'height': height,
              'opacity': opacity,
              'background': background,
              'blur': blur,
              'fallback': fallback}

    db = database.MonitorDatabase()
    db.upsert('image_hash_lookup', key_dict=keys, value_dict=values)

    return img_hash


def get_hash_image_info(img_hash=None):
    db = database.MonitorDatabase()
    query = 'SELECT * FROM image_hash_lookup WHERE img_hash = ?'
    result = db.select_single(query, args=[img_hash])
    return result


def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
    db = database.MonitorDatabase()

    try:
        query = 'SELECT imdb_id, tvmaze_id, tvmaze_url FROM tvmaze_lookup ' \
                'WHERE rating_key = ?'
        tvmaze_info = db.select_single(query, args=[rating_key])
    except Exception as e:
        logger.warn(u"Tautulli NotificationHandler :: Unable to execute database query for lookup_tvmaze_by_tvdb_id: %s." % e)
        return {}

    if not tvmaze_info:
        tvmaze_info = {}

        if thetvdb_id:
            logger.debug(u"Tautulli NotificationHandler :: Looking up TVmaze info for thetvdb_id '{}'.".format(thetvdb_id))
        else:
            logger.debug(u"Tautulli NotificationHandler :: Looking up TVmaze info for imdb_id '{}'.".format(imdb_id))

        params = {'thetvdb': thetvdb_id} if thetvdb_id else {'imdb': imdb_id}
        response, err_msg, req_msg = request.request_response2('http://api.tvmaze.com/lookup/shows', params=params)

        if response and not err_msg:
            tvmaze_json = response.json()
            thetvdb_id = tvmaze_json.get('externals', {}).get('thetvdb', '')
            imdb_id = tvmaze_json.get('externals', {}).get('imdb', '')
            tvmaze_id = tvmaze_json.get('id', '')
            tvmaze_url = tvmaze_json.get('url', '')
            
            keys = {'tvmaze_id': tvmaze_id}
            tvmaze_info = {'rating_key': rating_key,
                           'thetvdb_id': thetvdb_id,
                           'imdb_id': imdb_id,
                           'tvmaze_url': tvmaze_url,
                           'tvmaze_json': json.dumps(tvmaze_json)}
            db.upsert(table_name='tvmaze_lookup', key_dict=keys, value_dict=tvmaze_info)

            tvmaze_info.pop('tvmaze_json')

        else:
            if err_msg:
                logger.error(u"Tautulli NotificationHandler :: {}".format(err_msg))

            if req_msg:
                logger.debug(u"Tautulli NotificationHandler :: Request response: {}".format(req_msg))

    return tvmaze_info


def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
    db = database.MonitorDatabase()

    try:
        query = 'SELECT thetvdb_id, imdb_id, themoviedb_id, themoviedb_url FROM themoviedb_lookup ' \
                'WHERE rating_key = ?'
        themoviedb_info = db.select_single(query, args=[rating_key])
    except Exception as e:
        logger.warn(u"Tautulli NotificationHandler :: Unable to execute database query for lookup_themoviedb_by_imdb_id: %s." % e)
        return {}

    if not themoviedb_info:
        themoviedb_info = {}

        if thetvdb_id:
            logger.debug(u"Tautulli NotificationHandler :: Looking up The Movie Database info for thetvdb_id '{}'.".format(thetvdb_id))
        else:
            logger.debug(u"Tautulli NotificationHandler :: Looking up The Movie Database info for imdb_id '{}'.".format(imdb_id))

        params = {'api_key': plexpy.CONFIG.THEMOVIEDB_APIKEY,
                  'external_source': 'tvdb_id' if thetvdb_id else 'imdb_id'
                  }
        response, err_msg, req_msg = request.request_response2('https://api.themoviedb.org/3/find/{}'.format(thetvdb_id or imdb_id), params=params)

        if response and not err_msg:
            themoviedb_find_json = response.json()
            if themoviedb_find_json.get('tv_results'):
                themoviedb_id = themoviedb_find_json['tv_results'][0]['id']
            elif themoviedb_find_json.get('movie_results'):
                themoviedb_id = themoviedb_find_json['movie_results'][0]['id']
            else:
                themoviedb_id = ''

            if themoviedb_id:
                media_type = 'tv' if thetvdb_id else 'movie'
                themoviedb_url = 'https://www.themoviedb.org/{}/{}'.format(media_type, themoviedb_id)
                themoviedb_json = get_themoviedb_info(rating_key=rating_key,
                                                      media_type=media_type,
                                                      themoviedb_id=themoviedb_id)

                keys = {'themoviedb_id': themoviedb_id}
                themoviedb_info = {'rating_key': rating_key,
                                   'thetvdb_id': thetvdb_id,
                                   'imdb_id': imdb_id or themoviedb_json.get('imdb_id'),
                                   'themoviedb_url': themoviedb_url,
                                   'themoviedb_json': json.dumps(themoviedb_json)
                                   }

                db.upsert(table_name='themoviedb_lookup', key_dict=keys, value_dict=themoviedb_info)

                themoviedb_info.pop('themoviedb_json')

        else:
            if err_msg:
                logger.error(u"Tautulli NotificationHandler :: {}".format(err_msg))

            if req_msg:
                logger.debug(u"Tautulli NotificationHandler :: Request response: {}".format(req_msg))

    return themoviedb_info


def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None):
    if media_type in ('show', 'season', 'episode'):
        media_type = 'tv'

    db = database.MonitorDatabase()

    try:
        query = 'SELECT themoviedb_json FROM themoviedb_lookup ' \
                'WHERE rating_key = ?'
        result = db.select_single(query, args=[rating_key])
    except Exception as e:
        logger.warn(u"Tautulli NotificationHandler :: Unable to execute database query for get_themoviedb_info: %s." % e)
        return {}

    if result:
        try:
            return json.loads(result['themoviedb_json'])
        except:
            pass

    themoviedb_json = {}

    logger.debug(u"Tautulli NotificationHandler :: Looking up The Movie Database info for themoviedb_id '{}'.".format(themoviedb_id))

    params = {'api_key': plexpy.CONFIG.THEMOVIEDB_APIKEY}
    response, err_msg, req_msg = request.request_response2('https://api.themoviedb.org/3/{}/{}'.format(media_type, themoviedb_id), params=params)

    if response and not err_msg:
        themoviedb_json = response.json()
        themoviedb_id = themoviedb_json['id']
        themoviedb_url = 'https://www.themoviedb.org/{}/{}'.format(media_type, themoviedb_id)

        keys = {'themoviedb_id': themoviedb_id}
        themoviedb_info = {'rating_key': rating_key,
                           'imdb_id': themoviedb_json.get('imdb_id'),
                           'themoviedb_url': themoviedb_url,
                           'themoviedb_json': json.dumps(themoviedb_json)
                           }

        db.upsert(table_name='themoviedb_lookup', key_dict=keys, value_dict=themoviedb_info)

    else:
        if err_msg:
            logger.error(u"Tautulli NotificationHandler :: {}".format(err_msg))

        if req_msg:
            logger.debug(u"Tautulli NotificationHandler :: Request response: {}".format(req_msg))

    return themoviedb_json


class CustomFormatter(Formatter):
    def convert_field(self, value, conversion):
        if conversion is None:
            return value
        elif conversion == 's':
            return str(value)
        elif conversion == 'r':
            return repr(value)
        elif conversion == 'u':  # uppercase
            return unicode(value).upper()
        elif conversion == 'l':  # lowercase
            return unicode(value).lower()
        elif conversion == 'c':  # capitalize
            return unicode(value).title()
        else:
            return value

    def format_field(self, value, format_spec):
        if format_spec.startswith('[') and format_spec.endswith(']'):
            pattern = re.compile(r'\[(-?\d*):?(-?\d*)\]')
            if re.match(pattern, format_spec):  # slice
                items = [x.strip() for x in unicode(value).split(',')]
                slice_start, slice_end = re.search(pattern, format_spec).groups()
                slice_start = helpers.cast_to_int(slice_start) or None
                slice_end = helpers.cast_to_int(slice_end) or None
                return ', '.join(items[slice(slice_start, slice_end)])
            else:
                return value
        else:
            return super(CustomFormatter, self).format_field(value, format_spec)
