예제 #1
0
    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()
예제 #2
0
    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")
예제 #3
0
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()
예제 #4
0
    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)
예제 #5
0
    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)
예제 #6
0
    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)
예제 #7
0
    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."))
예제 #8
0
    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
예제 #9
0
    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)
예제 #10
0
 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
예제 #11
0
    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)
예제 #12
0
    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()
예제 #13
0
    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()
예제 #14
0
    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)
예제 #15
0
 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)
예제 #16
0
    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')
예제 #17
0
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)
예제 #18
0
 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()
예제 #19
0
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'))
예제 #21
0
    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)
예제 #22
0
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
예제 #23
0
    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))
예제 #24
0
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 {}
예제 #25
0
    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)
예제 #26
0
    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
예제 #27
0
    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)
예제 #28
0
 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(
         )
예제 #29
0
    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()
예제 #30
0
    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