# -*- coding: utf-8 -*-

#  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 base64
from collections import defaultdict
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import email.utils
import json
import os
import re
import smtplib
import subprocess
import sys
import threading
import time
from urllib.parse import urlencode
from urllib.parse import urlparse

import bleach
import paho.mqtt.client
import paho.mqtt.publish
import requests
from requests.auth import HTTPBasicAuth

try:
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
    _CRYPTOGRAPHY = True
except ImportError:
    _CRYPTOGRAPHY = False

import gntp.notifier
import facebook
import twitter

import plexpy
from plexpy import common
from plexpy import database
from plexpy import helpers
from plexpy import logger
from plexpy import mobile_app
from plexpy import pmsconnect
from plexpy import request
from plexpy import users


BROWSER_NOTIFIERS = {}

AGENT_IDS = {'growl': 0,
             'prowl': 1,
             'xbmc': 2,
             'plex': 3,
             'pushbullet': 6,
             'pushover': 7,
             'osx': 8,
             'boxcar': 9,
             'email': 10,
             'twitter': 11,
             'ifttt': 12,
             'telegram': 13,
             'slack': 14,
             'scripts': 15,
             'facebook': 16,
             'browser': 17,
             'join': 18,
             'discord': 20,
             'remoteapp': 21,
             'groupme': 22,
             'mqtt': 23,
             'zapier': 24,
             'webhook': 25,
             'plexmobileapp': 26,
             'lunasea': 27,
             'microsoftteams': 28,
             'gotify': 29,
             'ntfy': 30
             }

DEFAULT_CUSTOM_CONDITIONS = [{'parameter': '', 'operator': '', 'value': [], 'type': None}]
CUSTOM_CONDITION_TYPE_OPERATORS = {
    'float': ['is', 'is not', 'is greater than', 'is less than'],
    'int': ['is', 'is not', 'is greater than', 'is less than'],
    'str': ['contains', 'does not contain', 'is', 'is not', 'begins with', 'does not begin with', 'ends with', 'does not end with'],
}


def available_notification_agents():
    agents = [{'label': 'Tautulli Remote App',
               'name': 'remoteapp',
               'id': AGENT_IDS['remoteapp'],
               'class': TAUTULLIREMOTEAPP,
               'action_types': ('all',)
               },
              {'label': 'Boxcar',
               'name': 'boxcar',
               'id': AGENT_IDS['boxcar'],
               'class': BOXCAR,
               'action_types': ('all',)
               },
              {'label': 'Browser',
               'name': 'browser',
               'id': AGENT_IDS['browser'],
               'class': BROWSER,
               'action_types': ('all',)
               },
              {'label': 'Discord',
               'name': 'discord',
               'id': AGENT_IDS['discord'],
               'class': DISCORD,
               'action_types': ('all',)
               },
              {'label': 'Email',
               'name': 'email',
               'id': AGENT_IDS['email'],
               'class': EMAIL,
               'action_types': ('all',)
               },
              {'label': 'Facebook',
               'name': 'facebook',
               'id': AGENT_IDS['facebook'],
               'class': FACEBOOK,
               'action_types': ('all',)
               },
              {'label': 'Gotify',
               'name': 'gotify',
               'id': AGENT_IDS['gotify'],
               'class': GOTIFY,
               'action_types': ('all',)
               },
              {'label': 'GroupMe',
               'name': 'groupme',
               'id': AGENT_IDS['groupme'],
               'class': GROUPME,
               'action_types': ('all',)
               },
              {'label': 'Growl',
               'name': 'growl',
               'id': AGENT_IDS['growl'],
               'class': GROWL,
               'action_types': ('all',)
               },
              {'label': 'IFTTT',
               'name': 'ifttt',
               'id': AGENT_IDS['ifttt'],
               'class': IFTTT,
               'action_types': ('all',)
               },
              {'label': 'Join',
               'name': 'join',
               'id': AGENT_IDS['join'],
               'class': JOIN,
               'action_types': ('all',)
               },
              {'label': 'Kodi',
               'name': 'xbmc',
               'id': AGENT_IDS['xbmc'],
               'class': XBMC,
               'action_types': ('all',)
               },
              {'label': 'LunaSea',
               'name': 'lunasea',
               'id': AGENT_IDS['lunasea'],
               'class': LUNASEA,
               'action_types': ('all',)
               },
              {'label': 'Microsoft Teams',
               'name': 'microsoftteams',
               'id': AGENT_IDS['microsoftteams'],
               'class': MICROSOFTTEAMS,
               'action_types': ('all',)
               },
              {'label': 'MQTT',
               'name': 'mqtt',
               'id': AGENT_IDS['mqtt'],
               'class': MQTT,
               'action_types': ('all',)
               },
              {'label': 'ntfy',
               'name': 'ntfy',
               'id': AGENT_IDS['ntfy'],
               'class': NTFY,
               'action_types': ('all',)
               },
              {'label': 'Plex Home Theater',
               'name': 'plex',
               'id': AGENT_IDS['plex'],
               'class': PLEX,
               'action_types': ('all',)
               },
              {'label': 'Plex Android / iOS App',
               'name': 'plexmobileapp',
               'id': AGENT_IDS['plexmobileapp'],
               'class': PLEXMOBILEAPP,
               'action_types': ('on_play', 'on_created', 'on_newdevice')
               },
              {'label': 'Prowl',
               'name': 'prowl',
               'id': AGENT_IDS['prowl'],
               'class': PROWL,
               'action_types': ('all',)
               },
              {'label': 'Pushbullet',
               'name': 'pushbullet',
               'id': AGENT_IDS['pushbullet'],
               'class': PUSHBULLET,
               'action_types': ('all',)
               },
              {'label': 'Pushover',
               'name': 'pushover',
               'id': AGENT_IDS['pushover'],
               'class': PUSHOVER,
               'action_types': ('all',)
               },
              {'label': 'Script',
               'name': 'scripts',
               'id': AGENT_IDS['scripts'],
               'class': SCRIPTS,
               'action_types': ('all',)
               },
              {'label': 'Slack',
               'name': 'slack',
               'id': AGENT_IDS['slack'],
               'class': SLACK,
               'action_types': ('all',)
               },
              {'label': 'Telegram',
               'name': 'telegram',
               'id': AGENT_IDS['telegram'],
               'class': TELEGRAM,
               'action_types': ('all',)
               },
              {'label': 'Twitter',
               'name': 'twitter',
               'id': AGENT_IDS['twitter'],
               'class': TWITTER,
               'action_types': ('all',)
               },
              {'label': 'Webhook',
               'name': 'webhook',
               'id': AGENT_IDS['webhook'],
               'class': WEBHOOK,
               'action_types': ('all',)
               },
              {'label': 'Zapier',
               'name': 'zapier',
               'id': AGENT_IDS['zapier'],
               'class': ZAPIER,
               'action_types': ('all',)
               }
              ]

    # OSX Notifications should only be visible if it can be used
    if OSX().validate():
        agents.append({'label': 'macOS Notification Center',
                       'name': 'osx',
                       'id': AGENT_IDS['osx'],
                       'class': OSX,
                       'action_types': ('all',)
                       })

    return agents


def available_notification_actions(agent_id=None):
    actions = [{'label': 'Playback Start',
                'name': 'on_play',
                'description': 'Trigger a notification when a stream is started.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) started playing {title}.',
                'icon': 'fa-play',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'Playback Stop',
                'name': 'on_stop',
                'description': 'Trigger a notification when a stream is stopped.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) has stopped {title}.',
                'icon': 'fa-stop',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'Playback Pause',
                'name': 'on_pause',
                'description': 'Trigger a notification when a stream is paused.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) has paused {title}.',
                'icon': 'fa-pause',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'Playback Resume',
                'name': 'on_resume',
                'description': 'Trigger a notification when a stream is resumed.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) has resumed {title}.',
                'icon': 'fa-play',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'Playback Error',
                'name': 'on_error',
                'description': 'Trigger a notification when a stream encounters an error.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) encountered an error trying to play {title}.',
                'icon': 'fa-exclamation-triangle',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'Transcode Decision Change',
                'name': 'on_change',
                'description': 'Trigger a notification when a stream changes transcode decision.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) has changed transcode decision for {title}.',
                'icon': 'fa-exchange-alt',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'Intro Marker',
                'name': 'on_intro',
                'description': 'Trigger a notification when a video stream reaches any intro marker.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) has reached an intro marker for {title}.',
                'icon': 'fa-bookmark',
                'media_types': ('episode',)
                },
               {'label': 'Commercial Marker',
                'name': 'on_commercial',
                'description': 'Trigger a notification when a video stream reaches any commercial marker.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) has reached a commercial marker for {title}.',
                'icon': 'fa-bookmark',
                'media_types': ('movie', 'episode')
                },
               {'label': 'Credits Marker',
                'name': 'on_credits',
                'description': 'Trigger a notification when a video stream reaches any credits marker.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) has reached a credits marker for {title}.',
                'icon': 'fa-bookmark',
                'media_types': ('movie', 'episode')
                },
               {'label': 'Watched',
                'name': 'on_watched',
                'description': 'Trigger a notification when a stream reaches the specified watched or listened threshold.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) has watched {title}.',
                'icon': 'fa-eye',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'Buffer Warning',
                'name': 'on_buffer',
                'description': 'Trigger a notification when a stream exceeds the specified buffer threshold.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} ({player}) is buffering {title}.',
                'icon': 'fa-spinner',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'User Concurrent Streams',
                'name': 'on_concurrent',
                'description': 'Trigger a notification when a user exceeds the concurrent stream threshold.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} has {user_streams} concurrent streams.',
                'icon': 'fa-arrow-circle-o-right',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'User New Device',
                'name': 'on_newdevice',
                'description': 'Trigger a notification when a user streams from a new device.',
                'subject': 'Tautulli ({server_name})',
                'body': '{user} is streaming from a new device: {player}.',
                'icon': 'fa-desktop',
                'media_types': ('movie', 'episode', 'track')
                },
               {'label': 'Recently Added',
                'name': 'on_created',
                'description': 'Trigger a notification when a media item is added to the Plex Media Server.',
                'subject': 'Tautulli ({server_name})',
                'body': '{title} was recently added to Plex.',
                'icon': 'fa-download',
                'media_types': ('movie', 'show', 'season', 'episode', 'artist', 'album', 'track')
                },
               {'label': 'Plex Server Down',
                'name': 'on_intdown',
                'description': 'Trigger a notification when the Plex Media Server cannot be reached internally.',
                'subject': 'Tautulli ({server_name})',
                'body': 'The Plex Media Server is down.',
                'icon': 'fa-server',
                'media_types': ('server',)
                },
               {'label': 'Plex Server Back Up',
                'name': 'on_intup',
                'description': 'Trigger a notification when the Plex Media Server can be reached internally after being down.',
                'subject': 'Tautulli ({server_name})',
                'body': 'The Plex Media Server is back up.',
                'icon': 'fa-server',
                'media_types': ('server',)
                },
               {'label': 'Plex Remote Access Down',
                'name': 'on_extdown',
                'description': 'Trigger a notification when the Plex Media Server cannot be reached externally.',
                'subject': 'Tautulli ({server_name})',
                'body': 'The Plex Media Server remote access is down. ({remote_access_reason})',
                'icon': 'fa-server',
                'media_types': ('server',)
                },
               {'label': 'Plex Remote Access Back Up',
                'name': 'on_extup',
                'description': 'Trigger a notification when the Plex Media Server can be reached externally after being down.',
                'subject': 'Tautulli ({server_name})',
                'body': 'The Plex Media Server remote access is back up.',
                'icon': 'fa-server',
                'media_types': ('server',)
                },
               {'label': 'Plex Update Available',
                'name': 'on_pmsupdate',
                'description': 'Trigger a notification when an update for the Plex Media Server is available.',
                'subject': 'Tautulli ({server_name})',
                'body': 'An update is available for the Plex Media Server (version {update_version}).',
                'icon': 'fa-refresh',
                'media_types': ('server',)
                },
               {'label': 'Tautulli Update Available',
                'name': 'on_plexpyupdate',
                'description': 'Trigger a notification when an update for the Tautulli is available.',
                'subject': 'Tautulli ({server_name})',
                'body': 'An update is available for Tautulli (version {tautulli_update_version}).',
                'icon': 'fa-refresh',
                'media_types': ('server',)
                },
               {'label': 'Tautulli Database Corruption',
                'name': 'on_plexpydbcorrupt',
                'description': 'Trigger a notification if Tautulli database corruption is detected when backing up the database.',
                'subject': 'Tautulli ({server_name})',
                'body': 'Tautulli database corruption detected. Automatic cleanup of database backups is suspended.',
                'icon': 'fa-database',
                'media_types': ('server',)
                },
               {'label': 'Tautulli Plex Token Expired',
                'name': 'on_tokenexpired',
                'description': 'Trigger a notification when the Tautulli Plex account token has expired.',
                'subject': 'Tautulli ({server_name})',
                'body': 'The Tautulli Plex account token has expired.',
                'icon': 'fa-key',
                'media_types': ('server',)
                }
               ]

    if str(agent_id).isdigit():
        action_types = get_notify_agents(return_dict=True).get(int(agent_id), {}).get('action_types', [])
        if 'all' not in action_types:
            actions = [a for a in actions if a['name'] in action_types]

    return actions


def get_agent_class(agent_id=None, config=None):
    if str(agent_id).isdigit():
        agent = get_notify_agents(return_dict=True).get(int(agent_id), {}).get('class', Notifier)
        return agent(config=config)
    else:
        return None


def get_notify_agents(return_dict=False):
    if return_dict:
        return {a['id']: a for a in available_notification_agents()}
    return tuple(a['name'] for a in sorted(available_notification_agents(), key=lambda k: k['label']))


def get_notify_actions(return_dict=False):
    if return_dict:
        return {a['name']: a for a in available_notification_actions()}
    return tuple(a['name'] for a in available_notification_actions())


def get_notifiers(notifier_id=None, notify_action=None):
    notify_actions = get_notify_actions()

    where = where_id = where_action = ''
    args = []

    if notifier_id or notify_action:
        where = 'WHERE '
        if notifier_id:
            where_id += 'notifiers.id = ?'
            args.append(notifier_id)
        if notify_action and notify_action in notify_actions:
            where_action = '%s = ?' % notify_action
            args.append(1)
        where += ' AND '.join([w for w in [where_id, where_action] if w])

    db = database.MonitorDatabase()
    result = db.select(
        (
            "SELECT notifiers.id, notifiers.agent_id, notifiers.agent_name, notifiers.agent_label, notifiers.friendly_name, %s, "
            "MAX(notify_log.timestamp) AS last_triggered, notify_log.success AS last_success "
            "FROM notifiers "
            "LEFT OUTER JOIN notify_log ON notifiers.id = notify_log.notifier_id "
            "%s "
            "GROUP BY notifiers.id"
        ) % (', '.join(notify_actions), where), args=args
    )

    for item in result:
        item['active'] = int(any([item.pop(k) for k in list(item.keys()) if k in notify_actions]))

    return result


def delete_notifier(notifier_id=None):
    db = database.MonitorDatabase()

    if str(notifier_id).isdigit():
        logger.debug("Tautulli Notifiers :: Deleting notifier_id %s from the database."
                     % notifier_id)
        result = db.action("DELETE FROM notifiers WHERE id = ?", args=[notifier_id])
        return True
    else:
        return False


def get_notifier_config(notifier_id=None, mask_passwords=False):
    if str(notifier_id).isdigit():
        notifier_id = int(notifier_id)
    else:
        logger.error("Tautulli Notifiers :: Unable to retrieve notifier config: invalid notifier_id %s."
                     % notifier_id)
        return None

    db = database.MonitorDatabase()
    result = db.select_single("SELECT * FROM notifiers WHERE id = ?", args=[notifier_id])

    if not result:
        return None

    try:
        config = json.loads(result.pop('notifier_config', '{}'))
        notifier_agent = get_agent_class(agent_id=result['agent_id'], config=config)
    except Exception as e:
        logger.error("Tautulli Notifiers :: Failed to get notifier config options: %s." % e)
        return

    if mask_passwords:
        notifier_agent.config = helpers.mask_config_passwords(notifier_agent.config)

    notify_actions = get_notify_actions(return_dict=True)

    notifier_actions = {}
    notifier_text = {}
    for k in list(result.keys()):
        if k in notify_actions:
            subject = result.pop(k + '_subject')
            body = result.pop(k + '_body')

            if subject is None:
                subject = "" if result['agent_name'] in ('scripts', 'webhook') else notify_actions[k]['subject']
            if body is None:
                body = "" if result['agent_name'] in ('scripts', 'webhook') else notify_actions[k]['body']

            notifier_actions[k] = helpers.cast_to_int(result.pop(k))
            notifier_text[k] = {'subject': subject,
                                'body': body}

    try:
        result['custom_conditions'] = json.loads(result['custom_conditions'])
    except (ValueError, TypeError):
        result['custom_conditions'] = DEFAULT_CUSTOM_CONDITIONS

    if not result['custom_conditions_logic']:
        result['custom_conditions_logic'] = ''

    result['config'] = notifier_agent.config
    result['config_options'] = notifier_agent.return_config_options(mask_passwords=mask_passwords)
    result['actions'] = notifier_actions
    result['notify_text'] = notifier_text

    return result


def add_notifier_config(agent_id=None, **kwargs):
    if str(agent_id).isdigit():
        agent_id = int(agent_id)
    else:
        logger.error("Tautulli Notifiers :: Unable to add new notifier: invalid agent_id %s."
                     % agent_id)
        return False

    agent = get_notify_agents(return_dict=True).get(agent_id, None)

    if not agent:
        logger.error("Tautulli Notifiers :: Unable to retrieve new notification agent: invalid agent_id %s."
                     % agent_id)
        return False

    agent_class = get_agent_class(agent_id=agent['id'])

    keys = {'id': None}
    values = {'agent_id': agent['id'],
              'agent_name': agent['name'],
              'agent_label': agent['label'],
              'friendly_name': '',
              'notifier_config': json.dumps(agent_class.config),
              'custom_conditions': json.dumps(DEFAULT_CUSTOM_CONDITIONS),
              'custom_conditions_logic': ''
              }

    if agent['name'] in ('scripts', 'webhook'):
        for a in available_notification_actions():
            values[a['name'] + '_subject'] = ''
            values[a['name'] + '_body'] = ''
    else:
        for a in available_notification_actions():
            values[a['name'] + '_subject'] = a['subject']
            values[a['name'] + '_body'] = a['body']

    db = database.MonitorDatabase()
    try:
        db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
        notifier_id = db.last_insert_id()
        logger.info("Tautulli Notifiers :: Added new notification agent: %s (notifier_id %s)."
                    % (agent['label'], notifier_id))
        blacklist_logger()
        return notifier_id
    except Exception as e:
        logger.warn("Tautulli Notifiers :: Unable to add notification agent: %s." % e)
        return False


def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
    if str(agent_id).isdigit():
        agent_id = int(agent_id)
    else:
        logger.error("Tautulli Notifiers :: Unable to set existing notifier: invalid agent_id %s."
                     % agent_id)
        return False

    agent = get_notify_agents(return_dict=True).get(agent_id, None)

    if not agent:
        logger.error("Tautulli Notifiers :: Unable to retrieve existing notification agent: invalid agent_id %s."
                     % agent_id)
        return False

    notify_actions = get_notify_actions()
    config_prefix = agent['name'] + '_'

    actions = {k: helpers.cast_to_int(kwargs.pop(k))
               for k in list(kwargs.keys()) if k in notify_actions}
    subject_text = {k: kwargs.pop(k)
                    for k in list(kwargs.keys()) if k.startswith(notify_actions) and k.endswith('_subject')}
    body_text = {k: kwargs.pop(k)
                 for k in list(kwargs.keys()) if k.startswith(notify_actions) and k.endswith('_body')}
    notifier_config = {k[len(config_prefix):]: kwargs.pop(k)
                       for k in list(kwargs.keys()) if k.startswith(config_prefix)}

    for cfg, val in notifier_config.items():
        # Check for a password config keys and a blank password from the HTML form
        if 'password' in cfg and val == '    ':
            # Get the previous password so we don't overwrite it with a blank value
            old_notifier_config = get_notifier_config(notifier_id=notifier_id)
            notifier_config[cfg] = old_notifier_config['config'][cfg]

    agent_class = get_agent_class(agent_id=agent['id'], config=notifier_config)

    custom_conditions = validate_conditions(kwargs.get('custom_conditions'))
    if custom_conditions is False:
        logger.error("Tautulli Notifiers :: Unable to update notification agent: Invalid custom conditions.")
        return False

    keys = {'id': notifier_id}
    values = {'agent_id': agent['id'],
              'agent_name': agent['name'],
              'agent_label': agent['label'],
              'friendly_name': kwargs.get('friendly_name', ''),
              'notifier_config': json.dumps(agent_class.config),
              'custom_conditions': json.dumps(custom_conditions or DEFAULT_CUSTOM_CONDITIONS),
              'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''),
              }
    values.update(actions)
    values.update(subject_text)
    values.update(body_text)

    db = database.MonitorDatabase()
    try:
        db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
        logger.info("Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)."
                    % (agent['label'], notifier_id))
        blacklist_logger()

        if agent['name'] == 'browser':
            check_browser_enabled()

        return True
    except Exception as e:
        logger.warn("Tautulli Notifiers :: Unable to update notification agent: %s." % e)
        return False


def send_notification(notifier_id=None, subject='', body='', notify_action='', notification_id=None, **kwargs):
    notifier_config = get_notifier_config(notifier_id=notifier_id)
    if notifier_config:
        agent = get_agent_class(agent_id=notifier_config['agent_id'],
                                config=notifier_config['config'])
        return agent.notify(subject=subject,
                            body=body,
                            action=notify_action.split('on_')[-1],
                            notification_id=notification_id,
                            **kwargs)
    else:
        logger.debug("Tautulli Notifiers :: Notification requested but no notifier_id received.")


def validate_conditions(custom_conditions):
    if custom_conditions is None:
        return DEFAULT_CUSTOM_CONDITIONS

    try:
        conditions = json.loads(custom_conditions)
    except ValueError:
        logger.error("Tautulli Notifiers :: Unable to parse custom conditions json: %s" % custom_conditions)
        return False

    if not isinstance(conditions, list):
        logger.error("Tautulli Notifiers :: Invalid custom conditions: %s. Conditions must be a list." % conditions)
        return False

    validated_conditions = []

    for condition in conditions:
        validated_condition = DEFAULT_CUSTOM_CONDITIONS[0].copy()

        if not isinstance(condition, dict):
            logger.error("Tautulli Notifiers :: Invalid custom condition: %s. Condition must be a dict." % condition)
            return False

        parameter = str(condition.get('parameter', '')).lower()
        operator = str(condition.get('operator', '')).lower()
        values = condition.get('value', [])

        if parameter:
            parameter_type = common.NOTIFICATION_PARAMETERS_TYPES.get(parameter)
        
            if not parameter_type:
                logger.error("Tautulli Notifiers :: Invalid parameter '%s' in custom condition: %s" % (parameter, condition))
                return False

            validated_condition['parameter'] = parameter.lower()
            validated_condition['type'] = parameter_type

            if operator:
                if operator not in CUSTOM_CONDITION_TYPE_OPERATORS.get(parameter_type, []):
                    logger.error("Tautulli Notifiers :: Invalid operator '%s' for parameter '%s' in custom condition: %s" % (operator, parameter, condition))
                    return False

                validated_condition['operator'] = operator

        if values:
            if not isinstance(values, list):
                values = [values]

            for value in values:
                if not isinstance(value, (str, int, float)):
                    logger.error("Tautulli Notifiers :: Invalid value '%s' for parameter '%s' in custom condition: %s" % (value, parameter, condition))
                    return False
            
            validated_condition['value'] = values

        validated_conditions.append(validated_condition)

    return validated_conditions


def blacklist_logger():
    db = database.MonitorDatabase()
    notifiers = db.select('SELECT notifier_config FROM notifiers')

    for n in notifiers:
        config = json.loads(n['notifier_config'] or '{}')
        logger.blacklist_config(config)


class PrettyMetadata(object):
    def __init__(self, parameters=None):
        self.parameters = parameters or {}
        self.media_type = self.parameters.get('media_type')

    @staticmethod
    def get_movie_providers():
        return {'': '',
                'plexweb': 'Plex Web',
                'imdb': 'IMDB',
                'themoviedb': 'The Movie Database',
                'thetvdb': 'TheTVDB',
                'trakt': 'Trakt',
                'simkl': 'Simkl'
                }

    @staticmethod
    def get_tv_providers():
        return {'': '',
                'plexweb': 'Plex Web',
                'imdb': 'IMDB',
                'themoviedb': 'The Movie Database',
                'thetvdb': 'TheTVDB',
                'tvmaze': 'TVmaze',
                'trakt': 'Trakt',
                'simkl': 'Simkl'
                }

    @staticmethod
    def get_music_providers():
        return {'': '',
                'plexweb': 'Plex Web',
                'lastfm': 'Last.fm',
                'musicbrainz': 'MusicBrainz'
                }

    def get_poster_url(self):
        poster_url = self.parameters.get('poster_url')
        if not poster_url:
            if self.media_type in ('artist', 'album', 'track'):
                poster_url = common.ONLINE_COVER_THUMB
            else:
                poster_url = common.ONLINE_POSTER_THUMB
        return poster_url

    def get_provider_name(self, provider):
        provider_name = ''
        if provider == 'plexweb':
            provider_name = 'Plex Web'
        elif provider == 'imdb':
            provider_name = 'IMDb'
        elif provider == 'thetvdb':
            provider_name = 'TheTVDB'
        elif provider == 'themoviedb':
            provider_name = 'The Movie Database'
        elif provider == 'tvmaze':
            provider_name = 'TVmaze'
        elif provider == 'trakt':
            provider_name = 'Trakt'
        elif provider == 'simkl':
            provider_name = 'Simkl'
        elif provider == 'lastfm':
            provider_name = 'Last.fm'
        elif provider == 'musicbrainz':
            provider_name = 'MusicBrainz'
        # else:
        #     if self.media_type == 'movie':
        #         provider_name = 'IMDb'
        #     elif self.media_type in ('show', 'season', 'episode'):
        #         provider_name = 'TheTVDB'
        #     elif self.media_type in ('artist', 'album', 'track'):
        #         provider_name = 'Last.fm'
        return provider_name

    def get_provider_link(self, provider=None):
        provider_link = ''
        if provider == 'plexweb':
            provider_link = self.get_plex_url()
        elif provider:
            provider_link = self.parameters.get(provider + '_url', '')
        # else:
        #     if self.media_type == 'movie':
        #         provider_link = self.parameters.get('imdb_url', '')
        #     elif self.media_type in ('show', 'season', 'episode'):
        #         provider_link = self.parameters.get('thetvdb_url', '')
        #     elif self.media_type in ('artist', 'album', 'track'):
        #         provider_link = self.parameters.get('lastfm_url', '')
        return provider_link

    def get_caption(self, provider):
        provider_name = self.get_provider_name(provider)
        return 'View on ' + provider_name

    def get_title(self, divider='-'):
        title = ''
        if self.media_type == 'movie':
            title = '%s (%s)' % (self.parameters['title'], self.parameters['year'])
        elif self.media_type == 'show':
            title = '%s (%s)' % (self.parameters['show_name'], self.parameters['year'])
        elif self.media_type == 'season':
            title = '%s - %s' % (self.parameters['show_name'], self.parameters['season_name'])
        elif self.media_type == 'episode':
            season = helpers.short_season(self.parameters['season_name'])
            title = '%s - %s (%s %s E%s)' % (self.parameters['show_name'],
                                             self.parameters['episode_name'],
                                             season,
                                             divider,
                                             self.parameters['episode_num'])
        elif self.media_type == 'artist':
            title = self.parameters['artist_name']
        elif self.media_type == 'album':
            title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
        elif self.media_type == 'track':
            title = '%s - %s' % (self.parameters['track_name'], self.parameters['track_artist'])
        return title

    def get_description(self):
        if self.media_type == 'track':
            description = self.parameters['album_name']
        else:
            description = self.parameters['summary']
        return description

    def get_plex_url(self):
        return self.parameters['plex_url']

    @staticmethod
    def get_parameters():
        parameters = {param['value']: param['name']
                for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']}
        parameters[''] = ''
        return parameters

    def get_image(self):
        result = pmsconnect.PmsConnect().get_image(img=self.parameters.get('poster_thumb', ''))
        if result and result[0]:
            poster_content = result[0]
            poster_filename = 'poster_{}.png'.format(self.parameters['rating_key'])
            return (poster_filename, poster_content, 'image/png')
        
        logger.error("Tautulli Notifiers :: Unable to retrieve image for notification.")


class Notifier(object):
    NAME = ''
    _DEFAULT_CONFIG = {}

    def __init__(self, config=None):
        self.config = self.set_config(config=config, default=self._DEFAULT_CONFIG)

    def set_config(self, config=None, default=None):
        return self._validate_config(config=config, default=default)

    def _validate_config(self, config=None, default=None):
        if config is None:
            return default

        new_config = {}
        for k, v in default.items():
            if isinstance(v, int):
                new_config[k] = helpers.cast_to_int(config.get(k, v))
            elif isinstance(v, list):
                c = config.get(k, v)
                if not isinstance(c, list):
                    new_config[k] = [c]
                else:
                    new_config[k] = c
            else:
                new_config[k] = config.get(k, v)

        return new_config

    def return_default_config(self):
        return self._DEFAULT_CONFIG.copy()

    def notify(self, subject='', body='', action='', **kwargs):
        if self.NAME not in ('Script', 'Webhook'):
            if not subject and self.config.get('incl_subject', True):
                logger.error("Tautulli Notifiers :: %s notification subject cannot be blank." % self.NAME)
                return
            elif not body:
                logger.error("Tautulli Notifiers :: %s notification body cannot be blank." % self.NAME)
                return

        return self.agent_notify(subject=subject, body=body, action=action, **kwargs)

    def agent_notify(self, subject='', body='', action='', **kwargs):
        pass

    def make_request(self, url, method='POST', **kwargs):
        logger.info("Tautulli Notifiers :: Sending {name} notification...".format(name=self.NAME))
        response, err_msg, req_msg = request.request_response2(url, method, **kwargs)

        if response and not err_msg:
            logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
            return True

        else:
            verify_msg = ""
            if response is not None and 400 <= response.status_code < 500:
                verify_msg = " Verify your notification agent settings are correct."

            logger.error("Tautulli Notifiers :: {name} notification failed.{msg}".format(msg=verify_msg, name=self.NAME))

            if err_msg:
                logger.error("Tautulli Notifiers :: {}".format(err_msg))

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

            return False

    def return_config_options(self, mask_passwords=False):
        config_options = self._return_config_options()

        # Mask password config options
        if mask_passwords:
            helpers.mask_config_passwords(config_options)

        return config_options

    def _return_config_options(self):
        config_options = []
        return config_options


class BOXCAR(Notifier):
    """
    Boxcar notifications
    """
    NAME = 'Boxcar'
    _DEFAULT_CONFIG = {'token': '',
                       'sound': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {'user_credentials': self.config['token'],
                'notification[title]': subject,
                'notification[long_message]': body,
                'notification[sound]': self.config['sound']
                }

        return self.make_request('https://new.boxcar.io/api/notifications', params=data)

    def get_sounds(self):
        sounds = {'': '',
                  'beep-crisp': 'Beep (Crisp)',
                  'beep-soft': 'Beep (Soft)',
                  'bell-modern': 'Bell (Modern)',
                  'bell-one-tone': 'Bell (One Tone)',
                  'bell-simple': 'Bell (Simple)',
                  'bell-triple': 'Bell (Triple)',
                  'bird-1': 'Bird (1)',
                  'bird-2': 'Bird (2)',
                  'boing': 'Boing',
                  'cash': 'Cash',
                  'clanging': 'Clanging',
                  'detonator-charge': 'Detonator Charge',
                  'digital-alarm': 'Digital Alarm',
                  'done': 'Done',
                  'echo': 'Echo',
                  'flourish': 'Flourish',
                  'harp': 'Harp',
                  'light': 'Light',
                  'magic-chime':'Magic Chime',
                  'magic-coin': 'Magic Coin',
                  'no-sound': 'No Sound',
                  'notifier-1': 'Notifier (1)',
                  'notifier-2': 'Notifier (2)',
                  'notifier-3': 'Notifier (3)',
                  'orchestral-long': 'Orchestral (Long)',
                  'orchestral-short': 'Orchestral (Short)',
                  'score': 'Score',
                  'success': 'Success',
                  'up': 'Up'}

        return sounds

    def _return_config_options(self):
        config_option = [{'label': 'Boxcar Access Token',
                          'value': self.config['token'],
                          'name': 'boxcar_token',
                          'description': 'Your Boxcar access token.',
                          'input_type': 'token'
                          },
                         {'label': 'Sound',
                          'value': self.config['sound'],
                          'name': 'boxcar_sound',
                          'description': 'Set the notification sound. Leave blank for the default sound.',
                          'input_type': 'select',
                          'select_options': self.get_sounds()
                          }
                         ]

        return config_option


class BROWSER(Notifier):
    """
    Browser notifications
    """
    NAME = 'Browser'
    _DEFAULT_CONFIG = {'auto_hide_delay': 5
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
        return True

    def _return_config_options(self):
        config_option = [{'label': 'Note',
                          'description': 'You may need to refresh the page after saving for changes to take effect.',
                          'input_type': 'help'
                          },
                         {'label': 'Allow Notifications',
                          'value': 'Allow Notifications',
                          'name': 'browser_allow_browser',
                          'description': 'Click to allow browser notifications. '
                                         'You must click this button for each browser.',
                          'input_type': 'button'
                          },
                         {'label': 'Auto Hide Delay',
                          'value': self.config['auto_hide_delay'],
                          'name': 'browser_auto_hide_delay',
                          'description': 'Set the number of seconds for the notification to remain visible. '
                                         'Set 0 to disable auto hiding. (Note: Some browsers have a maximum time limit.)',
                          'input_type': 'number'
                          }
                         ]

        return config_option


class DISCORD(Notifier):
    """
    Discord Notifications
    """
    NAME = 'Discord'
    _DEFAULT_CONFIG = {'hook': '',
                       'username': '',
                       'avatar_url': '',
                       'color': '',
                       'tts': 0,
                       'incl_subject': 1,
                       'incl_card': 0,
                       'incl_description': 1,
                       'incl_thumbnail': 0,
                       'incl_pmslink': 0,
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        if self.config['incl_subject']:
            text = subject + '\r\n' + body
        else:
            text = body

        data = {'content': text}
        if self.config['username']:
            data['username'] = self.config['username']
        if self.config['avatar_url']:
            data['avatar_url'] = self.config['avatar_url']
        if self.config['tts']:
            data['tts'] = True

        files = {}

        if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            image = pretty_metadata.get_image()
            if image:
                files = {'files[0]': image}

            if pretty_metadata.media_type == 'movie':
                provider = self.config['movie_provider']
            elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                provider = self.config['tv_provider']
            elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                provider = self.config['music_provider']
            else:
                provider = None

            poster_url = pretty_metadata.get_poster_url()
            provider_name = pretty_metadata.get_provider_name(provider)
            provider_link = pretty_metadata.get_provider_link(provider)
            title = pretty_metadata.get_title('\u00B7')
            description = pretty_metadata.get_description()
            plex_url = pretty_metadata.get_plex_url()

            # Build Discord post attachment
            attachment = {'title': title,
                          'timestamp': pretty_metadata.parameters['utctime']
                          }

            if self.config['color']:
                hex_match = re.match(r'^#([0-9a-fA-F]{3}){1,2}$', self.config['color'])
                if hex_match:
                    hex = hex_match.group(0).lstrip('#')
                    hex = ''.join(h * 2 for h in hex) if len(hex) == 3 else hex
                    attachment['color'] = helpers.hex_to_int(hex)

            if self.config['incl_thumbnail']:
                attachment['thumbnail'] = {'url': 'attachment://{}'.format(image[0]) if image else poster_url}
            else:
                attachment['image'] = {'url': 'attachment://{}'.format(image[0]) if image else poster_url}

            if self.config['incl_description']:
                attachment['description'] = description[:2045] + (description[2045:] and '...')

            fields = []
            if provider_link:
                attachment['url'] = provider_link
                fields.append({'name': 'View Details',
                               'value': '[%s](%s)' % (provider_name, provider_link),
                               'inline': True})
            if self.config['incl_pmslink']:
                fields.append({'name': 'View Details',
                               'value': '[Plex Web](%s)' % plex_url,
                               'inline': True})
            if fields:
                attachment['fields'] = fields

            data['embeds'] = [attachment]

        params = {'wait': True}

        if files:
            files['payload_json'] = (None, json.dumps(data), 'application/json')
            return self.make_request(self.config['hook'], params=params, files=files)
        else:
            return self.make_request(self.config['hook'], params=params, json=data)

    def _return_config_options(self):
        config_option = [{'label': 'Discord Webhook URL',
                          'value': self.config['hook'],
                          'name': 'discord_hook',
                          'description': 'Your Discord incoming webhook URL.',
                          'input_type': 'token'
                          },
                         {'label': 'Discord Username',
                          'value': self.config['username'],
                          'name': 'discord_username',
                          'description': 'The Discord username which will be used. Leave blank for webhook integration default.',
                          'input_type': 'text'
                          },
                         {'label': 'Discord Avatar',
                          'value': self.config['avatar_url'],
                          'description': 'The image url for the avatar which will be used. Leave blank for webhook integration default.',
                          'name': 'discord_avatar_url',
                          'input_type': 'text'
                          },
                         {'label': 'Discord Color',
                          'value': self.config['color'],
                          'description': 'The hex color value (starting with \'#\') for the border along the left side of the message attachment.',
                          'name': 'discord_color',
                          'input_type': 'text'
                          },
                         {'label': 'TTS',
                          'value': self.config['tts'],
                          'name': 'discord_tts',
                          'description': 'Send the notification using text-to-speech.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'discord_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Rich Metadata Info',
                          'value': self.config['incl_card'],
                          'name': 'discord_incl_card',
                          'description': 'Include an info card with a poster and metadata with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Summary',
                          'value': self.config['incl_description'],
                          'name': 'discord_incl_description',
                          'description': 'Include a summary for the media on the info card.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Link to Plex Web',
                          'value': self.config['incl_pmslink'],
                          'name': 'discord_incl_pmslink',
                          'description': 'Include a second link to the media in Plex Web on the info card.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Use Poster Thumbnail',
                          'value': self.config['incl_thumbnail'],
                          'name': 'discord_incl_thumbnail',
                          'description': 'Use a thumbnail instead of a full sized poster on the info card.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'discord_movie_provider',
                          'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'discord_tv_provider',
                          'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'discord_music_provider',
                          'description': 'Select the source for music links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


class EMAIL(Notifier):
    """
    Email notifications
    """
    NAME = 'Email'
    _DEFAULT_CONFIG = {'from_name': 'Tautulli',
                       'from': '',
                       'to': [],
                       'cc': [],
                       'bcc': [],
                       'smtp_server': '',
                       'smtp_port': 465,
                       'smtp_user': '',
                       'smtp_password': '',
                       'tls': 2,
                       'html_support': 1
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        if not self.config['smtp_server']:
            logger.error("Tautulli Notifiers :: %s notification failed: %s",
                         self.NAME, "Missing SMTP server")
            return False

        if self.config['html_support']:
            plain = MIMEText(None, 'plain', 'utf-8')
            plain.replace_header('Content-Transfer-Encoding', 'quoted-printable')
            plain.set_payload(kwargs.get('plaintext', bleach.clean(body, strip=True)), 'utf-8')

            html = MIMEText(body, 'html', 'utf-8')

            msg = MIMEMultipart('alternative')
            msg.attach(plain)
            msg.attach(html)
        else:
            msg = MIMEText(None, 'plain', 'utf-8')
            msg.replace_header('Content-Transfer-Encoding', 'quoted-printable')
            msg.set_payload(body, 'utf-8')

        msg_id = kwargs.get('msg_id', email.utils.make_msgid())
        reply_msg_id = kwargs.get('reply_msg_id')

        msg['Message-ID'] = msg_id
        msg['Date'] = email.utils.formatdate(localtime=True)
        msg['Subject'] = subject
        msg['From'] = email.utils.formataddr((self.config['from_name'], self.config['from']))
        msg['To'] = ','.join(self.config['to'])
        msg['CC'] = ','.join(self.config['cc'])

        if reply_msg_id:
            msg["In-Reply-To"] = reply_msg_id
            msg["References"] = reply_msg_id

        recipients = self.config['to'] + self.config['cc'] + self.config['bcc']

        mailserver = None
        success = False

        try:
            if self.config['tls'] == 2:
                mailserver = smtplib.SMTP_SSL(self.config['smtp_server'], self.config['smtp_port'])
            else:
                mailserver = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])

            mailserver.ehlo()

            if self.config['tls'] == 1:
                mailserver.starttls()
                mailserver.ehlo()

            if self.config['smtp_user']:
                mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))

            mailserver.sendmail(self.config['from'], recipients, msg.as_string())
            logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
            success = True

        except Exception as e:
            logger.error("Tautulli Notifiers :: %s notification failed: %s", self.NAME, e)

        finally:
            if mailserver:
                mailserver.quit()

        return success

    def get_user_emails(self):
        emails = {u['email']: u['friendly_name'] for u in users.Users().get_users() if u['email']}

        user_emails_to = {v: '' for v in self.config['to']}
        user_emails_cc = {v: '' for v in self.config['cc']}
        user_emails_bcc = {v: '' for v in self.config['bcc']}

        user_emails_to.update(emails)
        user_emails_cc.update(emails)
        user_emails_bcc.update(emails)

        user_emails_to = [{'value': k, 'text': v} for k, v in user_emails_to.items()]
        user_emails_cc = [{'value': k, 'text': v} for k, v in user_emails_cc.items()]
        user_emails_bcc = [{'value': k, 'text': v} for k, v in user_emails_bcc.items()]

        return user_emails_to, user_emails_cc, user_emails_bcc

    def _return_config_options(self):
        user_emails_to, user_emails_cc, user_emails_bcc = self.get_user_emails()

        config_option = [{'label': 'From Name',
                          'value': self.config['from_name'],
                          'name': 'email_from_name',
                          'description': 'The name of the sender.',
                          'input_type': 'text'
                          },
                         {'label': 'From',
                          'value': self.config['from'],
                          'name': 'email_from',
                          'description': 'The email address of the sender.',
                          'input_type': 'text'
                          },
                         {'label': 'To',
                          'value': self.config['to'],
                          'name': 'email_to',
                          'description': 'The email address(es) of the recipients.',
                          'input_type': 'selectize',
                          'select_options': user_emails_to,
                          'select_all': True
                          },
                         {'label': 'CC',
                          'value': self.config['cc'],
                          'name': 'email_cc',
                          'description': 'The email address(es) to CC.',
                          'input_type': 'selectize',
                          'select_options': user_emails_cc,
                          'select_all': True
                          },
                         {'label': 'BCC',
                          'value': self.config['bcc'],
                          'name': 'email_bcc',
                          'description': 'The email address(es) to BCC.',
                          'input_type': 'selectize',
                          'select_options': user_emails_bcc,
                          'select_all': True
                          },
                         {'label': 'SMTP Server',
                          'value': self.config['smtp_server'],
                          'name': 'email_smtp_server',
                          'description': 'Host for the SMTP server.',
                          'input_type': 'text'
                          },
                         {'label': 'SMTP Port',
                          'value': self.config['smtp_port'],
                          'name': 'email_smtp_port',
                          'description': 'Port for the SMTP server.',
                          'input_type': 'number'
                          },
                         {'label': 'SMTP Username',
                          'value': self.config['smtp_user'],
                          'name': 'email_smtp_user',
                          'description': 'Username for the SMTP server.',
                          'input_type': 'text'
                          },
                         {'label': 'SMTP Password',
                          'value': self.config['smtp_password'],
                          'name': 'email_smtp_password',
                          'description': 'Password for the SMTP server.',
                          'input_type': 'password'
                          },
                         {'label': 'Encryption',
                          'value': self.config['tls'],
                          'name': 'email_tls',
                          'description': 'Send emails encrypted using SSL or TLS.',
                          'input_type': 'select',
                          'select_options': {0: 'None',
                                             1: 'TLS/STARTTLS (Typically port 587)',
                                             2: 'SSL/TLS (Typically port 465)'}
                          },
                         {'label': 'Enable HTML Support',
                          'value': self.config['html_support'],
                          'name': 'email_html_support',
                          'description': 'Style your messages using HTML tags.',
                          'input_type': 'checkbox'
                          }
                         ]

        return config_option


class FACEBOOK(Notifier):
    """
    Facebook notifications
    """
    NAME = 'Facebook'
    _DEFAULT_CONFIG = {'redirect_uri': '',
                       'access_token': '',
                       'app_id': '',
                       'app_secret': '',
                       'group_id': '',
                       'incl_subject': 1,
                       'incl_card': 0,
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def _get_authorization(self, app_id='', app_secret='', redirect_uri=''):
        # Temporarily store settings in the config so we can retrieve them in Facebook step 2.
        # Assume the user won't be requesting authorization for multiple Facebook notifiers at the same time.
        plexpy.CONFIG.FACEBOOK_APP_ID = app_id
        plexpy.CONFIG.FACEBOOK_APP_SECRET = app_secret
        plexpy.CONFIG.FACEBOOK_REDIRECT_URI = redirect_uri
        plexpy.CONFIG.FACEBOOK_TOKEN = 'temp'

        return facebook.auth_url(app_id=app_id,
                                 canvas_url=redirect_uri,
                                 perms=['publish_to_groups'])

    def _get_credentials(self, code=''):
        logger.info("Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME))

        app_id = plexpy.CONFIG.FACEBOOK_APP_ID
        app_secret = plexpy.CONFIG.FACEBOOK_APP_SECRET
        redirect_uri = plexpy.CONFIG.FACEBOOK_REDIRECT_URI

        try:
            # Request user access token
            api = facebook.GraphAPI(version='2.12')
            response = api.get_access_token_from_code(code=code,
                                                      redirect_uri=redirect_uri,
                                                      app_id=app_id,
                                                      app_secret=app_secret)
            access_token = response['access_token']

            # Request extended user access token
            api = facebook.GraphAPI(access_token=access_token, version='2.12')
            response = api.extend_access_token(app_id=app_id,
                                               app_secret=app_secret)

            plexpy.CONFIG.FACEBOOK_TOKEN = response['access_token']
        except Exception as e:
            logger.error("Tautulli Notifiers :: Error requesting {name} access token: {e}".format(name=self.NAME, e=e))
            plexpy.CONFIG.FACEBOOK_TOKEN = ''

        # Clear out temporary config values
        plexpy.CONFIG.FACEBOOK_APP_ID = ''
        plexpy.CONFIG.FACEBOOK_APP_SECRET = ''
        plexpy.CONFIG.FACEBOOK_REDIRECT_URI = ''

        return plexpy.CONFIG.FACEBOOK_TOKEN

    def _post_facebook(self, **data):
        if self.config['group_id']:
            api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.12')

            try:
                api.put_object(parent_object=self.config['group_id'], connection_name='feed', **data)
                logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
                return True
            except Exception as e:
                logger.error("Tautulli Notifiers :: Error sending {name} post: {e}".format(name=self.NAME, e=e))
                return False

        else:
            logger.error("Tautulli Notifiers :: Error sending {name} post: No {name} Group ID provided.".format(name=self.NAME))
            return False

    def agent_notify(self, subject='', body='', action='', **kwargs):
        if self.config['incl_subject']:
            text = subject + '\r\n' + body
        else:
            text = body

        data = {'message': text}

        if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            if pretty_metadata.media_type == 'movie':
                provider = self.config['movie_provider']
            elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                provider = self.config['tv_provider']
            elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                provider = self.config['music_provider']
            else:
                provider = None

            data['link'] = pretty_metadata.get_provider_link(provider)

        return self._post_facebook(**data)

    def _return_config_options(self):
        config_option = [{'label': 'OAuth Redirect URI',
                          'value': self.config['redirect_uri'],
                          'name': 'facebook_redirect_uri',
                          'description': 'Fill in this address for the "Valid OAuth redirect URIs" '
                                         'in your Facebook App.',
                          'input_type': 'text'
                          },
                         {'label': 'Facebook App ID',
                          'value': self.config['app_id'],
                          'name': 'facebook_app_id',
                          'description': 'Your Facebook app ID.',
                          'input_type': 'token'
                          },
                         {'label': 'Facebook App Secret',
                          'value': self.config['app_secret'],
                          'name': 'facebook_app_secret',
                          'description': 'Your Facebook app secret.',
                          'input_type': 'token'
                          },
                         {'label': 'Request Authorization',
                          'value': 'Request Authorization',
                          'name': 'facebook_facebook_auth',
                          'description': 'Request Facebook authorization. (Ensure you allow the browser pop-up).',
                          'input_type': 'button'
                          },
                         {'label': 'Facebook Access Token',
                          'value': self.config['access_token'],
                          'name': 'facebook_access_token',
                          'description': 'Your Facebook access token. '
                                         'Automatically filled in after requesting authorization.',
                          'input_type': 'token'
                          },
                         {'label': 'Facebook Group ID',
                          'value': self.config['group_id'],
                          'name': 'facebook_group_id',
                          'description': 'Your Facebook Group ID.',
                          'input_type': 'text'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'facebook_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Rich Metadata Info',
                          'value': self.config['incl_card'],
                          'name': 'facebook_incl_card',
                          'description': 'Include an info card with a poster and metadata with the notifications.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
                                         'data-target="notify_upload_posters">Image Hosting</a> '
                                         'must be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'facebook_movie_provider',
                          'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'facebook_tv_provider',
                          'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'facebook_music_provider',
                          'description': 'Select the source for music links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


class GOTIFY(Notifier):
    """
    Gotify notifications
    """
    NAME = 'Gotify'
    _DEFAULT_CONFIG = {'host': '',
                       'app_token': '',
                       'priority': 0,
                       'incl_subject': 1,
                       'incl_poster': 0,
                       'incl_url': 1,
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {
            'extras': {
                'client::display': {
                    'contentType': 'text/markdown'
                }
            },
            'message': body,
            'priority': self.config['priority']
        }

        if self.config['incl_subject']:
            data['title'] = subject

        headers = {'X-Gotify-Key': self.config['app_token']}

        if kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            if self.config['incl_url']:
                if pretty_metadata.media_type == 'movie':
                    provider = self.config['movie_provider']
                elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                    provider = self.config['tv_provider']
                elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                    provider = self.config['music_provider']
                else:
                    provider = None

                provider_link = pretty_metadata.get_provider_link(provider)
                data['extras']['client::notification'] = {'click': {'url': provider_link}}

            if self.config['incl_poster']:
                poster_url = pretty_metadata.get_poster_url()
                data['message'] += '\n\n![]({})'.format(poster_url)

        return self.make_request('{}/message'.format(self.config['host']), headers=headers, json=data)

    def _return_config_options(self):
        config_option = [{'label': 'Gotify Host Address',
                          'value': self.config['host'],
                          'name': 'gotify_host',
                          'description': 'Host running Gotify (e.g. http://localhost:8080).',
                          'input_type': 'text'
                          },
                         {'label': 'Gotify App Token',
                          'value': self.config['app_token'],
                          'name': 'gotify_app_token',
                          'description': 'Your Gotify app token.',
                          'input_type': 'token'
                          },
                         {'label': 'Priority',
                          'value': self.config['priority'],
                          'name': 'gotify_priority',
                          'description': 'Set the notification priority.',
                          'input_type': 'select',
                          'select_options': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'gotify_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Poster Image',
                          'value': self.config['incl_poster'],
                          'name': 'gotify_incl_poster',
                          'description': 'Include a poster with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Open URL on Notification Click (Android Only)',
                          'value': self.config['incl_url'],
                          'name': 'gotify_incl_url',
                          'description': 'Open a URL instead of the Gotify app when clicking on the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'gotify_movie_provider',
                          'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'gotify_tv_provider',
                          'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'gotify_music_provider',
                          'description': 'Select the source for music links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


class GROUPME(Notifier):
    """
    GroupMe notifications
    """
    NAME = 'GroupMe'
    _DEFAULT_CONFIG = {'access_token': '',
                       'bot_id': '',
                       'incl_subject': 1,
                       'incl_poster': 0
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {'bot_id': self.config['bot_id']}

        if self.config['incl_subject']:
            data['text'] = subject + '\r\n' + body
        else:
            data['text'] = body

        if self.config['incl_poster'] and kwargs.get('parameters'):
            pretty_metadata = PrettyMetadata(kwargs.get('parameters'))

            image = pretty_metadata.get_image()
            if image:
                headers = {'X-Access-Token': self.config['access_token'],
                           'Content-Type': 'image/png'}

                r = requests.post('https://image.groupme.com/pictures', headers=headers, data=image[1])

                if r.status_code == 200:
                    logger.info("Tautulli Notifiers :: {name} poster sent.".format(name=self.NAME))
                    r_content = r.json()
                    data['attachments'] = [{'type': 'image',
                                            'url': r_content['payload']['picture_url']}]
                else:
                    logger.error("Tautulli Notifiers :: {name} poster failed: "
                                 "[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
                    logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))

        return self.make_request('https://api.groupme.com/v3/bots/post', json=data)

    def _return_config_options(self):
        config_option = [{'label': 'GroupMe Access Token',
                          'value': self.config['access_token'],
                          'name': 'groupme_access_token',
                          'description': 'Your GroupMe access token.',
                          'input_type': 'token'
                          },
                         {'label': 'GroupMe Bot ID',
                          'value': self.config['bot_id'],
                          'name': 'groupme_bot_id',
                          'description': 'Your GroupMe bot ID.',
                          'input_type': 'token'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'groupme_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Poster Image',
                          'value': self.config['incl_poster'],
                          'name': 'groupme_incl_poster',
                          'description': 'Include a poster with the notifications.',
                          'input_type': 'checkbox'
                          }
                         ]

        return config_option


class GROWL(Notifier):
    """
    Growl notifications, for OS X.
    """
    NAME = 'Growl'
    _DEFAULT_CONFIG = {'host': '',
                       'password': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        # Split host and port
        if self.config['host'] == "":
            host, port = "localhost", 23053
        if ":" in self.config['host']:
            host, port = self.config['host'].split(':', 1)
            port = int(port)
        else:
            host, port = self.config['host'], 23053

        # If password is empty, assume none
        if self.config['password'] == "":
            password = None
        else:
            password = self.config['password']

        # Register notification
        growl = gntp.notifier.GrowlNotifier(
            applicationName='Tautulli',
            notifications=['New Event'],
            defaultNotifications=['New Event'],
            hostname=host,
            port=port,
            password=password
        )

        try:
            growl.register()
        except gntp.notifier.errors.NetworkError:
            logger.error("Tautulli Notifiers :: {name} notification failed: network error".format(name=self.NAME))
            return False
        except gntp.notifier.errors.AuthError:
            logger.error("Tautulli Notifiers :: {name} notification failed: authentication error".format(name=self.NAME))
            return False

        # Send it, including an image
        image_file = os.path.join(str(plexpy.PROG_DIR),
            "data/interfaces/default/images/logo-circle.png")

        with open(image_file, 'rb') as f:
            image = f.read()

        try:
            growl.notify(
                noteType='New Event',
                title=subject,
                description=body,
                icon=image
            )
            logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
            return True
        except gntp.notifier.errors.NetworkError:
            logger.error("Tautulli Notifiers :: {name} notification failed: network error".format(name=self.NAME))
            return False

    def _return_config_options(self):
        config_option = [{'label': 'Growl Host',
                          'value': self.config['host'],
                          'name': 'growl_host',
                          'description': 'Your Growl hostname or IP address.',
                          'input_type': 'text'
                          },
                         {'label': 'Growl Password',
                          'value': self.config['password'],
                          'name': 'growl_password',
                          'description': 'Your Growl password.',
                          'input_type': 'password'
                          }
                         ]

        return config_option


class IFTTT(Notifier):
    """
    IFTTT notifications
    """
    NAME = 'IFTTT'
    _DEFAULT_CONFIG = {'key': '',
                       'event': 'tautulli',
                       'value3': '',
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        event = str(self.config['event']).format(action=action)

        data = {'value1': subject,
                'value2': body}

        if self.config['value3']:
            pretty_metadata = PrettyMetadata(kwargs['parameters'])
            data['value3'] = pretty_metadata.parameters.get(self.config['value3'], '')

        headers = {'Content-type': 'application/json'}

        return self.make_request('https://maker.ifttt.com/trigger/{}/with/key/{}'.format(event, self.config['key']),
                                 headers=headers, json=data)

    def _return_config_options(self):
        config_option = [{'label': 'IFTTT Webhook Key',
                          'value': self.config['key'],
                          'name': 'ifttt_key',
                          'description': 'Your IFTTT webhook key. You can get a key from'
                                         ' <a href="' + helpers.anon_url('https://ifttt.com/maker_webhooks') +
                                         '" target="_blank" rel="noreferrer">here</a>.',
                          'input_type': 'token'
                          },
                         {'label': 'IFTTT Event',
                          'value': self.config['event'],
                          'name': 'ifttt_event',
                          'description': 'The IFTTT maker event to fire. You can include'
                                         ' <span class="inline-pre">{action}</span>'
                                         ' to be substituted with the action name.'
                                         ' The notification subject and body will be sent'
                                         ' as <span class="inline-pre">value1</span>'
                                         ' and <span class="inline-pre">value2</span> respectively.',
                          'input_type': 'text'
                          },
                         {'label': 'Value 3',
                          'value': self.config['value3'],
                          'name': 'ifttt_value3',
                          'description': 'Optional: Select a parameter to send as <span class="inline-pre">value3</span>.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_parameters()
                          }
                         ]

        return config_option


class JOIN(Notifier):
    """
    Join notifications
    """
    NAME = 'Join'
    _DEFAULT_CONFIG = {'api_key': '',
                       'device_names': [],
                       'priority': 2,
                       'incl_subject': 1,
                       'incl_poster': 0,
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {'apikey': self.config['api_key'],
                'deviceNames': ','.join(self.config['device_names']),
                'text': body}

        if self.config['incl_subject']:
            data['title'] = subject

        if kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            poster_url = pretty_metadata.get_poster_url()
            if poster_url and self.config['incl_poster']:
                data['icon'] = poster_url

            if pretty_metadata.media_type == 'movie':
                provider = self.config['movie_provider']
            elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                provider = self.config['tv_provider']
            elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                provider = self.config['music_provider']
            else:
                provider = None

            provider_link = pretty_metadata.get_provider_link(provider)
            if provider_link:
                data['url'] = provider_link

        r = requests.post('https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush', params=data)

        if r.status_code == 200:
            response_data = r.json()
            if response_data.get('success'):
                logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
                return True
            else:
                error_msg = response_data.get('errorMessage')
                logger.error("Tautulli Notifiers :: {name} notification failed: {msg}".format(name=self.NAME, msg=error_msg))
                return False
        else:
            logger.error("Tautulli Notifiers :: {name} notification failed: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
            logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
            return False

    def get_devices(self):
        devices = {d: d for d in self.config['device_names']}
        devices.update({'': ''})

        if self.config['api_key']:
            params = {'apikey': self.config['api_key']}

            try:
                r = requests.get('https://joinjoaomgcd.appspot.com/_ah/api/registration/v1/listDevices', params=params)

                if r.status_code == 200:
                    response_data = r.json()
                    if response_data.get('success'):
                        response_devices = response_data.get('records', [])
                        devices.update({d['deviceName']: d['deviceName'] for d in response_devices})
                    else:
                        error_msg = response_data.get('errorMessage')
                        logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=error_msg))

                else:
                    logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
                    logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))

            except Exception as e:
                logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=e))

        return devices

    def _return_config_options(self):
        config_option = [{'label': 'Join API Key',
                          'value': self.config['api_key'],
                          'name': 'join_api_key',
                          'description': 'Your Join API key. Required for group notifications.',
                          'input_type': 'token',
                          'refresh': True
                          },
                         {'label': 'Device Name(s)',
                          'value': self.config['device_names'],
                          'name': 'join_device_names',
                          'description': 'Select your Join device(s).',
                          'input_type': 'select',
                          'select_options': self.get_devices()
                          },
                         {'label': 'Priority',
                          'value': self.config['priority'],
                          'name': 'join_priority',
                          'description': 'Set the notification priority.',
                          'input_type': 'select',
                          'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'join_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Poster Image',
                          'value': self.config['incl_poster'],
                          'name': 'join_incl_poster',
                          'description': 'Include a poster with the notifications.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
                                         'data-target="notify_upload_posters">Image Hosting</a> '
                                         'must be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'join_movie_provider',
                          'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'join_tv_provider',
                          'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'join_music_provider',
                          'description': 'Select the source for music links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


class LUNASEA(Notifier):
    """
    LunaSea Notifications
    """
    NAME = 'LunaSea'
    _DEFAULT_CONFIG = {'hook': '',
                       'profile': '',
                       'incl_subject': 1
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        if self.config['incl_subject']:
            text = subject + '\r\n' + body
        else:
            text = body

        if self.config['profile']:
            auth = HTTPBasicAuth(self.config['profile'], '')
        else:
            auth = None

        pretty_metadata = PrettyMetadata(kwargs['parameters'])

        payload = {
            'action': action,
            'data': {
                'message': text,
                'user': pretty_metadata.parameters.get('user'),
                'user_id': pretty_metadata.parameters.get('user_id'),
                'player': pretty_metadata.parameters.get('player'),
                'title': pretty_metadata.get_title(),
                'poster_url': pretty_metadata.get_poster_url(),
                'session_key': pretty_metadata.parameters.get('session_key'),
                'session_id': pretty_metadata.parameters.get('session_id'),
                'user_streams': pretty_metadata.parameters.get('user_streams'),
                'remote_access_reason': pretty_metadata.parameters.get('remote_access_reason'),
                'update_version': pretty_metadata.parameters.get('update_version'),
                'tautulli_update_version': pretty_metadata.parameters.get('tautulli_update_version')
            }
        }

        return self.make_request(self.config['hook'], json=payload, auth=auth)

    def _return_config_options(self):
        config_option = [{'label': 'LunaSea Webhook URL',
                          'value': self.config['hook'],
                          'name': 'lunasea_hook',
                          'description': 'Your LunaSea device-based or user-based webhook URL.',
                          'input_type': 'token'
                          },
                         {'label': 'LunaSea Profile',
                          'value': self.config['profile'],
                          'name': 'lunasea_profile',
                          'description': 'Your LunaSea profile name. Leave blank for the default profile.',
                          'input_type': 'text'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'lunasea_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          }
                         ]

        return config_option


class MICROSOFTTEAMS(Notifier):
    """
    Microsoft Teams Notifications
    """
    NAME = 'Microsoft Teams'
    _DEFAULT_CONFIG = {'hook': '',
                       'incl_subject': 1,
                       'incl_card': 0,
                       'incl_description': 1,
                       'incl_pmslink': 0,
                       'poster_size': 2,
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {
            'type': 'message'
        }
        attachment = {
            'contentType': 'application/vnd.microsoft.card.adaptive'
        }
        content = {
            '$schema': 'http://adaptivecards.io/schemas/adaptive-card.json',
            'type': 'AdaptiveCard',
            'version': '1.4',
        }

        card = []

        if self.config['incl_subject']:
            card.append({
                'type': 'TextBlock',
                'size': 'Large',
                'weight': 'Bolder',
                'text': subject
            })
        card.append({
            'type': 'TextBlock',
            'text': body,
            'wrap': True
        })

        if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            if pretty_metadata.media_type == 'movie':
                provider = self.config['movie_provider']
            elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                provider = self.config['tv_provider']
            elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                provider = self.config['music_provider']
            else:
                provider = None

            poster_url = pretty_metadata.get_poster_url()
            provider_name = pretty_metadata.get_provider_name(provider)
            provider_link = pretty_metadata.get_provider_link(provider)
            title = pretty_metadata.get_title('\u00B7')
            description = pretty_metadata.get_description()
            plex_url = pretty_metadata.get_plex_url()

            columns = []

            if poster_url and self.config['poster_size']:
                columns.append({
                    'type': 'Column',
                    'width': 'auto',
                    'items': [
                        {
                            'type': 'Image',
                            'url': poster_url,
                            'altText': title,
                            'size': 'Large',
                            'height': '{}px'.format(self.config['poster_size'] * 75)
                        }
                    ]
                })
            columns.append({
                'type': 'Column',
                'width': 'stretch',
                'items': []
            })

            columns[-1]['items'].append({
                'type': 'TextBlock',
                'weight': 'Bolder',
                'text': title,
                'wrap': True
            })
            if self.config['incl_description']:
                columns[-1]['items'].append({
                    'type': 'TextBlock',
                    'text': description,
                    'size': 'Small',
                    'spacing': 'Small',
                    'wrap': True
                })

            card.append({
                'type': 'ColumnSet',
                'padding': 'Default',
                'spacing': 'Large',
                'columns': columns
            })

            actions = []

            if provider_link:
                actions.append({
                    'type': 'Action.OpenUrl',
                    'title': 'View on {}'.format(provider_name),
                    'url': provider_link
                })
            if self.config['incl_pmslink']:
                actions.append({
                    'type': 'Action.OpenUrl',
                    'title': 'View on Plex',
                    'url': plex_url
                })

            if actions:
                card.append({
                    'type': 'ActionSet',
                    'actions': actions
                })

        content['body'] = card
        attachment['content'] = content
        data['attachments'] = [attachment]

        headers = {'Content-type': 'application/json'}

        return self.make_request(self.config['hook'], headers=headers, json=data)

    def _return_config_options(self):
        config_option = [{'label': 'Teams Webhook URL',
                          'value': self.config['hook'],
                          'name': 'microsoftteams_hook',
                          'description': 'Your Microsoft Teams incoming webhook URL.',
                          'input_type': 'token'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'microsoftteams_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Rich Metadata Info',
                          'value': self.config['incl_card'],
                          'name': 'microsoftteams_incl_card',
                          'description': 'Include an info card with a poster and metadata with the notifications.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
                                         'data-target="notify_upload_posters">Image Hosting</a> '
                                         'must be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Summary',
                          'value': self.config['incl_description'],
                          'name': 'microsoftteams_incl_description',
                          'description': 'Include a summary for the media on the info card.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Link to Plex Web',
                          'value': self.config['incl_pmslink'],
                          'name': 'microsoftteams_incl_pmslink',
                          'description': 'Include a second link to the media in Plex Web on the info card.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Poster Size',
                          'value': self.config['poster_size'],
                          'name': 'microsoftteams_poster_size',
                          'description': 'Select the size of the poster on the info card.',
                          'input_type': 'select',
                          'select_options': {
                              0: 'None',
                              1: 'Small',
                              2: 'Medium',
                              3: 'Large'}
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'microsoftteams_movie_provider',
                          'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'microsoftteams_tv_provider',
                          'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'microsoftteams_music_provider',
                          'description': 'Select the source for music links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


class MQTT(Notifier):
    """
    MQTT notifications
    """
    NAME = 'MQTT'
    _DEFAULT_CONFIG = {'broker': '',
                       'port': 1883,
                       'protocol': 'MQTTv311',
                       'username': '',
                       'password': '',
                       'clientid': 'tautulli',
                       'topic': '',
                       'qos': 1,
                       'retain': 0,
                       'keep_alive': 60,
                       'as_json': 0
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        topic = self.config['topic']
        if not topic:
            logger.error("Tautulli Notifiers :: MQTT topic not specified.")
            return

        topic = topic.format(**kwargs.get('parameters', {}))

        if self.config['as_json']:
            subject = json.loads(subject)
            body = json.loads(body)

        data = {'subject': subject,
                'body': body,
                'topic': topic}

        auth = {}
        if self.config['username']:
            auth['username'] = self.config['username']
        if self.config['password']:
            auth['password'] = self.config['password']

        protocol = getattr(paho.mqtt.client, self.config['protocol'])

        logger.info("Tautulli Notifiers :: Sending {name} notification...".format(name=self.NAME))

        paho.mqtt.publish.single(
            topic, payload=json.dumps(data), qos=self.config['qos'], retain=bool(self.config['retain']),
            hostname=self.config['broker'], port=self.config['port'], client_id=self.config['clientid'],
            keepalive=self.config['keep_alive'], auth=auth or None, protocol=protocol
        )

        logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))

        return True

    def _return_config_options(self):
        config_option = [{'label': 'Broker',
                          'value': self.config['broker'],
                          'name': 'mqtt_broker',
                          'description': 'The hostname or IP address of the MQTT broker.',
                          'input_type': 'text'
                          },
                         {'label': 'Port',
                          'value': self.config['port'],
                          'name': 'mqtt_port',
                          'description': 'The network port for connecting to the MQTT broker.',
                          'input_type': 'number'
                          },
                         {'label': 'Protocol',
                          'value': self.config['protocol'],
                          'name': 'mqtt_protocol',
                          'description': 'The MQTT protocol version.',
                          'input_type': 'select',
                          'select_options': {'MQTTv31': '3.1',
                                             'MQTTv311': '3.1.1',
                                             'MQTTv5': '5.0'
                                             }
                          },
                         {'label': 'Client ID',
                          'value': self.config['clientid'],
                          'name': 'mqtt_clientid',
                          'description': 'The client ID for connecting to the MQTT broker.',
                          'input_type': 'text'
                          },
                         {'label': 'Username',
                          'value': self.config['username'],
                          'name': 'mqtt_username',
                          'description': 'The username to authenticate with the MQTT broker.',
                          'input_type': 'text'
                          },
                         {'label': 'Password',
                          'value': self.config['password'],
                          'name': 'mqtt_password',
                          'description': 'The password to authenticate with the MQTT broker.',
                          'input_type': 'password'
                          },
                         {'label': 'Topic',
                          'value': self.config['topic'],
                          'name': 'mqtt_topic',
                          'description': 'The topic to publish notifications to. You can include'
                                         ' <a href="#notify-text-sub-modal" data-toggle="modal">notification parameters</a>'
                                         ' to be substituted.',
                          'input_type': 'text'
                          },
                         {'label': 'Quality of Service',
                          'value': self.config['qos'],
                          'name': 'mqtt_qos',
                          'description': 'The quality of service level to use when publishing the notification.',
                          'input_type': 'select',
                          'select_options': {0: 0,
                                             1: 1,
                                             2: 2
                                             }
                          },
                         {'label': 'Retain Message',
                          'value': self.config['retain'],
                          'name': 'mqtt_retain',
                          'description': 'Set the message to be retained on the MQTT broker.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Keep-Alive',
                          'value': self.config['keep_alive'],
                          'name': 'mqtt_keep_alive',
                          'description': 'Maximum period in seconds before timing out the connection with the broker.',
                          'input_type': 'number'
                          },
                         {'label': 'Send Message as JSON',
                          'value': self.config['as_json'],
                          'name': 'mqtt_as_json',
                          'description': 'Parse and send the subject and body as JSON instead of as a raw string.',
                          'input_type': 'checkbox'
                          },
                         ]

        return config_option


class NTFY(Notifier):
    """
    ntfy notifications
    """
    NAME = 'ntfy'
    _DEFAULT_CONFIG = {'host': '',
                       'access_token': '',
                       'topic': '',
                       'priority': 'default',
                       'incl_subject': 1,
                       'incl_description': 1,
                       'incl_poster': 0,
                       'incl_url': 0,
                       'incl_pmslink': 0,
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        method = "POST"
        url = f"{self.config['host']}/{self.config['topic']}"
        params = {}
        data = body.encode('utf-8')
        headers = {
            'Priority': self.config['priority'],
            'Authorization': f'Bearer {self.config["access_token"]}',
            'Icon': 'https://tautulli.com/images/favicon.ico'
        }

        # Add optional subject
        if self.config['incl_subject']:
            headers['Title'] = subject.encode('utf-8')

        # Add optional parameters (dependent on notification type + metadata extraction)
        if kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            # Add optional description
            if self.config['incl_description']:
                description = pretty_metadata.get_description()
                if description:
                    data += f"\n\n{description}".encode('utf-8')

            # Add optional poster
            if self.config['incl_poster']:
                method = "PUT"  # Need to use PUT instead of POST to send attachments
                image_file_name, image_content, image_file_type = pretty_metadata.get_image()

                if image_file_name and image_content:
                    # Image data will take up "data", message content needs to shift to the "message" query parameter
                    params['message'] = data
                    data = image_content
                    headers['Filename'] = image_file_name
                else:
                    # Fallback to default Tautulli media type poster
                    # Can keep message content in "data" payload and just add a header for the attachment
                    poster_url = pretty_metadata.get_poster_url()
                    headers['Attach'] = poster_url

            # Add optional links (actions)
            actions = []

            if self.config['incl_url']:
                if pretty_metadata.media_type == 'movie':
                    provider = self.config['movie_provider']
                elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                    provider = self.config['tv_provider']
                elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                    provider = self.config['music_provider']
                else:
                    provider = None

                provider_name = pretty_metadata.get_provider_name(provider)
                provider_link = pretty_metadata.get_provider_link(provider)
                if provider_link:
                    actions.append(f"view, View on {provider_name}, {provider_link}, clear=true")

            if self.config['incl_pmslink']:
                plex_url = pretty_metadata.get_plex_url()
                actions.append(f"view, View on Plex, {plex_url}, clear=true")

            if actions:
                headers['Actions'] = ';'.join(actions)

        return self.make_request(url=url, method=method, headers=headers, params=params, data=data)

    def _return_config_options(self):
        config_option = [{'label': 'ntfy Host Address',
                          'value': self.config['host'],
                          'name': 'ntfy_host',
                          'description': 'Host running ntfy (e.g. http://localhost:80).',
                          'input_type': 'text'
                          },
                         {'label': 'ntfy Access Token',
                          'value': self.config['access_token'],
                          'name': 'ntfy_access_token',
                          'description': 'Your ntfy access token.',
                          'input_type': 'token'
                          },
                         {'label': 'ntfy Topic',
                          'value': self.config['topic'],
                          'name': 'ntfy_topic',
                          'description': 'The topic to publish notifications to.',
                          'input_type': 'text'
                          },
                         {'label': 'Priority',
                          'value': self.config['priority'],
                          'name': 'ntfy_priority',
                          'description': 'Set the notification priority.',
                          'input_type': 'select',
                          'select_options': {
                              'min': 1,
                              'low': 2,
                              'default': 3,
                              'high': 4,
                              'max': 5
                          }
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'ntfy_incl_subject',
                          'description': 'Include a subject line in the notification.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Poster Image',
                          'value': self.config['incl_poster'],
                          'name': 'ntfy_incl_poster',
                          'description': 'Include a poster of the media item in the notification.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Summary',
                          'value': self.config['incl_description'],
                          'name': 'ntfy_incl_description',
                          'description': 'Include a summary of the media item in the notification.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Link to Metadata Provider',
                          'value': self.config['incl_url'],
                          'name': 'ntfy_incl_url',
                          'description': 'Include a link to the media item on the metadata provider in the notification.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Link to Plex Web',
                          'value': self.config['incl_pmslink'],
                          'name': 'ntfy_incl_pmslink',
                          'description': 'Include a link to the media item in Plex Web in the notification.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'ntfy_movie_provider',
                          'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'ntfy_tv_provider',
                          'description': 'Select the source for TV show links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'ntfy_music_provider',
                          'description': 'Select the source for music links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


class OSX(Notifier):
    """
    macOS notifications
    """
    NAME = 'macOS'
    _DEFAULT_CONFIG = {'notify_app': '/Applications/Tautulli'
                       }

    def __init__(self, config=None):
        super(OSX, self).__init__(config=config)

        try:
            self.objc = __import__("objc")
            self.AppKit = __import__("AppKit")
        except:
            # logger.error("Tautulli Notifiers :: Cannot load OSX Notifications agent.")
            pass

    def validate(self):
        try:
            self.objc = __import__("objc")
            self.AppKit = __import__("AppKit")
            return True
        except:
            return False

    def _swizzle(self, cls, SEL, func):
        old_IMP = cls.instanceMethodForSelector_(SEL)

        def wrapper(self, *args, **kwargs):
            return func(self, old_IMP, *args, **kwargs)
        new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector,
                                     signature=old_IMP.signature)
        self.objc.classAddMethod(cls, SEL, new_IMP)

    def _swizzled_bundleIdentifier(self, original, swizzled):
        return 'ade.tautulli.osxnotify'

    def agent_notify(self, subject='', body='', action='', **kwargs):

        subtitle = kwargs.get('subtitle', '')
        sound = kwargs.get('sound', '')
        image = kwargs.get('image', '')

        try:
            self._swizzle(self.objc.lookUpClass('NSBundle'),
                b'bundleIdentifier',
                self._swizzled_bundleIdentifier)

            NSUserNotification = self.objc.lookUpClass('NSUserNotification')
            NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
            NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')

            if not NSUserNotification or not NSUserNotificationCenter:
                return False

            pool = NSAutoreleasePool.alloc().init()

            notification = NSUserNotification.alloc().init()
            notification.setTitle_(subject)
            if subtitle:
                notification.setSubtitle_(subtitle)
            if body:
                notification.setInformativeText_(body)
            if sound:
                notification.setSoundName_("NSUserNotificationDefaultSoundName")
            if image:
                source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image)
                notification.setContentImage_(source_img)
                # notification.set_identityImage_(source_img)
            notification.setHasActionButton_(False)

            notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
            notification_center.deliverNotification_(notification)
            logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))

            del pool
            return True

        except Exception as e:
            logger.error("Tautulli Notifiers :: {name} failed: {e}".format(name=self.NAME, e=e))
            return False

    def _return_config_options(self):
        config_option = [{'label': 'Register Notify App',
                          'value': self.config['notify_app'],
                          'name': 'osx_notify_app',
                          'description': 'Enter the path/application name to be registered with the Notification Center. '
                                         'Default is <span class="inline-pre">/Applications/Tautulli</span>.',
                          'input_type': 'text'
                          },
                         {'label': 'Register App',
                          'value': 'Register App',
                          'name': 'osx_notify_register',
                          'description': 'Register Tautulli with the Notification Center.',
                          'input_type': 'button'
                          }
                         ]

        return config_option


class PLEX(Notifier):
    """
    Plex Home Theater notifications
    """
    NAME = 'Plex Home Theater'
    _DEFAULT_CONFIG = {'hosts': '',
                       'username': '',
                       'password': '',
                       'display_time': 5,
                       'image': ''
                       }

    def _sendhttp(self, host, command):
        url_command = urlencode(command)
        url = host + '/xbmcCmds/xbmcHttp/?' + url_command

        if self.config['password']:
            return request.request_content(url, auth=(self.config['username'], self.config['password']))
        else:
            return request.request_content(url)

    def _sendjson(self, host, method, params=None):
        params = params or {}
        data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
        headers = {'Content-Type': 'application/json'}
        url = host + '/jsonrpc'

        if self.config['password']:
            response = request.request_json(url, method="post", data=json.dumps(data), headers=headers,
                                            auth=(self.config['username'], self.config['password']))
        else:
            response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)

        if response:
            return response[0]['result']

    def agent_notify(self, subject='', body='', action='', **kwargs):
        hosts = [x.strip() for x in self.config['hosts'].split(',')]

        if self.config['display_time'] > 0:
            display_time = 1000 * self.config['display_time']  # in ms
        else:
            display_time = 5000

        if self.config['image']:
            image = self.config['image']
        else:
            image = os.path.join(plexpy.DATA_DIR, os.path.abspath("data/interfaces/default/images/logo-circle.png"))

        for host in hosts:
            logger.info("Tautulli Notifiers :: Sending notification command to {name} @ {host}".format(name=self.NAME, host=host))
            try:
                version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major']

                if version < 12:  # Eden
                    notification = subject + "," + body + "," + str(display_time)
                    notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
                    request = self._sendhttp(host, notifycommand)

                else:  # Frodo
                    params = {'title': subject, 'message': body, 'displaytime': display_time, 'image': image}
                    request = self._sendjson(host, 'GUI.ShowNotification', params)

                if not request:
                    raise Exception
                else:
                    logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))

            except Exception as e:
                logger.error("Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
                return False

        return True

    def _return_config_options(self):
        config_option = [{'label': 'Plex Home Theater Host Address',
                          'value': self.config['hosts'],
                          'name': 'plex_hosts',
                          'description': 'Host running Plex Home Theater (eg. http://localhost:3005). Separate multiple hosts with commas (,).',
                          'input_type': 'text'
                          },
                         {'label': 'Plex Home Theater Username',
                          'value': self.config['username'],
                          'name': 'plex_username',
                          'description': 'Username of your Plex Home Theater client API (blank for none).',
                          'input_type': 'text'
                          },
                         {'label': 'Plex Home Theater Password',
                          'value': self.config['password'],
                          'name': 'plex_password',
                          'description': 'Password of your Plex Home Theater client API (blank for none).',
                          'input_type': 'password'
                          },
                         {'label': 'Notification Duration',
                          'value': self.config['display_time'],
                          'name': 'plex_display_time',
                          'description': 'The duration (in seconds) for the notification to stay on screen.',
                          'input_type': 'number'
                          },
                         {'label': 'Notification Icon',
                          'value': self.config['image'],
                          'name': 'plex_image',
                          'description': 'Full path or URL to an image to display with the notification. Leave blank for the default.',
                          'input_type': 'text'
                          }
                         ]

        return config_option


class PLEXMOBILEAPP(Notifier):
    """
    Plex Mobile App Notifications
    """
    NAME = 'Plex Android / iOS App'
    NOTIFICATION_URL = 'https://notifications.plex.tv/api/v1/notifications'
    _DEFAULT_CONFIG = {'user_ids': [],
                       'tap_action': 'preplay',
                       }

    def __init__(self, config=None):
        super(PLEXMOBILEAPP, self).__init__(config=config)

        self.configurations = {
            'created': {'group': 'media', 'identifier': 'tv.plex.notification.library.new'},
            'play': {'group': 'media', 'identifier': 'tv.plex.notification.playback.started'},
            'newdevice': {'group': 'admin', 'identifier': 'tv.plex.notification.device.new'}
        }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        if action not in self.configurations and not action.startswith('test'):
            logger.error(u"Tautulli Notifiers :: Notification action %s not allowed for %s." % (action, self.NAME))
            return

        if action == 'test':
            tests = []
            for configuration in self.configurations:
                tests.append(self.agent_notify(subject=subject, body=body, action='test_'+configuration))
            return all(tests)

        configuration_action = action.split('test_')[-1]

        # No subject to always show up regardless of client selected filters
        # icon can be info, warning, or error
        # play = true to start playing when tapping the notification
        # Send the minimal amount of data necessary through Plex servers
        data = {
            'group': self.configurations[configuration_action]['group'],
            'identifier': self.configurations[configuration_action]['identifier'],
            'to': self.config['user_ids'],
            'data': {
                'provider': {
                    'identifier': plexpy.CONFIG.PMS_IDENTIFIER,
                    'title': helpers.pms_name()
                }
            }
        }

        pretty_metadata = PrettyMetadata(kwargs.get('parameters'))

        if action.startswith('test'):
            data['data']['player'] = {
                'title': 'Device',
                'platform': 'Platform',
                'machineIdentifier': 'Tautulli'
            }
            data['data']['user'] = {
                'title': 'User',
                'id': 0
            }
            data['metadata'] = {
                'type': 'movie',
                'title': subject,
                'year': body
            }

        elif action in ('play', 'newdevice'):
            data['data']['player'] = {
                'title': pretty_metadata.parameters['player'],
                'platform': pretty_metadata.parameters['platform'],
                'machineIdentifier': pretty_metadata.parameters['machine_id']
            }
            data['data']['user'] = {
                'title': pretty_metadata.parameters['user'],
                'id': pretty_metadata.parameters['user_id'],
                'thumb': pretty_metadata.parameters['user_thumb'],
            }

        elif action == 'created':
            # No addition data required for recently added
            pass

        else:
            logger.error(u"Tautulli Notifiers :: Notification action %s not supported for %s." % (action, self.NAME))
            return

        if data['group'] == 'media' and not action.startswith('test'):
            media_type = pretty_metadata.media_type
            uri_rating_key = None

            if media_type == 'movie':
                metadata = {
                    'type': media_type,
                    'title': pretty_metadata.parameters['title'],
                    'year': pretty_metadata.parameters['year'],
                    'thumb': pretty_metadata.parameters['thumb']
                }
            elif media_type == 'show':
                metadata = {
                    'type': media_type,
                    'title': pretty_metadata.parameters['show_name'],
                    'thumb': pretty_metadata.parameters['thumb']
                }
            elif media_type == 'season':
                metadata = {
                    'type': 'show',
                    'title': pretty_metadata.parameters['show_name'],
                    'thumb': pretty_metadata.parameters['thumb'],
                }
                data['data']['count'] = pretty_metadata.parameters['episode_count']
            elif media_type == 'episode':
                metadata = {
                    'type': media_type,
                    'title': pretty_metadata.parameters['episode_name'],
                    'grandparentTitle': pretty_metadata.parameters['show_name'],
                    'index': pretty_metadata.parameters['episode_num'],
                    'parentIndex': pretty_metadata.parameters['season_num'],
                    'grandparentThumb': pretty_metadata.parameters['grandparent_thumb']
                }
            elif media_type == 'artist':
                metadata = {
                    'type': media_type,
                    'title': pretty_metadata.parameters['artist_name'],
                    'thumb': pretty_metadata.parameters['thumb']
                }
            elif media_type == 'album':
                metadata = {
                    'type': media_type,
                    'title': pretty_metadata.parameters['album_name'],
                    'year': pretty_metadata.parameters['year'],
                    'parentTitle': pretty_metadata.parameters['artist_name'],
                    'thumb': pretty_metadata.parameters['thumb'],
                }
            elif media_type == 'track':
                metadata = {
                    'type': 'album',
                    'title': pretty_metadata.parameters['album_name'],
                    'year': pretty_metadata.parameters['year'],
                    'parentTitle': pretty_metadata.parameters['artist_name'],
                    'thumb': pretty_metadata.parameters['parent_thumb']
                }
                uri_rating_key = pretty_metadata.parameters['parent_rating_key']
            else:
                logger.error(u"Tautulli Notifiers :: Media type %s not supported for %s." % (media_type, self.NAME))
                return

            data['metadata'] = metadata
            data['uri'] = 'server://{}/com.plexapp.plugins.library/library/metadata/{}'.format(
                plexpy.CONFIG.PMS_IDENTIFIER, uri_rating_key or pretty_metadata.parameters['rating_key']
            )
            data['play'] = self.config['tap_action'] == 'play'

        headers = {'X-Plex-Token': plexpy.CONFIG.PMS_TOKEN}

        return self.make_request(self.NOTIFICATION_URL, headers=headers, json=data)

    def get_users(self):
        user_ids = {u['user_id']: u['friendly_name'] for u in users.Users().get_users() if u['user_id']}
        return user_ids

    def _return_config_options(self):
        config_option = [{'label': 'Plex User(s)',
                          'value': self.config['user_ids'],
                          'name': 'plexmobileapp_user_ids',
                          'description': 'Select which Plex User(s) to receive notifications.<br>'
                                         'Note: The user(s) must have notifications enabled '
                                         'for the matching Tautulli triggers in their Plex mobile app.',
                          'input_type': 'select',
                          'select_options': self.get_users()
                          },
                         {'label': 'Notification Tap Action',
                          'value': self.config['tap_action'],
                          'name': 'plexmobileapp_tap_action',
                          'description': 'Set the action when tapping on the notification.',
                          'input_type': 'select',
                          'select_options': {'preplay': 'Go to media pre-play screen',
                                             'play': 'Start playing the media'}
                          },
                         ]

        return config_option


class PROWL(Notifier):
    """
    Prowl notifications.
    """
    NAME = 'Prowl'
    _DEFAULT_CONFIG = {'key': '',
                       'priority': 0
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {'apikey': self.config['key'],
                'application': 'Tautulli',
                'event': subject,
                'description': body,
                'priority': self.config['priority']}

        headers = {'Content-type': 'application/x-www-form-urlencoded'}

        return self.make_request('https://api.prowlapp.com/publicapi/add', headers=headers, data=data)

    def _return_config_options(self):
        config_option = [{'label': 'Prowl API Key',
                          'value': self.config['key'],
                          'name': 'prowl_key',
                          'description': 'Your Prowl API key.',
                          'input_type': 'token'
                          },
                         {'label': 'Priority',
                          'value': self.config['priority'],
                          'name': 'prowl_priority',
                          'description': 'Set the notification priority.',
                          'input_type': 'select',
                          'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
                          }
                         ]

        return config_option


class PUSHBULLET(Notifier):
    """
    Pushbullet notifications
    """
    NAME = 'Pushbullet'
    _DEFAULT_CONFIG = {'api_key': '',
                       'device_id': '',
                       'channel_tag': '',
                       'incl_subject': 1,
                       'incl_poster': 0
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {'type': 'note',
                'body': body}

        headers = {'Content-type': 'application/json',
                   'Access-Token': self.config['api_key']
                   }

        if self.config['incl_subject']:
            data['title'] = subject

        # Can only send to a device or channel, not both.
        if self.config['device_id']:
            data['device_iden'] = self.config['device_id']
        elif self.config['channel_tag']:
            data['channel_tag'] = self.config['channel_tag']

        if self.config['incl_poster'] and kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            image = pretty_metadata.get_image()
            if image:
                file_json = {'file_name': image[0], 'file_type': image[2]}
                files = {'file': image}

                r = requests.post('https://api.pushbullet.com/v2/upload-request', headers=headers, json=file_json)

                file_response = r.json()
                upload_url = file_response.pop('upload_url')

                r = requests.post(upload_url, files=files)

                if r.status_code == 204:
                    data['type'] = 'file'
                    file_response.pop('data', None)
                    data.update(file_response)
                else:
                    logger.error("Tautulli Notifiers :: Unable to upload image to {name}: "
                                 "[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
                    logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))

        return self.make_request('https://api.pushbullet.com/v2/pushes', headers=headers, json=data)

    def get_devices(self):
        devices = {'': ''}

        if self.config['api_key']:
            headers = {'Content-type': "application/json",
                       'Access-Token': self.config['api_key']
                       }
            try:
                r = requests.get('https://api.pushbullet.com/v2/devices', headers=headers)

                if r.status_code == 200:
                    response_data = r.json()
                    pushbullet_devices = response_data.get('devices', [])
                    devices.update({d['iden']: d['nickname'] for d in pushbullet_devices if d['active']})
                else:
                    logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: "
                                 "[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
                    logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))

            except Exception as e:
                logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=e))

        return devices

    def _return_config_options(self):
        config_option = [{'label': 'Pushbullet Access Token',
                          'value': self.config['api_key'],
                          'name': 'pushbullet_api_key',
                          'description': 'Your Pushbullet access token.',
                          'input_type': 'token',
                          'refresh': True
                          },
                         {'label': 'Device',
                          'value': self.config['device_id'],
                          'name': 'pushbullet_device_id',
                          'description': 'Set your Pushbullet device. If set, will override channel tag. '
                                         'Leave blank to notify on all devices.',
                          'input_type': 'select',
                          'select_options': self.get_devices()
                          },
                         {'label': 'Channel',
                          'value': self.config['channel_tag'],
                          'name': 'pushbullet_channel_tag',
                          'description': 'A channel tag (optional).',
                          'input_type': 'text'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'pushbullet_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Poster Image',
                          'value': self.config['incl_poster'],
                          'name': 'pushbullet_incl_poster',
                          'description': 'Include a poster with the notifications.',
                          'input_type': 'checkbox'
                          }
                         ]

        return config_option


class PUSHOVER(Notifier):
    """
    Pushover notifications
    """
    NAME = 'Pushover'
    _DEFAULT_CONFIG = {'api_token': '',
                       'key': '',
                       'html_support': 1,
                       'sound': '',
                       'priority': 0,
                       'retry': 30,
                       'expire': 3600,
                       'incl_url': 1,
                       'incl_subject': 1,
                       'incl_poster': 0,
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {'token': self.config['api_token'],
                'user': self.config['key'],
                'message': body,
                'sound': self.config['sound'],
                'html': self.config['html_support'],
                'priority': self.config['priority'],
                'timestamp': helpers.timestamp()}

        if self.config['incl_subject']:
            data['title'] = subject

        if self.config['priority'] == 2:
            data['retry'] = max(30, self.config['retry'])
            data['expire'] = max(30, self.config['expire'])

        headers = {'Content-type': 'application/x-www-form-urlencoded'}

        files = {}

        if self.config['incl_url'] and kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            if pretty_metadata.media_type == 'movie':
                provider = self.config['movie_provider']
            elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                provider = self.config['tv_provider']
            elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                provider = self.config['music_provider']
            else:
                provider = None

            provider_link = pretty_metadata.get_provider_link(provider)
            caption = pretty_metadata.get_caption(provider)

            data['url'] = provider_link
            data['url_title'] = caption

        if self.config['incl_poster'] and kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            image = pretty_metadata.get_image()
            if image:
                if len(image[1]) <= 5242880:  # 5MB max attachment size
                    files = {'attachment': image}
                    headers = {}
                else:
                    logger.warn("Tautulli Notifiers :: Image size exceeds 5MB limit for {name}.".format(name=self.NAME))

        return self.make_request('https://api.pushover.net/1/messages.json', headers=headers, data=data, files=files)

    def get_sounds(self):
        sounds = [
            {'value': '', 'text': ''},
            {'value': 'alien', 'text': 'Alien Alarm (long)'},
            {'value': 'bike', 'text': 'Bike'},
            {'value': 'bugle', 'text': 'Bugle'},
            {'value': 'cashregister', 'text': 'Cash Register'},
            {'value': 'classical', 'text': 'Classical'},
            {'value': 'climb', 'text': 'Climb (long)'},
            {'value': 'cosmic', 'text': 'Cosmic'},
            {'value': 'echo', 'text': 'Pushover Echo (long)'},
            {'value': 'falling', 'text': 'Falling'},
            {'value': 'gamelan', 'text': 'Gamelan'},
            {'value': 'incoming', 'text': 'Incoming'},
            {'value': 'intermission', 'text': 'Intermission'},
            {'value': 'magic', 'text': 'Magic'},
            {'value': 'mechanical', 'text': 'Mechanical'},
            {'value': 'none', 'text': 'None (silent)'},
            {'value': 'persistent', 'text': 'Persistent (long)'},
            {'value': 'pianobar', 'text': 'Piano Bar'},
            {'value': 'pushover', 'text': 'Pushover (default)'},
            {'value': 'siren', 'text': 'Siren'},
            {'value': 'spacealarm', 'text': 'Space Alarm'},
            {'value': 'tugboat', 'text': 'Tug Boat'},
            {'value': 'updown', 'text': 'Up Down (long)'},
            {'value': 'vibrate', 'text': 'Vibrate Only'},
        ]
        if self.config['sound'] not in [s['value'] for s in sounds]:
            sounds.append({'value': self.config['sound'], 'text': self.config['sound']})

        return sounds

        # if self.config['api_token']:
        #     params = {'token': self.config['api_token']}
        #
        #     r = requests.get('https://api.pushover.net/1/sounds.json', params=params)
        #
        #     if r.status_code == 200:
        #         response_data = r.json()
        #         sounds = response_data.get('sounds', {})
        #         sounds.update({'': ''})
        #         print sounds
        #         return sounds
        #     else:
        #         logger.error("Tautulli Notifiers :: Unable to retrieve {name} sounds list: "
        #                      "[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
        #         logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
        #         return {'': ''}
        #
        # else:
        #     return {'': ''}

    def _return_config_options(self):
        config_option = [{'label': 'Pushover API Token',
                          'value': self.config['api_token'],
                          'name': 'pushover_api_token',
                          'description': 'Your Pushover API token.',
                          'input_type': 'token'
                          },
                         {'label': 'Pushover User or Group Key',
                          'value': self.config['key'],
                          'name': 'pushover_key',
                          'description': 'Your Pushover user or group key.',
                          'input_type': 'token'
                          },
                         {'label': 'Sound',
                          'value': self.config['sound'],
                          'name': 'pushover_sound',
                          'description': 'Select a notification sound or enter a custom sound name. Leave blank for the default sound.',
                          'input_type': 'selectize',
                          'select_options': self.get_sounds(),
                          'select_all': False
                          },
                         {'label': 'Priority',
                          'value': self.config['priority'],
                          'name': 'pushover_priority',
                          'description': 'Set the notification priority.',
                          'input_type': 'select',
                          'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
                          },
                         {'label': 'Retry Interval',
                          'value': self.config['retry'],
                          'name': 'pushover_retry',
                          'description': 'Set the interval in seconds to keep retrying the notification.<br>'
                                         'Note: For priority 2 only. Minimum 30 seconds.',
                          'input_type': 'number'
                          },
                         {'label': 'Expire Duration',
                          'value': self.config['expire'],
                          'name': 'pushover_expire',
                          'description': 'Set the duration in seconds when the notification will stop retrying.<br>'
                                         'Note: For priority 2 only. Minimum 30 seconds.',
                          'input_type': 'number'
                          },
                         {'label': 'Enable HTML Support',
                          'value': self.config['html_support'],
                          'name': 'pushover_html_support',
                          'description': 'Style your messages using these HTML tags: b, i, u, a[href], font[color]',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include supplementary URL',
                          'value': self.config['incl_url'],
                          'name': 'pushover_incl_url',
                          'description': 'Include a supplementary URL with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'pushover_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Poster Image',
                          'value': self.config['incl_poster'],
                          'name': 'pushover_incl_poster',
                          'description': 'Include a poster with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'pushover_movie_provider',
                          'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'pushover_tv_provider',
                          'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'pushover_music_provider',
                          'description': 'Select the source for music links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


class SCRIPTS(Notifier):
    """
    Script notifications
    """
    NAME = 'Script'
    _DEFAULT_CONFIG = {'script_folder': '',
                       'script': '',
                       'timeout': 30
                       }

    def __init__(self, config=None):
        super(SCRIPTS, self).__init__(config=config)

        self.script_exts = {
            '.bat': '',
            '.cmd': '',
            '.php': 'php',
            '.pl': 'perl',
            '.ps1': 'powershell -executionPolicy bypass -file',
            '.py': 'python' if plexpy.FROZEN else sys.executable,
            '.pyw': 'pythonw',
            '.rb': 'ruby',
            '.sh': ''
        }

        self.pythonpath_override = 'nopythonpath'
        self.pythonpath = True
        self.prefix_overrides = {
            'python': ['.py'],
            'python2': ['.py'],
            'python3': ['.py'],
            'pythonw': ['.py', '.pyw']
        }
        self.script_killed = False

    def list_scripts(self):
        scriptdir = self.config['script_folder']
        scripts = {'': ''}

        if scriptdir and (not helpers.allow_mount(scriptdir) or not os.path.exists(scriptdir)):
            return scripts

        for root, dirs, files in os.walk(scriptdir):
            for f in files:
                name, ext = os.path.splitext(f)
                if ext in self.script_exts:
                    rfp = os.path.join(os.path.relpath(root, scriptdir), f)
                    fp = os.path.join(root, f)
                    scripts[fp] = rfp

        return scripts

    def run_script(self, script, user_id):
        # Common environment variables
        custom_env = {
            'PLEX_URL': plexpy.CONFIG.PMS_URL,
            'PLEX_TOKEN': plexpy.CONFIG.PMS_TOKEN,
            'PLEX_USER_TOKEN': '',
            'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
            'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
            'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
            'TAUTULLI_ENCODING': plexpy.SYS_ENCODING,
            'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION,
            'PLEXAPI_LOG_PATH': os.path.join(plexpy.CONFIG.LOG_DIR, 'plexapi_script.log')
            }

        if user_id:
            user_tokens = users.Users().get_tokens(user_id=user_id)
            custom_env['PLEX_USER_TOKEN'] = str(user_tokens['server_token'])

        if self.pythonpath and plexpy.INSTALL_TYPE not in ('windows', 'macos'):
            custom_env['PYTHONPATH'] = os.pathsep.join([p for p in sys.path if p])

        env = os.environ.copy()
        env.update(custom_env)

        try:
            process = subprocess.Popen(script,
                                       stdin=subprocess.PIPE,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE,
                                       cwd=self.config['script_folder'],
                                       env=env)

            if self.config['timeout'] > 0:
                timer = threading.Timer(self.config['timeout'], self.kill_script, (process,))
            else:
                timer = None

            try:
                if timer:
                    timer.start()
                output, error = process.communicate()
                status = process.returncode
                logger.debug("Tautulli Notifiers :: Subprocess returned with status code %s." % status)
            finally:
                if timer:
                    timer.cancel()

        except OSError as e:
            logger.error("Tautulli Notifiers :: Failed to run script: %s" % e)
            return False

        if error:
            err = '\n  '.join(error.decode('utf-8').splitlines())
            logger.error("Tautulli Notifiers :: Script error: \n  %s" % err)

        if output:
            out = '\n  '.join(output.decode('utf-8').splitlines())
            logger.debug("Tautulli Notifiers :: Script returned: \n  %s" % out)

        if not self.script_killed:
            logger.info("Tautulli Notifiers :: Script notification sent.")
            return True

    def kill_script(self, process):
        process.kill()
        self.script_killed = True
        logger.warn("Tautulli Notifiers :: Script exceeded timeout limit of %d seconds. "
                    "Script killed." % self.config['timeout'])

    def agent_notify(self, subject='', body='', action='', **kwargs):
        """
            Args:
                  subject(string, optional): Subject text,
                  body(string, optional): Body text,
                  action(string): 'play'
        """
        if not self.config['script_folder']:
            logger.error("Tautulli Notifiers :: No script folder specified.")
            return
        
        if not helpers.allow_mount(self.config['script_folder']):
            logger.warn("Tautulli Notifiers :: Script folder is on a mounted path which is not allowed.")
            return

        script = kwargs.get('script', self.config.get('script', ''))
        script_args = helpers.split_args(kwargs.get('script_args', subject))
        user_id = kwargs.get('parameters', {}).get('user_id')

        logger.debug("Tautulli Notifiers :: Trying to run notify script: %s, arguments: %s, action: %s"
                     % (script, script_args, action))

        # Don't try to run the script if the action does not have one
        if action and not script:
            logger.debug("Tautulli Notifiers :: No script selected for action '%s', exiting..." % action)
            return
        elif not script:
            logger.debug("Tautulli Notifiers :: No script selected, exiting...")
            return
        # Check for a valid script file
        elif not os.path.isfile(script) or not script.endswith(tuple(self.script_exts)):
            logger.error("Tautulli Notifiers :: Invalid script file '%s' specified, exiting..." % script)
            return

        name, ext = os.path.splitext(script)
        prefix = self.script_exts.get(ext, '')

        if prefix:
            script = prefix.split() + [script]
        else:
            script = [script]

        # Allow overrides for PYTHONPATH
        if prefix and script_args:
            if script_args[0] == self.pythonpath_override:
                self.pythonpath = False
                del script_args[0]

        # Allow overrides for shitty systems
        if prefix and script_args and script_args[0] in self.prefix_overrides:
            if ext in self.prefix_overrides[script_args[0]]:
                script[0] = script_args[0]
                del script_args[0]
            else:
                logger.error("Tautulli Notifiers :: Invalid prefix override '%s' for '%s' script, exiting..."
                             % (script_args[0], ext))
                return

        script.extend(script_args)

        logger.debug("Tautulli Notifiers :: Full script is: %s" % script)
        logger.debug("Tautulli Notifiers :: Executing script in a new thread.")
        thread = threading.Thread(target=self.run_script, args=(script, user_id)).start()

        return True

    def _return_config_options(self):
        config_option = [{'label': 'Supported File Types',
                          'description': '<span class="inline-pre">' + \
                              ', '.join(self.script_exts) + '</span>',
                          'input_type': 'help'
                          },
                         {'label': 'Script Folder',
                          'value': self.config['script_folder'],
                          'name': 'scripts_script_folder',
                          'description': 'Enter the full path to your script folder.<br>'
                              'Note: <span class="inline-pre">allow_mounted_folders = 1</span> must be manually enabled in the configuration file to allow mounted folder paths. '
                              'Enabling this setting could pose a security risk. Enable at your own risk. It is recommended to leave this feature disabled if it is not being used.',
                          'input_type': 'text',
                          'refresh': True
                          },
                         {'label': 'Script File',
                          'value': self.config['script'],
                          'name': 'scripts_script',
                          'description': 'Select the script file to run.',
                          'input_type': 'select',
                          'select_options': self.list_scripts()
                          },
                         {'label': 'Script Timeout',
                          'value': self.config['timeout'],
                          'name': 'scripts_timeout',
                          'description': 'The number of seconds to wait before killing the script. 0 to disable timeout.',
                          'input_type': 'number'
                          }
                         ]

        return config_option


class SLACK(Notifier):
    """
    Slack Notifications
    """
    NAME = 'Slack'
    _DEFAULT_CONFIG = {'hook': '',
                       'channel': '',
                       'username': '',
                       'icon_emoji': '',
                       'color': '',
                       'incl_subject': 1,
                       'incl_card': 0,
                       'incl_description': 1,
                       'incl_thumbnail': 0,
                       'incl_pmslink': 0,
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        if self.config['incl_subject']:
            text = subject + '\n' + body
        else:
            text = body

        data = {'text': text}
        if self.config['channel'] and self.config['channel'].startswith('#'):
            data['channel'] = self.config['channel']
        if self.config['username']:
            data['username'] = self.config['username']
        if self.config['icon_emoji']:
            if urlparse(self.config['icon_emoji']).scheme == '':
                data['icon_emoji'] = self.config['icon_emoji']
            else:
                data['icon_url'] = self.config['icon_emoji']

        if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            if pretty_metadata.media_type == 'movie':
                provider = self.config['movie_provider']
            elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                provider = self.config['tv_provider']
            elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                provider = self.config['music_provider']
            else:
                provider = None

            poster_url = pretty_metadata.get_poster_url()
            provider_name = pretty_metadata.get_provider_name(provider)
            provider_link = pretty_metadata.get_provider_link(provider)
            title = pretty_metadata.get_title('\u00B7')
            description = pretty_metadata.get_description()
            plex_url = pretty_metadata.get_plex_url()

            if provider_link:
                text = f"*<{provider_link}|{title}>*"
            else:
                text = f"*{title}*"

            if self.config['incl_description']:
                text = f'{text}\n{description}'

            # Max length of text is 3000 characters
            text = (text[:2997] + (text[2997:] and '...'))

            section = {
                'type': 'section',
                'text': {
                   'type': 'mrkdwn',
                    'text': text,
                }
            }
            if self.config['incl_thumbnail']:
                section['accessory'] = {
                    'type': 'image',
                    'image_url': poster_url,
                    'alt_text': title,
                }
            blocks = [section]

            fields = []
            field_title = {
                'type': 'mrkdwn',
                'text': 'View Details',
            }
            if provider_link:
                fields.append(field_title)
                fields.append({
                    'type': 'mrkdwn',
                    'text': f'<{provider_link}|{provider_name}>',
                })
            if self.config['incl_pmslink']:
                fields.append(field_title)
                fields.append({
                    'type': 'mrkdwn',
                    'text': f'<{plex_url}|Plex Web>',
                })
            if fields:
                if len(fields) <= 2:
                    fields.append({
                        'type': 'plain_text',
                        'text': ' ',
                    })
                blocks.append({
                    'type': 'section',
                    'fields': fields[::2] + fields[1::2],
                })

            if not self.config['incl_thumbnail']:
                blocks.append({
                    'type': 'image',
                    'image_url': poster_url,
                    'alt_text': title,
                })

            attachment = {
                'blocks': blocks,

            }

            if self.config['color'] and re.match(r'^#(?:[0-9a-fA-F]{3}){1,2}$', self.config['color']):
                attachment['color'] = self.config['color']

            data['attachments'] = [attachment]

        headers = {'Content-type': 'application/json'}

        return self.make_request(self.config['hook'], headers=headers, json=data)

    def _return_config_options(self):
        config_option = [{'label': 'Slack Webhook URL',
                          'value': self.config['hook'],
                          'name': 'slack_hook',
                          'description': 'Your Slack incoming webhook URL.',
                          'input_type': 'token'
                          },
                         {'label': 'Slack Channel',
                          'value': self.config['channel'],
                          'name': 'slack_channel',
                          'description': 'The Slack channel name (starting with \'#\') which will be used. Leave blank for webhook integration default.',
                          'input_type': 'text'
                          },
                         {'label': 'Slack Username',
                          'value': self.config['username'],
                          'name': 'slack_username',
                          'description': 'The Slack username which will be used. Leave blank for webhook integration default.',
                          'input_type': 'text'
                          },
                         {'label': 'Slack Icon',
                          'value': self.config['icon_emoji'],
                          'description': 'The Slack emoji or image url for the icon which will be used. Leave blank for webhook integration default.',
                          'name': 'slack_icon_emoji',
                          'input_type': 'text'
                          },
                         {'label': 'Slack Color',
                          'value': self.config['color'],
                          'description': 'The hex color value (starting with \'#\') for the border along the left side of the message attachment.',
                          'name': 'slack_color',
                          'input_type': 'text'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'slack_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Rich Metadata Info',
                          'value': self.config['incl_card'],
                          'name': 'slack_incl_card',
                          'description': 'Include an info card with a poster and metadata with the notifications.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
                                         'data-target="notify_upload_posters">Image Hosting</a> '
                                         'must be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Summary',
                          'value': self.config['incl_description'],
                          'name': 'slack_incl_description',
                          'description': 'Include a summary for the media on the info card.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Link to Plex Web',
                          'value': self.config['incl_pmslink'],
                          'name': 'slack_incl_pmslink',
                          'description': 'Include a second link to the media in Plex Web on the info card.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Use Poster Thumbnail',
                          'value': self.config['incl_thumbnail'],
                          'name': 'slack_incl_thumbnail',
                          'description': 'Use a thumbnail instead of a full sized poster on the info card.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'slack_movie_provider',
                          'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'slack_tv_provider',
                          'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'slack_music_provider',
                          'description': 'Select the source for music links on the info cards. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


class TAUTULLIREMOTEAPP(Notifier):
    """
    Tautulli Remote app notifications
    """
    NAME = 'Tautulli Remote App'
    _DEFAULT_CONFIG = {'device_id': '',
                       'priority': 3,
                       'notification_type': 0
                       }

    def agent_notify(self, subject='', body='', action='', notification_id=None, **kwargs):
        # Check mobile device is still registered
        device = mobile_app.get_mobile_devices(device_id=self.config['device_id'])
        if not device:
            logger.warn("Tautulli Notifiers :: Unable to send Tautulli Remote app notification: device not registered.")
            return
        else:
            device = device[0]

        pretty_metadata = PrettyMetadata(kwargs.get('parameters'))

        plaintext_data = {'notification_id': notification_id,
                          'subject': subject,
                          'body': body,
                          'action': action,
                          'priority': self.config['priority'],
                          'notification_type': self.config['notification_type'],
                          'session_key': pretty_metadata.parameters.get('session_key', ''),
                          'session_id': pretty_metadata.parameters.get('session_id', ''),
                          'user_id': pretty_metadata.parameters.get('user_id', ''),
                          'rating_key': pretty_metadata.parameters.get('rating_key', ''),
                          'poster_thumb': pretty_metadata.parameters.get('poster_thumb', '')}

        #logger.debug("Plaintext data: {}".format(plaintext_data))

        if _CRYPTOGRAPHY:
            # Key generation
            salt = os.urandom(16)
            passphrase = device['device_token']
            key_length = 32  # AES256
            iterations = 600000
            kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=key_length, salt=salt, iterations=iterations)
            key = kdf.derive(passphrase.encode())

            #logger.debug("Encryption key (base64): {}".format(base64.b64encode(key)))

            # Encrypt using AES GCM
            nonce = os.urandom(16)
            cipher = AESGCM(key)
            encrypted_data = cipher.encrypt(nonce, json.dumps(plaintext_data).encode('utf-8'), None)

            #logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data)))
            #logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag)))
            #logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
            #logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))

            payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
                       'include_subscription_ids': [device['onesignal_id']],
                       'contents': {'en': 'Tautulli Notification'},
                       'data': {'encrypted': True,
                                'version': 2,
                                'cipher_text': helpers.base64str(encrypted_data),
                                'nonce': helpers.base64str(nonce),
                                'salt': helpers.base64str(salt),
                                'server_id': plexpy.CONFIG.PMS_UUID}
                       }
        else:
            logger.warn("Tautulli Notifiers :: Cryptography library is missing. "
                        "Tautulli Remote app notifications will be sent unencrypted. "
                        "Install the library to encrypt the notifications.")

            payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
                       'include_subscription_ids': [device['onesignal_id']],
                       'contents': {'en': 'Tautulli Notification'},
                       'data': {'encrypted': False,
                                'plain_text': plaintext_data,
                                'server_id': plexpy.CONFIG.PMS_UUID}
                       }

        #logger.debug("OneSignal payload: {}".format(payload))

        headers = {'Content-Type': 'application/json'}

        return self.make_request('https://api.onesignal.com/notifications', headers=headers, json=payload)

    def get_devices(self):
        db = database.MonitorDatabase()

        try:
            query = "SELECT * FROM mobile_devices WHERE official = 1 " \
                    "AND onesignal_id IS NOT NULL AND onesignal_id != ''"
            return db.select(query=query)
        except Exception as e:
            logger.warn("Tautulli Notifiers :: Unable to retrieve Tautulli Remote app devices list: %s." % e)
            return []

    def _return_config_options(self):
        config_option = []

        if not _CRYPTOGRAPHY:
            config_option.append({
                'label': 'Warning',
                'description': '<strong>The Cryptography library is missing. '
                               'The content of your notifications will be sent unencrypted!</strong><br>'
                               'Please install the library to encrypt the notification contents. '
                               'Instructions can be found in the '
                               '<a href="' + helpers.anon_url(
                                 'https://github.com/%s/%s/wiki/Frequently-Asked-Questions#notifications-cryptography'
                                 % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)) + '" target="_blank" rel="noreferrer">FAQ</a>.' ,
                'input_type': 'help'
            })
        else:
            config_option.append({
                'label': 'Note',
                'description': 'The Cryptography library was found. '
                               'The content of your notifications will be sent encrypted!',
                'input_type': 'help'
            })

        config_option[-1]['description'] += ('<br><br>Notifications are sent using '
            '<a href="' + helpers.anon_url('https://onesignal.com') + '" target="_blank" rel="noreferrer">'
            'OneSignal</a>. Some user data is collected and cannot be encrypted.<br>'
            'Please read the <a href="' + helpers.anon_url(
                'https://onesignal.com/privacy_policy') + '" target="_blank" rel="noreferrer">'
            'OneSignal Privacy Policy</a> for more details.')

        devices = self.get_devices()

        if not devices:
            config_option.append({
                'label': 'Device',
                'description': 'No mobile devices registered with OneSignal. '
                               '<a data-tab-destination="remote_app" data-toggle="tab" data-dismiss="modal">'
                               'Get the Tautulli Remote App</a> and register a device.<br>'
                               'Note: Only devices registered with a valid OneSignal ID will appear in the list.',
                'input_type': 'help'
            })
        else:
            if len({d['platform'] for d in devices}) <= 1:
                device_select = {d['device_id']: d['friendly_name'] or d['device_name'] for d in devices}
            else:
                device_select = defaultdict(list)
                for d in devices:
                    platform = 'iOS' if d['platform'] == 'ios' else d['platform'].capitalize()
                    device_select[platform].append({
                        'value': d['device_id'],
                        'text': d['friendly_name'] or d['device_name']
                    })

            config_option.append({
                'label': 'Device',
                'value': self.config['device_id'],
                'name': 'remoteapp_device_id',
                'description': 'Select your mobile device or '
                               '<a data-tab-destination="remote_app" data-toggle="tab" data-dismiss="modal">'
                               'register a new device</a> with Tautulli.<br>'
                               'Note: Only devices registered with a valid OneSignal ID will appear in the list.',
                'input_type': 'select',
                'select_options': device_select,
                'refresh': True
            })

        platform = next((d['platform'] for d in devices if d['device_id'] == self.config['device_id']), None)

        if platform == 'android':
            config_option.append({
                'label': 'Priority',
                'value': self.config['priority'],
                'name': 'remoteapp_priority',
                'description': 'Set the notification priority.',
                'input_type': 'select',
                'select_options': {
                    1: 'Minimum',
                    2: 'Low',
                    3: 'Normal',
                    4: 'High'
                }
            })
            config_option.append({
                'label': 'Notification Image Type',
                'value': self.config['notification_type'],
                'name': 'remoteapp_notification_type',
                'description': 'Set the notification image type.',
                'input_type': 'select',
                'select_options': {
                    0: 'No notification image',
                    1: 'Small image (Expandable text)',
                    2: 'Large image (Non-expandable text)'
                }
            })
        elif platform == 'ios':
            config_option.append({
                'label': 'Include Poster Image',
                'value': self.config['notification_type'],
                'name': 'remoteapp_notification_type',
                'description': 'Include a poster with the notifications.',
                'input_type': 'checkbox'
            })

        return config_option


class TELEGRAM(Notifier):
    """
    Telegram notifications
    """
    NAME = 'Telegram'
    _DEFAULT_CONFIG = {'bot_token': '',
                       'chat_id': '',
                       'disable_web_preview': 0,
                       'silent_notification': 0,
                       'html_support': 1,
                       'incl_subject': 1,
                       'incl_poster': 0
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        chat_id, *message_thread_id = self.config['chat_id'].split('/')
        data = {'chat_id': chat_id}
        if message_thread_id:
            data['message_thread_id'] = message_thread_id[0]

        if self.config['incl_subject']:
            text = subject + '\r\n' + body
        else:
            text = body

        if self.config['html_support']:
            data['parse_mode'] = 'HTML'

        if self.config['incl_poster'] and kwargs.get('parameters'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            image = pretty_metadata.get_image()
            if image:
                files = {'photo': image}

                if len(text) > 1024:
                    data['disable_notification'] = True
                else:
                    data['caption'] = text.encode('utf-8')
                    if self.config['silent_notification']:
                        data['disable_notification'] = True

                self.make_request('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']),
                                  data=data, files=files)

                if 'caption' in data:
                    return

                data.pop('disable_notification', None)

        data['text'] = (text[:4093] + (text[4093:] and '...')).encode('utf-8')

        if self.config['disable_web_preview']:
            data['disable_web_page_preview'] = True

        if self.config['silent_notification']:
            data['disable_notification'] = True

        headers = {'Content-type': 'application/x-www-form-urlencoded'}

        return self.make_request('https://api.telegram.org/bot{}/sendMessage'.format(self.config['bot_token']),
                                 headers=headers, data=data)

    def _return_config_options(self):
        config_option = [{'label': 'Telegram Bot Token',
                          'value': self.config['bot_token'],
                          'name': 'telegram_bot_token',
                          'description': 'Your Telegram bot token. '
                                         'Contact <a href="' + helpers.anon_url('https://telegram.me/BotFather') +
                                         '" target="_blank" rel="noreferrer">@BotFather</a>'
                                         ' on Telegram to get one.',
                          'input_type': 'token'
                          },
                         {'label': 'Telegram Chat ID, Group ID, or Channel ID/Username',
                          'value': self.config['chat_id'],
                          'name': 'telegram_chat_id',
                          'description': 'Your Telegram Chat ID, Group ID, Channel ID or @channelusername. '
                                         'Contact <a href="' + helpers.anon_url('https://telegram.me/myidbot') +
                                         '" target="_blank" rel="noreferrer">@myidbot</a>'
                                         ' on Telegram to get an ID. '
                                         'For a group topic, append <span class="inline-pre">/topicID</span> to the group ID.',
                          'input_type': 'text'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'telegram_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Poster Image',
                          'value': self.config['incl_poster'],
                          'name': 'telegram_incl_poster',
                          'description': 'Include a poster with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Enable HTML Support',
                          'value': self.config['html_support'],
                          'name': 'telegram_html_support',
                          'description': 'Enable to style your messages using these HTML tags:<br>'
                                         'b, strong, i, em, u, ins, s, strike, del, span[class], '
                                         'tg-spoiler, a[href], code[class], pre',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Disable Web Page Previews',
                          'value': self.config['disable_web_preview'],
                          'name': 'telegram_disable_web_preview',
                          'description': 'Disables automatic link previews for links in the message',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Enable Silent Notifications',
                          'value': self.config['silent_notification'],
                          'name': 'telegram_silent_notification',
                          'description': 'Send notifications silently without any alert sounds.',
                          'input_type': 'checkbox'
                          }
                         ]

        return config_option


class TWITTER(Notifier):
    """
    Twitter notifications
    """
    NAME = 'Twitter'
    REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
    ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
    AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize'
    SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
    _DEFAULT_CONFIG = {'access_token': '',
                       'access_token_secret': '',
                       'consumer_key': '',
                       'consumer_secret': '',
                       'incl_subject': 1,
                       'incl_poster': 0
                       }

    def _send_tweet(self, message=None, attachment=None):
        consumer_key = self.config['consumer_key']
        consumer_secret = self.config['consumer_secret']
        access_token = self.config['access_token']
        access_token_secret = self.config['access_token_secret']

        # logger.info("Tautulli Notifiers :: Sending tweet: " + message)

        api = twitter.Api(consumer_key, consumer_secret, access_token, access_token_secret)

        try:
            api.PostUpdate(message, media=attachment)
            logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
            return True
        except Exception as e:
            logger.error("Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
            return False

    def agent_notify(self, subject='', body='', action='', **kwargs):
        poster_url = ''
        if self.config['incl_poster'] and kwargs.get('parameters'):
            parameters = kwargs['parameters']
            poster_url = parameters.get('poster_url','')

        # Hack to add media type to attachment
        if poster_url and not helpers.get_img_service():
            poster_url += '.png'

        if self.config['incl_subject']:
            return self._send_tweet(subject + '\r\n' + body, attachment=poster_url)
        else:
            return self._send_tweet(body, attachment=poster_url)

    def _return_config_options(self):
        config_option = [{'label': 'Twitter Consumer Key',
                          'value': self.config['consumer_key'],
                          'name': 'twitter_consumer_key',
                          'description': 'Your Twitter consumer key.',
                          'input_type': 'token'
                          },
                         {'label': 'Twitter Consumer Secret',
                          'value': self.config['consumer_secret'],
                          'name': 'twitter_consumer_secret',
                          'description': 'Your Twitter consumer secret.',
                          'input_type': 'token'
                          },
                         {'label': 'Twitter Access Token',
                          'value': self.config['access_token'],
                          'name': 'twitter_access_token',
                          'description': 'Your Twitter access token.',
                          'input_type': 'token'
                          },
                         {'label': 'Twitter Access Token Secret',
                          'value': self.config['access_token_secret'],
                          'name': 'twitter_access_token_secret',
                          'description': 'Your Twitter access token secret.',
                          'input_type': 'token'
                          },
                         {'label': 'Include Subject Line',
                          'value': self.config['incl_subject'],
                          'name': 'twitter_incl_subject',
                          'description': 'Include the subject line with the notifications.',
                          'input_type': 'checkbox'
                          },
                         {'label': 'Include Poster Image',
                          'value': self.config['incl_poster'],
                          'name': 'twitter_incl_poster',
                          'description': 'Include a poster with the notifications.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
                                         'data-target="notify_upload_posters">Image Hosting</a> '
                                         'must be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'checkbox'
                          }
                         ]

        return config_option


class WEBHOOK(Notifier):
    """
    Webhook notifications
    """
    NAME = 'Webhook'
    _DEFAULT_CONFIG = {'hook': '',
                       'method': 'POST'
                       }

    def agent_notify(self, subject='', body='', action='', **kwargs):
        subject = kwargs.get('headers', subject)
        if subject:
            try:
                webhook_headers = json.loads(subject)
            except ValueError as e:
                logger.error("Tautulli Notifiers :: Invalid {name} json header data: {e}".format(name=self.NAME, e=e))
                return False
        else:
            webhook_headers = None

        if body:
            try:
                webhook_body = json.loads(body)
            except ValueError as e:
                logger.error("Tautulli Notifiers :: Invalid {name} json body data: {e}".format(name=self.NAME, e=e))
                return False
        else:
            webhook_body = None

        headers = {'Content-Type': 'application/json'}
        if webhook_headers:
            headers.update(webhook_headers)

        if headers['Content-Type'] == 'application/json':
            data = {'json': webhook_body}
        else:
            data = {'data': webhook_body}

        return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, **data)

    def _return_config_options(self):
        config_option = [{'label': 'Webhook URL',
                          'value': self.config['hook'],
                          'name': 'webhook_hook',
                          'description': 'Your Webhook URL.',
                          'input_type': 'token'
                          },
                         {'label': 'Webhook Method',
                          'value': self.config['method'],
                          'name': 'webhook_method',
                          'description': 'The Webhook HTTP request method.',
                          'input_type': 'select',
                          'select_options': {'GET': 'GET',
                                             'POST': 'POST',
                                             'PUT': 'PUT',
                                             'DELETE': 'DELETE',
                                             'PATCH': 'PATCH'}
                          }
                         ]

        return config_option


class XBMC(Notifier):
    """
    Kodi notifications
    """
    NAME = 'Kodi'
    _DEFAULT_CONFIG = {'hosts': '',
                       'username': '',
                       'password': '',
                       'display_time': 5,
                       'image': ''
                       }

    def _sendhttp(self, host, command):
        url_command = urlencode(command)
        url = host + '/xbmcCmds/xbmcHttp/?' + url_command

        if self.config['password']:
            return request.request_content(url, auth=(self.config['username'], self.config['password']))
        else:
            return request.request_content(url)

    def _sendjson(self, host, method, params=None):
        params = params or {}
        data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
        headers = {'Content-Type': 'application/json'}
        url = host + '/jsonrpc'

        if self.config['password']:
            response = request.request_json(url, method="post", data=json.dumps(data), headers=headers,
                                            auth=(self.config['username'], self.config['password']))
        else:
            response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)

        if response:
            return response[0]['result']

    def agent_notify(self, subject='', body='', action='', **kwargs):
        hosts = [x.strip() for x in self.config['hosts'].split(',')]

        if self.config['display_time'] > 0:
            display_time = 1000 * self.config['display_time']  # in ms
        else:
            display_time = 5000

        if self.config['image']:
            image = self.config['image']
        else:
            image = os.path.join(plexpy.DATA_DIR, os.path.abspath("data/interfaces/default/images/logo-circle.png"))

        for host in hosts:
            logger.info("Tautulli Notifiers :: Sending notification command to XMBC @ " + host)
            try:
                version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major']

                if version < 12:  # Eden
                    notification = subject + "," + body + "," + str(display_time)
                    notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
                    request = self._sendhttp(host, notifycommand)

                else:  # Frodo
                    params = {'title': subject, 'message': body, 'displaytime': display_time, 'image': image}
                    request = self._sendjson(host, 'GUI.ShowNotification', params)

                if not request:
                    raise Exception
                else:
                    logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))

            except Exception as e:
                logger.error("Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
                return False

        return True

    def _return_config_options(self):
        config_option = [{'label': 'Kodi Host Address',
                          'value': self.config['hosts'],
                          'name': 'xbmc_hosts',
                          'description': 'Host running Kodi (e.g. http://localhost:8080). Separate multiple hosts with commas (,).',
                          'input_type': 'text'
                          },
                         {'label': 'Kodi Username',
                          'value': self.config['username'],
                          'name': 'xbmc_username',
                          'description': 'Username of your Kodi client API (blank for none).',
                          'input_type': 'text'
                          },
                         {'label': 'Kodi Password',
                          'value': self.config['password'],
                          'name': 'xbmc_password',
                          'description': 'Password of your Kodi client API (blank for none).',
                          'input_type': 'password'
                          },
                         {'label': 'Notification Duration',
                          'value': self.config['display_time'],
                          'name': 'xbmc_display_time',
                          'description': 'The duration (in seconds) for the notification to stay on screen.',
                          'input_type': 'number'
                          },
                         {'label': 'Notification Icon',
                          'value': self.config['image'],
                          'name': 'xbmc_image',
                          'description': 'Full path or URL to an image to display with the notification. Leave blank for the default.',
                          'input_type': 'text'
                          }
                         ]

        return config_option


class ZAPIER(Notifier):
    """
    Zapier notifications
    """
    NAME = 'Zapier'
    _DEFAULT_CONFIG = {'hook': '',
                       'movie_provider': '',
                       'tv_provider': '',
                       'music_provider': ''
                       }

    def _test_hook(self):
        _test_data = {'subject': 'Subject',
                      'body': 'Body',
                      'action': 'Action',
                      'poster_url': 'https://i.imgur.com',
                      'provider_name': 'Provider Name',
                      'provider_link': 'http://www.imdb.com',
                      'plex_url': 'https://app.plex.tv/desktop'}

        return self.agent_notify(_test_data=_test_data)

    def agent_notify(self, subject='', body='', action='', **kwargs):
        data = {'subject': subject,
                'body': body,
                'action': action}

        if kwargs.get('parameters', {}).get('media_type'):
            # Grab formatted metadata
            pretty_metadata = PrettyMetadata(kwargs['parameters'])

            if pretty_metadata.media_type == 'movie':
                provider = self.config['movie_provider']
            elif pretty_metadata.media_type in ('show', 'season', 'episode'):
                provider = self.config['tv_provider']
            elif pretty_metadata.media_type in ('artist', 'album', 'track'):
                provider = self.config['music_provider']
            else:
                provider = None

            poster_url = pretty_metadata.get_poster_url()
            provider_name = pretty_metadata.get_provider_name(provider)
            provider_link = pretty_metadata.get_provider_link(provider)
            plex_url = pretty_metadata.get_plex_url()

            data['poster_url'] = poster_url
            data['provider_name'] = provider_name
            data['provider_link'] = provider_link
            data['plex_url'] = plex_url

        if kwargs.get('_test_data'):
            data.update(kwargs['_test_data'])

        headers = {'Content-type': 'application/json'}

        return self.make_request(self.config['hook'], headers=headers, json=data)

    def _return_config_options(self):
        config_option = [{'label': 'Zapier Webhook URL',
                          'value': self.config['hook'],
                          'name': 'zapier_hook',
                          'description': 'Your Zapier webhook URL.',
                          'input_type': 'token'
                          },
                         {'label': 'Test Zapier Webhook',
                          'value': 'Send Test Data',
                          'name': 'zapier_test_hook',
                          'description': 'Click this button when prompted on then "Test Webhooks by Zapier" step.',
                          'input_type': 'button'
                          },
                         {'label': 'Movie Link Source',
                          'value': self.config['movie_provider'],
                          'name': 'zapier_movie_provider',
                          'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_movie_providers()
                          },
                         {'label': 'TV Show Link Source',
                          'value': self.config['tv_provider'],
                          'name': 'zapier_tv_provider',
                          'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_tv_providers()
                          },
                         {'label': 'Music Link Source',
                          'value': self.config['music_provider'],
                          'name': 'zapier_music_provider',
                          'description': 'Select the source for music links in the notification. Leave blank to disable.<br>'
                                         'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" >Metadata Lookups</a> '
                                         'may need to be enabled under the 3rd Party APIs settings tab.',
                          'input_type': 'select',
                          'select_options': PrettyMetadata().get_music_providers()
                          }
                         ]

        return config_option


def check_browser_enabled():
    global BROWSER_NOTIFIERS
    BROWSER_NOTIFIERS = {}
    for n in get_notifiers():
        if n['agent_id'] == 17 and n['active']:
            notifier_config = get_notifier_config(n['id'])
            BROWSER_NOTIFIERS[n['id']] = notifier_config['config']['auto_hide_delay']


def get_browser_notifications():
    db = database.MonitorDatabase()
    result = db.select("SELECT notifier_id, subject_text, body_text FROM notify_log "
                       "WHERE agent_id = 17 AND timestamp >= ? ",
                       args=[time.time() - 5])

    notifications = []
    for item in result:
        notification = {'subject_text': item['subject_text'],
                        'body_text': item['body_text'],
                        'delay': BROWSER_NOTIFIERS.get(item['notifier_id'], 5)}
        notifications.append(notification)

    return {'notifications': notifications}
