def main(): parser = create_parser() # Parse arguments if len(sys.argv) <= 1: args = parser.parse_args(['-h']) else: args = parser.parse_args() if args.version: print(mal.__version__) sys.exit(0) # if the command is login, create credentials and exits # NOTE: if this statement is removed the `mal login` and # no credentials exists, login.create_credentials() will # be called twice! On login.get_credentials and args.func(mal, args) if args.command == 'login': login.create_credentials() sys.exit(0) # Check if authorized config = login.get_credentials() if config['config']['animation'].lower() == 'false': decorating.animated.enabled = False mal_api = MyAnimeList.login(config) if not mal_api: print(color.colorize('Invalid credentials! :(', 'red', 'bold')) print(color.colorize('Tip: Try "mal login" again :D', 'white', 'bold')) sys.exit(1) # Execute sub command args.func(mal_api, args)
def search(mal, regex, full=False): """Search the MAL database for an anime.""" result = mal.search(regex) # if no results or only one was found we treat them special if len(result) == 0: print(color.colorize("No matches in MAL database ᕙ(⇀‸↼‶)ᕗ", 'red')) return if len(result) == 1: full = True # full info if only one anime was found lines = ["{index}: {title}", " Episodes: {episodes}\tScore: {score}", " Synopsis: {synopsis}"] extra_lines = [" Start date: {start}\tEnd data: {end}", " Status: {status}"] print("Found", color.colorize(str(len(result)), "cyan", "underline"), "animes:") for i, anime in enumerate(result): # replace tags and special html chars (like —) with actual characters synopsis = html.unescape(str(anime["synopsis"])).replace("<br />", "") if len(synopsis) > 70 and not full: synopsis = synopsis[:70] + "..." # this template/line stuff might need some refactoring template = { "index": str(i + 1), "title": color.colorize(anime["title"], "red", "bold"), "episodes": color.colorize(anime["episodes"], "white", "bold"), "score": color.score_color(float(anime["score"])), "synopsis": synopsis, "start": anime["start_date"] if anime["start_date"] != "0000-00-00" else "NA", "end": anime["end_date"] if anime["end_date"] != "0000-00-00" else "NA", "status": anime["status"] } print("\n".join(line.format_map(template) for line in lines)) if full: print("\n".join(line.format_map(template) for line in extra_lines)) print("\n")
def print_error(error_name, status, reason): padding = (len(error_name) + 2) * ' ' error = color.colorize(error_name, 'red', 'bold') status = color.colorize(status, 'cyan') print(('{error}: {status}\n' '{padding}{reason} ¯\_(ツ)_/¯'.format_map(locals())), file=sys.stderr)
def print_error(error_name, status, reason, kill=False): padding = (len(error_name) + 2) * ' ' error = color.colorize(error_name, 'red', 'bold') status = color.colorize(status, 'cyan') print(('{error}: {status}\n' '{padding}{reason} ¯\_(ツ)_/¯'.format_map(locals())), file=sys.stderr) if kill: os._exit(1)
def drop(mal, regex): """Drop a anime based a regex expression""" items = remove_completed(mal.find(regex)) item = select_item(items) entry = dict(status=mal.status_codes['dropped']) old_status = mal.status_names[item['status']] template = { 'title': color.colorize(item['title'], 'yellow', 'bold'), 'old-status': color.colorize(old_status, 'green', 'bold'), 'action': color.colorize('Dropping', 'red', 'bold') } print(('{action} anime {title} from list ' '{old-status}'.format_map(template))) response = mal.update(item['id'], entry) report_if_fails(response)
def select_item(items): """Select a single item from a list of results.""" item = None if len(items) > 1: # ambigious search results print(color.colorize('Multiple results:', 'cyan')) # show user the results and make them choose one for index, title in enumerate(map(itemgetter('title'), items)): print('{index}: {title}'.format_map(locals())) index = int(input('Which one? ')) item = items[index] elif len(items) == 1: item = items[0] else: print(color.colorize("No matches in list ᕙ(⇀‸↼‶)ᕗ", 'red')) sys.exit(1) return item
def progress_update(mal, regex, inc): items = remove_completed(mal.find(regex)) item = select_item(items) # also handles ambigious searches episode = item['episode'] + inc entry = dict(episode=episode, score=item.get('score', 0)) template = { 'title': color.colorize(item['title'], 'yellow', 'bold'), 'episode': color.colorize(episode, 'red' if inc < 1 else 'green'), 'total_episodes': color.colorize(item['total_episodes'], 'cyan'), 'procedure': color.procedure_color(inc) } print(('{procedure} progress for {title} to ' '{episode}/{total_episodes}'.format_map(template))) entry = start_end(entry, episode, item['total_episodes']) response = mal.update(item['id'], entry) report_if_fails(response)
def find(mal, regex, filtering='all', extra=False, user=None): """Find all anime in a certain status given a regex.""" items = mal.find(regex, extra=extra, user=user) if len(items) == 0: print(color.colorize("No matches in list ᕙ(⇀‸↼‶)ᕗ", 'red')) return # filter the results if necessary if filtering != 'all': items = [x for x in items if x['status_name'] == filtering] n_items = color.colorize(str(len(items)), 'cyan', 'underline') print("Matched {} items:".format(n_items)) # pretty print all the animes found sorted_items = sorted(items, key=itemgetter('status'), reverse=True) for index, item in enumerate(sorted_items): anime_pprint(index + 1, item, extra=extra)
def anime_pprint(index, item, extra=False): """Pretty print an anime's information.""" padding = int(math.log10(index)) + 3 remaining_color = ('blue' if item['episode'] < item['total_episodes'] else 'green') remaining = '{episode}/{total_episodes}'.format_map(item) in_rewatching = ('#in-rewatching-{rewatching}'.format_map(item) if item['rewatching'] else '') template = { 'index': index, 'padding': ' ' * padding, 'status': MyAnimeList.status_names[item['status']].capitalize(), 'title': color.colorize(item['title'], 'red', 'bold'), 'remaining': color.colorize(remaining, remaining_color, 'bold'), 'score': color.score_color(item['score']), 'rewatching': (color.colorize(in_rewatching, 'yellow', 'bold')) } # add formating options for extra info if extra: template.update({ 'start': item['start_date'] if item['start_date'] != '0000-00-00' else 'NA', 'finish': item['finish_date'] if item['finish_date'] != '0000-00-00' else 'NA', 'tags': item['tags'] }) message_lines = [ "{index}: {title}".format_map(template), ("{padding}{status} at {remaining} episodes " "with score {score} {rewatching}".format_map(template)) ] # the extra information lines if extra: message_lines.extend([ "{padding}Started: {start} \t Finished: {finish}".format_map( template), "{padding}Tags: {tags}".format_map(template) ]) print('\n'.join(message_lines), "\n")
def add(mal, regex, status="plan to watch"): """Add an entry to the user list.""" results = mal.search(regex) selected = select_item(results) print("Adding {title} to list as '{status}'".format(title=color.colorize( selected["title"], "yellow", "bold"), status=status)) mal.update(selected["id"], {"status": mal.status_codes[status]}, action="add")
def spinner(control): animation = ''.join(x * 5 for x in animation_diagram) if not sys.stdout.isatty(): # not send to pipe/redirection return anim = zip(cycle(animation), cycle(animation_spinner)) for n, start_end_anim in enumerate(anim): start, end = start_end_anim padding = '█' * int(20 * abs(sin(0.05 * (n + control.position)))) padding_colored = color.colorize(padding, 'cyan') banner = '{} {} {}'.format(start, control.message, end) banner_colored = color.colorize(banner, 'cyan') message = '\r' + padding_colored + banner_colored sys.stdout.write(message) time.sleep(0.05) sys.stdout.write('\r' + len(message) * ' ') sys.stdout.write(2 * len(message) * "\010") if control.done: control.position = n break sys.stdout.write(len(message) * ' ') sys.stdout.write('\r' + 2 * len(message) * "\010")
def anime_pprint(index, item, extra=False): """Pretty print an anime's information.""" padding = int(math.log10(index)) + 3 remaining_color = ('blue' if item['episode'] < item['total_episodes'] else 'green') remaining = '{episode}/{total_episodes}'.format_map(item) in_rewatching = ('#in-rewatching-{rewatching}'.format_map(item) if item['rewatching'] else '') template = { 'index': index, 'padding': ' ' * padding, 'status': MyAnimeList.status_names[item['status']].capitalize(), 'title': color.colorize(item['title'], 'red', 'bold'), 'remaining': color.colorize(remaining, remaining_color, 'bold'), 'score': color.score_color(item['score']), 'rewatching': (color.colorize(in_rewatching, 'yellow', 'bold')) } # add formating options for extra info if extra: template.update({ 'start': item['start_date'] if item['start_date'] != '0000-00-00' else 'NA', 'finish': item['finish_date'] if item['finish_date'] != '0000-00-00' else 'NA', 'tags': item['tags'] }) message_lines = [ "{index}: {title}".format_map(template), ("{padding}{status} at {remaining} episodes " "with score {score} {rewatching}".format_map(template)) ] # the extra information lines if extra: message_lines.extend([ "{padding}Started: {start} \t Finished: {finish}".format_map(template), "{padding}Tags: {tags}".format_map(template) ]) print('\n'.join(message_lines), "\n")
def start_end(entry, episode, total_episodes): """Fill details of anime if user just started it or finished it.""" if total_episodes == episode: entry['status'] = MyAnimeList.status_codes['completed'] entry['date_finish'] = date.today().strftime('%m%d%Y') print(color.colorize('Series completed!', 'green')) # set/change score user_score = input( 'Enter new score (leave blank to keep score at {}): '.format( entry.get('score', 0))).strip() if user_score: # do nothing if blank answer try: entry['score'] = int(user_score) except ValueError: print(color.colorize('Invalid score.', 'red')) elif episode == 1: entry['status'] = MyAnimeList.status_codes['watching'] entry['date_start'] = date.today().strftime('%m%d%Y') return entry
def create_credentials(): # logging messages login_header = color.colorize("-- MAL login", 'cyan') successful = color.colorize(':: valid credentials!', 'green') invalid = color.colorize(':: invalid credentials! try again', 'red') print(login_header) config = setup.config() if setup.LOGIN_SECTION not in config: config.add_section(setup.LOGIN_SECTION) config.set(setup.LOGIN_SECTION, 'username', input('Username: '******'password', getpass()) # confirm that account credentials are correct by trying to log in if MyAnimeList.login(config): # account is ok, create a config file with open(setup.CONFIG_PATH, 'w') as cfg: config.write(cfg) print(successful, 'saved in {}'.format(setup.CONFIG_PATH)) else: print(invalid) config = create_credentials() return config
def add(mal, regex, status="plan to watch"): """Add an entry to the user list.""" results = mal.search(regex) selected = select_item(results) print("Adding {title} to list as '{status}'".format( title=color.colorize(selected["title"], "yellow", "bold"), status=status) ) mal.update( selected["id"], {"status": mal.status_codes[status]}, action="add" )
def start_end(entry, episode, total_episodes): """Fill details of anime if user just started it or finished it.""" if total_episodes == episode: entry['status'] = MyAnimeList.status_codes['completed'] entry['date_finish'] = date.today().strftime('%m%d%Y') print(color.colorize('Series completed!', 'green')) # set/change score user_score = input( 'Enter new score (leave blank to keep score at {}): '.format( entry.get('score', 0)) ).strip() if user_score: # do nothing if blank answer try: entry['score'] = int(user_score) except ValueError: print(color.colorize('Invalid score.', 'red')) elif episode == 1: entry['status'] = MyAnimeList.status_codes['watching'] entry['date_start'] = date.today().strftime('%m%d%Y') return entry
def start_end(entry, episode, total_episodes): """Fill details of anime if user just started it or finished it.""" if total_episodes == episode: entry['status'] = MyAnimeList.status_codes['completed'] entry['date_finish'] = date.today().strftime('%m%d%Y') print(color.colorize('Series completed!', 'green')) score = int(input('Enter a score (or 0 for no score): ')) if score != 0: entry['score'] = score elif episode == 1: entry['status'] = MyAnimeList.status_codes['watching'] entry['date_start'] = date.today().strftime('%m%d%Y') return entry
def killed(): """Show a message if user terminated the program.""" message = ("\n ┑( ̄Д  ̄)┍ somebody seems killed me..." "\nw a s Y O U ?! ︵ヽ(`Д´)ノ︵ ") print(color.colorize(message, 'red'), file=sys.stderr) os._exit(1)
def point_color(color_name): return color.colorize("● ", color_name, "bold")
def stats(mal, username=None): """Print user anime stats.""" # get all the info animes = mal.list(stats=True, user=username) if not animes: print_error("Empty query", "username not found", "could not fetch list for user '{}'".format(username), kill=True) user_info = animes.pop("stats") # remove stats from anime list # gather all the numbers total_entries = len(animes) rewatched, episodes, mean_score, scored = 0, 0, 0, 0 for anime in animes.values(): episodes += anime["episode"] # this is watched episodes if anime["rewatching"] != 0: rewatched += anime["rewatching"] # take into account episodes seen in previous watchings episodes += anime["rewatching"] * anime["total_episodes"] if anime["score"] != 0: scored += 1 mean_score += anime["score"] if scored != 0: mean_score /= scored # added two for circle colored + space on each list line_size = 44 + 2 # ↑ code for calculating this was so messy I hardcoded instead # it's 20 spaces for each of the 'sides' and 4 spaces in between them # colored bar. borrowed the bar char from neofetch bar = "█" colors = ["green", "blue", "yellow", "red", "gray"] lists = ["watching", "completed", "onhold", "dropped", "plantowatch"] colored = str() if total_entries != 0: # to prevent division by zero for i, status in enumerate(lists): entries = int(user_info[status]) bars = round(line_size * (entries / total_entries)) colored += color.colorize(bar * bars, colors[i]) else: colored = color.colorize(bar * line_size, "white") # format the lines to print more easily afterwards template = { "days": user_info["days_spent_watching"], "mean_score": "{:.2f}".format(mean_score), "watching": user_info["watching"], "completed": user_info["completed"], "hold": user_info["onhold"], "plan": user_info["plantowatch"], "dropped": user_info["dropped"], "total_entries": str(total_entries), "episodes": str(episodes), "rewatched": str(rewatched), "padd": "{p}" # needed to format with padding afterwards } def point_color(color_name): return color.colorize("● ", color_name, "bold") lines = [ "Days: {days}{padd}Mean Score: {mean_score}", colored, (point_color("green"), ["Watching:{padd}{watching}", "Total Entries:{padd}{total_entries}"]), (point_color("blue"), ["Completed:{padd}{completed}", "Rewatched:{padd}{rewatched}"]), (point_color("yellow"), ["On-Hold:{padd}{hold}", "Episodes:{padd}{episodes}"]), (point_color("red"), ["Dropped:{padd}{dropped}"]), (point_color("gray"), ["Plan to Watch:{padd}{plan}"]) ] # add info to lines and format them to look nice def padd_str(string, final_size): return string.replace("{p}", " " * (final_size - len(string) + len("{p}"))) lines = [ padd_str(line.format_map(template), line_size) if not isinstance(line, tuple) else # first format each side, then add padding then join with the tab line[0] + (" " * 4).join(padd_str(side.format_map(template), 20) for side in line[1]) for line in lines ] print(color.colorize("Anime Stats", "white", "underline")) print("\n".join(lines))
def killed(): message = ("\n ┑( ̄Д  ̄)┍ somebody seems killed me..." "\nw a s Y O U ?! ︵ヽ(`Д´)ノ︵ ") print(color.colorize(message, 'red'), file=sys.stderr) os._exit(1)
def report_if_fails(response): if response != 200: print(color.colorize("Failed with HTTP: {}".format(response), 'red'))
def stats(mal, username=None): """Print user anime stats.""" # get all the info animes = mal.list(stats=True, user=username) if not animes: print_error("Empty query", "username not found", "could not fetch list for user '{}'".format(username), kill=True) user_info = animes.pop("stats") # remove stats from anime list # gather all the numbers total_entries = len(animes) rewatched, episodes, mean_score, scored = 0, 0, 0, 0 for anime in animes.values(): episodes += anime["episode"] # this is watched episodes if anime["rewatching"] != 0: rewatched += anime["rewatching"] # take into account episodes seen in previous watchings episodes += anime["rewatching"] * anime["total_episodes"] if anime["score"] != 0: scored += 1 mean_score += anime["score"] if scored != 0: mean_score /= scored # added two for circle colored + space on each list line_size = 44 + 2 # ↑ code for calculating this was so messy I hardcoded instead # it's 20 spaces for each of the 'sides' and 4 spaces in between them # colored bar. borrowed the bar char from neofetch bar = "█" colors = ["green", "blue", "yellow", "red", "gray"] lists = ["watching", "completed", "onhold", "dropped", "plantowatch"] colored = str() if total_entries != 0: # to prevent division by zero for i, status in enumerate(lists): entries = int(user_info[status]) bars = round(line_size * (entries / total_entries)) colored += color.colorize(bar * bars, colors[i]) else: colored = color.colorize(bar * line_size, "white") # format the lines to print more easily afterwards template = { "days": user_info["days_spent_watching"], "mean_score": "{:.2f}".format(mean_score), "watching": user_info["watching"], "completed": user_info["completed"], "hold": user_info["onhold"], "plan": user_info["plantowatch"], "dropped": user_info["dropped"], "total_entries": str(total_entries), "episodes": str(episodes), "rewatched": str(rewatched), "padd": "{p}" # needed to format with padding afterwards } def point_color(color_name): return color.colorize("● ", color_name, "bold") lines = [ "Days: {days}{padd}Mean Score: {mean_score}", colored, (point_color("green"), ["Watching:{padd}{watching}", "Total Entries:{padd}{total_entries}"]), (point_color("blue"), ["Completed:{padd}{completed}", "Rewatched:{padd}{rewatched}"]), (point_color("yellow"), ["On-Hold:{padd}{hold}", "Episodes:{padd}{episodes}"]), (point_color("red"), ["Dropped:{padd}{dropped}"]), (point_color("gray"), ["Plan to Watch:{padd}{plan}"]) ] # add info to lines and format them to look nice def padd_str(string, final_size): return string.replace("{p}", " " * (final_size - len(string) + len("{p}"))) lines = [ padd_str(line.format_map(template), line_size) if not isinstance(line, tuple) else # first format each side, then add padding then join with the tab line[0] + (" " * 4).join( padd_str(side.format_map(template), 20) for side in line[1]) for line in lines ] print(color.colorize("Anime Stats", "white", "underline")) print("\n".join(lines))
# # Copyright © Manoel Vilela # # import os from configparser import ConfigParser from getpass import getpass from mal.api import MyAnimeList from mal import color DEFAULT_FILE = '~/.myanimelist.init' DEFAULT_SECTION = 'mal' DEFAULT_PATH = os.path.expanduser(DEFAULT_FILE) LOGIN_HEADER = color.colorize("-- MAL login", 'cyan') SUCCESSFUL = color.colorize(':: valid credentials!', 'green') INVALID = color.colorize(':: invalid credentials! try again', 'red') def get_credentials(): config = ConfigParser() config.read(DEFAULT_PATH) if DEFAULT_SECTION not in config: config = create_credentials() return config[DEFAULT_SECTION] def create_credentials(): print(LOGIN_HEADER)