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.')
Exemple #3
0
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}
Exemple #4
0
    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.')
Exemple #5
0
    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.")
Exemple #6
0
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
Exemple #7
0
 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)
Exemple #8
0
 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()
Exemple #9
0
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 {}
Exemple #10
0
 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)
Exemple #11
0
    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()
Exemple #12
0
 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']
Exemple #13
0
 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']
Exemple #16
0
    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