Exemple #1
0
 def __init__(self, manual=False):
     self.backlog = read_json(self.BACKLOG_PATH) or []
     self.clear_interval = config["backlog"]["clear_interval"].get(
         confuse.Number())
     self.expiry = config["backlog"]["expiry"].get(confuse.Number())
     self.timer_enabled = not manual
     if self.timer_enabled:
         self._make_timer()
         self.clear()
Exemple #2
0
 def test_validate_first_good_choice_in_list(self):
     config = _root({'foo': 3.14})
     valid = config['foo'].get(
         confuse.OneOf([
             confuse.Integer(),
             confuse.Number(),
         ]))
     self.assertEqual(valid, 3)
Exemple #3
0
class VLCMon(WebInterfaceMon):
    name = 'vlc'
    URL = "http://{ip}:{port}"
    STATES = ['stopped', 'paused', 'playing']
    CONFIG_TEMPLATE = {
        "ip": confuse.String(default="localhost"),
        "port": confuse.String(default="auto-detect"),
        "password": confuse.String(default="auto-detect"),
        "poll_interval": confuse.Number(default=10),
    }

    def __init__(self, scrobble_queue):
        try:
            web_pwd = self.config['password']
            self.URL = self.URL.format(**self.config)
        except KeyError:
            logger.exception('Check config for correct VLC params.')
            return
        super().__init__(scrobble_queue)
        self.sess.auth = ('', web_pwd)
        self.status_url = self.URL + '/requests/status.json'
        self.playlist_url = self.URL + '/requests/playlist.json'

    @classmethod
    def read_player_cfg(cls, auto_keys=None):
        if sys.platform == "darwin":
            prefs_dir = Path(
                "~/Library/Preferences/org.videolan.vlc").expanduser()
        else:
            prefs_dir = Path(
                appdirs.user_config_dir("vlc", False, roaming=True))
        vlcrc_path = prefs_dir / "vlcrc"
        vlcrc = ConfigParser(strict=False)
        vlcrc.optionxform = lambda option: option
        if not vlcrc.read(vlcrc_path, encoding="utf-8-sig"):
            raise FileNotFoundError(vlcrc_path)
        return {
            "port": lambda: vlcrc.get("core", "http-port", fallback=8080),
            "password": lambda: vlcrc.get("lua", "http-password"),
        }

    def update_status(self):
        try:
            status_data = self.sess.get(self.status_url).json()
        except json.JSONDecodeError:
            raise requests.ConnectionError
        if not status_data['length']:
            self.status = {}
            return
        self.status['duration'] = status_data['length']
        self.status['position'] = status_data['time']
        self.status['state'] = self.STATES.index(status_data['state'])
        self.status['filepath'] = self._get_filepath()

    def _get_filepath(self):
        playlist_data = self.sess.get(self.playlist_url).json()
        file_data = search_dict_for_current(playlist_data)
        return file_uri_to_path(file_data['uri'])
Exemple #4
0
class MPCMon(WebInterfaceMon):
    exclude_import = True
    URL = "http://{ip}:{port}/variables.html"
    PATTERN = re.compile(r'\<p id=\"([a-z]+)\"\>(.*?)\<', re.MULTILINE)
    CONFIG_TEMPLATE = {
        "ip": confuse.String(default="localhost"),
        "port": confuse.String(default="auto-detect"),
        "poll_interval": confuse.Number(default=10),
    }

    def __init__(self, scrobble_queue):
        try:
            self.URL = self.URL.format(**self.config)
        except KeyError:
            logger.exception(f'Check config for correct {self.name} params.')
            return
        super().__init__(scrobble_queue)

    @staticmethod
    def _read_registry_cfg(path):
        import winreg
        try:
            hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path)
        except FileNotFoundError as e:
            e.filename = path
            raise
        return {"port": lambda: winreg.QueryValueEx(hkey, "WebServerPort")[0]}

    def get_vars(self):
        response = self.sess.get(self.URL)
        matches = self.PATTERN.findall(response.text)
        return dict(matches)

    def update_status(self):
        variables = self.get_vars()
        # when a file has just started, it may happen that the variables page is
        # pingable, but not populated. So check that variables isn't empty
        if not variables or variables['duration'] == '0':
            self.status = {}
            return
        self.status['state'] = int(variables['state'])
        for key in ('position', 'duration'):
            self.status[key] = int(variables[key]) / 1000
        # instead of stopping, mpc pauses the file at the last second
        if variables['positionstring'] == variables['durationstring']:
            self.status['state'] = 0
        self.status['filepath'] = variables['filepath']
Exemple #5
0
class Config:
    base_template = {
        'twitter': {
            'consumer_key': confuse.String(),
            'consumer_secret': confuse.String(),
            'access_token_key': confuse.String(),
            'access_token_secret': confuse.String(),
            'min_ratelimit_percent': confuse.Integer(),

            'search': {
                'queries': confuse.TypeTemplate(list),
                'max_queue': confuse.Integer(),
                'max_quote_depth': confuse.Integer(),
                'min_quote_similarity': confuse.Number(),
                'skip_retweeted': confuse.TypeTemplate(bool),
                # Is updated on runtime
                'filter': {},
                # Is updated on runtime
                'sort': {},
            },
            # Is updated on runtime
            'actions': {},
            'scheduler': {
                'search_interval': confuse.Integer(),
                'retweet_interval': confuse.Integer(),
                'retweet_random_margin': confuse.Integer(),
                'blocked_users_update_interval': confuse.Integer(),
                'clear_queue_interval': confuse.Integer(),
                'rate_limit_update_interval': confuse.Integer(),
                'check_mentions_interval': confuse.Integer(),
            },
        },
        # Is updated on runtime
        'notifiers': {}
    }

    _valid = None

    @staticmethod
    def get():
        """
        Gets the static config object
        :return:
        """
        if Config._valid is None:
            raise ValueError("Configuration not loaded")
        return Config._valid

    @staticmethod
    def load(filename=None):
        """
        Loads a file and imports the settings
        :param filename: the file to import
        """
        config = confuse.LazyConfig('Yatcobot', __name__)

        # Add default config when in egg (using this way because egg is breaking the default way)
        if len(config.sources) == 0:
            default_config_text = pkg_resources.resource_string("yatcobot.config", "config_default.yaml")
            default_config = confuse.ConfigSource(yaml.load(default_config_text, Loader=confuse.Loader),
                                                  'pkg/config/config_default.yaml',
                                                  True)
            config.add(default_config)

        # Add user specified config
        if filename is not None and os.path.isfile(filename):
            config.set_file(filename)

        logger.info('Loading config files (From highest priority to lowest):')
        config.resolve()
        for i, config_source in enumerate(config.sources):
            logger.info(f'{i}: Path: {config_source.filename}')

        # Update template from plugins
        template = Config.get_template()
        Config._valid = config.get(template)

    @staticmethod
    def get_template():
        """
        Updates the config template dynamically from plugins
        :return: config template
        """
        template = Config.base_template

        # Merge filters templates
        from yatcobot.plugins.filters import FilterABC
        template['twitter']['search']['filter'].update(FilterABC.get_config_template())

        # Merge ratings templates
        from yatcobot.plugins.ratings import RatingABC
        template['twitter']['search']['sort'].update(RatingABC.get_config_template())

        # Merge actions templates
        from yatcobot.plugins.actions import ActionABC
        template['twitter']['actions'].update(ActionABC.get_config_template())

        # Merge notifiers templates
        from yatcobot.plugins.notifiers import NotifierABC
        template['notifiers'].update(NotifierABC.get_config_template())

        return template
Exemple #6
0
class MPVMon(Monitor):
    name = 'mpv'
    exclude_import = True
    WATCHED_PROPS = [
        'pause', 'path', 'working-directory', 'duration', 'time-pos'
    ]
    CONFIG_TEMPLATE = {
        "ipc_path": confuse.String(default="auto-detect"),
        "poll_interval": confuse.Number(default=10),
    }

    def __init__(self, scrobble_queue):
        try:
            self.ipc_path = self.config['ipc_path']
        except KeyError:
            logger.exception('Check config for correct MPV params.')
            return
        super().__init__(scrobble_queue)
        self.buffer = ''
        self.lock = threading.Lock()
        self.poll_timer = None
        self.write_queue = Queue()
        self.sent_commands = {}
        self.command_counter = 1
        self.vars = {}

    @classmethod
    def read_player_cfg(cls, auto_keys=None):
        conf_path = (Path(
            appdirs.user_config_dir("mpv", roaming=True, appauthor=False)) /
                     "mpv.conf")
        mpv_conf = ConfigParser(allow_no_value=True, strict=False)
        mpv_conf.optionxform = lambda option: option
        mpv_conf.read_string("[root]\n" + conf_path.read_text())
        return {"ipc_path": lambda: mpv_conf.get("root", "input-ipc-server")}

    def run(self):
        while True:
            if self.can_connect():
                self.update_vars()
                self.conn_loop()
                if self.poll_timer:
                    self.poll_timer.cancel()
                time.sleep(1)
            else:
                logger.info('Unable to connect to MPV. Check ipc path.')
                time.sleep(10)

    def update_status(self):
        fpath = Path(self.vars['working-directory']) / Path(self.vars['path'])

        # Update last known position if player is stopped
        pos = self.vars['time-pos']
        if self.vars['state'] == 0 and self.status['state'] == 2:
            pos += round(time.time() - self.status['time'], 3)
        self.status = {
            'state': self.vars['state'],
            'filepath': str(fpath),
            'position': pos,
            'duration': self.vars['duration'],
            'time': time.time()
        }
        self.handle_status_update()

    def update_vars(self):
        """Query mpv for required properties."""
        self.updated_props_count = 0
        for prop in self.WATCHED_PROPS:
            self.send_command(['get_property', prop])
        if self.poll_timer:
            self.poll_timer.cancel()
        self.poll_timer = threading.Timer(10, self.update_vars)
        self.poll_timer.name = 'mpvpoll'
        self.poll_timer.start()

    def handle_event(self, event):
        if event == 'end-file':
            self.vars['state'] = 0
            self.is_running = False
            self.update_status()
        elif event == 'pause':
            self.vars['state'] = 1
            self.update_vars()
        elif event == 'unpause' or event == 'playback-restart':
            self.vars['state'] = 2
            self.update_vars()

    def handle_cmd_response(self, resp):
        command = self.sent_commands[resp['request_id']]['command']
        del self.sent_commands[resp['request_id']]
        if resp['error'] != 'success':
            logger.error(f'Error with command {command!s}. Response: {resp!s}')
            return
        elif command[0] != 'get_property':
            return
        param = command[1]
        data = resp['data']
        if param == 'pause':
            self.vars['state'] = 1 if data else 2
        if param in self.WATCHED_PROPS:
            self.vars[param] = data
            self.updated_props_count += 1
        if self.updated_props_count == len(self.WATCHED_PROPS):
            self.update_status()

    def on_data(self, data):
        self.buffer = self.buffer + data.decode('utf-8')
        while True:
            line_end = self.buffer.find('\n')
            if line_end == -1:
                # partial line received
                # self.on_line() is called in next data batch
                break
            else:
                self.on_line(self.buffer[:line_end])  # doesn't include \n
                self.buffer = self.buffer[line_end + 1:]  # doesn't include \n

    def on_line(self, line):
        try:
            mpv_json = json.loads(line)
        except json.JSONDecodeError:
            logger.warning('Invalid JSON received. Skipping. ' + line,
                           exc_info=True)
            return
        if 'event' in mpv_json:
            self.handle_event(mpv_json['event'])
        elif 'request_id' in mpv_json:
            self.handle_cmd_response(mpv_json)

    def send_command(self, elements):
        with self.lock:
            command = {'command': elements, 'request_id': self.command_counter}
            self.sent_commands[self.command_counter] = command
            self.command_counter += 1
            self.write_queue.put(str.encode(json.dumps(command) + '\n'))
Exemple #7
0
class MPVMon(Monitor):
    name = 'mpv'
    exclude_import = True
    WATCHED_PROPS = frozenset(
        ('pause', 'path', 'working-directory', 'duration', 'time-pos'))
    CONFIG_TEMPLATE = {
        "ipc_path": confuse.String(default="auto-detect"),
        "poll_interval": confuse.Number(default=10),
        # seconds to wait while reading data from mpv
        "read_timeout": confuse.Number(default=2),
        # seconds to wait while writing data to mpv
        "write_timeout": confuse.Number(default=60),
        # seconds to wait after one file ends to check for the next play
        # needed to make sure we don't reconnect too soon after end-file
        "restart_delay": confuse.Number(default=0.1)
    }

    def __init__(self, scrobble_queue):
        super().__init__(scrobble_queue)
        self.ipc_path = self.config['ipc_path']
        self.read_timeout = self.config['read_timeout']
        self.write_timeout = self.config['write_timeout']
        self.poll_interval = self.config['poll_interval']
        self.restart_delay = self.config['restart_delay']
        self.buffer: bytes = b''
        self.ipc_lock = threading.Lock()  # for IPC write queue
        self.poll_timer = None
        self.write_queue = Queue()
        self.sent_commands = {}
        self.command_counter = 1
        self.vars = {}

    @classmethod
    def read_player_cfg(cls, auto_keys=None):
        if sys.platform == "darwin":
            conf_path = Path.home() / ".config" / "mpv" / "mpv.conf"
        else:
            conf_path = (Path(
                appdirs.user_config_dir("mpv", roaming=True, appauthor=False))
                         / "mpv.conf")
        mpv_conf = ConfigParser(allow_no_value=True,
                                strict=False,
                                inline_comment_prefixes="#")
        mpv_conf.optionxform = lambda option: option
        mpv_conf.read_string("[root]\n" + conf_path.read_text())
        return {"ipc_path": lambda: mpv_conf.get("root", "input-ipc-server")}

    def run(self):
        while True:
            if self.can_connect():
                self.update_vars()
                self.conn_loop()
                if self.vars.get('state', 0) != 0:
                    # create a 'stop' event in case the player didn't send 'end-file'
                    self.vars['state'] = 0
                    self.update_status()
                self.vars = {}
                if self.poll_timer:
                    self.poll_timer.cancel()
                time.sleep(self.restart_delay)
            else:
                logger.info('Unable to connect to MPV. Check ipc path.')
                time.sleep(self.poll_interval)

    def update_status(self):
        if not self.WATCHED_PROPS.issubset(self.vars):
            logger.warning("Incomplete media status info")
            return
        fpath = self.vars['path']
        if not is_url(fpath) and not Path(fpath).is_absolute():
            fpath = str(Path(self.vars['working-directory']) / fpath)

        # Update last known position if player is stopped
        pos = self.vars['time-pos']
        if self.vars['state'] == 0 and self.status['state'] == 2:
            pos += round(time.time() - self.status['time'], 3)
        pos = min(pos, self.vars['duration'])

        self.status = {
            'state': self.vars['state'],
            'filepath': fpath,
            'position': pos,
            'duration': self.vars['duration'],
            'time': time.time()
        }
        self.handle_status_update()

    def update_vars(self):
        """Query mpv for required properties."""
        self.updated_props_count = 0
        for prop in self.WATCHED_PROPS:
            self.send_command(['get_property', prop])
        if self.poll_timer:
            self.poll_timer.cancel()
        self.poll_timer = threading.Timer(self.poll_interval, self.update_vars)
        self.poll_timer.name = 'mpvpoll'
        self.poll_timer.start()

    def handle_event(self, event):
        if event == 'end-file':
            # Since the player might be shutting down, we can't update vars.
            # Reuse the previous self.vars and only update the state value.
            # This might be inaccurate in terms of position, which is why
            # regular polling is needed
            self.vars['state'] = 0
            self.update_status()
        elif event == 'pause':
            self.vars['state'] = 1
            self.update_vars()
        elif event == 'unpause' or event == 'playback-restart':
            self.vars['state'] = 2
            self.update_vars()

    def handle_cmd_response(self, resp):
        command = self.sent_commands.pop(resp['request_id'])
        if resp['error'] != 'success':
            logger.error(f'Error with command {command!s}. Response: {resp!s}')
            return
        elif command[0] != 'get_property':
            return
        param = command[1]
        data = resp['data']
        if param == 'pause':
            self.vars['state'] = 1 if data else 2
        if param in self.WATCHED_PROPS:
            self.vars[param] = data
            self.updated_props_count += 1
        if self.updated_props_count == len(self.WATCHED_PROPS):
            self.update_status()
            # resetting self.vars to {} here is a bad idea.

    def on_data(self, data: bytes):
        self.buffer += data
        partial_line = b""
        for line in self.buffer.splitlines(keepends=True):
            if line.endswith(b"\n"):
                # no need to strip, json.loads will handle it
                self.on_line(line)
            else:
                # partial line received
                # self.on_line() is called in next data batch
                partial_line = line
        self.buffer = partial_line

    def on_line(self, line: bytes):
        try:
            mpv_json = json.loads(line)
        except json.JSONDecodeError:
            logger.warning('Invalid JSON received. Skipping.', exc_info=True)
            logger.debug(line)
            return
        if 'event' in mpv_json:
            self.handle_event(mpv_json['event'])
        elif 'request_id' in mpv_json:
            self.handle_cmd_response(mpv_json)

    def send_command(self, elements):
        with self.ipc_lock:
            command = {'command': elements, 'request_id': self.command_counter}
            self.sent_commands[self.command_counter] = elements
            self.command_counter += 1
            self.write_queue.put(str.encode(json.dumps(command) + '\n'))
Exemple #8
0
class Monitor(Thread):
    """Generic base class that polls the player for state changes,
     and sends the info to scrobble queue."""

    CONFIG_TEMPLATE = {
        # min percent jump to consider for scrobbling to trakt
        'skip_interval': confuse.Number(default=5),
        # min progress (in %) at which file should be opened for preview to be started
        'preview_threshold': confuse.Number(default=80),
        # in seconds. How long the monitor should wait to start sending scrobbles
        'preview_duration': confuse.Number(default=60),
        # in seconds. Max time elapsed between a "play->pause" transition to trigger
        # the "fast_pause" state
        'fast_pause_threshold': confuse.Number(default=1),
        # in seconds. How long the monitor should wait to start sending scrobbles
        'fast_pause_duration': confuse.Number(default=5),
    }

    def __new__(cls, *args, **kwargs):
        try:
            cls.inject_base_config()
            cls.config = cls.autoload_cfg()
        except AutoloadError as e:
            logger.debug(str(e))
            logger.error(f"Config value autoload failed for {cls.name}.")
        except Exception:
            logger.exception(f"Config value autoload failed for {cls.name}.")
        else:
            return super().__new__(cls)

    @classmethod
    def inject_base_config(cls):
        """Inject default values from base config to allow player-specific overrides"""
        base_config = config['players'].get(Monitor.CONFIG_TEMPLATE)
        base_template = confuse.as_template(base_config)
        template = getattr(cls, 'CONFIG_TEMPLATE', {})
        updated = {**base_template.subtemplates, **template}
        cls.CONFIG_TEMPLATE = updated

    @classmethod
    def autoload_cfg(cls):
        template = getattr(cls, 'CONFIG_TEMPLATE', None)
        monitor_cfg = config['players'][cls.name].get(template)
        auto_keys = {k for k, v in monitor_cfg.items() if v == "auto-detect"}
        if not auto_keys:
            return monitor_cfg
        try:
            loaders = getattr(cls, "read_player_cfg")(auto_keys)
        except AttributeError:
            logger.debug(f"Auto val not found for {', '.join(auto_keys)}")
            logger.error(f"Autoload not supported for {cls.name}.")
            raise AutoloadError
        except FileNotFoundError as e:
            raise AutoloadError(src=e.filename)
        while auto_keys:
            param = auto_keys.pop()
            try:
                param_loader = loaders[param]
            except KeyError:
                logger.error(f"Autoload not supported for '{param}'.")
                raise AutoloadError(param)
            try:
                monitor_cfg[param] = param_loader()
                logger.debug(
                    f"Autoloaded {cls.name} {param} = {monitor_cfg[param]}")
            except FileNotFoundError as e:
                raise AutoloadError(src=e.filename)
        return monitor_cfg

    def __init__(self, scrobble_queue):
        super().__init__()
        logger.info('Started monitor for ' + self.name)
        self.scrobble_queue = scrobble_queue
        self.skip_interval = self.config['skip_interval']
        self.preview_threshold = self.config['preview_threshold']
        self.preview_duration = self.config['preview_duration']
        self.fast_pause_threshold = self.config['fast_pause_threshold']
        self.fast_pause_duration = self.config['fast_pause_duration']
        self.is_running = False
        self.status = {}
        self.prev_state = {}
        self.preview = False
        self.fast_pause = False
        self.scrobble_buf = None
        self.lock = Lock()
        self.preview_timer: ResumableTimer = None
        self.fast_pause_timer: ResumableTimer = None

    def parse_status(self):
        if ('filepath' not in self.status and 'media_info'
                not in self.status) or not self.status.get('duration'):
            return {}

        if 'filepath' in self.status:
            media_info = get_media_info(self.status['filepath'])
        else:
            media_info = self.status['media_info']

        if media_info is None:
            return {}

        ep = media_info.get('episode')
        if isinstance(ep, list):
            media_info = media_info.copy()
            num_eps = len(media_info['episode'])
            self.status['duration'] = self.status['duration'] // num_eps
            ep_num = int(self.status['position'] // self.status['duration'])
            media_info['episode'] = media_info['episode'][ep_num]
            self.status['position'] %= self.status['duration']
        elif isinstance(ep, str):
            media_info['episode'] = int(ep)

        progress = min(
            round(self.status['position'] * 100 / self.status['duration'], 2),
            100)
        return {
            'state': self.status['state'],
            'progress': progress,
            'media_info': media_info,
            'updated_at': time.time(),
        }

    def decide_action(self, prev, current):
        """
        Decide what action(s) to take depending on the prev and current states.
        """

        if not prev and not current:
            return None
        transition = Transition(prev, current)
        if (not prev or not current or not transition.is_same_media()
                or prev['state'] == State.Stopped):
            # media changed
            if self.preview:
                yield 'exit_preview'
            elif prev and prev['state'] != State.Stopped:
                yield 'stop_previous'
            if self.fast_pause:
                yield 'exit_fast_pause'
            if current:
                if current['progress'] > self.preview_threshold:
                    yield 'enter_preview'
                elif not prev or not transition.is_same_media():
                    yield 'scrobble'
        elif transition.state_changed(
        ) or transition.progress() > self.skip_interval:
            # state changed
            if self.preview:
                if current['state'] == State.Stopped:
                    yield 'exit_preview'
                elif transition.is_state_jump(State.Playing, State.Paused):
                    yield 'pause_preview'
                elif current['state'] == State.Playing:
                    yield 'resume_preview'
                else:
                    yield 'invalid_state'
            elif self.fast_pause:
                if (current['state'] == State.Stopped
                        or transition.progress() > self.skip_interval):
                    yield 'scrobble'
                    yield 'exit_fast_pause'
                elif current['state'] == State.Paused:
                    yield 'clear_buf'
                elif current['state'] == State.Playing:
                    yield 'delayed_play'
            else:  # normal state
                yield 'scrobble'
                if (transition.is_state_jump(State.Playing, State.Paused)
                        and transition.elapsed_realtime() <
                        self.fast_pause_threshold):
                    yield 'enter_fast_pause'

    def scrobble_status(self, status):
        verb = SCROBBLE_VERBS[status['state']]
        self.scrobble_queue.put((verb, status))

    def delayed_scrobble(self, cleanup=None):
        logger.debug("Delayed scrobble")
        with self.lock:
            if self.scrobble_buf:
                logger.debug(self.scrobble_buf)
                self.scrobble_status(self.scrobble_buf)
            if cleanup:
                cleanup()

    def clear_timer(self, timer_name):
        timer = getattr(self, timer_name)
        if timer is not None:
            timer.cancel()
            setattr(self, timer_name, None)

    def exit_preview(self):
        logger.debug("Exiting preview")
        if self.preview:
            self.preview = False
            self.scrobble_buf = None
            self.clear_timer('preview_timer')

    def exit_fast_pause(self):
        logger.debug("Exiting fast_pause")
        if self.fast_pause:
            self.fast_pause = False
            self.scrobble_buf = None
            self.clear_timer('fast_pause_timer')

    def scrobble_if_state_changed(self, prev, current):
        """
        Possible race conditions:
        1) start_preview, after __preview_duration__ secs, stop_preview
           start_preview starts preview_timer for " secs, with cleanup=exit_preview.
           the stop_preview also triggers exit_preview, both are run parallelly.
        """
        for action in self.decide_action(prev, current):
            logger.debug(f"action={action}")
            if action == "scrobble":
                logger.debug(current)
                self.scrobble_status(current)
            elif action == "stop_previous":
                self.scrobble_queue.put(("stop", prev))
            elif action == "exit_preview":
                self.exit_preview()
            elif action == "enter_preview":
                assert not self.preview and not self.scrobble_buf, "Invalid state"
                self.preview = True
                self.scrobble_buf = current
                self.preview_timer = ResumableTimer(self.preview_duration,
                                                    self.delayed_scrobble,
                                                    (self.exit_preview, ))
                self.preview_timer.start()
            elif action == "pause_preview":
                self.scrobble_buf = current
                self.preview_timer.pause()
            elif action == "resume_preview":
                self.scrobble_buf = current
                self.preview_timer.resume()
            elif action == "enter_fast_pause":
                assert not self.fast_pause, "Invalid state"
                self.fast_pause = True
            elif action == "clear_buf":
                self.clear_timer('fast_pause_timer')
                self.scrobble_buf = None
            elif action == "delayed_play":
                self.clear_timer('fast_pause_timer')
                self.scrobble_buf = current
                self.fast_pause_timer = ResumableTimer(
                    self.fast_pause_duration,
                    self.delayed_scrobble,
                    (self.exit_fast_pause, ),
                )
                self.fast_pause_timer.start()
            elif action == "exit_fast_pause":
                self.exit_fast_pause()
            else:
                logger.warning(f"Invalid action {action}")

    def handle_status_update(self):
        current_state = self.parse_status()
        with self.lock:
            self.scrobble_if_state_changed(self.prev_state, current_state)
        self.prev_state = current_state
Exemple #9
0
 def test_validate_string_as_number(self):
     config = _root({'foo': 'bar'})
     with self.assertRaises(confuse.ConfigTypeError):
         config['foo'].get(confuse.Number())
Exemple #10
0
 def test_validate_float_as_number(self):
     config = _root({'foo': 3.0})
     valid = config['foo'].get(confuse.Number())
     self.assertIsInstance(valid, float)
     self.assertEqual(valid, 3.0)
Exemple #11
0
 def test_validate_int_as_number(self):
     config = _root({'foo': 2})
     valid = config['foo'].get(confuse.Number())
     self.assertIsInstance(valid, int)
     self.assertEqual(valid, 2)
Exemple #12
0
class PlexMon(WebInterfaceMon):
    name = "plex"
    exclude_import = False
    URL = "http://{ip}:{port}"
    STATES = {"stopped": 0, "paused": 1, "buffering": 1, "playing": 2}
    CONFIG_TEMPLATE = {
        "ip": confuse.String(default="localhost"),
        "port": confuse.String(default="32400"),
        "poll_interval": confuse.Number(default=10),
    }

    def __init__(self, scrobble_queue):
        try:
            self.token = get_token()
            self.URL = self.URL.format(**self.config)
        except KeyError:
            logger.exception("Check config for correct Plex params.")
            return
        if not self.token:
            logger.error("Unable to retrieve plex token.")
            return
        super().__init__(scrobble_queue)
        self.sess.headers["Accept"] = "application/json"
        self.sess.headers["X-Plex-Token"] = self.token
        self.session_url = self.URL + "/status/sessions"
        self.media_info_cache = {}

    def get_data(self, url):
        data = self.sess.get(url).json()["MediaContainer"]
        if data["size"] > 0:
            return data["Metadata"][0]

    def update_status(self):
        status_data = self.get_data(self.session_url)
        if not status_data:
            self.status = {}
            return
        self.status["duration"] = int(status_data["duration"]) / 1000
        self.status["position"] = int(status_data["viewOffset"]) / 1000
        self.status["state"] = self.STATES.get(status_data["Player"]["state"],
                                               0)
        self.status["media_info"] = self.get_media_info(status_data)

    def get_media_info(self, status_data):
        media_info = self.media_info_cache.get(status_data["ratingKey"])
        if not media_info:
            media_info = self._get_media_info(status_data)
            self.media_info_cache[status_data["ratingKey"]] = media_info
        return media_info

    @staticmethod
    def _get_media_info(status_data):
        if status_data["type"] == "movie":
            return {"type": "movie", "title": status_data["title"]}
        elif status_data["type"] == "episode":
            return {
                "type": "episode",
                "title": status_data["grandparentTitle"],
                "season": status_data["parentIndex"],
                "episode": status_data["index"],
            }
Exemple #13
0
class PlexMon(WebInterfaceMon):
    name = "plex"
    exclude_import = False
    URL = "http://{ip}:{port}"
    STATES = {"stopped": 0, "paused": 1, "buffering": 1, "playing": 2}
    CONFIG_TEMPLATE = {
        "ip": confuse.String(default="localhost"),
        "port": confuse.String(default="32400"),
        "poll_interval": confuse.Number(default=10),
        "scrobble_user": confuse.String(default="")
    }

    def __init__(self, scrobble_queue):
        try:
            self.token = get_token()
            self.URL = self.URL.format(**self.config)
        except KeyError:
            logger.exception("Check config for correct Plex params.")
            return
        if not self.token:
            logger.error("Unable to retrieve plex token.")
            return
        super().__init__(scrobble_queue)
        self.sess.headers["Accept"] = "application/json"
        self.sess.headers["X-Plex-Token"] = self.token
        self.session_url = self.URL + "/status/sessions"
        self.media_info_cache = {}

    def get_data(self, url):
        resp = self.sess.get(url)
        # TODO: If we get a 401, clear token and restart plex auth flow
        resp.raise_for_status()
        data = resp.json()["MediaContainer"]

        if data["size"] <= 0:
            return None

        # no user filter
        if not self.config["scrobble_user"] or "User" not in data["Metadata"][
                0]:
            return data["Metadata"][0]

        for metadata in data["Metadata"]:
            if metadata["User"]["title"] == self.config["scrobble_user"]:
                return metadata

    def update_status(self):
        status_data = self.get_data(self.session_url)
        if not status_data:
            self.status = {}
            return
        self.status["duration"] = int(status_data["duration"]) / 1000
        self.status["position"] = int(status_data["viewOffset"]) / 1000
        self.status["state"] = self.STATES.get(status_data["Player"]["state"],
                                               0)
        self.status["media_info"] = self.get_media_info(status_data)

    def get_media_info(self, status_data):
        media_info = self.media_info_cache.get(status_data["ratingKey"])
        if not media_info:
            if status_data["type"] == "episode":
                # get the show's data
                show_key = status_data["grandparentKey"]
                show_data = self.media_info_cache.get(show_key)
                if not show_data:
                    show_data = self.get_data(self.URL + show_key)
                    self.media_info_cache[show_key] = show_data
            else:
                show_data = None
            media_info = self._get_media_info(status_data, show_data)
            self.media_info_cache[status_data["ratingKey"]] = media_info
        return media_info

    @staticmethod
    def _get_media_info(status_data, show_data=None):
        if status_data["type"] == "movie":
            info = {
                "type": "movie",
                "title": status_data["title"],
                "year": status_data.get("year"),
            }
        elif status_data["type"] == "episode":
            info = {
                "type": "episode",
                "title": status_data["grandparentTitle"],
                "season": status_data["parentIndex"],
                "episode": status_data["index"],
                "year": show_data and show_data.get("year"),
            }
        else:
            logger.warning(f"Unknown media type {status_data['type']}")
            return None

        if info["year"] is not None:
            info["year"] = year = int(info["year"])
            # if year is at the end of the title, like "The Boys (2019)", remove it
            # otherwise it might not show up on Trakt search
            suffix = f" ({year})"
            if info["title"].endswith(suffix):
                info["title"] = info["title"].replace(suffix, "")
        return cleanup_guess(info)
Exemple #14
0
class Config:
    template = {
        'twitter': {
            'consumer_key': confuse.String(),
            'consumer_secret': confuse.String(),
            'access_token_key': confuse.String(),
            'access_token_secret': confuse.String(),
            'min_ratelimit_percent': confuse.Integer(),

            'search': {
                'queries': confuse.TypeTemplate(list),
                'max_queue': confuse.Integer(),
                'max_quote_depth': confuse.Integer(),
                'min_quote_similarity': confuse.Number(),
                'skip_retweeted': confuse.TypeTemplate(bool),
                'filter': {
                    'min_retweets': {
                        'enabled': confuse.TypeTemplate(bool),
                        'number': confuse.Integer()
                    }
                },
                'sort': {
                    'by_keywords': {
                        'enabled': confuse.TypeTemplate(bool),
                        'keywords': confuse.StrSeq()
                    },
                    'by_age': {
                        'enabled': confuse.TypeTemplate(bool),
                    },
                    'by_retweets_count': {
                        'enabled': confuse.TypeTemplate(bool),
                    }
                }
            },

            'actions': {
                'follow': {
                    'enabled': confuse.TypeTemplate(bool),
                    'keywords': confuse.StrSeq(),
                    'max_following': confuse.Integer(),
                    'multiple': confuse.TypeTemplate(bool)
                },
                'favorite': {
                    'enabled': confuse.TypeTemplate(bool),
                    'keywords': confuse.StrSeq()
                },
                'tag_friend': {
                    'enabled': confuse.TypeTemplate(bool),
                    'friends': confuse.StrSeq(),
                }
            },

            'scheduler': {
                'search_interval': confuse.Integer(),
                'retweet_interval': confuse.Integer(),
                'retweet_random_margin': confuse.Integer(),
                'blocked_users_update_interval': confuse.Integer(),
                'clear_queue_interval': confuse.Integer(),
                'rate_limit_update_interval': confuse.Integer(),
                'check_mentions_interval': confuse.Integer(),
            },
        },
        'notifiers': {
            'mail': {
                'enabled': confuse.TypeTemplate(bool),
                'host': confuse.String(),
                'port': confuse.Integer(),
                'tls': confuse.TypeTemplate(bool),
                'username': confuse.String(),
                'password': confuse.String(),
                'recipient': confuse.String()
            },
            'pushbullet': {
                'enabled': confuse.TypeTemplate(bool),
                'token': confuse.String()
            }
        }
    }

    _valid = None

    @staticmethod
    def get():
        """
        Gets the static config object
        :return:
        """
        if Config._valid is None:
            raise ValueError("Configuration not loaded")
        return Config._valid

    @staticmethod
    def load(filename=None):
        """
        Loads a file and imports the settings
        :param filename: the file to import
        """
        config = confuse.LazyConfig('Yatcobot', __name__)

        # Add default config when in egg (using this way because egg is breaking the default way)
        if len(config.sources) == 0:
            default_config_text = pkg_resources.resource_string("yatcobot", "config_default.yaml")
            default_config = confuse.ConfigSource(yaml.load(default_config_text, Loader=confuse.Loader),
                                                  'pkg/config_default.yaml',
                                                  True)
            config.add(default_config)

        # Add user specified config
        if filename is not None and os.path.isfile(filename):
            config.set_file(filename)

        logger.info('Loading config files (From highest priority to lowest):')
        for i, config_source in enumerate(config.sources):
            logger.info('{}: Path: {}'.format(i, config_source.filename))
        Config._valid = config.get(Config.template)
Exemple #15
0
 "target": confuse.TypeTemplate(str, default=None),
 "paths": {
     "json_dir": confuse.Filename(default=str(DEFAULT_JSON_DIR)),
     "conf_dir": confuse.Filename(default=str(DEFAULT_JSON_DIR)),
     "log_dir": confuse.Filename(default=str(DEFAULT_LOG_DIR)),
 },
 "wobjs": {
     "picking_wobj_name": str,
     "placing_wobj_name": str
 },
 "tool": {
     "tool_name": str,
     "io_needles_pin": str,
     "grip_state": int,
     "release_state": int,
     "wait_before_io": confuse.Number(default=2),
     "wait_after_io": confuse.Number(default=0.5),
 },
 "speed_values": {
     "speed_override": confuse.Number(default=100),
     "speed_max_tcp": float,
     "accel": float,
     "accel_ramp": confuse.Number(default=100),
 },
 "safe_joint_positions": {
     "start": confuse.Sequence([float] * 6),
     "end": confuse.Sequence([float] * 6),
 },
 "movement": {
     "offset_distance": float,
     "speed_placing": float,