def should_defer(self, task): if task and task.result: # Ignore sync conditions on manual triggers if task.result.trigger == SyncResult.Trigger.Manual: return False # Ignore sync conditions if the task has been queued for over 12 hours started_ago = datetime.utcnow() - task.result.started_at if started_ago > timedelta(hours=12): log.debug( 'Task has been queued for over 12 hours, ignoring sync conditions' ) return False if Preferences.get('sync.idle_defer'): # Defer sync tasks until server finishes streaming (and is idle for 30 minutes) if ModuleManager['sessions'].is_streaming(): log.debug( 'Deferring sync task, server is currently streaming media') return True if not ModuleManager['sessions'].is_idle(): log.debug( 'Deferring sync task, server has been streaming media recently (in the last %d minutes)', Preferences.get('sync.idle_delay')) return True return False
def should_defer(self, task): if task and task.result: # Ignore sync conditions on manual triggers if task.result.trigger == SyncResult.Trigger.Manual: return False # Ignore sync conditions if the task has been queued for over 12 hours started_ago = datetime.utcnow() - task.result.started_at if started_ago > timedelta(hours=12): log.debug('Task has been queued for over 12 hours, ignoring sync conditions') return False if Preferences.get('sync.idle_defer'): # Defer sync tasks until server finishes streaming (and is idle for 30 minutes) if ModuleManager['sessions'].is_streaming(): log.debug('Deferring sync task, server is currently streaming media') return True if not ModuleManager['sessions'].is_idle(): log.debug( 'Deferring sync task, server has been streaming media recently (in the last %d minutes)', Preferences.get('sync.idle_delay') ) return True return False
def configure(self): extended = Preferences.get('matcher.mode') == MatcherMode.PlexExtended # Configure matchers matchers = [ self._database_matcher, self._metadata_matcher, # Default matchers plex_database.matcher.Default, plex_metadata.matcher.Default ] for matcher in matchers: if not matcher: continue # Update cache if self._cache: matcher.cache = self._cache # Configure features if matcher.caper_enabled != extended or matcher.extend_enabled != extended: matcher.caper_enabled = extended matcher.extend_enabled = extended log.info('Extended matcher has been %s on %r', 'enabled' if extended else 'disabled', matcher) return True
def run(self): # Check for authentication token log.info('X-Plex-Token: %s', 'available' if os.environ.get('PLEXTOKEN') else 'unavailable') # Process server startup state self.process_server_state() # Start new-style modules module_start() # Start modules names = [] for module in self.modules: if not hasattr(module, 'start'): continue names.append(get_class_name(module)) module.start() log.info('Started %s modules: %s', len(names), ', '.join(names)) ModuleManager.start() # Start plex.activity.py Activity.start(ACTIVITY_MODE.get(Preferences.get('activity.mode')))
def _queue(self): accounts = Account.select( Account.id ).where( Account.id > 0 ) for account in accounts: if account.deleted: # Ignore library update trigger for deleted accounts continue enabled = Preferences.get('sync.library_update', account) log.debug('account: %r, enabled: %r', account.id, enabled) if not enabled: continue try: # Queue sync for account self.sync.queue( account=account, mode=SyncMode.Full, priority=100, trigger=SyncResult.Trigger.LibraryUpdate ) except QueueError, ex: log.info('Queue error: %s', ex)
def on_added(self, data, *args, **kwargs): if data.get('type') not in [1, 2, 4]: return log.debug( 'Item added: %s (id: %r, type: %r)', data.get('title'), data.get('itemID'), data.get('type') ) # Retrieve accounts accounts = Account.select( Account.id ).where( Account.id > 0 ) # Update library state for accounts for account in accounts: if account.deleted: continue # Ensure account has the library update trigger enabled enabled = Preferences.get('sync.library_update', account) if not enabled: continue # Update library state for account self.get(account.id).on_added(data)
def is_duplicate(cls, action): if action.event != 'scrobble/stop': return False # Retrieve scrobble duplication period duplication_period = Preferences.get('scrobble.duplication_period') if duplication_period is None: return False # Check for duplicate scrobbles in `duplication_period` scrobbled = ActionHistory.has_scrobbled( action.account, action.rating_key, part=action.part, after=action.queued_at - timedelta(minutes=duplication_period) ) if scrobbled: log.info( 'Ignoring duplicate %r action, scrobble already performed in the last %d minutes', action.event, duplication_period ) return True return False
def _is_duplicate(self, data, action, p_key): if data != SyncData.Watched or action != 'add': return False # Retrieve scrobble duplication period duplication_period = Preferences.get('scrobble.duplication_period') if duplication_period is None: return False # Check for duplicate scrobbles in `duplication_period` # TODO check `part` attribute scrobbled = ActionHistory.has_scrobbled( self.task.account, p_key, after=datetime.utcnow() - timedelta(minutes=duplication_period) ) if scrobbled: log.info( 'Ignoring duplicate history addition, scrobble already performed in the last %d minutes', duplication_period ) return True return False
def is_idle(self): # Cleanup stale sessions self.cleanup() # Check if server has been idle for `sync.idle_delay` seconds return self._idle_since and datetime.utcnow( ) - self._idle_since > timedelta( minutes=Preferences.get('sync.idle_delay'))
def queue(cls, event, request, session=None, account=None): if event is None: return None obj = None if request is not None: request = json.dumps(request) # Retrieve `account_id` for action account_id = None if session: try: account_id = session.account_id except KeyError: account_id = None if account_id is None and account: account_id = account.id if account_id is None: log.debug('Unable to find valid account for event %r, session %r', event, session) return None if not Preferences.get('scrobble.enabled', account_id): log.debug('Scrobbler not enabled for account %r', account_id) return None # Try queue the event try: obj = ActionQueue.create(account=account_id, session=session, progress=session.progress, part=session.part, rating_key=session.rating_key, event=event, request=request, queued_at=datetime.utcnow()) log.debug('Queued %r event for %r', event, session) except (apsw.ConstraintError, peewee.IntegrityError) as ex: log.info('Event %r has already been queued for session %r: %s', event, session.session_key, ex, exc_info=True) except Exception as ex: log.warn('Unable to queue event %r for %r: %s', event, session, ex, exc_info=True) # Ensure process thread is started cls.start() return obj
def RestartRequired(last_activity_mode): if Preferences.get("activity.mode") != last_activity_mode: return True for key in ["language", "proxy_host", "proxy_username", "proxy_password"]: if Prefs[key] != Dict[key]: return True return False
def RestartRequired(last_activity_mode): if Preferences.get('activity.mode') != last_activity_mode: return True for key in ['language', 'proxy_host', 'proxy_username', 'proxy_password']: if Prefs[key] != Dict[key]: return True return False
def run(self): # Migrate server preferences Preferences.initialize() Preferences.migrate() # Try migrate administrator preferences try: Preferences.initialize(account=1) Preferences.migrate(account=1) except Account.DoesNotExist: log.debug('Unable to migrate administrator preferences, no account found')
def queue(cls, event, request, session=None, account=None): if event is None: return None obj = None if request is not None: request = json.dumps(request) # Retrieve `account_id` for action account_id = None if session: try: account_id = session.account_id except KeyError: account_id = None if account_id is None and account: account_id = account.id if account_id is None: log.debug('Unable to find valid account for event %r, session %r', event, session) return None if not Preferences.get('scrobble.enabled', account_id): log.debug('Scrobbler not enabled for account %r', account_id) return None # Try queue the event try: obj = ActionQueue.create( account=account_id, session=session, progress=session.progress, part=session.part, rating_key=session.rating_key, event=event, request=request, queued_at=datetime.utcnow() ) log.debug('Queued %r event for %r', event, session) except (apsw.ConstraintError, peewee.IntegrityError) as ex: log.info('Event %r has already been queued for session %r: %s', event, session.session_key, ex, exc_info=True) except Exception as ex: log.warn('Unable to queue event %r for %r: %s', event, session, ex, exc_info=True) # Ensure process thread is started cls.start() return obj
def run(self): # Migrate server preferences Preferences.initialize() Preferences.migrate() # Try migrate administrator preferences try: Preferences.initialize(account=1) Preferences.migrate(account=1) except Account.DoesNotExist: log.debug( 'Unable to migrate administrator preferences, no account found' )
def process(cls, method, headers, body, key, *args, **kwargs): log.debug("Handling API %s request %r - args: %r, kwargs: %r", method, key, len(args), len(kwargs.keys())) if not Preferences.get("api.enabled"): log.debug("Unable to process request, API is currently disabled") return cls.build_error("disabled", "Unable to process request, API is currently disabled") k_service, k_method = key.rsplit(".", 1) # Try find matching service service = cls.get_service(k_service) if service is None: log.warn("Unable to find service: %r", k_service) return cls.build_error("unknown.service", "Unable to find service: %r" % k_service) func = getattr(service, k_method, None) if func is None: log.warn("Unable to find method: %r", k_method) return cls.build_error("unknown.method", "Unable to find method: %r" % k_method) # Validate meta = getattr(func, "__meta__", {}) if not meta.get("exposed", False): log.warn("Method is not exposed: %r", k_method) return cls.build_error("restricted.method", "Method is not exposed: %r" % k_method) # Decode strings in the `args` parameter try: args = cls.decode(args) except Exception as ex: return cls.build_error("args.decode_error", "Unable to decode provided args") # Decode strings in the `kwargs` parameter try: kwargs = cls.decode(kwargs) except Exception as ex: return cls.build_error("kwargs.decode_error", "Unable to decode provided kwargs") # Execute request handler try: result = cls.call(method, headers, body, func, args, kwargs) except ApiError as ex: log.warn("Error returned while handling request %r: %r", key, ex, exc_info=True) return cls.build_error("error.%s" % ex.code, ex.message) except Exception as ex: log.error("Exception raised while handling request %r: %s", key, ex, exc_info=True) return cls.build_error("exception", "Exception raised while handling the request") # Build response return cls.build_response(result)
def ValidatePrefs(): # Retrieve plex token token_plex = AccountMigration.get_token(Request.Headers) # Retrieve current activity mode last_activity_mode = Preferences.get('activity.mode') if Request.Headers.get('X-Disable-Preference-Migration', '0') == '0': # Run account migration am = AccountMigration() am.run(token_plex) # Migrate server preferences Preferences.migrate() # Try migrate administrator preferences try: Preferences.initialize(account=1) Preferences.migrate(account=1) except Account.DoesNotExist: log.debug( 'Unable to migrate administrator preferences, no account found' ) else: log.debug('Ignoring preference migration (disabled by header)') # Restart if activity_mode has changed if Preferences.get('activity.mode') != last_activity_mode or Prefs[ 'language'] != Dict['language']: log.info('Restart required to apply changes, restarting plugin...') def restart(): # Delay until after `ValidatePrefs` returns time.sleep(3) # Restart plugin Plex[':/plugins'].restart(PLUGIN_IDENTIFIER) spawn(restart) return MessageContainer(_("Success"), _("Success")) # Fire configuration changed callback spawn(Main.on_configuration_changed) return MessageContainer(_("Success"), _("Success"))
def process(cls, method, headers, body, key, *args, **kwargs): log.debug('Handling API %s request %r - args: %r, kwargs: %r', method, key, args, kwargs) if not Preferences.get('api.enabled'): log.debug('Unable to process request, API is currently disabled') return cls.build_error( 'disabled', 'Unable to process request, API is currently disabled') k_service, k_method = key.rsplit('.', 1) # Try find matching service service = cls.get_service(k_service) if service is None: log.warn('Unable to find service: %r', k_service) return cls.build_error('unknown.service', 'Unable to find service: %r' % k_service) func = getattr(service, k_method, None) if func is None: log.warn('Unable to find method: %r', k_method) return cls.build_error('unknown.method', 'Unable to find method: %r' % k_method) # Validate meta = getattr(func, '__meta__', {}) if not meta.get('exposed', False): log.warn('Method is not exposed: %r', k_method) return cls.build_error('restricted.method', 'Method is not exposed: %r' % k_method) # TODO validate authentication # Execute request handler try: result = cls.call(method, headers, body, func, args, kwargs) except ApiError, ex: log.warn('Error returned while handling request %r: %r', key, ex, exc_info=True) return cls.build_error('error.%s' % ex.code, ex.message)
def ValidatePrefs(): # Retrieve plex token token_plex = AccountMigration.get_token(Request.Headers) # Retrieve current activity mode last_activity_mode = Preferences.get('activity.mode') if Request.Headers.get('X-Disable-Preference-Migration', '0') == '0': # Run account migration am = AccountMigration() am.run(token_plex) # Migrate server preferences Preferences.migrate() # Try migrate administrator preferences try: Preferences.initialize(account=1) Preferences.migrate(account=1) except Account.DoesNotExist: log.debug('Unable to migrate administrator preferences, no account found') else: log.debug('Ignoring preference migration (disabled by header)') # Restart if activity_mode has changed if Preferences.get('activity.mode') != last_activity_mode: log.info('Activity mode has changed, restarting plugin...') def restart(): # Delay until after `ValidatePrefs` returns time.sleep(3) # Restart plugin Plex[':/plugins'].restart(PLUGIN_IDENTIFIER) spawn(restart) return MessageContainer("Success", "Success") # Fire configuration changed callback spawn(Main.on_configuration_changed) return MessageContainer("Success", "Success")
def ValidatePrefs(): # Retrieve plex token token_plex = AccountMigration.get_token(Request.Headers) # Retrieve current activity mode last_activity_mode = Preferences.get("activity.mode") if Request.Headers.get("X-Disable-Preference-Migration", "0") == "0": # Run account migration am = AccountMigration() am.run(token_plex) # Migrate server preferences Preferences.migrate() # Try migrate administrator preferences try: Preferences.initialize(account=1) Preferences.migrate(account=1) except Account.DoesNotExist: log.debug("Unable to migrate administrator preferences, no account found") else: log.debug("Ignoring preference migration (disabled by header)") # Restart if activity_mode has changed if RestartRequired(last_activity_mode): log.info("Restart required to apply changes, restarting plugin...") def restart(): # Delay until after `ValidatePrefs` returns time.sleep(3) # Restart plugin Plex[":/plugins"].restart(PLUGIN_IDENTIFIER) spawn(restart, daemon=True) return MessageContainer(_("Success"), _("Success")) # Fire configuration changed callback spawn(Main.on_configuration_changed, daemon=True) return MessageContainer(_("Success"), _("Success"))
def _is_duplicate(self, data, action, p_key): if data != SyncData.Watched or action != 'add': return False # Retrieve scrobble duplication period duplication_period = Preferences.get('scrobble.duplication_period') if duplication_period is None: return False # Check for duplicate scrobbles in `duplication_period` scrobbled = ActionHistory.has_scrobbled( self.task.account, p_key, after=datetime.utcnow() - timedelta(minutes=duplication_period)) if scrobbled: log.info( 'Ignoring duplicate history addition, scrobble already performed in the last %d minutes', duplication_period) return True return False
def _queue(self): accounts = Account.select(Account.id).where(Account.id > 0) for account in accounts: if account.deleted: # Ignore library update trigger for deleted accounts continue enabled = Preferences.get('sync.library_update', account) log.debug('account: %r, enabled: %r', account.id, enabled) if not enabled: continue try: # Queue sync for account self.sync.queue(account=account, mode=SyncMode.Full, priority=100, trigger=SyncResult.Trigger.LibraryUpdate) except QueueError, ex: log.info('Queue error: %s', ex)
def process(cls, method, headers, body, key, *args, **kwargs): log.debug('Handling API %s request %r - args: %r, kwargs: %r', method, key, args, kwargs) if not Preferences.get('api.enabled'): log.debug('Unable to process request, API is currently disabled') return cls.build_error('disabled', 'Unable to process request, API is currently disabled') k_service, k_method = key.rsplit('.', 1) # Try find matching service service = cls.get_service(k_service) if service is None: log.warn('Unable to find service: %r', k_service) return cls.build_error('unknown.service', 'Unable to find service: %r' % k_service) func = getattr(service, k_method, None) if func is None: log.warn('Unable to find method: %r', k_method) return cls.build_error('unknown.method', 'Unable to find method: %r' % k_method) # Validate meta = getattr(func, '__meta__', {}) if not meta.get('exposed', False): log.warn('Method is not exposed: %r', k_method) return cls.build_error('restricted.method', 'Method is not exposed: %r' % k_method) # TODO validate authentication # Execute request handler try: result = cls.call(method, headers, body, func, args, kwargs) except ApiError, ex: log.warn('Error returned while handling request %r: %r', key, ex, exc_info=True) return cls.build_error('error.%s' % ex.code, ex.message)
def is_duplicate(cls, action): if action.event != 'scrobble/stop': return False # Retrieve scrobble duplication period duplication_period = Preferences.get('scrobble.duplication_period') if duplication_period is None: return False # Check for duplicate scrobbles in `duplication_period` scrobbled = ActionHistory.has_scrobbled( action.account, action.rating_key, after=action.queued_at - timedelta(minutes=duplication_period) ) if scrobbled: log.info( 'Ignoring duplicate %r action, scrobble already performed in the last %d minutes', action.event, duplication_period ) return True return False
def _queue(self): started_at = self._state.started_at if started_at: log.info('Scanner started at: %r', started_at) # Retrieve accounts accounts = Account.select( Account.id ).where( Account.id > 0 ) # Trigger sync on enabled accounts for account in accounts: if account.deleted: continue # Ensure account has the library update trigger enabled enabled = Preferences.get('sync.library_update', account) if not enabled: continue # Retrieve recently added items items_added = self._state.get(account.id).pop_added() log.info( 'Detected %d item(s) have been added for account %r', account.id, len(items_added) ) # Build pull parameters pull = { # Run pull on items we explicitly know have been created 'ids': set(items_added) } if started_at: # Run pull on items created since the scanner started pull['created_since'] = started_at - timedelta(seconds=30) # Queue sync for account try: self.sync.queue( account=account, mode=SyncMode.Full, priority=100, trigger=SyncResult.Trigger.LibraryUpdate, pull=pull ) except QueueError as ex: log.info('Queue error: %s', ex) # Unable to queue sync, add items back to the account library state self._state.get(account.id).extend_added(items_added) finally: # Reset library state self._state.reset()
def process(cls, method, headers, body, key, *args, **kwargs): log.debug('Handling API %s request %r - args: %r, kwargs: %r', method, key, len(args), len(kwargs.keys())) if not Preferences.get('api.enabled'): log.debug('Unable to process request, API is currently disabled') return cls.build_error( 'disabled', 'Unable to process request, API is currently disabled') k_service, k_method = key.rsplit('.', 1) # Try find matching service service = cls.get_service(k_service) if service is None: log.warn('Unable to find service: %r', k_service) return cls.build_error('unknown.service', 'Unable to find service: %r' % k_service) func = getattr(service, k_method, None) if func is None: log.warn('Unable to find method: %r', k_method) return cls.build_error('unknown.method', 'Unable to find method: %r' % k_method) # Validate meta = getattr(func, '__meta__', {}) if not meta.get('exposed', False): log.warn('Method is not exposed: %r', k_method) return cls.build_error('restricted.method', 'Method is not exposed: %r' % k_method) # Decode strings in the `args` parameter try: args = cls.decode(args) except Exception as ex: return cls.build_error('args.decode_error', 'Unable to decode provided args') # Decode strings in the `kwargs` parameter try: kwargs = cls.decode(kwargs) except Exception as ex: return cls.build_error('kwargs.decode_error', 'Unable to decode provided kwargs') # Execute request handler try: result = cls.call(method, headers, body, func, args, kwargs) except ApiError as ex: log.warn('Error returned while handling request %r: %r', key, ex, exc_info=True) return cls.build_error('error.%s' % ex.code, ex.message) except Exception as ex: log.error('Exception raised while handling request %r: %s', key, ex, exc_info=True) return cls.build_error( 'exception', 'Exception raised while handling the request') # Build response return cls.build_response(result)
def start(cls, blocking=False): enabled = ACTIVITY_MODE.get(Preferences.get('activity.mode')) # Start methods cls.started = cls.methods.start(enabled)
def is_idle(self): # Cleanup stale sessions self.cleanup() # Check if server has been idle for `sync.idle_delay` seconds return self._idle_since and datetime.utcnow() - self._idle_since > timedelta(minutes=Preferences.get('sync.idle_delay'))