def copy(self): title = 'Copy files to %s' % Config.tv_dir choice, data = self.display_list(title, table_type='copy') click.echo() if choice == 'copy_all': copied_all = True for episode in data[1]: torrent_hash = episode[3] torrent_dir, torrent_name = os.path.split(episode[2]) click.echo('Copying: %s... ' % episode[1], nl=False) try: DownloadManager(torrent_hash, torrent_dir, torrent_name) except OSError as e: copied_all = False click.echo(tvu.style(str(e), fg='red')) else: click.echo(tvu.style('Done', fg='green')) if not copied_all: click.echo() click.echo('Error: Some files could not be copied.') else: selected = [i for i in data[1] if choice in i][0] torrent_hash = selected[3] torrent_dir, torrent_name = os.path.split(selected[2]) click.echo('Copying: %s... ' % selected[1], nl=False) try: DownloadManager(torrent_hash, torrent_dir, torrent_name) except OSError as e: click.echo(tvu.style(str(e), fg='red')) sys.exit(1) click.echo('Done')
def list_missing(today): """List episodes that are ready to download. """ send(te, v) fp = tvu.FancyPrint() shows = Shows() with click.progressbar( shows, item_show_func=tfunct, show_percent=False, show_eta=False, width=Config.pb.width, empty_char=tvu.style(Config.pb.empty_char, fg=Config.pb.dark, bg=Config.pb.dark), fill_char=tvu.style(Config.pb.fill_char, fg=Config.pb.light, bg=Config.pb.light), bar_template=Config.pb.template, ) as bar: for series in bar: if series.is_missing(today): fp.standard_print(series.show_missing()) fp.done()
def notify(self, *msg): msg = ', '.join([str(i) for i in msg]) base_pad = (math.ceil(len(msg) / Config.console_columns) * Config.console_columns) padding = ' ' * (base_pad - (len(msg) + len(self.name) + 2)) msg = tvu.style(msg, fg='green') sitename = tvu.style(self.name, fg='yellow') fullmsg = '%s: %s%s' % (sitename, msg, padding) click.echo('%s' % fullmsg, err=True)
def display_error(self, message): colors = Config.color click.echo() click.echo('%s %s' % ( style('[!]', fg=colors.warn.fg, bg=colors.warn.bg), style(message, fg=colors.warn.fg)) )
def tvol(no_cache, config_name): """Download and manage tv shows. Use `tvol COMMAND -h` to get help for each command. \b \/ TVOverlord source code is available at: [. ] https://github.com/8cylinder/tv-overlord ^ /^\\ Any feature requests or bug reports should go there. //^\\\\ -^-._.--.-^^-.____._^-.^._ """ Config.get_config_data(config_name) DB.configure() if Config.version_notification: if v.new_version(): msg = tvu.style(v.message, fg='green') click.secho(msg, err=True) te.ask() if no_cache: Config.use_cache = False else: Config.use_cache = True
def exists(self, filename): if filename is None: return '' elif os.path.exists(filename): filename = filename else: filename = style(filename, fg='black', strike=True) return filename
def test_each(self, search_string): engines = self.torrent_engines + self.newsgroup_engines indent = ' ' click.echo('Searching for: %s' % search_string) for engine in engines: search = engine.Provider() name = '%s (%s)' % (search.name, search.shortname) click.echo(style(name, bold=True)) results = search.search(search_string) click.echo(indent + style(search.url, fg='blue', ul=True)) results_count = str(len(results)) if results_count == '0': results_count = style(results_count, fg='red') else: results_count = style(results_count, fg='green') click.echo(indent + 'Search results: %s' % results_count)
def test_each(self, search_string, show_results): """Do a test search for each search engine""" engines = self.torrent_engines + self.newsgroup_engines indent = ' ' click.echo() click.echo('Searching for: %s' % search_string) for engine in engines: search = engine name = '%s (%s)' % (search.name, search.shortname) click.echo(tu.style(name, bold=True)) results = search.search(search_string) click.echo(indent + tu.style(search.url, fg='blue', ul=True)) results_count = str(len(results)) if results_count == '0': results_count = tu.style(results_count, fg='red') else: results_count = tu.style(results_count, fg='green') click.echo(indent + 'Search results: %s' % results_count) if show_results: for result in results: click.echo('%s* %s' % (indent, result[0]))
def show_missing(self): missing = self.missing if len(missing) == 0: return False ret = style(self.db_name, fg='green', bold=True) ret += ' - %s, %s' % (self.airsDayOfWeek, self.airsTime) ret += '\n' indent = ' ' missing_list = [] for s in missing: se = 'S%sE%s' % (s['season'].rjust(2, '0'), s['episode'].rjust( 2, '0')) missing_list.append(se) ret += textwrap.fill(', '.join(missing_list), width=int(self.console_columns), initial_indent=indent, subsequent_indent=indent) return ret
def show_missing(self): missing = self.missing if len(missing) == 0: return False ret = style(self.db_name, fg='green', bold=True) ret += ' - %s, %s' % (self.airs_dayofweek, self.airs_time) ret += '\n' indent = ' ' missing_list = [] for s in missing: se = 'S%sE%s' % (s['season'].rjust(2, '0'), s['episode'].rjust(2, '0')) missing_list.append(se) ret += textwrap.fill(', '.join(missing_list), width=int(self.console_columns), initial_indent=indent, subsequent_indent=indent) return ret
def generate(self): colors = Config.color.table title_bar = style( '|', fg=colors.bar.fg, bg=colors.header.bg,) bar = style( '|', fg=colors.bar.fg, ) # TITLE -------------------------------------------- title = ' %s' % self.table.title.text title = title.ljust(Config.console_columns) title = style(title, bold=True, fg=colors.title.fg, bg=colors.title.bg) click.echo(title) # HEADER ROW --------------------------------------- header = self.table.header header_row = [style(' ', bg=colors.header.bg, fg=colors.header.fg)] NUMBER_SPACE = 1 BAR_COUNT = len(header.widths) flex_width = (Config.console_columns - sum(header.widths) - NUMBER_SPACE - BAR_COUNT) for title, width in zip(header.titles, header.widths): if width == 0: width = flex_width title = title[:width].ljust(width) header_row.append( style(title, bg=colors.header.bg, fg=colors.header.fg, ) ) header_row = title_bar.join(header_row) click.echo(header_row) # BODY ROWS ----------------------------------------- key = """abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW""" key += """XYZ0123456789!#$%&()*+-./:;<=>?@[\\]^_`|}{"'~""" if self.table_type == 'download': options = '\nLetter, [s]kip, skip [r]est of show, [q]uit or [m]ark as downloaded: ' key = re.sub('[srqm]', '', key) elif self.table_type == 'nondb': options = '\nLetter or [q]uit: ' key = re.sub('[q]', '', key) elif self.table_type == 'copy': options = '\nLetter, [a]ll or [q]uit: ' key = re.sub('[aq]', '', key) elif self.table_type == 'redownload': options = '\nLetter or [q]uit: ' key = re.sub('[q]', '', key) self.table.body = self.table.body[:self.display_count] for row, counter in zip(self.table.body, key): # look through the title cell to see if any have 720 or # 1080 in the string and mark this row as high def. is_hidef = False if '720p' in row[0] or '1080p' in row[0]: is_hidef = True row_arr = [counter] for i, width, align in zip(row, header.widths, header.alignments): i = str(i) if width == 0: width = flex_width row_item = i row_item = U.snip(row_item, width) row_item = row_item.strip() if align == '<': row_item = row_item.ljust(width) if align == '>': row_item = row_item.rjust(width) if align == '=': row_item = row_item.center(width) else: row_item = row_item.ljust(width) # if hi def, set the foreground to green if is_hidef: row_item = style(row_item, fg=colors.hidef.fg) row_arr.append(row_item) click.echo(bar.join(row_arr)) # USER INPUT --------------------------------------- choice = False while not choice: choice = self.ask(options, key) return choice
def search(self, search_string, season=False, episode=False, date_search=None, search_type='torrent'): """ Return an array of values: [ [ ['Title string', 'search url'], [head1, head2, head3, id], [head1-width, head2-width, head3-width], [head1-alignment, head2-alignment, head3-alignment] ], [ [<column 1 data>, <column 2 data>, <column 3 data>, <id>], [<column 1 data>, <column 2 data>, <column 3 data>, <id>], # etc... ] ] """ self.season = season self.episode = episode self.show_name = search_string self.search_type = search_type click.echo() if self.search_type == 'torrent': header = [[search_string, ''], ['Name', 'Size', 'Date', 'Seeds', 'SE'], [0, 10, 12, 6, 2], ['<', '>', '<', '>', '<']] else: header = [[search_string, ''], ['Name', 'Size', 'Date', 'SE'], [0, 10, 12, 3], ['<', '>', '<', '<']] if self.search_type == 'torrent': engines = self.torrent_engines elif self.search_type == 'nzb': engines = self.newsgroup_engines else: raise ValueError('search_type can only be "torrent" or "nzb"') socket.setdefaulttimeout(10) # socket.setdefaulttimeout(0.1) episodes = [] with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: # For nzb's, the idx is needed so Provider.download knows # which engine was used. It's not needed for torrents # because there is no download method. res = { executor.submit(self.job, engine, search_string, season, episode, date_search, idx): engine for idx, engine in enumerate(engines) } names = [i.name for i in engines] names.sort() names = [' %s ' % i for i in names] names = [tu.style(i, fg='white', bg='red') for i in names] for future in concurrent.futures.as_completed(res): results = future.result() finished_name = results[-1] for i, e in enumerate(names): e = click.unstyle(e).strip() if e == finished_name: e = ' %s ' % e names[i] = tu.style(e, fg='white', bg='green') if date_search: title = '%s %s' % (search_string.strip(), date_search) else: title = '%s %s' % (search_string.strip(), tu.sxxexx(season, episode)) title = title.ljust(Config.console_columns) click.echo(tu.style(title, bold=True)) click.echo(' '.join(names)) # move up two lines click.echo('[%sA' % 3) episodes = episodes + results[:-1] # go up 3 lines to remove the progress bar click.echo('[%sA' % 2) if self.search_type == 'torrent': self.sort_torrents(episodes) if Config.filter_list: episodes = [i for i in episodes if self.filter_episode(i)] self.episodes = episodes # return search_results return [header] + [episodes]
def search(self, search_string, season=False, episode=False, date_search=None, search_type='torrent'): """ Return an array of values: [ [ ['Title string', 'search url'], [head1, head2, head3, id], [head1-width, head2-width, head3-width], [head1-alignment, head2-alignment, head3-alignment] ], [ [<column 1 data>, <column 2 data>, <column 3 data>, <id>], [<column 1 data>, <column 2 data>, <column 3 data>, <id>], # etc... ] ] """ self.season = season self.episode = episode self.show_name = search_string self.search_type = search_type click.echo() if self.search_type == 'torrent': header = [ [search_string, ''], ['Name', 'Size', 'Date', 'Seeds', 'SE'], [0, 10, 12, 6, 2], ['<', '>', '<', '>', '<']] else: header = [ [search_string, ''], ['Name', 'Size', 'Date', 'SE'], [0, 10, 12, 3], ['<', '>', '<', '<']] if self.search_type == 'torrent': engines = self.torrent_engines elif self.search_type == 'nzb': engines = self.newsgroup_engines else: raise ValueError('search_type can only be "torrent" or "nzb"') # socket.setdefaulttimeout(5) # socket.setdefaulttimeout(0.1) episodes = [] with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: # For nzb's, the idx is needed so Provider.download knows # which engine was used. It's not needed for torrents # because there is no download method. res = { executor.submit( self.job, engine, search_string, season, episode, date_search, idx ): engine for idx, engine in enumerate(engines) } names = [i.name for i in engines] names.sort() names = [' %s ' % i for i in names] names = [tu.style(i, fg='white', bg='red') for i in names] for future in concurrent.futures.as_completed(res): results = future.result() finished_name = results[-1] for i, e in enumerate(names): e = click.unstyle(e).strip() if e == finished_name: e = ' %s ' % e names[i] = tu.style(e, fg='white', bg='green') if date_search: title = '%s %s' % (search_string.strip(), date_search) else: title = '%s %s' % (search_string.strip(), tu.sxxexx(season, episode)) title = title.ljust(Config.console_columns) click.echo(tu.style(title, bold=True)) click.echo(' '.join(names)) # move up two lines click.echo('[%sA' % 3) episodes = episodes + results[:-1] # go up 3 lines to remove the progress bar click.echo('[%sA' % 2) if self.search_type == 'torrent': self.sort_torrents(episodes) self.episodes = episodes # return search_results return [header] + [episodes]
def calendar(show_name, show_all, sort_by_next, no_color, days): """Display a calendar of upcoming episodes. If SHOW_NAME is used, it will display a calendar for any shows that match that string. --days can be one number or two numbers. If one number is used, it will show that many days ahead. If two numbers are used, the first number is where the calendar will start and the second number is where it will end. The two number must be seperated by a comma with no spaces. \b --days 10 will show from today to 10 days in the future --days 10,20 will start ten days from now and then show 20 days ahead. --days -20,10 will go back 20 days from today and then show ahead from there. """ if no_color: use_color = False else: use_color = True # set colors for ui elements if Config.is_win: foreground = "white" header_color = "blue" date_color_1 = "blue" date_color_2 = 0 title_color_1 = "blue" title_color_2 = 0 else: foreground = 225 header_color = 17 date_color_1 = 17 date_color_2 = 0 title_color_1 = 18 title_color_2 = 0 title_width = 20 # width of show titles column spacer = " " # can be any string, any length today = datetime.datetime.today() if days: days = days.split(",") days = [int(x) for x in days] if len(days) == 2: today = today + datetime.timedelta(days=days[0]) calendar_columns = days[1] if len(days) == 1: calendar_columns = days[0] else: calendar_columns = Config.console_columns - (title_width + len(spacer)) # Days_chars can be any string of seven chars. eg: 'mtwtfSS' days_chars = ".....::" # first char is monday monthstart = "|" # marker used to indicate the begining of month # build date title row months_row = today.strftime("%b") + (" " * calendar_columns) days_row = "" daybefore = today - datetime.timedelta(days=1) for days in range(calendar_columns): cur_date = today + datetime.timedelta(days=days) if cur_date.month != daybefore.month: days_row += monthstart month = cur_date.strftime("%b") month_len = len(month) months_row = months_row[:days] + month + months_row[(days + month_len) :] else: days_row += days_chars[cur_date.weekday()] daybefore = cur_date months_row = months_row[:calendar_columns] # chop off any extra spaces created by adding the months if use_color: months_row = style(months_row, fg=foreground, bg=header_color) days_row = style(days_row, fg=foreground, bg=header_color) months_row = (" " * title_width) + (" " * len(spacer)) + months_row days_row = (" " * title_width) + (" " * len(spacer)) + days_row click.echo(months_row) click.echo(days_row) # build shows rows step = 3 color_row = False counter = 1 season_marker = "-" filter_date = "" filter_name = "" if sort_by_next: filter_date = True if show_name: filter_name = show_name show = Shows(name_filter=filter_name, by_date=filter_date) for show in show: broadcast_row = "" title = show.db_name[:title_width].ljust(title_width) has_episode = False first_display_date = True last_days_away = 0 last_date = 0 for i in show.series: # season for j in show.series[i]: # episode episode_number = show.series[i][j]["episodenumber"] b_date = show.series[i][j]["firstaired"] if not b_date: continue # some episode have no broadcast date? split_date = b_date.split("-") broadcast_date = datetime.datetime(int(split_date[0]), int(split_date[1]), int(split_date[2])) if broadcast_date == last_date: continue # sometimes multiple episodes have the same date, don't repeat them. last_date = broadcast_date if broadcast_date.date() < today.date(): continue # don't include episodes before today days_away = (broadcast_date - today).days + 1 if days_away >= calendar_columns: continue # don't include days after the width of the screen if show.series[i][j]["seasonnumber"] == "0": continue # not interested in season 0 episodes. if first_display_date: if int(episode_number) > 1: before_first = season_marker * days_away else: before_first = " " * days_away broadcast_row = before_first + episode_number first_display_date = False # set the next episode date in the db while we're here: show.set_next_episode(broadcast_date.date()) else: episode_char_len = len(str(int(episode_number) - 1)) broadcast_row = ( broadcast_row + (season_marker * (days_away - last_days_away - episode_char_len)) + episode_number ) last_days_away = days_away has_episode = True broadcast_row = broadcast_row[:calendar_columns].ljust(calendar_columns) if has_episode or show_all: if use_color and color_row: title = style(title, fg=foreground, bg=title_color_1) broadcast_row = style(broadcast_row, fg=foreground, bg=date_color_1) elif use_color and not color_row: title = style(title, fg=foreground, bg=title_color_2) broadcast_row = style(broadcast_row, fg=foreground, bg=date_color_2) row = title + spacer + broadcast_row click.echo(row) if counter >= step: counter = 0 color_row = True else: color_row = False counter += 1
def add_new(self, name): try: result = self.tvapi.search(name) except KeyError as e: click.echo( 'Show data returned from TheTVDB.com has an error in it.', err=True) sys.exit(1) if not result: click.echo('No show found.') return else: click.echo('Multiple shows found, type a number to select.') click.echo('Type "<ctrl> c" to cancel.') click.echo() indent = ' ' for index, show in enumerate(result): title = show['seriesname'] click.echo(' %2s. ' % (index + 1), nl=False) click.secho(title, bold=True) if 'overview' in show: click.echo(format_paragraphs( show['overview'], indent=indent)) if 'firstaired' in show: click.secho( '%sFirst aired: %s' % (indent, show['firstaired']), fg='green') click.echo() choice = click.prompt( 'Choose number (or [enter] for #1)', default=1, type=click.IntRange(min=1, max=len(result))) idchoice = choice - 1 show = result[idchoice] self.db_name = show['seriesname'] # name self._get_thetvdb_series_data() last_season = 1 last_episode = 0 for season in self.series: last_season = season for episode in self.series[season]: b_date = self.series[season][episode]['firstaired'] if not b_date: continue # some episodes have no broadcast date split_date = b_date.split('-') broadcast_date = datetime.datetime( int(split_date[0]), int(split_date[1]), int(split_date[2])) today = datetime.datetime.today() if broadcast_date > today: break last_episode = episode last_sxxexx = style(sxxexx(last_season, last_episode), bold=True) if int(last_season) == 1 and int(last_episode) == 0: se = 0 ep = 0 else: click.echo() click.echo('The last episode broadcast was %s.' % last_sxxexx) msg = 'Start downloading the [f]irst (or [enter]), [l]atest or season and episode?' start = click.prompt(msg, default='f') if start == 'f': se = ep = 0 elif start == 'l': se = last_season ep = last_episode else: try: se, ep = [int(i) for i in start.split()] except ValueError: sys.exit('Season and episode must be two numbers seperated by a space.') if ep > 0: ep -= 1 # episode in db is the NEXT episode msg = self._add_new_db(season=se, episode=ep) click.echo() click.echo(msg)
def info(show_name, show_all, sort_by_next, db_status, ask_inactive, show_links, synopsis): """Show information about your tv shows. SHOW_NAME can be a full or partial name of a show. If used, it will show information about any shows that match that string, else it will show informaton about all your shows. """ show_info = {} counter = 0 status = 'active' # When the user specifies a single show, turn on --show-all # because the show they are asking for might an inactive show # and turn on --synopsis and --show-links since its only one # show we may as well show everything filter_name = '' if show_name: show_all = True synopsis = True show_links = True filter_name = show_name db_status = 'all' if Config.is_win: colors = {'links': 'blue', 'ended': 'red', 'last': 'cyan', 'future': 'green'} else: colors = {'links': 20, 'ended': 'red', 'last': 48, 'future': 22} shows = Shows(name_filter=filter_name, status=db_status) for show in shows: title = show.db_name if show.status == 'Ended': status = style(show.status, fg=colors['ended']) else: status = '' # build first row of info for each show se = 'Last downloaded: S%sE%s' % ( str(show.db_current_season).rjust(2, '0'), str(show.db_last_episode).rjust(2, '0'), ) se = style(se, fg=colors['last']) imdb_url = thetvdb_url = '' if show_links: imdb_url = style('\n IMDB.com: http://imdb.com/title/%s' % show.imdb_id, fg=colors['links']) thetvdb_url = style('\n TheTVDB.com: http://thetvdb.com/?tab=series&id=%s' % show.id, fg=colors['links']) synopsis_text = None if synopsis and show.overview: paragraph = show.overview indent = ' ' paragraph = format_paragraphs(paragraph, indent=indent) synopsis_text = '\n%s' % paragraph first_row_a = [] fancy_title = style(title, bold=True) for i in [fancy_title + ',', se, status, imdb_url, thetvdb_url, synopsis_text]: if i: first_row_a.append(i) first_row = ' '.join(first_row_a) # build 'upcoming episodes' list today = datetime.datetime.today() first_time = True episodes_list = [] counter += 1 for i in show.series: # season for j in show.series[i]: # episode b_date = show.series[i][j]['firstaired'] if not b_date: continue # some episode have no broadcast date? split_date = b_date.split('-') broadcast_date = datetime.datetime( int(split_date[0]), int(split_date[1]), int(split_date[2])) if not show_all: if broadcast_date < today: continue future_date = date_parser.parse(b_date) diff = future_date - today fancy_date = future_date.strftime('%b %d') if broadcast_date >= today: episodes_list.append('S%sE%s, %s (%s)' % ( show.series[i][j]['seasonnumber'].rjust(2, '0'), show.series[i][j]['episodenumber'].rjust(2, '0'), fancy_date, diff.days + 1, )) if first_time: first_time = False if sort_by_next: sort_key = str(diff.days).rjust(5, '0') + str(counter) else: sort_key = show.db_name.replace('The ', '') if not first_time: if episodes_list: indent = ' ' episode_list = 'Future episodes: ' + ' - '.join(episodes_list) episodes = format_paragraphs(episode_list, indent=indent) show_info[sort_key] = first_row + '\n' + episodes else: show_info[sort_key] = first_row if ask_inactive: if show.status == 'Ended' and first_time: click.echo( '%s has ended, and all have been downloaded. Set as inactive? [y/n]: ' % title) set_status = click.getchar() click.echo(set_status) if set_status == 'y': show.set_inactive() keys = list(show_info.keys()) keys.sort() full_output = '' for i in keys: full_output = full_output + show_info[i] + '\n\n' # remove the last '\n\n' full_output = full_output.strip() if len(keys) < 4: click.echo(full_output) elif Config.is_win: click.echo(full_output) else: click.echo_via_pager(full_output)
def edit_db(search_str): try: import readline except ImportError: pass sql = 'SELECT * FROM shows WHERE name like :search' conn = sqlite3.connect(Config.db_file) conn.row_factory = tvu.dict_factory curs = conn.cursor() values = {'search': '%{}%'.format(search_str)} results = curs.execute(sql, values) data = [] for i in results: data.append(i) if len(data) == 0: sys.exit('"%s" not found' % search_str) elif len(data) > 1: click.echo('Multiple shows found, type a number to edit.') click.echo('Type "<ctrl> c" to cancel.') click.echo() for index, show in enumerate(data): click.echo(' %s. %s' % (index + 1, show['name'])) click.echo() choice = click.prompt( 'Choose number', default=1, type=click.IntRange(min=1, max=len(data))) idchoice = choice - 1 if idchoice not in range(len(data)): sys.exit('Invalid choice: %s' % choice) row = data[idchoice] else: row = data[0] editcolor = 'green' if Config.is_win else 31 dirty = False is_error = False click.echo() click.echo(tvu.format_paragraphs(''' While editing a field, hit <enter> in an empty field to leave it unchanged and skip to the next one. Type "<ctrl> c" to cancel all edits. The current value is shown in ()\'s beside the field name.''')) click.echo() title = '%s' % row['name'] click.echo(tvu.style(title, bold=True, ul=True)) click.echo() try: msg = tvu.style('Search engine name (%s): ', fg=editcolor) new_search_engine_name = input(msg % (row['search_engine_name'])) if not new_search_engine_name: new_search_engine_name = row['search_engine_name'] else: dirty = True msg = tvu.style('Current season (%s): ', fg=editcolor) new_season = input(msg % (row['season'])) if not new_season: new_season = str(row['season']) else: dirty = True msg = tvu.style('Last episode (%s): ', fg=editcolor) new_episode = input(msg % (row['episode'])) if not new_episode: new_episode = str(row['episode']) else: dirty = True msg = tvu.style('Status (%s): ', fg=editcolor) new_status = input(msg % (row['status'])) if not new_status: new_status = row['status'] else: dirty = True date_pretty = 'y' if row['search_by_date'] == 1 else 'n' msg = tvu.style('Search by date (%s) [y/n]: ', fg=editcolor) new_date = input(msg % (date_pretty)) if not new_date: new_date = date_pretty else: dirty = True new_format = row['date_format'] if new_date == 'y': click.echo(' The optional date format string can be any valid Python format string.') click.echo(' https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior') msg = tvu.style(' Date format string (%s): ', fg=editcolor) new_format = input(msg % (row['date_format'])) if not new_format: new_format = row['date_format'] else: dirty = True except KeyboardInterrupt: click.echo('\n\nDatabase edit cancelled.') sys.exit(0) click.echo() if dirty is False: click.echo('No changes made.') sys.exit(0) if not new_season.isdigit(): click.echo('Error: Season must be a number') is_error = True if not new_episode.isdigit(): click.echo('Error: Episode must be a number') is_error = True if new_status not in ['active', 'inactive']: click.echo('Error: Status must be either "active" or "inactive"') is_error = True if new_date not in ['y', 'n']: click.echo('Error: Search by date must be either "y" or "n"') is_error = True if is_error: sys.exit(1) if not click.confirm('Are these changes correct? (you can always change it back)', default='Y'): click.echo('\nDatabase edit cancelled.') sys.exit() sql = '''UPDATE shows SET season=:season, episode=:episode, status=:status, search_engine_name=:search_engine_name, search_by_date=:search_by_date, date_format=:date_format WHERE thetvdb_series_id=:tvdb_id''' row_values = { 'season': new_season, 'episode': new_episode, 'status': new_status, 'search_engine_name': new_search_engine_name, 'tvdb_id': row['thetvdb_series_id'], 'search_by_date': 1 if new_date == 'y' else 0, 'date_format': new_format, } curs.execute(sql, row_values) conn.commit() conn.close()
def add_new(self, name): try: result = self.tvapi.search(name) except KeyError as e: click.echo( 'Show data returned from TheTVDB.com has an error in it.', err=True) sys.exit(1) if not result: click.echo('No show found.') return else: click.echo('Multiple shows found, type a number to select.') click.echo('Type "<ctrl> c" to cancel.') click.echo() indent = ' ' for index, show in enumerate(result): title = show['seriesName'] click.echo(' %2s. ' % (index + 1), nl=False) click.secho(title, bold=True) if 'overview' in show: click.echo( format_paragraphs(show['overview'], indent=indent)) if 'firstaired' in show: click.secho('%sFirst aired: %s' % (indent, show['firstaired']), fg='green') click.echo() choice = click.prompt('Choose number (or [enter] for #1)', default=1, type=click.IntRange(min=1, max=len(result))) idchoice = choice - 1 show = result[idchoice] self.db_name = show['seriesName'] # name self._get_thetvdb_series_data() last_season = 1 last_episode = 0 for season in self.series: last_season = season for episode in self.series[season]: b_date = self.series[season][episode]['firstaired'] if not b_date: continue # some episodes have no broadcast date split_date = b_date.split('-') broadcast_date = datetime.datetime(int(split_date[0]), int(split_date[1]), int(split_date[2])) today = datetime.datetime.today() if broadcast_date > today: break last_episode = episode last_sxxexx = style(sxxexx(last_season, last_episode), bold=True) if int(last_season) == 1 and int(last_episode) == 0: se = 0 ep = 0 else: click.echo() click.echo('The last episode broadcast was %s.' % last_sxxexx) msg = 'Start downloading the [f]irst (or [enter]), [l]atest or season and episode?' start = click.prompt(msg, default='f') if start == 'f': se = ep = 0 elif start == 'l': se = last_season ep = last_episode else: try: se, ep = [int(i) for i in start.split()] except ValueError: sys.exit( 'Season and episode must be two numbers seperated by a space.' ) if ep > 0: ep -= 1 # episode in db is the NEXT episode msg = self._add_new_db(season=se, episode=ep) click.echo() click.echo(msg)
def generate(self): colors = Config.color.table title_bar = style( '|', fg=colors.bar.fg, bg=colors.header.bg, ) bar = style( '|', fg=colors.bar.fg, ) # TITLE -------------------------------------------- title = ' %s' % self.table.title.text title = title.ljust(Config.console_columns) title = style(title, bold=True, fg=colors.title.fg, bg=colors.title.bg) click.echo(title) # HEADER ROW --------------------------------------- header = self.table.header header_row = [style(' ', bg=colors.header.bg, fg=colors.header.fg)] NUMBER_SPACE = 1 BAR_COUNT = len(header.widths) flex_width = (Config.console_columns - sum(header.widths) - NUMBER_SPACE - BAR_COUNT) for title, width, alignment in zip(header.titles, header.widths, header.alignments): if width == 0: width = flex_width if alignment == '<': title = title[:width].ljust(width) elif alignment == '>': title = title[:width].rjust(width) elif alignment == '=': title = title[:width].center(width) else: title = title[:width].ljust(width) header_row.append( style( title, bg=colors.header.bg, fg=colors.header.fg, )) header_row = title_bar.join(header_row) click.echo(header_row) # BODY ROWS ----------------------------------------- # key has the s, r, q, m removed to not interfere with the # ask_user options. This list can have anything, as long as # they are single characters. This is aprox 90 characters. key = """abcdefghijklnoptuvwxyzABCDEFGHIJKLMNOPQRSTUVW""" key += """XYZ0123456789!#$%&()*+-./:;<=>?@[\\]^_`{|}"'~""" key = list(key) self.table.body = self.table.body[:self.display_count] for row, counter in zip(self.table.body, key): # look through the title cell to see if any have 720 or # 1080 in the string and mark this row as high def. is_hidef = False if '720p' in row[0] or '1080p' in row[0]: is_hidef = True row_arr = [counter] for i, width, align in zip(row, header.widths, header.alignments): i = str(i) if width == 0: width = flex_width row_item = i row_item = U.snip(row_item, width) row_item = row_item.strip() if align == '<': row_item = row_item.ljust(width) if align == '>': row_item = row_item.rjust(width) if align == '=': row_item = row_item.center(width) else: row_item = row_item.ljust(width) # if hi def, set the foreground to green if is_hidef: row_item = style(row_item, fg=colors.hidef.fg) row_arr.append(row_item) click.echo(bar.join(row_arr)) # USER INPUT --------------------------------------- choice = False while not choice: if self.is_postdownload: choice = self.ask_simple(key) else: if self.nondb: choice = self.ask_simple(key) else: choice = self.ask(key) return choice
def search(self, search_string, season=False, episode=False, search_type='torrent'): """ Return an array of values: [ [ ['Title string', 'search url'], [head1, head2, head3, id], [head1-width, head2-width, head3-width], [head1-alignment, head2-alignment, head3-alignment] ], [ [<column 1 data>, <column 2 data>, <column 3 data>, <id>], [<column 1 data>, <column 2 data>, <column 3 data>, <id>], # etc... ] ] """ self.season = season self.episode = episode self.show_name = search_string self.search_type = search_type click.echo() if self.search_type == 'torrent': header = [[search_string, ''], ['Name', 'Size', 'Date', 'Seeds', 'SE'], [0, 10, 12, 6, 2], ['<', '>', '<', '>', '<']] else: header = [[search_string, ''], ['Name', 'Size', 'Date', 'SE'], [0, 10, 12, 2], ['<', '>', '<', '<']] if self.search_type == 'torrent': engines = self.torrent_engines elif self.search_type == 'nzb': engines = self.newsgroup_engines else: raise ValueError('search_type can only be "torrent" or "nzb"') # socket.setdefaulttimeout(3.05) socket.setdefaulttimeout(1) episodes = [] with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: res = { executor.submit(self.job, engine, search_string, season, episode): engine for engine in engines } with click.progressbar( concurrent.futures.as_completed(res), label=U.style(' %s' % search_string, bold=True), empty_char=style(Config.pb.empty_char, fg=Config.pb.dark, bg=Config.pb.dark), fill_char=style(Config.pb.fill_char, fg=Config.pb.light, bg=Config.pb.light), length=len(engines), show_percent=False, show_eta=False, item_show_func=self.progress_title, width=Config.pb.width, bar_template=Config.pb.template, show_pos=True, ) as bar: for future in bar: results = future.result() # remove the search engine name from the end of # the results array that was added in self.job() # so progress_title() could make use of it. results = results[:-1] episodes = episodes + results # go up 3 lines to remove the progress bar click.echo('[%sA' % 3) if self.search_type == 'torrent': self.sort_torrents(episodes) # return search_results return [header] + [episodes]
def calendar(show_name, show_all, sort_by_next, no_color, days): """Display a calendar of upcoming episodes. If SHOW_NAME is used, it will display a calendar for any shows that match that string. --days can be one number or two numbers. If one number is used, it will show that many days ahead. If two numbers are used, the first number is where the calendar will start and the second number is where it will end. The two number must be seperated by a comma with no spaces. \b --days 10 will show from today to 10 days in the future --days 10,20 will start ten days from now and then show 20 days ahead. --days -20,10 will go back 20 days from today and then show ahead from there. """ if no_color: use_color = False else: use_color = True # set colors for ui elements if Config.is_win: foreground = 'white' header_color = 'blue' date_color_1 = 'blue' date_color_2 = 0 title_color_1 = 'blue' title_color_2 = 0 else: foreground = 225 header_color = 17 date_color_1 = 17 date_color_2 = 0 title_color_1 = 18 title_color_2 = 0 title_width = 20 # width of show titles column spacer = ' ' # can be any string, any length today = datetime.datetime.today() if days: days = days.split(',') days = [int(x) for x in days] if len(days) == 2: today = today + datetime.timedelta(days=days[0]) calendar_columns = days[1] if len(days) == 1: calendar_columns = days[0] else: calendar_columns = Config.console_columns - (title_width + len(spacer)) # Days_chars can be any string of seven chars. eg: 'mtwtfSS' days_chars = '.....::' # first char is monday monthstart = '|' # marker used to indicate the begining of month # build date title row months_row = today.strftime('%b') + (' ' * calendar_columns) days_row = '' daybefore = today - datetime.timedelta(days=1) for days in range(calendar_columns): cur_date = today + datetime.timedelta(days=days) if cur_date.month != daybefore.month: days_row += monthstart month = cur_date.strftime('%b') month_len = len(month) months_row = months_row[:days] + month + months_row[(days + month_len):] else: days_row += days_chars[cur_date.weekday()] daybefore = cur_date months_row = months_row[: calendar_columns] # chop off any extra spaces created by adding the months if use_color: months_row = style(months_row, fg=foreground, bg=header_color) days_row = style(days_row, fg=foreground, bg=header_color) months_row = (' ' * title_width) + (' ' * len(spacer)) + months_row days_row = (' ' * title_width) + (' ' * len(spacer)) + days_row click.echo(months_row) click.echo(days_row) # build shows rows step = 3 color_row = False counter = 1 season_marker = '-' filter_date = '' filter_name = '' if sort_by_next: filter_date = True if show_name: filter_name = show_name show = Shows(name_filter=filter_name, by_date=filter_date) for show in show: broadcast_row = '' title = show.db_name[:title_width].ljust(title_width) has_episode = False first_display_date = True last_days_away = 0 last_date = 0 for i in show.series: # season for j in show.series[i]: # episode episode_number = show.series[i][j]['episodenumber'] b_date = show.series[i][j]['firstaired'] if not b_date: continue # some episode have no broadcast date? split_date = b_date.split('-') broadcast_date = datetime.datetime(int(split_date[0]), int(split_date[1]), int(split_date[2])) if broadcast_date == last_date: continue # sometimes multiple episodes have the same date, don't repeat them. last_date = broadcast_date if broadcast_date.date() < today.date(): continue # don't include episodes before today days_away = (broadcast_date - today).days + 1 if days_away >= calendar_columns: continue # don't include days after the width of the screen if show.series[i][j]['seasonnumber'] == '0': continue # not interested in season 0 episodes. if first_display_date: if int(episode_number) > 1: before_first = season_marker * days_away else: before_first = ' ' * days_away broadcast_row = before_first + episode_number first_display_date = False # set the next episode date in the db while we're here: show.set_next_episode(broadcast_date.date()) else: episode_char_len = len(str(int(episode_number) - 1)) broadcast_row = broadcast_row + ( season_marker * (days_away - last_days_away - episode_char_len)) + episode_number last_days_away = days_away has_episode = True broadcast_row = broadcast_row[:calendar_columns].ljust( calendar_columns) if has_episode or show_all: if use_color and color_row: title = style(title, fg=foreground, bg=title_color_1) broadcast_row = style(broadcast_row, fg=foreground, bg=date_color_1) elif use_color and not color_row: title = style(title, fg=foreground, bg=title_color_2) broadcast_row = style(broadcast_row, fg=foreground, bg=date_color_2) row = title + spacer + broadcast_row click.echo(row) if counter >= step: counter = 0 color_row = True else: color_row = False counter += 1