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 prepare_scrobble_data(title, type, year=None, *args, **kwargs): trakt_id = get_trakt_id(title, type, year) if trakt_id < 1: logger.warning(f"Invalid trakt id for {title}") return None if type == 'movie': return {'movie': {'ids': {'trakt': trakt_id}}} elif type == 'episode': return { 'show': {'ids': {'trakt': trakt_id}}, 'episode': { 'season': kwargs['season'], 'number': kwargs['episode'] } }
def scrobble(self, verb, data): resp = trakt.scrobble(verb, **data) if resp: if 'movie' in resp: name = resp['movie']['title'] else: name = (resp['show']['title'] + " S{season:02}E{number:02}".format(**resp['episode'])) msg = f"Scrobble {verb} successful for {name}" logger.info(msg) notify(msg) self.backlog_cleaner.clear() elif resp is False and verb == 'stop' and data['progress'] > 80: logger.warning('Scrobble unsuccessful. Will try again later.') self.backlog_cleaner.add(data) else: logger.warning('Scrobble unsuccessful.')
def run(self): while True: try: self.update_status() except requests.ConnectionError: logger.info(f'Unable to connect to {self.name}. Ensure that ' 'the web interface is running.') self.status = {} except requests.HTTPError as e: logger.error(f"Error while getting data from {self.name}: {e}") break if not self.status.get("filepath") and not self.status.get( "media_info"): self.status = {} self.handle_status_update() time.sleep(self.poll_interval) logger.warning(f"{self.name} monitor stopped")
def conn_loop(self): sock = socket.socket(socket.AF_UNIX) try: sock.connect(self.ipc_path) except ConnectionRefusedError: logger.warning("Connection refused. Maybe we retried too soon?") return self.is_running = True sock_list = [sock] while self.is_running: r, _, _ = select.select(sock_list, [], [], self.read_timeout) if r: # r == [sock] # socket has data to be read try: data = sock.recv(4096) except ConnectionResetError: self.is_running = False break if len(data) == 0: # EOF reached self.is_running = False break self.on_data(data) while not self.write_queue.empty(): # block until sock can be written to _, w, _ = select.select([], sock_list, [], self.write_timeout) if not w: logger.warning( "Timed out writing to socket. Killing connection.") self.is_running = False break try: sock.sendall(self.write_queue.get_nowait()) except BrokenPipeError: self.is_running = False break else: self.write_queue.task_done() sock.close() while not self.write_queue.empty(): self.write_queue.get_nowait() self.write_queue.task_done() logger.debug('Sock closed')
def update_status(self): if not self.WATCHED_PROPS.issubset(self.vars): logger.warning("Incomplete media status info") return 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) pos = min(pos, self.vars['duration']) self.status = { 'state': self.vars['state'], 'filepath': str(fpath), 'position': pos, 'duration': self.vars['duration'], 'time': time.time() } self.handle_status_update()
def scrobble(self, verb, data): logger.debug(f"Scrobbling {verb} at {data['progress']:.2f}% for " f"{data['media_info']['title']}") resp = trakt.scrobble(verb, **data) if resp: if 'movie' in resp: name = resp['movie']['title'] else: name = (resp['show']['title'] + " S{season:02}E{number:02}".format(**resp['episode'])) category = 'resume' if self.is_resume(verb, data) else verb msg = f"Scrobble {category} successful for {name}" logger.info(msg) notify(msg, category=f"scrobble.{category}") self.backlog_cleaner.clear() elif resp is False and verb == 'stop' and data['progress'] > 80: logger.warning('Scrobble unsuccessful. Will try again later.') self.backlog_cleaner.add(data) else: logger.warning('Scrobble unsuccessful.') self.prev_scrobble = (verb, data)
def get_trakt_id(title, item_type, year=None): required_type = 'show' if item_type == 'episode' else 'movie' global trakt_cache if not trakt_cache: trakt_cache = read_json(TRAKT_CACHE_PATH) or {'movie': {}, 'show': {}} key = f"{title}{year or ''}" trakt_id = trakt_cache[required_type].get(key) if trakt_id: return trakt_id logger.debug( f'Searching trakt: Title: "{title}"{year and f", Year: {year}" or ""}') results = search(title, [required_type], year) if results == [] and year is not None: # no match, possibly a mismatch in year metadata msg = ( f'Trakt search yielded no results for the {required_type}, {title}, ' f'Year: {year}. Retrying search without filtering by year.') logger.warning(msg) notify(msg, category="trakt") results = search(title, [required_type]) # retry without 'year' if results is None: # Connection error return 0 # Dont store in cache elif results == [] or results[0]['score'] < 5: # Weak or no match msg = f'Trakt search yielded no results for the {required_type}, {title}' msg += f", Year: {year}" * bool(year) logger.warning(msg) notify(msg, category="trakt") trakt_id = -1 else: trakt_id = results[0][required_type]['ids']['trakt'] trakt_cache[required_type][key] = trakt_id logger.debug(f'Trakt ID: {trakt_id}') write_json(trakt_cache, TRAKT_CACHE_PATH) return trakt_id
def scrobble(verb, media_info, progress, *args, **kwargs): scrobble_data = prepare_scrobble_data(**media_info) if not scrobble_data: return None scrobble_data['progress'] = progress scrobble_params = { "url": API_URL + '/scrobble/' + verb, "headers": trakt_auth.headers, "json": scrobble_data, "timeout": 30, } scrobble_resp = safe_request('post', scrobble_params) if scrobble_resp is not None: if scrobble_resp.status_code == HTTPStatus.NOT_FOUND: logger.warning("Not found on trakt. The media info is incorrect.") return None elif scrobble_resp.status_code == HTTPStatus.CONFLICT: logger.warning("Scrobble already exists on trakt server.") return None return scrobble_resp.json() if scrobble_resp else False
def main(): assert get_access_token() scrobble_queue = Queue() backlog_cleaner = BacklogCleaner() scrobbler = Scrobbler(scrobble_queue, backlog_cleaner) scrobbler.start() allowed_monitors = config['players']['monitored'].get(confuse.StrSeq(default=[])) all_monitors = collect_monitors() unknown = set(allowed_monitors).difference(Mon.name for Mon in all_monitors) if unknown: logger.warning(f"Unknown player(s): {', '.join(unknown)}") for Mon in all_monitors: if Mon.name not in allowed_monitors: continue mon = Mon(scrobble_queue) if not mon or not mon._initialized: logger.warning(f"Could not start monitor for {Mon.name}") continue mon.start()
def get_trakt_id(title, item_type): required_type = 'show' if item_type == 'episode' else 'movie' logger.debug('Searching cache.') trakt_id = trakt_cache[required_type].get(title) if trakt_id: return trakt_id logger.debug('Searching trakt.') results = search(title, [required_type]) if results is None: # Connection error return 0 # Dont store in cache elif results == [] or results[0]['score'] < 5: # Weak or no match logger.warning('Trakt search yielded no results.') notify('Trakt search yielded no results for ' + title) trakt_id = -1 else: trakt_id = results[0][required_type]['ids']['trakt'] trakt_cache[required_type][title] = trakt_id logger.debug(f'Trakt ID: {trakt_id}') write_json(trakt_cache, TRAKT_CACHE_PATH) return trakt_id
from win10toast import ToastNotifier toaster = ToastNotifier() elif sys.platform == 'darwin': import subprocess as sp else: try: from jeepney import DBusAddress, new_method_call from jeepney.io.blocking import open_dbus_connection except (ImportError, ModuleNotFoundError): import subprocess as sp notifier = None else: try: dbus_connection = open_dbus_connection(bus='SESSION') except Exception as e: logger.warning(f"Could not connect to DBUS: {e}") logger.warning("Disabling notifications") enabled_categories.clear() notifier = None else: notifier = DBusAddress( '/org/freedesktop/Notifications', bus_name='org.freedesktop.Notifications', interface='org.freedesktop.Notifications') def dbus_notify(title, body, timeout): msg = new_method_call( notifier, 'Notify', 'susssasa{sv}i',