class Trackma_cmd(cmd.Cmd): """ Main program, inherits from the useful Cmd class for interactive console """ engine = None filter_num = 1 sort = 'title' completekey = 'Tab' cmdqueue = [] stdout = sys.stdout sortedlist = [] needed_args = { 'altname': (1, 2), 'filter': (0, 1), 'sort': 1, 'mediatype': (0, 1), 'info': 1, 'search': 1, 'add': 1, 'delete': 1, 'play': (1, 2), 'update': 2, 'score': 2, 'status': 2, } def __init__(self, account_num=None, debug=False): print('Trackma v'+utils.VERSION+' Copyright (C) 2012 z411') print('This program comes with ABSOLUTELY NO WARRANTY; for details type `info\'') print('This is free software, and you are welcome to redistribute it') print('under certain conditions; see the file COPYING for details.') print() self.debug = debug self.accountman = Trackma_accounts() if account_num: try: self.account = self.accountman.get_account(int(account_num)) except KeyError: print("Account {} doesn't exist.".format(account_num)) self.account = self.accountman.select_account(True) except ValueError: print("Account {} must be numeric.".format(account_num)) self.account = self.accountman.select_account(True) else: self.account = self.accountman.select_account(False) def _update_prompt(self): self.prompt = "{c_u}{u}{c_r} [{c_a}{a}{c_r}] ({c_mt}{mt}{c_r}) {c_s}{s}{c_r} >> ".format( u = self.engine.get_userconfig('username'), a = self.engine.api_info['shortname'], mt = self.engine.api_info['mediatype'], s = self.engine.mediainfo['statuses_dict'][self.filter_num].lower().replace(' ', ''), c_r = _PCOLOR_RESET, c_u = _PCOLOR_USER, c_a = _PCOLOR_API, c_mt = _PCOLOR_MEDIATYPE, c_s = _COLOR_RESET ) def _load_list(self, *args): showlist = self.engine.filter_list(self.filter_num) sortedlist = sorted(showlist, key=itemgetter(self.sort)) self.sortedlist = list(enumerate(sortedlist, 1)) def _get_show(self, title): # Attempt parsing list index # otherwise use title try: index = int(title)-1 return self.sortedlist[index][1] except (ValueError, AttributeError, IndexError): return self.engine.get_show_info_title(title) def _ask_update(self, show, episode): do_update = input("Should I update %s to episode %d? [y/N] " % (show['title'], episode)) if do_update.lower() == 'y': self.engine.set_episode(show['id'], episode) def start(self): """ Initializes the engine Creates an Engine object and starts it. """ print('Initializing engine...') self.engine = Engine(self.account, self.messagehandler) self.engine.connect_signal('show_added', self._load_list) self.engine.connect_signal('show_deleted', self._load_list) self.engine.connect_signal('status_changed', self._load_list) self.engine.connect_signal('episode_changed', self._load_list) self.engine.connect_signal('prompt_for_update', self._ask_update) self.engine.start() # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt() print() print("Ready. Type 'help' for a list of commands.") print("Press tab for autocompletion and up/down for command history.") self.do_filter(None) # Show available filters print() def do_help(self, arg): if arg: try: doc = getattr(self, 'do_' + arg).__doc__ if doc: (name, args, expl, usage) = self._parse_doc(arg, doc) print() print(name) for line in expl: print(" {}".format(line)) if args: print("\n Arguments:") for arg in args: if arg[2]: print(" {}: {}".format(arg[0], arg[1])) else: print(" {} (optional): {}".format(arg[0], arg[1])) if usage: print("\n Usage: " + usage) print() return except AttributeError: pass print("No help available.") return else: CMD_LENGTH = 11 ARG_LENGTH = 13 (height, width) = utils.get_terminal_size() prev_width = CMD_LENGTH + ARG_LENGTH + 3 tw = textwrap.TextWrapper() tw.width = width - 2 tw.subsequent_indent = ' ' * prev_width print() print(" {0:>{1}} {2:{3}} {4}".format( 'command', CMD_LENGTH, 'args', ARG_LENGTH, 'description')) print(" " + "-"*(min(prev_width+81, width-3))) names = self.get_names() names.sort() cmds = [] for name in names: if name[:3] == 'do_': doc = getattr(self, name).__doc__ if not doc: continue cmd = name[3:] (name, args, expl, usage) = self._parse_doc(cmd, doc) line = " {0:>{1}} {2:{3}} {4}".format( name, CMD_LENGTH, '<' + ','.join( a[0] for a in args) + '>', ARG_LENGTH, expl[0]) print(tw.fill(line)) print() print("Use `help <command>` for detailed information.") print() def do_account(self, args): """ Switch to a different account. """ self.account = self.accountman.select_account(True) self.engine.reload(account=self.account) # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt() def do_filter(self, args): """ Changes the filtering of list by status.s :optparam status Name of status to filter :usage filter [filter type] """ # Query the engine for the available statuses # that the user can choose if args: try: self.filter_num = self._guess_status(args[0].lower()) self._load_list() self._update_prompt() except KeyError: print("Invalid filter.") else: print("Available statuses: %s" % ', '.join( v.lower().replace(' ', '') for v in self.engine.mediainfo['statuses_dict'].values() )) def do_sort(self, args): """ Change of the lists :param type Sort type; available types: id, title, my_progress, total, my_score :usage sort <sort type> """ sorts = ('id', 'title', 'my_progress', 'total', 'my_score') if args[0] in sorts: self.sort = args[0] self._load_list() else: print("Invalid sort.") def do_mediatype(self, args): """ Reloads engine with different mediatype. Call with no arguments to see supported mediatypes. :optparam mediatype Mediatype name :usage mediatype [mediatype] """ if args: if args[0] in self.engine.api_info['supported_mediatypes']: self.engine.reload(mediatype=args[0]) # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt() else: print("Invalid mediatype.") else: print("Supported mediatypes: %s" % ', '.join(self.engine.api_info['supported_mediatypes'])) def do_ls(self,args): self.do_list(args) def do_list(self, args): """ Lists all shows available in the local list. :name list|ls """ # Show the list in memory self._make_list(self.sortedlist) def do_info(self, args): """ Gets detailed information about a local show. :param show Show index or title. :usage info <show index or title> """ try: show = self._get_show(args[0]) details = self.engine.get_show_details(show) except utils.TrackmaError as e: self.display_error(e) return print("Title: %s" % details['title']) for line in details['extra']: print("%s: %s" % line) def do_search(self, args): """ Does a regex search on shows in the local lists. :param pattern Regex pattern to search for. :usage search <pattern> """ sortedlist = list(v for v in self.sortedlist if re.search(args[0], v[1]['title'], re.I)) self._make_list(sortedlist) def do_add(self, args): """ Search for a show in the remote service and add it. :param pattern Show criteria to search. :usage add <pattern> """ try: entries = self.engine.search(args[0]) except utils.TrackmaError as e: self.display_error(e) return for i, entry in enumerate(entries, start=1): print("%d: (%s) %s" % (i, entry['type'], entry['title'])) do_update = input("Choose show to add (blank to cancel): ") if do_update != '': try: show = entries[int(do_update)-1] except ValueError: print("Choice must be numeric.") return except IndexError: print("Invalid show.") return # Tell the engine to add the show try: self.engine.add_show(show, self.filter_num) except utils.TrackmaError as e: self.display_error(e) def do_delete(self, args): """ Deletes a show from the local list. :param show Show index or title. :usage delete <show index or title> """ try: show = self._get_show(args[0]) do_delete = input("Delete %s? [y/N] " % show['title']) if do_delete.lower() == 'y': self.engine.delete_show(show) except utils.TrackmaError as e: self.display_error(e) def do_rescan(self, args): """ Re-scans the local library. """ self.engine.scan_library(rescan=True) def do_random(self, args): """ Starts the media player with a random new episode. """ try: self.engine.play_random() except utils.TrackmaError as e: self.display_error(e) def do_play(self, args): """ Starts the media player with the specified episode number (next if not specified). :param show Episode index or title. :optparam ep Episode number. Assume next if not specified. :usage play <show index or title> [episode number] """ try: episode = 0 show = self._get_show(args[0]) # If the user specified an episode, play it # otherwise play the next episode not watched yet try: episode = args[1] if episode == (show['my_progress'] + 1): playing_next = True else: playing_next = False except IndexError: playing_next = True played_episode = self.engine.play_episode(show, episode) except utils.TrackmaError as e: self.display_error(e) def do_update(self, args): """ Updates the progress of a show to the specified episode. :param show Show index or name. :param ep Episode number (numeric). :usage update <show index or name> <episode number> """ try: show = self._get_show(args[0]) self.engine.set_episode(show['id'], args[1]) except IndexError: print("Missing arguments.") except utils.TrackmaError as e: self.display_error(e) def do_score(self, args): """ Changes the score of a show. :param show Show index or name. :param score Score to set (numeric/decimal). :usage score <show index or name> <score> """ try: show = self._get_show(args[0]) self.engine.set_score(show['id'], args[1]) except IndexError: print("Missing arguments.") except utils.TrackmaError as e: self.display_error(e) def do_status(self, args): """ Changes the status of a show. Use the command `filter` without arguments to see the available statuses. :param show Show index or name. :param status Status name. Use `filter` without args to list them. :usage status <show index or name> <status name> """ try: _showtitle = args[0] _filter = args[1] except IndexError: print("Missing arguments.") return try: _filter_num = self._guess_status(_filter) except KeyError: print("Invalid filter.") return try: show = self._get_show(_showtitle) self.engine.set_status(show['id'], _filter_num) except utils.TrackmaError as e: self.display_error(e) def do_altname(self, args): """ Changes the alternative name of a show. Use the command 'altname' without arguments to clear the alternative name. :param show Show index or name :param alt The alternative name. Use `altname` without alt to clear it :usage altname <show index or name> <alternative name> """ try: altnames = self.engine.altnames() show = self._get_show(args[0]) altname = args[1] if len(args) > 1 else '' self.engine.altname(show['id'],altname) except IndexError: print("Missing arguments") return except utils.TrackmaError as e: self.display_error(e) def do_send(self, args): """ Sends queued changes to the remote service. """ try: self.engine.list_upload() except utils.TrackmaError as e: self.display_error(e) def do_retrieve(self, args): """ Retrieves the remote list overwrites the local one. """ try: if self.engine.get_queue(): answer = input("There are unqueued changes. Overwrite local list? [y/N] ") if answer.lower() == 'y': self.engine.list_download() else: self.engine.list_download() self._load_list() except utils.TrackmaError as e: self.display_error(e) def do_undoall(self, args): """ Undo all changes in queue. """ try: self.engine.undoall() except utils.TrackmaError as e: self.display_error(e) def do_viewqueue(self, args): """ List the queued changes. """ queue = self.engine.get_queue() if len(queue): print("Queue:") for show in queue: print("- %s" % show['title']) else: print("Queue is empty.") def do_exit(self, args): self.do_quit(args) def do_quit(self, args): """ Quits the program. :name quit|exit """ try: self.engine.unload() except utils.TrackmaError as e: self.display_error(e) print('Bye!') sys.exit(0) def do_EOF(self, args): print() self.do_quit(args) def complete_update(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_play(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_score(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_status(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_delete(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_filter(self, text, line, begidx, endidx): return [v.lower().replace(' ', '') for v in self.engine.mediainfo['statuses_dict'].values()] def parse_args(self, arg): if arg: return shlex.split(arg) else: return [] def emptyline(self): return def onecmd(self, line): """ Override. """ cmd, arg, line = self.parseline(line) if not line: return self.emptyline() if cmd is None: return self.default(line) self.lastcmd = line if line == 'EOF' : self.lastcmd = '' if cmd == '': return self.default(line) elif cmd == 'help': return self.do_help(arg) else: return self.execute(cmd, arg, line) def execute(self, cmd, arg, line): try: func = getattr(self, 'do_' + cmd) except AttributeError: return self.default(line) args = self.parse_args(arg) try: needed = self.needed_args[cmd] except KeyError: needed = 0 if isinstance(needed, int): needed = (needed, needed) if needed[0] <= len(args) <= needed[1]: return func(args) else: print("Incorrent number of arguments. See `help %s`" % cmd) def display_error(self, e): print("%s%s: %s%s" % (_COLOR_ERROR, type(e), e, _COLOR_RESET)) def messagehandler(self, classname, msgtype, msg): """ Handles and shows messages coming from the engine messenger to provide feedback. """ color_escape = '' color_reset = _COLOR_RESET if classname == 'Engine': color_escape = _COLOR_ENGINE elif classname == 'Data': color_escape = _COLOR_DATA elif classname == 'Tracker': color_escape = _COLOR_TRACKER elif classname.startswith('lib'): color_escape = _COLOR_API else: color_reset = '' if msgtype == messenger.TYPE_INFO: print("%s%s: %s%s" % (color_escape, classname, msg, color_reset)) elif msgtype == messenger.TYPE_WARN: print("%s%s warning: %s%s" % (color_escape, classname, msg, color_reset)) elif self.debug and msgtype == messenger.TYPE_DEBUG: print("%s%s: %s%s" % (color_escape, classname, msg, color_reset)) def _guess_status(self, string): for k, v in self.engine.mediainfo['statuses_dict'].items(): if string.lower() == v.lower().replace(' ', ''): return k raise KeyError def _parse_doc(self, cmd, doc): lines = doc.split('\n') name = cmd args = [] expl = [] usage = None for line in lines: line = line.strip() if line[:6] == ":param": args.append( line[7:].split(' ', 1) + [True] ) elif line[:9] == ":optparam": args.append( line[10:].split(' ', 1) + [False] ) elif line[:6] == ':usage': usage = line[7:] elif line[:5] == ':name': name = line[6:] elif line: expl.append(line) return (name, args, expl, usage) def _make_list(self, showlist): """ Helper function for printing a formatted show list """ # Fixed column widths col_id_length = 7 col_index_length = 6 col_title_length = 5 col_episodes_length = 9 col_score_length = 6 altnames = self.engine.altnames() # Calculate maximum width for the title column # based on the width of the terminal (height, width) = utils.get_terminal_size() max_title_length = width - col_id_length - col_episodes_length - col_score_length - col_index_length - 5 # Find the widest title so we can adjust the title column for index, show in showlist: if len(show['title']) > col_title_length: if len(show['title']) > max_title_length: # Stop if we exceeded the maximum column width col_title_length = max_title_length break else: col_title_length = len(show['title']) # Print header print("| {0:{1}} {2:{3}} {4:{5}} {6:{7}} |".format( 'Index', col_index_length, 'Title', max_title_length, 'Progress', col_episodes_length, 'Score', col_score_length)) # List shows for index, show in showlist: if self.engine.mediainfo['has_progress']: episodes_str = "{0:3} / {1}".format(show['my_progress'], show['total']) else: episodes_str = "-" #Get title (and alt. title) and if need be, truncate it title_str = show['title'] if altnames.get(show['id']): title_str += "[{}]".format(altnames.get(show['id'])) title_str = title_str[:max_title_length] if len(title_str) > max_title_length else title_str # Color title according to status if show['status'] == utils.STATUS_AIRING: colored_title = _COLOR_AIRING + title_str + _COLOR_RESET else: colored_title = title_str print("| {0:^{1}} {2}{3} {4:{5}} {6:^{7}} |".format( index, col_index_length, colored_title, '.' * (max_title_length-len(title_str)), episodes_str, col_episodes_length, show['my_score'], col_score_length)) # Print result count print('%d results' % len(showlist)) print()
class TrackmaWindow(Gtk.ApplicationWindow): __gtype_name__ = 'TrackmaWindow' btn_appmenu = Gtk.Template.Child() btn_mediatype = Gtk.Template.Child() header_bar = Gtk.Template.Child() def __init__(self, app, debug=False): Gtk.ApplicationWindow.__init__(self, application=app) self.init_template() self._debug = debug self._configfile = utils.to_config_path('ui-Gtk.json') self._config = utils.parse_config(self._configfile, utils.gtk_defaults) self.statusicon = None self._main_view = None self._modals = [] self._account = None self._engine = None self.close_thread = None self.hidden = False self._init_widgets() def init_account_selection(self): manager = AccountManager() # Use the remembered account if there's one if manager.get_default(): self._create_engine(manager.get_default()) else: self._show_accounts(switch=False) def _init_widgets(self): Gtk.Window.set_default_icon_from_file(utils.DATADIR + '/icon.png') self.set_position(Gtk.WindowPosition.CENTER) self.set_title('Trackma') if self._config['remember_geometry']: self.resize(self._config['last_width'], self._config['last_height']) if not self._main_view: self._main_view = MainView(self._config) self._main_view.connect('error', self._on_main_view_error) self._main_view.connect( 'success', lambda x: self._set_buttons_sensitive(True)) self._main_view.connect('error-fatal', self._on_main_view_error_fatal) self._main_view.connect('show-action', self._on_show_action) self.add(self._main_view) self.connect('delete_event', self._on_delete_event) builder = Gtk.Builder.new_from_file( os.path.join(gtk_dir, 'data/shortcuts.ui')) help_overlay = builder.get_object('shortcuts-window') self.set_help_overlay(help_overlay) # Status icon if TrackmaStatusIcon.is_tray_available(): self.statusicon = TrackmaStatusIcon() self.statusicon.connect('hide-clicked', self._on_tray_hide_clicked) self.statusicon.connect('about-clicked', self._on_tray_about_clicked) self.statusicon.connect('quit-clicked', self._on_tray_quit_clicked) if self._config['show_tray']: self.statusicon.set_visible(True) else: self.statusicon.set_visible(False) # Don't show the main window if start in tray option is set if self.statusicon and self._config['show_tray'] and self._config[ 'start_in_tray']: self.hidden = True else: self.present() def _on_tray_hide_clicked(self, status_icon): self._destroy_modals() if self.hidden: self.deiconify() self.present() if not self._engine: self._show_accounts(switch=False) else: self.hide() self.hidden = not self.hidden def _destroy_modals(self): self.get_help_overlay().hide() for modal_window in self._modals: modal_window.destroy() self._modals = [] def _on_tray_about_clicked(self, status_icon): self._on_about(None, None) def _on_tray_quit_clicked(self, status_icon): self._quit() def _on_delete_event(self, widget, event, data=None): if self.statusicon and self.statusicon.get_visible( ) and self._config['close_to_tray']: self.hidden = True self.hide() else: self._quit() return True def _create_engine(self, account): self._engine = Engine(account, self._message_handler) self._main_view.load_engine_account(self._engine, account) self._set_actions() self._set_mediatypes_menu() self._update_widgets(account) self._set_buttons_sensitive(True) def _set_actions(self): builder = Gtk.Builder.new_from_file( os.path.join(gtk_dir, 'data/app-menu.ui')) settings = Gtk.Settings.get_default() if not settings.get_property("gtk-shell-shows-menubar"): self.btn_appmenu.set_menu_model(builder.get_object('app-menu')) else: self.get_application().set_menubar(builder.get_object('menu-bar')) self.btn_appmenu.set_property('visible', False) def add_action(name, callback): action = Gio.SimpleAction.new(name, None) action.connect('activate', callback) self.add_action(action) add_action('search', self._on_search) add_action('syncronize', self._on_synchronize) add_action('upload', self._on_upload) add_action('download', self._on_download) add_action('scanfiles', self._on_scanfiles) add_action('accounts', self._on_accounts) add_action('preferences', self._on_preferences) add_action('about', self._on_about) add_action('play_next', self._on_action_play_next) add_action('play_random', self._on_action_play_random) add_action('episode_add', self._on_action_episode_add) add_action('episode_remove', self._on_action_episode_remove) add_action('delete', self._on_action_delete) add_action('copy', self._on_action_copy) def _set_mediatypes_action(self): action_name = 'change-mediatype' if self.has_action(action_name): self.remove_action(action_name) state = GLib.Variant.new_string(self._engine.api_info['mediatype']) action = Gio.SimpleAction.new_stateful(action_name, state.get_type(), state) action.connect('change-state', self._on_change_mediatype) self.add_action(action) def _set_mediatypes_menu(self): self._set_mediatypes_action() menu = Gio.Menu() for mediatype in self._engine.api_info['supported_mediatypes']: variant = GLib.Variant.new_string(mediatype) menu_item = Gio.MenuItem() menu_item.set_label(mediatype) menu_item.set_action_and_target_value('win.change-mediatype', variant) menu.append_item(menu_item) self.btn_mediatype.set_menu_model(menu) if len(self._engine.api_info['supported_mediatypes']) <= 1: self.btn_mediatype.hide() def _update_widgets(self, account): current_api = utils.available_libs[account['api']] api_iconpath = 1 api_iconfile = current_api[api_iconpath] self.header_bar.set_subtitle(self._engine.api_info['name'] + " (" + self._engine.api_info['mediatype'] + ")") if self.statusicon and self._config['tray_api_icon']: self.statusicon.set_from_file(api_iconfile) def _on_change_mediatype(self, action, value): action.set_state(value) mediatype = value.get_string() self._set_buttons_sensitive(False) self._main_view.load_account_mediatype(None, mediatype, self.header_bar) def _on_search(self, action, param): current_status = self._main_view.get_current_status() win = SearchWindow(self._engine, self._config['colors'], current_status, transient_for=self) win.connect('search-error', self._on_search_error) win.connect('destroy', self._on_modal_destroy) win.present() self._modals.append(win) def _on_search_error(self, search_window, error_msg): print(error_msg) def _on_synchronize(self, action, param): threading.Thread(target=self._synchronization_task, args=(True, True)).start() def _on_upload(self, action, param): threading.Thread(target=self._synchronization_task, args=(True, False)).start() def _on_download(self, action, param): def _download_lists(): threading.Thread(target=self._synchronization_task, args=(False, True)).start() def _on_download_response(_dialog, response): _dialog.destroy() if response == Gtk.ResponseType.YES: _download_lists() queue = self._engine.get_queue() if queue: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, "There are %d queued changes in your list. If you retrieve the remote list now you will lose your queued changes. Are you sure you want to continue?" % len(queue)) dialog.show_all() dialog.connect("response", _on_download_response) else: # If the user doesn't have any queued changes # just go ahead _download_lists() def _synchronization_task(self, send, retrieve): self._set_buttons_sensitive_idle(False) try: if send: self._engine.list_upload() if retrieve: self._engine.list_download() # GLib.idle_add(self._set_score_ranges) GLib.idle_add(self._main_view.populate_all_pages) except utils.TrackmaError as e: self._error_dialog_idle(e) except utils.TrackmaFatal as e: self._show_accounts_idle(switch=False, forget=True) self._error_dialog_idle("Fatal engine error: %s" % e) return self._main_view.set_status_idle("Ready.") self._set_buttons_sensitive_idle(True) def _on_scanfiles(self, action, param): threading.Thread(target=self._scanfiles_task).start() def _scanfiles_task(self): self._set_buttons_sensitive_idle(False) try: self._engine.scan_library(rescan=True) except utils.TrackmaError as e: self._error_dialog_idle(e) GLib.idle_add(self._main_view.populate_all_pages) self._main_view.set_status_idle("Ready.") self._set_buttons_sensitive_idle(True) def _on_accounts(self, action, param): self._show_accounts() def _show_accounts_idle(self, switch=True, forget=False): GLib.idle_add(self._show_accounts, switch, forget) def _show_accounts(self, switch=True, forget=False): manager = AccountManager() if forget: manager.set_default(None) accountsel = AccountsWindow(manager, transient_for=self) accountsel.connect('account-open', self._on_account_open) accountsel.connect('account-cancel', self._on_account_cancel, switch) accountsel.connect('destroy', self._on_modal_destroy) accountsel.present() self._modals.append(accountsel) def _on_account_open(self, accounts_window, account_num, remember): manager = AccountManager() account = manager.get_account(account_num) if remember: manager.set_default(account_num) else: manager.set_default(None) # Reload the engine if already started, # start it otherwise self._set_buttons_sensitive(False) if self._engine and self._engine.loaded: self._main_view.load_account_mediatype(account, None, None) else: self._create_engine(account) def _on_account_cancel(self, _accounts_window, switch): manager = AccountManager() if not switch or not manager.get_accounts(): self._quit() def _on_preferences(self, _action, _param): win = SettingsWindow(self._engine, self._config, self._configfile, transient_for=self) win.connect('destroy', self._on_modal_destroy) win.present() self._modals.append(win) def _on_about(self, _action, _param): about = Gtk.AboutDialog(parent=self) about.set_modal(True) about.set_transient_for(self) about.set_program_name("Trackma GTK") about.set_version(utils.VERSION) about.set_license_type(Gtk.License.GPL_3_0_ONLY) about.set_comments( "Trackma is an open source client for media tracking websites.\nThanks to all contributors." ) about.set_website("http://github.com/z411/trackma") about.set_copyright("© z411, et al.") about.set_authors(["See AUTHORS file"]) about.set_artists(["shuuichi"]) about.connect('destroy', self._on_modal_destroy) about.connect('response', lambda dialog, response: dialog.destroy()) about.present() self._modals.append(about) def _on_modal_destroy(self, modal_window): self._modals.remove(modal_window) def _quit(self): if self._config['remember_geometry']: self._store_geometry() if not self._engine: self.get_application().quit() return if self.close_thread is None: self._set_buttons_sensitive_idle(False) self.close_thread = threading.Thread(target=self._unload_task) self.close_thread.start() def _unload_task(self): self._engine.unload() GLib.idle_add(self.get_application().quit) def _store_geometry(self): (width, height) = self.get_size() self._config['last_width'] = width self._config['last_height'] = height utils.save_config(self._config, self._configfile) def _message_handler(self, classname, msgtype, msg): # Thread safe # print("%s: %s" % (classname, msg)) if msgtype == messenger.TYPE_WARN: self._main_view.set_status_idle("%s warning: %s" % (classname, msg)) elif msgtype != messenger.TYPE_DEBUG: self._main_view.set_status_idle("%s: %s" % (classname, msg)) elif self._debug: print('[D] {}: {}'.format(classname, msg)) def _on_main_view_error(self, main_view, error_msg): self._error_dialog_idle(error_msg) def _on_main_view_error_fatal(self, main_view, error_msg): self._show_accounts_idle(switch=False, forget=True) self._error_dialog_idle(error_msg) def _error_dialog_idle(self, msg, icon=Gtk.MessageType.ERROR): # Thread safe GLib.idle_add(self._error_dialog, msg, icon) def _error_dialog(self, msg, icon=Gtk.MessageType.ERROR): def error_dialog_response(widget, response_id): widget.destroy() dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL, icon, Gtk.ButtonsType.OK, str(msg)) dialog.show_all() dialog.connect("response", error_dialog_response) print('Error: {}'.format(msg)) def _on_action_play_next(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._play_next(selected_show) def _on_action_play_random(self, action, param): self._play_random() def _on_action_episode_add(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._episode_add(selected_show) def _on_action_episode_remove(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._episode_remove(selected_show) def _on_action_delete(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._remove_show(selected_show) def _on_action_copy(self, action, param): selected_show = self._main_view.get_selected_show() if selected_show: self._copy_title(selected_show) def _on_show_action(self, main_view, event_type, data): if event_type == ShowEventType.PLAY_NEXT: self._play_next(*data) elif event_type == ShowEventType.PLAY_EPISODE: self._play_episode(*data) elif event_type == ShowEventType.EPISODE_REMOVE: self._episode_remove(*data) elif event_type == ShowEventType.EPISODE_SET: self._episode_set(*data) elif event_type == ShowEventType.EPISODE_ADD: self._episode_add(*data) elif event_type == ShowEventType.SET_SCORE: self._set_score(*data) elif event_type == ShowEventType.SET_STATUS: self._set_status(*data) elif event_type == ShowEventType.DETAILS: self._open_details(*data) elif event_type == ShowEventType.OPEN_WEBSITE: self._open_website(*data) elif event_type == ShowEventType.OPEN_FOLDER: self._open_folder(*data) elif event_type == ShowEventType.COPY_TITLE: self._copy_title(*data) elif event_type == ShowEventType.CHANGE_ALTERNATIVE_TITLE: self._change_alternative_title(*data) elif event_type == ShowEventType.REMOVE: self._remove_show(*data) def _play_next(self, show_id): show = self._engine.get_show_info(show_id) try: args = self._engine.play_episode(show) utils.spawn_process(args) except utils.TrackmaError as e: self._error_dialog(e) def _play_episode(self, show_id, episode): show = self._engine.get_show_info(show_id) try: if not episode: episode = self.show_ep_num.get_value_as_int() args = self._engine.play_episode(show, episode) utils.spawn_process(args) except utils.TrackmaError as e: self._error_dialog(e) def _play_random(self): try: args = self._engine.play_random() utils.spawn_process(args) except utils.TrackmaError as e: self._error_dialog(e) def _episode_add(self, show_id): show = self._engine.get_show_info(show_id) self._episode_set(show_id, show['my_progress'] + 1) def _episode_remove(self, show_id): show = self._engine.get_show_info(show_id) self._episode_set(show_id, show['my_progress'] - 1) def _episode_set(self, show_id, episode): try: self._engine.set_episode(show_id, episode) except utils.TrackmaError as e: self._error_dialog(e) def _set_score(self, show_id, score): try: self._engine.set_score(show_id, score) except utils.TrackmaError as e: self._error_dialog(e) def _set_status(self, show_id, status): try: self._engine.set_status(show_id, status) except utils.TrackmaError as e: self._error_dialog(e) def _open_details(self, show_id): show = self._engine.get_show_info(show_id) win = ShowInfoWindow(self._engine, show, transient_for=self) win.connect('destroy', self._on_modal_destroy) win.present() self._modals.append(win) def _open_website(self, show_id): show = self._engine.get_show_info(show_id) if show['url']: Gtk.show_uri(None, show['url'], Gdk.CURRENT_TIME) def _open_folder(self, show_id): show = self._engine.get_show_info(show_id) try: filename = self._engine.get_episode_path(show, 1) with open(os.devnull, 'wb') as DEVNULL: if sys.platform == 'darwin': subprocess.Popen( ["open", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) elif sys.platform == 'win32': subprocess.Popen( ["explorer", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) else: subprocess.Popen( ["/usr/bin/xdg-open", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) except OSError: # xdg-open failed. raise utils.EngineError("Could not open folder.") except utils.EngineError: # Show not in library. self._error_dialog_idle("No folder found.") def _copy_title(self, show_id): show = self._engine.get_show_info(show_id) clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(show['title'], -1) self._main_view.set_status_idle('Title copied to clipboard.') def _change_alternative_title(self, show_id): show = self._engine.get_show_info(show_id) current_altname = self._engine.altname(show_id) def altname_response(entry, dialog, response): dialog.response(response) dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, None) dialog.set_markup('Set the <b>alternate title</b> for the show.') entry = Gtk.Entry() entry.set_text(current_altname) entry.connect("activate", altname_response, dialog, Gtk.ResponseType.OK) hbox = Gtk.HBox() hbox.pack_start(Gtk.Label("Alternate Title:"), False, 5, 5) hbox.pack_end(entry, True, True, 0) dialog.format_secondary_markup( "Use this if the tracker is unable to find this show. Leave blank to disable." ) dialog.vbox.pack_end(hbox, True, True, 0) dialog.show_all() retval = dialog.run() if retval == Gtk.ResponseType.OK: text = entry.get_text() self._engine.altname(show_id, text) self._main_view.change_show_title_idle(show, text) dialog.destroy() def _remove_show(self, show_id): try: show = self._engine.get_show_info(show_id) self._engine.delete_show(show) except utils.TrackmaError as e: self._error_dialog_idle(e) def _set_buttons_sensitive_idle(self, sensitive): GLib.idle_add(self._set_buttons_sensitive, sensitive) self._main_view.set_buttons_sensitive_idle(sensitive) def _set_buttons_sensitive(self, sensitive): actions_names = [ 'search', 'syncronize', 'upload', 'download', 'scanfiles', 'accounts', 'play_next', 'play_random', 'episode_add', 'episode_remove', 'delete', 'copy' ] for action_name in actions_names: action = self.lookup_action(action_name) if action is not None: action.set_enabled(sensitive)
class TrackmaWindow(Gtk.ApplicationWindow): __gtype_name__ = 'TrackmaWindow' btn_appmenu = GtkTemplate.Child() btn_mediatype = GtkTemplate.Child() _config = None show_lists = dict() image_thread = None close_thread = None hidden = False quit = False statusicon = None def __init__(self, debug=False): Gtk.ApplicationWindow.__init__(self) self.init_template() self._debug = debug self._configfile = utils.to_config_path('ui-Gtk.json') self._config = utils.parse_config(self._configfile, utils.gtk_defaults) self._main_view = None self._account = None self._engine = None self._init_widgets() self.present() def main(self): """Start the Account Selector""" manager = AccountManager() # Use the remembered account if there's one if manager.get_default(): self._create_engine(manager.get_default()) else: self._show_accounts(switch=False) def _init_widgets(self): Gtk.Window.set_default_icon_from_file(utils.DATADIR + '/icon.png') self.set_position(Gtk.WindowPosition.CENTER) self.set_title('Trackma-gtk ' + utils.VERSION) if self._config['remember_geometry']: self.resize(self._config['last_width'], self._config['last_height']) if not self._main_view: self._main_view = MainView(self._config) self._main_view.connect('error', self._on_main_view_error) self._main_view.connect('error-fatal', self._on_main_view_error_fatal) self._main_view.connect('show-action', self._on_show_action) self.add(self._main_view) self.connect('delete_event', self._on_delete_event) self.connect('destroy', self._on_destroy) # Status icon if tray_available: self.statusicon = Gtk.StatusIcon() self.statusicon.set_from_file(utils.DATADIR + '/icon.png') self.statusicon.set_tooltip_text('Trackma-gtk ' + utils.VERSION) self.statusicon.connect('activate', self._tray_status_event) self.statusicon.connect('popup-menu', self._tray_status_menu_event) if self._config['show_tray']: self.statusicon.set_visible(True) else: self.statusicon.set_visible(False) def _on_delete_event(self, widget, event, data=None): if self.statusicon and self.statusicon.get_visible( ) and self._config['close_to_tray']: self.hidden = True self.hide() else: self._quit() return True def _on_destroy(self, widget): if self.quit: Gtk.main_quit() def _create_engine(self, account): self._engine = Engine(account, self._message_handler) self._main_view.load_engine_account(self._engine, account) self._set_actions() self._set_mediatypes_menu() self._update_widgets(account) def _set_actions(self): builder = Gtk.Builder.new_from_file( os.path.join(gtk_dir, 'data/app-menu.ui')) self.btn_appmenu.set_menu_model(builder.get_object('app-menu')) def add_action(name, callback): action = Gio.SimpleAction.new(name, None) action.connect('activate', callback) self.add_action(action) add_action('search', self._on_search) add_action('syncronize', self._on_synchronize) add_action('upload', self._on_upload) add_action('download', self._on_download) add_action('scanfiles', self._on_scanfiles) add_action('accounts', self._on_accounts) add_action('preferences', self._on_preferences) add_action('about', self._on_about) add_action('quit', self._on_quit) def _set_mediatypes_action(self): action_name = 'change-mediatype' if self.has_action(action_name): self.remove_action(action_name) state = GLib.Variant.new_string(self._engine.api_info['mediatype']) action = Gio.SimpleAction.new_stateful(action_name, state.get_type(), state) action.connect('change-state', self._on_change_mediatype) self.add_action(action) def _set_mediatypes_menu(self): self._set_mediatypes_action() menu = Gio.Menu() for mediatype in self._engine.api_info['supported_mediatypes']: variant = GLib.Variant.new_string(mediatype) menu_item = Gio.MenuItem() menu_item.set_label(mediatype) menu_item.set_action_and_target_value('win.change-mediatype', variant) menu.append_item(menu_item) self.btn_mediatype.set_menu_model(menu) if len(self._engine.api_info['supported_mediatypes']) <= 1: self.btn_mediatype.hide() def _update_widgets(self, account): current_api = utils.available_libs[account['api']] api_iconpath = 1 api_iconfile = current_api[api_iconpath] self.set_title('Trackma-gtk %s [%s (%s)]' % (utils.VERSION, self._engine.api_info['name'], self._engine.api_info['mediatype'])) if self.statusicon and self._config['tray_api_icon']: self.statusicon.set_from_file(api_iconfile) # Don't show the main dialog if start in tray option is set if self.statusicon and self._config['show_tray'] and self._config[ 'start_in_tray']: self.hidden = True else: self.show() def _on_change_mediatype(self, action, value): action.set_state(value) mediatype = value.get_string() self._main_view.load_account_mediatype(None, mediatype) def _on_search(self, action, param): current_status = self._main_view.get_current_status() win = SearchWindow(self._engine, self._config['colors'], current_status, transient_for=self) win.connect('search-error', self._on_search_error) win.show_all() def _on_search_error(self, search_window, error_msg): print(error_msg) def _on_synchronize(self, action, param): threading.Thread(target=self._synchronization_task, args=(True, True)).start() def _on_upload(self, action, param): threading.Thread(target=self._synchronization_task, args=(True, False)).start() def _on_download(self, action, param): def _download_lists(): threading.Thread(target=self._synchronization_task, args=(False, True)).start() def _on_download_response(_dialog, response): _dialog.destroy() if response == Gtk.ResponseType.YES: _download_lists() queue = self._engine.get_queue() if not queue: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, "There are %d queued changes in your list. If you retrieve the remote list now you will lose your queued changes. Are you sure you want to continue?" % len(queue)) dialog.show_all() dialog.connect("response", _on_download_response) else: # If the user doesn't have any queued changes # just go ahead _download_lists() def _synchronization_task(self, send, retrieve): self._main_view.set_buttons_sensitive_idle(False) try: if send: self._engine.list_upload() if retrieve: self._engine.list_download() # GLib.idle_add(self._set_score_ranges) GLib.idle_add(self._main_view.populate_all_pages) except utils.TrackmaError as e: self._error_dialog_idle(e) except utils.TrackmaFatal as e: self._show_accounts_idle(switch=False, forget=True) self._error_dialog_idle("Fatal engine error: %s" % e) return self._main_view.set_status_idle("Ready.") self._main_view.set_buttons_sensitive_idle(True) def _on_scanfiles(self, action, param): threading.Thread(target=self._scanfiles_task).start() def _scanfiles_task(self): try: self._engine.scan_library(rescan=True) except utils.TrackmaError as e: self._error_dialog_idle(e) GLib.idle_add(self._main_view.populate_page, self._engine.mediainfo['status_start']) self._main_view.set_status_idle("Ready.") self._main_view.set_buttons_sensitive_idle(True) def _on_accounts(self, action, param): self._show_accounts() def _show_accounts_idle(self, switch=True, forget=False): GLib.idle_add(self._show_accounts, switch, forget) def _show_accounts(self, switch=True, forget=False): manager = AccountManager() if forget: manager.set_default(None) def _on_accountsel_cancel(accounts_window): Gtk.main_quit() accountsel = AccountsWindow(manager, transient_for=self) accountsel.connect('account-open', self._on_account_open) if not switch: accountsel.connect('account-cancel', _on_accountsel_cancel) def _on_account_open(self, accounts_window, account_num, remember): manager = AccountManager() account = manager.get_account(account_num) if remember: manager.set_default(account_num) else: manager.set_default(None) # Reload the engine if already started, # start it otherwise if self._engine and self._engine.loaded: self._main_view.load_account_mediatype(account, None) else: self._create_engine(account) def _on_preferences(self, action, param): win = SettingsWindow(self._engine, self._config, self._configfile, transient_for=self) win.show_all() def _on_about(self, action, param): about = Gtk.AboutDialog(parent=self) about.set_program_name("Trackma-gtk") about.set_version(utils.VERSION) about.set_license_type(Gtk.License.GPL_3_0_ONLY) about.set_comments( "Trackma is an open source client for media tracking websites.\nThanks to all contributors." ) about.set_website("http://github.com/z411/trackma") about.set_copyright("© z411, et al.") about.set_authors(["See AUTHORS file"]) about.set_artists(["shuuichi"]) about.run() about.destroy() def _on_quit(self, action, param): self._quit() def _quit(self): if self._config['remember_geometry']: self._store_geometry() if self.close_thread is None: self._main_view.set_buttons_sensitive_idle(False) self.close_thread = threading.Thread(target=self._unload_task) self.close_thread.start() def _unload_task(self): self._engine.unload() self._destroy_idle() def _destroy_idle(self): GLib.idle_add(self._destroy_push) def _destroy_push(self): self.quit = True self.destroy() def _store_geometry(self): (width, height) = self.get_size() self._config['last_width'] = width self._config['last_height'] = height utils.save_config(self._config, self._configfile) def _message_handler(self, classname, msgtype, msg): # Thread safe # print("%s: %s" % (classname, msg)) if msgtype == messenger.TYPE_WARN: self._main_view.set_status_idle("%s warning: %s" % (classname, msg)) elif msgtype != messenger.TYPE_DEBUG: self._main_view.set_status_idle("%s: %s" % (classname, msg)) elif self._debug: print('[D] {}: {}'.format(classname, msg)) def _on_main_view_error(self, main_view, error_msg): self._error_dialog_idle(error_msg) def _on_main_view_error_fatal(self, main_view, error_msg): self._show_accounts_idle(switch=False, forget=True) self._error_dialog_idle(error_msg) def _column_toggled(self, w, column_name, visible): if visible: # Make column visible self._config['visible_columns'].append(column_name) for view in self.show_lists.values(): view.cols[column_name].set_visible(True) else: # Make column invisible if len(self._config['visible_columns']) <= 1: return # There should be at least 1 column visible self._config['visible_columns'].remove(column_name) for view in self.show_lists.values(): view.cols[column_name].set_visible(False) utils.save_config(self._config, self._configfile) def _tray_status_event(self, widget): # Called when the tray icon is left-clicked if self.hidden: self.show() self.hidden = False else: self.hide() self.hidden = True def _tray_status_menu_event(self, icon, button, time): # Called when the tray icon is right-clicked menu = Gtk.Menu() mb_show = Gtk.MenuItem("Show/Hide") mb_about = Gtk.ImageMenuItem( 'About', Gtk.Image.new_from_icon_name(Gtk.STOCK_ABOUT, 0)) mb_quit = Gtk.ImageMenuItem( 'Quit', Gtk.Image.new_from_icon_name(Gtk.STOCK_QUIT, 0)) def on_mb_about(): self._on_about(None, None) def on_mb_quit(): self._quit() mb_show.connect("activate", self._tray_status_event) mb_about.connect("activate", on_mb_about) mb_quit.connect("activate", on_mb_quit) menu.append(mb_show) menu.append(mb_about) menu.append(Gtk.SeparatorMenuItem()) menu.append(mb_quit) menu.show_all() def pos(menu, icon): return Gtk.StatusIcon.position_menu(menu, icon) menu.popup(None, None, None, pos, button, time) def _error_dialog_idle(self, msg, icon=Gtk.MessageType.ERROR): # Thread safe GLib.idle_add(self._error_dialog, msg, icon) def _error_dialog(self, msg, icon=Gtk.MessageType.ERROR): def modal_close(widget, response_id): widget.destroy() dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL, icon, Gtk.ButtonsType.OK, str(msg)) dialog.show_all() dialog.connect("response", modal_close) print('Error: {}'.format(msg)) def _on_show_action(self, main_view, event_type, selected_show, data): if event_type == ShowEventType.PLAY_NEXT: self._play_next(selected_show) elif event_type == ShowEventType.PLAY_EPISODE: self._play_episode(selected_show, data) elif event_type == ShowEventType.DETAILS: self._open_details(selected_show) elif event_type == ShowEventType.OPEN_WEBSITE: self._open_website(selected_show) elif event_type == ShowEventType.OPEN_FOLDER: self._open_folder(selected_show) elif event_type == ShowEventType.COPY_TITLE: self._copy_title(selected_show) elif event_type == ShowEventType.CHANGE_ALTERNATIVE_TITLE: self._change_alternative_title(selected_show) elif event_type == ShowEventType.REMOVE: self._remove_show(selected_show) def _play_next(self, show_id): threading.Thread(target=self._play_task, args=[show_id, True, None]).start() def _play_episode(self, show_id, episode): threading.Thread(target=self._play_task, args=[show_id, False, episode]).start() def _play_task(self, show_id, playnext, episode): self._main_view.set_buttons_sensitive_idle(False) show = self._engine.get_show_info(show_id) try: if playnext: self._engine.play_episode(show) else: if not episode: episode = self.show_ep_num.get_value_as_int() self._engine.play_episode(show, episode) except utils.TrackmaError as e: self._error_dialog_idle(e) self._main_view.set_status_idle("Ready.") self._main_view.set_buttons_sensitive_idle(True) def _play_random(self): # TODO: Reimplement functionality in GUI threading.Thread(target=self._play_random_task).start() def _play_random_task(self): self._main_view.set_buttons_sensitive_idle(False) try: self._engine.play_random() except utils.TrackmaError as e: self._error_dialog_idle(e) self._main_view.set_status_idle("Ready.") self._main_view.set_buttons_sensitive_idle(True) def _open_details(self, show_id): show = self._engine.get_show_info(show_id) ShowInfoWindow(self._engine, show, transient_for=self) def _open_website(self, show_id): show = self._engine.get_show_info(show_id) if show['url']: Gtk.show_uri(None, show['url'], Gdk.CURRENT_TIME) def _open_folder(self, show_id): show = self._engine.get_show_info(show_id) try: filename = self._engine.get_episode_path(show, 1) with open(os.devnull, 'wb') as DEVNULL: subprocess.Popen( ["/usr/bin/xdg-open", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) except OSError: # xdg-open failed. raise utils.EngineError("Could not open folder.") except utils.EngineError: # Show not in library. self._error_dialog_idle("No folder found.") def _copy_title(self, show_id): show = self._engine.get_show_info(show_id) clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(show['title'], -1) self._main_view.set_status_idle('Title copied to clipboard.') def _change_alternative_title(self, show_id): show = self._engine.get_show_info(show_id) current_altname = self._engine.altname(show_id) def altname_response(entry, dialog, response): dialog.response(response) dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, None) dialog.set_markup('Set the <b>alternate title</b> for the show.') entry = Gtk.Entry() entry.set_text(current_altname) entry.connect("activate", altname_response, dialog, Gtk.ResponseType.OK) hbox = Gtk.HBox() hbox.pack_start(Gtk.Label("Alternate Title:"), False, 5, 5) hbox.pack_end(entry, True, True, 0) dialog.format_secondary_markup( "Use this if the tracker is unable to find this show. Leave blank to disable." ) dialog.vbox.pack_end(hbox, True, True, 0) dialog.show_all() retval = dialog.run() if retval == Gtk.ResponseType.OK: text = entry.get_text() self._engine.altname(show_id, text) self._main_view.change_show_title_idle(show, text) dialog.destroy() def _remove_show(self, show_id): print('Window__remove_show: ', show_id) try: show = self._engine.get_show_info(show_id) self._engine.delete_show(show) except utils.TrackmaError as e: self._error_dialog_idle(e)
class Trackma_cmd(cmd.Cmd): """ Main program, inherits from the useful Cmd class for interactive console """ engine = None filter_num = 1 sort = 'title' completekey = 'Tab' cmdqueue = [] stdout = sys.stdout in_prompt = False sortedlist = [] needed_args = { 'altname': (1, 2), 'filter': (0, 1), 'sort': 1, 'mediatype': (0, 1), 'info': 1, 'search': 1, 'add': 1, 'del': 1, 'delete': 1, 'play': (1, 2), 'openfolder': 1, 'update': (1, 2), 'score': 2, 'status': 2, } def __init__(self, account_num=None, debug=False, interactive=True): super().__init__() if interactive: print('Trackma v'+utils.VERSION+' Copyright (C) 2012-2017 z411') print('This program comes with ABSOLUTELY NO WARRANTY; for details type `about\'') print('This is free software, and you are welcome to redistribute it') print('under certain conditions; see the COPYING file for details.') print() self.interactive = interactive self.debug = debug self.accountman = Trackma_accounts() if account_num: try: self.account = self.accountman.get_account(account_num) except KeyError: print("Account {} doesn't exist.".format(account_num)) self.account = self.accountman.select_account(True) except ValueError: print("Account {} must be numeric.".format(account_num)) self.account = self.accountman.select_account(True) else: self.account = self.accountman.select_account(False) def forget_account(self): self.accountman.set_default(None) def _update_prompt(self): self.prompt = "{c_u}{u}{c_r} [{c_a}{a}{c_r}] ({c_mt}{mt}{c_r}) {c_s}{s}{c_r} >> ".format( u = self.engine.get_userconfig('username'), a = self.engine.api_info['shortname'], mt = self.engine.api_info['mediatype'], s = self.engine.mediainfo['statuses_dict'][self.filter_num].lower().replace(' ', ''), c_r = _PCOLOR_RESET, c_u = _PCOLOR_USER, c_a = _PCOLOR_API, c_mt = _PCOLOR_MEDIATYPE, c_s = _COLOR_RESET ) def _load_list(self, *args): showlist = self.engine.filter_list(self.filter_num) sortedlist = sorted(showlist, key=itemgetter(self.sort)) self.sortedlist = list(enumerate(sortedlist, 1)) def _get_show(self, title): # Attempt parsing list index # otherwise use title try: index = int(title)-1 return self.sortedlist[index][1] except (ValueError, AttributeError, IndexError): return self.engine.get_show_info(title=title) def _ask_update(self, show, episode): do = input("Should I update {} to episode {}? [y/N] ".format(show['title'], episode)) if do.lower() == 'y': self.engine.set_episode(show['id'], episode) def _ask_add(self, show_title, episode): do = input("Should I search for the show {}? [y/N] ".format(show_title)) if do.lower() == 'y': self.do_add([show_title]) def start(self): """ Initializes the engine Creates an Engine object and starts it. """ if self.interactive: print('Initializing engine...') self.engine = Engine(self.account, self.messagehandler) else: self.engine = Engine(self.account) self.engine.set_config("tracker_enabled", False) self.engine.set_config("library_autoscan", False) self.engine.set_config("use_hooks", False) self.engine.connect_signal('show_added', self._load_list) self.engine.connect_signal('show_deleted', self._load_list) self.engine.connect_signal('status_changed', self._load_list) self.engine.connect_signal('episode_changed', self._load_list) self.engine.connect_signal('prompt_for_update', self._ask_update) self.engine.connect_signal('prompt_for_add', self._ask_add) self.engine.start() # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() if self.interactive: self._update_prompt() print() print("Ready. Type 'help' for a list of commands.") print("Press tab for autocompletion and up/down for command history.") self.do_filter(None) # Show available filters print() else: # We set the message handler only after initializing # so we still receive the important messages but avoid # the initial spam. self.engine.set_message_handler(self.messagehandler) def do_about(self, args): print("Trackma {} by z411 ([email protected])".format(utils.VERSION)) print("Trackma is an open source client for media tracking websites.") print("https://github.com/z411/trackma") print() print("This program is licensed under the GPLv3 and it comes with ASOLUTELY NO WARRANTY.") print("Many contributors have helped to run this project; for more information see the AUTHORS file.") print("For more information about the license, see the COPYING file.") print() print("If you encounter any problems please report them in https://github.com/z411/trackma/issues") print() print("This is the CLI version of Trackma. To see available commands type `help'.") print("For other available interfaces please see the README file.") print() def do_help(self, arg): if arg: try: doc = getattr(self, 'do_' + arg).__doc__ if doc: (name, args, expl, usage, examples) = self._parse_doc(arg, doc) print() print(name) for line in expl: print(" {}".format(line)) if args: print("\n Arguments:") for arg in args: if arg[2]: print(" {}: {}".format(arg[0], arg[1])) else: print(" {} (optional): {}".format(arg[0], arg[1])) if usage: print("\n Usage: " + usage) for example in examples: print(" Example: " + example) print() return except AttributeError: pass print("No help available.") return else: CMD_LENGTH = 11 ARG_LENGTH = 13 (height, width) = utils.get_terminal_size() prev_width = CMD_LENGTH + ARG_LENGTH + 3 tw = textwrap.TextWrapper() tw.width = width - 2 tw.subsequent_indent = ' ' * prev_width print() print(" {0:>{1}} {2:{3}} {4}".format( 'command', CMD_LENGTH, 'args', ARG_LENGTH, 'description')) print(" " + "-"*(min(prev_width+81, width-3))) names = self.get_names() names.sort() for name in names: if name[:3] == 'do_': doc = getattr(self, name).__doc__ if not doc: continue cmd = name[3:] (name, args, expl, usage, examples) = self._parse_doc(cmd, doc) line = " {0:>{1}} {2:{3}} {4}".format( name, CMD_LENGTH, '<' + ','.join( a[0] for a in args) + '>', ARG_LENGTH, expl[0]) print(tw.fill(line)) print() print("Use `help <command>` for detailed information.") print() def do_account(self, args): """ Switch to a different account. """ self.account = self.accountman.select_account(True) self.engine.reload(account=self.account) # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt() def do_filter(self, args): """ Changes the filtering of list by status (shows current if empty). :optparam status Name of status to filter :usage filter [filter type] """ # Query the engine for the available statuses # that the user can choose if args: try: self.filter_num = self._guess_status(args[0].lower()) self._load_list() self._update_prompt() except KeyError: print("Invalid filter.") else: print("Available statuses: %s" % ', '.join( v.lower().replace(' ', '') for v in self.engine.mediainfo['statuses_dict'].values() )) def do_sort(self, args): """ Change of the lists :param type Sort type; available types: id, title, my_progress, total, my_score :usage sort <sort type> """ sorts = ('id', 'title', 'my_progress', 'total', 'my_score') if args[0] in sorts: self.sort = args[0] self._load_list() else: print("Invalid sort.") def do_mediatype(self, args): """ Reloads engine with different mediatype (shows current if empty). Call with no arguments to see supported mediatypes. :optparam mediatype Mediatype name :usage mediatype [mediatype] """ if args: if args[0] in self.engine.api_info['supported_mediatypes']: self.engine.reload(mediatype=args[0]) # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt() else: print("Invalid mediatype.") else: print("Supported mediatypes: %s" % ', '.join(self.engine.api_info['supported_mediatypes'])) def do_ls(self,args): self.do_list(args) def do_list(self, args): """ Lists all shows available in the local list. :name list|ls """ # Show the list in memory self._make_list(self.sortedlist) def do_info(self, args): """ Gets detailed information about a local show. :param show Show index or title. :usage info <show index or title> """ try: show = self._get_show(args[0]) details = self.engine.get_show_details(show) except utils.TrackmaError as e: self.display_error(e) return print(show['title']) print("-" * len(show['title'])) print(show['url']) print() for line in details['extra']: print("%s: %s" % line) def do_search(self, args): """ Does a regex search on shows in the local lists. :param pattern Regex pattern to search for. :usage search <pattern> """ sortedlist = list(v for v in self.sortedlist if re.search(args[0], v[1]['title'], re.I)) self._make_list(sortedlist) def do_add(self, args): """ Search for a show in the remote service and add it. :param pattern Show criteria to search. :usage add <pattern> """ try: entries = self.engine.search(args[0]) except utils.TrackmaError as e: self.display_error(e) return for i, entry in enumerate(entries, start=1): print("%d: (%s) %s" % (i, entry['type'], entry['title'])) do_update = input("Choose show to add (blank to cancel): ") if do_update != '': try: show = entries[int(do_update)-1] except ValueError: print("Choice must be numeric.") return except IndexError: print("Invalid show.") return # Tell the engine to add the show try: self.engine.add_show(show, self.filter_num) except utils.TrackmaError as e: self.display_error(e) def do_del(self, args): self.do_delete(args) def do_delete(self, args): """ Deletes a show from the local list. :name delete|del :param show Show index or title. :usage delete <show index or title> """ try: show = self._get_show(args[0]) do_delete = input("Delete %s? [y/N] " % show['title']) if do_delete.lower() == 'y': self.engine.delete_show(show) except utils.TrackmaError as e: self.display_error(e) def do_rescan(self, args): """ Re-scans the local library. """ self.engine.scan_library(rescan=True) def do_random(self, args): """ Starts the media player with a random new episode. """ try: self.engine.play_random() except utils.TrackmaError as e: self.display_error(e) def do_tracker(self, args): """ Shows information about the tracker, if it's running. :usage trackmer """ try: info = self.engine.tracker_status() print("- Tracker status -") if info: if info['state'] == utils.TRACKER_NOVIDEO: state = 'No video' elif info['state'] == utils.TRACKER_PLAYING: state = 'Playing' elif info['state'] == utils.TRACKER_UNRECOGNIZED: state = 'Unrecognized' elif info['state'] == utils.TRACKER_NOT_FOUND: state = 'Not found' elif info['state'] == utils.TRACKER_IGNORED: state = 'Ignored' else: state = 'N/A' print("State: {}".format(state)) print("Filename: {}".format(info['filename'] or 'N/A')) print("Timer: {}{}".format(info['timer'] or 'N/A', ' [P]' if info['paused'] else '')) if info['show']: (show, ep) = info['show'] print("Show: {}\nEpisode: {}".format(show['title'], ep)) else: print("Show: N/A") else: print("Not started") except utils.TrackmaError as e: self.display_error(e) def do_play(self, args): """ Starts the media player with the specified episode number (next if unspecified). :param show Episode index or title. :optparam ep Episode number. Assume next if not specified. :usage play <show index or title> [episode number] """ try: episode = 0 show = self._get_show(args[0]) # If the user specified an episode, play it # otherwise play the next episode not watched yet if len(args) > 1: episode = args[1] self.engine.play_episode(show, episode) except utils.TrackmaError as e: self.display_error(e) def do_openfolder(self, args): """ Opens the folder containing the show :param show Show index or name. :usage openfolder <show index or name> """ try: show = self._get_show(args[0]) filename = self.engine.get_episode_path(show, 1) with open(os.devnull, 'wb') as DEVNULL: subprocess.Popen(["/usr/bin/xdg-open", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) except OSError: # xdg-open failed. self.display_error("Could not open folder.") except utils.TrackmaError as e: self.display_error(e) def do_update(self, args): """ Updates the progress of a show to the specified episode (next if unspecified). :param show Show index, title or filename (prepend with file:). :optparam ep Episode number (numeric). :usage update <show index or name> [episode number] :example update Toradora! 5 :example update 6 :example update file:filename.mkv """ try: if args[0][:5] == "file:": (show, ep) = self.engine.get_show_info(filename=args[0][5:]) else: (show, ep) = (self._get_show(args[0]), None) if len(args) > 1: self.engine.set_episode(show['id'], args[1]) else: self.engine.set_episode(show['id'], ep or show['my_progress']+1) except IndexError: print("Missing arguments.") except utils.TrackmaError as e: self.display_error(e) def do_score(self, args): """ Changes the score of a show. :param show Show index or name. :param score Score to set (numeric/decimal). :usage score <show index or name> <score> """ try: show = self._get_show(args[0]) self.engine.set_score(show['id'], args[1]) except IndexError: print("Missing arguments.") except utils.TrackmaError as e: self.display_error(e) def do_status(self, args): """ Changes the status of a show. Use the command `filter` without arguments to see the available statuses. :param show Show index or name. :param status Status name. Use `filter` without args to list them. :usage status <show index or name> <status name> """ try: _showtitle = args[0] _filter = args[1] except IndexError: print("Missing arguments.") return try: _filter_num = self._guess_status(_filter) except KeyError: print("Invalid filter.") return try: show = self._get_show(_showtitle) self.engine.set_status(show['id'], _filter_num) except utils.TrackmaError as e: self.display_error(e) def do_altname(self, args): """ Changes the alternative name of a show (removes if unspecified). Use the command 'altname' without arguments to clear the alternative name. :param show Show index or name :param alt The alternative name. Use `altname` without alt to clear it :usage altname <show index or name> <alternative name> """ try: show = self._get_show(args[0]) altname = args[1] if len(args) > 1 else '' self.engine.altname(show['id'],altname) except IndexError: print("Missing arguments") return except utils.TrackmaError as e: self.display_error(e) def do_send(self, args): """ Sends queued changes to the remote service. """ try: self.engine.list_upload() except utils.TrackmaError as e: self.display_error(e) def do_retrieve(self, args): """ Retrieves the remote list overwrites the local one. """ try: if self.engine.get_queue(): answer = input("There are unqueued changes. Overwrite local list? [y/N] ") if answer.lower() == 'y': self.engine.list_download() else: self.engine.list_download() self._load_list() except utils.TrackmaError as e: self.display_error(e) def do_undoall(self, args): """ Undo all changes in queue. """ try: self.engine.undoall() except utils.TrackmaError as e: self.display_error(e) def do_viewqueue(self, args): """ List the queued changes. """ queue = self.engine.get_queue() if queue: print("Queue:") for show in queue: print("- %s" % show['title']) else: print("Queue is empty.") def do_exit(self, args): self.do_quit(args) def do_quit(self, args): """ Quits the program. :name quit|exit """ try: self.engine.unload() except utils.TrackmaError as e: self.display_error(e) print('Bye!') sys.exit(0) def do_EOF(self, args): print() self.do_quit(args) def complete_update(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_play(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_score(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_status(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_delete(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_filter(self, text, line, begidx, endidx): return [v.lower().replace(' ', '') for v in self.engine.mediainfo['statuses_dict'].values()] def parse_args(self, arg): if arg: return shlex.split(arg) return [] def emptyline(self): return def preloop(self): """ Override. """ self.in_prompt = True def precmd(self, line): """ Override. """ self.in_prompt = False return line def postcmd(self, stop, line): """ Override. """ self.in_prompt = True return stop def onecmd(self, line): """ Override. """ cmd, arg, line = self.parseline(line) if not line: return self.emptyline() if cmd is None: return self.default(line) self.lastcmd = line if line == 'EOF' : self.lastcmd = '' if cmd == '': return self.default(line) elif cmd == 'help': return self.do_help(arg) else: return self.execute(cmd, self.parse_args(arg), line) def execute(self, cmd, args, line): try: func = getattr(self, 'do_' + cmd) except AttributeError: return self.default(line) try: needed = self.needed_args[cmd] except KeyError: needed = 0 if isinstance(needed, int): needed = (needed, needed) if needed[0] <= len(args) <= needed[1]: return func(args) else: print("Incorrent number of arguments. See `help %s`" % cmd) def display_error(self, e): print("%s%s: %s%s" % (_COLOR_ERROR, type(e).__name__, e, _COLOR_RESET)) def messagehandler(self, classname, msgtype, msg): """ Handles and shows messages coming from the engine messenger to provide feedback. """ color_escape = '' color_reset = _COLOR_RESET if classname == 'Engine': color_escape = _COLOR_ENGINE elif classname == 'Data': color_escape = _COLOR_DATA elif classname.startswith('lib'): color_escape = _COLOR_API elif classname.startswith('Tracker'): color_escape = _COLOR_TRACKER else: color_reset = '' if msgtype == messenger.TYPE_INFO: out = "%s%s: %s%s" % (color_escape, classname, msg, color_reset) elif msgtype == messenger.TYPE_WARN: out = "%s%s warning: %s%s" % (color_escape, classname, msg, color_reset) elif self.debug and msgtype == messenger.TYPE_DEBUG: out = "[D] %s%s: %s%s" % (color_escape, classname, msg, color_reset) else: return # Unrecognized message, don't show anything if has_readline and self.in_prompt: # If we're in a prompt and receive a message # (often from the tracker) we need to clear the line # first, show the message, then re-show the prompt. buf = readline.get_line_buffer() self.stdout.write('\r' + ' '*(len(self.prompt)+len(buf)) + '\r') print(out) self.stdout.write(self.prompt + buf) self.stdout.flush() else: print(out) def _guess_status(self, string): for k, v in self.engine.mediainfo['statuses_dict'].items(): if string.lower() == v.lower().replace(' ', ''): return k raise KeyError def _parse_doc(self, cmd, doc): lines = doc.split('\n') name = cmd args = [] expl = [] usage = None examples = [] for line in lines: line = line.strip() if line[:6] == ":param": args.append( line[7:].split(' ', 1) + [True] ) elif line[:9] == ":optparam": args.append( line[10:].split(' ', 1) + [False] ) elif line[:6] == ':usage': usage = line[7:] elif line[:5] == ':name': name = line[6:] elif line[:8] == ':example': examples.append(line[9:]) elif line: expl.append(line) return (name, args, expl, usage, examples) def _make_list(self, showlist): """ Helper function for printing a formatted show list """ # Fixed column widths col_id_length = 7 col_index_length = 6 col_title_length = 5 col_episodes_length = 9 col_score_length = 6 altnames = self.engine.altnames() # Calculate maximum width for the title column # based on the width of the terminal (height, width) = utils.get_terminal_size() max_title_length = width - col_id_length - col_episodes_length - col_score_length - col_index_length - 5 # Find the widest title so we can adjust the title column for index, show in showlist: if len(show['title']) > col_title_length: if len(show['title']) > max_title_length: # Stop if we exceeded the maximum column width col_title_length = max_title_length break else: col_title_length = len(show['title']) # Print header print("| {0:{1}} {2:{3}} {4:{5}} {6:{7}} |".format( 'Index', col_index_length, 'Title', max_title_length, 'Progress', col_episodes_length, 'Score', col_score_length)) # List shows for index, show in showlist: if self.engine.mediainfo['has_progress']: episodes_str = "{0:3} / {1}".format(show['my_progress'], show['total'] or '?') else: episodes_str = "-" #Get title (and alt. title) and if need be, truncate it title_str = show['title'] if altnames.get(show['id']): title_str += " [{}]".format(altnames.get(show['id'])) title_str = title_str[:max_title_length] if len(title_str) > max_title_length else title_str # Color title according to status if show['status'] == utils.STATUS_AIRING: colored_title = _COLOR_AIRING + title_str + _COLOR_RESET else: colored_title = title_str print("| {0:^{1}} {2}{3} {4:{5}} {6:^{7}} |".format( index, col_index_length, colored_title, '.' * (max_title_length-len(title_str)), episodes_str, col_episodes_length, show['my_score'], col_score_length)) # Print result count print('%d results' % len(showlist)) print()
class Trackma_cmd(cmd.Cmd): """ Main program, inherits from the useful Cmd class for interactive console """ engine = None filter_num = 1 sort = 'title' completekey = 'Tab' cmdqueue = [] stdout = sys.stdout in_prompt = False sortedlist = [] needed_args = { 'altname': (1, 2), 'filter': (0, 1), 'sort': 1, 'mediatype': (0, 1), 'info': 1, 'search': 1, 'add': 1, 'del': 1, 'delete': 1, 'play': (1, 2), 'openfolder': 1, 'update': (1, 2), 'score': 2, 'status': 2, } def __init__(self, account_num=None, debug=False, interactive=True): super().__init__() if interactive: print('Trackma v'+utils.VERSION+' Copyright (C) 2012-2017 z411') print('This program comes with ABSOLUTELY NO WARRANTY; for details type `about\'') print('This is free software, and you are welcome to redistribute it') print('under certain conditions; see the COPYING file for details.') print() self.interactive = interactive self.debug = debug self.accountman = Trackma_accounts() if account_num: try: self.account = self.accountman.get_account(account_num) except KeyError: print("Account {} doesn't exist.".format(account_num)) self.account = self.accountman.select_account(True) except ValueError: print("Account {} must be numeric.".format(account_num)) self.account = self.accountman.select_account(True) else: self.account = self.accountman.select_account(False) def forget_account(self): self.accountman.set_default(None) def _update_prompt(self): self.prompt = "{c_u}{u}{c_r} [{c_a}{a}{c_r}] ({c_mt}{mt}{c_r}) {c_s}{s}{c_r} >> ".format( u = self.engine.get_userconfig('username'), a = self.engine.api_info['shortname'], mt = self.engine.api_info['mediatype'], s = self.engine.mediainfo['statuses_dict'][self.filter_num].lower().replace(' ', ''), c_r = _PCOLOR_RESET, c_u = _PCOLOR_USER, c_a = _PCOLOR_API, c_mt = _PCOLOR_MEDIATYPE, c_s = _COLOR_RESET ) def _load_list(self, *args): showlist = self.engine.filter_list(self.filter_num) sortedlist = sorted(showlist, key=itemgetter(self.sort)) self.sortedlist = list(enumerate(sortedlist, 1)) def _get_show(self, title): # Attempt parsing list index # otherwise use title try: index = int(title)-1 return self.sortedlist[index][1] except (ValueError, AttributeError, IndexError): return self.engine.get_show_info(title=title) def _ask_update(self, show, episode): do = input("Should I update {} to episode {}? [y/N] ".format(show['title'], episode)) if do.lower() == 'y': self.engine.set_episode(show['id'], episode) def _ask_add(self, show, episode): do = input("Should I search for the show {}? [y/N] ".format(show['title'])) if do.lower() == 'y': self.do_add([show['title']]) def start(self): """ Initializes the engine Creates an Engine object and starts it. """ if self.interactive: print('Initializing engine...') self.engine = Engine(self.account, self.messagehandler) else: self.engine = Engine(self.account) self.engine.set_config("tracker_enabled", False) self.engine.set_config("library_autoscan", False) self.engine.set_config("use_hooks", False) self.engine.connect_signal('show_added', self._load_list) self.engine.connect_signal('show_deleted', self._load_list) self.engine.connect_signal('status_changed', self._load_list) self.engine.connect_signal('episode_changed', self._load_list) self.engine.connect_signal('prompt_for_update', self._ask_update) self.engine.connect_signal('prompt_for_add', self._ask_add) self.engine.start() # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() if self.interactive: self._update_prompt() print() print("Ready. Type 'help' for a list of commands.") print("Press tab for autocompletion and up/down for command history.") self.do_filter(None) # Show available filters print() else: # We set the message handler only after initializing # so we still receive the important messages but avoid # the initial spam. self.engine.set_message_handler(self.messagehandler) def do_about(self, args): print("Trackma {} by z411 ([email protected])".format(utils.VERSION)) print("Trackma is an open source client for media tracking websites.") print("https://github.com/z411/trackma") print() print("This program is licensed under the GPLv3 and it comes with ASOLUTELY NO WARRANTY.") print("Many contributors have helped to run this project; for more information see the AUTHORS file.") print("For more information about the license, see the COPYING file.") print() print("If you encounter any problems please report them in https://github.com/z411/trackma/issues") print() print("This is the CLI version of Trackma. To see available commands type `help'.") print("For other available interfaces please see the README file.") print() def do_help(self, arg): if arg: try: doc = getattr(self, 'do_' + arg).__doc__ if doc: (name, args, expl, usage, examples) = self._parse_doc(arg, doc) print() print(name) for line in expl: print(" {}".format(line)) if args: print("\n Arguments:") for arg in args: if arg[2]: print(" {}: {}".format(arg[0], arg[1])) else: print(" {} (optional): {}".format(arg[0], arg[1])) if usage: print("\n Usage: " + usage) for example in examples: print(" Example: " + example) print() return except AttributeError: pass print("No help available.") return else: CMD_LENGTH = 11 ARG_LENGTH = 13 (height, width) = utils.get_terminal_size() prev_width = CMD_LENGTH + ARG_LENGTH + 3 tw = textwrap.TextWrapper() tw.width = width - 2 tw.subsequent_indent = ' ' * prev_width print() print(" {0:>{1}} {2:{3}} {4}".format( 'command', CMD_LENGTH, 'args', ARG_LENGTH, 'description')) print(" " + "-"*(min(prev_width+81, width-3))) names = self.get_names() names.sort() for name in names: if name[:3] == 'do_': doc = getattr(self, name).__doc__ if not doc: continue cmd = name[3:] (name, args, expl, usage, examples) = self._parse_doc(cmd, doc) line = " {0:>{1}} {2:{3}} {4}".format( name, CMD_LENGTH, '<' + ','.join( a[0] for a in args) + '>', ARG_LENGTH, expl[0]) print(tw.fill(line)) print() print("Use `help <command>` for detailed information.") print() def do_account(self, args): """ Switch to a different account. """ self.account = self.accountman.select_account(True) self.engine.reload(account=self.account) # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt() def do_filter(self, args): """ Changes the filtering of list by status (shows current if empty). :optparam status Name of status to filter :usage filter [filter type] """ # Query the engine for the available statuses # that the user can choose if args: try: self.filter_num = self._guess_status(args[0].lower()) self._load_list() self._update_prompt() except KeyError: print("Invalid filter.") else: print("Available statuses: %s" % ', '.join( v.lower().replace(' ', '') for v in self.engine.mediainfo['statuses_dict'].values() )) def do_sort(self, args): """ Change of the lists :param type Sort type; available types: id, title, my_progress, total, my_score :usage sort <sort type> """ sorts = ('id', 'title', 'my_progress', 'total', 'my_score') if args[0] in sorts: self.sort = args[0] self._load_list() else: print("Invalid sort.") def do_mediatype(self, args): """ Reloads engine with different mediatype (shows current if empty). Call with no arguments to see supported mediatypes. :optparam mediatype Mediatype name :usage mediatype [mediatype] """ if args: if args[0] in self.engine.api_info['supported_mediatypes']: self.engine.reload(mediatype=args[0]) # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt() else: print("Invalid mediatype.") else: print("Supported mediatypes: %s" % ', '.join(self.engine.api_info['supported_mediatypes'])) def do_ls(self,args): self.do_list(args) def do_list(self, args): """ Lists all shows available in the local list. :name list|ls """ # Show the list in memory self._make_list(self.sortedlist) def do_info(self, args): """ Gets detailed information about a local show. :param show Show index or title. :usage info <show index or title> """ try: show = self._get_show(args[0]) details = self.engine.get_show_details(show) except utils.TrackmaError as e: self.display_error(e) return print(show['title']) print("-" * len(show['title'])) print(show['url']) print() for line in details['extra']: print("%s: %s" % line) def do_search(self, args): """ Does a regex search on shows in the local lists. :param pattern Regex pattern to search for. :usage search <pattern> """ sortedlist = list(v for v in self.sortedlist if re.search(args[0], v[1]['title'], re.I)) self._make_list(sortedlist) def do_add(self, args): """ Search for a show in the remote service and add it. :param pattern Show criteria to search. :usage add <pattern> """ try: entries = self.engine.search(args[0]) except utils.TrackmaError as e: self.display_error(e) return for i, entry in enumerate(entries, start=1): print("%d: (%s) %s" % (i, entry['type'], entry['title'])) do_update = input("Choose show to add (blank to cancel): ") if do_update != '': try: show = entries[int(do_update)-1] except ValueError: print("Choice must be numeric.") return except IndexError: print("Invalid show.") return # Tell the engine to add the show try: self.engine.add_show(show, self.filter_num) except utils.TrackmaError as e: self.display_error(e) def do_del(self, args): self.do_delete(args) def do_delete(self, args): """ Deletes a show from the local list. :name delete|del :param show Show index or title. :usage delete <show index or title> """ try: show = self._get_show(args[0]) do_delete = input("Delete %s? [y/N] " % show['title']) if do_delete.lower() == 'y': self.engine.delete_show(show) except utils.TrackmaError as e: self.display_error(e) def do_rescan(self, args): """ Re-scans the local library. """ self.engine.scan_library(rescan=True) def do_random(self, args): """ Starts the media player with a random new episode. """ try: self.engine.play_random() except utils.TrackmaError as e: self.display_error(e) def do_tracker(self, args): """ Shows information about the tracker, if it's running. :usage trackmer """ try: info = self.engine.tracker_status() print("- Tracker status -") if info: if info['state'] == utils.TRACKER_NOVIDEO: state = 'No video' elif info['state'] == utils.TRACKER_PLAYING: state = 'Playing' elif info['state'] == utils.TRACKER_UNRECOGNIZED: state = 'Unrecognized' elif info['state'] == utils.TRACKER_NOT_FOUND: state = 'Not found' elif info['state'] == utils.TRACKER_IGNORED: state = 'Ignored' else: state = 'N/A' print("State: {}".format(state)) print("Filename: {}".format(info['filename'] or 'N/A')) print("Timer: {}{}".format(info['timer'] or 'N/A', ' [P]' if info['paused'] else '')) if info['show']: (show, ep) = info['show'] print("Show: {}\nEpisode: {}".format(show['title'], ep)) else: print("Show: N/A") else: print("Not started") except utils.TrackmaError as e: self.display_error(e) def do_play(self, args): """ Starts the media player with the specified episode number (next if unspecified). :param show Episode index or title. :optparam ep Episode number. Assume next if not specified. :usage play <show index or title> [episode number] """ try: episode = 0 show = self._get_show(args[0]) # If the user specified an episode, play it # otherwise play the next episode not watched yet if len(args) > 1: episode = args[1] self.engine.play_episode(show, episode) except utils.TrackmaError as e: self.display_error(e) def do_openfolder(self, args): """ Opens the folder containing the show :param show Show index or name. :usage openfolder <show index or name> """ try: show = self._get_show(args[0]) filename = self.engine.get_episode_path(show, 1) with open(os.devnull, 'wb') as DEVNULL: subprocess.Popen(["/usr/bin/xdg-open", os.path.dirname(filename)], stdout=DEVNULL, stderr=DEVNULL) except OSError: # xdg-open failed. self.display_error("Could not open folder.") except utils.TrackmaError as e: self.display_error(e) def do_update(self, args): """ Updates the progress of a show to the specified episode (next if unspecified). :param show Show index, title or filename (prepend with file:). :optparam ep Episode number (numeric). :usage update <show index or name> [episode number] :example update Toradora! 5 :example update 6 :example update file:filename.mkv """ try: if args[0][:5] == "file:": (show, ep) = self.engine.get_show_info(filename=args[0][5:]) else: (show, ep) = (self._get_show(args[0]), None) if len(args) > 1: self.engine.set_episode(show['id'], args[1]) else: self.engine.set_episode(show['id'], ep or show['my_progress']+1) except IndexError: print("Missing arguments.") except utils.TrackmaError as e: self.display_error(e) def do_score(self, args): """ Changes the score of a show. :param show Show index or name. :param score Score to set (numeric/decimal). :usage score <show index or name> <score> """ try: show = self._get_show(args[0]) self.engine.set_score(show['id'], args[1]) except IndexError: print("Missing arguments.") except utils.TrackmaError as e: self.display_error(e) def do_status(self, args): """ Changes the status of a show. Use the command `filter` without arguments to see the available statuses. :param show Show index or name. :param status Status name. Use `filter` without args to list them. :usage status <show index or name> <status name> """ try: _showtitle = args[0] _filter = args[1] except IndexError: print("Missing arguments.") return try: _filter_num = self._guess_status(_filter) except KeyError: print("Invalid filter.") return try: show = self._get_show(_showtitle) self.engine.set_status(show['id'], _filter_num) except utils.TrackmaError as e: self.display_error(e) def do_altname(self, args): """ Changes the alternative name of a show (removes if unspecified). Use the command 'altname' without arguments to clear the alternative name. :param show Show index or name :param alt The alternative name. Use `altname` without alt to clear it :usage altname <show index or name> <alternative name> """ try: show = self._get_show(args[0]) altname = args[1] if len(args) > 1 else '' self.engine.altname(show['id'],altname) except IndexError: print("Missing arguments") return except utils.TrackmaError as e: self.display_error(e) def do_send(self, args): """ Sends queued changes to the remote service. """ try: self.engine.list_upload() except utils.TrackmaError as e: self.display_error(e) def do_retrieve(self, args): """ Retrieves the remote list overwrites the local one. """ try: if self.engine.get_queue(): answer = input("There are unqueued changes. Overwrite local list? [y/N] ") if answer.lower() == 'y': self.engine.list_download() else: self.engine.list_download() self._load_list() except utils.TrackmaError as e: self.display_error(e) def do_undoall(self, args): """ Undo all changes in queue. """ try: self.engine.undoall() except utils.TrackmaError as e: self.display_error(e) def do_viewqueue(self, args): """ List the queued changes. """ queue = self.engine.get_queue() if queue: print("Queue:") for show in queue: print("- %s" % show['title']) else: print("Queue is empty.") def do_exit(self, args): self.do_quit(args) def do_quit(self, args): """ Quits the program. :name quit|exit """ try: self.engine.unload() except utils.TrackmaError as e: self.display_error(e) print('Bye!') sys.exit(0) def do_EOF(self, args): print() self.do_quit(args) def complete_update(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_play(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_score(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_status(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_delete(self, text, line, begidx, endidx): if text: return self.engine.regex_list_titles(text) def complete_filter(self, text, line, begidx, endidx): return [v.lower().replace(' ', '') for v in self.engine.mediainfo['statuses_dict'].values()] def parse_args(self, arg): if arg: return shlex.split(arg) return [] def emptyline(self): return def preloop(self): """ Override. """ self.in_prompt = True def precmd(self, line): """ Override. """ self.in_prompt = False return line def postcmd(self, stop, line): """ Override. """ self.in_prompt = True return stop def onecmd(self, line): """ Override. """ cmd, arg, line = self.parseline(line) if not line: return self.emptyline() if cmd is None: return self.default(line) self.lastcmd = line if line == 'EOF' : self.lastcmd = '' if cmd == '': return self.default(line) elif cmd == 'help': return self.do_help(arg) else: try: args = self.parse_args(arg) except ValueError: return self.default(line) else: return self.execute(cmd, args, line) def execute(self, cmd, args, line): try: func = getattr(self, 'do_' + cmd) except AttributeError: return self.default(line) try: needed = self.needed_args[cmd] except KeyError: needed = 0 if isinstance(needed, int): needed = (needed, needed) if needed[0] <= len(args) <= needed[1]: return func(args) else: print("Incorrent number of arguments. See `help %s`" % cmd) def display_error(self, e): print("%s%s: %s%s" % (_COLOR_ERROR, type(e).__name__, e, _COLOR_RESET)) def messagehandler(self, classname, msgtype, msg): """ Handles and shows messages coming from the engine messenger to provide feedback. """ color_escape = '' color_reset = _COLOR_RESET if classname == 'Engine': color_escape = _COLOR_ENGINE elif classname == 'Data': color_escape = _COLOR_DATA elif classname.startswith('lib'): color_escape = _COLOR_API elif classname.startswith('Tracker'): color_escape = _COLOR_TRACKER else: color_reset = '' if msgtype == messenger.TYPE_INFO: out = "%s%s: %s%s" % (color_escape, classname, msg, color_reset) elif msgtype == messenger.TYPE_WARN: out = "%s%s warning: %s%s" % (color_escape, classname, msg, color_reset) elif self.debug and msgtype == messenger.TYPE_DEBUG: out = "[D] %s%s: %s%s" % (color_escape, classname, msg, color_reset) else: return # Unrecognized message, don't show anything if has_readline and self.in_prompt: # If we're in a prompt and receive a message # (often from the tracker) we need to clear the line # first, show the message, then re-show the prompt. buf = readline.get_line_buffer() self.stdout.write('\r' + ' '*(len(self.prompt)+len(buf)) + '\r') print(out) self.stdout.write(self.prompt + buf) self.stdout.flush() else: print(out) def _guess_status(self, string): for k, v in self.engine.mediainfo['statuses_dict'].items(): if string.lower() == v.lower().replace(' ', ''): return k raise KeyError def _parse_doc(self, cmd, doc): lines = doc.split('\n') name = cmd args = [] expl = [] usage = None examples = [] for line in lines: line = line.strip() if line[:6] == ":param": args.append( line[7:].split(' ', 1) + [True] ) elif line[:9] == ":optparam": args.append( line[10:].split(' ', 1) + [False] ) elif line[:6] == ':usage': usage = line[7:] elif line[:5] == ':name': name = line[6:] elif line[:8] == ':example': examples.append(line[9:]) elif line: expl.append(line) return (name, args, expl, usage, examples) def _make_list(self, showlist): """ Helper function for printing a formatted show list """ # Fixed column widths col_id_length = 7 col_index_length = 6 col_title_length = 5 col_episodes_length = 9 col_score_length = 6 altnames = self.engine.altnames() # Calculate maximum width for the title column # based on the width of the terminal (height, width) = utils.get_terminal_size() max_title_length = width - col_id_length - col_episodes_length - col_score_length - col_index_length - 5 # Find the widest title so we can adjust the title column for index, show in showlist: if len(show['title']) > col_title_length: if len(show['title']) > max_title_length: # Stop if we exceeded the maximum column width col_title_length = max_title_length break else: col_title_length = len(show['title']) # Print header print("| {0:{1}} {2:{3}} {4:{5}} {6:{7}} |".format( 'Index', col_index_length, 'Title', max_title_length, 'Progress', col_episodes_length, 'Score', col_score_length)) # List shows for index, show in showlist: if self.engine.mediainfo['has_progress']: episodes_str = "{0:3} / {1}".format(show['my_progress'], show['total'] or '?') else: episodes_str = "-" #Get title (and alt. title) and if need be, truncate it title_str = show['title'] if altnames.get(show['id']): title_str += " [{}]".format(altnames.get(show['id'])) title_str = title_str[:max_title_length] if len(title_str) > max_title_length else title_str # Color title according to status if show['status'] == utils.STATUS_AIRING: colored_title = _COLOR_AIRING + title_str + _COLOR_RESET else: colored_title = title_str print("| {0:^{1}} {2}{3} {4:{5}} {6:^{7}} |".format( index, col_index_length, colored_title, '.' * (max_title_length-len(title_str)), episodes_str, col_episodes_length, show['my_score'], col_score_length)) # Print result count print('%d results' % len(showlist)) print()