def __init__(self, config_view_tabs, parent, root, tab_id): """ A GUI Widget that reads and sets config.ini settings :param parent: """ super().__init__(parent, root) self.root = root self.parent = parent self.tab_id = tab_id self.config_view_tabs = config_view_tabs # Populate options for the given tab if self.tab_id == 'GUI': self.add_config_tab_gui() elif self.tab_id == 'Views': self.add_config_tab_views() elif self.tab_id == 'Debug' and read_config('Debug', 'debug'): self.add_config_tab_debug() elif self.tab_id == 'Download' and read_config('Play', 'enabled'): self.add_config_tab_download() elif self.tab_id == 'Apps && Players': self.add_config_tab_apps() elif self.tab_id == "Time && Date": self.add_config_tab_datetime() elif self.tab_id == 'Logging': self.add_config_tab_logging() elif self.tab_id == 'Advanced': self.add_config_tab_advanced() self.init_ui()
def run(self): # Enable Qt built-in High DPI scaling attribute (must be set *before* creating a QApplication!) QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) app = QApplication(sys.argv) self.logger.info("Running Controller instance") vid_limit = read_config('Model', 'loaded_videos') start_with_stored_videos = read_config('Debug', 'start_with_stored_videos') main_model = MainModel([], vid_limit) if start_with_stored_videos: main_model.update_subfeed_videos_from_db() else: main_model.update_subfeed_videos_from_remote() main_model.update_playback_videos_from_db() self.logger.info( "Created MainModel: len(subscription_feed) = {}, vid_limit = {}". format(len(main_model.subfeed_videos), vid_limit)) self.logger.info("Created QApplication({})".format(sys.argv)) window = MainWindow(app, main_model) window.show() self.logger.info("Executing Qt Application") app.exec_() self.logger.info("*** APPLICATION EXIT ***\n")
def download_thumbnails_threaded(input_vid_list, progress_listener=None): download_thumbnails_threaded_logger = create_logger( __name__ + ".download_thumbnails_threaded") thread_list = [] thread_limit = int(read_config('Threading', 'img_threads')) force_dl_best = read_config('Thumbnails', 'force_download_best') if progress_listener: progress_listener.setText.emit('Downloading thumbnails') vid_list = [] chunk_size = math.ceil(len(input_vid_list) / thread_limit) for i in range(0, len(input_vid_list), max(chunk_size, 1)): vid_list.append(input_vid_list[i:i + chunk_size]) counter = 0 download_thumbnails_threaded_logger.info( "Starting thumbnail download threads for {} videos in {} threads". format(len(input_vid_list), len(vid_list))) for vid_list_chunk in vid_list: t = DownloadThumbnail(vid_list_chunk, force_dl_best=force_dl_best, progress_listener=progress_listener) thread_list.append(t) t.start() counter += 1 if progress_listener: progress_listener.updateProgress.emit() for t in thread_list: t.join()
def update_subfeed_videos_from_remote( self, filtered=True, refresh_type=LISTENER_SIGNAL_NORMAL_REFRESH): """ Updates Subscription feed video list from a remote source (likely YouTube API). :param filtered: Whether to filter out certain videos based on set boolean attributes. :param refresh_type: A signal determining whether it is a Normal (int(0)) or Deep (int(1)) refresh. This kwarg is not used here, but passed on to the refresh function. :return: """ self.logger.info("Reloading and getting newest videos from YouTube") try: if filtered: show_downloaded = not read_config('SubFeed', 'show_downloaded') show_dismissed = not read_config('GridView', 'show_dismissed') self.subfeed_videos = refresh_and_get_newest_videos( self.videos_limit, progress_listener=self.status_bar_listener, refresh_type=refresh_type, filter_discarded=show_dismissed, filter_downloaded=show_downloaded) self.subfeed_grid_view_listener.videosChanged.emit() else: self.videos = refresh_and_get_newest_videos( self.videos_limit, filtered, self.status_bar_listener, refresh_type=refresh_type) except SaneAbortedOperation as exc_sao: # FIXME: Send aborted operation signal back up to GUI self.logger.critical( "A SaneAbortedOperation exc occurred while updating subfeed from remote! Exceptions:" ) for exc in exc_sao.exceptions: self.logger.exception(str(exc), exc_info=exc_sao)
def update_subfeed_videos_from_db(self, filtered=True): """ Updates Subscription feed video list from DB. Updates the filter with values in model and calls static database (read operation) function which doesn't have direct access to the model object. :param filtered: :return: """ self.logger.info("Getting newest stored videos from DB") # FIXME: only does filtered videos if filtered: show_downloaded = read_config('SubFeed', 'show_downloaded') show_dismissed = read_config('GridView', 'show_dismissed') update_filter = () if not show_downloaded: update_filter += (~Video.downloaded, ) if not show_dismissed: update_filter += (~Video.discarded, ) self.subfeed_videos = get_db_videos_subfeed(self.videos_limit, filters=update_filter) self.subfeed_grid_view_listener.videosChanged.emit() else: self.videos = get_db_videos_subfeed(self.videos_limit, filtered)
def __init__(self, parent, root, main_model: MainModel): super(GridView, self).__init__(parent=parent) self.logger = create_logger(__name__) self.setMinimumSize(0, 0) self.parent = parent self.root = root # MainWindow self.clipboard = self.root.clipboard self.status_bar = self.root.status_bar self.buffer = 10 self.bar_correction = 0 self.main_model = main_model self.pref_tile_height = read_config('Gui', 'tile_pref_height') self.pref_tile_width = read_config('Gui', 'tile_pref_width') self.q_labels = {} self.grid = QGridLayout() self.items_x = read_config('Gui', 'grid_view_x') self.items_y = read_config('Gui', 'grid_view_y') self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.grid.setContentsMargins(5, 5, 0, 0) self.grid.setSpacing(2) self.grid.setAlignment(Qt.AlignTop) self.setLayout(self.grid) self.setAutoFillBackground(True) if root.bgcolor: self.set_bgcolor(root.bgcolor)
def contextMenuEvent(self, event): """ Override context menu event to set own custom menu :param event: :return: """ if self.download_progress_listener: menu = QMenu(self) is_paused = not self.download_progress_listener.threading_event.is_set() pause_action = None continue_dl_action = None retry_dl_action = None mark_dl_failed = None if is_paused and not self.failed: continue_dl_action = menu.addAction("Continue download") else: if not self.finished and not self.failed: pause_action = menu.addAction("Pause download") if self.failed or (is_paused and self.failed): retry_dl_action = menu.addAction("Retry failed download") if not self.finished: delete_incomplete_entry = menu.addAction("Delete incomplete entry") if read_config('Debug', 'debug'): if not self.failed: menu.addSeparator() mark_dl_failed = menu.addAction("Mark download as FAILED") action = menu.exec_(self.mapToGlobal(event.pos())) if not self.finished and not self.failed: if action == pause_action and pause_action: self.paused_download() elif action == continue_dl_action and continue_dl_action: self.resumed_download() elif action == delete_incomplete_entry: self.delete_incomplete_entry() if self.failed and not self.finished: if action == delete_incomplete_entry: self.delete_incomplete_entry() if self.failed: if action == retry_dl_action: self.logger.error("Implement retry failed download handling!") self.retry() # Highly experimental, brace for impact! if read_config('Debug', 'debug'): if not self.failed: if action == mark_dl_failed: self.logger.critical("DEBUG: Marking FAILED: {}".format(self.video)) # Send a custom Exception, due to failed_download requiring one by design (signal reasons). self.failed_download(Exception("Manually marked as failed."))
def run(self): """ Override threading.Thread.run() with its own code :return: """ try: # youtube = youtube_auth_keys() # self.videos = get_channel_uploads(self.youtube, channel_id) use_tests = read_config('Requests', 'use_tests') if self.deep_search: temp_videos = [] list_uploaded_videos_search(self.youtube, self.channel_id, temp_videos, self.search_pages) list_uploaded_videos(self.youtube, temp_videos, self.playlist_id, self.list_pages) self.merge_same_videos_in_list(temp_videos) self.videos.extend(temp_videos) elif use_tests: channel = db_session.query(Channel).get(self.channel_id) miss = read_config('Requests', 'miss_limit') pages = read_config('Requests', 'test_pages') extra_pages = read_config('Requests', 'extra_list_pages') list_pages = 0 list_videos = [] search_videos = [] for test in channel.tests: if test.test_pages > list_pages: list_pages = test.test_pages if test.test_miss < miss or test.test_pages > pages: db_session.remove() list_uploaded_videos_search(self.youtube, self.channel_id, search_videos, self.search_pages) break db_session.remove() list_uploaded_videos(self.youtube, list_videos, self.playlist_id, min(pages + extra_pages, list_pages + extra_pages)) if len(search_videos) > 0: return_videos = self.merge_two_videos_list_grab_info(list_videos, search_videos) else: return_videos = list_videos self.videos.extend(return_videos) else: use_playlist_items = read_config('Debug', 'use_playlistitems') if use_playlist_items: list_uploaded_videos(self.youtube, self.videos, self.playlist_id, self.list_pages) else: list_uploaded_videos_search(self.youtube, self.channel_id, self.videos, self.search_pages) except Exception as e: # Save the exception details, but don't rethrow. self.exc = e pass self.job_done = True
def __init__(self, parent, video, vid_id, clipboard, status_bar): QWidget.__init__(self, parent=parent) self.logger = create_logger(__name__) self.clipboard = clipboard self.status_bar = status_bar self.video = video self.id = vid_id self.parent = parent self.root = parent.root # MainWindow self.history = self.root.history self.pref_height = read_config('Gui', 'tile_pref_height') self.pref_width = read_config('Gui', 'tile_pref_width') # NB: If you don't use a fixed size tile loading becomes glitchy as it needs to stretch to size after painting. self.setFixedSize(self.pref_width, self.pref_height) self.layout = QGridLayout() self.layout.setSpacing(0) # Don't use Qt's "global padding" spacing. # Make sure layout items don't overlap self.layout.setContentsMargins(0, 0, 0, 0) self.thumbnail_label = self.init_thumbnail_tile() if read_config('GridView', 'tile_title_lines') != 0: self.title_label = TitleLabel(video.title, self) if read_config('GridView', 'tile_channel_lines') != 0: self.channel_label = ChannelLabel(video.channel_title, self) if read_config('GridView', 'tile_date_lines') != 0: self.date_label = DateLabel('', self) self.setFixedWidth(self.pref_width) # Use a blank QLabel as spacer item for increased control of spacing (avoids global padding). spacer_label = QLabel() spacer_label.setFixedHeight( read_config('GridView', 'tile_line_spacing')) # Add labels to layout self.layout.addWidget(self.thumbnail_label) if read_config('GridView', 'add_thumbnail_spacer'): # This should only be necessary when not using fixed size scaling for ThumbnailTile. self.layout.addWidget(spacer_label) if read_config('GridView', 'tile_title_lines') != 0: self.layout.addWidget(self.title_label) self.layout.addWidget(spacer_label) if read_config('GridView', 'tile_channel_lines') != 0: self.layout.addWidget(self.channel_label) self.layout.addWidget(spacer_label) if read_config('GridView', 'tile_date_lines') != 0: self.layout.addWidget(self.date_label) self.setLayout(self.layout) # Add video on the layout/tile. self.set_video(video)
def filter_playback_view_videos(): """ Applies filters to the PlaybackGridView Videos list based on config. :return: """ show_watched = read_config('GridView', 'show_watched') show_dismissed = read_config('GridView', 'show_dismissed') update_filter = (Video.downloaded, ) if not show_watched: update_filter += (or_(Video.watched == false(), Video.watched == None), ) if not show_dismissed: update_filter += (~Video.discarded, ) return update_filter
def add_tabs(self, tabs: list): """ Adds a ConfigScrollArea tab widgets to the ConfigView. :param tabs: :return: """ for tab in tabs: # Don't add tabs if explicitly disabled. if tab == 'Download' and not read_config('Play', 'enabled'): continue elif tab == 'Debug' and not read_config('Debug', 'debug'): continue self.add_tab(tab)
def __init__(self, main_window: QMainWindow, popup_dialog=None): super(SaneThemeHandler, self).__init__() self.logger = create_logger(__name__) self.main_window = main_window self.popup_dialog = popup_dialog self.themes = None # Theme lookup where key is the absolute path to variant filename and value is the corresponding SaneTheme. self.themes_by_variant_absolute_path = {} self.styles = None self.current_theme = None self.current_theme_idx = 0 self.current_style = None # Generate available themes and styles self.generate_themes() self.generate_styles() # Set the last used theme. variant_absolute_path = read_config('Theme', 'last_theme', literal_eval=False) if variant_absolute_path: self.logger.info( "Using 'last used' theme: {}".format(variant_absolute_path)) # Retrieve the respective theme dict. theme = self.get_theme_by_variant_absolute_path( variant_absolute_path) if theme is not None: # Set the theme self.set_theme(variant_absolute_path) else: self.logger.error( "Unable to restore last theme (INVALID: NoneType): {}". format(variant_absolute_path)) # Set the last used style. last_style = read_config('Theme', 'last_style', literal_eval=False) if last_style: self.logger.info("Using 'last used' style: {}".format(last_style)) self.set_style(last_style) # Apply custom user theme mod overrides self.apply_user_overrides()
def __init__(self, model): super().__init__() self.logger = create_logger(__name__ + '.YoutubeDirListener') self.model = model self.newFile.connect(self.new_file) self.manualCheck.connect(self.manual_check) disable_dir_observer = read_config('Play', 'disable_dir_listener') if not disable_dir_observer: path = read_config('Play', 'yt_file_path', literal_eval=False) event_handler = VidEventHandler(self) self.observer = Observer() self.observer.schedule(event_handler, path) self.observer.start()
def strf_delta(date_published, fmt=None): tdelta = relativedelta(date_published, datetime.datetime.utcnow()) d = { 'decadesdecades': "{0:02d}".format(int(abs(tdelta.years / 10))), 'decades': int(abs(tdelta.years) / 10), 'ydyd': "{0:02d}".format(abs(tdelta.years)), 'yd': abs(tdelta.years), 'mm': "{0:02d}".format(abs(tdelta.months)), 'm': abs(tdelta.months), 'dd': "{0:02d}".format(abs(tdelta.days)), 'd': abs(tdelta.days), 'HH': "{0:02d}".format(abs(tdelta.hours)), 'H': abs(tdelta.hours), 'MM': "{0:02d}".format(abs(tdelta.minutes)), 'M': abs(tdelta.minutes), 'SS': "{0:02d}".format(abs(tdelta.seconds)), 'S': abs(tdelta.seconds), 'f': abs(tdelta.microseconds) } if fmt is None: if int(abs(tdelta.years)) > 10: fmt = read_config('GridView', 'timedelta_format_decades', literal_eval=False) # Update years in relation to decade d['yd'] = d['yd'] - 10 d['ydyd'] = "{0:02d}".format(abs(d['yd'])) elif int(abs(tdelta.years)) > 0: fmt = read_config('GridView', 'timedelta_format_years', literal_eval=False) elif int(abs(tdelta.months)) > 0: fmt = read_config('GridView', 'timedelta_format_months', literal_eval=False) elif int(abs(tdelta.days)) > 0: fmt = read_config('GridView', 'timedelta_format_days', literal_eval=False) else: fmt = read_config('GridView', 'timedelta_format', literal_eval=False) t = DeltaTemplate(fmt) return t.substitute(**d)
def open_in_browser(self, mark_watched=True): """ Opens the video URL in a web browser, if none is specified it will guess the default using the webbrowser module. :param mark_watched: Whether or not to mark video as watched. :return: """ if mark_watched: self.mark_watched() self.logger.info('Playing {}, in web browser'.format(self.video)) specific_browser = read_config('Player', 'url_player', literal_eval=False) if specific_browser: popen_args = [specific_browser, self.video.url_video] if sys.platform.startswith('linux'): popen_args.insert(0, 'nohup') subprocess.Popen(popen_args, preexec_fn=os.setpgrp, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: subprocess.Popen(popen_args) else: webbrowser.open_new_tab(self.video.url_video)
def add_config_tab_download(self): # Section [Youtube-dl] if 'youtube_dl' in sys.modules: self.add_option_checkbox('Use youtube-dl?', 'Youtube-dl', 'use_youtube_dl', restart_check=False) # Section [Youtube-dl_proxies] _counter = 1 for proxy in get_options('Youtube-dl_proxies'): self.add_option_line_edit('Geoblock proxy #{}'.format(_counter), 'Youtube-dl_proxies', proxy, restart_check=False) _counter += 1 else: self.add_option_checkbox('Use youtube-dl?<br/>' '<b><font color=#EF6262>MODULE UNAVAILABLE! (Is it installed?)</font></b>', 'Youtube-dl', 'use_youtube_dl', disabled=True) # Section [Youtube-dl_opts] if has_section('Youtube-dl_opts') and 'youtube_dl' in sys.modules: if len(get_options('Youtube-dl_opts')) > 0: self.add_section('Youtube-DL options overrides (Config file only)') for option in get_options('Youtube-dl_opts'): value = read_config('Youtube-dl_opts', option) self.add_option_info("{}: ".format(option), "{}".format(value)) self.add_section('Postprocessing') self.add_option_checkbox('Prefer ffmpeg (over avconv)?', 'Postprocessing', 'prefer_ffmpeg', restart_check=False) self.add_option_line_edit('ffmpeg location', 'Postprocessing', 'ffmpeg_location', restart_check=False) self.add_option_checkbox('Embed metadata?', 'Postprocessing', 'embed_metadata', restart_check=False) if 'youtube_dl' not in sys.modules: self.add_option_info_restart_required() self.add_option_line_edit('YouTube video directory', 'Play', 'yt_file_path', restart_check=False) self.add_option_checkbox('Disable directory listener (inotify)', 'Play', 'disable_dir_listener')
def youtube_auth_oauth(): """ Authorize the request using OAuth and store authorization credentials. OAuth is required for most higher level user actions like accessing user's subscriptions. :return: """ logger.info("OAuth: Authorising API...") flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES) if mutable_settings.using_gui: try: credentials = flow.run_local_server(host='localhost', port=read_config('Authentication', 'oauth2_local_server_port', literal_eval=True), authorization_prompt_message='Please visit this URL: {url}', success_message='The auth flow is complete; you may close this window.', open_browser=True) except MissingCodeError as exc_mce: logger.exception("A MissingCodeError Exception occurred during OAuth2", exc_info=exc_mce) return None else: credentials = flow.run_console() logger.info("OAuth: Instantiated flow (console)") # Note: If you try to send in requestBuilder here it will fail, but OAuth isn't threaded so it should be fine... return build(API_SERVICE_NAME, API_VERSION, credentials=credentials)
def add_config_tab_gui(self): self.add_option_line_edit('Videos to load by default', 'Model', 'loaded_videos', cfg_validator=QIntValidator()) self.add_option_checkbox('Grey background on old (1d+) videos', 'Gui', 'grey_old_videos') self.add_option_line_edit('Grid tile height (px)', 'Gui', 'tile_pref_height', cfg_validator=QIntValidator()) self.add_option_line_edit('Grid tile width (px)', 'Gui', 'tile_pref_width', cfg_validator=QIntValidator()) self.add_option_line_edit('Grid tile overlay height (%)', 'Gui', 'tile_overlay_height_pct', cfg_validator=QIntValidator()) self.add_option_line_edit('Grid tile overlay width (%)', 'Gui', 'tile_overlay_width_pct', cfg_validator=QIntValidator()) self.add_option_checkbox('Embed thumbnails in tooltips', 'Gui', 'tooltip_pictures') tooltip_thumb_disabled = not read_config('Gui', 'tooltip_pictures') self.add_option_line_edit('\tTooltip picture width', 'Gui', 'tooltip_picture_width', cfg_validator=QIntValidator(), disabled=tooltip_thumb_disabled) self.add_option_line_edit('\tTooltip picture height', 'Gui', 'tooltip_picture_height', cfg_validator=QIntValidator(), disabled=tooltip_thumb_disabled) self.add_option_combobox('\tTooltip picture font size', 'Gui', 'tooltip_picture_size', TT_FONT_SIZES, disabled=tooltip_thumb_disabled) self.add_option_checkbox('Keep Aspect Ratio on resized thumbnails', 'Gui', 'keep_thumb_ar', restart_check=False) self.add_option_checkbox('Auto copy to clipboard', 'Gui', 'enable_auto_copy_to_clipboard', restart_check=False) self.add_section('{}Theme{}'.format(self.deco_l, self.deco_r)) self.add_option_line_edit('Set custom background color hexadecimal <br/>' '(only works in default theme. Ex: #ffffff for white bg)', 'Gui', 'bgcolor', cfg_validator=QRegExpValidator(QRegExp(HEXADECIMAL_COLOR_REGEX))) self.add_option_button('Clear bgcolor', 'Clears the background color setting ', 'Gui', 'bgcolor', tooltip='(required due to validator shenanigans)', clear=True) self.add_option_checkbox('Use darkmode icon set', 'Gui', 'darkmode_icons') self.add_option_line_edit('Toolbar icon size modifier (Useful on High DPI displays)', 'Gui', 'toolbar_icon_size_modifier', actions=[self.root.update_toolbar_size, self.root.respawn_menubar_and_toolbar]) self.add_option_info_restart_required()
def create_file_handler(log_file=LOG_FILE, formatter=FORMATTER): """ Creates *the* (singular) file handler for logging to text file. File handler needs to be global and a singular instance, to avoid spamming FDs for each create_logger() call. :param log_file: :param formatter: :return: """ global LOG_FILE_HANDLER # Only create one instance of the file handler if not read_config('Logging', 'use_socket_log') and LOG_FILE_HANDLER is None: logfile_path = os.path.join(LOG_DIR, log_file) # Make sure logs dir exists, if not create it. if not os.path.isdir(LOG_DIR): os.makedirs(LOG_DIR) # Make sure logfile exists, if not create it. if not os.path.isfile(logfile_path): open(logfile_path, 'a').close() LOG_FILE_HANDLER = logging.FileHandler(logfile_path, encoding="UTF-8") LOG_FILE_HANDLER.setLevel(logging.DEBUG) LOG_FILE_HANDLER.setFormatter(formatter)
def get_subs(self): """ Retrieve Channels table from DB :return: """ self.logger.info("Getting subscriptions") self.subs = get_subscriptions(read_config('Debug', 'cached_subs'))
def __init__(self, text, parent): font = QFont() font.fromString( read_config("Fonts", "video_channel_font", literal_eval=False)) ElidedLabel.__init__(self, text, parent, font, CFG_LINES_ENTRY, CFG_ELIDED_MOD_ENTRY)
def force_download_best(video): vid_path = video.thumbnail_path url = 'https://i.ytimg.com/vi/{vid_id}/'.format_map( defaultdict(vid_id=video.video_id)) for i in range(5): quality = read_config('Thumbnails', '{}'.format(i)) if quality == 'maxres': temp_url = url + '{url_quality}.jpg'.format_map( defaultdict(url_quality='maxresdefault')) if download_thumb_file(temp_url, vid_path, crop=False, quality=quality, check_404=True): # Got 404 image, try lower quality break if quality == 'standard': temp_url = url + '{url_quality}.jpg'.format_map( defaultdict(url_quality='sddefault')) if download_thumb_file(temp_url, vid_path, crop=True, quality=quality, check_404=True): # Got 404 image, try lower quality break if quality == 'high': temp_url = url + '{url_quality}.jpg'.format_map( defaultdict(url_quality='hqdefault')) if download_thumb_file(temp_url, vid_path, crop=True, quality=quality, check_404=True): # Got 404 image, try lower quality break if quality == 'medium': temp_url = url + '{url_quality}.jpg'.format_map( defaultdict(url_quality='mqdefault')) if download_thumb_file(temp_url, vid_path, crop=False, quality=quality, check_404=True): # Got 404 image, try lower quality break if quality == 'default': temp_url = url + '{url_quality}.jpg'.format_map( defaultdict(url_quality='default')) if download_thumb_file(temp_url, vid_path, crop=True, quality=quality, check_404=True): # Got 404 image, try lower quality... Oh wait there is none! uh-oh.... logger.error( "ERROR: force_download_best() tried to go lower than 'default' quality!" ) break
def set_tool_tip(self): if not read_config('Debug', 'disable_tooltips'): if read_config('Gui', 'tooltip_pictures'): text_element = read_config('Gui', 'tooltip_picture_size') thumb_width = read_config('Gui', 'tooltip_picture_width') thumb_height = read_config('Gui', 'tooltip_picture_height') resized_thumb = resize_thumbnail(self.video.thumbnail_path, thumb_width, thumb_height) self.setToolTip( "<{} style='text-align:center;'><img src={} style='float:below'><br/>{}: {}</{}>" .format(text_element, resized_thumb, self.video.channel_title, self.video.title, text_element)) else: self.setToolTip("{}: {}".format(self.video.channel_title, self.video.title))
def get_best_thumbnail(vid): for i in range(5): quality = read_config('Thumbnails', '{}'.format(i)) if quality in vid.thumbnails: return_dict = vid.thumbnails[quality] return_dict.update({'quality': quality}) return return_dict return {}
def add_overlay(self, painter, thumb): """ Override inherited class to set custom overlay labels on thumbnail tiles. Since only one overlay can be clearly displayed at a time it checks which to set through a set of if cases ranking highest to lowest priority label. :param painter: :param thumb: :return: """ # Overlay conditions watched = read_config('GridView', 'show_watched') and self.parent.video.watched dismissed = read_config( 'GridView', 'show_dismissed') and self.parent.video.discarded downloaded = read_config( 'SubFeed', 'show_downloaded') and self.parent.video.downloaded missed = self.parent.video.missed new = self.parent.video.new if downloaded or watched or dismissed or missed or new: if self.parent.video.downloaded: overlay = QPixmap(OVERLAY_DOWNLOADED_PATH) elif self.parent.video.watched: overlay = QPixmap(OVERLAY_WATCHED_PATH) elif self.parent.video.discarded: overlay = QPixmap(OVERLAY_DISCARDED_PATH) elif missed: overlay = QPixmap(OVERLAY_MISSED_PATH) else: overlay = QPixmap(OVERLAY_NEW_PATH) overlay_h = read_config( 'Gui', 'tile_overlay_height_pct', literal_eval=True) / 100 overlay_w = read_config( 'Gui', 'tile_overlay_width_pct', literal_eval=True) / 100 resize_ratio = min(thumb.width() * overlay_w / thumb.width(), thumb.height() * overlay_h / thumb.height()) new_size = QSize(thumb.width() * resize_ratio, thumb.height() * resize_ratio) overlay = overlay.scaled(new_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) point = QPoint(thumb.width() - overlay.width(), 0) painter.drawPixmap(point, overlay)
def sort_playback_view_videos(self): """ Applies a sort-by rule to the PlaybackGridView videos list. update_sort is a tuple of priority sort categories, first element is highest, last is lowest. update_sort += operations requires at least two items on rhs. :return: """ sort_by_ascending_date = read_config('PlaySort', 'ascending_date') sort_by_channel = read_config('PlaySort', 'by_channel') self.logger.info( "Sorting PlaybackGridView Videos: date = {} | channel = {}".format( sort_by_ascending_date, sort_by_channel)) update_sort = (asc(Video.watch_prio), ) # Sort-by ascending date if sort_by_ascending_date: update_sort += (asc(Video.date_downloaded), asc(Video.date_published)) # Sort-by channel name (implied by default: then descending date) if sort_by_channel: update_sort += (desc(Video.channel_title), ) # Sort-by channel name then ascending date # FIXME: Implement handling both sorts toggled if sort_by_channel and sort_by_ascending_date: # update_sort += (asc(Video.channel_title),) self.logger.debug5("By-Channel|By-date update_sort: {}".format( str(update_sort))) for t in update_sort: self.logger.debug5(t.compile(dialect=postgresql.dialect())) # FIXME: workaround for not handling both: disable channel sort if both toggled, and run date sort set_config('PlaySort', 'by_channel', format(not read_config('PlaySort', 'by_channel'))) sort_by_channel = read_config('PlaySort', 'by_channel') update_sort += (asc(Video.date_downloaded), asc(Video.date_published)) # DEFAULT: Sort-by descending date else: update_sort += (desc(Video.date_downloaded), desc(Video.date_published)) self.logger.info( "Sorted PlaybackGridView Videos: date = {} | channel = {}".format( sort_by_ascending_date, sort_by_channel)) return update_sort
def __init__(self, text, parent, font: QFont, cfg_lines_entry, cfg_elided_mod_entry): """ Elided label (superclass). :param text: String to put on QLabel. :param parent: Parent ptr. :param cfg_lines_entry: QFont font to use. """ QLabel.__init__(self, text) self.parent = parent # Unescape HTML/XML codes, if any (usually happens with youtube.search() results) text = BeautifulSoup(text, "html.parser").text self.setFont(font) # Set label type independent config entries self.cfg_lines_entry: list = cfg_lines_entry self.cfg_elided_mod_entry: list = cfg_elided_mod_entry # Elided overwrites the original, so we need to keep a copy. self.original_text = text # Get font metrics/info. metrics = QFontMetrics(self.font()) # Lines of text to show (determines height of title text item). lines = read_config(*self.cfg_lines_entry) # Offset the unicode because it has tall characters and its line spacing is thus larger than ASCII's. # # If set to 2 there will be 1px clearing beneath unicode, # but ASCII will show 1px of its supposedly cut-off next line. unicode_height_offset = read_config('GridView', 'tile_unicode_line_height_offset') # Set height equal to lines and add some newline spacing for unicode. self.setFixedHeight((metrics.height() * lines) + (unicode_height_offset * lines)) # Set alignment and enable word wrapping so the text newlines instead of continuing OOB self.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) self.setWordWrap(True) # Finally, set the text string. self.setText(text, elided=True)
def mark_downloaded(self): """ Mark the video as downloaded :return: """ logger.info('Mark downloaded: {:2d}: {}'.format(self.id, self.video)) update_plaintext_history('Downloaded: {}'.format(self.video)) self.video.date_downloaded = datetime.datetime.utcnow() if read_config('Gui', 'enable_auto_copy_to_clipboard'): self.copy_url() if read_config('Youtube-dl', 'use_youtube_dl'): self.status_bar.showMessage( 'Downloading video with youtube-dl: {}'.format(self.video)) self.parent.main_model.playback_grid_view_listener.tileDownloaded.emit( self.video) # Update Subfeed to remove the video from its list unless show_downloaded=True. if not read_config('SubFeed', 'show_downloaded'): self.parent.main_model.subfeed_grid_view_listener.videosChanged.emit( )
def scroll_reached_end(self): """ Reaction to a GridView scrollbar reaching the end. If there are more videos in the list, load a videos_limit amount of them. :return: """ add_value = read_config("Model", "loaded_videos") self.model.videos_limit = self.model.videos_limit + add_value self.update_from_db()
def __init__(self, videos, videos_limit): super().__init__() self.logger = create_logger(__name__) self.videos_limit = videos_limit self.playview_videos_limit = videos_limit self.videos = videos self.subfeed_videos = [] self.subfeed_videos_removed = {} self.playview_videos = [] self.playview_videos_removed = {} self.download_progress_signals = [] self.logger.info("Creating listeners and threads") self.playback_grid_view_listener = PlaybackGridViewListener(self) self.playback_grid_thread = QThread() self.playback_grid_thread.setObjectName('playback_grid_thread') self.playback_grid_view_listener.moveToThread( self.playback_grid_thread) self.playback_grid_thread.start() self.subfeed_grid_view_listener = SubfeedGridViewListener(self) self.subfeed_grid_thread = QThread() self.subfeed_grid_thread.setObjectName('subfeed_grid_thread') self.subfeed_grid_view_listener.moveToThread(self.subfeed_grid_thread) self.subfeed_grid_thread.start() self.database_listener = DatabaseListener(self) self.db_thread = QThread() self.db_thread.setObjectName('db_thread') self.database_listener.moveToThread(self.db_thread) self.db_thread.start() self.main_window_listener = MainWindowListener(self) self.main_w_thread = QThread() self.main_w_thread.setObjectName('main_w_thread') self.main_window_listener.moveToThread(self.main_w_thread) self.main_w_thread.start() self.download_handler = DownloadViewListener(self) self.download_thread = QThread() self.download_thread.setObjectName('download_thread') self.download_handler.moveToThread(self.download_thread) self.download_thread.start() if read_config("Play", "yt_file_path", literal_eval=False): self.yt_dir_listener = YoutubeDirListener(self) self.yt_dir_thread = QThread() self.yt_dir_thread.setObjectName('yt_dir_thread') self.yt_dir_listener.moveToThread(self.yt_dir_thread) self.yt_dir_thread.start() else: self.logger.warning( "No youtube file path provided, directory listener is disabled" ) self.yt_dir_listener = None