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 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.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() 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 __init__(self, accountnum=1): """ Defaults to the first account. """ self.watch = Watcher() self.accs = dict(AccountManager().get_accounts()) self.engine = Engine(self.accs.get(accountnum)) self.engine.start() self.tList = list(self.engine.get_list()) with open(self.watch.WATCH_FILE, 'r') as watch_file: self.adList = list(json.load(watch_file)) watch_file.close() self._sort_lists()
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 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.start() # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt()
def _start(self, account): self.engine = Engine(account, self._messagehandler) self.engine.connect_signal('episode_changed', self._changed_show) self.engine.connect_signal('score_changed', self._changed_show) self.engine.connect_signal('tags_changed', self._changed_show) self.engine.connect_signal('status_changed', self._changed_show_status) self.engine.connect_signal('playing', self._playing_show) self.engine.connect_signal('show_added', self._changed_list) self.engine.connect_signal('show_deleted', self._changed_list) self.engine.connect_signal('show_synced', self._changed_show) self.engine.connect_signal('queue_changed', self._changed_queue) self.engine.connect_signal('prompt_for_update', self._prompt_for_update) self.engine.connect_signal('prompt_for_add', self._prompt_for_add) self.engine.connect_signal('tracker_state', self._tracker_state) self.engine.start()
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 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 start(self, account): """Starts the engine""" # Engine configuration self.started = False self.status("Starting engine...") self.engine = Engine(account, self.message_handler) self.engine.connect_signal('episode_changed', self.changed_show) self.engine.connect_signal('score_changed', self.changed_show) self.engine.connect_signal('status_changed', self.changed_show_status) self.engine.connect_signal('playing', self.playing_show) self.engine.connect_signal('show_added', self.changed_list) self.engine.connect_signal('show_deleted', self.changed_list) self.engine.connect_signal('show_synced', self.changed_show) self.engine.connect_signal('queue_changed', self.changed_queue) self.engine.connect_signal('prompt_for_update', self.prompt_update) self.engine.connect_signal('tracker_state', self.tracker_state) # Engine start and list rebuildi self.status("Building lists...") self.engine.start() self._rebuild()
def start(self, account): """Starts the engine""" # Engine configuration self.started = False self.status("Starting engine...") self.engine = Engine(account, self.message_handler) self.engine.connect_signal("episode_changed", self.changed_show) self.engine.connect_signal("score_changed", self.changed_show) self.engine.connect_signal("status_changed", self.changed_show_status) self.engine.connect_signal("playing", self.playing_show) self.engine.connect_signal("show_added", self.changed_list) self.engine.connect_signal("show_deleted", self.changed_list) self.engine.connect_signal("show_synced", self.changed_show) self.engine.connect_signal("prompt_for_update", self.prompt_update) # Engine start and list rebuildi self.status("Building lists...") self.engine.start() self._rebuild()
class EngineWorker(QtCore.QThread): """ Worker thread Contains the engine and manages every process in a separate thread. """ engine = None function = None finished = QtCore.pyqtSignal(dict) # Message handler signals changed_status = QtCore.pyqtSignal(str, int, str) raised_error = QtCore.pyqtSignal(str) raised_fatal = QtCore.pyqtSignal(str) # Event handler signals changed_show = QtCore.pyqtSignal(dict) changed_show_status = QtCore.pyqtSignal(dict, object) changed_list = QtCore.pyqtSignal(dict) changed_queue = QtCore.pyqtSignal(int) tracker_state = QtCore.pyqtSignal(dict) playing_show = QtCore.pyqtSignal(dict, bool, int) prompt_for_update = QtCore.pyqtSignal(dict, int) prompt_for_add = QtCore.pyqtSignal(dict, int) def __init__(self): super(EngineWorker, self).__init__() self.overrides = {'start': self._start} def _messagehandler(self, classname, msgtype, msg): self.changed_status.emit(classname, msgtype, msg) def _error(self, msg): self.raised_error.emit(str(msg)) def _fatal(self, msg): self.raised_fatal.emit(str(msg)) def _changed_show(self, show, changes=None): self.changed_show.emit(show) def _changed_show_status(self, show, old_status=None): self.changed_show_status.emit(show, old_status) def _changed_list(self, show): self.changed_list.emit(show) def _changed_queue(self, queue): self.changed_queue.emit(len(queue)) def _tracker_state(self, status): self.tracker_state.emit(status) def _playing_show(self, show, is_playing, episode): self.playing_show.emit(show, is_playing, episode) def _prompt_for_update(self, show, episode): self.prompt_for_update.emit(show, episode) def _prompt_for_add(self, show, episode): self.prompt_for_add.emit(show, episode) def _start(self, account): self.engine = Engine(account, self._messagehandler) self.engine.connect_signal('episode_changed', self._changed_show) self.engine.connect_signal('score_changed', self._changed_show) self.engine.connect_signal('tags_changed', self._changed_show) self.engine.connect_signal('status_changed', self._changed_show_status) self.engine.connect_signal('playing', self._playing_show) self.engine.connect_signal('show_added', self._changed_list) self.engine.connect_signal('show_deleted', self._changed_list) self.engine.connect_signal('show_synced', self._changed_show) self.engine.connect_signal('queue_changed', self._changed_queue) self.engine.connect_signal('prompt_for_update', self._prompt_for_update) self.engine.connect_signal('prompt_for_add', self._prompt_for_add) self.engine.connect_signal('tracker_state', self._tracker_state) self.engine.start() def set_function(self, function, ret_function, *args, **kwargs): if function in self.overrides: self.function = self.overrides[function] else: self.function = getattr(self.engine, function) try: self.finished.disconnect() except Exception: pass if ret_function: self.finished.connect(ret_function) self.args = args self.kwargs = kwargs def __del__(self): self.wait() def run(self): try: ret = self.function(*self.args, **self.kwargs) self.finished.emit({'success': True, 'result': ret}) except utils.TrackmaError as e: self._error(e) self.finished.emit({'success': False}) except utils.TrackmaFatal as e: self._fatal(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, 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()
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 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 = { '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): 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.accountman = Trackma_accounts() 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) self.sortedlist = sorted(showlist, key=itemgetter(self.sort)) def _get_show(self, title): # Attempt parsing list index # otherwise use title try: index = int(title)-1 return self.sortedlist[index] except (ValueError, AttributeError, IndexError): return self.engine.get_show_info_title(title) def _ask_update(self, show, episode): do_update = raw_input("Should I update %s to episode %d? [y/N] " % (show['title'].encode('utf-8'), 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, e: self.display_error(e) return print "Title: %s" % details['title'] for line in details['extra']: print "%s: %s" % line
class Trackma_urwid(): """ Main class for the urwid version of Trackma """ """Main objects""" engine = None mainloop = None cur_sort = 'title' sorts_iter = cycle(('my_progress', 'total', 'my_score', 'id', 'title')) cur_order = False orders_iter = cycle((True, False)) keymapping = dict() positions = list() last_search = None last_update_prompt = () """Widgets""" header = None listbox = None view = None def __init__(self): """Creates main widgets and creates mainloop""" self.config = utils.parse_config(utils.to_config_path('ui-curses.json'), utils.curses_defaults) keymap = utils.curses_defaults['keymap'] keymap.update(self.config['keymap']) self.keymap_str = self.get_keymap_str(keymap) self.keymapping = self.map_key_to_func(keymap) palette = [] for k, color in self.config['palette'].items(): palette.append( (k, color[0], color[1]) ) # Prepare header sys.stdout.write("\x1b]0;Trackma-curses "+utils.VERSION+"\x07"); self.header_title = urwid.Text('Trackma-curses ' + utils.VERSION) self.header_api = urwid.Text('API:') self.header_filter = urwid.Text('Filter:') self.header_sort = urwid.Text('Sort:title') self.header_order = urwid.Text('Order:d') self.header = urwid.AttrMap(urwid.Columns([ self.header_title, ('fixed', 30, self.header_filter), ('fixed', 17, self.header_sort), ('fixed', 16, self.header_api)]), 'status') top_pile = [self.header] if self.config['show_help']: top_text = "{help}:Help {sort}:Sort " + \ "{update}:Update {play}:Play " + \ "{status}:Status {score}:Score " + \ "{quit}:Quit" top_text = top_text.format(**self.keymap_str) top_pile.append(urwid.AttrMap(urwid.Text(top_text), 'status')) self.top_pile = urwid.Pile(top_pile) # Prepare status bar self.status_text = urwid.Text('Trackma-curses '+utils.VERSION) self.status_queue = urwid.Text('Q:N/A') self.status_tracker = urwid.Text('T:N/A') self.statusbar = urwid.AttrMap(urwid.Columns([ self.status_text, ('fixed', 10, self.status_tracker), ('fixed', 6, self.status_queue), ]), 'status') self.listheader = urwid.AttrMap( urwid.Columns([ ('weight', 1, urwid.Text('Title')), ('fixed', 10, urwid.Text('Progress')), ('fixed', 7, urwid.Text('Score')), ]), 'header') self.listwalker = ShowWalker([]) self.listbox = urwid.ListBox(self.listwalker) self.listframe = urwid.Frame(self.listbox, header=self.listheader) self.viewing_info = False self.view = urwid.Frame(self.listframe, header=self.top_pile, footer=self.statusbar) self.mainloop = urwid.MainLoop(self.view, palette, unhandled_input=self.keystroke, screen=urwid.raw_display.Screen()) def run(self): self.mainloop.set_alarm_in(0, self.do_switch_account) self.mainloop.run() def map_key_to_func(self, keymap): keymapping = dict() funcmap = { 'help': self.do_help, 'prev_filter': self.do_prev_filter, 'next_filter': self.do_next_filter, 'sort': self.do_sort, 'sort_order': self.change_sort_order, 'update': self.do_update, 'play': self.do_play, 'openfolder': self.do_openfolder, 'play_random': self.do_play_random, 'status': self.do_status, 'score': self.do_score, 'send': self.do_send, 'retrieve': self.do_retrieve, 'addsearch': self.do_addsearch, 'reload': self.do_reload, 'switch_account': self.do_switch_account, 'delete': self.do_delete, 'quit': self.do_quit, 'altname': self.do_altname, 'search': self.do_search, 'neweps': self.do_neweps, 'details': self.do_info, 'details_exit': self.do_info_exit, 'open_web': self.do_open_web, 'left': self.key_left, 'down': self.key_down, 'up': self.key_up, 'right': self.key_right, 'page_down': self.key_page_down, 'page_up': self.key_page_up, } for func, keybind in keymap.items(): try: if isinstance(keybind, list): for keybindm in keybind: keymapping[keybindm] = funcmap[func] else: keymapping[keybind] = funcmap[func] except KeyError: # keymap.json requested an action not available in funcmap pass return keymapping def get_keymap_str(self, keymap): stringed = {} for k, keybind in keymap.items(): if isinstance(keybind, list): stringed[k] = ','.join(keybind) else: stringed[k] = keybind return stringed def _rebuild(self): self.header_api.set_text('API:%s' % self.engine.api_info['name']) self.lists = dict() self.filters = self.engine.mediainfo['statuses_dict'] self.filters_nums = self.engine.mediainfo['statuses'] self.filters_sizes = [] track_info = self.engine.tracker_status() if track_info: self.tracker_state(track_info) for status in self.filters_nums: self.lists[status] = urwid.ListBox(ShowWalker([])) self._rebuild_lists() # Put the number of shows in every status in a list for status in self.filters_nums: self.filters_sizes.append(len(self.lists[status].body)) self.set_filter(0) self.status('Ready.') self.started = True def _rebuild_lists(self, status=None): if status: self.lists[status].body[:] = [] showlist = self.engine.filter_list(status) else: for _status in self.lists.keys(): self.lists[_status].body[:] = [] showlist = self.engine.get_list() library = self.engine.library() sortedlist = sorted(showlist, key=itemgetter(self.cur_sort), reverse=self.cur_order) for show in sortedlist: item = ShowItem(show, self.engine.mediainfo['has_progress'], self.engine.altname(show['id']), library.get(show['id'])) self.lists[show['my_status']].body.append(item) def start(self, account): """Starts the engine""" # Engine configuration self.started = False self.status("Starting engine...") self.engine = Engine(account, self.message_handler) self.engine.connect_signal('episode_changed', self.changed_show) self.engine.connect_signal('score_changed', self.changed_show) self.engine.connect_signal('status_changed', self.changed_show_status) self.engine.connect_signal('playing', self.playing_show) self.engine.connect_signal('show_added', self.changed_list) self.engine.connect_signal('show_deleted', self.changed_list) self.engine.connect_signal('show_synced', self.changed_show) self.engine.connect_signal('queue_changed', self.changed_queue) self.engine.connect_signal('prompt_for_update', self.prompt_update) self.engine.connect_signal('tracker_state', self.tracker_state) # Engine start and list rebuildi self.status("Building lists...") self.engine.start() self._rebuild() def set_filter(self, filter_num): self.cur_filter = filter_num _filter = self.filters_nums[self.cur_filter] self.header_filter.set_text("Filter:%s (%d)" % (self.filters[_filter], self.filters_sizes[self.cur_filter])) self.listframe.body = self.lists[_filter] def _get_cur_list(self): _filter = self.filters_nums[self.cur_filter] return self.lists[_filter].body def _get_selected_item(self): return self._get_cur_list().get_focus()[0] def status(self, msg): self.status_text.set_text(msg) def error(self, msg): self.status_text.set_text([('error', "Error: %s" % msg)]) def message_handler(self, classname, msgtype, msg): if msgtype != messenger.TYPE_DEBUG: try: self.status(msg) self.mainloop.draw_screen() except AssertionError: print(msg) def keystroke(self, input): try: self.keymapping[input]() except KeyError: # Unbinded key pressed; do nothing pass def key_left(self): self.mainloop.process_input(['left']) def key_down(self): self.mainloop.process_input(['down']) def key_up(self): self.mainloop.process_input(['up']) def key_right(self): self.mainloop.process_input(['right']) def key_page_down(self): self.mainloop.process_input(['page down']) def key_page_up(self): self.mainloop.process_input(['page up']) def forget_account(self): manager = AccountManager() manager.set_default(None) def do_switch_account(self, loop=None, data=None): manager = AccountManager() if self.engine is None: if manager.get_default(): self.start(manager.get_default()) else: self.dialog = AccountDialog(self.mainloop, manager, False) urwid.connect_signal(self.dialog, 'done', self.start) else: self.dialog = AccountDialog(self.mainloop, manager, True) urwid.connect_signal(self.dialog, 'done', self.do_reload_engine) def do_addsearch(self): self.ask('Search on remote: ', self.addsearch_request) def do_delete(self): if self._get_selected_item(): self.question('Delete selected show? [y/n] ', self.delete_request) def do_prev_filter(self): if self.cur_filter > 0: self.set_filter(self.cur_filter - 1) def do_next_filter(self): if self.cur_filter < len(self.filters)-1: self.set_filter(self.cur_filter + 1) def do_sort(self): self.status("Sorting...") _sort = next(self.sorts_iter) self.cur_sort = _sort self.header_sort.set_text("Sort:%s" % _sort) self._rebuild_lists() self.status("Ready.") def change_sort_order(self): self.status("Sorting...") _order = next(self.orders_iter) self.cur_order = _order self._rebuild_lists() self.status("Ready.") def do_update(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) self.ask('[Update] Episode # to update to: ', self.update_request, show['my_progress']+1) def do_play(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) self.ask('[Play] Episode # to play: ', self.play_request, show['my_progress']+1) def do_openfolder(self): item = self._get_selected_item() try: show = self.engine.get_show_info(item.showid) 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("No folder found.") def do_play_random(self): try: self.engine.play_random() except utils.TrackmaError as e: self.error(e) return def do_send(self): self.engine.list_upload() self.status("Ready.") def do_retrieve(self): try: self.engine.list_download() self._rebuild_lists() self.status("Ready.") except utils.TrackmaError as e: self.error(e) def do_help(self): helptext = "Trackma-curses "+utils.VERSION+" by z411 ([email protected])\n\n" helptext += "Trackma is an open source client for media tracking websites.\n" helptext += "http://github.com/z411/trackma\n\n" helptext += "This program is licensed under the GPLv3,\nfor more information read COPYING file.\n\n" helptext += "More controls:\n {prev_filter}/{next_filter}:Change Filter\n {search}:Search\n {addsearch}:Add\n {reload}:Change API/Mediatype\n" helptext += " {delete}:Delete\n {send}:Send changes\n {sort_order}:Change sort order\n {retrieve}:Retrieve list\n {details}: View details\n {open_web}: Open website\n {openfolder}: Open folder containing show\n {altname}:Set alternative title\n {neweps}:Search for new episodes\n {play_random}:Play Random\n {switch_account}: Change account" helptext = helptext.format(**self.keymap_str) ok_button = urwid.Button('OK', self.help_close) ok_button_wrap = urwid.Padding(urwid.AttrMap(ok_button, 'button', 'button hilight'), 'center', 6) pile = urwid.Pile([urwid.Text(helptext), ok_button_wrap]) self.dialog = Dialog(pile, self.mainloop, width=62, title='About/Help') self.dialog.show() def help_close(self, widget): self.dialog.close() def do_altname(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) self.status(show['title']) self.ask('[Altname] New alternative name: ', self.altname_request, self.engine.altname(item.showid)) def do_score(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) self.ask('[Score] Score to change to: ', self.score_request, show['my_score']) def do_status(self): item = self._get_selected_item() if not item: return show = self.engine.get_show_info(item.showid) buttons = list() num = 1 selected = 1 title = urwid.Text('Choose status:') title.align = 'center' buttons.append(title) for status in self.filters_nums: name = self.filters[status] button = urwid.Button(name, self.status_request, status) button._label.align = 'center' buttons.append(urwid.AttrMap(button, 'button', 'button hilight')) if status == show['my_status']: selected = num num += 1 pile = urwid.Pile(buttons) pile.set_focus(selected) self.dialog = Dialog(pile, self.mainloop, width=22) self.dialog.show() def do_reload(self): # Create a list of buttons to select the mediatype rb_mt = [] mediatypes = [] for mediatype in self.engine.api_info['supported_mediatypes']: but = urwid.RadioButton(rb_mt, mediatype) # Make it selected if it's the current mediatype if self.engine.api_info['mediatype'] == mediatype: but.set_state(True) urwid.connect_signal(but, 'change', self.reload_request, [None, mediatype]) mediatypes.append(urwid.AttrMap(but, 'button', 'button hilight')) mediatype = urwid.Columns([urwid.Text('Mediatype:'), urwid.Pile(mediatypes)]) #main_pile = urwid.Pile([mediatype, urwid.Divider(), api]) self.dialog = Dialog(mediatype, self.mainloop, width=30, title='Change media type') self.dialog.show() def do_reload_engine(self, account=None, mediatype=None): self.started = False self.engine.reload(account, mediatype) self._rebuild() def do_open_web(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) if show['url']: webbrowser.open(show['url'], 2, True) def do_info(self): if self.viewing_info: return item = self._get_selected_item() if not item: return show = self.engine.get_show_info(item.showid) self.status("Getting show details...") try: details = self.engine.get_show_details(show) except utils.TrackmaError as e: self.error(e) return title = urwid.Text( ('info_title', show['title']), 'center', 'any') widgets = [] for line in details['extra']: if line[0] and line[1]: widgets.append( urwid.Text( ('info_section', "%s: " % line[0] ) ) ) if isinstance(line[1], dict): linestr = repr(line[1]) elif isinstance(line[1], int) or isinstance(line[1], list): linestr = str(line[1]) else: linestr = line[1] widgets.append( urwid.Padding(urwid.Text( linestr + "\n" ), left=3) ) self.view.body = urwid.Frame(urwid.ListBox(widgets), header=title) self.viewing_info = True self.status("Detail View | ESC:Return Up/Down:Scroll O:View website") def do_info_exit(self): if self.viewing_info: self.view.body = self.listframe self.viewing_info = False self.status("Ready.") def do_neweps(self): try: shows = self.engine.scan_library(rescan=True) self._rebuild_lists() self.status("Ready.") except utils.TrackmaError as e: self.error(e) def do_quit(self): if self.viewing_info: self.do_info_exit() else: self.engine.unload() raise urwid.ExitMainLoop() def addsearch_request(self, data): self.ask_finish(self.addsearch_request) if data: try: shows = self.engine.search(data) except utils.TrackmaError as e: self.error(e) return if len(shows) > 0: self.status("Ready.") self.dialog = AddDialog(self.mainloop, self.engine, showlist=shows, width=('relative', 80)) urwid.connect_signal(self.dialog, 'done', self.addsearch_do) self.dialog.show() else: self.status("No results.") def addsearch_do(self, show): self.dialog.close() # Add show as current status _filter = self.filters_nums[self.cur_filter] try: self.engine.add_show(show, _filter) except utils.TrackmaError as e: self.error(e) def delete_request(self, data): self.ask_finish(self.delete_request) if data == 'y': showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) try: self.engine.delete_show(show) except utils.TrackmaError as e: self.error(e) def status_request(self, widget, data=None): self.dialog.close() if data is not None: item = self._get_selected_item() try: show = self.engine.set_status(item.showid, data) except utils.TrackmaError as e: self.error(e) return def reload_request(self, widget, selected, data): if selected: self.dialog.close() self.do_reload_engine(data[0], data[1]) def update_request(self, data): self.ask_finish(self.update_request) if data: item = self._get_selected_item() try: show = self.engine.set_episode(item.showid, data) except utils.TrackmaError as e: self.error(e) return def score_request(self, data): self.ask_finish(self.score_request) if data: item = self._get_selected_item() try: show = self.engine.set_score(item.showid, data) except utils.TrackmaError as e: self.error(e) return def altname_request(self, data): self.ask_finish(self.altname_request) if data: item = self._get_selected_item() try: self.engine.altname(item.showid, data) item.update_altname(self.engine.altname(item.showid)) except utils.TrackmaError as e: self.error(e) return def play_request(self, data): self.ask_finish(self.play_request) if data: item = self._get_selected_item() show = self.engine.get_show_info(item.showid) try: self.engine.play_episode(show, data) except utils.TrackmaError as e: self.error(e) return def prompt_update_request(self, data): (show, episode) = self.last_update_prompt self.ask_finish(self.prompt_update_request) if data == 'y': try: show = self.engine.set_episode(show['id'], episode) except utils.TrackmaError as e: self.error(e) return else: self.status('Ready.') def prompt_update(self, show, episode): self.last_update_prompt = (show, episode) self.question("Update %s to episode %d? [y/N] " % (show['title'], episode), self.prompt_update_request) def changed_show(self, show, changes=None): if self.started and show: status = show['my_status'] self.lists[status].body.update_show(show) self.mainloop.draw_screen() def changed_show_status(self, show, old_status=None): self._rebuild_lists(show['my_status']) if old_status is not None: self._rebuild_lists(old_status) go_filter = 0 for _filter in self.filters_nums: if _filter == show['my_status']: break go_filter += 1 self.set_filter(go_filter) self._get_cur_list().select_show(show) def changed_queue(self, queue): self.status_queue.set_text("Q:{}".format(len(queue))) def tracker_state(self, status): state = status['state'] timer = status['timer'] paused = status['paused'] if state == utils.TRACKER_NOVIDEO: st = 'LISTEN' elif state == utils.TRACKER_PLAYING: st = '{}{}'.format('#' if paused else '+', timer) elif state == utils.TRACKER_UNRECOGNIZED: st = 'UNRECOG' elif state == utils.TRACKER_NOT_FOUND: st = 'NOTFOUN' elif state == utils.TRACKER_IGNORED: st = 'IGNORE' else: st = '???' self.status_tracker.set_text("T:{}".format(st)) self.mainloop.draw_screen() def playing_show(self, show, is_playing, episode=None): status = show['my_status'] self.lists[status].body.playing_show(show, is_playing) self.mainloop.draw_screen() def changed_list(self, show): self._rebuild_lists(show['my_status']) def ask(self, msg, callback, data=u''): self.asker = Asker(msg, str(data)) self.view.set_footer(urwid.AttrMap(self.asker, 'status')) self.view.set_focus('footer') urwid.connect_signal(self.asker, 'done', callback) def question(self, msg, callback, data=u''): self.asker = QuestionAsker(msg, str(data)) self.view.set_footer(urwid.AttrMap(self.asker, 'status')) self.view.set_focus('footer') urwid.connect_signal(self.asker, 'done', callback) def ask_finish(self, callback): self.view.set_focus('body') urwid.disconnect_signal(self, self.asker, 'done', callback) self.view.set_footer(self.statusbar) def do_search(self, key=''): if self.last_search: text = "Search forward [%s]: " % self.last_search else: text = "Search forward: " self.ask(text, self.search_request, key) #urwid.connect_signal(self.asker, 'change', self.search_live) #def search_live(self, widget, data): # if data: # self.listwalker.select_match(data) def search_request(self, data): self.ask_finish(self.search_request) if data: self.last_search = data self._get_cur_list().select_match(data) elif self.last_search: self._get_cur_list().select_match(self.last_search)
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 sortedlist = [] needed_args = { '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): 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.accountman = Trackma_accounts() self.account = self.accountman.select_account(False) def _update_prompt(self): self.prompt = "{0}@{1}({2}) {3}> ".format( self.engine.get_userconfig('username'), self.engine.api_info['name'], self.engine.api_info['mediatype'], self.engine.mediainfo['statuses_dict'][self.filter_num]) def _load_list(self, *args): showlist = self.engine.filter_list(self.filter_num) self.sortedlist = sorted(showlist, key=itemgetter(self.sort)) def _get_show(self, title): # Attempt parsing list index # otherwise use title try: index = int(title) - 1 return self.sortedlist[index] except (ValueError, AttributeError, IndexError): return self.engine.get_show_info_title(title) 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.start() # Start with default filter selected self.filter_num = self.engine.mediainfo['statuses'][0] self._load_list() self._update_prompt() def do_account(self, args): """ account - Switch to a different account Usage: 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): """ filter - Changes the filtering of list by status; call with no arguments to see available filters 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 filters: %s" % ', '.join( v.lower().replace(' ', '') for v in self.engine.mediainfo['statuses_dict'].values()) def do_sort(self, args): """ sort - Change sort Usage: sort <sort type> Available types: id, title, my_progress, total, my_score """ sorts = ('id', 'title', 'my_progress', 'total', 'my_score') if arg[0] in sorts: self.sort = arg[0] self._load_list() else: print "Invalid sort." def do_mediatype(self, args): """ mediatype - Reloads engine with different mediatype; call with no arguments to see supported mediatypes 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_list(self, args): """ list - Lists all shows available in the local list as a nice formatted list. """ # Show the list in memory self._make_list(self.sortedlist) def do_info(self, args): """ info - Gets detailed information about a show in the local list. Usage: info <show index or title> """ try: show = self._get_show(args[0]) details = self.engine.get_show_details(show) except utils.TrackmaError, e: self.display_error(e) return print "Title: %s" % details['title'] for line in details['extra']: print "%s: %s" % line
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 = { '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): 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.accountman = Trackma_accounts() 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) self.sortedlist = sorted(showlist, key=itemgetter(self.sort)) def _get_show(self, title): # Attempt parsing list index # otherwise use title try: index = int(title) - 1 return self.sortedlist[index] except (ValueError, AttributeError, IndexError): return self.engine.get_show_info_title(title) def _ask_update(self, show, episode): do_update = raw_input("Should I update %s to episode %d? [y/N] " % (show['title'].encode('utf-8'), 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, e: self.display_error(e) return print "Title: %s" % details['title'] for line in details['extra']: print "%s: %s" % line
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 = { '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): 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.accountman = Trackma_accounts() self.account = self.accountman.select_account(False) def _update_prompt(self): self.prompt = "{0}@{1}({2}) {3}> ".format( self.engine.get_userconfig('username'), self.engine.api_info['name'], self.engine.api_info['mediatype'], self.engine.mediainfo['statuses_dict'][self.filter_num] ) def _load_list(self, *args): showlist = self.engine.filter_list(self.filter_num) self.sortedlist = sorted(showlist, key=itemgetter(self.sort)) def _get_show(self, title): # Attempt parsing list index # otherwise use title try: index = int(title)-1 return self.sortedlist[index] except (ValueError, AttributeError, IndexError): return self.engine.get_show_info_title(title) def _ask_update(self, show, episode): do_update = raw_input("Should I update %s to episode %d? [y/N] " % (show['title'].encode('utf-8'), 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() def do_account(self, args): """ account - Switch to a different account Usage: 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): """ filter - Changes the filtering of list by status; call with no arguments to see available filters 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 filters: %s" % ', '.join( v.lower().replace(' ', '') for v in self.engine.mediainfo['statuses_dict'].values() ) def do_sort(self, args): """ sort - Change sort Usage: sort <sort type> Available types: id, title, my_progress, total, my_score """ 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): """ mediatype - Reloads engine with different mediatype; call with no arguments to see supported mediatypes 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_list(self, args): """ list - Lists all shows available in the local list as a nice formatted list. """ # Show the list in memory self._make_list(self.sortedlist) def do_info(self, args): """ info - Gets detailed information about a show in the local list. Usage: info <show index or title> """ try: show = self._get_show(args[0]) details = self.engine.get_show_details(show) except utils.TrackmaError, e: self.display_error(e) return print "Title: %s" % details['title'] for line in details['extra']: print "%s: %s" % line
class Trackma_urwid(): """ Main class for the urwid version of Trackma """ """Main objects""" engine = None mainloop = None cur_sort = 'title' sorts_iter = cycle(('my_progress', 'total', 'my_score', 'id', 'title')) cur_order = False orders_iter = cycle((True, False)) keymapping = dict() positions = list() last_search = None last_update_prompt = () """Widgets""" header = None listbox = None view = None def __init__(self): """Creates main widgets and creates mainloop""" self.config = utils.parse_config(utils.get_root_filename('ui-curses.json'), utils.curses_defaults) keymap = utils.curses_defaults['keymap'] keymap.update(self.config['keymap']) self.keymap_str = self.get_keymap_str(keymap) self.keymapping = self.map_key_to_func(keymap) palette = [] for k, color in self.config['palette'].items(): palette.append( (k, color[0], color[1]) ) # Prepare header sys.stdout.write("\x1b]0;Trackma-curses "+utils.VERSION+"\x07"); self.header_title = urwid.Text('Trackma-curses ' + utils.VERSION) self.header_api = urwid.Text('API:') self.header_filter = urwid.Text('Filter:') self.header_sort = urwid.Text('Sort:title') self.header_order = urwid.Text('Order:d') self.header = urwid.AttrMap(urwid.Columns([ self.header_title, ('fixed', 30, self.header_filter), ('fixed', 17, self.header_sort), ('fixed', 16, self.header_api)]), 'status') top_pile = [self.header] if self.config['show_help']: top_text = "{help}:Help {sort}:Sort " + \ "{update}:Update {play}:Play " + \ "{status}:Status {score}:Score " + \ "{quit}:Quit" top_text = top_text.format(**self.keymap_str) top_pile.append(urwid.AttrMap(urwid.Text(top_text), 'status')) self.top_pile = urwid.Pile(top_pile) # Prepare status bar self.status_text = urwid.Text('Trackma-curses '+utils.VERSION) self.status_queue = urwid.Text('Q:N/A') self.status_tracker = urwid.Text('T:N/A') self.statusbar = urwid.AttrMap(urwid.Columns([ self.status_text, ('fixed', 10, self.status_tracker), ('fixed', 6, self.status_queue), ]), 'status') self.listheader = urwid.AttrMap( urwid.Columns([ ('weight', 1, urwid.Text('Title')), ('fixed', 10, urwid.Text('Progress')), ('fixed', 7, urwid.Text('Score')), ]), 'header') self.listwalker = ShowWalker([]) self.listbox = urwid.ListBox(self.listwalker) self.listframe = urwid.Frame(self.listbox, header=self.listheader) self.viewing_info = False self.view = urwid.Frame(self.listframe, header=self.top_pile, footer=self.statusbar) self.mainloop = urwid.MainLoop(self.view, palette, unhandled_input=self.keystroke, screen=urwid.raw_display.Screen()) def run(self): self.mainloop.set_alarm_in(0, self.do_switch_account) self.mainloop.run() def map_key_to_func(self, keymap): keymapping = dict() funcmap = { 'help': self.do_help, 'prev_filter': self.do_prev_filter, 'next_filter': self.do_next_filter, 'sort': self.do_sort, 'sort_order': self.change_sort_order, 'update': self.do_update, 'play': self.do_play, 'openfolder': self.do_openfolder, 'play_random': self.do_play_random, 'status': self.do_status, 'score': self.do_score, 'send': self.do_send, 'retrieve': self.do_retrieve, 'addsearch': self.do_addsearch, 'reload': self.do_reload, 'switch_account': self.do_switch_account, 'delete': self.do_delete, 'quit': self.do_quit, 'altname': self.do_altname, 'search': self.do_search, 'neweps': self.do_neweps, 'details': self.do_info, 'details_exit': self.do_info_exit, 'open_web': self.do_open_web, } for func, keybind in keymap.items(): try: if isinstance(keybind, list): for keybindm in keybind: keymapping[keybindm] = funcmap[func] else: keymapping[keybind] = funcmap[func] except KeyError: # keymap.json requested an action not available in funcmap pass return keymapping def get_keymap_str(self, keymap): stringed = {} for k, keybind in keymap.items(): if isinstance(keybind, list): stringed[k] = ','.join(keybind) else: stringed[k] = keybind return stringed def _rebuild(self): self.header_api.set_text('API:%s' % self.engine.api_info['name']) self.lists = dict() self.filters = self.engine.mediainfo['statuses_dict'] self.filters_nums = self.engine.mediainfo['statuses'] self.filters_sizes = [] track_info = self.engine.tracker_status() if track_info: self.tracker_state(track_info['state'], None) for status in self.filters_nums: self.lists[status] = urwid.ListBox(ShowWalker([])) self._rebuild_lists() # Put the number of shows in every status in a list for status in self.filters_nums: self.filters_sizes.append(len(self.lists[status].body)) self.set_filter(0) self.status('Ready.') self.started = True def _rebuild_lists(self, status=None): if status: self.lists[status].body[:] = [] showlist = self.engine.filter_list(status) else: for _status in self.lists.keys(): self.lists[_status].body[:] = [] showlist = self.engine.get_list() library = self.engine.library() sortedlist = sorted(showlist, key=itemgetter(self.cur_sort), reverse=self.cur_order) for show in sortedlist: if show['my_status'] == self.engine.mediainfo['status_start']: item = ShowItem(show, self.engine.mediainfo['has_progress'], self.engine.altname(show['id']), library.get(show['id'])) else: item = ShowItem(show, self.engine.mediainfo['has_progress'], self.engine.altname(show['id'])) self.lists[show['my_status']].body.append(item) def start(self, account): """Starts the engine""" # Engine configuration self.started = False self.status("Starting engine...") self.engine = Engine(account, self.message_handler) self.engine.connect_signal('episode_changed', self.changed_show) self.engine.connect_signal('score_changed', self.changed_show) self.engine.connect_signal('status_changed', self.changed_show_status) self.engine.connect_signal('playing', self.playing_show) self.engine.connect_signal('show_added', self.changed_list) self.engine.connect_signal('show_deleted', self.changed_list) self.engine.connect_signal('show_synced', self.changed_show) self.engine.connect_signal('queue_changed', self.changed_queue) self.engine.connect_signal('prompt_for_update', self.prompt_update) self.engine.connect_signal('tracker_state', self.tracker_state) # Engine start and list rebuildi self.status("Building lists...") self.engine.start() self._rebuild() def set_filter(self, filter_num): self.cur_filter = filter_num _filter = self.filters_nums[self.cur_filter] self.header_filter.set_text("Filter:%s (%d)" % (self.filters[_filter], self.filters_sizes[self.cur_filter])) self.listframe.body = self.lists[_filter] def _get_cur_list(self): _filter = self.filters_nums[self.cur_filter] return self.lists[_filter].body def _get_selected_item(self): return self._get_cur_list().get_focus()[0] def status(self, msg): self.status_text.set_text(msg) def error(self, msg): self.status_text.set_text([('error', "Error: %s" % msg)]) def message_handler(self, classname, msgtype, msg): if msgtype != messenger.TYPE_DEBUG: try: self.status(msg) self.mainloop.draw_screen() except AssertionError: print(msg) def keystroke(self, input): try: self.keymapping[input]() except KeyError: # Unbinded key pressed; do nothing pass def forget_account(self): manager = AccountManager() manager.set_default(None) def do_switch_account(self, loop=None, data=None): manager = AccountManager() if self.engine is None: if manager.get_default(): self.start(manager.get_default()) else: self.dialog = AccountDialog(self.mainloop, manager, False) urwid.connect_signal(self.dialog, 'done', self.start) else: self.dialog = AccountDialog(self.mainloop, manager, True) urwid.connect_signal(self.dialog, 'done', self.do_reload_engine) def do_addsearch(self): self.ask('Search on remote: ', self.addsearch_request) def do_delete(self): if self._get_selected_item(): self.question('Delete selected show? [y/n] ', self.delete_request) def do_prev_filter(self): if self.cur_filter > 0: self.set_filter(self.cur_filter - 1) def do_next_filter(self): if self.cur_filter < len(self.filters)-1: self.set_filter(self.cur_filter + 1) def do_sort(self): self.status("Sorting...") _sort = next(self.sorts_iter) self.cur_sort = _sort self.header_sort.set_text("Sort:%s" % _sort) self._rebuild_lists() self.status("Ready.") def change_sort_order(self): self.status("Sorting...") _order = next(self.orders_iter) self.cur_order = _order self._rebuild_lists() self.status("Ready.") def do_update(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) self.ask('[Update] Episode # to update to: ', self.update_request, show['my_progress']+1) def do_play(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) self.ask('[Play] Episode # to play: ', self.play_request, show['my_progress']+1) def do_openfolder(self): item = self._get_selected_item() try: show = self.engine.get_show_info(item.showid) 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("No folder found.") def do_play_random(self): try: self.engine.play_random() except utils.TrackmaError as e: self.error(e) return def do_send(self): self.engine.list_upload() self.status("Ready.") def do_retrieve(self): try: self.engine.list_download() self._rebuild_lists() self.status("Ready.") except utils.TrackmaError as e: self.error(e) def do_help(self): helptext = "Trackma-curses "+utils.VERSION+" by z411 ([email protected])\n\n" helptext += "Trackma is an open source client for media tracking websites.\n" helptext += "http://github.com/z411/trackma\n\n" helptext += "This program is licensed under the GPLv3,\nfor more information read COPYING file.\n\n" helptext += "More controls:\n {prev_filter}/{next_filter}:Change Filter\n {search}:Search\n {addsearch}:Add\n {reload}:Change API/Mediatype\n" helptext += " {delete}:Delete\n {send}:Send changes\n {sort_order}:Change sort order\n {retrieve}:Retrieve list\n {details}: View details\n {open_web}: Open website\n {openfolder}: Open folder containing show\n {altname}:Set alternative title\n {neweps}:Search for new episodes\n {play_random}:Play Random\n {switch_account}: Change account" helptext = helptext.format(**self.keymap_str) ok_button = urwid.Button('OK', self.help_close) ok_button_wrap = urwid.Padding(urwid.AttrMap(ok_button, 'button', 'button hilight'), 'center', 6) pile = urwid.Pile([urwid.Text(helptext), ok_button_wrap]) self.dialog = Dialog(pile, self.mainloop, width=62, title='About/Help') self.dialog.show() def help_close(self, widget): self.dialog.close() def do_altname(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) self.status(show['title']) self.ask('[Altname] New alternative name: ', self.altname_request, self.engine.altname(item.showid)) def do_score(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) self.ask('[Score] Score to change to: ', self.score_request, show['my_score']) def do_status(self): item = self._get_selected_item() if not item: return show = self.engine.get_show_info(item.showid) buttons = list() num = 1 selected = 1 title = urwid.Text('Choose status:') title.align = 'center' buttons.append(title) for status in self.filters_nums: name = self.filters[status] button = urwid.Button(name, self.status_request, status) button._label.align = 'center' buttons.append(urwid.AttrMap(button, 'button', 'button hilight')) if status == show['my_status']: selected = num num += 1 pile = urwid.Pile(buttons) pile.set_focus(selected) self.dialog = Dialog(pile, self.mainloop, width=22) self.dialog.show() def do_reload(self): # Create a list of buttons to select the mediatype rb_mt = [] mediatypes = [] for mediatype in self.engine.api_info['supported_mediatypes']: but = urwid.RadioButton(rb_mt, mediatype) # Make it selected if it's the current mediatype if self.engine.api_info['mediatype'] == mediatype: but.set_state(True) urwid.connect_signal(but, 'change', self.reload_request, [None, mediatype]) mediatypes.append(urwid.AttrMap(but, 'button', 'button hilight')) mediatype = urwid.Columns([urwid.Text('Mediatype:'), urwid.Pile(mediatypes)]) #main_pile = urwid.Pile([mediatype, urwid.Divider(), api]) self.dialog = Dialog(mediatype, self.mainloop, width=30, title='Change media type') self.dialog.show() def do_reload_engine(self, account=None, mediatype=None): self.started = False self.engine.reload(account, mediatype) self._rebuild() def do_open_web(self): item = self._get_selected_item() if item: show = self.engine.get_show_info(item.showid) if show['url']: webbrowser.open(show['url'], 2, True) def do_info(self): if self.viewing_info: return item = self._get_selected_item() if not item: return show = self.engine.get_show_info(item.showid) self.status("Getting show details...") try: details = self.engine.get_show_details(show) except utils.TrackmaError as e: self.error(e) return title = urwid.Text( ('info_title', show['title']), 'center', 'any') widgets = [] for line in details['extra']: if line[0] and line[1]: widgets.append( urwid.Text( ('info_section', "%s: " % line[0] ) ) ) if isinstance(line[1], dict): linestr = repr(line[1]) elif isinstance(line[1], int) or isinstance(line[1], list): linestr = str(line[1]) else: linestr = line[1] widgets.append( urwid.Padding(urwid.Text( linestr + "\n" ), left=3) ) self.view.body = urwid.Frame(urwid.ListBox(widgets), header=title) self.viewing_info = True self.status("Detail View | ESC:Return Up/Down:Scroll O:View website") def do_info_exit(self): if self.viewing_info: self.view.body = self.listframe self.viewing_info = False self.status("Ready.") def do_neweps(self): try: shows = self.engine.scan_library(rescan=True) self._rebuild_lists(self.engine.mediainfo['status_start']) self.status("Ready.") except utils.TrackmaError as e: self.error(e) def do_quit(self): self.engine.unload() raise urwid.ExitMainLoop() def addsearch_request(self, data): self.ask_finish(self.addsearch_request) if data: try: shows = self.engine.search(data) except utils.TrackmaError as e: self.error(e) return if len(shows) > 0: self.status("Ready.") self.dialog = AddDialog(self.mainloop, self.engine, showlist=shows, width=('relative', 80)) urwid.connect_signal(self.dialog, 'done', self.addsearch_do) self.dialog.show() else: self.status("No results.") def addsearch_do(self, show): self.dialog.close() # Add show as current status _filter = self.filters_nums[self.cur_filter] try: self.engine.add_show(show, _filter) except utils.TrackmaError as e: self.error(e) def delete_request(self, data): self.ask_finish(self.delete_request) if data == 'y': showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) try: show = self.engine.delete_show(show) except utils.TrackmaError as e: self.error(e) def status_request(self, widget, data=None): self.dialog.close() if data is not None: item = self._get_selected_item() try: show = self.engine.set_status(item.showid, data) except utils.TrackmaError as e: self.error(e) return def reload_request(self, widget, selected, data): if selected: self.dialog.close() self.do_reload_engine(data[0], data[1]) def update_request(self, data): self.ask_finish(self.update_request) if data: item = self._get_selected_item() try: show = self.engine.set_episode(item.showid, data) except utils.TrackmaError as e: self.error(e) return def score_request(self, data): self.ask_finish(self.score_request) if data: item = self._get_selected_item() try: show = self.engine.set_score(item.showid, data) except utils.TrackmaError as e: self.error(e) return def altname_request(self, data): self.ask_finish(self.altname_request) if data: item = self._get_selected_item() try: self.engine.altname(item.showid, data) item.update_altname(self.engine.altname(item.showid)) except utils.TrackmaError as e: self.error(e) return def play_request(self, data): self.ask_finish(self.play_request) if data: item = self._get_selected_item() show = self.engine.get_show_info(item.showid) try: self.engine.play_episode(show, data) except utils.TrackmaError as e: self.error(e) return def prompt_update_request(self, data): (show, episode) = self.last_update_prompt self.ask_finish(self.prompt_update_request) if data == 'y': try: show = self.engine.set_episode(show['id'], episode) except utils.TrackmaError as e: self.error(e) return else: self.status('Ready.') def prompt_update(self, show, episode): self.last_update_prompt = (show, episode) self.question("Update %s to episode %d? [y/N] " % (show['title'], episode), self.prompt_update_request) def changed_show(self, show, changes=None): if self.started and show: status = show['my_status'] self.lists[status].body.update_show(show) self.mainloop.draw_screen() def changed_show_status(self, show, old_status=None): self._rebuild_lists(show['my_status']) if old_status is not None: self._rebuild_lists(old_status) go_filter = 0 for _filter in self.filters_nums: if _filter == show['my_status']: break go_filter += 1 self.set_filter(go_filter) self._get_cur_list().select_show(show) def changed_queue(self, queue): self.status_queue.set_text("Q:{}".format(len(queue))) def tracker_state(self, state, timer): if state == utils.TRACKER_NOVIDEO: st = 'LISTEN' elif state == utils.TRACKER_PLAYING: st = '+{}'.format(timer) elif state == utils.TRACKER_UNRECOGNIZED: st = 'UNRECOG' elif state == utils.TRACKER_NOT_FOUND: st = 'NOTFOUN' elif state == utils.TRACKER_IGNORED: st = 'IGNORE' else: st = '???' self.status_tracker.set_text("T:{}".format(st)) self.mainloop.draw_screen() def tracker_timer(self, timer): if timer is not None: self.status_tracker.set_text("T:+{}".format(timer)) self.mainloop.draw_screen() def playing_show(self, show, is_playing, episode=None): status = show['my_status'] self.lists[status].body.playing_show(show, is_playing) self.mainloop.draw_screen() def changed_list(self, show): self._rebuild_lists(show['my_status']) def ask(self, msg, callback, data=u''): self.asker = Asker(msg, str(data)) self.view.set_footer(urwid.AttrMap(self.asker, 'status')) self.view.set_focus('footer') urwid.connect_signal(self.asker, 'done', callback) def question(self, msg, callback, data=u''): self.asker = QuestionAsker(msg, str(data)) self.view.set_footer(urwid.AttrMap(self.asker, 'status')) self.view.set_focus('footer') urwid.connect_signal(self.asker, 'done', callback) def ask_finish(self, callback): self.view.set_focus('body') urwid.disconnect_signal(self, self.asker, 'done', callback) self.view.set_footer(self.statusbar) def do_search(self, key=''): if self.last_search: text = "Search forward [%s]: " % self.last_search else: text = "Search forward: " self.ask(text, self.search_request, key) #urwid.connect_signal(self.asker, 'change', self.search_live) #def search_live(self, widget, data): # if data: # self.listwalker.select_match(data) def search_request(self, data): self.ask_finish(self.search_request) if data: self.last_search = data self._get_cur_list().select_match(data) elif self.last_search: self._get_cur_list().select_match(self.last_search)
class APIHandler: """ The class used to communicate with Trackma. """ comparision = { "mal_ID": "id", "title": "title", "episodes_done": "my_progress", "watch_status": "my_status", "score": "my_score", "_len": "total" } watch = None accs = None engine = None adList = [] tList = [] def __init__(self, accountnum=1): """ Defaults to the first account. """ self.watch = Watcher() self.accs = dict(AccountManager().get_accounts()) self.engine = Engine(self.accs.get(accountnum)) self.engine.start() self.tList = list(self.engine.get_list()) with open(self.watch.WATCH_FILE, 'r') as watch_file: self.adList = list(json.load(watch_file)) watch_file.close() self._sort_lists() def _sort_lists(self, key="mal_ID"): """ Sorts lists for easier comparision.\n Called on initializing the class. Mutilates the lists. """ self.adList.sort(key=lambda val: val[key]) self.tList.sort(key=lambda val: val[self.comparision[key]]) def _equalize_lists(self, format=False): """ Strips both the lists to the common categories and returns it as two sublists with the animedl list being first. """ tempList = [[], []] for i in range(len(self.tList)): entry = [{}, {}] for cat in self.comparision: entry[0][self.comparision[cat] if format else cat] = self.adList[i][cat] entry[1][self.comparision[cat] if format else cat] = "planned" if self.tList[i][self.comparision[ cat]] == "plan_to_watch" else self.tList[i][ self.comparision[cat]] tempList[0].append(entry[0]) tempList[1].append(entry[1]) return tempList def _stage_changes(self, preference=True): """ Returns the modified items in the animedl list (compares to Trackma).\n Replaces it with the Trackma entry, to reverse this set preference to False. """ (sadList, stList) = self._equalize_lists() tempList = [] for i in range(len(stList)): if (sadList[i] != stList[i]): tempList.append((sadList[i] if preference else stList[i])) return tempList def add_staged_to_trackma(self): """ Updates the Trackma queue. """ qList = self._stage_changes() for item in qList: self.engine.get_show_info(item["mal_ID"]) """
class Trackma_urwid(): """ Main class for the urwid version of Trackma """ """Main objects""" engine = None mainloop = None cur_sort = 'title' sorts_iter = cycle(('my_progress', 'total', 'my_score', 'id', 'title')) cur_order = False orders_iter = cycle((True, False)) keymapping = dict() positions = list() last_search = None last_update_prompt = () """Widgets""" header = None listbox = None view = None def __init__(self): """Creates main widgets and creates mainloop""" palette = [ ('body','', ''), ('focus','standout', ''), ('head','light red', 'black'), ('header','bold', ''), ('status', 'white', 'dark blue'), ('error', 'light red', 'dark blue'), ('window', 'white', 'dark blue'), ('button', 'black', 'light gray'), ('button hilight', 'white', 'dark red'), ('item_airing', 'dark blue', ''), ('item_notaired', 'yellow', ''), ('item_neweps', 'white', 'brown'), ('item_updated', 'white', 'dark green'), ('item_playing', 'white', 'dark blue'), ('info_title', 'light red', ''), ('info_section', 'dark blue', ''), ] keymap = utils.parse_config(utils.get_root_filename('keymap.json'), utils.keymap_defaults) self.keymapping = self.map_key_to_func(keymap) sys.stdout.write("\x1b]0;Trackma-curses "+utils.VERSION+"\x07"); self.header_title = urwid.Text('Trackma-curses ' + utils.VERSION) self.header_api = urwid.Text('API:') self.header_filter = urwid.Text('Filter:') self.header_sort = urwid.Text('Sort:title') self.header_order = urwid.Text('Order:d') self.header = urwid.AttrMap(urwid.Columns([ self.header_title, ('fixed', 23, self.header_filter), ('fixed', 17, self.header_sort), ('fixed', 16, self.header_api)]), 'status') top_text = keymap['help'] + ':Help ' + keymap['sort'] +':Sort ' + \ keymap['update'] + ':Update ' + keymap['play'] + ':Play ' + \ keymap['status'] + ':Status ' + keymap['score'] + ':Score ' + \ keymap['quit'] + ':Quit' self.top_pile = urwid.Pile([self.header, urwid.AttrMap(urwid.Text(top_text), 'status') ]) self.statusbar = urwid.AttrMap(urwid.Text('Trackma-curses '+utils.VERSION), 'status') self.listheader = urwid.AttrMap( urwid.Columns([ ('weight', 1, urwid.Text('Title')), ('fixed', 10, urwid.Text('Progress')), ('fixed', 7, urwid.Text('Score')), ]), 'header') self.listwalker = ShowWalker([]) self.listbox = urwid.ListBox(self.listwalker) self.listframe = urwid.Frame(self.listbox, header=self.listheader) self.viewing_info = False self.view = urwid.Frame(self.listframe, header=self.top_pile, footer=self.statusbar) self.mainloop = urwid.MainLoop(self.view, palette, unhandled_input=self.keystroke, screen=urwid.raw_display.Screen()) self.mainloop.set_alarm_in(0, self.do_switch_account) self.mainloop.run() def map_key_to_func(self, keymap): keymapping = dict() funcmap = { 'help': self.do_help, 'prev_filter': self.do_prev_filter, 'next_filter': self.do_next_filter, 'sort': self.do_sort, 'sort_order': self.change_sort_order, 'update': self.do_update, 'play': self.do_play, 'status': self.do_status, 'score': self.do_score, 'send': self.do_send, 'retrieve': self.do_retrieve, 'addsearch': self.do_addsearch, 'reload': self.do_reload, 'switch_account': self.do_switch_account, 'delete': self.do_delete, 'quit': self.do_quit, 'altname': self.do_altname, 'search': self.do_search, 'neweps': self.do_neweps, 'details': self.do_info, 'details_exit': self.do_info_exit, 'open_web': self.do_open_web, } for key, value in keymap.items(): try: keymapping.update({value: funcmap[key]}) except KeyError: # keymap.json requested an action not available in funcmap pass return keymapping def _rebuild(self): self.header_api.set_text('API:%s' % self.engine.api_info['name']) self.lists = dict() self.filters = self.engine.mediainfo['statuses_dict'] self.filters_nums = self.engine.mediainfo['statuses'] for status in self.filters_nums: self.lists[status] = urwid.ListBox(ShowWalker([])) self._rebuild_lists() self.set_filter(0) self.status('Ready.') self.started = True def _rebuild_lists(self, status=None): if status: self.lists[status].body[:] = [] showlist = self.engine.filter_list(status) else: for _status in self.lists.keys(): self.lists[_status].body[:] = [] showlist = self.engine.get_list() library = self.engine.library() sortedlist = sorted(showlist, key=itemgetter(self.cur_sort), reverse=self.cur_order) for show in sortedlist: if show['my_status'] == self.engine.mediainfo['status_start']: item = ShowItem(show, self.engine.mediainfo['has_progress'], self.engine.altname(show['id']), library.get(show['id'])) else: item = ShowItem(show, self.engine.mediainfo['has_progress'], self.engine.altname(show['id'])) self.lists[show['my_status']].body.append(item) def start(self, account): """Starts the engine""" # Engine configuration self.started = False self.status("Starting engine...") self.engine = Engine(account, self.message_handler) self.engine.connect_signal('episode_changed', self.changed_show) self.engine.connect_signal('score_changed', self.changed_show) self.engine.connect_signal('status_changed', self.changed_show_status) self.engine.connect_signal('playing', self.playing_show) self.engine.connect_signal('show_added', self.changed_list) self.engine.connect_signal('show_deleted', self.changed_list) self.engine.connect_signal('show_synced', self.changed_show) self.engine.connect_signal('prompt_for_update', self.prompt_update) # Engine start and list rebuildi self.status("Building lists...") self.engine.start() self._rebuild() def set_filter(self, filter_num): self.cur_filter = filter_num _filter = self.filters_nums[self.cur_filter] self.header_filter.set_text("Filter:%s" % self.filters[_filter]) self.listframe.body = self.lists[_filter] def _get_cur_list(self): _filter = self.filters_nums[self.cur_filter] return self.lists[_filter].body def _get_selected_item(self): return self._get_cur_list().get_focus()[0] def status(self, msg): self.statusbar.base_widget.set_text(msg) def error(self, msg): self.statusbar.base_widget.set_text([('error', "Error: %s" % msg)]) def message_handler(self, classname, msgtype, msg): if msgtype != messenger.TYPE_DEBUG: try: self.status(msg) self.mainloop.draw_screen() except AssertionError: print(msg) def keystroke(self, input): try: self.keymapping[input]() except KeyError: # Unbinded key pressed; do nothing pass def do_switch_account(self, loop=None, data=None): manager = AccountManager() if self.engine is None: if manager.get_default(): self.start(manager.get_default()) else: self.dialog = AccountDialog(self.mainloop, manager, False) urwid.connect_signal(self.dialog, 'done', self.start) else: self.dialog = AccountDialog(self.mainloop, manager, True) urwid.connect_signal(self.dialog, 'done', self.do_reload_engine) def do_addsearch(self): self.ask('Search on remote: ', self.addsearch_request) def do_delete(self): self.question('Delete selected show? [y/n] ', self.delete_request) def do_prev_filter(self): if self.cur_filter > 0: self.set_filter(self.cur_filter - 1) def do_next_filter(self): if self.cur_filter < len(self.filters)-1: self.set_filter(self.cur_filter + 1) def do_sort(self): self.status("Sorting...") _sort = next(self.sorts_iter) self.cur_sort = _sort self.header_sort.set_text("Sort:%s" % _sort) self._rebuild_lists() self.status("Ready.") def change_sort_order(self): self.status("Sorting...") _order = next(self.orders_iter) self.cur_order = _order self._rebuild_lists() self.status("Ready.") def do_update(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.ask('[Update] Episode # to update to: ', self.update_request, show['my_progress']) def do_play(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.ask('[Play] Episode # to play: ', self.play_request, show['my_progress']+1) def do_send(self): self.engine.list_upload() self.status("Ready.") def do_retrieve(self): try: self.engine.list_download() self._rebuild_lists() self.status("Ready.") except utils.TrackmaError as e: self.error(e) def do_help(self): helptext = "Trackma-curses "+utils.VERSION+" by z411 ([email protected])\n\n" helptext += "Trackma is an open source client for media tracking websites.\n" helptext += "http://github.com/z411/trackma\n\n" helptext += "This program is licensed under the GPLv3,\nfor more information read COPYING file.\n\n" helptext += "More controls:\n Left/Right:Change Filter\n /:Search\n a:Add\n c:Change API/Mediatype\n" helptext += " d:Delete\n s:Send changes\n r:Change sort order\n R:Retrieve list\n Enter: View details\n O: Open website\n A:Set alternative title\n N:Search for new episodes\n F9: Change account" ok_button = urwid.Button('OK', self.help_close) ok_button_wrap = urwid.Padding(urwid.AttrMap(ok_button, 'button', 'button hilight'), 'center', 6) pile = urwid.Pile([urwid.Text(helptext), ok_button_wrap]) self.dialog = Dialog(pile, self.mainloop, width=62, title='About/Help') self.dialog.show() def help_close(self, widget): self.dialog.close() def do_altname(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.status(show['title']) self.ask('[Altname] New alternative name: ', self.altname_request, self.engine.altname(showid)) def do_score(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.ask('[Score] Score to change to: ', self.score_request, show['my_score']) def do_status(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) buttons = list() num = 1 selected = 1 title = urwid.Text('Choose status:') title.align = 'center' buttons.append(title) for status in self.filters_nums: name = self.filters[status] button = urwid.Button(name, self.status_request, status) button._label.align = 'center' buttons.append(urwid.AttrMap(button, 'button', 'button hilight')) if status == show['my_status']: selected = num num += 1 pile = urwid.Pile(buttons) pile.set_focus(selected) self.dialog = Dialog(pile, self.mainloop, width=22) self.dialog.show() def do_reload(self): # Create a list of buttons to select the mediatype rb_mt = [] mediatypes = [] for mediatype in self.engine.api_info['supported_mediatypes']: but = urwid.RadioButton(rb_mt, mediatype) # Make it selected if it's the current mediatype if self.engine.api_info['mediatype'] == mediatype: but.set_state(True) urwid.connect_signal(but, 'change', self.reload_request, [None, mediatype]) mediatypes.append(urwid.AttrMap(but, 'button', 'button hilight')) mediatype = urwid.Columns([urwid.Text('Mediatype:'), urwid.Pile(mediatypes)]) #main_pile = urwid.Pile([mediatype, urwid.Divider(), api]) self.dialog = Dialog(mediatype, self.mainloop, width=30, title='Change media type') self.dialog.show() def do_reload_engine(self, account=None, mediatype=None): self.started = False self.engine.reload(account, mediatype) self._rebuild() def do_open_web(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) if show['url']: webbrowser.open(show['url'], 2, True) def do_info(self): if self.viewing_info: return showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.status("Getting show details...") try: details = self.engine.get_show_details(show) except utils.TrackmaError as e: self.error(e) return title = urwid.Text( ('info_title', show['title']), 'center', 'any') widgets = [] for line in details['extra']: if line[0] and line[1]: widgets.append( urwid.Text( ('info_section', "%s: " % line[0] ) ) ) if isinstance(line[1], dict): linestr = repr(line[1]) elif isinstance(line[1], int): linestr = str(line[1]) else: linestr = line[1] widgets.append( urwid.Padding(urwid.Text( linestr + "\n" ), left=3) ) self.view.body = urwid.Frame(urwid.ListBox(widgets), header=title) self.viewing_info = True self.status("Detail View | ESC:Return Up/Down:Scroll O:View website") def do_info_exit(self): if self.viewing_info: self.view.body = self.listframe self.viewing_info = False self.status("Ready.") def do_neweps(self): try: shows = self.engine.scan_library() self._rebuild_lists(self.engine.mediainfo['status_start']) self.status("Ready.") except utils.TrackmaError as e: self.error(e) def do_quit(self): self.engine.unload() raise urwid.ExitMainLoop() def addsearch_request(self, data): self.ask_finish(self.addsearch_request) if data: try: shows = self.engine.search(data) except utils.TrackmaError as e: self.error(e) return if len(shows) > 0: self.status("Ready.") self.dialog = AddDialog(self.mainloop, self.engine, showlist=shows, width=('relative', 80)) urwid.connect_signal(self.dialog, 'done', self.addsearch_do) self.dialog.show() else: self.status("No results.") def addsearch_do(self, show): self.dialog.close() # Add show as current status _filter = self.filters_nums[self.cur_filter] try: self.engine.add_show(show, _filter) except utils.TrackmaError as e: self.error(e) def delete_request(self, data): self.ask_finish(self.delete_request) if data == 'y': showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) try: show = self.engine.delete_show(show) except utils.TrackmaError as e: self.error(e) def status_request(self, widget, data=None): self.dialog.close() if data is not None: item = self._get_selected_item() try: show = self.engine.set_status(item.showid, data) except utils.TrackmaError as e: self.error(e) return def reload_request(self, widget, selected, data): if selected: self.dialog.close() self.do_reload_engine(data[0], data[1]) def update_request(self, data): self.ask_finish(self.update_request) if data: item = self._get_selected_item() try: show = self.engine.set_episode(item.showid, data) except utils.TrackmaError as e: self.error(e) return def score_request(self, data): self.ask_finish(self.score_request) if data: item = self._get_selected_item() try: show = self.engine.set_score(item.showid, data) except utils.TrackmaError as e: self.error(e) return def altname_request(self, data): self.ask_finish(self.altname_request) if data: item = self._get_selected_item() try: self.engine.altname(item.showid, data) item.update_altname(self.engine.altname(item.showid)) except utils.TrackmaError as e: self.error(e) return def play_request(self, data): self.ask_finish(self.play_request) if data: item = self._get_selected_item() show = self.engine.get_show_info(item.showid) try: self.engine.play_episode(show, data) except utils.TrackmaError as e: self.error(e) return self.status('Ready.') def prompt_update_request(self, data): (show, episode) = self.last_update_prompt self.ask_finish(self.prompt_update_request) if data == 'y': try: show = self.engine.set_episode(show['id'], episode) except utils.TrackmaError as e: self.error(e) return else: self.status('Ready.') def prompt_update(self, show, episode): self.last_update_prompt = (show, episode) self.question("Update %s to episode %d? [y/N] " % (show['title'], episode), self.prompt_update_request) def changed_show(self, show, changes=None): if self.started and show: status = show['my_status'] self.lists[status].body.update_show(show) self.mainloop.draw_screen() def changed_show_status(self, show, old_status=None): self._rebuild_lists(show['my_status']) if old_status is not None: self._rebuild_lists(old_status) go_filter = 0 for _filter in self.filters_nums: if _filter == show['my_status']: break go_filter += 1 self.set_filter(go_filter) self._get_cur_list().select_show(show) def playing_show(self, show, is_playing, episode=None): status = show['my_status'] self.lists[status].body.playing_show(show, is_playing) self.mainloop.draw_screen() def changed_list(self, show): self._rebuild_lists(show['my_status']) def ask(self, msg, callback, data=u''): self.asker = Asker(msg, str(data)) self.view.set_footer(urwid.AttrMap(self.asker, 'status')) self.view.set_focus('footer') urwid.connect_signal(self.asker, 'done', callback) def question(self, msg, callback, data=u''): self.asker = QuestionAsker(msg, str(data)) self.view.set_footer(urwid.AttrMap(self.asker, 'status')) self.view.set_focus('footer') urwid.connect_signal(self.asker, 'done', callback) def ask_finish(self, callback): self.view.set_focus('body') urwid.disconnect_signal(self, self.asker, 'done', callback) self.view.set_footer(self.statusbar) def do_search(self, key=''): if self.last_search: text = "Search forward [%s]: " % self.last_search else: text = "Search forward: " self.ask(text, self.search_request, key) #urwid.connect_signal(self.asker, 'change', self.search_live) #def search_live(self, widget, data): # if data: # self.listwalker.select_match(data) def search_request(self, data): self.ask_finish(self.search_request) if data: self.last_search = data self._get_cur_list().select_match(data) elif self.last_search: self._get_cur_list().select_match(self.last_search)
class Trackma_urwid: """ Main class for the urwid version of Trackma """ """Main objects""" engine = None mainloop = None cur_sort = "title" sorts_iter = cycle(("my_progress", "total", "my_score", "id", "title")) cur_order = False orders_iter = cycle((True, False)) keymapping = dict() positions = list() last_search = None last_update_prompt = () """Widgets""" header = None listbox = None view = None def __init__(self): """Creates main widgets and creates mainloop""" self.config = utils.parse_config(utils.get_root_filename("ui-curses.json"), utils.curses_defaults) keymap = self.config["keymap"] self.keymap_str = self.get_keymap_str(keymap) self.keymapping = self.map_key_to_func(keymap) palette = [] for k, color in self.config["palette"].items(): palette.append((k, color[0], color[1])) sys.stdout.write("\x1b]0;Trackma-curses " + utils.VERSION + "\x07") self.header_title = urwid.Text("Trackma-curses " + utils.VERSION) self.header_api = urwid.Text("API:") self.header_filter = urwid.Text("Filter:") self.header_sort = urwid.Text("Sort:title") self.header_order = urwid.Text("Order:d") self.header = urwid.AttrMap( urwid.Columns( [ self.header_title, ("fixed", 23, self.header_filter), ("fixed", 17, self.header_sort), ("fixed", 16, self.header_api), ] ), "status", ) top_pile = [self.header] if self.config["show_help"]: top_text = ( "{help}:Help {sort}:Sort " + "{update}:Update {play}:Play " + "{status}:Status {score}:Score " + "{quit}:Quit" ) top_text = top_text.format(**self.keymap_str) top_pile.append(urwid.AttrMap(urwid.Text(top_text), "status")) self.top_pile = urwid.Pile(top_pile) self.statusbar = urwid.AttrMap(urwid.Text("Trackma-curses " + utils.VERSION), "status") self.listheader = urwid.AttrMap( urwid.Columns( [ ("weight", 1, urwid.Text("Title")), ("fixed", 10, urwid.Text("Progress")), ("fixed", 7, urwid.Text("Score")), ] ), "header", ) self.listwalker = ShowWalker([]) self.listbox = urwid.ListBox(self.listwalker) self.listframe = urwid.Frame(self.listbox, header=self.listheader) self.viewing_info = False self.view = urwid.Frame(self.listframe, header=self.top_pile, footer=self.statusbar) self.mainloop = urwid.MainLoop( self.view, palette, unhandled_input=self.keystroke, screen=urwid.raw_display.Screen() ) self.mainloop.set_alarm_in(0, self.do_switch_account) self.mainloop.run() def map_key_to_func(self, keymap): keymapping = dict() funcmap = { "help": self.do_help, "prev_filter": self.do_prev_filter, "next_filter": self.do_next_filter, "sort": self.do_sort, "sort_order": self.change_sort_order, "update": self.do_update, "play": self.do_play, "status": self.do_status, "score": self.do_score, "send": self.do_send, "retrieve": self.do_retrieve, "addsearch": self.do_addsearch, "reload": self.do_reload, "switch_account": self.do_switch_account, "delete": self.do_delete, "quit": self.do_quit, "altname": self.do_altname, "search": self.do_search, "neweps": self.do_neweps, "details": self.do_info, "details_exit": self.do_info_exit, "open_web": self.do_open_web, } for func, keybind in keymap.items(): try: if isinstance(keybind, list): for keybindm in keybind: keymapping[keybindm] = funcmap[func] else: keymapping[keybind] = funcmap[func] except KeyError: # keymap.json requested an action not available in funcmap pass return keymapping def get_keymap_str(self, keymap): stringed = {} for k, keybind in keymap.items(): if isinstance(keybind, list): stringed[k] = ",".join(keybind) else: stringed[k] = keybind return stringed def _rebuild(self): self.header_api.set_text("API:%s" % self.engine.api_info["name"]) self.lists = dict() self.filters = self.engine.mediainfo["statuses_dict"] self.filters_nums = self.engine.mediainfo["statuses"] for status in self.filters_nums: self.lists[status] = urwid.ListBox(ShowWalker([])) self._rebuild_lists() self.set_filter(0) self.status("Ready.") self.started = True def _rebuild_lists(self, status=None): if status: self.lists[status].body[:] = [] showlist = self.engine.filter_list(status) else: for _status in self.lists.keys(): self.lists[_status].body[:] = [] showlist = self.engine.get_list() library = self.engine.library() sortedlist = sorted(showlist, key=itemgetter(self.cur_sort), reverse=self.cur_order) for show in sortedlist: if show["my_status"] == self.engine.mediainfo["status_start"]: item = ShowItem( show, self.engine.mediainfo["has_progress"], self.engine.altname(show["id"]), library.get(show["id"]), ) else: item = ShowItem(show, self.engine.mediainfo["has_progress"], self.engine.altname(show["id"])) self.lists[show["my_status"]].body.append(item) def start(self, account): """Starts the engine""" # Engine configuration self.started = False self.status("Starting engine...") self.engine = Engine(account, self.message_handler) self.engine.connect_signal("episode_changed", self.changed_show) self.engine.connect_signal("score_changed", self.changed_show) self.engine.connect_signal("status_changed", self.changed_show_status) self.engine.connect_signal("playing", self.playing_show) self.engine.connect_signal("show_added", self.changed_list) self.engine.connect_signal("show_deleted", self.changed_list) self.engine.connect_signal("show_synced", self.changed_show) self.engine.connect_signal("prompt_for_update", self.prompt_update) # Engine start and list rebuildi self.status("Building lists...") self.engine.start() self._rebuild() def set_filter(self, filter_num): self.cur_filter = filter_num _filter = self.filters_nums[self.cur_filter] self.header_filter.set_text("Filter:%s" % self.filters[_filter]) self.listframe.body = self.lists[_filter] def _get_cur_list(self): _filter = self.filters_nums[self.cur_filter] return self.lists[_filter].body def _get_selected_item(self): return self._get_cur_list().get_focus()[0] def status(self, msg): self.statusbar.base_widget.set_text(msg) def error(self, msg): self.statusbar.base_widget.set_text([("error", "Error: %s" % msg)]) def message_handler(self, classname, msgtype, msg): if msgtype != messenger.TYPE_DEBUG: try: self.status(msg) self.mainloop.draw_screen() except AssertionError: print(msg) def keystroke(self, input): try: self.keymapping[input]() except KeyError: # Unbinded key pressed; do nothing pass def do_switch_account(self, loop=None, data=None): manager = AccountManager() if self.engine is None: if manager.get_default(): self.start(manager.get_default()) else: self.dialog = AccountDialog(self.mainloop, manager, False) urwid.connect_signal(self.dialog, "done", self.start) else: self.dialog = AccountDialog(self.mainloop, manager, True) urwid.connect_signal(self.dialog, "done", self.do_reload_engine) def do_addsearch(self): self.ask("Search on remote: ", self.addsearch_request) def do_delete(self): self.question("Delete selected show? [y/n] ", self.delete_request) def do_prev_filter(self): if self.cur_filter > 0: self.set_filter(self.cur_filter - 1) def do_next_filter(self): if self.cur_filter < len(self.filters) - 1: self.set_filter(self.cur_filter + 1) def do_sort(self): self.status("Sorting...") _sort = next(self.sorts_iter) self.cur_sort = _sort self.header_sort.set_text("Sort:%s" % _sort) self._rebuild_lists() self.status("Ready.") def change_sort_order(self): self.status("Sorting...") _order = next(self.orders_iter) self.cur_order = _order self._rebuild_lists() self.status("Ready.") def do_update(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.ask("[Update] Episode # to update to: ", self.update_request, show["my_progress"]) def do_play(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.ask("[Play] Episode # to play: ", self.play_request, show["my_progress"] + 1) def do_send(self): self.engine.list_upload() self.status("Ready.") def do_retrieve(self): try: self.engine.list_download() self._rebuild_lists() self.status("Ready.") except utils.TrackmaError as e: self.error(e) def do_help(self): helptext = "Trackma-curses " + utils.VERSION + " by z411 ([email protected])\n\n" helptext += "Trackma is an open source client for media tracking websites.\n" helptext += "http://github.com/z411/trackma\n\n" helptext += "This program is licensed under the GPLv3,\nfor more information read COPYING file.\n\n" helptext += "More controls:\n {prev_filter}/{next_filter}:Change Filter\n {search}:Search\n {addsearch}:Add\n {reload}:Change API/Mediatype\n" helptext += " {delete}:Delete\n {send}:Send changes\n {sort_order}:Change sort order\n {retrieve}:Retrieve list\n {details}: View details\n {open_web}: Open website\n {altname}:Set alternative title\n {neweps}:Search for new episodes\n {switch_account}: Change account" helptext = helptext.format(**self.keymap_str) ok_button = urwid.Button("OK", self.help_close) ok_button_wrap = urwid.Padding(urwid.AttrMap(ok_button, "button", "button hilight"), "center", 6) pile = urwid.Pile([urwid.Text(helptext), ok_button_wrap]) self.dialog = Dialog(pile, self.mainloop, width=62, title="About/Help") self.dialog.show() def help_close(self, widget): self.dialog.close() def do_altname(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.status(show["title"]) self.ask("[Altname] New alternative name: ", self.altname_request, self.engine.altname(showid)) def do_score(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.ask("[Score] Score to change to: ", self.score_request, show["my_score"]) def do_status(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) buttons = list() num = 1 selected = 1 title = urwid.Text("Choose status:") title.align = "center" buttons.append(title) for status in self.filters_nums: name = self.filters[status] button = urwid.Button(name, self.status_request, status) button._label.align = "center" buttons.append(urwid.AttrMap(button, "button", "button hilight")) if status == show["my_status"]: selected = num num += 1 pile = urwid.Pile(buttons) pile.set_focus(selected) self.dialog = Dialog(pile, self.mainloop, width=22) self.dialog.show() def do_reload(self): # Create a list of buttons to select the mediatype rb_mt = [] mediatypes = [] for mediatype in self.engine.api_info["supported_mediatypes"]: but = urwid.RadioButton(rb_mt, mediatype) # Make it selected if it's the current mediatype if self.engine.api_info["mediatype"] == mediatype: but.set_state(True) urwid.connect_signal(but, "change", self.reload_request, [None, mediatype]) mediatypes.append(urwid.AttrMap(but, "button", "button hilight")) mediatype = urwid.Columns([urwid.Text("Mediatype:"), urwid.Pile(mediatypes)]) # main_pile = urwid.Pile([mediatype, urwid.Divider(), api]) self.dialog = Dialog(mediatype, self.mainloop, width=30, title="Change media type") self.dialog.show() def do_reload_engine(self, account=None, mediatype=None): self.started = False self.engine.reload(account, mediatype) self._rebuild() def do_open_web(self): showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) if show["url"]: webbrowser.open(show["url"], 2, True) def do_info(self): if self.viewing_info: return showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) self.status("Getting show details...") try: details = self.engine.get_show_details(show) except utils.TrackmaError as e: self.error(e) return title = urwid.Text(("info_title", show["title"]), "center", "any") widgets = [] for line in details["extra"]: if line[0] and line[1]: widgets.append(urwid.Text(("info_section", "%s: " % line[0]))) if isinstance(line[1], dict): linestr = repr(line[1]) elif isinstance(line[1], int) or isinstance(line[1], list): linestr = str(line[1]) else: linestr = line[1] widgets.append(urwid.Padding(urwid.Text(linestr + "\n"), left=3)) self.view.body = urwid.Frame(urwid.ListBox(widgets), header=title) self.viewing_info = True self.status("Detail View | ESC:Return Up/Down:Scroll O:View website") def do_info_exit(self): if self.viewing_info: self.view.body = self.listframe self.viewing_info = False self.status("Ready.") def do_neweps(self): try: shows = self.engine.scan_library(rescan=True) self._rebuild_lists(self.engine.mediainfo["status_start"]) self.status("Ready.") except utils.TrackmaError as e: self.error(e) def do_quit(self): self.engine.unload() raise urwid.ExitMainLoop() def addsearch_request(self, data): self.ask_finish(self.addsearch_request) if data: try: shows = self.engine.search(data) except utils.TrackmaError as e: self.error(e) return if len(shows) > 0: self.status("Ready.") self.dialog = AddDialog(self.mainloop, self.engine, showlist=shows, width=("relative", 80)) urwid.connect_signal(self.dialog, "done", self.addsearch_do) self.dialog.show() else: self.status("No results.") def addsearch_do(self, show): self.dialog.close() # Add show as current status _filter = self.filters_nums[self.cur_filter] try: self.engine.add_show(show, _filter) except utils.TrackmaError as e: self.error(e) def delete_request(self, data): self.ask_finish(self.delete_request) if data == "y": showid = self._get_selected_item().showid show = self.engine.get_show_info(showid) try: show = self.engine.delete_show(show) except utils.TrackmaError as e: self.error(e) def status_request(self, widget, data=None): self.dialog.close() if data is not None: item = self._get_selected_item() try: show = self.engine.set_status(item.showid, data) except utils.TrackmaError as e: self.error(e) return def reload_request(self, widget, selected, data): if selected: self.dialog.close() self.do_reload_engine(data[0], data[1]) def update_request(self, data): self.ask_finish(self.update_request) if data: item = self._get_selected_item() try: show = self.engine.set_episode(item.showid, data) except utils.TrackmaError as e: self.error(e) return def score_request(self, data): self.ask_finish(self.score_request) if data: item = self._get_selected_item() try: show = self.engine.set_score(item.showid, data) except utils.TrackmaError as e: self.error(e) return def altname_request(self, data): self.ask_finish(self.altname_request) if data: item = self._get_selected_item() try: self.engine.altname(item.showid, data) item.update_altname(self.engine.altname(item.showid)) except utils.TrackmaError as e: self.error(e) return def play_request(self, data): self.ask_finish(self.play_request) if data: item = self._get_selected_item() show = self.engine.get_show_info(item.showid) try: self.engine.play_episode(show, data) except utils.TrackmaError as e: self.error(e) return self.status("Ready.") def prompt_update_request(self, data): (show, episode) = self.last_update_prompt self.ask_finish(self.prompt_update_request) if data == "y": try: show = self.engine.set_episode(show["id"], episode) except utils.TrackmaError as e: self.error(e) return else: self.status("Ready.") def prompt_update(self, show, episode): self.last_update_prompt = (show, episode) self.question("Update %s to episode %d? [y/N] " % (show["title"], episode), self.prompt_update_request) def changed_show(self, show, changes=None): if self.started and show: status = show["my_status"] self.lists[status].body.update_show(show) self.mainloop.draw_screen() def changed_show_status(self, show, old_status=None): self._rebuild_lists(show["my_status"]) if old_status is not None: self._rebuild_lists(old_status) go_filter = 0 for _filter in self.filters_nums: if _filter == show["my_status"]: break go_filter += 1 self.set_filter(go_filter) self._get_cur_list().select_show(show) def playing_show(self, show, is_playing, episode=None): status = show["my_status"] self.lists[status].body.playing_show(show, is_playing) self.mainloop.draw_screen() def changed_list(self, show): self._rebuild_lists(show["my_status"]) def ask(self, msg, callback, data=u""): self.asker = Asker(msg, str(data)) self.view.set_footer(urwid.AttrMap(self.asker, "status")) self.view.set_focus("footer") urwid.connect_signal(self.asker, "done", callback) def question(self, msg, callback, data=u""): self.asker = QuestionAsker(msg, str(data)) self.view.set_footer(urwid.AttrMap(self.asker, "status")) self.view.set_focus("footer") urwid.connect_signal(self.asker, "done", callback) def ask_finish(self, callback): self.view.set_focus("body") urwid.disconnect_signal(self, self.asker, "done", callback) self.view.set_footer(self.statusbar) def do_search(self, key=""): if self.last_search: text = "Search forward [%s]: " % self.last_search else: text = "Search forward: " self.ask(text, self.search_request, key) # urwid.connect_signal(self.asker, 'change', self.search_live) # def search_live(self, widget, data): # if data: # self.listwalker.select_match(data) def search_request(self, data): self.ask_finish(self.search_request) if data: self.last_search = data self._get_cur_list().select_match(data) elif self.last_search: self._get_cur_list().select_match(self.last_search)
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 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 EngineWorker(QtCore.QThread): """ Worker thread Contains the engine and manages every process in a separate thread. """ engine = None function = None finished = QtCore.pyqtSignal(dict) # Message handler signals changed_status = QtCore.pyqtSignal(str, int, str) raised_error = QtCore.pyqtSignal(str) raised_fatal = QtCore.pyqtSignal(str) # Event handler signals changed_show = QtCore.pyqtSignal(dict) changed_show_status = QtCore.pyqtSignal(dict, object) changed_list = QtCore.pyqtSignal(dict) changed_queue = QtCore.pyqtSignal(int) tracker_state = QtCore.pyqtSignal(dict) playing_show = QtCore.pyqtSignal(dict, bool, int) prompt_for_update = QtCore.pyqtSignal(dict, int) prompt_for_add = QtCore.pyqtSignal(dict, int) def __init__(self): super(EngineWorker, self).__init__() self.overrides = {'start': self._start} def _messagehandler(self, classname, msgtype, msg): self.changed_status.emit(classname, msgtype, msg) def _error(self, msg): self.raised_error.emit(str(msg)) def _fatal(self, msg): self.raised_fatal.emit(str(msg)) def _changed_show(self, show, changes=None): self.changed_show.emit(show) def _changed_show_status(self, show, old_status=None): self.changed_show_status.emit(show, old_status) def _changed_list(self, show): self.changed_list.emit(show) def _changed_queue(self, queue): self.changed_queue.emit(len(queue)) def _tracker_state(self, status): self.tracker_state.emit(status) def _playing_show(self, show, is_playing, episode): self.playing_show.emit(show, is_playing, episode) def _prompt_for_update(self, show, episode): self.prompt_for_update.emit(show, episode) def _prompt_for_add(self, show, episode): self.prompt_for_add.emit(show, episode) # Callable functions def _start(self, account): self.engine = Engine(account, self._messagehandler) self.engine.connect_signal('episode_changed', self._changed_show) self.engine.connect_signal('score_changed', self._changed_show) self.engine.connect_signal('tags_changed', self._changed_show) self.engine.connect_signal('status_changed', self._changed_show_status) self.engine.connect_signal('playing', self._playing_show) self.engine.connect_signal('show_added', self._changed_list) self.engine.connect_signal('show_deleted', self._changed_list) self.engine.connect_signal('show_synced', self._changed_show) self.engine.connect_signal('queue_changed', self._changed_queue) self.engine.connect_signal('prompt_for_update', self._prompt_for_update) self.engine.connect_signal('prompt_for_add', self._prompt_for_add) self.engine.connect_signal('tracker_state', self._tracker_state) self.engine.start() def set_function(self, function, ret_function, *args, **kwargs): if function in self.overrides: self.function = self.overrides[function] else: self.function = getattr(self.engine, function) try: self.finished.disconnect() except Exception: pass if ret_function: self.finished.connect(ret_function) self.args = args self.kwargs = kwargs def __del__(self): self.wait() def run(self): try: ret = self.function(*self.args, **self.kwargs) self.finished.emit({'success': True, 'result': ret}) except utils.TrackmaError as e: self._error(e) self.finished.emit({'success': False}) except utils.TrackmaFatal as e: self._fatal(e)