Source code for Code

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

# standard imports
import inspect
import re

# plex debugging
try:
    import plexhints  # noqa: F401
except ImportError:
    pass
else:  # the code is running outside of Plex
    from plexhints import plexhints_setup, update_sys_path
    plexhints_setup()  # read the plugin plist file and determine if plexhints should use elevated policy or not
    update_sys_path()  # when running outside plex, append the path

    from plexhints.agent_kit import Agent, Media  # agent kit
    from plexhints.decorator_kit import handler  # decorator kit
    from plexhints.locale_kit import Locale
    from plexhints.log_kit import Log  # log kit
    from plexhints.model_kit import Movie  # model kit
    from plexhints.object_kit import MessageContainer, MetadataSearchResult, SearchResult  # object kit
    from plexhints.prefs_kit import Prefs  # prefs kit

# imports from Libraries\Shared
from typing import Optional

try:
    # get the original Python builtins module
    python_builtins = inspect.getmodule(object)

    # get the Sandbox instance
    sandbox = inspect.stack()[1][0].f_locals["self"]

    # bypass RestrictedPython
    getattr(sandbox, "_core").loader.compile = lambda src, name, _=False: python_builtins.compile(src, name, "exec")

    # restore Python builtins
    sandbox.environment.update(python_builtins.vars(python_builtins))
except Exception as e:
    Log.Exception("Failed to bypass RestrictedPython: {}".format(e))

# local imports
from default_prefs import default_prefs
from constants import contributes_to, version
from plex_api_helper import plex_listener, start_queue_threads, update_plex_item
import migration_helper
from scheduled_tasks import setup_scheduling
from webapp import start_server

# variables
last_prefs = dict()


[docs]def copy_prefs(): # type: () -> None """ Copy the current preferences to the last preferences. This function is used to copy the current preferences to the last preferences. This is useful to determine if the preferences have changed. Examples -------- >>> copy_prefs() """ global last_prefs last_prefs = dict() for key in default_prefs: try: last_prefs[key] = Prefs[key] except KeyError: pass # this was already logged
[docs]def ValidatePrefs(): # type: () -> MessageContainer """ Validate plug-in preferences. This function is called when the user modifies their preferences. The developer can check the newly provided values to ensure they are correct (e.g. attempting a login to validate a username and password), and optionally return a ``MessageContainer`` to display any error information to the user. See the archived Plex documentation `Predefined functions <https://web.archive.org/web/https://dev.plexapp.com/docs/channels/basics.html#predefined-functions>`_ for more information. Returns ------- MessageContainer Success or Error message dependeing on results of validation. Examples -------- >>> ValidatePrefs() ... """ global last_prefs # todo - validate username and password error_message = '' # start with a blank error message for key in default_prefs: try: Prefs[key] except KeyError: Log.Critical("Setting '%s' missing from 'DefaultPrefs.json'" % key) error_message += "Setting '%s' missing from 'DefaultPrefs.json'<br/>" % key else: # test all types except 'str_' as string cannot fail if key.startswith('int_'): try: int(Prefs[key]) except ValueError: Log.Error("Setting '%s' must be an integer; Value '%s'" % (key, Prefs[key])) error_message += "Setting '%s' must be an integer; Value '%s'<br/>" % (key, Prefs[key]) elif key.startswith('bool_'): if Prefs[key] is not True and Prefs[key] is not False: Log.Error("Setting '%s' must be True or False; Value '%s'" % (key, Prefs[key])) error_message += "Setting '%s' must be True or False; Value '%s'<br/>" % (key, Prefs[key]) # special cases int_greater_than_zero = [ 'int_plexapi_plexapi_timeout', 'int_plexapi_upload_threads' ] for test in int_greater_than_zero: if key == test and int(Prefs[key]) <= 0: Log.Error("Setting '%s' must be greater than 0; Value '%s'" % (key, Prefs[key])) error_message += "Setting '%s' must be greater than 0; Value '%s'<br/>" % (key, Prefs[key]) # restart webserver if required requires_restart = [ 'str_webapp_http_host', 'int_webapp_http_port', 'bool_webapp_log_werkzeug_messages' ] if key in requires_restart: try: if last_prefs[key] != Prefs[key]: Log.Info('Changing this setting ({}) requires a Plex Media Server restart.'.format(key)) except KeyError: pass copy_prefs() # since validate prefs runs on startup, this will have already run at least once # perform migrations migration_object = migration_helper.MigrationHelper() for key in default_prefs: migration_key_prefix = 'bool_migrate_' if key.startswith(migration_key_prefix): migration = key.replace(migration_key_prefix, '') migrated = migration_object.get_migration_status(key=migration) if Prefs[key] and not migrated: migration_object.perform_migration(key=migration) if error_message != '': return MessageContainer(header='Error', message=error_message) else: Log.Info("DefaultPrefs.json is valid") return MessageContainer(header='Success', message='Themerr-plex - Provided preference values are ok')
[docs]def Start(): # type: () -> None """ Start the plug-in. This function is called when the plug-in first starts. It can be used to perform extra initialisation tasks such as configuring the environment and setting default attributes. See the archived Plex documentation `Predefined functions <https://web.archive.org/web/https://dev.plexapp.com/docs/channels/basics.html#predefined-functions>`_ for more information. Preferences are validated, then additional threads are started for the web server, queue, plex listener, and scheduled tasks. Examples -------- >>> Start() ... """ Log.Info('Themerr-plex, version: {}'.format(version)) # validate prefs prefs_valid = ValidatePrefs() if prefs_valid.header == 'Error': Log.Warn('Themerr-plex plug-in preferences are not valid.') start_server() # start the web server Log.Debug('web server started.') start_queue_threads() # start queue threads Log.Debug('queue threads started.') if Prefs['bool_plex_movie_support']: plex_listener() # start watching plex Log.Debug('plex_listener started, watching for activity from new Plex Movie agent.') setup_scheduling() # start scheduled tasks Log.Debug('scheduled tasks started.') Log.Debug('plug-in started.')
@handler(prefix='/applications/themerr-plex', name='Themerr-plex ({})'.format(version), thumb='icon-default.png') def main(): """ Create the main plug-in ``handler``. This is responsible for displaying the plug-in in the plug-ins menu. Since we are using the ``@handler`` decorator, and since Plex removed menu's from plug-ins, this method does not need to perform any other function. """ pass
[docs]class Themerr(Agent.Movies): """ Class representing the Themerr-plex Movie Agent. This class defines the metadata agent. See the archived Plex documentation `Defining an agent class <https://web.archive.org/web/https://dev.plexapp.com/docs/agents/basics.html#defining-an-agent-class>`_ for more information. References ---------- name : str A string defining the name of the agent for display in the GUI. languages : list A list of strings defining the languages supported by the agent. These values should be taken from the constants defined in the `Locale <https://web.archive.org/web/https://dev.plexapp.com/docs/api/localekit.html#module-Locale>`_ API. primary_provider : py:class:`bool` A boolean value defining whether the agent is a primary metadata provider or not. Primary providers can be selected as the main source of metadata for a particular media type. If an agent is secondary (``primary_provider`` is set to ``False``) it will only be able to contribute to data provided by another primary agent. fallback_agent : Optional[str] A string containing the identifier of another agent to use as a fallback. If none of the matches returned by an agent are a close enough match to the given set of hints, this fallback agent will be called to attempt to find a better match. accepts_from : Optional[list] A list of strings containing the identifiers of agents that can contribute secondary data to primary data provided by this agent. contributes_to : Optional[list] A list of strings containing the identifiers of primary agents that the agent can contribute secondary data to. Methods ------- search: Search for an item. update: Add or update metadata for an item. Examples -------- >>> Themerr() ... """ name = 'Themerr-plex' languages = [ Locale.Language.English ] primary_provider = False fallback_agent = False accepts_from = [] contributes_to = contributes_to
[docs] @staticmethod def search(results, media, lang, manual): # type: (SearchResult, Media.Movie, str, bool) -> Optional[SearchResult] """ Search for an item. When the media server needs an agent to perform a search, it calls the agent’s ``search`` method. See the archived Plex documentation `Searching for results to provide matches for media <https://web.archive.org/web/https://dev.plexapp.com/docs/agents/search.html>`_ for more information. Parameters ---------- results : SearchResult An empty container that the developer should populate with potential matches. media : Media.Movie An object containing hints to be used when performing the search. lang : str A string identifying the user’s currently selected language. This will be one of the constants added to the agent’s ``languages`` attribute. manual : py:class:`bool` A boolean value identifying whether the search was issued automatically during scanning, or manually by the user (in order to fix an incorrect match). Returns ------- Optional[SearchResult] The search result object, if the search was successful. Examples -------- >>> Themerr().search(results=..., media=..., lang='en', manual=True) ... """ Log.Debug('Searching with arguments: {results=%s, media=%s, lang=%s, manual=%s' % (results, media, lang, manual)) if media.primary_metadata is None or media.primary_agent is None: Log.Error('Search is being called in a primary agent.') return Log.Debug('Primary agent: %s' % media.primary_agent) Log.Debug('media.primary_metadata.id: %s' % media.primary_metadata.id) # the media_id will be used to create the url path, replacing `-` with `/` if media.primary_agent == 'dev.lizardbyte.retroarcher-plex': media_id = 'games-%s' % re.search(r'((igdb)-(\d+))', media.primary_metadata.id).group(1) else: media_id = 'movies-%s-%s' % (media.primary_agent.rsplit('.', 1)[-1], media.primary_metadata.id) # e.g. = 'movies-imdb-tt0113189' # e.g. = 'movies-themoviedb-710' results.Append(MetadataSearchResult( id=media_id, name=media.primary_metadata.title, year=media.primary_metadata.year, score=100, lang=lang, # no lang to get from db thumb=None # no point in adding thumb since plex won't show it anywhere )) # sort the results first by year, then by score results.Sort(attr='year') results.Sort(attr='score', descending=True) return results
[docs] @staticmethod def update(metadata, media, lang, force): # type: (Movie, Media.Movie, str, bool) -> Optional[Movie] """ Update metadata for an item. Once an item has been successfully matched, it is added to the update queue. As the framework processes queued items, it calls the ``update`` method of the relevant agents. See the archived Plex documentation `Adding metadata to media <https://web.archive.org/web/https://dev.plexapp.com/docs/agents/update.html>`_ for more information. Parameters ---------- metadata : object A pre-initialized metadata object if this is the first time the item is being updated, or the existing metadata object if the item is being refreshed. media : object An object containing information about the media hierarchy in the database. lang : str A string identifying which language should be used for the metadata. This will be one of the constants defined in the agent’s ``languages`` attribute. force : py:class:`bool` A boolean value identifying whether the user forced a full refresh of the metadata. If this argument is ``True``, all metadata should be refreshed, regardless of whether it has been populated previously. Examples -------- >>> Themerr().update(metadata=..., media=..., lang='en', force=True) ... """ Log.Debug('Updating with arguments: {metadata=%s, media=%s, lang=%s, force=%s' % (metadata, media, lang, force)) rating_key = int(media.id) # rating key of plex item update_plex_item(rating_key=rating_key) return metadata