def device_auth(): code_data = get_device_code() if not code_data: logger.error('Failed device auth.') sys.exit(1) logger.info(f"Verification URL: {code_data['verification_url']}") logger.info(f"User Code: {code_data['user_code']}") notify("Open {verification_url} in your browser and enter this code: " "{user_code}".format(**code_data), timeout=60, stdout=True) webbrowser.open(code_data['verification_url']) start = time.time() while time.time() - start < code_data['expires_in']: token_data = get_device_token(code_data['device_code']) if not token_data: logger.debug('Waiting for user to authorize the app.') time.sleep(int(code_data['interval'])) else: notify('App authorized successfully.', stdout=True) logger.info('Device auth successful.') break else: logger.error('Timed out during auth.') return token_data
def device_auth(self): code_data = self.get_device_code() if not code_data: logger.error("Could not get device code.") return logger.info(f"Verification URL: {code_data['verification_url']}") logger.info(f"User Code: {code_data['user_code']}") notify("Open {verification_url} in your browser and enter this code: " "{user_code}".format(**code_data), timeout=30, stdout=True, category="trakt") webbrowser.open(code_data['verification_url']) start = time.time() while time.time() - start < code_data['expires_in']: if self.get_device_token(code_data['device_code']): notify('App authorized successfully.', stdout=True, category="trakt") logger.info('App authorized successfully.') break logger.debug('Waiting for user to authorize the app.') time.sleep(int(code_data['interval'])) else: logger.error('Timed out during auth.')
def cleanup_guess(guess): if not guess: return None if any(key not in guess for key in ('title', 'type')) or \ (guess['type'] == 'episode' and 'episode' not in guess): logger.warning('Failed to parse filename for episode/movie info. ' 'Consider renaming/using custom regex.') return None if isinstance(guess['title'], list): guess['title'] = " ".join(guess['title']) req_keys = ['type', 'title'] if guess['type'] == 'episode': season = guess.get('season') if season is None: # if we don't find a season, default to 1 season = 1 # TODO: Add proper support for absolute-numbered episodes if isinstance(season, list): from trakt_scrobbler.notifier import notify msg = f"Multiple probable seasons found: ({','.join(map(str, season))}). " msg += "Consider renaming the folder." logger.warning(msg) notify(msg) return None guess['season'] = int(season) req_keys += ['season', 'episode'] if 'year' in guess: req_keys += ['year'] return {key: guess[key] for key in req_keys}
def device_auth(self): code_data = self.get_device_code() if not code_data: logger.error("Could not get device code.") return logger.info(f"Verification URL: {code_data['verification_url']}") logger.info(f"User Code: {code_data['user_code']}") notify( "Open {verification_url} in your browser and enter this code: " "{user_code}".format(**code_data), timeout=30, stdout=True, category="trakt") # automatically open the url in the default browser # but we don't want to use terminal-based browsers - most likely not # what the user wants term_bak = os.environ.pop("TERM", None) webbrowser.open(code_data['verification_url']) if term_bak is not None: os.environ["TERM"] = term_bak start = time.time() while time.time() - start < code_data['expires_in']: if self.get_device_token(code_data['device_code']): notify('App authorized successfully.', stdout=True, category="trakt") logger.info('App authorized successfully.') break logger.debug('Waiting for user to authorize the app.') time.sleep(int(code_data['interval'])) else: logger.error('Timed out during auth.')
def refresh_token(self): if self._refresh_retries == self._REFRESH_RETRIES_LIMIT: self.token_data = {} self._refresh_retries = 0 logger.critical("Too many failed refreshes. Clearing token.") notify("Trakt token expired. Couldn't auto-refresh token.", stdout=True) self.device_auth() return exchange_params = { "url": API_URL + '/oauth/token', "headers": {"Content-Type": "application/json"}, "json": { "refresh_token": self.token_data['refresh_token'], "client_id": self.CLIENT_ID, "client_secret": self.CLIENT_SECRET, "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "grant_type": "refresh_token" } } self._refresh_retries += 1 exchange_resp = safe_request('post', exchange_params) if exchange_resp and exchange_resp.status_code == 200: self.token_data = exchange_resp.json() self._refresh_retries = 0 logger.info('Refreshed access token.') else: logger.error("Error refreshing token.")
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': {}} trakt_id = trakt_cache[required_type].get(title) if trakt_id: return trakt_id logger.debug(f'Searching trakt: Title: "{title}", Year: {year}') results = search(title, [required_type], 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) 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
def get_device_token(self, device_code): token_request_params = { "url": API_URL + "/oauth/device/token", "headers": {"Content-Type": "application/json"}, "json": { "code": device_code, "client_id": self.CLIENT_ID, "client_secret": self.CLIENT_SECRET } } token_resp = safe_request('post', token_request_params) if token_resp is None: self._code_fetch_fails += 1 if self._code_fetch_fails == self._CODE_FETCH_FAILS_LIMIT: logger.critical("Unable to get response from trakt.") notify("Unable to get response from trakt.", stdout=True, category="trakt") sys.exit(1) return elif token_resp.status_code == 400: self._code_fetch_fails = 0 return False elif token_resp.status_code == 200: self.token_data = token_resp.json() self._code_fetch_fails = 0 return True else: logger.critical("Invalid status code of token response.") sys.exit(1)
def handle_successful_scrobble(self, verb, data, resp): if 'movie' in resp: name = resp['movie']['title'] else: name = (resp['show']['title'] + " S{season:02}E{number:02}".format(**resp['episode'])) category = self._determine_category(verb, data['media_info'], resp['action']) msg = f"Scrobble {category} successful for {name} at {resp['progress']:.2f}%" logger.info(msg) notify(msg, category=f"scrobble.{category}") self.backlog_cleaner.clear()
def use_guessit(file_path: str): try: return guessit.guessit(file_path) except guessit.api.GuessitException: # lazy import the notifier module # This codepath will not be executed 99.99% of the time, and importing notify # in the outer scope is expensive due to the categories parsing # It is unneeded when using the "trakts whitelist" command from trakt_scrobbler.notifier import notify logger.exception("Encountered guessit error.") notify("Encountered guessit error. File a bug report!", category="exception") return {}
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}.") notify(f"Check log file. {e!s}", category="exception") except Exception: msg = f"Config value autoload failed for {cls.name}." logger.exception(msg) notify(f"{msg} Check log file.", category="exception") else: return super().__new__(cls)
def handle_successful_scrobble(self, verb, data, resp): if 'movie' in resp: name = "{title} ({year:04})".format(**resp['movie']) else: name = ( "{title} ({year:04})".format(**resp['show']) + " [{season}x{number:02}] {title}".format(**resp['episode'])) os.environ['TRAKTTITLE'] = name category = self._determine_category(verb, data['media_info'], resp['action']) msg = f"Scrobble {category} successful {name} at {resp['progress']:.2f}%" logger.info(msg) notify(title=name, body=msg, category=f"scrobble.{category}") self.backlog_cleaner.clear()
def get_access_token(self): if not self.token_data: logger.info("Access token not found. Initiating device authentication.") self.device_auth() elif self.is_token_expired(): logger.info("Trakt access token expired. Refreshing.") notify("Trakt access token expired. Refreshing.", category="trakt") self.refresh_token() if not self.token_data or self.is_token_expired(): # either device_auth or refresh_token failed to get token logger.critical("Unable to get access token.") notify("Failed to authorize application with Trakt. " "Run 'trakts auth' manually to retry.", stdout=True, category="trakt") else: return self.token_data['access_token']
def __init__(self, scrobble_queue): try: self.URL = self.URL.format(**self.config) except KeyError: logger.exception("Check config for correct Plex params.") return self.token = token.data if not self.token: logger.error("Unable to retrieve plex token.") notify("Unable to retrieve plex token. Rerun plex auth.", category="exception") 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 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 get_access_token(): global token_data if not token_data: logger.info("Access token not found in config. " "Initiating device authentication.") token_data = device_auth() write_json(token_data, TRAKT_TOKEN_PATH) elif token_data['created_at'] + token_data['expires_in'] - \ time.time() < 86400: logger.info("Access token about to expire. Refreshing.") token_data = refresh_token(token_data) write_json(token_data, TRAKT_TOKEN_PATH) if not token_data: logger.error("Unable to get access token. " f"Try deleting {TRAKT_TOKEN_PATH!s} and retry.") notify("Failed to authorize application.", stdout=True) sys.exit(1) return token_data['access_token']
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}") notify(f"Error while getting data from {self.name}: {e}", category="exception") 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 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 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