def pending_list_approve(options, approve=None): with Session() as session: try: entry_list = get_list_by_exact_name(options.list_name) except NoResultFound: console('Could not find pending list with name `{}`'.format(options.list_name)) return try: db_entry = get_entry_by_id(entry_list.id, int(options.entry), session=session) except NoResultFound: console('Could not find matching entry with ID {} in list `{}`'.format(int(options.entry), options.list_name)) return except ValueError: db_entry = get_entry_by_title(entry_list.id, options.entry, session=session) if not db_entry: console('Could not find matching entry with title `{}` in list `{}`'.format(options.entry, options.list_name)) return approve_text = 'approved' if approve else 'rejected' if (db_entry.approved is True and approve is True) or (db_entry.approved is False and approve is False): console('entry {} is already {}'.format(db_entry.title, approve_text)) return db_entry.approved = approve console('Successfully marked pending entry {} as {}'.format(db_entry.title, approve_text))
def list_entries(options): """List pending entries""" approved = options.approved task_name = options.task_name with Session() as session: entries = db.list_pending_entries(session=session, task_name=task_name, approved=approved) header = ['#', 'Task Name', 'Title', 'URL', 'Approved', 'Added'] table_data = [header] for entry in entries: table_data.append( [ entry.id, entry.task_name, entry.title, entry.url, colorize('green', 'Yes') if entry.approved else 'No', entry.added.strftime("%c"), ] ) try: table = TerminalTable( options.table_type, table_data, wrap_columns=[1, 2, 3], drop_columns=[5, 1, 3] ) console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def entry_list_add(options): with Session() as session: try: entry_list = get_list_by_exact_name(options.list_name, session=session) except NoResultFound: console('Could not find entry list with name `{}`, creating'.format(options.list_name)) entry_list = EntryListList(name=options.list_name) session.add(entry_list) session.merge(entry_list) session.commit() title = options.entry_title entry = {'title': options.entry_title, 'url': options.url} db_entry = get_entry_by_title(list_id=entry_list.id, title=title, session=session) if db_entry: console("Entry with the title `{}` already exist with list `{}`. Will replace identifiers if given".format( title, entry_list.name)) output = 'Successfully updated entry `{}` to entry list `{}` '.format(title, entry_list.name) else: console("Adding entry with title `{}` to list `{}`".format(title, entry_list.name)) db_entry = EntryListEntry(entry=entry, entry_list_id=entry_list.id) session.add(db_entry) output = 'Successfully added entry `{}` to entry list `{}` '.format(title, entry_list.name) if options.attributes: console('Adding attributes to entry `{}`'.format(title)) for identifier in options.attributes: for k, v in identifier.items(): entry[k] = v db_entry.entry = entry console(output)
def remove(manager, options, forget=False): name = options.series_name if options.episode_id: # remove by id identifier = options.episode_id try: remove_series_episode(name, identifier, forget) console('Removed episode `%s` from series `%s`.' % (identifier, name.capitalize())) except ValueError: # Try upper casing identifier if we fail at first try: remove_series_episode(name, identifier.upper(), forget) console('Removed episode `%s` from series `%s`.' % (identifier, name.capitalize())) except ValueError as e: console(e.args[0]) else: # remove whole series try: remove_series(name, forget) console('Removed series `%s` from database.' % name.capitalize()) except ValueError as e: console(e.args[0]) manager.config_changed()
def bootstrap(self, session, config): """bootstrap the plugin configuration and update db with cached chat_ids""" console('{0} - bootstrapping...'.format(_PLUGIN_NAME)) chat_ids = self._real_init(session, config) found_usernames = [x.username for x in chat_ids if x.username] found_fullnames = [(x.firstname, x.surname) for x in chat_ids if x.firstname] found_grps = [x.group for x in chat_ids if x.group] missing_usernames = [x for x in self._usernames if x not in found_usernames] missing_fullnames = [x for x in self._fullnames if x not in found_fullnames] missing_grps = [x for x in self._groups if x not in found_grps] if missing_usernames or missing_fullnames or missing_grps: for i in missing_usernames: console('ERR: could not find chat_id for username: {0}'.format(i)) for i in missing_fullnames: console('ERR: could not find chat_id for fullname: {0} {1}'.format(*i)) for i in missing_grps: console('ERR: could not find chat_id for group: {0}'.format(i)) res = False else: console('{0} - bootstrap was successful'.format(_PLUGIN_NAME)) res = True return res
def action_list(options): with Session() as session: if not options.account: # Print all accounts accounts = session.query(TraktUserAuth).all() if not accounts: console('No trakt authorizations stored in database.') return header = ['Account', 'Created', 'Expires'] table_data = [header] for auth in accounts: table_data.append([auth.account, auth.created.strftime('%Y-%m-%d'), auth.expires.strftime('%Y-%m-%d')]) try: table = TerminalTable(options.table_type, table_data) console(table.output) return except TerminalTableError as e: console('ERROR: %s' % str(e)) # Show a specific account acc = session.query(TraktUserAuth).filter(TraktUserAuth.account == options.account).first() if acc: console('Authorization expires on %s' % acc.expires) else: console('Flexget has not been authorized to access your account.')
def pending_list_show(options): with Session() as session: try: pending_list = get_list_by_exact_name(options.list_name, session=session) except NoResultFound: console('Could not find pending list with name {}'.format(options.list_name)) return try: entry = get_entry_by_id(pending_list.id, int(options.entry), session=session) except NoResultFound: console('Could not find matching pending entry with ID {} in list `{}`'.format(int(options.entry), options.list_name)) return except ValueError: entry = get_entry_by_title(pending_list.id, options.entry, session=session) if not entry: console('Could not find matching pending entry with title `{}` in list `{}`'.format(options.entry, options.list_name)) return header = ['Field name', 'Value'] table_data = [header] for k, v in sorted(entry.entry.items()): table_data.append([k, str(v)]) table = TerminalTable(options.table_type, table_data, wrap_columns=[1]) table.table.justify_columns[0] = 'center' try: console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def seen_search(options, session=None): search_term = options.search_term if is_imdb_url(search_term): console('IMDB url detected, parsing ID') imdb_id = extract_id(search_term) if imdb_id: search_term = imdb_id else: console("Could not parse IMDB ID") else: search_term = '%' + options.search_term + '%' seen_entries = db.search(value=search_term, status=None, session=session) table_data = [] for se in seen_entries.all(): table_data.append(['Title', se.title]) for sf in se.fields: if sf.field.lower() == 'title': continue table_data.append(['{}'.format(sf.field.upper()), str(sf.value)]) table_data.append(['Task', se.task]) table_data.append(['Added', se.added.strftime('%Y-%m-%d %H:%M')]) if options.table_type != 'porcelain': table_data.append(['', '']) if not table_data: console('No results found for search') return if options.table_type != 'porcelain': del table_data[-1] try: table = TerminalTable(options.table_type, table_data, wrap_columns=[1]) table.table.inner_heading_row_border = False console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def reset_plugin(options): plugin = options.plugin_name[0] try: reset_schema(plugin) console("The database for `%s` has been reset." % plugin) except ValueError as e: console("Unable to reset %s: %s" % (plugin, e.message))
def clear_rejected(manager): with Session() as session: results = session.query(RememberEntry).delete() console('Cleared %i items.' % results) session.commit() if results: manager.config_changed()
def do_cli(manager, options): with Session() as session: query = session.query(History) if options.search: search_term = options.search.replace(' ', '%').replace('.', '%') query = query.filter(History.title.like('%' + search_term + '%')) if options.task: query = query.filter(History.task.like('%' + options.task + '%')) query = query.order_by(desc(History.time)).limit(options.limit) table_data = [] if options.short: table_data.append(['Time', 'Title']) for item in reversed(query.all()): if not options.short: table_data.append(['Task', item.task]) table_data.append(['Title', item.title]) table_data.append(['URL', item.url]) table_data.append(['Time', item.time.strftime("%c")]) table_data.append(['Details', item.details]) if item.filename: table_data.append(['Stored', item.filename]) if options.table_type != 'porcelain': table_data.append(['']) else: table_data.append([item.time.strftime("%c"), item.title]) title = 'Showing {} entries from History'.format(query.count()) if options.table_type != 'porcelain': del table_data[-1] table = TerminalTable(options.table_type, table_data, title=title, wrap_columns=[1]) if not options.short: table.table.inner_heading_row_border = False try: console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def do_cli(manager, options): if not options.url: # Determine if first positional argument is a URL or a title if '://' in options.title: options.url = options.title options.title = None if options.url and not options.title: # Attempt to get a title from the URL response's headers try: value, params = cgi.parse_header(requests.head(options.url).headers['Content-Disposition']) options.title = params['filename'] except KeyError: console('No title given, and couldn\'t get one from the URL\'s HTTP response. Aborting.') return entry = Entry(title=options.title) if options.url: entry['url'] = options.url else: entry['url'] = 'http://localhost/inject/%s' % ''.join(random.sample(string.ascii_letters + string.digits, 30)) if options.force: entry['immortal'] = True if options.accept: entry.accept(reason='accepted by CLI inject') if options.fields: for key, value in options.fields: entry[key] = value options.inject = [entry] manager.execute_command(options)
def get_access_token(account, token=None, refresh=False, re_auth=False, called_from_cli=False): """ Gets authorization info from a pin or refresh token. :param account: Arbitrary account name to attach authorization to. :param unicode token: The pin or refresh token, as supplied by the trakt website. :param bool refresh: If True, refresh the access token using refresh_token from db. :param bool re_auth: If True, account is re-authorized even if it already exists in db. :raises RequestException: If there is a network error while authorizing. """ data = { 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET, 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', } with Session() as session: acc = session.query(TraktUserAuth).filter(TraktUserAuth.account == account).first() if acc and datetime.now() < acc.expires and not refresh and not re_auth: return acc.access_token else: if ( acc and (refresh or datetime.now() >= acc.expires - timedelta(days=5)) and not re_auth ): log.debug('Using refresh token to re-authorize account %s.', account) data['refresh_token'] = acc.refresh_token data['grant_type'] = 'refresh_token' token_dict = token_oauth(data) elif token: # We are only in here if a pin was specified, so it's safe to use console instead of logging console( 'Warning: PIN authorization has been deprecated. Use Device Authorization instead.' ) data['code'] = token data['grant_type'] = 'authorization_code' token_dict = token_oauth(data) elif called_from_cli: log.debug( 'No pin specified for an unknown account %s. Attempting to authorize device.', account, ) token_dict = device_auth() else: raise plugin.PluginError( 'Account %s has not been authorized. See `flexget trakt auth -h` on how to.' % account ) try: new_acc = TraktUserAuth( account, token_dict['access_token'], token_dict['refresh_token'], token_dict.get('created_at', time.time()), token_dict['expires_in'], ) session.merge(new_acc) return new_acc.access_token except requests.RequestException as e: raise plugin.PluginError('Token exchange with trakt failed: {0}'.format(e))
def action_purge(options): with Session() as session: regexp_list = db.get_list_by_exact_name(options.list_name) if not regexp_list: console('Could not find regexp list with name {}'.format(options.list_name)) return console('Deleting list %s' % options.list_name) session.delete(regexp_list)
def clear_failed(manager): # TODO: this should be a function in db.py with Session() as session: results = session.query(db.FailedEntry).delete() console('Cleared %i items.' % results) session.commit() if results: manager.config_changed()
def clear_entries(options): """Clear pending entries""" with Session() as session: query = session.query(PendingEntry).filter(PendingEntry.approved == False) if options.task_name: query = query.filter(PendingEntry.task_name == options.task_name) deleted = query.delete() console('Successfully deleted %i pending entries' % deleted)
def seen_add(options): seen_name = options.add_value if is_imdb_url(seen_name): imdb_id = extract_id(seen_name) if imdb_id: seen_name = imdb_id seen.add(seen_name, "cli_add", {"cli_add": seen_name}) console("Added %s as seen. This will affect all tasks." % seen_name)
def vacuum(): console("Running VACUUM on sqlite database, this could take a while.") session = Session() try: session.execute("VACUUM") session.commit() finally: session.close() console("VACUUM complete.")
def pending_list_purge(options): with Session() as session: try: entry_list = get_list_by_exact_name(options.list_name) except NoResultFound: console('Could not find entry list with name `{}`'.format(options.list_name)) return console('Deleting list {}'.format(options.list_name)) session.delete(entry_list)
def movie_list_purge(options): with Session() as session: try: movie_list = db.get_list_by_exact_name(options.list_name) except NoResultFound: console('Could not find movie list with name {}'.format(options.list_name)) return console('Deleting list {}'.format(options.list_name)) session.delete(movie_list)
def test_msg(self, session, config): """send test message to configured recipients""" console('{0} loading chat_ids...'.format(_PLUGIN_NAME)) chat_ids = self._real_init(session, config) console('{0} sending test message(s)...'.format(_PLUGIN_NAME)) for chat_id in (x.id for x in chat_ids): self._bot.sendMessage(chat_id=chat_id, text='test message from flexget') return True
def clear_failed(manager): session = Session() try: results = session.query(FailedEntry).delete() console('Cleared %i items.' % results) session.commit() if results: manager.config_changed() finally: session.close()
def seen_forget(manager, options): forget_name = options.forget_value if is_imdb_url(forget_name): imdb_id = extract_id(forget_name) if imdb_id: forget_name = imdb_id count, fcount = seen.forget(forget_name) console("Removed %s titles (%s fields)" % (count, fcount)) manager.config_changed()
def cli_perf_test(manager, options): if options.test_name not in TESTS: console("Unknown performance test %s" % options.test_name) return session = Session() try: if options.test_name == "imdb_query": imdb_query(session) finally: session.close()
def print_doc(manager, options): plugin_name = options.doc plugin = plugins.get(plugin_name, None) if plugin: if not plugin.instance.__doc__: console('Plugin %s does not have documentation' % plugin_name) else: console('') console(trim(plugin.instance.__doc__)) console('') else: console('Could not find plugin %s' % plugin_name)
def movie_list_lists(options): """ Show all movie lists """ lists = get_movie_lists() header = ["#", "List Name"] table_data = [header] for movie_list in lists: table_data.append([movie_list.id, movie_list.name]) table = TerminalTable(options.table_type, table_data) try: console(table.output) except TerminalTableError as e: console("ERROR: %s" % str(e))
def action_all(options): """ Show all regexp lists """ lists = db.get_regexp_lists() header = ['#', 'List Name'] table_data = [header] for regexp_list in lists: table_data.append([regexp_list.id, regexp_list.name]) table = TerminalTable(options.table_type, table_data) try: console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def pending_list_lists(options): """ Show all pending lists """ with Session() as session: lists = get_pending_lists(session=session) header = ['#', 'List Name'] table_data = [header] for entry_list in lists: table_data.append([entry_list.id, entry_list.name]) table = TerminalTable(options.table_type, table_data) try: console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def list_rejected(options): with Session() as session: results = session.query(RememberEntry).all() header = ['#', 'Title', 'Task', 'Rejected by', 'Reason'] table_data = [header] for entry in results: table_data.append([entry.id, entry.title, entry.task.name, entry.rejected_by, entry.reason or '']) try: table = TerminalTable(options.table_type, table_data) table.table.justify_columns[0] = 'center' console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def movie_list_lists(options): """ Show all movie lists """ lists = db.get_movie_lists() header = ['#', 'List Name'] table_data = [header] for movie_list in lists: table_data.append([movie_list.id, movie_list.name]) try: table = TerminalTable(options.table_type, table_data) except TerminalTableError as e: console('ERROR: {}'.format(e)) else: console(table.output)
def plugins_summary(manager, options): if options.table_type == 'porcelain': disable_all_colors() header = ['Keyword', 'Interfaces', 'Phases', 'Flags'] table_data = [header] for plugin in sorted( get_plugins(phase=options.phase, interface=options.interface)): if options.builtins and not plugin.builtin: continue flags = [] if plugin.instance.__doc__: flags.append('doc') if plugin.builtin: flags.append('builtin') if plugin.debug: if not options.debug: continue flags.append('developers') handlers = plugin.phase_handlers roles = [] for phase in handlers: priority = handlers[phase].priority roles.append('{0}({1})'.format(phase, priority)) name = colorize('green', plugin.name) if 'builtin' in flags else plugin.name table_data.append([ name, ', '.join(plugin.interfaces), ', '.join(roles), ', '.join(flags) ]) try: table = TerminalTable(options.table_type, table_data, wrap_columns=[1, 2]) console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e)) return console(colorize('green', ' Built-in plugins'))
def print_categories(parent_category_name=None): """ Print category and its sub-categories :param parent_category_name: if None, all categories will be displayed :return: """ proxy = T411Proxy() proxy.set_credential() with Session() as session: if parent_category_name is None: categories = proxy.main_categories(session=session) else: categories = proxy.find_categories(parent_category_name, session=session) formatting_main = '%-30s %-5s %-5s' formatting_sub = ' %-25s %-5s %-5s' console(formatting_main % ('Category name', 'PID', 'ID')) for category in categories: console(formatting_main % (category.name, category.parent_id, category.id)) for sub_category in category.sub_categories: console(formatting_sub % (sub_category.name, sub_category.parent_id, sub_category.id))
def do_cli(manager, options): with Session() as session: query = session.query(History) if options.search: search_term = options.search.replace(' ', '%').replace('.', '%') query = query.filter(History.title.like('%' + search_term + '%')) if options.task: query = query.filter(History.task.like('%' + options.task + '%')) query = query.order_by(desc(History.time)).limit(options.limit) table_data = [] if options.short: table_data.append(['Time', 'Title']) for item in reversed(query.all()): if not options.short: table_data.append(['Task', item.task]) table_data.append(['Title', item.title]) table_data.append(['URL', item.url]) table_data.append(['Time', item.time.strftime("%c")]) table_data.append(['Details', item.details]) if item.filename: table_data.append(['Stored', item.filename]) if options.table_type != 'porcelain': table_data.append(['']) else: table_data.append([item.time.strftime("%c"), item.title]) if not table_data: console('No history to display') return title = 'Showing {} entries from History'.format(query.count()) if options.table_type != 'porcelain' and not options.short: del table_data[-1] table = TerminalTable(options.table_type, table_data, title=title, wrap_columns=[1]) if not options.short: table.table.inner_heading_row_border = False try: console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def do_cli(manager, options): if options.action == 'clear': num = db.clear_entries(options.task, all=True) console('%s entries cleared from backlog.' % num) else: header = ['Title', 'Task', 'Expires'] table_data = [header] with Session() as session: entries = db.get_entries(options.task, session=session) for entry in entries: table_data.append([ entry.title, entry.task, entry.expire.strftime('%Y-%m-%d %H:%M') ]) try: table = TerminalTable(options.table_type, table_data, wrap_columns=[0]) console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def movie_list_del(options): with Session() as session: try: movie_list = get_list_by_exact_name(options.list_name) except NoResultFound: console('Could not find movie list with name {}'.format( options.list_name)) return title, year = split_title_year(options.movie_title) movie_exist = get_movie_by_title_and_year(list_id=movie_list.id, title=title, year=year, session=session) if movie_exist: console('Removing movie %s from list %s' % (options.movie_title, options.list_name)) session.delete(movie_exist) else: console('Could not find movie with title %s in list %s' % (options.movie_title, options.list_name)) return
def do_cli_task(manager, options): header = [ 'Start', 'Duration', 'Produced', 'Accepted', 'Rejected', 'Failed', 'Abort Reason' ] table_data = [header] with Session() as session: try: task = session.query(StatusTask).filter( StatusTask.name == options.task).one() except NoResultFound: console( 'Task name `%s` does not exists or does not have any records' % options.task) return else: query = task.executions.order_by(desc( TaskExecution.start))[:options.limit] for ex in reversed(query): start = ex.start.strftime('%Y-%m-%d %H:%M') start = colorize('green', start) if ex.succeeded else colorize( 'red', start) if ex.end is not None and ex.start is not None: delta = ex.end - ex.start duration = '%1.fs' % delta.total_seconds() else: duration = '?' table_data.append([ start, duration, ex.produced, ex.accepted, ex.rejected, ex.failed, ex.abort_reason if ex.abort_reason is not None else '' ]) try: table = TerminalTable(options.table_type, table_data) console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def entry_list_list(options): """List entry list""" with Session() as session: try: entry_list = db.get_list_by_exact_name(options.list_name, session=session) except NoResultFound: console('Could not find entry list with name {}'.format( options.list_name)) return header = ['#', 'Title', '# of fields'] table_data = [header] for entry in db.get_entries_by_list_id(entry_list.id, order_by='added', descending=True, session=session): table_data.append([entry.id, entry.title, len(entry.entry)]) try: table = TerminalTable(options.table_type, table_data) console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def action_list(options): """List regexp list""" with Session() as session: regexp_list = get_list_by_exact_name(options.list_name) if not regexp_list: console('Could not find regexp list with name {}'.format( options.list_name)) return header = ['Regexp'] table_data = [header] regexps = get_regexps_by_list_id(regexp_list.id, order_by='added', descending=True, session=session) for regexp in regexps: regexp_row = [regexp.regexp or ''] table_data.append(regexp_row) table = TerminalTable(options.table_type, table_data) try: console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def begin(manager, options): series_name = options.series_name series_name = series_name.replace(r'\!', '!') ep_id = options.episode_id normalized_name = normalize_series_name(series_name) with Session() as session: series = shows_by_exact_name(normalized_name, session) if not series: console('Series not yet in database, adding `%s`' % series_name) series = Series() series.name = series_name session.add(series) else: series = series[0] try: set_series_begin(series, ep_id) except ValueError as e: console(e) else: console('Episodes for `%s` will be accepted starting with `%s`' % (series.name, ep_id)) session.commit() manager.config_changed()
def movie_list_list(options): """List movie list""" with Session() as session: try: movie_list = get_list_by_exact_name(options.list_name) except NoResultFound: console('Could not find movie list with name {}'.format(options.list_name)) return header = ['Movie Name', 'Movie year'] header += MovieListBase().supported_ids table_data = [header] movies = get_movies_by_list_id(movie_list.id, order_by='added', descending=True, session=session) for movie in movies: movie_row = [movie.title, movie.year or ''] for identifier in MovieListBase().supported_ids: movie_row.append(movie.identifiers.get(identifier, '')) table_data.append(movie_row) title = '{} Movies in movie list: `{}`'.format(len(movies), options.list_name) table = TerminalTable(options.table_type, table_data, title, drop_columns=[5, 2, 4]) try: console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def cli_search(options): search_term = ' '.join(options.keywords) tags = options.tags sources = options.sources query = re.sub(r'[ \(\)]+', ' ', search_term).strip() table_data = [] with Session() as session: for archived_entry in plugin_archive.search(session, query, tags=tags, sources=sources): days_ago = (datetime.now() - archived_entry.added).days source_names = ', '.join([s.name for s in archived_entry.sources]) tag_names = ', '.join([t.name for t in archived_entry.tags]) table_data.append(['ID', str(archived_entry.id)]) table_data.append(['Title', archived_entry.title]) table_data.append(['Added', str(days_ago) + ' days ago']) table_data.append(['URL', archived_entry.url]) table_data.append(['Source(s)', source_names or 'N/A']) table_data.append(['Tag(s)', tag_names or 'N/A']) if archived_entry.description: table_data.append( ['Description', strip_html(archived_entry.description)]) table_data.append([]) if not table_data: console('No results found for search') return try: table = TerminalTable(options.table_type, table_data, wrap_columns=[1]) table.table.inner_heading_row_border = False console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def display_summary(options): """ Display series summary. :param options: argparse options from the CLI """ porcelain = options.table_type == 'porcelain' configured = options.configured or os.environ.get(ENV_LIST_CONFIGURED, 'configured') premieres = (True if (os.environ.get(ENV_LIST_PREMIERES) == 'yes' or options.premieres) else False) sort_by = options.sort_by or os.environ.get(ENV_LIST_SORTBY_FIELD, 'name') if options.order is not None: descending = True if options.order == 'desc' else False else: descending = True if os.environ.get( ENV_LIST_SORTBY_ORDER) == 'desc' else False with Session() as session: kwargs = { 'configured': configured, 'premieres': premieres, 'session': session, 'sort_by': sort_by, 'descending': descending, } if sort_by == 'name': kwargs['sort_by'] = 'show_name' else: kwargs['sort_by'] = 'last_download_date' query = db.get_series_summary(**kwargs) header = [ 'Name', 'Begin', 'Last Encountered', 'Age', 'Downloaded', 'Identified By' ] for index, value in enumerate(header): if value.lower() == options.sort_by: header[index] = colorize(SORT_COLUMN_COLOR, value) table = TerminalTable(*header, table_type=options.table_type) for series in query: name_column = series.name behind = (0, ) begin = series.begin.identifier if series.begin else '-' latest_release = '-' age_col = '-' episode_id = '-' latest = db.get_latest_release(series) identifier_type = series.identified_by if identifier_type == 'auto': identifier_type = colorize('yellow', 'auto') if latest: behind = db.new_entities_after(latest) latest_release = get_latest_status(latest) # colorize age age_col = latest.age if latest.age_timedelta is not None: if latest.age_timedelta < timedelta(days=1): age_col = colorize(NEW_EP_COLOR, latest.age) elif latest.age_timedelta < timedelta(days=3): age_col = colorize(FRESH_EP_COLOR, latest.age) elif latest.age_timedelta > timedelta(days=400): age_col = colorize(OLD_EP_COLOR, latest.age) episode_id = latest.identifier if not porcelain: if behind[0] > 0: name_column += colorize( BEHIND_EP_COLOR, ' {} {} behind'.format(behind[0], behind[1])) table.add_row(name_column, begin, episode_id, age_col, latest_release, identifier_type) console(table) if not porcelain: if not query.count(): console('Use `flexget series list all` to view all known series.') else: console( 'Use `flexget series show NAME` to get detailed information.')
def display_details(options): """Display detailed series information, ie. series show NAME""" name = options.series_name sort_by = options.sort_by or os.environ.get(ENV_SHOW_SORTBY_FIELD, 'age') if options.order is not None: reverse = True if options.order == 'desc' else False else: reverse = True if os.environ.get( ENV_SHOW_SORTBY_ORDER) == 'desc' else False with Session() as session: name = flexget.components.series.utils.normalize_series_name(name) # Sort by length of name, so that partial matches always show shortest matching title matches = db.shows_by_name(name, session=session) if not matches: console(colorize(ERROR_COLOR, 'ERROR: Unknown series `%s`' % name)) return # Pick the best matching series series = matches[0] table_title = series.name if len(matches) > 1: warning = ( colorize('red', ' WARNING: ') + 'Multiple series match to `{}`.\n ' 'Be more specific to see the results of other matches:\n\n' ' {}'.format(name, ', '.join(s.name for s in matches[1:]))) if not options.table_type == 'porcelain': console(warning) header = [ 'Identifier', 'Last seen', 'Release titles', 'Quality', 'Proper' ] table_data = [] entities = db.get_all_entities(series, session=session, sort_by=sort_by, reverse=reverse) for entity in entities: if not entity.releases: continue if entity.identifier is None: identifier = colorize(ERROR_COLOR, 'MISSING') age = '' else: identifier = entity.identifier age = entity.age entity_data = [identifier, age] release_titles = [] release_qualities = [] release_propers = [] for release in entity.releases: title = release.title quality = release.quality.name if not release.downloaded: title = colorize(UNDOWNLOADED_RELEASE_COLOR, title) quality = quality else: title += ' *' title = colorize(DOWNLOADED_RELEASE_COLOR, title) quality = quality release_titles.append(title) release_qualities.append(quality) release_propers.append( 'Yes' if release.proper_count > 0 else '') entity_data.append('\n'.join(release_titles)) entity_data.append('\n'.join(release_qualities)) entity_data.append('\n'.join(release_propers)) table_data.append(entity_data) footer = ' %s \n' % (colorize(DOWNLOADED_RELEASE_COLOR, '* Downloaded')) if not series.identified_by: footer += ( '\n Series plugin is still learning which episode numbering mode is \n' ' correct for this series (identified_by: auto).\n' ' Few duplicate downloads can happen with different numbering schemes\n' ' during this time.') else: footer += '\n `%s` uses `%s` mode to identify episode numbering.' % ( series.name, series.identified_by, ) begin_text = 'option' if series.begin: footer += ' \n Begin for `%s` is set to `%s`.' % ( series.name, series.begin.identifier) begin_text = 'and `begin` options' footer += ' \n See `identified_by` %s for more information.' % begin_text table = TerminalTable(*header, table_type=options.table_type, title=table_title) for row in table_data: table.add_row(*row) console(table) if not options.table_type == 'porcelain': console(footer)
def begin(manager, options): series_name = options.series_name series_name = series_name.replace(r'\!', '!') normalized_name = flexget.components.series.utils.normalize_series_name( series_name) with Session() as session: series = db.shows_by_exact_name(normalized_name, session) if options.forget: if not series: console('Series `%s` was not found in the database.' % series_name) else: series = series[0] series.begin = None console('The begin episode for `%s` has been forgotten.' % series.name) session.commit() manager.config_changed() elif options.episode_id: ep_id = options.episode_id if not series: console('Series not yet in database. Adding `%s`.' % series_name) series = db.Series() series.name = series_name session.add(series) else: series = series[0] try: _, entity_type = db.set_series_begin(series, ep_id) except ValueError as e: console(e) else: if entity_type == 'season': console('`%s` was identified as a season.' % ep_id) ep_id += 'E01' console( 'Releases for `%s` will be accepted starting with `%s`.' % (series.name, ep_id)) session.commit() manager.config_changed()
def dump(entries, debug=False, eval_lazy=False, trace=False, title_only=False): """ Dump *entries* to stdout :param list entries: Entries to be dumped. :param bool debug: Print non printable fields as well. :param bool eval_lazy: Evaluate lazy fields. :param bool trace: Display trace information. :param bool title_only: Display only title field """ def sort_key(field): # Sort certain fields above the rest if field == 'title': return (0, ) if field == 'url': return (1, ) if field == 'original_url': return (2, ) return 3, field highlighter = ReprHighlighter() for entry in entries: entry_table = TerminalTable( 'field', ':', 'value', show_header=False, show_edge=False, pad_edge=False, collapse_padding=True, box=None, padding=0, ) for field in sorted(entry, key=sort_key): if field.startswith('_') and not debug: continue if title_only and field != 'title': continue if entry.is_lazy(field) and not eval_lazy: renderable = ( '[italic]<LazyField - value will be determined when it is accessed>[/italic]' ) else: try: value = entry[field] except KeyError: renderable = '[italic]<LazyField - lazy lookup failed>[/italic]' else: if field.rsplit('_', maxsplit=1)[-1] == 'url': renderable = f'[link={value}][repr.url]{value}[/repr.url][/link]' elif isinstance(value, str): renderable = value.replace('\r', '').replace('\n', '') elif is_expandable(value): renderable = Pretty(value) else: try: renderable = highlighter(str(value)) except Exception: renderable = f'[[i]not printable[/i]] ({repr(value)})' entry_table.add_row(f'{field}', ': ', renderable) console(entry_table) if trace: console('── Processing trace:', style='italic') trace_table = TerminalTable( 'Plugin', 'Operation', 'Message', show_edge=False, pad_edge=False, ) for item in entry.traces: trace_table.add_row(item[0], '' if item[1] is None else item[1], item[2]) console(trace_table) if not title_only: console('')
def movie_list_add(options): with Session() as session: try: movie_list = db.get_list_by_exact_name(options.list_name, session=session) except NoResultFound: console('Could not find movie list with name {}, creating'.format( options.list_name)) movie_list = db.MovieListList(name=options.list_name) session.add(movie_list) session.commit() title, year = split_title_year(options.movie_title) console('Trying to lookup movie title: `{}`'.format(title)) movie_lookup = lookup_movie(title=title, session=session, identifiers=options.identifiers) if not movie_lookup: console('ERROR: movie lookup failed for movie {}, aborting'.format( options.movie_title)) return title = movie_lookup['movie_name'] movie = db.get_movie_by_title_and_year(list_id=movie_list.id, title=title, year=year, session=session) if not movie: console("Adding movie with title {} to list {}".format( title, movie_list.name)) movie = db.MovieListMovie(title=title, year=year, list_id=movie_list.id) else: console("Movie with title {} already exist in list {}".format( title, movie_list.name)) id_list = [] if options.identifiers: id_list = options.identifiers else: for _id in db.MovieListBase().supported_ids: if movie_lookup.get(_id): id_list.append({_id: movie_lookup.get(_id)}) if id_list: console('Setting movie identifiers:') for ident in id_list: for key in ident: console('{}: {}'.format(key, ident[key])) movie.ids = db.get_db_movie_identifiers(identifier_list=id_list, session=session) session.merge(movie) console('Successfully added movie {} to movie list {} '.format( title, movie_list.name))
def pending_list_add(options): with Session() as session: try: pending_list = plugin_pending_list.get_list_by_exact_name(options.list_name, session=session) except NoResultFound: console('Could not find a pending list with name `{}`, creating'.format(options.list_name)) pending_list = plugin_pending_list.PendingListList(name=options.list_name) session.add(pending_list) session.merge(pending_list) session.commit() title = options.entry_title entry = {'title': options.entry_title, 'url': options.url} db_entry = plugin_pending_list.get_entry_by_title(list_id=pending_list.id, title=title, session=session) if db_entry: console("Entry with the title `{}` already exist with list `{}`. Will replace identifiers if given".format( title, pending_list.name)) operation = 'updated' else: console("Adding entry with title `{}` to list `{}`".format(title, pending_list.name)) db_entry = plugin_pending_list.PendingListEntry(entry=entry, pending_list_id=pending_list.id) if options.approved: console('marking entry as approved') db_entry.approved = True session.add(db_entry) operation = 'added' if options.attributes: console('Adding attributes to entry `{}`'.format(title)) for identifier in options.attributes: for k, v in identifier.items(): entry[k] = v db_entry.entry = entry console('Successfully {} entry `{}` to pending list `{}` '.format(operation, title, pending_list.name))
def display_details(options): """Display detailed series information, ie. series show NAME""" name = options.series_name with Session() as session: name = normalize_series_name(name) # Sort by length of name, so that partial matches always show shortest matching title matches = shows_by_name(name, session=session) if not matches: console(colorize(ERROR_COLOR, 'ERROR: Unknown series `%s`' % name)) return # Pick the best matching series series = matches[0] table_title = colorize('white', series.name) if len(matches) > 1: warning = ( colorize('red', ' WARNING: ') + 'Multiple series match to `{}`.\n ' 'Be more specific to see the results of other matches:\n\n' ' {}'.format(name, ', '.join(s.name for s in matches[1:]))) if not options.table_type == 'porcelain': console(warning) header = [ 'Entity ID', 'Latest age', 'Release titles', 'Release Quality', 'Proper' ] table_data = [header] entities = get_all_entities(series, session=session) for entity in entities: if entity.identifier is None: identifier = colorize(ERROR_COLOR, 'MISSING') age = '' else: identifier = entity.identifier age = entity.age entity_data = [identifier, age] release_titles = [] release_qualities = [] release_propers = [] for release in entity.releases: title = release.title quality = release.quality.name if not release.downloaded: title = colorize(UNDOWNLOADED_RELEASE_COLOR, title) quality = quality else: title += ' *' title = colorize(DOWNLOADED_RELEASE_COLOR, title) quality = quality release_titles.append(title) release_qualities.append(quality) release_propers.append( 'Yes' if release.proper_count > 0 else '') entity_data.append('\n'.join(release_titles)) entity_data.append('\n'.join(release_qualities)) entity_data.append('\n'.join(release_propers)) table_data.append(entity_data) footer = ' %s \n' % (colorize(DOWNLOADED_RELEASE_COLOR, '* Downloaded')) if not series.identified_by: footer += ( '\n Series plugin is still learning which episode numbering mode is \n' ' correct for this series (identified_by: auto).\n' ' Few duplicate downloads can happen with different numbering schemes\n' ' during this time.') else: footer += '\n Series uses `%s` mode to identify episode numbering (identified_by).' % series.identified_by footer += ' \n See option `identified_by` for more information.\n' if series.begin: footer += ' Begin episode for this series set to `%s`.' % series.begin.identifier try: table = TerminalTable(options.table_type, table_data, table_title, drop_columns=[4, 3, 1]) console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e)) return if not options.table_type == 'porcelain': console(footer)
def do_cli_summary(manager, options): header = [ 'Task', 'Last execution', 'Last success', 'Produced', 'Accepted', 'Rejected', 'Failed', 'Duration', ] table_data = [header] with Session() as session: for task in session.query(db.StatusTask).all(): ok = ( session.query(db.TaskExecution) .filter(db.TaskExecution.task_id == task.id) .filter(db.TaskExecution.succeeded == True) .filter(db.TaskExecution.produced > 0) .order_by(db.TaskExecution.start.desc()) .first() ) if ok is None: duration = None last_success = '-' else: duration = ok.end - ok.start last_success = ok.start.strftime('%Y-%m-%d %H:%M') age = datetime.datetime.utcnow() - ok.start if age > timedelta(days=7): last_success = colorize('red', last_success) elif age < timedelta(minutes=10): last_success = colorize('green', last_success) # Fix weird issue that a task registers StatusTask but without an execution. GH #2022 last_exec = ( task.last_execution_time.strftime('%Y-%m-%d %H:%M') if task.last_execution_time else '-' ) table_data.append( [ task.name, last_exec, last_success, ok.produced if ok is not None else '-', ok.accepted if ok is not None else '-', ok.rejected if ok is not None else '-', ok.failed if ok is not None else '-', '%1.fs' % duration.total_seconds() if duration is not None else '-', ] ) table = TerminalTable(options.table_type, table_data) try: console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e))
def exposed_console(self, text, *args, **kwargs): text = rpyc.classic.obtain(text) terminal.console(text, *args, **kwargs)
def reset(manager): Base.metadata.drop_all(bind=manager.engine) Base.metadata.create_all(bind=manager.engine) console('The FlexGet database has been reset.')
def cleanup(manager): manager.db_cleanup(force=True) console('Database cleanup complete.')
def get_access_token(account, token=None, refresh=False, re_auth=False, called_from_cli=False): """ Gets authorization info from a pin or refresh token. :param account: Arbitrary account name to attach authorization to. :param unicode token: The pin or refresh token, as supplied by the trakt website. :param bool refresh: If True, refresh the access token using refresh_token from db. :param bool re_auth: If True, account is re-authorized even if it already exists in db. :raises RequestException: If there is a network error while authorizing. """ data = { 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET, 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', } with Session() as session: acc = session.query(TraktUserAuth).filter( TraktUserAuth.account == account).first() if acc and datetime.now( ) < acc.expires and not refresh and not re_auth: return acc.access_token else: if (acc and (refresh or datetime.now() >= acc.expires - timedelta(days=5)) and not re_auth): logger.debug('Using refresh token to re-authorize account {}.', account) data['refresh_token'] = acc.refresh_token data['grant_type'] = 'refresh_token' token_dict = token_oauth(data) elif token: # We are only in here if a pin was specified, so it's safe to use console instead of logging console( 'Warning: PIN authorization has been deprecated. Use Device Authorization instead.' ) data['code'] = token data['grant_type'] = 'authorization_code' token_dict = token_oauth(data) elif called_from_cli: logger.debug( 'No pin specified for an unknown account {}. Attempting to authorize device.', account, ) token_dict = device_auth() else: raise plugin.PluginError( 'Account %s has not been authorized. See `flexget trakt auth -h` on how to.' % account) try: new_acc = TraktUserAuth( account, token_dict['access_token'], token_dict['refresh_token'], token_dict.get('created_at', time.time()), token_dict['expires_in'], ) session.merge(new_acc) return new_acc.access_token except requests.RequestException as e: raise plugin.PluginError( 'Token exchange with trakt failed: {0}'.format(e))
def daemon_command(self, options: argparse.Namespace) -> None: """ Handles the 'daemon' CLI command. Fires events: * manager.daemon.started * manager.daemon.completed :param options: argparse options """ # Import API so it can register to daemon.started event if options.action == 'start': if self.is_daemon: logger.error('Daemon already running for this config.') return elif self.task_queue.is_alive(): logger.error( 'Non-daemon execution of FlexGet is running. Cannot start daemon until it is finished.' ) return if options.daemonize: self.daemonize() if options.autoreload_config: self.autoreload_config = True try: signal.signal(signal.SIGTERM, self._handle_sigterm) except ValueError as e: # If flexget is being called from another script, e.g. windows service helper, and we are not the # main thread, this error will occur. logger.debug('Error registering sigterm handler: {}', e) self.is_daemon = True def run_daemon(tray_icon: 'TrayIcon' = None): fire_event('manager.daemon.started', self) self.task_queue.start() self.ipc_server.start() self.task_queue.wait() fire_event('manager.daemon.completed', self) if tray_icon: tray_icon.stop() if options.tray_icon: from flexget.tray_icon import tray_icon # noqa self._add_tray_icon_items(tray_icon) # Tray icon must be run in the main thread. m = threading.Thread(target=run_daemon, args=(tray_icon, )) m.start() tray_icon.run() m.join() else: run_daemon() elif options.action in ['stop', 'reload-config', 'status']: if not self.is_daemon: console('There does not appear to be a daemon running.') return if options.action == 'status': logger.debug( '`daemon status` called. Daemon running. (PID: {})', os.getpid()) console(f'Daemon running. (PID: {os.getpid()})') elif options.action == 'stop': tasks = ('all queued tasks (if any) have' if options.wait else 'currently running task (if any) has') logger.info( 'Daemon shutdown requested. Shutdown will commence when {} finished executing.', tasks, ) self.shutdown(options.wait) elif options.action == 'reload-config': logger.info('Reloading config from disk.') try: self.load_config() except ValueError as e: logger.error('Error loading config: {}', e.args[0]) else: logger.info('Config successfully reloaded from disk.')
def display_summary(options): """ Display series summary. :param options: argparse options from the CLI """ porcelain = options.table_type == 'porcelain' with Session() as session: kwargs = { 'configured': options.configured, 'premieres': options.premieres, 'session': session, 'sort_by': options.sort_by, 'descending': options.order } if options.new: kwargs['status'] = 'new' kwargs['days'] = options.new elif options.stale: kwargs['status'] = 'stale' kwargs['days'] = options.stale if options.sort_by == 'name': kwargs['sort_by'] = 'show_name' else: kwargs['sort_by'] = 'last_download_date' query = get_series_summary(**kwargs) header = ['Name', 'Latest', 'Age', 'Downloaded', 'Identified By'] for index, value in enumerate(header): if value.lower() == options.sort_by: header[index] = colorize(SORT_COLUMN_COLOR, value) table_data = [header] for series in query: name_column = series.name behind = (0, ) latest_release = '-' age_col = '-' episode_id = '-' latest = get_latest_release(series) identifier_type = series.identified_by if identifier_type == 'auto': identifier_type = colorize('yellow', 'auto') if latest: behind = new_entities_after(latest) latest_release = get_latest_status(latest) # colorize age age_col = latest.age if latest.age_timedelta is not None: if latest.age_timedelta < timedelta(days=1): age_col = colorize(NEW_EP_COLOR, latest.age) elif latest.age_timedelta < timedelta(days=3): age_col = colorize(FRESH_EP_COLOR, latest.age) elif latest.age_timedelta > timedelta(days=400): age_col = colorize(OLD_EP_COLOR, latest.age) episode_id = latest.identifier if not porcelain: if behind[0] > 0: name_column += colorize( BEHIND_EP_COLOR, ' {} {} behind'.format(behind[0], behind[1])) table_data.append([ name_column, episode_id, age_col, latest_release, identifier_type ]) try: table = TerminalTable(options.table_type, table_data, wrap_columns=[3], drop_columns=[4, 3, 2]) console(table.output) except TerminalTableError as e: console('ERROR: %s' % str(e)) return if not porcelain: if not query.count(): console('Use `flexget series list all` to view all known series.') else: console( 'Use `flexget series show NAME` to get detailed information.')
def on_task_filter(self, task, config): if not task.options.try_regexp: return if self.abort: return console('-' * 79) console('Hi there, welcome to try regexps in realtime!') console( 'Press ^D or type \'exit\' to continue. Type \'continue\' to continue non-interactive execution.' ) console( 'Task \'%s\' has %s entries, enter regexp to see what matches it.' % (task.name, len(task.entries))) while (True): try: s = input('--> ') if s == 'exit': break if s == 'abort' or s == 'continue': self.abort = True break except EOFError: break count = 0 for entry in task.entries: try: match, field = self.matches(entry, s) if match: console('Title: %-40s URL: %-30s From: %s' % (entry['title'], entry['url'], field)) count += 1 except re.error: console('Invalid regular expression') break console('%s of %s entries matched' % (count, len(task.entries))) console('Bye!')
def exposed_console(self, text, *args, **kwargs): console(text, *args, **kwargs)
def on_task_output(self, task, config): if not config and task.options.dump_entries is None: return eval_lazy = 'eval' in task.options.dump_entries trace = 'trace' in task.options.dump_entries title = 'title' in task.options.dump_entries states = ['accepted', 'rejected', 'failed', 'undecided'] dumpstates = [s for s in states if s in task.options.dump_entries] specificstates = dumpstates if not dumpstates: dumpstates = states undecided = [entry for entry in task.all_entries if entry.undecided] if 'undecided' in dumpstates: if undecided: console('-- Undecided: --------------------------') dump(undecided, task.options.debug, eval_lazy, trace, title) elif specificstates: console('No undecided entries') if 'accepted' in dumpstates: if task.accepted: console('-- Accepted: ---------------------------') dump(task.accepted, task.options.debug, eval_lazy, trace, title) elif specificstates: console('No accepted entries') if 'rejected' in dumpstates: if task.rejected: console('-- Rejected: ---------------------------') dump(task.rejected, task.options.debug, eval_lazy, trace, title) elif specificstates: console('No rejected entries') if 'failed' in dumpstates: if task.failed: console('-- Failed: -----------------------------') dump(task.failed, task.options.debug, eval_lazy, trace, title) elif specificstates: console('No failed entries')
def on_manager_shutdown(manager): if not manager.options.mem_usage: return import resource console( 'Resource Module memory usage: %s (kb)' % resource.getrusage(resource.RUSAGE_SELF).ru_maxrss ) global heapy console('Heapy module calculating memory usage:') console(heapy.heap()) console('-' * 79) console('Heapy module calculating report (this may take a while):') console(heapy.heap().get_rp(40)) heapy = None
def dump(entries, debug=False, eval_lazy=False, trace=False, title_only=False): """ Dump *entries* to stdout :param list entries: Entries to be dumped. :param bool debug: Print non printable fields as well. :param bool eval_lazy: Evaluate lazy fields. :param bool trace: Display trace information. :param bool title_only: Display only title field """ def sort_key(field): # Sort certain fields above the rest if field == 'title': return (0,) if field == 'url': return (1,) if field == 'original_url': return (2,) return 3, field for entry in entries: for field in sorted(entry, key=sort_key): if field.startswith('_') and not debug: continue if title_only and field != 'title': continue if entry.is_lazy(field) and not eval_lazy: value = '<LazyField - value will be determined when it is accessed>' else: try: value = entry[field] except KeyError: value = '<LazyField - lazy lookup failed>' if isinstance(value, str): try: console('%-17s: %s' % (field, value.replace('\r', '').replace('\n', ''))) except Exception: console('%-17s: %r (warning: unable to print)' % (field, value)) elif isinstance(value, list): console('%-17s: %s' % (field, '[%s]' % ', '.join(str(v) for v in value))) elif isinstance(value, (int, float, dict)): console('%-17s: %s' % (field, value)) elif value is None: console('%-17s: %s' % (field, value)) else: try: value = str(entry[field]) console('%-17s: %s' % (field, value.replace('\r', '').replace('\n', ''))) except Exception: if debug: console('%-17s: [not printable] (%r)' % (field, value)) if trace: console('-- Processing trace:') for item in entry.traces: console('%-10s %-7s %s' % (item[0], '' if item[1] is None else item[1], item[2])) if not title_only: console('')