diff --git a/libs/plex_activity/__init__.py b/libs/plex_activity/__init__.py deleted file mode 100644 index 4da218f59..000000000 --- a/libs/plex_activity/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -import logging -import traceback - -log = logging.getLogger(__name__) - -__version__ = '0.7.1' - - -try: - from plex_activity import activity - - # Global objects (using defaults) - Activity = activity.Activity() -except Exception as ex: - log.warn('Unable to import submodules: %s - %s', ex, traceback.format_exc()) diff --git a/libs/plex_activity/activity.py b/libs/plex_activity/activity.py deleted file mode 100644 index f85632a5e..000000000 --- a/libs/plex_activity/activity.py +++ /dev/null @@ -1,96 +0,0 @@ -from plex.lib import six as six -from plex.lib.six.moves import xrange -from plex_activity.sources import Logging, WebSocket - -from pyemitter import Emitter -import logging - -log = logging.getLogger(__name__) - - -class ActivityMeta(type): - def __getitem__(self, key): - for (weight, source) in self.registered: - if source.name == key: - return source - - return None - - -@six.add_metaclass(ActivityMeta) -class Activity(Emitter): - registered = [] - - def __init__(self, sources=None): - self.available = self.get_available(sources) - self.enabled = [] - - def start(self, sources=None): - # TODO async start - - if sources is not None: - self.available = self.get_available(sources) - - # Test methods until an available method is found - for weight, source in self.available: - if weight is None: - # None = always start - self.start_source(source) - elif source.test(): - # Test passed - self.start_source(source) - else: - log.info('activity source "%s" is not available', source.name) - - log.info( - 'Finished starting %s method(s): %s', - len(self.enabled), - ', '.join([('"%s"' % source.name) for source in self.enabled]) - ) - - def start_source(self, source): - instance = source(self) - instance.start() - - self.enabled.append(instance) - - def __getitem__(self, key): - for (weight, source) in self.registered: - if source.name == key: - return source - - return None - - @classmethod - def get_available(cls, sources): - if sources: - return [ - (weight, source) for (weight, source) in cls.registered - if source.name in sources - ] - - return cls.registered - - @classmethod - def register(cls, source, weight=None): - item = (weight, source) - - # weight = None, highest priority - if weight is None: - cls.registered.insert(0, item) - return - - # insert in DESC order - for x in xrange(len(cls.registered)): - w, _ = cls.registered[x] - - if w is not None and w < weight: - cls.registered.insert(x, item) - return - - # otherwise append - cls.registered.append(item) - -# Register activity sources -Activity.register(WebSocket) -Activity.register(Logging, weight=1) diff --git a/libs/plex_activity/core/__init__.py b/libs/plex_activity/core/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/libs/plex_activity/core/helpers.py b/libs/plex_activity/core/helpers.py deleted file mode 100644 index 8ce99c65e..000000000 --- a/libs/plex_activity/core/helpers.py +++ /dev/null @@ -1,44 +0,0 @@ -def str_format(s, *args, **kwargs): - """Return a formatted version of S, using substitutions from args and kwargs. - - (Roughly matches the functionality of str.format but ensures compatibility with Python 2.5) - """ - - args = list(args) - - x = 0 - while x < len(s): - # Skip non-start token characters - if s[x] != '{': - x += 1 - continue - - end_pos = s.find('}', x) - - # If end character can't be found, move to next character - if end_pos == -1: - x += 1 - continue - - name = s[x + 1:end_pos] - - # Ensure token name is alpha numeric - if not name.isalnum(): - x += 1 - continue - - # Try find value for token - value = args.pop(0) if args else kwargs.get(name) - - if value: - value = str(value) - - # Replace token with value - s = s[:x] + value + s[end_pos + 1:] - - # Update current position - x = x + len(value) - 1 - - x += 1 - - return s diff --git a/libs/plex_activity/sources/__init__.py b/libs/plex_activity/sources/__init__.py deleted file mode 100644 index adc1c937e..000000000 --- a/libs/plex_activity/sources/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from plex_activity.sources.s_logging import Logging -from plex_activity.sources.s_websocket import WebSocket - -__all__ = ['Logging', 'WebSocket'] diff --git a/libs/plex_activity/sources/base.py b/libs/plex_activity/sources/base.py deleted file mode 100644 index 773126afd..000000000 --- a/libs/plex_activity/sources/base.py +++ /dev/null @@ -1,24 +0,0 @@ -from pyemitter import Emitter -from threading import Thread -import logging - -log = logging.getLogger(__name__) - - -class Source(Emitter): - name = None - - def __init__(self): - self.thread = Thread(target=self._run_wrapper) - - def start(self): - self.thread.start() - - def run(self): - pass - - def _run_wrapper(self): - try: - self.run() - except Exception as ex: - log.error('Exception raised in "%s" activity source: %s', self.name, ex, exc_info=True) diff --git a/libs/plex_activity/sources/s_logging/__init__.py b/libs/plex_activity/sources/s_logging/__init__.py deleted file mode 100644 index 949d13b34..000000000 --- a/libs/plex_activity/sources/s_logging/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from plex_activity.sources.s_logging.main import Logging - -__all__ = ['Logging'] diff --git a/libs/plex_activity/sources/s_logging/main.py b/libs/plex_activity/sources/s_logging/main.py deleted file mode 100644 index 1cae499c7..000000000 --- a/libs/plex_activity/sources/s_logging/main.py +++ /dev/null @@ -1,249 +0,0 @@ -from plex import Plex -from plex_activity.sources.base import Source -from plex_activity.sources.s_logging.parsers import NowPlayingParser, ScrobbleParser - -from asio import ASIO -from asio.file import SEEK_ORIGIN_CURRENT -from io import BufferedReader -import inspect -import logging -import os -import platform -import time - -log = logging.getLogger(__name__) - -PATH_HINTS = { - 'Darwin': [ - lambda: os.path.join(os.getenv('HOME'), 'Library/Logs/Plex Media Server.log') - ], - 'FreeBSD': [ - # FreeBSD - '/usr/local/plexdata/Plex Media Server/Logs/Plex Media Server.log', - '/usr/local/plexdata-plexpass/Plex Media Server/Logs/Plex Media Server.log', - - # FreeNAS - '/usr/pbi/plexmediaserver-amd64/plexdata/Plex Media Server/Logs/Plex Media Server.log', - '/var/db/plexdata/Plex Media Server/Logs/Plex Media Server.log', - '/var/db/plexdata-plexpass/Plex Media Server/Logs/Plex Media Server.log' - ], - 'Linux': [ - # QNAP - '/share/HDA_DATA/.qpkg/PlexMediaServer/Library/Plex Media Server/Logs/Plex Media Server.log', - - # Debian - '/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Logs/Plex Media Server.log' - ], - 'Windows': [ - lambda: os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\\Logs\\Plex Media Server.log') - ] -} - - -class Logging(Source): - name = 'logging' - events = [ - 'logging.playing', - 'logging.action.played', - 'logging.action.unplayed' - ] - - parsers = [] - - path = None - path_hints = PATH_HINTS - - def __init__(self, activity): - super(Logging, self).__init__() - - self.parsers = [p(self) for p in Logging.parsers] - - self.file = None - self.reader = None - - self.path = None - - # Pipe events to the main activity instance - self.pipe(self.events, activity) - - def run(self): - line = self.read_line_retry(ping=True, stale_sleep=0.5) - if not line: - log.info('Unable to read log file') - return - - log.debug('Ready') - - while True: - # Grab the next line of the log - line = self.read_line_retry(ping=True) - - if line: - self.process(line) - else: - log.info('Unable to read log file') - - def process(self, line): - for parser in self.parsers: - if parser.process(line): - return True - - return False - - def read_line(self): - if not self.file: - path = self.get_path() - if not path: - raise Exception('Unable to find the location of "Plex Media Server.log"') - - # Open file - self.file = ASIO.open(path, opener=False) - self.file.seek(self.file.get_size(), SEEK_ORIGIN_CURRENT) - - # Create buffered reader - self.reader = BufferedReader(self.file) - - self.path = self.file.get_path() - log.info('Opened file path: "%s"' % self.path) - - return self.reader.readline() - - def read_line_retry(self, timeout=60, ping=False, stale_sleep=1.0): - line = None - stale_since = None - - while not line: - line = self.read_line() - - if line: - stale_since = None - time.sleep(0.05) - break - - if stale_since is None: - stale_since = time.time() - time.sleep(stale_sleep) - continue - elif (time.time() - stale_since) > timeout: - return None - elif (time.time() - stale_since) > timeout / 2: - # Nothing returned for 5 seconds - if self.file.get_path() != self.path: - log.debug("Log file moved (probably rotated), closing") - self.close() - elif ping: - # Ping server to see if server is still active - Plex.detail() - ping = False - - time.sleep(stale_sleep) - - return line - - def close(self): - if not self.file: - return - - try: - # Close the buffered reader - self.reader.close() - except Exception as ex: - log.error('reader.close() - raised exception: %s', ex, exc_info=True) - finally: - self.reader = None - - try: - # Close the file handle - self.file.close() - except OSError as ex: - if ex.errno == 9: - # Bad file descriptor, already closed? - log.info('file.close() - ignoring raised exception: %s (already closed)', ex) - else: - log.error('file.close() - raised exception: %s', ex, exc_info=True) - except Exception as ex: - log.error('file.close() - raised exception: %s', ex, exc_info=True) - finally: - self.file = None - - @classmethod - def get_path(cls): - if cls.path: - return cls.path - - hints = cls.get_hints() - - log.debug('hints: %r', hints) - - if not hints: - log.error('Unable to find any hints for "%s", operating system not supported', platform.system()) - return None - - for hint in hints: - log.debug('Testing if "%s" exists', hint) - - if os.path.exists(hint): - cls.path = hint - break - - if cls.path: - log.debug('Using the path: %r', cls.path) - else: - log.error('Unable to find a valid path for "Plex Media Server.log"', extra={ - 'data': { - 'hints': hints - } - }) - - return cls.path - - @classmethod - def add_hint(cls, path, system=None): - if system not in cls.path_hints: - cls.path_hints[system] = [] - - cls.path_hints[system].append(path) - - @classmethod - def get_hints(cls): - # Retrieve system hints - hints_system = PATH_HINTS.get(platform.system(), []) - - # Retrieve global hints - hints_global = PATH_HINTS.get(None, []) - - # Retrieve hint from server preferences (if available) - data_path = Plex[':/prefs'].get('LocalAppDataPath') - - if data_path: - hints_global.append(os.path.join(data_path.value, "Plex Media Server", "Logs", "Plex Media Server.log")) - else: - log.info('Unable to retrieve "LocalAppDataPath" from server') - - hints = [] - - for hint in (hints_global + hints_system): - # Resolve hint function - if inspect.isfunction(hint): - hint = hint() - - # Check for duplicate - if hint in hints: - continue - - hints.append(hint) - - return hints - - @classmethod - def test(cls): - # TODO "Logging" source testing - return True - - @classmethod - def register(cls, parser): - cls.parsers.append(parser) - - -Logging.register(NowPlayingParser) -Logging.register(ScrobbleParser) diff --git a/libs/plex_activity/sources/s_logging/parsers/__init__.py b/libs/plex_activity/sources/s_logging/parsers/__init__.py deleted file mode 100644 index 4792a4548..000000000 --- a/libs/plex_activity/sources/s_logging/parsers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from plex_activity.sources.s_logging.parsers.now_playing import NowPlayingParser -from plex_activity.sources.s_logging.parsers.scrobble import ScrobbleParser - -__all__ = ['NowPlayingParser', 'ScrobbleParser'] diff --git a/libs/plex_activity/sources/s_logging/parsers/base.py b/libs/plex_activity/sources/s_logging/parsers/base.py deleted file mode 100644 index b8fecf04d..000000000 --- a/libs/plex_activity/sources/s_logging/parsers/base.py +++ /dev/null @@ -1,96 +0,0 @@ -from plex.lib.six.moves import urllib_parse as urlparse -from plex_activity.core.helpers import str_format - -from pyemitter import Emitter -import logging -import re - -log = logging.getLogger(__name__) - -LOG_PATTERN = r'^.*?\[\w+\]\s\w+\s-\s{message}$' -REQUEST_HEADER_PATTERN = str_format(LOG_PATTERN, message=r"Request: (\[(?P
.*?):(?P\d+)[^]]*\]\s)?{method} {path}.*?") - -IGNORE_PATTERNS = [ - r'error parsing allowedNetworks.*?', - r'Comparing request from.*?', - r'(Auth: )?We found auth token (.*?), enabling token-based authentication\.', - r'(Auth: )?Came in with a super-token, authorization succeeded\.', - r'(Auth: )?Refreshing tokens inside the token-based authentication filter\.', - r'\[Now\] Updated play state for .*?', - r'Play progress on .*? - got played .*? ms by account .*?!', - r'(Statistics: )?\(.*?\) Reporting active playback in state \d+ of type \d+ \(.*?\) for account \d+', - r'Request: \[.*?\] (GET|PUT) /video/:/transcode/.*?', - r'Received transcode session ping for session .*?' -] - -IGNORE_REGEX = re.compile(str_format(LOG_PATTERN, message='(%s)' % ('|'.join('(%s)' % x for x in IGNORE_PATTERNS))), re.IGNORECASE) - - -PARAM_REGEX = re.compile(str_format(LOG_PATTERN, message=r' \* (?P.*?) =\> (?P.*?)'), re.IGNORECASE) - - -class Parser(Emitter): - def __init__(self, core): - self.core = core - - def read_parameters(self, *match_functions): - match_functions = [self.parameter_match] + list(match_functions) - - info = {} - - while True: - line = self.core.read_line_retry(timeout=5) - if not line: - log.info('Unable to read log file') - return {} - - # Run through each match function to find a result - match = None - for func in match_functions: - match = func(line) - - if match is not None: - break - - # Update info dict with result, otherwise finish reading - if match: - info.update(match) - elif match is None and IGNORE_REGEX.match(line.strip()) is None: - log.debug('break on "%s"', line.strip()) - break - - return info - - def process(self, line): - raise NotImplementedError() - - @staticmethod - def parameter_match(line): - match = PARAM_REGEX.match(line.strip()) - if not match: - return None - - match = match.groupdict() - - return {match['key']: match['value']} - - @staticmethod - def regex_match(regex, line): - match = regex.match(line.strip()) - if not match: - return None - - return match.groupdict() - - @staticmethod - def query(match, value): - if not value: - return - - try: - parameters = urlparse.parse_qsl(value, strict_parsing=True) - except ValueError: - return - - for key, value in parameters: - match.setdefault(key, value) diff --git a/libs/plex_activity/sources/s_logging/parsers/now_playing.py b/libs/plex_activity/sources/s_logging/parsers/now_playing.py deleted file mode 100644 index c7242414c..000000000 --- a/libs/plex_activity/sources/s_logging/parsers/now_playing.py +++ /dev/null @@ -1,116 +0,0 @@ -from plex_activity.core.helpers import str_format -from plex_activity.sources.s_logging.parsers.base import Parser, LOG_PATTERN, REQUEST_HEADER_PATTERN - -import logging -import re - -log = logging.getLogger(__name__) - -PLAYING_HEADER_PATTERN = str_format(REQUEST_HEADER_PATTERN, method="GET", path="/:/(?Ptimeline|progress)/?(?:\?(?P.*?))?\s") -PLAYING_HEADER_REGEX = re.compile(PLAYING_HEADER_PATTERN, re.IGNORECASE) - -RANGE_REGEX = re.compile(str_format(LOG_PATTERN, message=r'Request range: \d+ to \d+'), re.IGNORECASE) -CLIENT_REGEX = re.compile(str_format(LOG_PATTERN, message=r'Client \[(?P.*?)\].*?'), re.IGNORECASE) - -NOW_USER_REGEX = re.compile(str_format(LOG_PATTERN, message=r'\[Now\] User is (?P.+) \(ID: (?P\d+)\)'), re.IGNORECASE) -NOW_CLIENT_REGEX = re.compile(str_format(LOG_PATTERN, message=r'\[Now\] Device is (?P.+?) \((?P.+)\)\.'), re.IGNORECASE) - - -class NowPlayingParser(Parser): - required_info = [ - 'ratingKey', - 'state', 'time' - ] - - extra_info = [ - 'duration', - - 'user_name', 'user_id', - 'machineIdentifier', 'client' - ] - - events = [ - 'logging.playing' - ] - - def __init__(self, main): - super(NowPlayingParser, self).__init__(main) - - # Pipe events to the main logging activity instance - self.pipe(self.events, main) - - def process(self, line): - header_match = PLAYING_HEADER_REGEX.match(line) - if not header_match: - return False - - activity_type = header_match.group('type') - - # Get a match from the activity entries - if activity_type == 'timeline': - match = self.timeline() - elif activity_type == 'progress': - match = self.progress() - else: - log.warn('Unknown activity type "%s"', activity_type) - return True - - print match, activity_type - - if match is None: - match = {} - - # Extend match with query info - self.query(match, header_match.group('query')) - - # Ensure we successfully matched a result - if not match: - return True - - # Sanitize the activity result - info = { - 'address': header_match.group('address'), - 'port': header_match.group('port') - } - - # - Get required info parameters - for key in self.required_info: - if key in match and match[key] is not None: - info[key] = match[key] - else: - log.info('Invalid activity match, missing key %s (matched keys: %s)', key, match.keys()) - return True - - # - Add in any extra info parameters - for key in self.extra_info: - if key in match: - info[key] = match[key] - else: - info[key] = None - - # Update the scrobbler with the current state - self.emit('logging.playing', info) - return True - - def timeline(self): - return self.read_parameters( - lambda line: self.regex_match(CLIENT_REGEX, line), - lambda line: self.regex_match(RANGE_REGEX, line), - - # [Now]* entries - lambda line: self.regex_match(NOW_USER_REGEX, line), - lambda line: self.regex_match(NOW_CLIENT_REGEX, line), - ) - - def progress(self): - data = self.read_parameters() - - if not data: - return {} - - # Translate parameters into timeline-style form - return { - 'state': data.get('state'), - 'ratingKey': data.get('key'), - 'time': data.get('time') - } diff --git a/libs/plex_activity/sources/s_logging/parsers/scrobble.py b/libs/plex_activity/sources/s_logging/parsers/scrobble.py deleted file mode 100644 index a1b2c93f4..000000000 --- a/libs/plex_activity/sources/s_logging/parsers/scrobble.py +++ /dev/null @@ -1,38 +0,0 @@ -from plex_activity.core.helpers import str_format -from plex_activity.sources.s_logging.parsers.base import Parser, LOG_PATTERN - -import re - - -class ScrobbleParser(Parser): - pattern = str_format(LOG_PATTERN, message=r'Library item (?P\d+) \'(?P.*?)\' got (?P<action>(?:un)?played) by account (?P<account_key>\d+)!.*?') - regex = re.compile(pattern, re.IGNORECASE) - - events = [ - 'logging.action.played', - 'logging.action.unplayed' - ] - - def __init__(self, main): - super(ScrobbleParser, self).__init__(main) - - # Pipe events to the main logging activity instance - self.pipe(self.events, main) - - def process(self, line): - match = self.regex.match(line) - if not match: - return False - - action = match.group('action') - if not action: - return False - - self.emit('logging.action.%s' % action, { - 'account_key': match.group('account_key'), - 'rating_key': match.group('rating_key'), - - 'title': match.group('title') - }) - - return True diff --git a/libs/plex_activity/sources/s_websocket/__init__.py b/libs/plex_activity/sources/s_websocket/__init__.py deleted file mode 100644 index 2657b37a9..000000000 --- a/libs/plex_activity/sources/s_websocket/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from plex_activity.sources.s_websocket.main import WebSocket - -__all__ = ['WebSocket'] diff --git a/libs/plex_activity/sources/s_websocket/main.py b/libs/plex_activity/sources/s_websocket/main.py deleted file mode 100644 index 429498fc3..000000000 --- a/libs/plex_activity/sources/s_websocket/main.py +++ /dev/null @@ -1,298 +0,0 @@ -from plex import Plex -from plex.lib.six.moves.urllib_parse import urlencode -from plex_activity.sources.base import Source - -import json -import logging -import re -import time -import websocket - -log = logging.getLogger(__name__) - -SCANNING_REGEX = re.compile('Scanning the "(?P<section>.*?)" section', re.IGNORECASE) -SCAN_COMPLETE_REGEX = re.compile('Library scan complete', re.IGNORECASE) - -TIMELINE_STATES = { - 0: 'created', - 2: 'matching', - 3: 'downloading', - 4: 'loading', - 5: 'finished', - 6: 'analyzing', - 9: 'deleted' -} - - -class WebSocket(Source): - name = 'websocket' - events = [ - 'websocket.playing', - - 'websocket.scanner.started', - 'websocket.scanner.progress', - 'websocket.scanner.finished', - - 'websocket.timeline.created', - 'websocket.timeline.matching', - 'websocket.timeline.downloading', - 'websocket.timeline.loading', - 'websocket.timeline.finished', - 'websocket.timeline.analyzing', - 'websocket.timeline.deleted' - ] - - opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) - - def __init__(self, activity): - super(WebSocket, self).__init__() - - self.ws = None - self.reconnects = 0 - - # Pipe events to the main activity instance - self.pipe(self.events, activity) - - def connect(self): - uri = 'ws://%s:%s/:/websockets/notifications' % ( - Plex.configuration.get('server.host', '127.0.0.1'), - Plex.configuration.get('server.port', 32400) - ) - - params = {} - - # Set authentication token (if one is available) - if Plex.configuration['authentication.token']: - params['X-Plex-Token'] = Plex.configuration['authentication.token'] - - # Append parameters to uri - if params: - uri += '?' + urlencode(params) - - # Create websocket connection - self.ws = websocket.create_connection(uri) - - def run(self): - self.connect() - - log.debug('Ready') - - while True: - try: - self.process(*self.receive()) - - # successfully received data, reset reconnects counter - self.reconnects = 0 - except websocket.WebSocketConnectionClosedException: - if self.reconnects <= 5: - self.reconnects += 1 - - # Increasing sleep interval between reconnections - if self.reconnects > 1: - time.sleep(2 * (self.reconnects - 1)) - - log.info('WebSocket connection has closed, reconnecting...') - self.connect() - else: - log.error('WebSocket connection unavailable, activity monitoring not available') - break - - def receive(self): - frame = self.ws.recv_frame() - - if not frame: - raise websocket.WebSocketException("Not a valid frame %s" % frame) - elif frame.opcode in self.opcode_data: - return frame.opcode, frame.data - elif frame.opcode == websocket.ABNF.OPCODE_CLOSE: - self.ws.send_close() - return frame.opcode, None - elif frame.opcode == websocket.ABNF.OPCODE_PING: - self.ws.pong("Hi!") - - return None, None - - def process(self, opcode, data): - if opcode not in self.opcode_data: - return False - - try: - info = json.loads(data) - except UnicodeDecodeError as ex: - log.warn('Error decoding message from websocket: %s' % ex, extra={ - 'event': { - 'module': __name__, - 'name': 'process.loads.unicode_decode_error', - 'key': '%s:%s' % (ex.encoding, ex.reason) - } - }) - log.debug(data) - return False - except Exception as ex: - log.warn('Error decoding message from websocket: %s' % ex, extra={ - 'event': { - 'module': __name__, - 'name': 'process.load_exception', - 'key': ex.message - } - }) - log.debug(data) - return False - - # Handle modern messages (PMS 1.3.0+) - if type(info.get('NotificationContainer')) is dict: - info = info['NotificationContainer'] - - # Process message - m_type = info.get('type') - - if not m_type: - log.debug('Received message with no "type" parameter: %r', info) - return False - - # Pre-process message (if function exists) - process_func = getattr(self, 'process_%s' % m_type, None) - - if process_func and process_func(info): - return True - - # Emit raw message - return self.emit_notification('%s.notification.%s' % (self.name, m_type), info) - - def process_playing(self, info): - children = info.get('_children') or info.get('PlaySessionStateNotification') - - if not children: - log.debug('Received "playing" message with no children: %r', info) - return False - - return self.emit_notification('%s.playing' % self.name, children) - - def process_progress(self, info): - children = info.get('_children') or info.get('ProgressNotification') - - if not children: - log.debug('Received "progress" message with no children: %r', info) - return False - - for notification in children: - self.emit('%s.scanner.progress' % self.name, { - 'message': notification.get('message') - }) - - return True - - def process_status(self, info): - children = info.get('_children') or info.get('StatusNotification') - - if not children: - log.debug('Received "status" message with no children: %r', info) - return False - - # Process children - count = 0 - - for notification in children: - title = notification.get('title') - - if not title: - continue - - # Scan complete message - if SCAN_COMPLETE_REGEX.match(title): - self.emit('%s.scanner.finished' % self.name) - count += 1 - continue - - # Scanning message - match = SCANNING_REGEX.match(title) - - if not match: - continue - - section = match.group('section') - - if not section: - continue - - self.emit('%s.scanner.started' % self.name, {'section': section}) - count += 1 - - # Validate result - if count < 1: - log.debug('Received "status" message with no valid children: %r', info) - return False - - return True - - def process_timeline(self, info): - children = info.get('_children') or info.get('TimelineEntry') - - if not children: - log.debug('Received "timeline" message with no children: %r', info) - return False - - # Process children - count = 0 - - for entry in children: - state = TIMELINE_STATES.get(entry.get('state')) - - if not state: - continue - - self.emit('%s.timeline.%s' % (self.name, state), entry) - count += 1 - - # Validate result - if count < 1: - log.debug('Received "timeline" message with no valid children: %r', info) - return False - - return True - - # - # Helpers - # - - def emit_notification(self, name, info=None): - if info is None: - info = {} - - # Emit children - children = self._get_children(info) - - if children: - for child in children: - self.emit(name, child) - - return True - - # Emit objects - if info: - self.emit(name, info) - else: - self.emit(name) - - return True - - @staticmethod - def _get_children(info): - if type(info) is list: - return info - - if type(info) is not dict: - return None - - # Return legacy children - if info.get('_children'): - return info['_children'] - - # Search for modern children container - for key, value in info.items(): - key = key.lower() - - if (key.endswith('entry') or key.endswith('notification')) and type(value) is list: - return value - - return None