From ac738687337478015093a1550b0f994178538a06 Mon Sep 17 00:00:00 2001 From: A S Lewis Date: Thu, 13 Apr 2023 16:24:43 +0100 Subject: [PATCH] Update to v2.4.370 --- build/lib/tartube/__init__.py | 0 build/lib/tartube/classes.py | 85 - build/lib/tartube/config.py | 35439 ----------------------- build/lib/tartube/dialogue.py | 522 - build/lib/tartube/downloads.py | 11434 -------- build/lib/tartube/ffmpeg_tartube.py | 1261 - build/lib/tartube/files.py | 171 - build/lib/tartube/formats.py | 1257 - build/lib/tartube/info.py | 628 - build/lib/tartube/mainapp.py | 28121 ------------------ build/lib/tartube/mainwin.py | 39151 -------------------------- build/lib/tartube/media.py | 4652 --- build/lib/tartube/options.py | 2160 -- build/lib/tartube/process.py | 1012 - build/lib/tartube/refresh.py | 618 - build/lib/tartube/tartube | 167 - build/lib/tartube/tidy.py | 1636 -- build/lib/tartube/updates.py | 1079 - build/lib/tartube/utils.py | 4787 ---- build/lib/tartube/wizwin.py | 4123 --- build/lib/tartube/xdg_tartube.py | 183 - build/scripts-3.10/tartube | 167 - 22 files changed, 138653 deletions(-) delete mode 100644 build/lib/tartube/__init__.py delete mode 100644 build/lib/tartube/classes.py delete mode 100644 build/lib/tartube/config.py delete mode 100644 build/lib/tartube/dialogue.py delete mode 100644 build/lib/tartube/downloads.py delete mode 100644 build/lib/tartube/ffmpeg_tartube.py delete mode 100644 build/lib/tartube/files.py delete mode 100644 build/lib/tartube/formats.py delete mode 100644 build/lib/tartube/info.py delete mode 100644 build/lib/tartube/mainapp.py delete mode 100644 build/lib/tartube/mainwin.py delete mode 100644 build/lib/tartube/media.py delete mode 100644 build/lib/tartube/options.py delete mode 100644 build/lib/tartube/process.py delete mode 100644 build/lib/tartube/refresh.py delete mode 100644 build/lib/tartube/tartube delete mode 100644 build/lib/tartube/tidy.py delete mode 100644 build/lib/tartube/updates.py delete mode 100644 build/lib/tartube/utils.py delete mode 100644 build/lib/tartube/wizwin.py delete mode 100644 build/lib/tartube/xdg_tartube.py delete mode 100755 build/scripts-3.10/tartube diff --git a/build/lib/tartube/__init__.py b/build/lib/tartube/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/build/lib/tartube/classes.py b/build/lib/tartube/classes.py deleted file mode 100644 index 114caea7..00000000 --- a/build/lib/tartube/classes.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Classes imported and slightly modified for use by Tartube.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import re -import textwrap - - -# Import our modules -# ... - - -# Classes - - -class ModTextWrapper(textwrap.TextWrapper): - - """Python class to modify the behaviour of textwrap by Gregory P. Ward. - - If 'break_on_hyphens' is specified, wrap the text on both hyphens and - forward slashes. In that way, we can break up long URLs in calls to - utils.tidy_up_long_descrip() and utils.tidy_up_long_string(). - - v2.3.606: Modified the '_whitespace' field to allow wrapping after an - underline. - """ - - def _split(self, text): - -# _whitespace = '\t\n\x0b\x0c\r\_ ' - _whitespace = '\t\n\x0b\x0c\r ' - - word_punct = r'[\w!"\'&.,?]' - letter = r'[^\d\W]' - whitespace = r'[%s]' % re.escape(_whitespace) - nowhitespace = '[^' + whitespace[1:] - mod_wordsep_re = re.compile(r''' - ( # any whitespace - %(ws)s+ - | # em-dash between words - (?<=%(wp)s) -{2,} (?=\w) - | # word, possibly hyphenated - %(nws)s+? (?: - # hyphenated word, or word with forward slash - [-\/](?: (?<=%(lt)s{2}[-\/]) | (?<=%(lt)s[-\/]%(lt)s[-\/])) - (?= %(lt)s [-\/]? %(lt)s) - | # end of word - (?=%(ws)s|\Z) - | # em-dash - (?<=%(wp)s) (?=-{2,}\w) - ) - )''' % {'wp': word_punct, 'lt': letter, - 'ws': whitespace, 'nws': nowhitespace}, - re.VERBOSE) - - if self.break_on_hyphens is True: - chunks = mod_wordsep_re.split(text) - else: - chunks = self.wordsep_simple_re.split(text) - chunks = [c for c in chunks if c] - return chunks - diff --git a/build/lib/tartube/config.py b/build/lib/tartube/config.py deleted file mode 100644 index 740d8365..00000000 --- a/build/lib/tartube/config.py +++ /dev/null @@ -1,35439 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Configuration window classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, Gdk, GdkPixbuf - - -# Import other modules -import datetime -import html -import json -import os -import pickle -import re -import stat -import sys -import urllib.parse - - -# Import our modules -import __main__ -import downloads -import formats -import mainapp -import mainwin -import media -import platform -import utils -# Use same gettext translations -from mainapp import _ - - -# Import matplotlib stuff -if mainapp.HAVE_MATPLOTLIB_FLAG: - from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg - from matplotlib.figure import Figure - from matplotlib.ticker import MaxNLocator - -# Classes - - -class GenericConfigWin(Gtk.Window): - - """Generic Python class for windows in which the user can modify various - settings. - - Inherited by two types of window - 'preference windows' (in which changes - are applied immediately), and 'edit windowS' (in which changes are stored - temporarily, and only applied once the user has finished making changes. - """ - - - # Standard class methods - - -# def __init__(): # Provided by child object - - - # Public class methods - - -# def is_duplicate(): # Provided by child object - - - def setup(self): - - """Called by self.__init__(). - - Sets up the config window when it opens. - """ - - # Set the default window size - self.set_default_size( - self.app_obj.config_win_width, - self.app_obj.config_win_height, - ) - - # Set the window's Gtk icon list - self.set_icon_list(self.app_obj.main_win_obj.config_win_pixbuf_list) - - # Set up main widgets - self.setup_grid() - self.setup_notebook() - self.setup_button_strip() - self.setup_gap() - - # Set up tabs - self.setup_tabs() - - # Procedure complete - self.show_all() - - # Inform the main window of this window's birth (so that Tartube - # doesn't allow an operation to start until all configuration windows - # have closed) - self.app_obj.main_win_obj.add_child_window(self) - # Add a callback so we can inform the main window of this window's - # destruction - self.connect('destroy', self.close) - - - def setup_grid(self): - - """Called by self.setup(). - - Sets up a Gtk.Grid, on which a notebook and a button strip will be - placed. (Each of the notebook's tabs also has its own Gtk.Grid.) - """ - - self.grid = Gtk.Grid() - self.add(self.grid) - - - def setup_notebook(self): - - """Called by self.setup(). - - Sets up a Gtk.Notebook, after which self.setup_tabs() is called to fill - it with tabs. - """ - - self.notebook = Gtk.Notebook() - self.grid.attach(self.notebook, 0, 1, 1, 1) - self.notebook.set_border_width(self.spacing_size) - # It shouldn't be necessary to scroll the notebook's tabs, but we'll - # make it possible anyway - self.notebook.set_scrollable(True) - - - def add_notebook_tab(self, name, border_width=None): - - """Called by various functions in the child edit/preference window. - - Adds a tab to the main Gtk.Notebook, creating a Gtk.Grid inside it, on - which the calling function can add more widgets. - - Args: - - name (str): The name of the tab - - border_width (int): If specified, the border width for the - Gtk.Grid contained in this tab (usually specified when an inner - Gtk.Notebook is to be added to this tab). If not specified, a - default width is used - - Return values: - - The tab created (in the form of a Gtk.Box) and its Gtk.Grid - - """ - - if border_width is None: - border_width = self.spacing_size - - tab = Gtk.Box() - self.notebook.append_page(tab, Gtk.Label.new_with_mnemonic(name)) - tab.set_hexpand(True) - tab.set_vexpand(True) - tab.set_border_width(self.spacing_size) - - scrolled = Gtk.ScrolledWindow() - tab.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - grid = Gtk.Grid() - scrolled.add_with_viewport(grid) - grid.set_border_width(border_width) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - return tab, grid - - - def add_inner_notebook(self, grid): - - """Called by various functions in the child edit/preference window. - - Adds an inner Gtk.Notebook to a tab inside the main Gtk.Notebook. - - Args: - - grid (Gtk.Grid): The widget to which the notebook is added - - Return values: - - Returns the new Gtk.Notebook - - """ - - inner_notebook = Gtk.Notebook() - grid.attach(inner_notebook, 0, 1, 1, 1) - # It shouldn't be necessary to scroll the notebook's tabs, but we'll - # make it possible anyway - inner_notebook.set_scrollable(True) - - return inner_notebook - - - def add_inner_notebook_tab(self, name, notebook): - - """Called by various functions in the child edit/preference window. - - A modified form of self.add_notebook_tab, for tabs to be placd in the - inner notebook created by a call to self.add_inner_notebook. - - Adds a tab to the specified Gtk.Notebook, creating a Gtk.Grid inside - it, on which the calling function can add more widgets. - - Args: - - name (str): The name of the tab - - notebook (Gtk.Notebook): The notebook to which the tab is added - - Return values: - - The tab created (in the form of a Gtk.Box) and its Gtk.Grid - - """ - - tab = Gtk.Box() - notebook.append_page(tab, Gtk.Label.new_with_mnemonic(name)) - tab.set_hexpand(True) - tab.set_vexpand(True) - - scrolled = Gtk.ScrolledWindow() - tab.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - grid = Gtk.Grid() - scrolled.add_with_viewport(grid) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - return tab, grid - - -# def setup_button_strip(): # Provided by child object - - - def setup_gap(self): - - """Called by self.setup(). - - Adds an empty box beneath the button strip for aesthetic purposes. - """ - - hbox = Gtk.HBox() - self.grid.attach(hbox, 0, 3, 1, 1) - hbox.set_border_width(self.spacing_size) - - - def close(self, also_self): - - """Called from callback in self.setup(). - - Inform the main window that this window is closing. - - Args: - - also_self (an object inheriting from config.GenericConfigWin): - Another copy of self - - """ - - self.app_obj.main_win_obj.del_child_window(self) - - - # (Add widgets) - - - def add_secondary_grid(self, grid, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds another Gtk.Grid, to be placed inside the tab's main grid, in - order to avoid messing up widget spacing elsewhere in the tab. - - Args: - - grid (Gtk.Grid): The existing grid on which the new grid will be - placed - - x, y, wid, hei (int): Position on the existing grid at which the - new grid is placed - - Return values: - - The new Gtk.Grid - - """ - - grid2 = Gtk.Grid() - grid.attach(grid2, x, y, wid, hei) - grid2.set_vexpand(False) - grid2.set_column_spacing(self.spacing_size) - grid2.set_row_spacing(self.spacing_size) - - return grid2 - - - def add_image(self, grid, image_path, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.Image to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - image_path (str): Full path to the image file to load - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The Gtk.Frame containing the image - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - image = Gtk.Image() - frame.add(image) - image.set_from_pixbuf( - self.app_obj.file_manager_obj.load_to_pixbuf(image_path), - ) - - return frame - - - def add_pixbuf(self, grid, pixbuf_name, x, y, wid, hei): - - """Called by various functions in the child config window. - - Adds a Gtk.Image to the tab's Gtk.Grid. A modified version of - self.add_image(), which is called with a path to an image file; this - function is called with one of the pixbuf names specified by - mainwin.MainWin.pixbuf_dict, e.g. 'video_both_large'. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - pixbuf_name (str): One of the keys in - mainwin.MainWin.pixbuf_dict, e.g. 'video_both_large'. - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The Gtk.Frame containing the image - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - image = Gtk.Image() - frame.add(image) - - main_win_obj = self.app_obj.main_win_obj - if pixbuf_name in main_win_obj.pixbuf_dict: - image.set_from_pixbuf(main_win_obj.pixbuf_dict[pixbuf_name]) - else: - # Unrecognised pixbuf name - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['question_large'], - ) - - return frame - - - def add_treeview(self, grid, x, y, wid, hei): - - """Called by various functions in the child preference/edit window. - - Adds a single-column Gtk.Treeview to the tab's Gtk.Grid. No callback - function is created by this function; it's up to the calling code to - supply one. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - A list containing the treeview widget and liststore created - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(False) - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - '', - renderer_text, - text=0, - ) - treeview.append_column(column_text) - - liststore = Gtk.ListStore(str) - treeview.set_model(liststore) - - return treeview, liststore - - - # (Shared support functions) - - - def add_combos_for_graphs(self, grid, row): - - """Called by tabs in ChannelPlaylistEditWin, FolderEditWin and - SystemPrefWin. - - The tabs that draw graphs share a standard set of comboboxes, with - which the graph is customised. - - This function is called to create the comboboxes, and to set up - callbacks so that changes to the settings are remembered between - windows. - - Args: - - grid (Gtk.Grid): The grid on which widgets are arranged in their - tab - - row (int): The grid row on which these combos appear - - """ - - if isinstance(self, GenericPrefWin): - pref_win_flag = True - else: - pref_win_flag = False - - # Add combos to customise the graph - combo_list = [ - [_('Downloads'), 'receive'], - [_('Uploads'), 'upload'], - [_('File size'), 'size'], - [_('Duration'), 'duration'], - ] - - if pref_win_flag: - - combo = self.add_combo_with_data(grid, - combo_list, - self.app_obj.graph_data_type, - 0, row, 1, 1, - ) - - else: - - combo = self.add_combo_with_data(grid, - combo_list, - None, - 0, row, 1, 1, - ) - - count = -1 - for mini_list in combo_list: - count += 1 - if mini_list[1] == self.app_obj.graph_data_type: - combo.set_active(count) - break - - combo.set_hexpand(True) - - combo_list2 = [ - [_('Graph'), 'graph'], - [_('Bar chart'), 'chart'], - ] - - if pref_win_flag: - - combo2 = self.add_combo_with_data(grid, - combo_list2, - self.app_obj.graph_plot_type, - 1, row, 1, 1, - ) - - else: - - combo2 = self.add_combo_with_data(grid, - combo_list2, - None, - 1, row, 1, 1, - ) - - count = -1 - for mini_list in combo_list2: - count += 1 - if mini_list[1] == self.app_obj.graph_plot_type: - combo2.set_active(count) - break - - combo2.set_hexpand(True) - - combo_list3 = [ - [_('Show decade'), 60*60*24*365*10], - [_('Show year'), 60*60*24*365], - [_('Show quarters'), 60*60*24*90], - [_('Show month'), 60*60*24*30], - [_('Show week'), 60*60*24*7], - [_('Show day'), 60*60*24], - ] - - if pref_win_flag: - - combo3 = self.add_combo_with_data(grid, - combo_list3, - self.app_obj.graph_time_period_secs, - 2, row, 1, 1, - ) - - else: - - combo3 = self.add_combo_with_data(grid, - combo_list3, - None, - 2, row, 1, 1, - ) - - count = -1 - for mini_list in combo_list3: - count += 1 - if mini_list[1] == self.app_obj.graph_time_period_secs: - combo3.set_active(count) - break - - combo3.set_hexpand(True) - if self.app_obj.graph_data_type != 'receive' \ - and self.app_obj.graph_data_type != 'upload': - combo3.set_sensitive(False) - - combo_list4 = [ - [_('Quarters'), 60*60*24*90], - [_('Months'), 60*60*24*30], - [_('Weeks'), 60*60*24*7], - [_('Days'), 60*60*24], - [_('Hours'), 60*60], - ] - - if pref_win_flag: - - combo4 = self.add_combo_with_data(grid, - combo_list4, - self.app_obj.graph_time_unit_secs, - 3, row, 1, 1, - ) - - else: - - combo4 = self.add_combo_with_data(grid, - combo_list4, - None, - 3, row, 1, 1, - ) - - count = -1 - for mini_list in combo_list4: - count += 1 - if mini_list[1] == self.app_obj.graph_time_unit_secs: - combo4.set_active(count) - break - - combo4.set_hexpand(True) - if self.app_obj.graph_data_type != 'receive' \ - and self.app_obj.graph_data_type != 'upload': - combo4.set_sensitive(False) - - combo_list5 = [ - [_('Red'), 'red'], - [_('Green'), 'green'], - [_('Blue'), 'blue'], - [_('Black'), 'black'], - [_('White'), 'white'], - ] - - if pref_win_flag: - - combo5 = self.add_combo_with_data(grid, - combo_list5, - self.app_obj.graph_ink_colour, - 4, row, 1, 1, - ) - - else: - - combo5 = self.add_combo_with_data(grid, - combo_list5, - None, - 4, row, 1, 1, - ) - - count = -1 - for mini_list in combo_list5: - count += 1 - if mini_list[1] == self.app_obj.graph_ink_colour: - combo5.set_active(count) - break - - combo5.set_hexpand(True) - - # (Signal connects from above) - combo.connect( - 'changed', - self.on_combo_graph_changed, - 'data_type', - combo3, - combo4, - ) - combo2.connect( - 'changed', - self.on_combo_graph_changed, - 'plot_type', - combo3, - combo4, - ) - combo3.connect( - 'changed', - self.on_combo_graph_changed, - 'time_period', - combo3, - combo4, - ) - combo4.connect( - 'changed', - self.on_combo_graph_changed, - 'time_unit', - combo3, - combo4, - ) - combo5.connect( - 'changed', - self.on_combo_graph_changed, - 'ink_colour', - combo3, - combo4, - ) - - return combo, combo2, combo3, combo4, combo5 - - - def get_options_applied_text(self, options_obj): - - """ Called by OptionsEditWin.setup_name_tab() and - SystemPrefWin.setup_options_dl_list_tab_add_row(). - - Generates text displaying the media data object(s) to which a - download options manager has been applied. - - Args: - - options_obj (options.OptionsManager): The download options manager - - Return values: - - A single line of displayable text - - """ - - if not options_obj.dbid_list: - - # Failsafe; calling code has already checked that - return '' - - # Database auto-fix: see comments in the called function - self.get_options_applied_text_autofix(options_obj) - - if len(options_obj.dbid_list) == 1: - - dbid = options_obj.dbid_list[0] - media_data_obj = self.app_obj.media_reg_dict[dbid] - return media_data_obj.get_translated_type(True) \ - + ': ' + media_data_obj.name - - else: - - video_count = 0 - channel_count = 0 - playlist_count = 0 - folder_count = 0 - - for dbid in options_obj.dbid_list: - media_data_obj = self.app_obj.media_reg_dict[dbid] - if isinstance(media_data_obj, media.Video): - video_count += 1 - elif isinstance(media_data_obj, media.Channel): - channel_count += 1 - elif isinstance(media_data_obj, media.Playlist): - playlist_count += 1 - elif isinstance(media_data_obj, media.Folder): - folder_count += 1 - - msg = '' - if video_count: - msg = _('Videos') + ': ' + str(video_count) + ' ' - - if channel_count: - msg = msg + _('Channels') + ': ' + str(channel_count) + ' ' - - if playlist_count: - msg = msg + _('Playlists') + ': ' + str(playlist_count) + ' ' - - if folder_count: - msg = msg + _('Folders') + ': ' + str(folder_count) + ' ' - - return msg - - - def get_options_applied_text_autofix(self, options_obj): - - """Called by self.get_options_applied_text(). - - Git #456 - options.OptionsObj.dbid_list includes .dbid of non-existent - media data objects. - - Since the cause of the issue is not known, do a quick auto-fix - whenever the prefwin opens. (The miniscule hit to performance is better - than a broken preferences window.) - - Args: - - options_obj (options.OptionsManager): The download options manager - - """ - - dbid_list = [] - fix_flag = False - - for dbid in options_obj.dbid_list: - if not dbid in self.app_obj.media_reg_dict: - fix_flag = True - else: - dbid_list.append(dbid) - - if fix_flag: - options_obj.dbid_list = dbid_list - self.app_obj.system_error( - 406, - 'Detected and auto-fixed download options applied to non-' \ - + ' existent media. Please do a full database check: in' \ - + ' Tartube\'s menu, click File > Check database integrity', - ) - - - def plot_graph(self, hbox, plot_type, data_type, ink_colour, x_label, - y_label, x_list, y_list): - - """Called by self.on_button_draw_graph_clicked(). - - Plots a graph, using the specified settings and data points. - - Args: - - hbox (Gtk.HBox): The container widget - - plot_type (str): 'graph' or 'chart' - - data_type (str): 'receive', 'upload', 'size' or 'duration' - - ink_colour (str): 'red', 'green', 'blue', 'black' or white' - - x_label, y_label (str): Text for each axis on the graph - - x_list (list): List of data points along the x axis - - y_list (list): List of data points along the y axis (both lists - should contain the same number of items) - - """ - - # Sanity check: when the video counts (values in y_list) are all 0, - # matplotlib will try to draw a y-axis with fractional values - # Check for that, so we can prevent it - simplify_y_axis_flag = True - for num in y_list: - if num: - simplify_y_axis_flag = False - break - - # Remove the old figure - for child in hbox.get_children(): - hbox.remove(child) - - # Plot a new figure - fig = Figure(dpi = 100) - - ax = fig.add_subplot(1, 1, 1) - - if plot_type == 'graph': - ax.plot(x_list, y_list, color = ink_colour) - else: - ax.bar(x_list, y_list, color = ink_colour) - - # Set up the axes - ax.set_xlabel(x_label) - ax.set_ylabel(y_label) - # (Negative values are meaningless, so don't allow them to appear on - # the x/y axes) - if simplify_y_axis_flag: - ax.set_ylim([0, 1]) - else: - ax.set_ylim(ymin=0) - if data_type == 'receive' or data_type == 'upload': - ax.set_xlim(xmin=0) - # (Fractional values are also meaningless) - ax.xaxis.set_major_locator(MaxNLocator(integer = True)) - ax.yaxis.set_major_locator(MaxNLocator(integer = True)) - # (For time graphs, reverse the X axis, to show days ago, etc) - if data_type == 'receive' or data_type == 'upload': - ax.set_xlim(ax.get_xlim()[::-1]) - # (For some reason, the x-axis label is drawn below the visible area. - # Not sure how to fix that, so move it to the top, in which there is - # empty space) - ax.xaxis.set_label_position('top') - # (Reduce wasted space around the edges) - fig.tight_layout() - - canvas = FigureCanvasGTK3Agg(fig) # a Gtk.DrawingArea - hbox.add(canvas) - - self.show_all() - - - def show_spinbutton_leading_zeroes(self, spinbutton, columns): - - """Callback which can be connected to any Gtk.SpinButton. - - Adds leading zeroes to the value displayed in the spinbutton, which is - expected to be an integer. - - Args: - - spinbutton (Gtk.SpinButton): The widget to modify - - columns (int): A value, 1 or above; e.g. use 2 to show the - components in a 24-hour clock - - """ - - if type(columns) is not int or columns < 1: - return False - - adjustment = spinbutton.get_adjustment() - format_str = '{:0' + str(columns) + 'd}' - spinbutton.set_text(format_str.format(int(adjustment.get_value()))) - - return True - - - def ytdlp_only(self): - - """Shorcut function, call for various underlined text. - - Return values: - - The formatted string 'yt-dlp only' - - """ - return ' [' + _('yt-dlp only') + ']' - - - # (Shared callbacks) - - - def on_button_draw_graph_clicked(self, button, hbox, combo, combo2, - combo3, combo4, combo5): - - """Called from callbacks in ChannelPlaylistEditWin, - FolderEditWin and SystemPrefWin. - - Prepares data for a graph using the specified settings, then calls - self.plot_graph() to actually draw it. - - Args: - - button (Gtk.Button): The widget clicked - - hbox (Gtk.HBox): The container widget for the graph - - combo, combo2, combo3, combo4, combo5 (Gtk.ComboBox): Five combos - specifying the data to view: - - The data type ('receive' for download times, 'upload' for - upload times, 'size' for file size, 'duration' for video - duration) - - The type of graph to plot ('graph' for a line plot graph, or - 'chart' for a bar chart) - - The period of time used as the span of the x-axis (in seconds, - e.g. 31536000 is the equivalent of a year) - - The time unit to use (in seconds, e.g. 604800 is the equivalent - of a week). We count the number of videos for the time unit - and use it as a single point on the x-axis - - The colour to use ('red', 'green', 'blue', 'black', white') - - """ - - # Extract data from the combos - - # Get the data type ('receive', 'upload', 'size' or 'duration') - tree_iter = combo.get_active_iter() - model = combo.get_model() - data_name = model[tree_iter][0] - data_type = model[tree_iter][1] - - # Get type of graph to plot ('graph' or 'chart') - tree_iter2 = combo2.get_active_iter() - model2 = combo2.get_model() - plot_type = model2[tree_iter2][1] - - # Get the period of time used as the span of the x-axis, in seconds - tree_iter3 = combo3.get_active_iter() - model3 = combo3.get_model() - time_period_secs = int(model3[tree_iter3][1]) - - # Get the time unit to use, in seconds - tree_iter4 = combo4.get_active_iter() - model4 = combo4.get_model() - time_unit = model4[tree_iter4][0] - time_unit_secs = int(model4[tree_iter4][1]) - # The time unit must not be larger than the total period of time - if time_unit_secs > time_period_secs: - time_unit_secs = time_period_secs - - # Get the colour to use ('red', 'green', 'blue', 'black', white') - tree_iter5 = combo5.get_active_iter() - model5 = combo5.get_model() - ink_colour = model5[tree_iter5][1] - - # Compile a dictionary of video counts - # For 'receive' and 'upload', dictonary in the form - # frequency_dict[time_unit_increment] = number_of_videos - # For 'size', dictionary in the form - # frequency_dict[size_category] = number_of_videos - # For 'duration', dictionary in the form - # frequency_dict[duration_category] = number_of_videos - # When called by SystemPrefWin, use the entire database - # When called by ChannelPlaylistEditWin or FolderEditWin, use only the - # children of that container - # 'size_category' and 'duration_category' are arbitrary ranges of - # values, chosen for aesthetic reasons - if isinstance(self, GenericEditWin): - - if data_type == 'receive' or data_type == 'upload': - - frequency_dict = self.edit_obj.compile_all_videos_by_frequency( - data_type, - time_unit_secs, - {}, - ) - - elif data_type == 'size': - - frequency_dict = self.edit_obj.compile_all_videos_by_size( - {}, - ) - - elif data_type == 'duration': - - frequency_dict = self.edit_obj.compile_all_videos_by_duration( - {}, - ) - - else: - - frequency_dict = {} - - for dbid in self.app_obj.container_reg_dict.keys(): - - media_data_obj = self.app_obj.media_reg_dict[dbid] - # Ignore private (system) folders, because they contain - # media.Video objects also stored in public folders - if not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.priv_flag: - - if data_type == 'receive' or data_type == 'upload': - - frequency_dict \ - = media_data_obj.compile_all_videos_by_frequency( - data_type, - time_unit_secs, - frequency_dict, - ) - - elif data_type == 'size': - - frequency_dict \ - = media_data_obj.compile_all_videos_by_size( - frequency_dict, - ) - - elif data_type == 'duration': - - frequency_dict \ - = media_data_obj.compile_all_videos_by_duration( - frequency_dict, - ) - - # Compile two lists, with each index giving the x and y coordinates - # for the graph to be plotted - if data_type == 'receive' or data_type == 'upload': - - period_list = [] - frequency_list = [] - - for i in range(0, int((time_period_secs / time_unit_secs) + 1)): - - period_list.append(i) - if i in frequency_dict: - frequency_list.append(frequency_dict[i]) - else: - frequency_list.append(0) - - # Draw the graph - self.plot_graph( - hbox, - plot_type, - data_type, - ink_colour, - time_unit, - data_name, - period_list, - frequency_list, - ) - - else: - - # NB If these labels are changed, when the corresponding literal - # values in media.GenericContainer.compile_all_videos_by_size() - # and .compile_all_videos_by_duration() must be changed too - if data_type == 'size': - - label_list = [ - '10MB', '25MB', '50MB', '100MB', '250MB', - '500MB', '1GB', '2GB', '5GB', '5GB+', - ] - - else: - - label_list = [ - '10s', '1m', '5m', '10m', '20m', - '30m', '1h', '2h', '5h', '5h+', - ] - - frequency_list = [] - for label in label_list: - - if label in frequency_dict: - frequency_list.append(frequency_dict[label]) - else: - frequency_list.append(0) - - # Draw the graph - self.plot_graph( - hbox, - plot_type, - data_type, - ink_colour, - data_name, - _('Videos'), - label_list, - frequency_list, - ) - - - def on_combo_graph_changed(self, combo, combo_type, combo2, combo3): - - """Called from callback in self.add_combos_for_graphs(). - - Graphs are drawn with five standard combos for customising the graph. - When the user selects a new setting in a combo, store the value. - - In some cases, one or more of the combos must be (de)sensitised. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - combo_type (str): A string describing which IV to update: - 'data_type', 'plot_type', 'time_period', 'time_unit', - 'ink_colour' - - combo2, combo3 (Gtk.ComboBox): Other combos to be (de)sensitised - - """ - - # Extract data from the combo - tree_iter = combo.get_active_iter() - model = combo.get_model() - value = model[tree_iter][1] - # Update IVs - self.app_obj.set_graph_values(combo_type, value) - - # (De)sensitise other combos, as appropriate - if combo_type == 'data_type': - - if (value == 'receive' or value == 'upload'): - combo2.set_sensitive(True) - combo3.set_sensitive(True) - else: - combo2.set_sensitive(False) - combo3.set_sensitive(False) - - -class GenericEditWin(GenericConfigWin): - - """Generic Python class for windows in which the user can modify various - settings in a class object (such as a media.Video or an - options.OptionsManager object). - - The modifications are stored temporarily, and only applied once the user - has finished making changes. - """ - - - # Standard class methods - - -# def __init__(): # Provided by child object - - - # Public class methods - - - def is_duplicate(self, app_obj, edit_obj): - - """Called by self.__init__. - - Don't open this edit window, if another with the same .edit_obj is - already open. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (options.OptionsManager): The object whose attributes will - be edited in this window - - Return values: - - True if a duplicate is found, False if not - - """ - - for config_win_obj in app_obj.main_win_obj.config_win_list: - - if isinstance(config_win_obj, GenericEditWin) \ - and config_win_obj.edit_obj == edit_obj: - - # Duplicate found - config_win_obj.present() - return True - - # Not a duplicate - return False - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - - def setup_button_strip(self): - - """Called by self.setup(). - - Creates a strip of buttons at the bottom of the window. Any changes the - user has made are applied by clicking the 'OK' or 'Apply' buttons, and - cancelled by using the 'Reset' or 'Cancel' buttons. - - The window is closed by using the 'OK' and 'Cancel' buttons. - - If self.multi_button_flag is True, only the 'OK' button is created. - """ - - hbox = Gtk.HBox() - self.grid.attach(hbox, 0, 2, 1, 1) - - if self.multi_button_flag: - - # 'Reset' button - self.reset_button = Gtk.Button(_('Reset')) - hbox.pack_start(self.reset_button, False, False, self.spacing_size) - self.reset_button.get_child().set_width_chars(10) - self.reset_button.set_tooltip_text( - _('Reset changes without closing the window'), - ); - self.reset_button.connect('clicked', self.on_button_reset_clicked) - - # 'Apply' button - self.apply_button = Gtk.Button(_('Apply')) - hbox.pack_start(self.apply_button, False, False, self.spacing_size) - self.apply_button.get_child().set_width_chars(10) - self.apply_button.set_tooltip_text( - _('Apply changes without closing the window'), - ); - self.apply_button.connect('clicked', self.on_button_apply_clicked) - - # 'OK' button - self.ok_button = Gtk.Button(_('OK')) - hbox.pack_end(self.ok_button, False, False, self.spacing_size) - self.ok_button.get_child().set_width_chars(10) - self.ok_button.set_tooltip_text(_('Apply changes')); - self.ok_button.connect('clicked', self.on_button_ok_clicked) - - if self.multi_button_flag: - - # 'Cancel' button - self.cancel_button = Gtk.Button(_('Cancel')) - hbox.pack_end(self.cancel_button, False, False, self.spacing_size) - self.cancel_button.get_child().set_width_chars(10) - self.cancel_button.set_tooltip_text(_('Cancel changes')); - self.cancel_button.connect( - 'clicked', - self.on_button_cancel_clicked, - ) - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - """ - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - setattr(self.edit_obj, key, self.edit_dict[key]) - - # The changes can now be cleared - self.edit_dict = {} - - - def reset_with_new_edit_obj(self, new_edit_obj): - - """Can be called by anything. - - Resets the object whose values are being edited in this window, i.e. - self.edit_obj, to the specified object. - - Then redraws the window itself, as if the user had clicked the 'Reset' - button at the bottom of the window. This makes new_edit_obj's IVs - visible in the edit window, without the need to destroy the old one and - replace it with a new one. - - Args: - - new_edit_obj (class): The replacement edit object - - """ - - self.edit_obj = new_edit_obj - - # The rest of this function is copied from - # self.on_button_reset_clicked() - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Empty self.edit_dict, destroying any changes the user has made - self.edit_dict = {} - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - def retrieve_val(self, name): - - """Can be called by anything. - - Any changes the user has made are temporarily stored in self.edit_dict. - - Each key corresponds to an attribute in the object being edited, - self.edit_obj. - - If 'name' exists as a key in that dictionary, retrieve the - corresponding value and return it. Otherwise, the user hasn't yet - modified the value, so retrieve directly from the attribute in the - object being edited. - - Args: - - name (str): The name of the attribute in the object being edited - - Return values: - - The original or modified value of that attribute - - """ - - if name in self.edit_dict: - return self.edit_dict[name] - else: - attrib = getattr(self.edit_obj, name) - if type(attrib) is list or type(attrib) is dict: - return attrib.copy() - else: - return attrib - - - # (Add widgets) - - - def add_checkbutton(self, grid, text, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.CheckButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (string or None): The text to display in the checkbutton's - label. No label is used if 'text' is an empty string or None - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The checkbutton widget created - - """ - - checkbutton = Gtk.CheckButton() - grid.attach(checkbutton, x, y, wid, hei) - checkbutton.set_hexpand(True) - if text is not None and text != '': - checkbutton.set_label(text) - - if prop is not None: - checkbutton.set_active(self.retrieve_val(prop)) - checkbutton.connect('toggled', self.on_checkbutton_toggled, prop) - - return checkbutton - - - def add_combo(self, grid, combo_list, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a simple Gtk.ComboBox to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - combo_list (list): A list of values to display in the combobox. - This function expects a simple, one-dimensional list. For - something more complex, see self.add_combo_with_data() - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The combobox widget created - - """ - - store = Gtk.ListStore(str) - for string in combo_list: - store.append( [str(string)] ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, x, y, wid, hei) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_entry_text_column(0) - - if prop is not None: - val = self.retrieve_val(prop) - if val in combo_list: - index = combo_list.index(val) - combo.set_active(index) - - combo.connect('changed', self.on_combo_changed, prop) - - return combo - - - def add_combo_with_data(self, grid, combo_list, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a more complex Gtk.ComboBox to the tab's Gtk.Grid. This function - expects a list of values in the form - - [ [val1, val2], [val1, val2], ... ] - - The combobox displays the 'val1' values. If one of them is selected, - the corresponding 'val2' is used to set the attribute described by - 'prop'. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - combo_list (list): The list described above. For something more - simple, see self.add_combo() - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The combobox widget created - - """ - - store = Gtk.ListStore(str, str) - - index_list = [] - for mini_list in combo_list: - store.append( [ str(mini_list[0]), str(mini_list[1]) ] ) - index_list.append(mini_list[1]) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, x, y, wid, hei) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_entry_text_column(0) - - if prop is not None: - val = self.retrieve_val(prop) - if val in index_list: - index = index_list.index(val) - combo.set_active(index) - - combo.connect('changed', self.on_combo_with_data_changed, prop) - - return combo - - - def add_entry(self, grid, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.Entry to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The entry widget created - - """ - - entry = Gtk.Entry() - grid.attach(entry, x, y, wid, hei) - entry.set_hexpand(True) - - if prop is not None: - value = self.retrieve_val(prop) - if value is not None: - entry.set_text(str(value)) - - entry.connect('changed', self.on_entry_changed, prop) - - return entry - - -# def add_image # Inherited from GenericConfigWin - - - def add_label(self, grid, text, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.Label to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (str): Pango markup displayed in the label - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The label widget created - - """ - - label = Gtk.Label() - grid.attach(label, x, y, wid, hei) - label.set_markup(text) - label.set_hexpand(True) - label.set_alignment(0, 0.5) - - return label - - - def add_radiobutton(self, grid, prev_button, text, prop, value, x, y, \ - wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.RadioButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - prev_button (Gtk.RadioButton or None): When this is the first - radio button in the group, None. Otherwise, the previous - radio button in the group. Use of this argument links the radio - buttons together, ensuring that only one of them can be active - at any time - - text (string or None): The text to display in the radiobutton's - label. No label is used if 'text' is an empty string or None - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - value (any): When this radiobutton becomes the active one, and if - 'prop' is not None, then 'prop' and 'value' are added as a new - key-value pair to self.edit_dict - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The radiobutton widget created - - """ - - radiobutton = Gtk.RadioButton.new_from_widget(prev_button) - grid.attach(radiobutton, x, y, wid, hei) - radiobutton.set_hexpand(True) - if text is not None and text != '': - radiobutton.set_label(text) - - if prop is not None: - if value is not None and self.retrieve_val(prop) == value: - radiobutton.set_active(True) - - radiobutton.connect( - 'toggled', - self.on_radiobutton_toggled, prop, value, - ) - - return radiobutton - - - def add_spinbutton(self, grid, min_val, max_val, step, prop, x, y, wid, \ - hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.SpinButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - min_val (int): The minimum permitted in the spinbutton - - max_val (int or None): The maximum values permitted in the - spinbutton. If None, this function assigns a very large maximum - value (a billion) - - step (int): Clicking the up/down arrows in the spin button - increments/decrements the value by this much - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The spinbutton widget created - - """ - - # If the specified value of 'max_valu' was none, just use a very big - # number (as Gtk.SpinButton won't accept the None argument) - if max_val is None: - max_val = 1000000000 - - spinbutton = Gtk.SpinButton.new_with_range(min_val, max_val, step) - grid.attach(spinbutton, x, y, wid, hei) - spinbutton.set_hexpand(False) - - if prop is not None: - spinbutton.set_value(self.retrieve_val(prop)) - spinbutton.connect( - 'value-changed', - self.on_spinbutton_changed, - prop, - ) - - return spinbutton - - - def add_textview(self, grid, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.TextView to the tab's Gtk.Grid. The contents of the textview - are used as a single string (perhaps including newline characters) to - set the value of a string attribute. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. The - attribute can be an integer, string, list or tuple. If None, no - changes are made to self.edit_dict; it's up to the calling - function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The textview and textbuffer widgets created - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - textview = Gtk.TextView() - scrolled.add(textview) - - textbuffer = textview.get_buffer() - - if prop is not None: - value = self.retrieve_val(prop) - if value is not None: - if type(value) is list or type(value) is tuple: - textbuffer.set_text(str.join('\n', value)) - else: - textbuffer.set_text(str(value)) - - textbuffer.connect('changed', self.on_textview_changed, prop) - - return textview, textbuffer - - -# def add_treeview # Inherited from GenericConfigWin - - - # Callback class methods - - - def on_button_apply_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Applies any changes made by the user and re-draws the window's tabs, - showing their new values. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Apply any changes the user has made - self.apply_changes() - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - def on_button_cancel_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Destroys any changes made by the user and re-draws the window's tabs, - showing their original values. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Destroy the window - self.destroy() - - - def on_button_ok_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Destroys any changes made by the user and then closes the window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Apply any changes the user has made - self.apply_changes() - - # Destroy the window - self.destroy() - - - def on_button_reset_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Destroys any changes made by the user and re-draws the window's tabs, - showing their original values. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Empty self.edit_dict, destroying any changes the user has made - self.edit_dict = {} - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - def on_checkbutton_toggled(self, checkbutton, prop): - - """Called from a callback in self.add_checkbutton(). - - Adds a key-value pair to self.edit_dict, using True if the button is - selected, False if not. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - if not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - - def on_combo_changed(self, combo, prop): - - """Called from a callback in self.add_combo(). - - Temporarily stores the contents of the widget in self.edit_dict. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.edit_dict[prop] = model[tree_iter][0] - - - def on_combo_with_data_changed(self, combo, prop): - - """Called from a callback in self.add_combo_with_data(). - - Extracts the value visible in the widget, converts it into another - value, and stores the later in self.edit_dict. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.edit_dict[prop] = model[tree_iter][1] - - - def on_entry_changed(self, entry, prop): - - """Called from a callback in self.add_entry(). - - Temporarily stores the contents of the widget in self.edit_dict. - - Args: - - entry (Gtk.Entry): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - self.edit_dict[prop] = entry.get_text() - - - def on_radiobutton_toggled(self, radiobutton, prop, value): - - """Called from a callback in self.add_radiobutton(). - - Adds a key-value pair to self.edit_dict, but only if this radiobutton - (from those in the group) is the selected one. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - value (-): The attribute's new value - - """ - - if radiobutton.get_active(): - self.edit_dict[prop] = value - - - def on_spinbutton_changed(self, spinbutton, prop): - - """Called from a callback in self.add_spinbutton(). - - Temporarily stores the contents of the widget in self.edit_dict. - - Args: - - spinbutton (Gtk.SpinkButton): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - self.edit_dict[prop] = int(spinbutton.get_value()) - - - def on_textview_changed(self, textbuffer, prop): - - """Called from a callback in self.add_textview(). - - Temporarily stores the contents of the widget in self.edit_dict. - - Args: - - textbuffer (Gtk.TextBuffer): The widget modified - - prop (str): The attribute in self.edit_obj to modify - - """ - - text = textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - old_value = self.retrieve_val(prop) - - if type(old_value) is list: - self.edit_dict[prop] = text.split() - elif type(old_value) is tuple: - self.edit_dict[prop] = text.split() - else: - self.edit_dict[prop] = text - - - # (Shared support functions) - - - def add_container_properties(self, grid): - - """Called by VideoEditWin.setup_general_tab(), - ChannelPlaylistEditWin.setup_general_tab() and - FolderEditWin.setup_general_tab(). - - Adds widgets common to those edit windows. - - Args: - - grid (Gtk.Grid): The grid on which widgets are arranged in their - tab - - """ - - entry = self.add_entry(grid, - None, - 0, 1, 1, 1, - ) - entry.set_text('#' + str(self.edit_obj.dbid)) - entry.set_editable(False) - entry.set_hexpand(False) - entry.set_width_chars(8) - - main_win_obj = self.app_obj.main_win_obj - if isinstance(self.edit_obj, media.Video): - icon_path = main_win_obj.icon_dict['video_small'] - elif isinstance(self.edit_obj, media.Channel): - icon_path = main_win_obj.icon_dict['channel_small'] - elif isinstance(self.edit_obj, media.Playlist): - icon_path = main_win_obj.icon_dict['playlist_small'] - else: - - if self.edit_obj.priv_flag: - icon_path = main_win_obj.icon_dict['folder_red_small'] - elif self.edit_obj.temp_flag: - icon_path = main_win_obj.icon_dict['folder_blue_small'] - elif self.edit_obj.fixed_flag: - icon_path = main_win_obj.icon_dict['folder_green_small'] - else: - icon_path = main_win_obj.icon_dict['folder_small'] - - frame = self.add_image(grid, - icon_path, - 1, 1, 1, 1, - ) - # (The frame looks cramped without this. The icon itself is 16x16) - frame.set_size_request( - 16 + (self.spacing_size * 2), - -1, - ) - - entry2 = self.add_entry(grid, - 'name', - 2, 1, 1, 1, - ) - entry2.set_editable(False) - - label = self.add_label(grid, - _('Listed as'), - 0, 2, 1, 1, - ) - label.set_hexpand(False) - - entry3 = self.add_entry(grid, - 'nickname', - 2, 2, 1, 1, - ) - entry3.set_editable(True) - - label2 = self.add_label(grid, - _('Contained in'), - 0, 3, 1, 1, - ) - label2.set_hexpand(False) - - parent_obj = self.edit_obj.parent_obj - if parent_obj: - if isinstance(parent_obj, media.Channel): - icon_path2 = main_win_obj.icon_dict['channel_small'] - elif isinstance(parent_obj, media.Playlist): - icon_path2 = main_win_obj.icon_dict['playlist_small'] - else: - - if parent_obj.priv_flag: - icon_path2 = main_win_obj.icon_dict['folder_red_small'] - elif parent_obj.temp_flag: - icon_path2 = main_win_obj.icon_dict['folder_blue_small'] - elif parent_obj.fixed_flag: - icon_path2 = main_win_obj.icon_dict['folder_green_small'] - else: - icon_path2 = main_win_obj.icon_dict['folder_small'] - - else: - icon_path2 = main_win_obj.icon_dict['folder_black_small'] - - frame2 = self.add_image(grid, - icon_path2, - 1, 3, 1, 1, - ) - frame2.set_size_request( - 16 + (self.spacing_size * 2), - -1, - ) - - entry4 = self.add_entry(grid, - None, - 2, 3, 1, 1, - ) - entry4.set_editable(False) - if parent_obj: - entry4.set_text(parent_obj.name) - - - def add_destination_properties(self, grid): - - """Called by ChannelPlaylistEditWin.setup_general_tab() and - FolderEditWin.setup_general_tab(). - - Adds widgets common to those edit windows. - - Args: - - grid (Gtk.Grid): The grid on which widgets are arranged in their - tab - - """ - - label = self.add_label(grid, - _('Download to'), - 0, 5, 1, 1, - ) - label.set_hexpand(False) - - main_win_obj = self.app_obj.main_win_obj - dest_obj = self.app_obj.media_reg_dict[self.edit_obj.master_dbid] - - if self.edit_obj.external_dir is not None: - if self.edit_obj.dbid in self.app_obj.container_unavailable_dict: - icon_path = main_win_obj.icon_dict['unavailable_small'] - else: - icon_path = main_win_obj.icon_dict['external_small'] - elif isinstance(dest_obj, media.Channel): - icon_path = main_win_obj.icon_dict['channel_small'] - elif isinstance(dest_obj, media.Playlist): - icon_path = main_win_obj.icon_dict['playlist_small'] - else: - - if dest_obj.priv_flag: - icon_path = main_win_obj.icon_dict['folder_red_small'] - elif dest_obj.temp_flag: - icon_path = main_win_obj.icon_dict['folder_blue_small'] - elif dest_obj.fixed_flag: - icon_path = main_win_obj.icon_dict['folder_green_small'] - else: - icon_path = main_win_obj.icon_dict['folder_small'] - - frame = self.add_image(grid, - icon_path, - 1, 5, 1, 1, - ) - frame.set_size_request( - 16 + (self.spacing_size * 2), - -1, - ) - - entry = self.add_entry(grid, - None, - 2, 5, 1, 1, - ) - entry.set_editable(False) - if self.edit_obj.external_dir is not None: - entry.set_text(self.edit_obj.external_dir) - else: - entry.set_text(dest_obj.name) - - label2 = self.add_label(grid, - _('Default location'), - 0, 6, 2, 1, - ) - label2.set_hexpand(False) - - entry2 = self.add_entry(grid, - None, - 2, 6, 1, 1, - ) - entry2.set_editable(False) - entry2.set_text(self.edit_obj.get_default_dir(self.app_obj)) - - - def add_source_properties(self, grid): - - """Called by VideoEditWin.setup_general_tab() and - ChannelPlaylistEditWin.setup_general_tab(). - - Adds widgets common to those edit windows. - - Args: - - grid (Gtk.Grid): The grid on which widgets are arranged in their - tab - - """ - - media_type = self.edit_obj.get_type() - if media_type == 'channel': - string = _('Channel URL') - elif media_type == 'playlist': - string = _('Playlist URL') - else: - string = _('Video URL') - - label = self.add_label(grid, - string, - 0, 4, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - 'source', - 2, 4, 1, 1, - ) - entry.set_editable(False) - - - def setup_download_options_tab(self): - - """Called by VideoEditWin.setup_tabs(), - ChannelPlaylistEditWin.setup_tabs() and FolderEditWin.setup_tabs(). - - Sets up the 'Download options' tab. - """ - - tab, grid = self.add_notebook_tab(_('_Options')) - - # (Many buttons are not clickable when a channel/playlist/folder's - # external directory is marked unavailable) - unavailable_flag = False - if isinstance(self.edit_obj, media.Video): - if self.edit_obj.parent_obj.dbid \ - in self.app_obj.container_unavailable_dict: - unavailable_flag = True - elif self.edit_obj.dbid in self.app_obj.container_unavailable_dict: - unavailable_flag = True - - # Download options - self.add_label(grid, - '' + _('Download options') + '', - 0, 0, 2, 1, - ) - - self.apply_options_button = Gtk.Button(_('Apply download options')) - grid.attach(self.apply_options_button, 0, 1, 1, 1) - self.apply_options_button.connect( - 'clicked', - self.on_button_apply_options_clicked, - ) - - self.edit_options_button = Gtk.Button(_('Edit download options')) - grid.attach(self.edit_options_button, 1, 1, 1, 1) - self.edit_options_button.connect( - 'clicked', - self.on_button_edit_options_clicked, - ) - - self.remove_options_button = Gtk.Button(_('Remove download options')) - grid.attach(self.remove_options_button, 1, 2, 1, 1) - self.remove_options_button.connect( - 'clicked', - self.on_button_remove_options_clicked, - ) - - if self.edit_obj.options_obj or unavailable_flag: - self.apply_options_button.set_sensitive(False) - - if not self.edit_obj.options_obj or unavailable_flag: - self.edit_options_button.set_sensitive(False) - self.remove_options_button.set_sensitive(False) - - - # (Shared callbacks) - - - def on_button_apply_options_clicked(self, button): - - """Called from callback in self.setup_download_options_tab(). - - Apply download options to the media data object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if self.edit_obj.options_obj: - return self.app_obj.system_error( - 401, - 'Download options already applied', - ) - - # Apply download options to the media data object - self.app_obj.apply_download_options(self.edit_obj) - # (De)sensitise buttons appropriately - self.apply_options_button.set_sensitive(False) - self.edit_options_button.set_sensitive(True) - self.remove_options_button.set_sensitive(True) - - - def on_button_edit_options_clicked(self, button): - - """Called from callback in self.setup_download_options_tab(). - - Edit download options for the media data object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if not self.edit_obj.options_obj: - return self.app_obj.system_error( - 402, - 'Download options not already applied', - ) - - # Open an edit window to show the options immediately - OptionsEditWin( - self.app_obj, - self.edit_obj.options_obj, - ) - - - def on_button_remove_options_clicked(self, button): - - """Called from callback in self.setup_download_options_tab(). - - Remove download options from the media data object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if not self.edit_obj.options_obj: - return self.app_obj.system_error( - 403, - 'Download options not already applied', - ) - - # Remove download options from the media data object - self.app_obj.remove_download_options(self.edit_obj) - # (De)sensitise buttons appropriately - self.apply_options_button.set_sensitive(True) - self.edit_options_button.set_sensitive(False) - self.remove_options_button.set_sensitive(False) - - -class GenericPrefWin(GenericConfigWin): - - """Generic Python class for windows in which the user can modify various - system settings. - - Any modifications are applied immediately (unlike in an 'edit window', in - which the modifications are stored temporarily, and only applied once the - user has finished making changes). - """ - - - # Standard class methods - - -# def __init__(): # Provided by child object - - - # Public class methods - - - def is_duplicate(self, app_obj): - - """Called by self.__init__. - - Don't open this preference window, if another preference window of the - same class is already open. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - Return values: - - True if a duplicate is found, False if not - - """ - - for config_win_obj in app_obj.main_win_obj.config_win_list: - - if type(self) == type(config_win_obj): - - # Duplicate found - config_win_obj.present() - return True - - # Not a duplicate - return False - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - - def setup_button_strip(self): - - """Called by self.setup(). - - Creates a strip of buttons at the bottom of the window. For preference - windows, there is only a single 'OK' button, which closes the window. - """ - - hbox = Gtk.HBox() - self.grid.attach(hbox, 0, 2, 1, 1) - - # 'OK' button - self.ok_button = Gtk.Button(_('OK')) - hbox.pack_end(self.ok_button, False, False, self.spacing_size) - self.ok_button.get_child().set_width_chars(10) - self.ok_button.set_tooltip_text(_('Close this window')); - self.ok_button.connect('clicked', self.on_button_ok_clicked) - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def reset_window(self): - - """Can be called by anything. - - Redraws the window, without the need to destroy the old one and replace - it with a new one. - """ - - # This code is copied from - # config.GenericEditWin.on_button_reset_clicked() - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - # (Add widgets) - - - def add_checkbutton(self, grid, text, set_flag, mod_flag, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.CheckButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (string or None): The text to display in the checkbutton's - label. No label is used if 'text' is an empty string or None - - set_flag (bool): True if the checkbutton is selected - - mod_flag (bool): True if the checkbutton can be toggled by the user - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The checkbutton widget created - - """ - - checkbutton = Gtk.CheckButton() - grid.attach(checkbutton, x, y, wid, hei) - checkbutton.set_active(set_flag) - checkbutton.set_sensitive(mod_flag) - checkbutton.set_hexpand(True) - if text is not None and text != '': - checkbutton.set_label(text) - - return checkbutton - - - def add_combo(self, grid, combo_list, active_val, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a simple Gtk.ComboBox to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - combo_list (list): A list of values to display in the combobox. - This function expects a simple, one-dimensional list - - active_val (string or None): If not None, a value matching one of - the items in combo_list, that should be the active row in the - combobox - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The combobox widget created - - """ - - store = Gtk.ListStore(str) - - count = -1 - active_index = 0 - for string in combo_list: - store.append( [string] ) - - count += 1 - if active_val is not None and active_val == string: - active_index = count - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, x, y, wid, hei) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_entry_text_column(0) - combo.set_active(active_index) - - return combo - - - def add_combo_with_data(self, grid, combo_list, active_val, x, y, wid, - hei): - - """Called by various functions in the child preference window. - - Adds a more complex Gtk.ComboBox to the tab's Gtk.Grid. This function - expects a list of values in the form - - [ [val1, val2], [val1, val2], ... ] - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - combo_list (list): The list described above. For something more - simple, see self.add_combo() - - active_val (string or None): If not None, a value matching a - the second item ('val2') in one of the combo_list pairs; the - specified pair is the active row in the combobox - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The combobox widget created - - """ - - store = Gtk.ListStore(str, str) - - count = -1 - active_index = 0 - for mini_list in combo_list: - store.append( [ str(mini_list[0]), str(mini_list[1]) ] ) - - count += 1 - if active_val is not None and active_val == mini_list[1]: - active_index = count - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, x, y, wid, hei) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_entry_text_column(0) - combo.set_active(active_index) - - return combo - - - def add_entry(self, grid, text, edit_flag, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.Entry to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (string or None): The initial contents of the entry. - - edit_flag (bool): True if the contents of the entry can be edited - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The entry widget created - - """ - - entry = Gtk.Entry() - grid.attach(entry, x, y, wid, hei) - entry.set_hexpand(True) - - if text is not None: - entry.set_text(str(text)) - - if not edit_flag: - entry.set_editable(False) - - return entry - - -# def add_image # Inherited from GenericConfigWin - - - def add_label(self, grid, text, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.Label to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (str): Pango markup displayed in the label - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The label widget created - - """ - - label = Gtk.Label() - grid.attach(label, x, y, wid, hei) - label.set_markup(text) - label.set_hexpand(True) - label.set_alignment(0, 0.5) - - return label - - - def add_radiobutton(self, grid, prev_button, text, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.RadioButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - prev_button (Gtk.RadioButton or None): When this is the first - radio button in the group, None. Otherwise, the previous - radio button in the group. Use of this argument links the radio - buttons together, ensuring that only one of them can be active - at any time - - text (string or None): The text to display in the radiobutton's - label. No label is used if 'text' is an empty string or None - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The radiobutton widget created - - """ - - radiobutton = Gtk.RadioButton.new_from_widget(prev_button) - grid.attach(radiobutton, x, y, wid, hei) - radiobutton.set_hexpand(True) - if text is not None and text != '': - radiobutton.set_label(text) - - return radiobutton - - - def add_spinbutton(self, grid, min_val, max_val, step, val, x, y, wid, \ - hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.SpinButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - min_val (int): The minimum permitted in the spinbutton - - max_val (int or None): The maximum values permitted in the - spinbutton. If None, this function assigns a very large maximum - value (a billion) - - step (int): Clicking the up/down arrows in the spin button - increments/decrements the value by this much - - val (int): The current value of the spinbutton - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The spinbutton widget created - - """ - - # If the specified value of 'max_valu' was none, just use a very big - # number (as Gtk.SpinButton won't accept the None argument) - if max_val is None: - max_val = 1000000000 - - spinbutton = Gtk.SpinButton.new_with_range(min_val, max_val, step) - grid.attach(spinbutton, x, y, wid, hei) - spinbutton.set_value(val) - spinbutton.set_hexpand(False) - - return spinbutton - - - def add_textview(self, grid, contents_list, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.TextView to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - contents_list (list): The initial contents of the textview. Each - item in the list is a line in the textview. - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The textview and textbuffer widgets created - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - textview = Gtk.TextView() - scrolled.add(textview) - - textbuffer = textview.get_buffer() - - if contents_list: - textbuffer.set_text(str.join('\n', contents_list)) - - return textview, textbuffer - - -# def add_treeview # Inherited from GenericConfigWin - - - # Callback class methods - - - def on_button_ok_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Closes the window. - - Args: - - button (Gtk.Button): The button clicked - - """ - - # Destroy the window - self.destroy() - - -class CustomDLEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in a - downloads.CustomDLManager object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (downloads.CustomDLManager): The object whose attributes will - be edited in this window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Custom downloads window starts here.' \ - + ' In the menu, click Edit > System preferences...' \ - + ' > Operations > Custom > Edit' - ) - - Gtk.Window.__init__(self, title=_('Custom download settings')) - - if self.is_duplicate(app_obj, edit_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The downloads.CustomDLManager object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (From self.setup_name_tab) - self.button = None # Gtk.Button - self.button2 = None # Gtk.Button - self.checkbutton = None # Gtk.CheckButton - self.checkbutton2 = None # Gtk.CheckButton - # (From self.setup_subtitles_tab) - self.checkbutton3 = None # Gtk.CheckButton - self.checkbutton4 = None # Gtk.CheckButton - self.treeview = None # Gtk.TreeView - self.liststore = None # Gtk.ListStore - self.button3 = None # Gtk.Button - self.treeview2 = None # Gtk.TreeView - self.liststore2 = None # Gtk.ListStore - self.button4 = None # Gtk.Button - # (From self.setup_clips_tab) - self.checkbutton5 = None # Gtk.CheckButton - # (From self.setup_slices_tab) - self.checkbutton6 = None # Gtk.CheckButton - self.liststore3 = None # Gtk.ListStore - self.button5 = None # Gkt.Button - self.button6 = None # Gkt.Button - # (From self.setup_delay_tab) - self.checkbutton7 = None # Gtk.CheckButton - self.spinbutton = None # Gtk.SpinButton - self.spinbutton2 = None # Gtk.SpinButton - # (From self.setup_mirrors_tab) - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.radiobutton3 = None # Gtk.RadioButton - self.radiobutton4 = None # Gtk.RadioButton - # (From self.setup_livestreams_tab) - self.checkbutton8 = None # Gtk.CheckButton - self.checkbutton9 = None # Gtk.CheckButton - self.checkbutton10 = None # Gtk.CheckButton - self.checkbutton11 = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK') are required, or False if just the 'OK' button is required - self.multi_button_flag = True - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - self.edit_dict = {} - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericConfigWin - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - """ - - # Import the main window (for convenience) - main_win_obj = self.app_obj.main_win_obj - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - setattr(self.edit_obj, key, self.edit_dict[key]) - - - # If 'divert_mode' has changed, the Video Catalogue must be redrawn - if 'divert_mode' in self.edit_dict \ - and self.app_obj.catalogue_mode_type != 'simple' \ - and main_win_obj.video_index_current_dbid is not None: - - main_win_obj.video_catalogue_redraw_all( - main_win_obj.video_index_current_dbid, - main_win_obj.catalogue_toolbar_current_page, - ) - - # The changes can now be cleared - self.edit_dict = {} - - -# def retrieve_val(): # Inherited from GenericConfigWin - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_name_tab() - self.setup_subtitles_tab() - self.setup_clips_tab() - self.setup_slices_tab() - self.setup_delay_tab() - self.setup_mirrors_tab() - self.setup_livestreams_tab() - - # Unusual step - signal connects go here, after all widgets have been - # created - - # (From self.setup_name_tab) - self.button.connect('clicked', self.on_clone_settings_clicked) - self.button2.connect('clicked', self.on_reset_settings_clicked) - self.checkbutton.connect( - 'toggled', - self.on_dl_by_video_button_toggled, - ) - self.checkbutton2.connect( - 'toggled', - self.on_dl_precede_button_toggled, - ) - - # (From self.setup_subtitles_tab) - self.checkbutton3.connect( - 'toggled', - self.on_dl_if_subs_button_toggled, - ) - self.button3.connect('clicked', self.on_add_language_clicked) - self.button4.connect('clicked', self.on_remove_language_clicked) - - # (From self.setup_clips_tab) - self.checkbutton5.connect( - 'toggled', - self.on_split_button_toggled, - ) - - # (From self.setup_slices_tab) - self.checkbutton6.connect( - 'toggled', - self.on_slice_button_toggled, - ) - self.button5.connect('clicked', self.on_select_all_button_clicked) - self.button6.connect('clicked', self.on_unselect_all_button_clicked) - - # (From self.setup_delay_tab) - self.checkbutton7.connect( - 'toggled', - self.on_delay_button_toggled, - ) - self.spinbutton.connect( - 'value-changed', - self.on_delay_spinbutton_changed, - ) - - # (From self.setup_mirrors_tab) - self.radiobutton.connect( - 'toggled', - self.on_divert_button_toggled, - ) - self.radiobutton2.connect( - 'toggled', - self.on_divert_button_toggled, - ) - self.radiobutton3.connect( - 'toggled', - self.on_divert_button_toggled, - ) - self.radiobutton4.connect( - 'toggled', - self.on_divert_button_toggled, - ) - - # (From self.setup_livestreams_tab) - self.checkbutton8.connect( - 'toggled', - self.on_ignore_live_button_toggled, - ) - self.checkbutton9.connect( - 'toggled', - self.on_ignore_old_live_button_toggled, - ) - self.checkbutton10.connect( - 'toggled', - self.on_dl_if_live_button_toggled, - ) - self.checkbutton11.connect( - 'toggled', - self.on_dl_if_old_live_button_toggled, - ) - - - def setup_name_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Name' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Custom downloads > Name' - ) - - tab, grid = self.add_notebook_tab(_('_Name')) - grid_width = 4 - - label = self.add_label(grid, - _('Name'), - 0, 0, 2, 1, - ) - - entry = self.add_entry(grid, - 'name', - 2, 0, 1, 1, - ) - entry.set_hexpand(True) - - entry2 = self.add_entry(grid, - None, - 3, 0, 1, 1, - ) - entry2.set_text('#' + str(self.edit_obj.uid)) - entry2.set_hexpand(False) - - label2 = self.add_label(grid, - _('Usage'), - 0, 1, 2, 1, - ) - - entry3 = self.add_entry(grid, - None, - 2, 1, 2, 1, - ) - entry3.set_editable(False) - - if self.edit_obj == self.app_obj.general_custom_dl_obj: - entry3.set_text( - _('Applies everywhere except the Classic Mode tab'), - ) - elif self.edit_obj == self.app_obj.classic_custom_dl_obj: - entry3.set_text(_('Applies to the Classic Mode tab')) - else: - entry3.set_text(_('Applies when selected')) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1) - grid_width2 = 3 - grid2.set_vexpand(True) - - # (Empty label for spacing) - label3 = Gtk.Label() - grid2.attach(label3, 0, 0, grid_width2, 1) - label3.set_hexpand(True) - - # (Frame containing text) - frame = Gtk.Frame() - grid2.attach(frame, 1, 1, 1, 1) - frame.set_hexpand(False) - - grid3 = Gtk.Grid() - frame.add(grid3) - grid3.set_border_width(self.spacing_size * 2) - grid3.set_column_spacing(self.spacing_size) - grid3.set_row_spacing(self.spacing_size) - grid3.set_hexpand(False) - - self.add_label(grid3, - '' + _('HINT') + ': ' + _('Enable these settings first!'), - 0, 0, 1, 1, - ) - - self.checkbutton = self.add_checkbutton(grid3, - _('Download each video independently of its channel or playlist'), - None, - 0, 1, 1, 1, - ) - self.checkbutton.set_active(self.edit_obj.dl_by_video_flag) - - self.checkbutton2 = self.add_checkbutton(grid3, - _( - 'Check channels/playlists/folders before each custom' \ - + ' download (recommended)', - ), - None, - 0, 2, 1, 1, - ) - self.checkbutton2.set_active(self.edit_obj.dl_precede_flag) - if not self.edit_obj.dl_by_video_flag \ - or ( - self.app_obj.classic_custom_dl_obj is not None \ - and self.app_obj.classic_custom_dl_obj == self.edit_obj - ): - self.checkbutton2.set_sensitive(False) - - # (Frame containing text) - frame2 = Gtk.Frame() - grid2.attach(frame2, 1, 2, 1, 1) - frame.set_hexpand(False) - - grid4 = Gtk.Grid() - frame2.add(grid4) - grid4.set_border_width(self.spacing_size * 2) - grid4.set_column_spacing(self.spacing_size) - grid4.set_row_spacing(self.spacing_size) - grid4.set_hexpand(False) - - self.add_label(grid4, - '' + _('HINT') + ': ' + _( - 'The Check all and Download all buttons will' \ - + ' not start a custom download!', - ), - 0, 0, 1, 1, - ) - self.add_label(grid4, - _( - 'Use the main menu, or right-click a video, channel,' \ - + ' playlist or folder!', - ), - 0, 1, 1, 1, - ) - - # (Strips of widgets at the bottom of the primary grid) - frame3 = self.add_pixbuf(grid, - 'copy_large', - 0, 3, 1, 1, - ) - frame3.set_hexpand(False) - - self.button = Gtk.Button( - _( - 'Import settings from the general custom download into this' \ - + ' window', - ), - ) - grid.attach(self.button, 1, 3, (grid_width - 1), 1) - self.button.set_hexpand(True) - if self.edit_obj == self.app_obj.general_custom_dl_obj: - # No point cloning the General Custom Download Manager onto itself - self.button.set_sensitive(False) - - frame4 = self.add_pixbuf(grid, - 'warning_large', - 0, 4, 1, 1, - ) - frame4.set_hexpand(False) - - self.button2 = Gtk.Button( - _('Completely reset all settings to their default values'), - ) - grid.attach(self.button2, 1, 4, (grid_width - 1), 1) - self.button2.set_hexpand(True) - - - def setup_subtitles_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Subtitles' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Custom downloads > Subtitles' - ) - - tab, grid = self.add_notebook_tab(_('_Subtitles')) - grid.set_column_homogeneous(True) - grid.set_row_homogeneous(False) - grid_width = 2 - - if not self.edit_obj.dl_by_video_flag \ - or not self.edit_obj.dl_precede_flag: - desens_flag = True - else: - desens_flag = False - - # Subtitles settings - self.add_label(grid, - '' + _('Subtitles settings') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'Note: this tab downloads videos. To download subtitles, use' \ - + ' the download options windows', - ) + '', - 0, 1, grid_width, 1, - ) - - self.checkbutton3 = self.add_checkbutton(grid, - _('Only download videos with available subtitles'), - None, - 0, 2, grid_width, 1, - ) - self.checkbutton3.set_active(self.edit_obj.dl_if_subs_flag) - if desens_flag: - self.checkbutton3.set_sensitive(False) - - self.checkbutton4 = self.add_checkbutton(grid, - _( - 'During the custom download, don\'t add videos without subtitles' \ - + ' to the database at all', - ), - 'ignore_if_no_subs_flag', - 0, 3, grid_width, 1, - ) - self.checkbutton4.set_active(self.edit_obj.ignore_if_no_subs_flag) - if desens_flag or not self.edit_obj.dl_if_subs_flag: - self.checkbutton4.set_sensitive(False) - - self.add_label(grid, - _( - 'Require subtitles in these languages (leave empty to download' \ - + ' videos with any subtitles):', - ), - 0, 4, grid_width, 1, - ) - - self.treeview, self.liststore = self.add_treeview(grid, - 0, 5, 1, 1) - self.treeview.set_vexpand(True) - - for language in formats.LANGUAGE_CODE_LIST: - self.liststore.append([ - language + ' [' + formats.LANGUAGE_CODE_DICT[language] + ']', - ]) - - self.button3 = Gtk.Button(_('Add language') + ' >>>') - grid.attach(self.button3, 0, 6, 1, 1) - if desens_flag or not self.edit_obj.dl_if_subs_flag: - self.button3.set_sensitive(False) - - self.treeview2, self.liststore2 = self.add_treeview(grid, - 1, 5, 1, 1) - self.treeview2.set_vexpand(True) - - # Initialise the right-hand treeview - self.setup_subtitles_tab_redraw_list() - - self.button4 = Gtk.Button('<<< ' + _('Remove language')) - grid.attach(self.button4, 1, 6, 1, 1) - if desens_flag or not self.edit_obj.dl_if_subs_flag: - self.button4.set_sensitive(False) - - - def setup_subtitles_tab_redraw_list(self): - - """Called by self.setup_subtitles_tab() and then again by - self.on_add_languages_clicked() and .on_remove_languages_clicked(). - - Update the Gtk.ListStore containing the user's preferred video/audio - formats. - """ - - # Empty the treeview - self.liststore2.clear() - - # (Need to reverse formats.LANGUAGE_CODE_DICT for quick lookup) - rev_dict = {} - for key in formats.LANGUAGE_CODE_DICT: - rev_dict[formats.LANGUAGE_CODE_DICT[key]] = key - - # Refill the treeview - lang_list = self.retrieve_val('dl_if_subs_list') - for lang_code in lang_list: - self.liststore2.append([ - rev_dict[lang_code] + ' [' + lang_code + ']', - ]) - - - def setup_clips_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Clips' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Custom downloads > Clips' - ) - - tab, grid = self.add_notebook_tab(_('_Clips')) - - # Clip settings - self.add_label(grid, - '' + _('Clip settings') + '', - 0, 0, 1, 1, - ) - - self.checkbutton5 = self.add_checkbutton(grid, - _( - 'Split videos into video clips using timestamps (requires' \ - + ' FFmpeg)', - ), - None, - 0, 1, 1, 1, - ) - self.checkbutton5.set_active(self.edit_obj.split_flag) - if not self.edit_obj.dl_by_video_flag: - self.checkbutton5.set_sensitive(False) - - - def setup_slices_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Slices' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Custom downloads > Slices' - ) - - tab, grid = self.add_notebook_tab(_('S_lices')) - grid_width = 2 - - # Slice settings - self.add_label(grid, - '' + _('Slice settings') + '', - 0, 0, grid_width, 1, - ) - - self.checkbutton6 = self.add_checkbutton(grid, - _( - 'Remove slices from the video using SponsorBlock data' \ - + ' (requires FFmpeg)'), - None, - 0, 1, grid_width, 1, - ) - self.checkbutton6.set_active(self.edit_obj.slice_flag) - if not self.edit_obj.dl_by_video_flag \ - or self.edit_obj.split_flag: - self.checkbutton6.set_sensitive(False) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 2, 1, 8) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - scrolled.set_hexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Remove'), _('Type'), ] - ): - if i == 0: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - renderer_toggle.set_sensitive(True) - renderer_toggle.set_activatable(True) - renderer_toggle.connect( - 'toggled', - self.on_treeview_button_toggled, - ) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.liststore3 = Gtk.ListStore(bool, str) - treeview.set_model(self.liststore3) - - # Initialise the list - self.setup_slices_tab_update_treeview() - - # Editing buttons - self.button5 = Gtk.Button(_('Remove all')) - grid.attach(self.button5, 1, 2, 1, 1) - self.button5.set_hexpand(True) - if not self.edit_obj.slice_flag: - self.button5.set_sensitive(False) - - self.button6 = Gtk.Button(_('Remove none')) - grid.attach(self.button6, 1, 3, 1, 1) - self.button6.set_hexpand(True) - if not self.edit_obj.slice_flag: - self.button6.set_sensitive(False) - - # (Empty labels for aesthetics) - for i in range(8): - self.add_label(grid, - '', - 1, (4 + i), 1, 1, - ) - - - def setup_slices_tab_update_treeview(self): - - """ Called by self.setup_downloads_tab. - - Fills or updates the treeview. - """ - - self.liststore3.clear() - - slice_dict = self.retrieve_val('slice_dict') - for category in formats.SPONSORBLOCK_CATEGORY_LIST: - - row_list = [] - if slice_dict[category]: - row_list.append(True) - else: - row_list.append(False) - - row_list.append(category) - - self.liststore3.append(row_list) - - - def setup_delay_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Delays' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Custom downloads > Delay' - ) - - tab, grid = self.add_notebook_tab(_('_Delays')) - grid_width = 2 - - # Download delay settings - self.add_label(grid, - '' + _('Download delay settings') + '', - 0, 0, grid_width, 1, - ) - - self.checkbutton7 = self.add_checkbutton(grid, - _('Apply a delay after each video/channel/playlist is downloaded'), - None, - 0, 1, grid_width, 1, - ) - self.checkbutton7.set_active(self.edit_obj.delay_flag) - - self.add_label(grid, - _('Maximum delay to apply (in minutes)'), - 0, 2, 1, 1, - ) - - self.spinbutton = self.add_spinbutton(grid, - 0.2, - None, - 0.2, # Step - None, - 1, 2, 1, 1, - ) - if not self.edit_obj.delay_flag: - self.spinbutton.set_sensitive(False) - - self.add_label(grid, - _( - 'Minimum delay to apply (in minutes; randomises the actual delay)' - ), - 0, 3, 1, 1, - ) - - self.spinbutton2 = self.add_spinbutton(grid, - 0, - self.edit_obj.delay_max, - 0.2, # Step - 'delay_min', - 1, 3, 1, 1, - ) - if not self.edit_obj.delay_flag: - self.spinbutton2.set_sensitive(False) - - - def setup_mirrors_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Mirrors' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Custom downloads > Mirrors' - ) - - tab, grid = self.add_notebook_tab(_('_Mirrors')) - grid_width = 2 - - # Mirror settings - self.add_label(grid, - '' + _('Mirror settings') + '', - 0, 0, grid_width, 1, - ) - - self.radiobutton = self.add_radiobutton(grid, - None, - _('Obtain a YouTube video from the original website'), - None, - None, - 0, 1, grid_width, 1, - ) - - self.radiobutton2 = self.add_radiobutton(grid, - self.radiobutton, - _('Obtain the video from HookTube rather than YouTube'), - None, - None, - 0, 2, grid_width, 1, - ) - if self.edit_obj.divert_mode == 'hooktube': - self.radiobutton2.set_active(True) - - self.radiobutton3 = self.add_radiobutton(grid, - self.radiobutton2, - _('Obtain the video from Invidious rather than YouTube'), - None, - None, - 0, 3, grid_width, 1, - ) - if self.edit_obj.divert_mode == 'invidious': - self.radiobutton3.set_active(True) - - self.radiobutton4 = self.add_radiobutton(grid, - self.radiobutton3, - _('Obtain the video from this YouTube front-end:'), - None, - None, - 0, 4, 1, 1, - ) - if self.edit_obj.divert_mode == 'other': - self.radiobutton4.set_active(True) - - self.entry = self.add_entry(grid, - 'divert_website', - 1, 4, 1, 1, - ) - self.entry.set_hexpand(True) - if not self.edit_obj.divert_mode == 'other': - self.entry.set_sensitive(False) - - msg = _('Type the exact text that replaces www.youtube.com e.g.') - msg = re.sub('www.youtube.com', ' www.youtube.com ', msg) - - self.add_label(grid, - '' + msg + ' hooktube.com', - 0, 6, grid_width, 1, - ) - - if not self.edit_obj.dl_by_video_flag: - self.radiobutton.set_sensitive(False) - self.radiobutton2.set_sensitive(False) - self.radiobutton3.set_sensitive(False) - self.radiobutton4.set_sensitive(False) - self.entry.set_sensitive(False) - - - def setup_livestreams_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Livestreams' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Custom downloads > Livestreams' - ) - - tab, grid = self.add_notebook_tab(_('L_ivestreams')) - - # Livestream settings - self.add_label(grid, - '' + _('Livestream settings') + '', - 0, 0, 1, 1, - ) - - self.checkbutton8 = self.add_checkbutton(grid, - _('Don\'t download broadcasting livestreams'), - None, - 0, 1, 1, 1, - ) - self.checkbutton8.set_active(self.edit_obj.ignore_stream_flag) - if not self.edit_obj.dl_by_video_flag: - self.checkbutton8.set_sensitive(False) - - self.checkbutton9 = self.add_checkbutton(grid, - _('Don\'t download finished livestreams'), - None, - 0, 2, 1, 1, - ) - self.checkbutton9.set_active(self.edit_obj.ignore_old_stream_flag) - if not self.edit_obj.dl_by_video_flag: - self.checkbutton9.set_sensitive(False) - - self.checkbutton10 = self.add_checkbutton(grid, - _('Only download broadcasting livestreams'), - None, - 0, 3, 1, 1, - ) - self.checkbutton10.set_active(self.edit_obj.ignore_stream_flag) - if not self.edit_obj.dl_by_video_flag: - self.checkbutton10.set_sensitive(False) - - self.checkbutton11 = self.add_checkbutton(grid, - _('Only download finished livestreams'), - None, - 0, 4, 1, 1, - ) - self.checkbutton11.set_active(self.edit_obj.ignore_old_stream_flag) - if not self.edit_obj.dl_by_video_flag: - self.checkbutton11.set_sensitive(False) - - - # Callback class methods - - - def on_add_language_clicked(self, button): - - """Called by callback in self.setup_tabs(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = self.treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - name = model[tree_iter][0] - # From a string in the form 'English [en]', remove the language code to - # get a key in formats.LANGUAGE_CODE_DICT, e.g. 'English' - match = re.search(r'^(.*)\s\[', name) - if match: - - language = match.groups()[0] - if language in formats.LANGUAGE_CODE_DICT: - - lang_code = formats.LANGUAGE_CODE_DICT[language] - new_list = self.retrieve_val('dl_if_subs_list') - # (Don't add duplicates) - if lang_code not in new_list: - - new_list.append(lang_code) - - self.edit_dict['dl_if_subs_list'] = new_list - - # Update the treeview - self.setup_subtitles_tab_redraw_list() - - - def on_clone_settings_clicked(self, button): - - """Called by callback in self.setup_tabs(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Custom downloads > Name' - ) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'This procedure cannot be reversed. Are you sure you want to' \ - + ' continue?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'clone_custom_dl_manager_from_window', - 'data': [self, self.edit_obj], - }, - ) - - - def on_delay_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables delays between downloads of individual media data - objects. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['delay_flag'] = True - - self.spinbutton.set_sensitive(True) - self.spinbutton2.set_sensitive(True) - - else: - - self.edit_dict['delay_flag'] = False - - self.spinbutton.set_sensitive(False) - self.spinbutton2.set_sensitive(False) - - - def on_delay_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_tabs(). - - Updates both delay spinbutton widgets, as well as setting the IV. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - value = spinbutton.get_value() - self.edit_dict['delay_max'] = value - self.spinbutton2.set_range(0, value) - - - def on_divert_button_toggled(self, radiobutton): - - """Called from callback in self.setup_tabs(). - - Sets the YouTube mirror from which downloads are obtained. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if self.radiobutton.get_active(): - self.edit_dict['divert_mode'] = 'default' - elif self.radiobutton2.get_active(): - self.edit_dict['divert_mode'] = 'hooktube' - elif self.radiobutton3.get_active(): - self.edit_dict['divert_mode'] = 'invidious' - elif self.radiobutton4.get_active(): - self.edit_dict['divert_mode'] = 'other' - - if self.radiobutton4.get_active(): - self.entry.set_sensitive(True) - else: - self.entry.set_sensitive(False) - self.entry.set_text('') - - - def on_dl_by_video_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables downloading videos independently of their channels/ - playlists. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['dl_by_video_flag'] = True - - self.checkbutton2.set_active(True) - self.checkbutton2.set_sensitive(True) - if not self.retrieve_val('dl_precede_flag'): - self.checkbutton3.set_active(False) - self.checkbutton3.set_sensitive(False) - self.checkbutton4.set_active(False) - self.checkbutton4.set_sensitive(False) - self.button3.set_sensitive(False) - self.button4.set_sensitive(False) - else: - self.checkbutton3.set_sensitive(True) - if not self.retrieve_val('dl_if_subs_flag'): - self.checkbutton4.set_active(False) - self.checkbutton4.set_sensitive(False) - self.button3.set_sensitive(False) - self.button4.set_sensitive(False) - else: - if not self.retrieve_val('ignore_if_no_subs_flag'): - self.checkbutton4.set_active(False) - self.checkbutton4.set_sensitive(False) - else: - self.checkbutton4.set_sensitive(True) - self.button3.set_sensitive(True) - self.button4.set_sensitive(True) - self.checkbutton5.set_sensitive(True) - if self.retrieve_val('split_flag'): - self.checkbutton6.set_sensitive(False) - else: - self.checkbutton6.set_sensitive(True) - self.radiobutton.set_sensitive(True) - self.radiobutton2.set_sensitive(True) - self.radiobutton3.set_sensitive(True) - self.radiobutton4.set_sensitive(True) - if self.retrieve_val('divert_mode') == 'other': - self.entry.set_sensitive(True) - else: - self.entry.set_sensitive(False) - self.checkbutton8.set_sensitive(True) - self.checkbutton9.set_sensitive(True) - self.checkbutton10.set_sensitive(True) - self.checkbutton11.set_sensitive(True) - - else: - - self.edit_dict['dl_by_video_flag'] = False - - self.checkbutton2.set_sensitive(False) - self.checkbutton2.set_active(False) - self.checkbutton3.set_sensitive(False) - self.checkbutton3.set_active(False) - self.checkbutton4.set_sensitive(False) - self.checkbutton4.set_active(False) - self.button3.set_sensitive(False) - self.button4.set_sensitive(False) - self.checkbutton5.set_sensitive(False) - self.checkbutton5.set_active(False) - self.checkbutton6.set_sensitive(False) - self.checkbutton6.set_active(False) - self.radiobutton.set_sensitive(False) - self.radiobutton2.set_sensitive(False) - self.radiobutton3.set_sensitive(False) - self.radiobutton4.set_sensitive(False) - self.entry.set_sensitive(False) - self.checkbutton8.set_active(False) - self.checkbutton8.set_sensitive(False) - self.checkbutton9.set_active(False) - self.checkbutton9.set_sensitive(False) - self.checkbutton10.set_active(False) - self.checkbutton10.set_sensitive(False) - self.checkbutton11.set_active(False) - self.checkbutton11.set_sensitive(False) - - - def on_dl_if_live_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables only downloading broadcasting livestreams. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['dl_if_stream_flag'] = True - self.checkbutton8.set_active(False) - - else: - - self.edit_dict['dl_if_stream_flag'] = False - - - def on_dl_if_old_live_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables only downloading finished livestreams. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['dl_if_old_stream_flag'] = True - self.checkbutton9.set_active(False) - - else: - - self.edit_dict['dl_if_old_stream_flag'] = False - - - def on_dl_if_subs_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables downloading only videos with subtitles. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['dl_if_subs_flag'] = True - - self.checkbutton4.set_sensitive(True) - self.button3.set_sensitive(True) - self.button4.set_sensitive(True) - - else: - - self.edit_dict['dl_if_subs_flag'] = False - - self.checkbutton4.set_sensitive(False) - self.checkbutton4.set_active(False) - self.button3.set_sensitive(False) - self.button4.set_sensitive(False) - - - def on_dl_precede_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables checking videos, before downloading them independently - of their channels/playlists. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['dl_precede_flag'] = True - - self.checkbutton3.set_sensitive(True) - if not self.retrieve_val('dl_if_subs_flag'): - self.checkbutton4.set_active(False) - self.checkbutton4.set_sensitive(False) - self.button3.set_sensitive(False) - self.button4.set_sensitive(False) - else: - self.checkbutton4.set_sensitive(True) - self.button3.set_sensitive(True) - self.button4.set_sensitive(True) - - else: - - self.edit_dict['dl_precede_flag'] = False - - self.checkbutton3.set_sensitive(False) - self.checkbutton3.set_active(False) - self.checkbutton4.set_sensitive(False) - self.checkbutton4.set_active(False) - self.button3.set_sensitive(False) - self.button4.set_sensitive(False) - - - def on_ignore_live_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables not downloading broadcasting livestreams. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['ignore_stream_flag'] = True - self.checkbutton10.set_active(False) - - else: - - self.edit_dict['ignore_stream_flag'] = False - - - def on_ignore_old_live_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables not downloading finished livestreams. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['ignore_old_stream_flag'] = True - self.checkbutton11.set_active(False) - - else: - - self.edit_dict['ignore_old_stream_flag'] = False - - - def on_remove_language_clicked(self, button): - - """Called by callback in self.setup_tabs(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = self.treeview2.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - name = model[tree_iter][0] - # From a string in the form 'English [en]', remove the language name to - # get a value in formats.LANGUAGE_CODE_DICT, e.g. 'en' - match = re.search(r'^.*\s\[(.*)\]', name) - if match: - - lang_code = match.groups()[0] - old_list = self.retrieve_val('dl_if_subs_list') - new_list = [] - for other_code in old_list: - if other_code != lang_code: - new_list.append(other_code) - - self.edit_dict['dl_if_subs_list'] = new_list - - # Update the treeview - self.setup_subtitles_tab_redraw_list() - - - def on_reset_settings_clicked(self, button): - - """Called by callback in self.setup_tabs(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Custom downloads > Name' - ) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'This procedure cannot be reversed. Are you sure you want to' \ - + ' continue?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'reset_custom_dl_manager', - # (Reset this edit window, if the user clicks 'yes') - 'data': [self], - }, - ) - - - def on_select_all_button_clicked(self, button): - - """Called by callback in self.setup_tabs(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - slice_dict = self.retrieve_val('slice_dict') - for key in slice_dict: - slice_dict[key] = True - - self.edit_dict['slice_dict'] = slice_dict - - # Update the treeview - self.setup_slices_tab_update_treeview() - - - def on_slice_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables removing slices from a video. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['slice_flag'] = True - - self.button5.set_sensitive(True) - self.button6.set_sensitive(True) - - else: - - self.edit_dict['split_flag'] = False - - self.button5.set_sensitive(False) - self.button6.set_sensitive(False) - - - def on_split_button_toggled(self, checkbutton): - - """Called from callback in self.setup_tabs(). - - Enables/disables splitting a video into video clips. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - - self.edit_dict['split_flag'] = True - - self.checkbutton6.set_sensitive(False) - self.checkbutton6.set_active(False) - - else: - - self.edit_dict['split_flag'] = False - - self.checkbutton6.set_sensitive(True) - - - def on_treeview_button_toggled(self, renderer_toggle, tree_path): - - """Called from callback in self.setup_slices_tab(). - - Enables/disables a category of video slice. - - Args: - - renderer_toggle (Gtk.CellRendererToggle): The widget clicked - - tree_path (Gtk.TreePath): Path to the clicked row - - """ - - # (This condition makes the treeview insensitive, when other widgets - # in the same tab are insensitive) -# if self.retrieve_val('slice_flag'): - if self.checkbutton6.get_active(): - - self.liststore3[tree_path][0] = not self.liststore3[tree_path][0] - - slice_dict = self.retrieve_val('slice_dict') - slice_dict[self.liststore3[tree_path][1]] \ - = self.liststore3[tree_path][0] - - self.edit_dict['slice_dict'] = slice_dict - - - def on_unselect_all_button_clicked(self, button): - - """Called by callback in self.setup_tabs(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - slice_dict = self.retrieve_val('slice_dict') - for key in slice_dict: - slice_dict[key] = False - - self.edit_dict['slice_dict'] = slice_dict - - # Update the treeview - self.setup_slices_tab_update_treeview() - - -class OptionsEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in an - options.OptionsManager object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (options.OptionsManager): The object whose attributes will be - edited in this window - - init_mode (str or None): If specified, a tab is automatically selected; - one of the values specified in the comments to self.select_tab() - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj, init_mode=None): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options window starts here.' \ - + ' In the menu, click Edit > General download options...' - ) - - Gtk.Window.__init__(self, title=_('Download options')) - - if self.is_duplicate(app_obj, edit_obj, init_mode): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The options.OptionManager object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # The 'embed_subs' option appears in two different places - self.embed_checkbutton = None # Gtk.CheckButton - self.embed_checkbutton2 = None # Gtk.CheckButton - # The Gtk.ListStore containing the user's preferred video/audio formats - # (which must be redrawn when self.apply_changes() is called) - self.formats_liststore = None # Gtk.ListStore - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK') are required, or False if just the 'OK' button is required - self.multi_button_flag = True - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # In this edit window, the key-value pairs directly correspond to those - # in options.OptionsManager.options_dict, rather than corresponding - # directly to attributes in the options.OptionsManager object - # Because of that, we use our own .apply_changes() and .retrieve_val() - # functions, rather than relying on the generic functions - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - # IVs used to keep track of widget changes in the 'Files' tab - # Flag set to to False when that tab's output template widgets are - # desensitised, True when sensitised - self.template_flag = False - # A list of Gtk widgets to (de)sensitise in when the flag changes - self.template_widget_list = [] - - # Code - # ---- - - # Set up the edit window - self.setup() - - # Automatically open a particular tab, if required - self.select_tab(init_mode) - - - # Public class methods - - - def is_duplicate(self, app_obj, edit_obj, init_mode): - - """Called by self.__init__. - - Don't open this edit window, if another with the same .edit_obj is - already open. - - If 'init_mode' is specified, switch the visible tab in the existing - preference window (if any). - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (options.OptionsManager): The object whose attributes will - be edited in this window - - init_mode (str or None): One of the values specified in the - comments to self.select_tab() - - Return values: - - True if a duplicate is found, False if not - - """ - - for config_win_obj in app_obj.main_win_obj.config_win_list: - - if isinstance(config_win_obj, GenericEditWin) \ - and config_win_obj.edit_obj == edit_obj: - - # Duplicate found. Make it prominent... - config_win_obj.present() - # ...and switch to a particular tab, if required - config_win_obj.select_tab(init_mode) - - return True - - # Not a duplicate - return False - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - def select_tab(self, init_mode=None): - - """Called by self.__init__(). - - On startup, automatically open a particular tab, if required. - - Args: - - init_mode (str or None): If specified: - - 'formats' - media formats - - 'subs' - subtitles - - (any other value is ignored) - - """ - - if init_mode is not None: - - if init_mode == 'formats': - self.select_formats_tab() - elif init_mode == 'subs': - self.select_subs_tab() - - - def select_formats_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the media format options are - displayed. - """ - - # Opens tab: self.setup_formats_preferred_tab() - self.notebook.set_current_page(3) - - - def select_subs_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the subtitle options are - displayed. - """ - - # Opens tab: self.setup_subtitles_options_tab() - self.notebook.set_current_page(5) - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - - In this edit window we apply changes to self.edit_obj.options_dict - (rather than to self.edit_obj's attributes directly, as in the generic - function.) - """ - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - - if key in self.edit_obj.options_dict: - self.edit_obj.options_dict[key] = self.edit_dict[key] - - # The name can also be updated, if it has been changed (but it the - # entry was blank, keep the old name) - if 'name' in self.edit_dict \ - and self.edit_dict['name'] != '': - self.edit_obj.name = self.edit_dict['name'] - # The description can also be updated, even if it is blank - if 'descrip' in self.edit_dict: - self.edit_obj.descrip = self.edit_dict['descrip'] - - # The changes can now be cleared - self.edit_dict = {} - - # The user can specify multiple video/audio formats. If a mixture of - # both is specified, then video formats must be listed before audio - # formats (or youtube-dl won't download them all) - # Tell the options.OptionManager object to rearrange them, if - # necessary - self.edit_obj.rearrange_formats() - # ...then redraw the textview in the Formats tab - self.formats_tab_redraw_list() - - # Update the associated mainwin.DropZoneBox in the main window's - # Drag and Drop tab - if self.edit_obj.uid in self.app_obj.classic_dropzone_list: - dropzone_obj \ - = self.app_obj.main_win_obj.drag_drop_dict[self.edit_obj.uid] - dropzone_obj.update_widgets() - - - def retrieve_val(self, name): - - """Can be called by anything. - - Any changes the user has made are temporarily stored in self.edit_dict. - - In the generic function, each key corresponds to an attribute in the - object being edited, self.edit_obj. In this window, it corresponds to a - key in self.edit_obj.options_dict. - - If 'name' exists as a key in that dictionary, retrieve the - corresponding value and return it. Otherwise, the user hasn't yet - modified the value, so retrieve directly from the attribute in the - object being edited. - - Args: - - name (str): The name of the attribute in the object being edited - - Return values: - - The original or modified value of that attribute - - """ - - if name in self.edit_dict: - - return self.edit_dict[name] - - elif name == 'uid' \ - or name == 'name' \ - or name == 'descrip' \ - or name == 'dbid_list': - - return getattr(self.edit_obj, name) - - elif name in self.edit_obj.options_dict: - - value = self.edit_obj.options_dict[name] - if type(value) is list or type(value) is dict: - return value.copy() - else: - return value - - else: - - return self.app_obj.system_error( - 404, - 'Unrecognised property name \'' + name + '\'', - ) - - - def add_tooltip(self, text, widget=None, widget2=None, widget3=None, \ - widget4=None, widget5=None): - - """Called by various tabs, to show the equivalent youtube-dl switches - for each Tartube download option. - - Adds the tooltip 'text' to any specified widgets (maximum four). - - Args: - - text (str): The text to use in the tooltip - - widget, widget2, widget3, widget4, widget5 (widget or None): Any - Gtk widget for which we can call .set_tooltip_text() - - """ - - if widget is not None: - widget.set_tooltip_text(text) - if widget2 is not None: - widget2.set_tooltip_text(text) - if widget3 is not None: - widget3.set_tooltip_text(text) - if widget4 is not None: - widget4.set_tooltip_text(text) - if widget5 is not None: - widget5.set_tooltip_text(text) - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_name_tab() - self.setup_downloads_tab() - self.setup_files_tab() - self.setup_formats_tab() - if not self.app_obj.simple_options_flag: - self.setup_post_process_tab() - else: - self.setup_convert_tab() - self.setup_subtitles_tab() - if not self.app_obj.simple_options_flag: - self.setup_advanced_tab() - - - def setup_name_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Name' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Name' - ) - - tab, grid = self.add_notebook_tab(_('_Name')) - grid_width = 3 - - label = self.add_label(grid, - _('Name for these download options'), - 0, 0, 1, 1, - ) - - entry = self.add_entry(grid, - 'name', - 1, 0, 1, 1, - ) - entry.set_hexpand(True) - - entry2 = self.add_entry(grid, - None, - 2, 0, 1, 1, - ) - entry2.set_text('#' + str(self.edit_obj.uid)) - entry2.set_hexpand(False) - entry2.set_max_length(8) - - label = self.add_label(grid, - _('Description'), - 0, 1, 1, 1, - ) - - entry3 = self.add_entry(grid, - 'descrip', - 1, 1, 2, 1, - ) - entry3.set_hexpand(True) - - label2 = self.add_label(grid, - _('Download options applied to'), - 0, 2, 1, 1, - ) - - entry4 = self.add_entry(grid, - None, - 1, 2, 2, 1, - ) - entry4.set_editable(False) - - if self.edit_obj == self.app_obj.general_options_obj: - entry4.set_text(_('All channels, playlists and folders')) - elif self.edit_obj == self.app_obj.classic_options_obj: - entry4.set_text(_('Downloads in the Classic Mode tab')) - elif self.edit_obj.dbid_list: - entry4.set_text(self.get_options_applied_text(self.edit_obj)) - else: - entry4.set_text(_('These options are not applied to anything')) - - if self.app_obj.simple_options_flag: - - self.add_label(grid, - _( - 'Additional download options, e.g. --write-subs (do not use' \ - + ' -o or --output)', - ), - 0, 3, grid_width, 1, - ) - - else: - - self.add_label(grid, - _('Additional download options'), - 0, 3, 1, 1, - ) - - if os.name == 'nt': - - checkbutton = self.add_checkbutton(grid, - _( - 'Use ONLY these options (Tartube adds the output folder)', - ), - None, - 1, 3, 2, 1, - ) - # (Signal connect appears below) - - else: - - checkbutton = self.add_checkbutton(grid, - _( - 'Use ONLY these options (Tartube adds the output' \ - + ' directory)', - ), - None, - 1, 3, 2, 1, - ) - # (Signal connect appears below) - - checkbutton.set_active(self.retrieve_val('direct_cmd_flag')) - - checkbutton2 = self.add_checkbutton(grid, - _('If URLs are specified below, use only those URLs'), - 'direct_url_flag', - 1, 4, 2, 1, - ) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_direct_cmd_toggled, - checkbutton2, - ) - - self.add_textview(grid, - 'extra_cmd_string', - 0, 5, grid_width, 1, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 6, grid_width, 1) - - if self.app_obj.simple_options_flag: - frame = self.add_pixbuf(grid2, - 'hand_right_large', - 0, 0, 1, 1, - ) - frame.set_hexpand(False) - frame.set_size_request(75, -1) - - else: - frame = self.add_pixbuf(grid2, - 'hand_left_large', - 0, 0, 1, 1, - ) - frame.set_hexpand(False) - frame.set_size_request(75, -1) - - button = Gtk.Button() - grid2.attach(button, 1, 0, 1, 1) - if not self.app_obj.simple_options_flag: - button.set_label(_('Hide advanced download options')) - else: - button.set_label(_('Show advanced download options')) - button.set_hexpand(True) - button.connect('clicked', self.on_simple_options_clicked) - - frame2 = self.add_pixbuf(grid2, - 'copy_large', - 0, 1, 1, 1, - ) - frame2.set_hexpand(False) - - button2 = Gtk.Button( - _('Import general download options into this window'), - ) - grid2.attach(button2, 1, 1, 1, 1) - button2.set_hexpand(True) - button2.connect('clicked', self.on_clone_options_clicked) - if self.edit_obj == self.app_obj.general_options_obj: - # No point cloning the General Options Manager onto itself - button2.set_sensitive(False) - - frame3 = self.add_pixbuf(grid2, - 'warning_large', - 0, 2, 1, 1, - ) - frame3.set_hexpand(False) - - button3 = Gtk.Button( - _('Completely reset all download options to their default values'), - ) - grid2.attach(button3, 1, 2, 1, 1) - button3.set_hexpand(True) - button3.connect('clicked', self.on_reset_options_clicked) - - - def setup_downloads_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Downloads' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads' - ) - - # Simple options only - if self.app_obj.simple_options_flag: - - tab, grid = self.add_notebook_tab(_('_Downloads')) - - row_count = self.downloads_age_widgets(grid, 0) - row_count = self.downloads_size_limit_widgets(grid, row_count) - row_count = self.downloads_date_widgets(grid, row_count) - row_count = self.downloads_views_widgets(grid, row_count) - - # All options - else: - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Downloads'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_downloads_general_tab(inner_notebook) - self.setup_downloads_videos_tab(inner_notebook) - self.setup_downloads_live_tab(inner_notebook) - self.setup_downloads_playlists_tab(inner_notebook) - self.setup_downloads_limits_tab(inner_notebook) - self.setup_downloads_merge_tab(inner_notebook) - self.setup_downloads_extractor_tab(inner_notebook) - self.setup_downloads_filtering_tab(inner_notebook) - self.setup_downloads_external_tab(inner_notebook) - - - def setup_downloads_general_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'General' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > General' \ - + ' (to make hidden tabs visible, click the \'Show advanced' \ - + ' download options\' button in the Name tab)' - ) - - tab, grid = self.add_inner_notebook_tab('_General', inner_notebook) - - # General download options - self.add_label(grid, - '' + _('General download options') + '', - 0, 0, 1, 1, - ) - - row_count = 1 - row_count = self.downloads_general_widgets(grid, row_count) - - # General download options (yt-dlp only) - self.add_label(grid, - '' + _('General download options') + '' + self.ytdlp_only(), - 0, (row_count + 1), 2, 1, - ) - - label = self.add_label(grid, - _( - 'Number of fragments of a DASH/HLS video to download' \ - + ' concurrently', - ), - 0, (row_count + 2), 1, 1 - ) - - spinbutton = self.add_spinbutton(grid, - 1, None, 1, - 'concurrent_fragments', - 1, (row_count + 2), 1, 1 - ) - self.add_tooltip('-N, --concurrent-fragments N', label, spinbutton) - - label2 = self.add_label(grid, - _( - 'Minimum download rate (bytes/sec) below which throttling' \ - + ' is assumed', - ), - 0, (row_count + 3), 1, 1 - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, None, 100, - 'throttled_rate', - 1, (row_count + 3), 1, 1 - ) - self.add_tooltip('--throttled-rate RATE', label2, spinbutton2) - - - def setup_downloads_videos_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Videos' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Videos' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Videos'), - inner_notebook, - ) - grid_width = 2 - - # Video selection options (yt-dlp only) - self.add_label(grid, - '' + _('Video selection options') + '' + self.ytdlp_only(), - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Stop download process when encountering a file in the archive'), - 'break_on_existing', - 0, 1, grid_width, 1, - ) - self.add_tooltip('--break-on-existing', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Stop download process when encountering a file that has' \ - + ' been filtered out', - ), - 'break_on_reject', - 0, 2, grid_width, 1, - ) - self.add_tooltip('--break-on-reject', checkbutton2) - - label = self.add_label(grid, - _('Number of failures allowed before rest of playlist is skipped'), - 0, 3, 1, 1 - ) - - spinbutton = self.add_spinbutton(grid, - 0, None, 1, - 'skip_playlist_after_errors', - 1, 3, 1, 1 - ) - self.add_tooltip('--skip-playlist-after-errors N', label, spinbutton) - - # Verbosity and simulation options (yt-dlp only) - self.add_label(grid, - '' + _('Verbosity and simulation options') + '' \ - + self.ytdlp_only(), - 0, 4, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Ignore \'No video formats\' error (useful for extracting' \ - + ' metadata from unavailable videos)', - ), - 'ignore_no_formats_error', - 0, 5, grid_width, 1, - ) - self.add_tooltip('--ignore-no-formats-error', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Force download archive entries to be written as long as' \ - + ' no errors occur', - ), - 'force_write_archive', - 0, 6, grid_width, 1, - ) - self.add_tooltip('--force-write-archive', checkbutton2) - - - def setup_downloads_live_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Live' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Live' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Live'), - inner_notebook, - ) - grid_width = 2 - - # Livestream options (yt-dlp only) - self.add_label(grid, - '' + _('Livestream options') + '' + self.ytdlp_only(), - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Download livestreams from start (experimental, YouTube only)'), - 'live_from_start', - 0, 1, grid_width, 1, - ) - self.add_tooltip('--live-from-start', checkbutton) - - label = self.add_label(grid, - _('Minimum seconds to wait for scheduled streams'), - 0, 2, 1, 1 - ) - - spinbutton = self.add_spinbutton(grid, - 0, None, 1, - 'wait_for_video_min', - 1, 2, 1, 1 - ) - self.add_tooltip('--wait-for-video MIN', label, spinbutton) - - - def setup_downloads_playlists_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Playlists' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Playlists' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Playlists'), - inner_notebook, - ) - - row_count = self.downloads_playlist_widgets(grid, 0) - - - def setup_downloads_limits_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Limits' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Limits' - ) - - tab, grid = self.add_inner_notebook_tab( - _('L_imits'), - inner_notebook, - ) - - row_count = self.downloads_age_widgets(grid, 0) - row_count = self.downloads_size_limit_widgets(grid, row_count) - row_count = self.downloads_date_widgets(grid, row_count) - row_count = self.downloads_views_widgets(grid, row_count) - - - def setup_downloads_merge_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Merge' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Merge' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Merge'), - inner_notebook, - ) - - # Video/audio merge options (yt-dlp only) - self.add_label(grid, - '' + _('Video/audio merge options') + '' \ - + self.ytdlp_only(), - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Allow multiple video streams to be merged into a single file'), - 'video_multistreams', - 0, 1, 1, 1, - ) - self.add_tooltip('--video-multistreams', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Allow multiple audio streams to be merged into a single file'), - 'audio_multistreams', - 0, 2, 1, 1, - ) - self.add_tooltip('--audio-multistreams', checkbutton2) - - - def setup_downloads_extractor_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Extractor' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Extractor' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Extractor'), - inner_notebook, - ) - grid_width = 2 - - # Extractor options (yt-dlp only) - self.add_label(grid, - '' + _('Extractor options') + '' + self.ytdlp_only(), - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - 'Number of retries for known extractor errors', - 0, 1, 1, 1, - ) - - combo_list = [ - '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'infinite', - ] - - combo = self.add_combo(grid, - combo_list, - 'extractor_retries', - 1, 1, 1, 1 - ) - combo.set_hexpand(True) - self.add_tooltip('--extractor-retries RETRIES', label, combo) - - checkbutton = self.add_checkbutton(grid, - _('Do not process dynamic DASH manifests'), - 'no_allow_dynamic_mpd', - 0, 2, grid_width, 1, - ) - self.add_tooltip('--no-allow-dynamic-mpd', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Split HLS playlists to different formats at discontinuities' \ - + ' such as ad breaks', - ), - 'hls_split_discontinuity', - 0, 3, grid_width, 1, - ) - self.add_tooltip('--hls-split-discontinuity', checkbutton2) - - # Extractor arguments - self.add_label(grid, - '' + _('Extractor arguments') + '', - 0, 4, grid_width, 1, - ) - - label2 = self.add_label(grid, - '' + _('One argument per line, e.g.') \ - + ' youtube:skip=dash,hls;player_client=android', - 0, 5, grid_width, 1, - ) - - textview, textbuffer = self.add_textview(grid, - 'extractor_args_list', - 0, 6, grid_width, 1, - ) - self.add_tooltip('--extractor-args KEY:ARGS', label2, textview) - - - def setup_downloads_filtering_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Filtering' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Filtering' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Filtering'), - inner_notebook, - ) - - row_count = self.downloads_filtering_widgets(grid, 0) - - - def setup_downloads_external_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'External' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > External' - ) - - tab, grid = self.add_inner_notebook_tab(_('E_xternal'), inner_notebook) - - row_count = self.downloads_external_widgets(grid, 0) - - - def setup_files_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Files' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Files'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_files_names_tab(inner_notebook) - if not self.app_obj.simple_options_flag: - self.setup_files_override_tab(inner_notebook) - self.setup_files_paths_tab(inner_notebook) - self.setup_files_filesystem_tab(inner_notebook) - self.setup_files_cookies_tab(inner_notebook) - if not self.app_obj.simple_options_flag: - self.setup_files_shortcuts_tab(inner_notebook) - self.setup_files_write_move_tab(inner_notebook) - self.setup_files_keep_tab(inner_notebook) - - - def setup_files_names_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'File names' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files > File names' - ) - - tab, grid = self.add_inner_notebook_tab( - _('File _names'), - inner_notebook, - ) - grid_width = 2 - - # File name options - self.add_label(grid, - '' + _('File name options') + '', - 0, 0, 2, 1, - ) - - label = self.add_label(grid, - _('Format for video file names'), - 0, 1, 1, 1, - ) - label.set_hexpand(False) - - store = Gtk.ListStore(int, str) - num_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] - for num in num_list: - store.append( [num, formats.FILE_OUTPUT_NAME_DICT[num]] ) - - current_format = self.edit_obj.options_dict['output_format'] - current_template = formats.FILE_OUTPUT_CONVERT_DICT[current_format] - if current_template is None: - current_template = self.edit_obj.options_dict['output_template'] - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 0, 2, 1, 1) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, "text", 1) - combo.set_entry_text_column(1) - combo.set_active(num_list.index(current_format)) - combo.set_hexpand(False) - # (Signal connect appears below) - self.add_tooltip('-o, --output TEMPLATE', combo) - - label2 = self.add_label(grid, - _('File output template'), - 1, 1, 1, 1, - ) - label2.set_hexpand(True) - - entry = self.add_entry(grid, - None, - 1, 2, 1, 1, - ) - entry.set_text(current_template) - entry.set_hexpand(True) - # (Signal connect appears below) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - # Add widgets to a list, so we can sensitise them when a custom - # template is selected, and desensitise them the rest of the time - self.template_widget_list = [entry] - - self.add_label(grid2, - _('Add to template:'), - 0, 0, 1, 1, - ) - - master_list = [ - _('Video properties'), - [ - 'id', _('Video ID'), - 'title', _('Video title'), - 'display_id', _('Alternative video ID'), - 'alt_title', _('Secondary video title'), - 'url', _('Video URL'), - 'ext', _('Video filename extension'), - 'license', _('Video licence'), - 'age_limit', _('Age restriction (years)'), - 'is_live', _('Is a livestream'), - 'video_autonumber', _('Autonumber videos'), - 'playlist_autonumber', - _('Autonumber videos (playlists)'), - ], - _('Creator/uploader'), - [ - 'uploader', _('Full name of video uploader'), - 'uploader_id', _('Uploader ID'), - 'creator', _('Nickname/ID of video uploader'), - 'channel', _('Channel name'), - 'channel_id', _('Channel ID'), - 'playlist', _('Playlist name'), - 'playlist_id', _('Playlist ID'), - 'playlist_index', _('Video index in playlist'), - ], - _('Date/time/location'), - [ - 'release_date', _('Release date (YYYYMMDD)'), - 'release_date_custom', - _('Release date (custom format)'), - 'timestamp', _('Release time (UNIX timestamp)'), - 'timestamp_custom', _('Release time (custom format)'), - 'upload_date', _('Upload date (YYYYMMDD)'), - 'upload_date_custom', - _('Upload date (custom format)'), - 'duration', _('Video length (seconds)'), - 'duration_custom', _('Video length (custom format)'), - 'location', _('Filming location'), - ], - _('Time format'), - [], - _('Video format'), - [ - 'format', _('Video format'), - 'format_id', _('Video format code'), - 'width', _('Video width'), - 'height', _('Video height'), - 'resolution', _('Video resolution'), - 'fps', _('Video frame rate'), - 'tbr', _('Average video/audio bitrate (KiB/s)'), - 'vbr', _('Average video bitrate (KiB/s)'), - 'abr', _('Average audio bitrate (KiB/s)'), - ], - _('Ratings/comments'), - [ - 'view_count', _('Number of views'), - 'like_count', _('Number of positive ratings'), - 'dislike_count', _('Number of negative ratings'), - 'average_rating', _('Average rating'), - 'repost_count', _('Number of reposts'), - 'comment_count', _('Number of comments'), - ], - ] - - # (Create the entry and its button first, so they are available to the - # callbacks) - entry2 = Gtk.Entry() - entry2.set_hexpand(True) - - button = Gtk.Button(_('Reset')) - button.set_sensitive(False) - # (Signal connect appears below) - - row_num = -1 - while master_list: - - row_num += 1 - - this_title = master_list.pop(0) - this_store_list = master_list.pop(0) - - self.add_label(grid2, - this_title, - 1, row_num, 1, 1, - ) - - if this_title == _('Time format'): - - grid2.attach(entry2, 2, row_num, 1, 1) - grid2.attach(button, 3, row_num, 1, 1) - - self.template_widget_list.append(entry2) - self.template_widget_list.append(button) - - else: - - this_store = Gtk.ListStore(str) - # (The dictionary is used by - # self.on_file_tab_add_button_clicked() to translate the - # visible string into the string youtube-dl uses) - this_store_dict = {} - while this_store_list: - item = this_store_list.pop(0) - mod_item = this_store_list.pop(0) - - this_store_dict[mod_item] = item - this_store.append( [mod_item] ) - - this_combo = Gtk.ComboBox.new_with_model(this_store) - grid2.attach(this_combo, 2, row_num, 1, 1) - this_renderer_text = Gtk.CellRendererText() - this_combo.pack_start(this_renderer_text, True) - this_combo.add_attribute(this_renderer_text, "text", 0) - this_combo.set_entry_text_column(0) - this_combo.set_active(0) - - this_button = Gtk.Button(_('Add')) - grid2.attach(this_button, 3, row_num, 1, 1) - this_button.connect( - 'clicked', - self.on_file_tab_add_button_clicked, - entry, - entry2, - this_combo, - this_store_dict, - ) - - self.template_widget_list.append(this_combo) - self.template_widget_list.append(this_button) - - if this_title == _('Date/time/location'): - - # (Signal connects from above) - this_combo.connect( - 'changed', - self.on_date_time_combo_changed, - entry2, - button, - this_store_dict, - ) - - button.connect( - 'clicked', - self.on_file_tab_reset_button_clicked, - entry2, - this_combo, - this_store_dict, - ) - - # (Signal connects from above) - combo.connect( - 'changed', - self.on_file_tab_main_combo_changed, - entry, - entry2, - button, - ) - entry.connect('changed', self.on_file_tab_main_entry_changed) - - # (De)sensitise widgets in self.template_widget_list - if current_format == 0: - self.file_tab_sensitise_widgets(True) - else: - self.file_tab_sensitise_widgets(False) - - - def setup_files_override_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Override' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files > Override' - ) - - - tab, grid = self.add_inner_notebook_tab( - _('_Override'), - inner_notebook, - ) - grid_width = 4 - - # List of output filename templates (yt-dlp only) - self.add_label(grid, - '' + _('List of output filename templates') + '' \ - + self.ytdlp_only(), - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'Overrides the output template in the \'File names\' tab', - ) + '', - 0, 1, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 2, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate([ _('Type'), _('Template') ]): - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - liststore = Gtk.ListStore(str, str) - treeview.set_model(liststore) - - # Initialise the list - self.setup_files_override_tab_update_treeview(liststore) - - # Add editing buttons - label = self.add_label(grid, - _('Output type'), - 0, 3, 1, 1, - ) - - combo_list = formats.YTDLP_OUTPUT_TYPE_LIST.copy() - combo = self.add_combo(grid, - combo_list, - None, - 1, 3, 1, 1, - ) - combo.set_active(0) - - label2 = self.add_label(grid, - _('Output template'), - 0, 4, 1, 1, - ) - - entry = self.add_entry(grid, - None, - 1, 4, (grid_width - 1), 1, - ) - - button = Gtk.Button() - grid.attach(button, 0, 5, 1, 1) - button.set_label(_('Add')) - button.connect( - 'clicked', - self.on_ytdlp_output_add_button_clicked, - liststore, - combo, - entry, - ) - - button2 = Gtk.Button() - grid.attach(button2, 1, 5, 1, 1) - button2.set_label(_('Delete')) - button2.connect( - 'clicked', - self.on_ytdlp_output_delete_button_clicked, - treeview, - ) - - button3 = Gtk.Button() - grid.attach(button3, 3, 5, 1, 1) - button3.set_label(_('Refresh list')) - button3.connect( - 'clicked', - self.on_ytdlp_output_refresnh_button_clicked, - treeview, - ) - - - def setup_files_override_tab_update_treeview(self, liststore): - - """Can be called by anything. - - Fills or updates the treeview. - - Args: - - liststore (Gtk.ListStore): The treeview's model - - """ - - liststore.clear() - - for item in self.retrieve_val('output_format_list'): - - # (Each 'item' is in the form TYPES:TEMPLATE) - match = re.search(r'^([^\:]+)\:(.*)', item) - if match: - liststore.append([ match.groups()[0], match.groups()[1] ]) - - - def setup_files_paths_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Paths' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files > Paths' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Paths'), - inner_notebook, - ) - grid_width = 5 - - # List of output paths (yt-dlp only) - self.add_label(grid, - '' + _('List of output paths') + '' + self.ytdlp_only(), - 0, 0, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate([ _('Type'), _('Path') ]): - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - liststore = Gtk.ListStore(str, str) - treeview.set_model(liststore) - - # Initialise the list - self.setup_files_paths_tab_update_treeview(liststore) - - # Add editing buttons - label = self.add_label(grid, - _('Output type'), - 0, 2, 1, 1, - ) - - combo_list = formats.YTDLP_OUTPUT_TYPE_LIST.copy() - combo_list.insert(0, 'home') - combo_list.insert(1, 'temp') - combo = self.add_combo(grid, - combo_list, - None, - 1, 2, 1, 1, - ) - combo.set_active(0) - - label2 = self.add_label(grid, - _('Output path'), - 0, 3, 1, 1, - ) - - entry = self.add_entry(grid, - None, - 1, 3, (grid_width - 2), 1, - ) - entry.set_editable(False) - - button = Gtk.Button() - grid.attach(button, 4, 3, 1, 1) - button.set_label(_('Set')) - button.connect( - 'clicked', - self.on_ytdlp_paths_set_button_clicked, - entry, - ) - - button2 = Gtk.Button() - grid.attach(button2, 0, 4, 1, 1) - button2.set_label(_('Add')) - button2.connect( - 'clicked', - self.on_ytdlp_paths_add_button_clicked, - liststore, - combo, - entry, - ) - - button3 = Gtk.Button() - grid.attach(button3, 1, 4, 1, 1) - button3.set_label(_('Delete')) - button3.connect( - 'clicked', - self.on_ytdlp_paths_delete_button_clicked, - treeview, - ) - - button4 = Gtk.Button() - grid.attach(button4, 3, 4, 2, 1) - button4.set_label(_('Refresh list')) - button4.connect( - 'clicked', - self.on_ytdlp_paths_refresh_button_clicked, - treeview, - ) - - - def setup_files_paths_tab_update_treeview(self, liststore): - - """Can be called by anything. - - Fills or updates the treeview. - - Args: - - liststore (Gtk.ListStore): The treeview's model - - """ - - liststore.clear() - - for item in self.retrieve_val('output_path_list'): - - # (Each 'item' is in the form TYPES:PATH) - match = re.search(r'^([^\:]+)\:(.*)', item) - if match: - liststore.append([ match.groups()[0], match.groups()[1] ]) - - - def setup_files_filesystem_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Filesystem' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files > Filesystem' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Filesystem'), - inner_notebook, - ) - grid_width = 2 - - # Filesystem options - self.add_label(grid, - '' + _('Filesystem options') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Restrict filenames to ASCII characters'), - 'restrict_filenames', - 0, 1, grid_width, 1, - ) - self.add_tooltip('--restrict-filenames', checkbutton) - - if not self.app_obj.simple_options_flag: - - checkbutton2 = self.add_checkbutton(grid, - _('Don\'t use the server\'s file modification time'), - 'nomtime', - 0, 2, grid_width, 1, - ) - self.add_tooltip('--no-mtime', checkbutton2) - - checkbutton3 = self.add_checkbutton(grid, - _('Download all videos into this folder'), - None, - 0, 3, 1, 1, - ) - # (Signal connect appears below) - - # (Currently, only three fixed folders are elligible for this mode, so - # we'll just add them individually) - store = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str) - # (Guard against mainapp.TartubeApp.debug_open_options_win_flag being - # set...) - count = 0 - if self.app_obj.fixed_misc_folder: - store.append([ - self.app_obj.main_win_obj.pixbuf_dict['folder_green_small'], - self.app_obj.fixed_misc_folder.dbid, - self.app_obj.fixed_misc_folder.name, - ]) - misc_index = count - count += 1 - - if self.app_obj.fixed_clips_folder: - store.append([ - self.app_obj.main_win_obj.pixbuf_dict['folder_green_small'], - self.app_obj.fixed_clips_folder.dbid, - self.app_obj.fixed_clips_folder.name, - ]) - clips_index = count - count += 1 - - if self.app_obj.fixed_temp_folder: - store.append([ - self.app_obj.main_win_obj.pixbuf_dict['folder_blue_small'], - self.app_obj.fixed_temp_folder.dbid, - self.app_obj.fixed_temp_folder.name, - ]) - temp_index = count - count += 1 - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 1, 3, 1, 1) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 2) - - combo.set_entry_text_column(0) - combo.set_hexpand(True) - # (Signal connect appears below) - - # (Acceptable values are 'temp', 'misc', 'clips' - current_override = self.edit_obj.options_dict['use_fixed_folder'] - if current_override is None: - checkbutton3.set_active(False) - combo.set_sensitive(False) - combo.set_active(0) - - else: - checkbutton3.set_active(True) - combo.set_sensitive(True) - if current_override == 'temp': - combo.set_active(temp_index) - elif current_override == 'misc': - combo.set_active(misc_index) - elif current_override == 'clips': - combo.set_active(clips_index) - else: - # Default to the 'Unsorted Videos' folder - combo.set_active(misc_index) - - # (Signal connects from above) - checkbutton3.connect('toggled', self.on_fixed_folder_toggled, combo) - combo.connect('changed', self.on_fixed_folder_changed) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 4, grid_width, 1) - - # Filesystem options (yt-dlp only) - self.add_label(grid2, - '' + _('Filesystem options') + '' + self.ytdlp_only(), - 0, 0, grid_width, 1, - ) - - checkbutton4 = self.add_checkbutton(grid2, - _('Force filenames to be MS Windows compatible'), - 'windows_filenames', - 0, 1, grid_width, 1, - ) - self.add_tooltip('--windows-filenames', checkbutton4) - - label = self.add_label(grid2, - _( - 'Limit filename length (excluding extension) to this' \ - + ' many characters', - ), - 0, 2, 1, 1 - ) - - spinbutton = self.add_spinbutton(grid2, - 0, None, 1, - 'trim_filenames', - 1, 2, 1, 1 - ) - self.add_tooltip('--trim-filenames', label, spinbutton) - - if os.name != 'nt': - msg = _( - 'WARNING: The filename length includes the length of the' \ - + ' folder name!', - ) - else: - msg = _( - 'WARNING: The filename length includes the length of the' \ - + ' directory name!', - ) - - self.add_label(grid2, - '' + msg + '', - 0, 3, grid_width, 1, - ) - - if not self.app_obj.simple_options_flag: - - checkbutton5 = self.add_checkbutton(grid2, - _('Do not overwrite any files'), - 'no_overwrites', - 0, 4, grid_width, 1, - ) - self.add_tooltip('--no-overwrites', checkbutton5) - - checkbutton6 = self.add_checkbutton(grid2, - _( - 'Overwrite all video and metadata files (includes' \ - + ' \'--no-continue\')', - ), - 'force_overwrites', - 0, 5, grid_width, 1, - ) - self.add_tooltip('--force-overwrites', checkbutton6) - - checkbutton7 = self.add_checkbutton(grid2, - _('Write playlist metadata in addition to video metadata'), - 'write_playlist_metafiles', - 0, 6, grid_width, 1, - ) - self.add_tooltip('--write-playlist-metafiles', checkbutton7) - - checkbutton8 = self.add_checkbutton(grid2, - _( - 'Write all fields, including private fields, to the' \ - + ' .info.json file', - ), - 'no_clean_info_json', - 0, 7, grid_width, 1, - ) - self.add_tooltip('--no-clean-infojson', checkbutton8) - - - def setup_files_cookies_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Cookies' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files > Cookies' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Cookies'), - inner_notebook, - ) - grid_width = 3 - - # Cookies options - self.add_label(grid, - '' + _('Cookies options') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Path to the downloader\'s cookie jar file'), - 0, 1, 1, 1, - ) - - set_button = Gtk.Button(_('Set')) - grid.attach(set_button, 1, 1, 1, 1) - # (Signal connect appears below) - - reset_button = Gtk.Button(_('Reset')) - grid.attach(reset_button, 2, 1, 1, 1) - # (Signal connect appears below) - - entry = self.add_entry(grid, - None, - 0, 2, grid_width, 1, - ) - entry.set_editable(False) - self.add_tooltip('--cookies FILE', label, entry) - - init_path = self.retrieve_val('cookies_path') - if init_path == '': - - entry.set_text( - os.path.abspath( - os.path.join( - self.app_obj.data_dir, - self.app_obj.cookie_file_name, - ), - ), - ) - - else: - - entry.set_text(init_path) - - # (Signal connects from above) - set_button.connect( - 'clicked', - self.on_cookies_set_button_clicked, - entry, - ) - reset_button.connect( - 'clicked', - self.on_cookies_reset_button_clicked, - entry, - ) - - if not self.app_obj.simple_options_flag: - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - # Cookie options (yt-dlp only) - self.add_label(grid2, - '' + _('Cookie options') + '' + self.ytdlp_only(), - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid2, - _('Do not read/dump cookies from/to the cookiejar file'), - 'no_cookies', - 0, 1, 1, 1, - ) - self.add_tooltip('--no-cookies', checkbutton) - - checkbutton2 = self.add_checkbutton(grid2, - _('Do not load cookies from browser'), - 'no_cookies_from_browser', - 0, 2, 1, 1, - ) - self.add_tooltip('--no-cookies-from-browser', checkbutton2) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid3 = self.add_secondary_grid(grid, 0, 4, grid_width, 1) - - label2 = self.add_label(grid3, - _('Retrieve cookies from browser'), - 0, 0, 1, 1 - ) - label2.set_hexpand(False) - - entry2 = self.add_entry(grid3, - None, - 1, 0, 1, 1, - ) - entry2.set_hexpand(True) - entry2.set_editable(False) - entry2.set_text(self.retrieve_val('cookies_from_browser')) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid4 = self.add_secondary_grid(grid, 0, 5, grid_width, 1) - - self.add_label(grid4, - _('Browser'), - 0, 0, 1, 1 - ) - - combo_list = [ - '', 'brave', 'chrome', 'chromium', 'edge', 'firefox', 'opera', - 'safari', 'vivaldi', - ] - combo = self.add_combo(grid4, - combo_list, - None, - 1, 0, 1, 1 - ) - combo.set_active(0) - combo.set_hexpand(True) - # (Signal connect appears below) - - self.add_label(grid4, - _('Chromium keyring'), - 2, 0, 1, 1 - ) - - combo_list2 = ['', 'basictext', 'gnomekeyring', 'kwallet'] - combo2 = self.add_combo(grid4, - combo_list2, - None, - 3, 0, 1, 1 - ) - combo2.set_active(0) - combo2.set_hexpand(True) - # (Signal connect appears below) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid5 = self.add_secondary_grid(grid, 0, 6, grid_width, 1) - - label3 = self.add_label(grid5, - _('Browser profile name (optional)'), - 0, 0, 1, 1 - ) - label3.set_hexpand(False) - - entry3 = self.add_entry(grid5, - None, - 1, 0, 3, 1, - ) - entry3.set_hexpand(True) - # (Signal connect appears below) - - label4 = self.add_label(grid5, - _('Browser profile path (optional)'), - 0, 1, 1, 1 - ) - label4.set_hexpand(False) - - entry4 = self.add_entry(grid5, - None, - 1, 1, 1, 1, - ) - entry4.set_hexpand(True) - entry4.set_editable(False) - - set_button = Gtk.Button(_('Set')) - grid5.attach(set_button, 2, 1, 1, 1) - # (Signal connect appears below) - - reset_button = Gtk.Button(_('Reset')) - grid5.attach(reset_button, 3, 1, 1, 1) - # (Signal connect appears below) - - self.add_tooltip( - '--cookies-from-browser BROWSER[+KEYRING][:PROFILE]', - entry2, - combo, - combo2, - entry3, - entry4, - ) - - # Set the initial values for those widgets, retrieving them from - # the 'cookies_from_browser' download option - match = re.search( - r'^([^+:]+)(\+[^+:]+)?(\:.*)?', - self.retrieve_val('cookies_from_browser'), - ) - if match: - browser = match.groups()[0] - keyring = match.groups()[1] - profile = match.groups()[2] - - if browser != '': - - for i in range(len(combo_list)): - if combo_list[i] == browser: - combo.set_active(i) - break - - if keyring != '' and keyring is not None: - - # (Remove initial +/: characters) - keyring = keyring[1:] - - for i in range(len(combo_list2)): - if combo_list2[i] == keyring: - combo2.set_active(i) - break - - if profile != '' and profile is not None: - - # (Remove initial +/: characters) - profile = profile[1:] - - # This might be a profile name, or a profile path. - # Assume it's a name unless the path exists - if not os.path.isfile(profile): - entry3.set_text(profile) - else: - entry4.set_text(profile) - - # (Signal connects from above) - combo.connect( - 'changed', - self.setup_files_cookies_tab_update, - checkbutton2, - entry2, - combo, - combo2, - entry3, - entry4, - ) - combo2.connect( - 'changed', - self.setup_files_cookies_tab_update, - checkbutton2, - entry2, - combo, - combo2, - entry3, - entry4, - ) - entry3.connect( - 'changed', - self.on_cookies_ytdlp_entry_changed, - entry2, - combo, - combo2, - entry3, - entry4, - ) - set_button.connect( - 'clicked', - self.on_cookies_ytdlp_set_button_clicked, - entry2, - combo, - combo2, - entry3, - entry4, - ) - reset_button.connect( - 'clicked', - self.on_cookies_ytdlp_reset_button_clicked, - entry2, - combo, - combo2, - entry3, - entry4, - ) - - - def setup_files_cookies_tab_update(self, widget, checkbutton, entry, - combo, combo2, entry2, entry3): - - """Called by self.setup_files_cookies_tab() to set the - 'cookies_from_browser' download option, updating several widgets. - - Also called by several callbacks. - - Args: - - widget (Gtk.Entry or Gtk.Combo): Ignored - - checkbutton (Gtk.CheckButton): The 'Do not load cookies from - browser' checkbutton - - entry (Gtk.Entry): The entry box displaying the download option - - combo, combo2, entry2, entry3 (Gtk.Combo, Gtk.Entry): Widgets whose - settings are combined to set the 'cookies_from_browser' - download option - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - browser = model[tree_iter][0] - - if browser == '': - - # Special case: reset the other two widgets, which causes further - # calls to this function, in which the download option is updated - entry2.set_text('') - combo2.set_active(0) - return - - # Otherwise, set the new value of 'cookies_from_browser' now - tree_iter2 = combo2.get_active_iter() - model2 = combo2.get_model() - keyring = model2[tree_iter2][0] - - profile_name = entry2.get_text() - profile_path = entry3.get_text() - - value = browser - if keyring != '': - value += '+' + keyring - - if profile_name != '': - value += ':' + profile_name - elif profile_path != '': - value += ':' + profile_path - - self.edit_dict['cookies_from_browser'] = value - entry.set_text(value) - - # (To avoid confusion, reset the 'Do not load cookies from browser' - # checkbutton; the user can re-enable it, if they want) - checkbutton.set_active(False) - - - def setup_files_shortcuts_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Shortcuts' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files > Shortcuts' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Shortcuts'), - inner_notebook, - ) - - # Internet shortcut options (yt-dlp only) - self.add_label(grid, - '' + _('Internet shortcut options') + '' \ - + self.ytdlp_only(), - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Write an internet shortcut file (.url, .webloc or .desktop)'), - 'write_link', - 0, 1, 1, 1, - ) - self.add_tooltip('--write-link', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Write a Windows internet shortcut file (.url)'), - 'write_url_link', - 0, 2, 1, 1, - ) - self.add_tooltip('--write-url-link', checkbutton2) - - checkbutton3 = self.add_checkbutton(grid, - _('Write a macOS inernet shortcut file (.webloc)'), - 'write_webloc_link', - 0, 3, 1, 1, - ) - self.add_tooltip('--write-webloc-link', checkbutton3) - - checkbutton4 = self.add_checkbutton(grid, - _('Write a Linux internet shortcut file (.desktop)'), - 'write_desktop_link', - 0, 4, 1, 1, - ) - self.add_tooltip('--write-desktop-link', checkbutton4) - - - def setup_files_write_move_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Write/move' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files > Write/move' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Write/move'), - inner_notebook, - ) - grid_width = 2 - - # File write options - self.add_label(grid, - '' + _('File write options') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Write video\'s description to a .description file'), - 'write_description', - 0, 1, grid_width, 1, - ) - self.add_tooltip('--write-description', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Write video\'s metadata to an .info.json file'), - 'write_info', - 0, 2, grid_width, 1, - ) - self.add_tooltip('--write-info-json', checkbutton2) - - checkbutton3 = self.add_checkbutton(grid, - _( - 'Write video\'s annotations to an .annotations.xml file', - ), - 'write_annotations', - 0, 3, grid_width, 1, - ) - self.add_tooltip('--write-annotations', checkbutton3) - - self.add_label(grid, - '' + _( - 'Annotations are not downloaded when checking videos/channels/' \ - + 'playlists/folders' - ) + '', - 1, 4, 1, 1, - ) - - checkbutton4 = self.add_checkbutton(grid, - _('Write the video\'s thumbnail to the same folder'), - 'write_thumbnail', - 0, 5, grid_width, 1, - ) - self.add_tooltip('--write-thumbnail', checkbutton4) - - # File move options - self.add_label(grid, - '' + _('File move options') + '', - 0, 6, grid_width, 1, - ) - - self.add_checkbutton(grid, - _('Move video\'s description file into a sub-folder'), - 'move_description', - 0, 7, grid_width, 1, - ) - - self.add_checkbutton(grid, - _('Write video\'s metadata file into a sub-folder'), - 'move_info', - 0, 8, grid_width, 1, - ) - - self.add_checkbutton(grid, - _('Write video\'s annotations file into a sub-folder'), - 'move_annotations', - 0, 9, grid_width, 1, - ) - - self.add_checkbutton(grid, - _('Write the video\'s thumbnail into a sub-folder'), - 'move_thumbnail', - 0, 10, grid_width, 1, - ) - - - def setup_files_keep_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Keep' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Files > Keep' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Keep'), - inner_notebook, - ) - - # Options during real (not simulated) downloads - self.add_label(grid, - '' + _('Options during real (not simulated) downloads') \ - + '', - 0, 0, 1, 1, - ) - - self.add_checkbutton(grid, - _('Keep the description file after the download has finished'), - 'keep_description', - 0, 1, 1, 1, - ) - - self.add_checkbutton(grid, - _('Keep the metadata file after the download has finished'), - 'keep_info', - 0, 2, 1, 1, - ) - - self.add_checkbutton(grid, - _('Keep the annotations file after the download has finished'), - 'keep_annotations', - 0, 3, 1, 1, - ) - - self.add_checkbutton(grid, - _('Keep the thumbnail file after the download has finished'), - 'keep_thumbnail', - 0, 4, 1, 1, - ) - - # Options during simulated (not real) downloads - self.add_label(grid, - '' + _('Options during simulated (not real) downloads') \ - + '', - 0, 5, 1, 1, - ) - - self.add_checkbutton(grid, - _('Keep the description file after the download has finished'), - 'sim_keep_description', - 0, 6, 1, 1, - ) - - self.add_checkbutton(grid, - _('Keep the metadata file after the download has finished'), - 'sim_keep_info', - 0, 7, 1, 1, - ) - - self.add_checkbutton(grid, - _('Keep the annotations file after the download has finished'), - 'sim_keep_annotations', - 0, 8, 1, 1, - ) - - self.add_checkbutton(grid, - _('Keep the thumbnail file after the download has finished'), - 'sim_keep_thumbnail', - 0, 9, 1, 1, - ) - - - def setup_formats_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Formats' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Formats' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('F_ormats'), 0) - - # When advanced download options are visible, use an inner tab; - # otherwise display the same content in the main tab - if self.app_obj.simple_options_flag: - - self.setup_formats_tab_add_grid(grid) - - else: - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_formats_preferred_tab(inner_notebook) - self.setup_formats_advanced_tab(inner_notebook) - - - def setup_formats_tab_add_grid(self, grid): - - """Called by self.setup_formats_tab() and - .setup_formats_preferred_tab(). - - Adds widgets to the specified grid. - - Args: - - grid (Gtk.Grid): The main grid for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Formats > Preferred' - ) - - # (Force both treeviews to take half the available width) - grid.set_column_homogeneous(True) - grid_width = 4 - - # Preferred format options - self.add_label(grid, - '' + _('Preferred format options') + '', - 0, 0, grid_width, 1, - ) - - if self.app_obj.simple_options_flag: - - # Newbies frequently get confused by this tab, so add an - # explanatory warning - - hbox = Gtk.HBox() - grid.attach(hbox, 0, 1, grid_width, 1) - - frame = Gtk.Frame() - hbox.pack_start(frame, False, False, 0) - frame.set_hexpand(False) - - hbox2 = Gtk.HBox() - frame.add(hbox2) - hbox2.set_border_width(self.spacing_size) - - image = Gtk.Image() - hbox2.pack_start(image, False, False, 0) - image.set_from_pixbuf( - self.app_obj.main_win_obj.pixbuf_dict['attention_large'], - ) - - frame2 = Gtk.Frame() - hbox.pack_start(frame2, True, True, self.spacing_size) - frame2.set_hexpand(True) - - vbox = Gtk.VBox() - frame2.add(vbox) - vbox.set_border_width(self.spacing_size) - - label = Gtk.Label() - vbox.pack_start(label, True, True, 0) - label.set_markup( - '' + _( - 'If your preferred formats are not available, the' \ - + ' download will fail!', - ) + '', - ) - - label2 = Gtk.Label() - vbox.pack_start(label2, True, True, 0) - label2.set_markup( - '' + _( - 'If you want a specific format, install FFmpeg and use' \ - + ' the Convert tab!', - ) + '', - ) - - # Left column - self.add_label(grid, - _('Recognised video/audio formats'), - 0, 2, 2, 1, - ) - - treeview, liststore = self.add_treeview(grid, - 0, 3, 2, 1, - ) - - for key in formats.VIDEO_OPTION_LIST: - liststore.append([key]) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 4, 2, 1) - - button = Gtk.Button(_('Add format') + ' >>>') - grid2.attach(button, 0, 0, 1, 1) - button.set_hexpand(True) - # (Signal connect appears below) - - button2 = Gtk.Button() - grid2.attach(button2, 1, 0, 1, 1) - button2.set_hexpand(False) - button2.set_image( - Gtk.Image.new_from_pixbuf( - self.app_obj.main_win_obj.pixbuf_dict['keyboard_small'], - ), - ) - button2.set_tooltip_text(_('Type extractor code directly')) - # (Signal connect appears below) - - # Right column - self.add_label(grid, - _('List of preferred formats'), - 2, 2, 2, 1, - ) - - treeview2, self.formats_liststore = self.add_treeview(grid, - 2, 3, 2, 1, - ) - self.add_tooltip('-f, --format FORMAT', treeview, treeview2) - - # There are multiple possible format options, any or all of which might - # be set - self.formats_tab_redraw_list() - - button3 = Gtk.Button('<<< ' + _('Remove format')) - grid.attach(button3, 2, 4, 2, 1) - # (Signal connect appears below) - - button4 = Gtk.Button('^ ' + _('Move up')) - grid.attach(button4, 2, 5, 1, 1) - # (Signal connect appears below) - - button5 = Gtk.Button('v ' + _('Move down')) - grid.attach(button5, 3, 5, 1, 1) - # (Signal connect appears below) - - # (Signal connects from above) - # 'Add format' - button.connect( - 'clicked', - self.on_formats_tab_add_clicked, - button3, - button4, - button5, - treeview, - ) - # 'Type extractor code directly' - button2.connect( - 'clicked', - self.on_formats_tab_type_clicked, - button3, - button4, - button5, - treeview, - ) - # 'Remove format' - button3.connect( - 'clicked', - self.on_formats_tab_remove_clicked, - button, - button2, - button4, - button5, - treeview2, - ) - # 'Move up' - button4.connect( - 'clicked', - self.on_formats_tab_up_clicked, - treeview2, - ) - # 'Move down' - button5.connect( - 'clicked', - self.on_formats_tab_down_clicked, - treeview2, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid3 = self.add_secondary_grid(grid, 0, 6, grid_width, 1) - - # Desensitise buttons, as appropriate - format_count = self.formats_tab_count_formats() - if format_count == 0: - button3.set_sensitive(False) - button4.set_sensitive(False) - button5.set_sensitive(False) - - label3 = self.add_label(grid3, - _( - 'If a merge is required after post-processing, output to this' \ - + ' format:', - ), - 0, 0, 1, 1, - ) - - combo_list = ['', 'flv', 'mkv', 'mp4', 'ogg', 'webm'] - combo = self.add_combo(grid3, - combo_list, - None, - 1, 0, 1, 1, - ) - combo.set_active( - combo_list.index(self.retrieve_val('merge_output_format')), - ) - combo.set_hexpand(True) - combo.connect('changed', self.on_formats_tab_combo_changed) - self.add_tooltip('--merge-output-format FORMAT', label3, combo) - - - def setup_formats_preferred_tab(self, inner_notebook): - - """Called by self.setup_formats_tab(). - - Sets up the 'Preferred' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - tab, grid = self.add_inner_notebook_tab( - _('_Preferred'), - inner_notebook, - ) - - self.setup_formats_tab_add_grid(grid) - - - def setup_formats_advanced_tab(self, inner_notebook): - - """Called by self.setup_formats_tab(). - - Sets up the 'Advanced' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Formats > Advanced' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Advanced'), - inner_notebook, - ) - grid_width = 2 - extra_row = 0 - - # Multiple format options - self.add_label(grid, - '' + _('Multiple format options') + '', - 0, 0, grid_width, 1, - ) - - if self.app_obj.allow_ytdl_archive_flag: - - extra_row = 1 - self.add_label(grid, - '' + _( - 'Multiple formats will not be downloaded, because an' \ - + ' archive file will be created' - ) + '\n' + _( - 'The archive file can be disabled in the System' \ - ' Preferences window', - ) + '', - 0, 1, grid_width, 1, - ) - - radiobutton = self.add_radiobutton(grid, - None, - _( - 'For each video, download the first available format from the' \ - + ' preferred list', - ), - None, - None, - 0, (1 + extra_row), grid_width, 1, - ) - if self.retrieve_val('video_format_mode') == 'single': - radiobutton.set_active(True) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _( - 'From the preferred list, download the first format that\'s' \ - + ' available for all videos', - ), - None, - None, - 0, (2 + extra_row), grid_width, 1, - ) - if self.retrieve_val('video_format_mode') == 'single_agree': - radiobutton2.set_active(True) - # (Signal connect appears below) - - radiobutton3 = self.add_radiobutton(grid, - radiobutton2, - _( - 'For each video, download all available formats from the' \ - + ' preferred list', - ), - None, - None, - 0, (3 + extra_row), grid_width, 1, - ) - if self.retrieve_val('video_format_mode') == 'multiple': - radiobutton3.set_active(True) - # (Signal connect appears below) - - radiobutton4 = self.add_radiobutton(grid, - radiobutton2, - _('Download all available formats for all videos'), - None, - None, - 0, (4 + extra_row), grid_width, 1, - ) - if self.retrieve_val('video_format_mode') == 'all': - radiobutton4.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_video_format_mode_toggled, - 'single', - ) - radiobutton2.connect( - 'toggled', - self.on_video_format_mode_toggled, - 'single_agree', - ) - radiobutton3.connect( - 'toggled', - self.on_video_format_mode_toggled, - 'multiple', - ) - radiobutton4.connect( - 'toggled', - self.on_video_format_mode_toggled, - 'all', - ) - - # Other format options - self.add_label(grid, - '' + _('Other format options') + '', - 0, (5 + extra_row), grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Prefer free video formats, unless one is specified above'), - 'prefer_free_formats', - 0, (6 + extra_row), grid_width, 1, - ) - self.add_tooltip('--prefer-free-formats', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Do not download DASH-related data for YouTube videos'), - 'yt_skip_dash', - 0, (7 + extra_row), grid_width, 1, - ) - self.add_tooltip('--youtube-skip-dash-manifest', checkbutton2) - - # Other format options (yt-dlp only) - self.add_label(grid, - '' + _('Other format options') + '' \ - + self.ytdlp_only(), - 0, (8 + extra_row), grid_width, 1, - ) - - checkbutton3 = self.add_checkbutton(grid, - _( - 'Check formats selected are actually downloadable' \ - + ' (Experimental)', - ), - 'check_formats', - 0, (9 + extra_row), grid_width, 1, - ) - self.add_tooltip('--check-formats', checkbutton3) - - checkbutton4 = self.add_checkbutton(grid, - _( - 'Allow unplayable formats to be listed and downloaded (also' \ - + ' disables post-processing)', - ), - 'allow_unplayable_formats', - 0, (10 + extra_row), grid_width, 1, - ) - self.add_tooltip('--allow-unplayable-formats', checkbutton4) - - - def setup_convert_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Convert' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Convert' - ) - - tab, grid = self.add_notebook_tab(_('_Convert')) - grid_width = 4 - - # Convert to video - self.add_label(grid, - '' + _('Convert to video') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Convert downloaded video to another format'), - 0, 1, 2, 1, - ) - - combo_list = ['', 'avi', 'flv', 'mkv', 'mp4', 'ogg', 'webm'] - combo = self.add_combo(grid, - combo_list, - 'recode_video', - 2, 1, 2, 1, - ) - self.add_tooltip('--recode-video FORMAT', label, combo) - - # Convert to audio - self.add_label(grid, - '' + _('Convert to audio') + '', - 0, 2, grid_width, 1, - ) - - # (The MS Windows installer includes FFmpeg) - text = _( - 'Download each video, extract the sound, and then discard the' \ - + ' original video', - ) - if os.name != 'nt': - text += '\n' + _( - '(requires that FFmpeg or AVConv is installed on your system)' - ) - - checkbutton = self.add_checkbutton(grid, - text, - 'extract_audio', - 0, 3, grid_width, 1, - ) - self.add_tooltip('-x, --extract-audio', checkbutton) - - label = self.add_label(grid, - _('Use this audio format:') + ' ', - 0, 4, 1, 1, - ) - label.set_hexpand(False) - - combo_list = formats.AUDIO_FORMAT_LIST.copy() - combo_list.insert(0, '') - combo = self.add_combo(grid, - combo_list, - 'audio_format', - 1, 4, 1, 1, - ) - combo.set_hexpand(True) - self.add_tooltip('--audio-format FORMAT', label, combo) - - label2 = self.add_label(grid, - _('Use this audio quality:') + ' ', - 2, 4, 1, 1, - ) - label2.set_hexpand(False) - - combo2_list = [ - [_('High'), '0'], - [_('Medium'), '5'], - [_('Low'), '9'], - ] - - combo2 = self.add_combo_with_data(grid, - combo2_list, - 'audio_quality', - 3, 4, 1, 1, - ) - combo2.set_hexpand(True) - self.add_tooltip('--audio-quality QUALITY', label2, combo2) - - - def setup_post_process_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Post-processing' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Post-processing' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Post-processing'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_post_process_general_tab(inner_notebook) - self.setup_post_process_ytdlp_tab(inner_notebook) - - - def setup_post_process_general_tab(self, inner_notebook): - - """Called by self.setup_post_process_tab(). - - Sets up the 'General' inner notebook tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Post-processing > General' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_General'), - inner_notebook, - ) - grid_width = 2 - grid.set_column_homogeneous(True) - - # Post-processing options - self.add_label(grid, - '' + _('Post-processing options') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Post-process video files to convert them to audio-only files', - ), - 'extract_audio', - 0, 1, grid_width, 1, - ) - self.add_tooltip('-x, --extract-audio', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Prefer AVConv over FFmpeg'), - 'prefer_avconv', - 0, 2, 1, 1, - ) - if os.name == 'nt': - checkbutton2.set_sensitive(False) - self.add_tooltip('-prefer-avconv', checkbutton2) - - checkbutton3 = self.add_checkbutton(grid, - _('Prefer FFmpeg over AVConv (default)'), - 'prefer_ffmpeg', - 1, 2, 1, 1, - ) - if os.name == 'nt': - checkbutton3.set_sensitive(False) - self.add_tooltip('--prefer-ffmpeg', checkbutton3) - - label = self.add_label(grid, - _('Audio format of the post-processed file'), - 0, 3, 1, 1, - ) - - combo_list = formats.AUDIO_FORMAT_LIST.copy() - combo_list.insert(0, '') - combo = self.add_combo(grid, - combo_list, - 'audio_format', - 1, 3, 1, 1, - ) - self.add_tooltip('--audio-format FORMAT', label, combo) - - label2 = self.add_label(grid, - _('Audio quality of the post-processed file'), - 0, 4, 1, 1, - ) - - combo2_list = [ - [_('High VBR'), '0'], - [_('Medium VBR'), '5'], - [_('Low VBR'), '9'], - [_('320 kb/s'), '320k'], - [_('256 kb/s'), '256k'], - [_('192 kb/s'), '192k'], - [_('128 kb/s'), '128k'], - [_('96 kb/s'), '96k'], - ] - - combo2 = self.add_combo_with_data(grid, - combo2_list, - 'audio_quality', - 1, 4, 1, 1, - ) - self.add_tooltip('--audio-quality QUALITY', label2, combo2) - - label3 = self.add_label(grid, - _('Encode video to another format, if necessary'), - 0, 5, 1, 1, - ) - - combo_list3 = ['', 'avi', 'flv', 'mkv', 'mp4', 'ogg', 'webm'] - combo3 = self.add_combo(grid, - combo_list3, - 'recode_video', - 1, 5, 1, 1, - ) - self.add_tooltip('--recode-video FORMAT', label3, combo3) - - label4 = self.add_label(grid, - _('Arguments to pass to post-processor'), - 0, 6, 1, 1, - ) - - entry = self.add_entry(grid, - 'pp_args', - 1, 6, 1, 1, - ) - self.add_tooltip('--postprocessor-args ARGS', label4, entry) - - checkbutton4 = self.add_checkbutton(grid, - _('Keep original file after processing it'), - 'keep_video', - 0, 7, 1, 1, - ) - self.add_tooltip('-k, --keep-video', checkbutton4) - - # (This option can also be modified in the Post-process tab) - self.embed_checkbutton = self.add_checkbutton(grid, - _('Merge subtitles file with video (.mp4 only)'), - None, - 1, 7, 1, 1, - ) - self.embed_checkbutton.set_active(self.retrieve_val('embed_subs')) - self.embed_checkbutton.connect( - 'toggled', - self.on_embed_checkbutton_toggled, - ) - self.add_tooltip('--embed-subs', self.embed_checkbutton) - - checkbutton5 = self.add_checkbutton(grid, - _('Embed thumbnail in audio file as cover art'), - 'embed_thumbnail', - 0, 8, 1, 1, - ) - self.add_tooltip('--embed-thumbnail', checkbutton5) - - checkbutton6 = self.add_checkbutton(grid, - _('Write metadata to the video file'), - 'add_metadata', - 1, 8, 1, 1, - ) - self.add_tooltip('--add-metadata', checkbutton6) - - label5 = self.add_label(grid, - _('Automatically correct known faults of the file'), - 0, 9, 1, 1, - ) - - combo_list4 = [ - ['', ''], - [_('Do nothing'), 'never'], - [_('Warn, but do nothing'), 'warn'], - [_('Fix if possible, otherwise warn'), 'detect_or_warn'], - ] - combo4 = self.add_combo_with_data(grid, - combo_list4, - 'fixup_policy', - 1, 9, 1, 1, - ) - self.add_tooltip('--fixup POLICY', combo4) - - - def setup_post_process_ytdlp_tab(self, inner_notebook): - - """Called by self.setup_post_process_tab(). - - Sets up the 'yt-dlp' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Post-processing > yt-dlp' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_yt-dlp'), - inner_notebook, - ) - grid_width = 2 - - # Post-processing options (yt-dlp only) - self.add_label(grid, - '' + _('Post-processing options') + '' + self.ytdlp_only(), - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Remux video into another container if necessary'), - 0, 1, 1, 1 - ) - - combo_list = [ - '', 'mp4', 'mkv', 'flv', 'webm', 'mov', 'avi', - 'mp3', 'mka', 'm4a', 'ogg', 'opus', - ] - - combo = self.add_combo(grid, - combo_list, - 'remux_video', - 1, 1, 1, 1 - ) - combo.set_hexpand(True) - self.add_tooltip('--remux-video FORMAT', label, combo) - - checkbutton = self.add_checkbutton(grid, - _( - 'Embed metadata including chapter markers (if supported by' \ - + ' format)', - ), - 'embed_metadata', - 0, 2, grid_width, 1, - ) - self.add_tooltip('--embed-metadata', checkbutton) - - label2 = self.add_label(grid, - _('Convert thumbnails to another format'), - 0, 3, 1, 1 - ) - - combo_list2 = ['', 'jpg', 'png'] - combo2 = self.add_combo(grid, - combo_list2, - 'convert_thumbnails', - 1, 3, 1, 1 - ) - combo2.set_hexpand(True) - self.add_tooltip('--convert-thumbnails FORMAT', label2, combo2) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Split video into multiple files based on internal chapters', - ), - 'split_chapters', - 0, 4, grid_width, 1, - ) - self.add_tooltip('--split-chapters', checkbutton2) - - self.add_label(grid, - '' + _( - 'N.B. The \'chapter\' prefix can be used in the \'Output\'' \ - + ' and \'Paths\' tabs', - ) + '', - 0, 5, grid_width, 1 - ) - - - def setup_subtitles_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Subtitles' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Subtitles' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Subtitles'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_subtitles_options_tab(inner_notebook) - self.setup_subtitles_more_options_tab(inner_notebook) - - - def setup_subtitles_options_tab(self, inner_notebook): - - """Called by self.setup_subtitles_tab(). - - Sets up the 'Options' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Subtitles > Options' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Options'), inner_notebook) - - # Subtitles options - self.add_label(grid, - '' + _('Subtitles options') + '', - 0, 0, 2, 1, - ) - - radiobutton = self.add_radiobutton(grid, - None, - _('Don\'t download the subtitles file'), - None, - None, - 0, 1, 2, 1, - ) - if self.retrieve_val('write_subs') is False: - radiobutton.set_active(True) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _('Download the automatic subtitles file (YouTube only)'), - None, - None, - 0, 2, 2, 1, - ) - if self.retrieve_val('write_subs') is True \ - and self.retrieve_val('write_auto_subs') is True: - radiobutton2.set_active(True) - # (Signal connect appears below) - self.add_tooltip('--write-sub, --write-auto-sub', radiobutton2) - - radiobutton3 = self.add_radiobutton(grid, - radiobutton2, - _('Download all available subtitle files'), - None, - None, - 0, 3, 2, 1, - ) - if self.retrieve_val('write_subs') is True \ - and self.retrieve_val('write_all_subs') is True: - radiobutton3.set_active(True) - # (Signal connect appears below) - self.add_tooltip('--write-sub, -all-subs', radiobutton3) - - radiobutton4 = self.add_radiobutton(grid, - radiobutton3, - _('Download subtitle file for these languages:'), - None, - None, - 0, 4, 2, 1, - ) - if self.retrieve_val('write_subs') is True \ - and self.retrieve_val('write_auto_subs') is False \ - and self.retrieve_val('write_all_subs') is False: - radiobutton4.set_active(True) - # (Signal connect appears below) - self.add_tooltip('--write-sub', radiobutton4) - - treeview, liststore = self.add_treeview(grid, - 0, 5, 1, 1, - ) - - for language in formats.LANGUAGE_CODE_LIST: - liststore.append([ - language + ' [' + formats.LANGUAGE_CODE_DICT[language] + ']', - ]) - - # We need a reverse dictionary for quick lookup - rev_dict = {} - for key in formats.LANGUAGE_CODE_DICT: - val = formats.LANGUAGE_CODE_DICT[key] - rev_dict[val] = key - - button = Gtk.Button(_('Add language') + ' >>>') - grid.attach(button, 0, 6, 1, 1) - # (Signal connect appears below) - - treeview2, liststore2 = self.add_treeview(grid, - 1, 5, 1, 1, - ) - lang_list = self.retrieve_val('subs_lang_list') - # The option stores values from formats.LANGUAGE_CODE_DICT, e.g. 'en', - # 'live_chat'. Convert them to the corresponding values, e.g. - # 'English' - for lang_code in lang_list: - liststore2.append([ - rev_dict[lang_code] + ' [' + lang_code + ']', - ]) - - self.add_tooltip('--sub-lang LANGS', treeview, treeview2) - - button2 = Gtk.Button('<<< ' + _('Remove language')) - grid.attach(button2, 1, 6, 1, 1) - # (Signal connect appears below) - - # Desensitise the buttons, if the matching radiobutton isn't active - if not radiobutton4.get_active(): - button.set_sensitive(False) - button2.set_sensitive(False) - - # (Signal connects from above) - button.connect( - 'clicked', - self.on_subtitles_tab_add_clicked, - treeview, - liststore2, - rev_dict, - ) - button2.connect( - 'clicked', - self.on_subtitles_tab_remove_clicked, - treeview2, - liststore2, - rev_dict, - ) - radiobutton.connect( - 'toggled', - self.on_subtitles_toggled, - button, button2, - 'write_subs', - ) - radiobutton2.connect( - 'toggled', - self.on_subtitles_toggled, - button, button2, - 'write_auto_subs', - ) - radiobutton3.connect( - 'toggled', - self.on_subtitles_toggled, - button, button2, - 'write_all_subs', - ) - radiobutton4.connect( - 'toggled', - self.on_subtitles_toggled, - button, button2, - 'subs_lang', - ) - - - def setup_subtitles_more_options_tab(self, inner_notebook): - - """Called by self.setup_subtitles_tab(). - - Sets up the 'More options' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Subtitles > More options' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_More options'), - inner_notebook, - ) - - # Subtitle format options - self.add_label(grid, - '' + _('Subtitle format options') + '', - 0, 0, 1, 1, - ) - - label = self.add_label(grid, - _( - 'Preferred subtitle format(s), e.g. \'srt\', \'vtt\',' \ - + ' \'srt/ass/vtt/lrc/best\'', - ), - 0, 1, 1, 1, - ) - - entry = self.add_entry(grid, - 'subs_format', - 0, 2, 1, 1, - ) - self.add_tooltip('--sub-format FORMAT', label, entry) - - # Post-processing options - self.add_label(grid, - '' + _('Post-processing options') + '', - 0, 3, 1, 1, - ) - - self.add_label(grid, - '' + _('Applies to .mp4 videos only; requires FFmpeg/AVConv') \ - + '', - 0, 4, 1, 1, - ) - - # (This option can also be modified in the Post-process tab) - self.embed_checkbutton2 = self.add_checkbutton(grid, - _('During post-processing, merge subtitles file with video'), - None, - 0, 5, 1, 1, - ) - self.embed_checkbutton2.set_active(self.retrieve_val('embed_subs')) - self.embed_checkbutton2.connect( - 'toggled', - self.on_embed_checkbutton_toggled, - ) - self.add_tooltip('--embed-subs', self.embed_checkbutton2) - - - def setup_advanced_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Advanced' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Advanced' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Advanced'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_advanced_configuration_tab(inner_notebook) - self.setup_advanced_authentication_tab(inner_notebook) - self.setup_advanced_netrc_tab(inner_notebook) - self.setup_advanced_network_tab(inner_notebook) - self.setup_advanced_georestrict_tab(inner_notebook) - self.setup_advanced_workaround_tab(inner_notebook) - - - def setup_advanced_configuration_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Configuration' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Advanced > Configurations' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Configurations'), - inner_notebook, - ) - grid_width = 3 - - # Configuration file options - self.add_label(grid, - '' + _('Configuration file options') + '', - 0, 0, grid_width, 1, - ) - - self.add_checkbutton(grid, - _('Use the downloader\'s configuration file'), - 'downloader_config', - 0, 1, grid_width, 1, - ) - - textview, textbuffer = self.add_textview(grid, - None, - 0, 2, grid_width, 1, - ) - - label = self.add_label(grid, - _('File loaded from:'), - 0, 3, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - None, - 1, 3, 1, 1, - ) - entry.set_hexpand(True) - - msg = _('Save file') - button = Gtk.Button(msg) - grid.attach(button, 2, 3, 1, 1) - button.get_child().set_width_chars(len(msg) + 6) - # (Signal connect appears below) - - # (If the downloader's configuration file exists, load it and update - # the textview/entry) - dl_config_path = utils.get_dl_config_path(self.app_obj) - if os.path.isfile(dl_config_path): - - line_list = [] - - try: - with open(dl_config_path) as fh: - line_list = fh.readlines() - - except: - pass - - textbuffer.set_text(str.join('', line_list)) - entry.set_text(dl_config_path) - - # (Signal connect from above) - button.connect( - 'clicked', - self.on_dl_config_button_clicked, - entry, - textbuffer, - dl_config_path, - ) - - - def setup_advanced_authentication_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Authentication' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Advanced > Authentication' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Authentication'), - inner_notebook, - ) - grid_width = 2 - - # Authentication options - self.add_label(grid, - '' + _('Authentication options') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Username with which to log in'), - 0, 1, 1, 1, - ) - - entry = self.add_entry(grid, - 'username', - 1, 1, 1, 1, - ) - self.add_tooltip('-u, --username USERNAME', label, entry) - - label2 = self.add_label(grid, - _('Password with which to log in'), - 0, 2, 1, 1, - ) - - entry2 = self.add_entry(grid, - 'password', - 1, 2, 1, 1, - ) - self.add_tooltip('-p, --password PASSWORD', label2, entry2) - - label3 = self.add_label(grid, - _('Password required for this URL'), - 0, 3, 1, 1, - ) - - entry3 = self.add_entry(grid, - 'video_password', - 1, 3, 1, 1, - ) - self.add_tooltip('--video-password PASSWORD', label3, entry3) - - label4 = self.add_label(grid, - _('Two-factor authentication code'), - 0, 4, 1, 1, - ) - - entry4 = self.add_entry(grid, - 'two_factor', - 1, 4, 1, 1, - ) - self.add_tooltip('-2, --twofactor TWOFACTOR', label4, entry4) - - label5 = self.add_label(grid, - _( - 'Adobe Pass multiple-system operator (TV provider)' \ - + ' identifier', - ), - 0, 5, 1, 1 - ) - - entry5 = self.add_entry(grid, - 'ap_mso', - 1, 5, 1, 1, - ) - self.add_tooltip('--ap-mso MSO', label5, entry5) - - label6 = self.add_label(grid, - _(' Adobe Pass multiple-system operator account login'), - 0, 6, 1, 1 - ) - - entry6 = self.add_entry(grid, - 'ap_username', - 1, 6, 1, 1, - ) - self.add_tooltip('--ap-username USERNAME', label6, entry6) - - label7 = self.add_label(grid, - _('Adobe Pass multiple-system operator account password'), - 0, 7, 1, 1 - ) - - entry7 = self.add_entry(grid, - 'ap_password', - 1, 7, 1, 1, - ) - self.add_tooltip('--ap-password PASSWORD', label7, entry7) - - - def setup_advanced_netrc_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the '.netrc' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Advanced > .netrc' - ) - - tab, grid = self.add_inner_notebook_tab( - _('._netrc'), - inner_notebook, - ) - grid_width = 3 - - # .netrc options - self.add_label(grid, - '' + _('.netrc options') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Use .netrc authentication data'), - 'net_rc', - 0, 1, grid_width, 1, - ) - self.add_tooltip('-n, --netrc', checkbutton) - - textview, textbuffer = self.add_textview(grid, - None, - 0, 2, grid_width, 1, - ) - - label = self.add_label(grid, - _('File loaded from:'), - 0, 3, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - None, - 1, 3, 1, 1, - ) - entry.set_hexpand(True) - - msg = _('Save file') - button = Gtk.Button(msg) - grid.attach(button, 2, 3, 1, 1) - button.get_child().set_width_chars(len(msg) + 6) - # (Signal connect appears below) - - # (If the .netrc file exists, load it and update the textview/entry) - netrc_path = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - '.netrc', - ), - ) - if os.path.isfile(netrc_path): - - line_list = [] - - try: - with open(netrc_path) as fh: - line_list = fh.readlines() - - except: - pass - - textbuffer.set_text(str.join('', line_list)) - entry.set_text(netrc_path) - - # (Signal connect from above) - button.connect( - 'clicked', - self.on_netrc_button_clicked, - entry, - textbuffer, - netrc_path, - ) - - - def setup_advanced_network_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Network' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Advanced > Network' - ) - - tab, grid = self.add_inner_notebook_tab(_('N_etwork'), inner_notebook) - grid_width = 2 - - # Network options - self.add_label(grid, - '' + _('Network options') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _( - 'Use this HTTP/HTTPS proxy (if set, overrides the proxies in' \ - + ' Tartube\'s preferences window', - ), - 0, 1, grid_width, 1, - ) - - entry = self.add_entry(grid, - 'proxy', - 0, 2, grid_width, 1, - ) - self.add_tooltip('--proxy URL', label, entry) - - label2 = self.add_label(grid, - _('Time to wait for socket connection, before giving up'), - 0, 3, 1, 1, - ) - - entry2 = self.add_entry(grid, - 'socket_timeout', - 1, 3, 1, 1, - ) - self.add_tooltip('--socket-timeout SECONDS', label2, entry2) - - label3 = self.add_label(grid, - _('Bind with this Client-side IP address'), - 0, 4, 1, 1, - ) - - entry3 = self.add_entry(grid, - 'source_address', - 1, 4, 1, 1, - ) - self.add_tooltip('--source-address IP', label3, entry3) - - checkbutton = self.add_checkbutton(grid, - _('Connect using IPv4 only'), - 'force_ipv4', - 0, 5, grid_width, 1, - ) - self.add_tooltip('-4, --force-ipv4', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Connect using IPv6 only'), - 'force_ipv6', - 0, 6, grid_width, 1, - ) - self.add_tooltip('-6, --force-ipv6', checkbutton2) - - - def setup_advanced_georestrict_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Geo-restriction' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Advanced > Geo-restriction' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Geo-restriction'), - inner_notebook, - ) - grid_width = 2 - - # Geo-restriction options - self.add_label(grid, - '' + _('Geo-restriction options') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Use this proxy to verify IP address'), - 0, 1, 1, 1, - ) - - entry = self.add_entry(grid, - 'geo_verification_proxy', - 1, 1, 1, 1, - ) - self.add_tooltip('--geo-verification-proxy URL', label, entry) - - checkbutton = self.add_checkbutton(grid, - _('Bypass using fake X-Forwarded-For HTTP header'), - 'geo_bypass', - 0, 2, 1, 1, - ) - self.add_tooltip('--geo-bypass', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Don\'t bypass using fake HTTP header'), - 'no_geo_bypass', - 1, 2, 1, 1, - ) - self.add_tooltip('--no-geo-bypass', checkbutton2) - - label2 = self.add_label(grid, - _('Bypass geo-restriction with ISO 3166-2 country code'), - 0, 3, 1, 1, - ) - - entry2 = self.add_entry(grid, - 'geo_bypass_country', - 1, 3, 1, 1, - ) - self.add_tooltip('--geo-bypass-country CODE', label2, entry2) - - label3 = self.add_label(grid, - _('Bypass with explicit IP block in CIDR notation'), - 0, 4, 1, 1, - ) - - entry3 = self.add_entry(grid, - 'geo_bypass_ip_block', - 1, 4, 1, 1, - ) - self.add_tooltip('--geo-bypass-ip-block IP_BLOCK', label3, entry3) - - - def setup_advanced_workaround_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Workaround' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Advanced > Workaround' - ) - - tab, grid = self.add_inner_notebook_tab('_Workaround', inner_notebook) - grid_width = 2 - - # Workaround options - self.add_label(grid, - '' + _('Workaround options') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Custom user agent'), - 0, 1, 1, 1, - ) - - entry = self.add_entry(grid, - 'user_agent', - 1, 1, 1, 1, - ) - self.add_tooltip('--user-agent UA', label, entry) - - label2 = self.add_label(grid, - _('Custom referer if video access has restricted domain'), - 0, 2, 1, 1, - ) - - entry2 = self.add_entry(grid, - 'referer', - 1, 2, 1, 1, - ) - self.add_tooltip('--referer URL', label2, entry2) - - label3 = self.add_label(grid, - _('Minimum seconds to sleep before each download'), - 0, 3, 1, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 0, 3600, 1, - None, - 1, 3, 1, 1 - ) - # (Signal connect appears below) - self.add_tooltip('--sleep-interval SECONDS', label3, spinbutton) - - label4 = self.add_label(grid, - _('Maximum seconds to sleep before each download'), - 0, 4, 1, 1, - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, 3600, 1, - 'max_sleep_interval', - 1, 4, 1, 1 - ) - if self.edit_obj.options_dict['min_sleep_interval'] == 0: - spinbutton2.set_sensitive(False) - self.add_tooltip('--max-sleep-interval SECONDS', label4, spinbutton2) - - # (Signal connect from above) - spinbutton.connect( - 'value-changed', - self.on_sleep_button_changed, - spinbutton2, - ) - - label5 = self.add_label(grid, - _('Force this encoding (experimental)'), - 0, 5, 1, 1, - ) - - entry3 = self.add_entry(grid, - 'force_encoding', - 1, 5, 1, 1, - ) - self.add_tooltip('--encoding ENCODING', label5, entry3) - - checkbutton = self.add_checkbutton(grid, - _('Suppress HTTPS certificate validation'), - 'no_check_certificate', - 0, 6, grid_width, 1, - ) - self.add_tooltip('--no-check-certificate', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Use an unencrypted connection to retrieve information about' \ - + ' videos (YouTube only)', - ), - 'prefer_insecure', - 0, 7, grid_width, 1, - ) - self.add_tooltip('--prefer-insecure', checkbutton2) - - # Workaround options (yt-dlp only) - self.add_label(grid, - '' + _('Workaround options') + '' + self.ytdlp_only(), - 0, 8, grid_width, 1, - ) - - label = self.add_label(grid, - _( - 'Number of seconds to sleep between requests during data' \ - + ' extraction', - ), - 0, 9, 1, 1 - ) - - spinbutton = self.add_spinbutton(grid, - 0, None, 1, - 'sleep_requests', - 1, 9, 1, 1 - ) - self.add_tooltip('--sleep-requests SECONDS', label, spinbutton) - - label2 = self.add_label(grid, - _( - 'Number of seconds to sleep before each download (or minimum' \ - + ' time)', - ), - 0, 10, 1, 1 - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, None, 1, - 'sleep_subtitles', - 1, 10, 1, 1 - ) - self.add_tooltip('--sleep-subtitles SECONDS', label2, spinbutton2) - - - # (Tab support functions - general) - - - def file_tab_sensitise_widgets(self, flag): - - """Called by self.setup_files_names_tab() and - self.on_file_tab_combo_changed(). - - Sensitises or desensitises a list of widgets in response to the user's - interactions with widgets on that tab. - - Also resets comboboxes to show their first items. - - Args: - - flag (bool): True to sensitise the widgets, False to desensitise - them - - """ - - self.template_flag = flag - for widget in self.template_widget_list: - - widget.set_sensitive(flag) - - # All combos in this tab (except for the one in the top-left - # corner, which is not in self.template_widget_list) must be - # reset to show their first item - if isinstance(widget, Gtk.ComboBox): - widget.set_active(0) - - - def file_tab_update_time_format(self, value, entry, button): - - """Called by self.on_date_time_combo_changed() and - .on_file_tab_reset_button_clicked(). - - Updates the contents of the 'Time format' entry box, and (de)sensitises - widgets. - - Args: - - value (str): A component used in youtube-dl's output template - - entry (Gtk.Entry): A widget to update - - button (Gtk.Button): Another widget to update - - """ - - if value == 'release_date_custom' \ - or value == 'upload_date_custom': - - entry.set_text('%Y-%m-%d') - entry.set_sensitive(True) - button.set_sensitive(True) - - elif value == 'timestamp_custom' \ - or value == 'duration_custom': - - entry.set_text('%H-%M-%S') - entry.set_sensitive(True) - button.set_sensitive(True) - - else: - - entry.set_text('') - entry.set_sensitive(False) - button.set_sensitive(False) - - - def formats_tab_count_formats(self): - - """Called by several parts of self.setup_formats_tab(). - - Counts the number of video/audio formats that are set. - - Return values: - - An integer in the range 0-3 - - """ - - format_list = self.retrieve_val('video_format_list') - - return len(format_list) - - - def formats_tab_redraw_list(self): - - """Called by self.setup_formats_tab() and then again by - self.apply_changes(). - - Update the Gtk.ListStore containing the user's preferred video/audio - formats. - """ - - # Empty the treeview - self.formats_liststore.clear() - - # (Need to reverse formats.VIDEO_OPTION_DICT for quick lookup) - rev_dict = {} - for key in formats.VIDEO_OPTION_DICT: - rev_dict[formats.VIDEO_OPTION_DICT[key]] = key - - # Refill the treeview - format_list = self.retrieve_val('video_format_list') - for item in format_list: - self.formats_liststore.append([rev_dict[item]]) - - - # (Tab support functions - Downloads tab) - - - def downloads_age_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Limits' - ) - - # Video age options - self.add_label(grid, - '' + _('Video age options') + '', - 0, row_count, 1, 1, - ) - - label = self.add_label(grid, - _('Download videos suitable for this age'), - 0, (row_count + 1), 1, 1, - ) - - entry = self.add_entry(grid, - 'age_limit', - 1, (row_count + 1), 1, 1, - ) - self.add_tooltip('--age-limit YEARS', label, entry) - - return row_count + 2 - - - def downloads_date_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Limits' - ) - - grid_width = 3 - - # Video date options - self.add_label(grid, - '' + _('Video date options') + '', - 0, row_count, grid_width, 1, - ) - - label = self.add_label(grid, - _('Only videos uploaded on this date'), - 0, (row_count + 1), (grid_width - 2), 1, - ) - - entry = self.add_entry(grid, - 'date', - (grid_width - 2), (row_count + 1), 1, 1, - ) - entry.set_editable(False) - - button = Gtk.Button(_('Set')) - grid.attach(button, (grid_width - 1), (row_count + 1), 1, 1) - button.connect( - 'clicked', - self.on_button_set_date_clicked, - entry, - 'date', - ) - self.add_tooltip('--date DATE', label, entry, button) - - label2 = self.add_label(grid, - _('Only videos uploaded before this date'), - 0, (row_count + 2), (grid_width - 2), 1, - ) - - entry2 = self.add_entry(grid, - 'date_before', - (grid_width - 2), (row_count + 2), 1, 1, - ) - entry2.set_editable(False) - - button2 = Gtk.Button(_('Set')) - grid.attach(button2, (grid_width - 1), (row_count + 2), 1, 1) - button2.connect( - 'clicked', - self.on_button_set_date_clicked, - entry2, - 'date_before', - ) - self.add_tooltip('--datebefore DATE', label2, entry2, button2) - - label3 = self.add_label(grid, - _('Only videos uploaded after this date'), - 0, (row_count + 3), (grid_width - 2), 1, - ) - - entry3 = self.add_entry(grid, - 'date_after', - (grid_width - 2), (row_count + 3), 1, 1, - ) - entry3.set_editable(False) - - button3 = Gtk.Button(_('Set')) - grid.attach(button3, (grid_width - 1), (row_count + 3), 1, 1) - button3.connect( - 'clicked', - self.on_button_set_date_clicked, - entry3, - 'date_after', - ) - self.add_tooltip('--dateafter DATE', label3, entry3, button3) - - return row_count + 4 - - - def downloads_external_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > External' - ) - - grid_width = 2 - - # External downloader options - self.add_label(grid, - '' + _('External downloader options') + '', - 0, row_count, grid_width, 1, - ) - - label = self.add_label(grid, - _('Use this external downloader'), - 0, (row_count + 1), 1, 1, - ) - - ext_list = [ - '', 'aria2c', 'avconv', 'axel', 'curl', 'ffmpeg', 'httpie', - 'wget', - ] - - combo = self.add_combo(grid, - ext_list, - 'external_downloader', - 1, (row_count + 1), 1, 1, - ) - combo.set_hexpand(True) - self.add_tooltip('--external-downloader COMMAND', label, combo) - - label2 = self.add_label(grid, - _('Arguments to pass to external downloader'), - 0, (row_count + 2), grid_width, 1, - ) - - entry = self.add_entry(grid, - 'external_arg_string', - 0, (row_count + 3), grid_width, 1, - ) - self.add_tooltip('--external-downloader-args ARGS', label2, entry) - - return row_count + 4 - - - def downloads_filtering_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Filtering' - ) - - grid_width = 3 - - # Video filtering options - self.add_label(grid, - '' + _('Video filtering options') + '', - 0, row_count, grid_width, 1, - ) - - label = self.add_label(grid, - _('Download only matching titles (regex or caseless substring)'), - 0, (row_count + 1), grid_width, 1, - ) - - textview, textbuffer = self.add_textview(grid, - 'match_title_list', - 0, (row_count + 2), grid_width, 1, - ) - self.add_tooltip('--match-title REGEX', label, textview) - - label2 = self.add_label(grid, - _( - 'Don\'t download only matching titles (regex or caseless' \ - + ' substring)', - ), - 0, (row_count + 3), grid_width, 1, - ) - - textview2, textbuffer2 = self.add_textview(grid, - 'reject_title_list', - 0, (row_count + 4), grid_width, 1, - ) - self.add_tooltip('--reject-title REGEX', label2, textview2) - - label3 = self.add_label(grid, - _('Generic video filter, for example:') + ' like_count > 100', - 0, (row_count + 5), grid_width, 1, - ) - - entry = self.add_entry(grid, - 'match_filter', - 0, (row_count + 6), grid_width, 1, - ) - self.add_tooltip('--match-filter FILTER', label3, entry) - - return row_count + 7 - - - def downloads_general_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > General' - ) - - grid_width = 3 - - checkbutton = self.add_checkbutton(grid, - _('Prefer HLS (HTTP Live Streaming)'), - 'native_hls', - 0, row_count, grid_width, 1, - ) - self.add_tooltip('--hls-prefer-native', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Prefer FFMpeg over native HLS downloader'), - 'hls_prefer_ffmpeg', - 0, (row_count + 1), grid_width, 1, - ) - self.add_tooltip('--hls-prefer-ffmpeg', checkbutton2) - - checkbutton3 = self.add_checkbutton(grid, - _('Include advertisements (experimental feature)'), - 'include_ads', - 0, (row_count + 2), grid_width, 1, - ) - self.add_tooltip('--include-ads', checkbutton3) - - checkbutton4 = self.add_checkbutton(grid, - _('Ignore errors and continue the download operation'), - 'ignore_errors', - 0, (row_count + 3), grid_width, 1, - ) - self.add_tooltip('-i, --ignore-errors', checkbutton4) - - checkbutton5 = self.add_checkbutton(grid, - _('Abort video download if fragments are unavailable'), - 'abort_on_unavailable_fragment', - 0, (row_count + 4), grid_width, 1, - ) - self.add_tooltip('--abort-on-unavailable-fragment', checkbutton5) - - label = self.add_label(grid, - _('Number of retries'), - 0, (row_count + 5), 1, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 1, 99, 1, - 'retries', - 1, (row_count + 5), 1, 1, - ) - self.add_tooltip('-R, --retries RETRIES', label, spinbutton) - - return row_count + 6 - - - def downloads_playlist_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Playlists' - ) - - grid_width = 2 - - # Playlist options - self.add_label(grid, - '' + _('Playlist options') + '', - 0, row_count, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'Channels and playlists are handled in the same way, so' \ - + ' these options can be used with both', - ) + '', - 0, (row_count + 1), grid_width, 1, - ) - - label = self.add_label(grid, - _('Start downloading playlist from index'), - 0, (row_count + 2), 1, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 1, None, 1, - 'playlist_start', - 1, (row_count + 2), 1, 1, - ) - self.add_tooltip('--playlist-start NUMBER', label, spinbutton) - - label2 = self.add_label(grid, - _('Stop downloading playlist at index'), - 0, (row_count + 3), 1, 1, - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, None, 1, - 'playlist_end', - 1, (row_count + 3), 1, 1, - ) - self.add_tooltip('--playlist-end NUMBER', label2, spinbutton2) - - label3 = self.add_label(grid, - _('Download playlist range, in form START:STOP:STEP'), - 0, (row_count + 4), 1, 1, - ) - - entry = self.add_entry(grid, - 'playlist_items', - 1, (row_count + 4), 1, 1, - ) - entry.set_hexpand(True) - self.add_tooltip('--playlist-items ITEM_SPEC', label3, entry) - - label4 = self.add_label(grid, - _('Abort operation after downloading this many videos'), - 0, (row_count + 5), 1, 1, - ) - - spinbutton3 = self.add_spinbutton(grid, - 0, None, 1, - 'max_downloads', - 1, (row_count + 5), 1, 1, - ) - self.add_tooltip('--max-downloads NUMBER', label4, spinbutton3) - - checkbutton = self.add_checkbutton(grid, - _('Abort downloading the playlist if an error occurs'), - 'abort_on_error', - 0, (row_count + 6), grid_width, 1, - ) - self.add_tooltip('--abort-on-error', checkbutton) - - checkbutton2 = self.add_checkbutton(grid, - _('Download playlist in reverse order'), - 'playlist_reverse', - 0, (row_count + 7), grid_width, 1, - ) - self.add_tooltip('--playlist-reverse', checkbutton2) - - checkbutton3 = self.add_checkbutton(grid, - _('Download playlist in random order'), - 'playlist_random', - 0, (row_count + 8), grid_width, 1, - ) - self.add_tooltip('--playlist-random', checkbutton3) - - return row_count + 8 - - - def downloads_size_limit_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Limits' - ) - - grid_width = 3 - - # Video size limit options - self.add_label(grid, - '' + _('Video size limit options') + '', - 0, row_count, grid_width, 1, - ) - - label = self.add_label(grid, - _('Minimum file size for video downloads'), - 0, (row_count + 1), (grid_width - 2), 1, - ) - - spinbutton = self.add_spinbutton(grid, - 0, None, 1, - 'min_filesize', - (grid_width - 2), (row_count + 1), 1, 1, - ) - - combo = self.add_combo_with_data(grid, - formats.FILE_SIZE_UNIT_LIST, - 'min_filesize_unit', - (grid_width - 1), (row_count + 1), 1, 1, - ) - self.add_tooltip('--min-filesize SIZE', label, spinbutton, combo) - - label2 = self.add_label(grid, - _('Maximum file size for video downloads'), - 0, (row_count + 2), (grid_width - 2), 1, - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, None, 1, - 'max_filesize', - (grid_width - 2), (row_count + 2), 1, 1, - ) - - combo2 = self.add_combo_with_data(grid, - formats.FILE_SIZE_UNIT_LIST, - 'max_filesize_unit', - (grid_width - 1), (row_count + 2), 1, 1, - ) - self.add_tooltip('--max-filesize SIZE', label2, spinbutton2, combo2) - - return row_count + 3 - - - def downloads_views_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download options > Downloads > Limits' - ) - - grid_width = 3 - - # Video views options - self.add_label(grid, - '' + _('Video views options') + '', - 0, row_count, grid_width, 1, - ) - - label = self.add_label(grid, - _('Minimum number of views'), - 0, (row_count + 1), (grid_width - 2), 1, - ) - - spinbutton = self.add_spinbutton(grid, - 0, None, 1, - 'min_views', - (grid_width - 2), (row_count + 1), 1, 1, - ) - self.add_tooltip('--min-views COUNT', label, spinbutton) - - label2 = self.add_label(grid, - _('Maximum number of views'), - 0, (row_count + 2), (grid_width - 2), 1, - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, None, 1, - 'max_views', - (grid_width - 2), (row_count + 2), 1, 1, - ) - self.add_tooltip('--max-views COUNT', label2, spinbutton2) - - # (This improves layout a little) - if not self.app_obj.simple_options_flag: - spinbutton.set_hexpand(True) - spinbutton2.set_hexpand(True) - - return row_count + 3 - - - # Callback class methods - - - def on_button_set_date_clicked(self, button, entry, prop): - - """Called by callback in self.downloads_date_widgets(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - prop (str): The attribute in self.edit_dict to modify - - """ - - # Prompt the user for a new calendar date - dialogue_win = mainwin.CalendarDialogue( - self, - self.retrieve_val(prop), - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - if response == Gtk.ResponseType.OK: - date_tuple = dialogue_win.calendar.get_date() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and date_tuple: - - year = str(date_tuple[0]) # e.g. 2011 - month = str(date_tuple[1] + 1) # Values in range 0-11 - day = str(date_tuple[2]) # Values in range 1-31 - - entry.set_text( - year.zfill(4) + month.zfill(2) + day.zfill(2) - ) - - else: - - entry.set_text('') - - - def on_clone_options_clicked(self, button): - - """Called by callback in self.setup_name_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Name' - ) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'This procedure cannot be reversed. Are you sure you want to' \ - + ' continue?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'clone_download_options_from_window', - 'data': [self, self.edit_obj], - }, - ) - - - def on_cookies_set_button_clicked(self, button, entry): - - """Called by callback in self.setup_files_cookies_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Files > Cookies' - ) - - # Prompt the user for a new file - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Select the cookie jar file'), - self, - 'open', - ) - - cookie_path = self.retrieve_val('cookies_path') - - if cookie_path == '': - cookie_dir = self.app_obj.data_dir - else: - cookie_dir, cookie_file = os.path.split(cookie_path) - - dialogue_win.set_current_folder(cookie_dir) - - # Get the user's response - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - if response == Gtk.ResponseType.OK: - - self.edit_dict['cookies_path'] = new_path - entry.set_text(new_path) - - - def on_cookies_reset_button_clicked(self, button, entry): - - """Called by callback in self.setup_files_cookies_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - self.edit_dict['cookies_path'] = '' - entry.set_text( - os.path.abspath( - os.path.join( - self.app_obj.data_dir, - self.app_obj.cookie_file_name, - ), - ), - ) - - - def on_cookies_ytdlp_entry_changed(self, widget, entry, combo, \ - combo2, entry2, entry3): - - """Called by callback in self.setup_files_cookies_tab(). - - Args: - - widget (Gtk.Entry): The widget modified (ignored) - - entry (Gtk.Entry): The entry box displaying the download option - - combo, combo2, entry2, entry3 (Gtk.Combo, Gtk.Entry): Widgets whose - settings are combined to set the 'cookies_from_browser' - download option - - """ - - # (The other entry also specifies the profile; at least one of them - # must be empty) - if entry2.get_text() != '': - entry3.set_text('') - - # Update the download option, and display it in 'entry' - self.setup_files_cookies_tab_update( - None, - entry, - combo, - combo2, - entry2, - entry3, - ) - - - def on_cookies_ytdlp_reset_button_clicked(self, button, entry, combo, \ - combo2, entry2, entry3): - - """Called by callback in self.setup_files_cookies_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): The entry box displaying the download option - - combo, combo2, entry2, entry3 (Gtk.Combo, Gtk.Entry): Widgets whose - settings are combined to set the 'cookies_from_browser' - download option - - """ - - entry3.set_text('') - - # Update the download option, and display it in 'entry' - self.setup_files_cookies_tab_update( - None, - entry, - combo, - combo2, - entry2, - entry3, - ) - - - def on_cookies_ytdlp_set_button_clicked(self, button, entry, combo, \ - combo2, entry2, entry3): - - """Called by callback in self.setup_files_cookies_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): The entry box displaying the download option - - combo, combo2, entry2, entry3 (Gtk.Combo, Gtk.Entry): Widgets whose - settings are combined to set the 'cookies_from_browser' - download option - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Files > Cookies' - ) - - # Prompt the user for a new file - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Select the browser profile'), - self, - 'open', - ) - - # Get the user's response - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - if response == Gtk.ResponseType.OK: - entry3.set_text(new_path) - - # (The other entry also specifies the profile; at least one of them - # must be empty) - if new_path != '': - entry2.set_text('') - - # Update the download option, and display it in 'entry' - self.setup_files_cookies_tab_update( - None, - entry, - combo, - combo2, - entry2, - entry3, - ) - - - def on_date_time_combo_changed(self, combo, entry, button, trans_dict): - - """Called by callback in self.setup_files_filesystem_tab(). - - The 'Date/time/location' combo sets or resets the entry just beneath - it, every time the user selects a new combo item. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - entry (Gtk.Entry): Another widget to update - - button (Gtk.Button): Another widget to update - - trans_dict (dict): Converts a translated string into the string - used by youtube-dl - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - value = trans_dict[model[tree_iter][0]] - - self.file_tab_update_time_format(value, entry, button) - - - def on_direct_cmd_toggled(self, checkbutton, checkbutton2): - - """Called by callback in self.setup_name_tab(). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to modify - - """ - - if not checkbutton.get_active(): - self.edit_dict['direct_cmd_flag'] = False - checkbutton2.set_active(False) - checkbutton2.set_sensitive(False) - - else: - - self.edit_dict['direct_cmd_flag'] = True - checkbutton2.set_sensitive(True) - - - def on_dl_config_button_clicked(self, button, entry, textbuffer, \ - dl_config_path): - - """Called by callback in self.setup_advanced_configuration_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to modify - - textbuffer (Gtk.TextBuffer): Buffer for the textview containing - the text to be saved - - dl_config_path (str): FUll path to the downloader's configuration - file - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Advanced > Configurations' - ) - - # Save the file - try: - - dir_path = os.path.dirname(dl_config_path) - if not os.path.isdir(dir_path): - self.app_obj.make_directory(dir_path) - - fh = open(dl_config_path, 'w') - fh.write( - textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ), - ) - fh.close() - - except: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Could not save the downloader\'s configuration file'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - entry.set_text(dl_config_path) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Downloader\'s configuration file saved'), - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_embed_checkbutton_toggled(self, checkbutton): - - """Called by callback in self.setup_post_process_tab() or - setup_subtitles_more_options_tab(). - - The 'embed_subs' option appears in both the Formats and Subtitles tabs. - When one widget is modified, we need to set the other widgets to match - without starting an infinite loop of signal connects. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - prop = 'embed_subs' - - if checkbutton == self.embed_checkbutton2 \ - and self.embed_checkbutton is None: - - # An easy case; the Formats tab isn't visible, so there is only one - # widget to think about - if not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - else: - - # We get around the infinite loop problem by setting the other - # checkbutton, if it's in the opposite state to this checkbutton - flag = checkbutton.get_active() - - if checkbutton == self.embed_checkbutton: - - if self.embed_checkbutton2.get_active() != flag: - self.embed_checkbutton2.set_active(flag) - elif not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - else: - - if self.embed_checkbutton.get_active() != flag: - self.embed_checkbutton.set_active(flag) - elif not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - - def on_fixed_folder_changed(self, combo): - - """Called by callback in self.setup_files_filesystem_tab(). - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - dbid = model[tree_iter][1] - if dbid == self.app_obj.fixed_temp_folder.dbid: - self.edit_dict['use_fixed_folder'] = 'temp' - elif dbid == self.app_obj.fixed_misc_folder.dbid: - self.edit_dict['use_fixed_folder'] = 'misc' - elif dbid == self.app_obj.fixed_clips_folder.dbid: - self.edit_dict['use_fixed_folder'] = 'clips' - else: - # Failsafe - self.edit_dict['use_fixed_folder'] = None - - - def on_fixed_folder_toggled(self, checkbutton, combo): - - """Called by callback in self.setup_files_filesystem_tab(). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - combo (Gtk.ComboBox): Another widget to be modified by this - function - - """ - - if not checkbutton.get_active(): - self.edit_dict['use_fixed_folder'] = None - combo.set_sensitive(False) - - else: - - tree_iter = combo.get_active_iter() - model = combo.get_model() - dbid = model[tree_iter][1] - if dbid == self.app_obj.fixed_temp_folder.dbid: - self.edit_dict['use_fixed_folder'] = 'temp' - elif dbid == self.app_obj.fixed_misc_folder.dbid: - self.edit_dict['use_fixed_folder'] = 'misc' - elif dbid == self.app_obj.fixed_clips_folder.dbid: - self.edit_dict['use_fixed_folder'] = 'clips' - else: - # Failsafe - self.edit_dict['use_fixed_folder'] = None - - combo.set_sensitive(True) - - - def on_file_tab_add_button_clicked(self, button, entry, entry2, combo, \ - trans_dict): - - """Called by callback in self.setup_files_names_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2 (Gtk.Entry): Other widgets to be modified by this - function - - combo (Gtk.ComboBox): Another widget to be modified by this - function - - trans_dict (dict): Converts a translated string into the string - used by youtube-dl - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - value = trans_dict[model[tree_iter][0]] - - # The new component should be inserted at the end of the filename, and - # before the file extension (if possible) - output_template = file_name = self.retrieve_val('output_template') - file_ext = '' - if value != 'ext' and output_template: - - match = re.search(r'(.*)(\.\%\(ext\)s)\s*$', output_template) - if match: - - file_name = match.groups()[0] - file_ext = match.groups()[1] - - if not output_template or output_template[-1] == os.sep: - prefix = '' - elif value == 'ext': - prefix = '.' - else: - prefix = '-' - - # e.g. to generate '(upload_date>%Y-%m-%d)s' - time_format = entry2.get_text() - if time_format != '': - - if value == 'release_date_custom': - value = 'release_date>' + time_format - elif value == 'upload_date_custom': - value = 'upload_date>' + time_format - elif value == 'timestamp_custom': - value = 'timestamp>' + time_format - elif value == 'duration_custom': - value = 'duration>' + time_format - - if value == 'video_autonumber': - formatted = '{0}%({1})3d'.format(prefix, value) - else: - formatted = '{0}%({1})s'.format(prefix, value) - - # (Setting the entry updates self.edit_dict) - if value == 'ext': - entry.set_text(file_name + file_ext + formatted) - else: - entry.set_text(file_name + formatted + file_ext) - - - def on_file_tab_reset_button_clicked(self, button, entry, combo, \ - trans_dict): - - """Called by callback in self.setup_files_names_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - combo (Gtk.ComboBox): Another widget to be modified by this - function - - trans_dict (dict): Converts a translated string into the string - used by youtube-dl - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - value = trans_dict[model[tree_iter][0]] - - self.file_tab_update_time_format(value, entry, button) - - - def on_file_tab_main_combo_changed(self, combo, entry, entry2, button): - - """Called by callback in self.setup_files_names_tab(). - - Args: - - combo (Gtk.ComboBox): The widget clicked - - entry, entry2 (Gtk.Entry): Other widgets to be modified - - button (Gtk.Button): Another widget to be modified - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - row_id, name = model[tree_iter][:2] - - self.edit_dict['output_format'] = row_id - - # The custom template is associated with the index 0 - if row_id == 0: - - self.file_tab_sensitise_widgets(True) - # (The call to that function sensitises the 'Time format' widgets, - # but they must be desensitised when a custom format is not - # selected) - entry2.set_sensitive(False) - button.set_sensitive(False) - - entry.set_text(self.retrieve_val('output_template')) - - else: - - self.file_tab_sensitise_widgets(False) - entry.set_text(formats.FILE_OUTPUT_CONVERT_DICT[row_id]) - - - def on_file_tab_main_entry_changed(self, entry): - - """Called by callback in self.setup_files_names_tab(). - - Args: - - entry (Gtk.Entry): The widget clicked - - """ - - # Only set 'output_template' when option 3 is selected, which is when - # the entry is sensitised - if self.template_flag: - self.edit_dict['output_template'] = entry.get_text() - - - def on_formats_tab_add_clicked(self, add_button, remove_button, \ - up_button, down_button, treeview): - - """Called by callback in self.setup_formats_tab_add_grid(). - - Args: - - add_button (Gtk.Button): The widget clicked - - remove_button, up_button, down_button (Gtk.Button): Other widgets - to be modified by this function - - treeview (Gtk.TreeView): The treeview on the left side of the tab - - """ - - selection = treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - else: - - name = model[tree_iter][0] - # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18' - extract_code = formats.VIDEO_OPTION_DICT[name] - - # Update the option - format_list = self.retrieve_val('video_format_list') - if extract_code in format_list: - return - else: - format_list.append(extract_code) - self.edit_dict['video_format_list'] = format_list - - # Update the other treeview, adding the format to it (and don't modify - # this treeview) - self.formats_liststore.append([name]) - - # Update other widgets, as required - remove_button.set_sensitive(True) - up_button.set_sensitive(True) - down_button.set_sensitive(True) - - - def on_formats_tab_combo_changed(self, combo): - - """Called by callback in self.setup_formats_tab_add_grid(). - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Formats > Preferred' - ) - - tree_iter = combo.get_active_iter() - model = combo.get_model() - val = model[tree_iter][0] - - self.edit_dict['merge_output_format'] = val - - # For some reason, this youtube-dl download option doesn't work if the - # specified format (e.g. 'mp4') isn't also specified in the list of - # preferred formats - # Warn the user about that, where appropriate - format_list = self.retrieve_val('video_format_list') - if val != '' and not val in format_list: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'This option won\'t work unless the format is also added to' \ - + ' the list of preferred formats above', - ), - 'warning', - 'ok', - self, # Parent window is this window - ) - - - def on_formats_tab_down_clicked(self, down_button, treeview): - - """Called by callback in self.setup_formats_tab_add_grid(). - - Args: - - down_button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): Another widget to be modified by this - function - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - else: - - this_iter = model.get_iter(path_list[0]) - name = model[this_iter][0] - # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18' - extract_code = formats.VIDEO_OPTION_DICT[name] - - # Update the option - format_list = self.retrieve_val('video_format_list') - if extract_code in format_list: - - index = format_list.index(extract_code) - if index < (len(format_list) - 1): - format_list.remove(extract_code) - format_list.insert((index + 1), extract_code) - - self.edit_dict['video_format_list'] = format_list - - # Update the other treeview - this_path = path_list[0] - next_path = this_path[0]+1 - model.move_after( - model.get_iter(this_path), - model.get_iter(next_path), - ) - - - def on_formats_tab_remove_clicked(self, remove_button, add_button, \ - type_button, up_button, down_button, other_treeview): - - """Called by callback in self.setup_formats_tab_add_grid(). - - Args: - - remove_button (Gtk.Button): The widget clicked - - add_button, type_button, up_button, down_button (Gtk.Button): Other - widgets to be modified by this function - - other_treeview (Gtk.TreeView): The treeview on the right side of - the tab - - """ - - selection = other_treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - else: - - name = model[tree_iter][0] - # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18' - extract_code = formats.VIDEO_OPTION_DICT[name] - - # Update the option - format_list = self.retrieve_val('video_format_list') - if extract_code in format_list: - format_list.remove(extract_code) - - self.edit_dict['video_format_list'] = format_list - - # Update the right-hand side treeview - model.remove(tree_iter) - - # Update other widgets, as required - add_button.set_sensitive(True) - type_button.set_sensitive(True) - if not format_list: - - # No formats left to remove - remove_button.set_sensitive(False) - up_button.set_sensitive(False) - down_button.set_sensitive(False) - - - def on_formats_tab_type_clicked(self, type_button, remove_button, \ - up_button, down_button, treeview): - - """Called by callback in self.setup_formats_tab_add_grid(). - - Args: - - type_button (Gtk.Button): The widget clicked - - remove_button, up_button, down_button (Gtk.Button): Other widgets - to be modified by this function - - treeview (Gtk.TreeView): The treeview on the left side of the tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Formats > Preferred' - ) - - # Prompt the user to type an extractor code directly - dialogue_win = mainwin.ExtractorCodeDialogue(self) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - if response == Gtk.ResponseType.OK: - extract_code = dialogue_win.extract_code - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and extract_code is not None: - - if not extract_code in formats.VIDEO_OPTION_TYPE_DICT: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Unrecognised extractor code'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - else: - - # Update the option (code copied from - # self.on_formats_tab_add_clicked() above) - format_list = self.retrieve_val('video_format_list') - if extract_code in format_list: - return - else: - format_list.append(extract_code) - self.edit_dict['video_format_list'] = format_list - - # Update the other treeview, adding the format to it (and don't - # modify this treeview) - for name in formats.VIDEO_OPTION_DICT.keys(): - if formats.VIDEO_OPTION_DICT[name] == extract_code: - self.formats_liststore.append([name]) - - # Update other widgets, as required - remove_button.set_sensitive(True) - up_button.set_sensitive(True) - down_button.set_sensitive(True) - - break - - - def on_formats_tab_up_clicked(self, up_button, treeview): - - """Called by callback in self.setup_formats_tab_add_grid(). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): Another widget to be modified by this - function - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - else: - - this_iter = model.get_iter(path_list[0]) - name = model[this_iter][0] - # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18' - extract_code = formats.VIDEO_OPTION_DICT[name] - - # Update the option - format_list = self.retrieve_val('video_format_list') - if extract_code in format_list: - - index = format_list.index(extract_code) - if index > 0: - format_list.remove(extract_code) - format_list.insert((index - 1), extract_code) - - self.edit_dict['video_format_list'] = format_list - - # Update the other treeview - this_path = path_list[0] - prev_path = this_path[0]-1 - model.move_before( - model.get_iter(this_path), - model.get_iter(prev_path), - ) - - - def on_netrc_button_clicked(self, button, entry, textbuffer, netrc_path): - - """Called by callback in self.setup_advanced_netrc_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to modify - - textbuffer (Gtk.TextBuffer): Buffer for the textview containing - the text to be saved - - netrc_path (str): FUll path to the .netrc file - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Advanced > .netrc' - ) - - if not os.path.isfile(netrc_path): - new_flag = True - else: - new_flag = False - - # Save the file - try: - fh = open(netrc_path, 'w') - fh.write( - textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ), - ) - fh.close() - - except: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Could not save the .netrc file'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - if new_flag: - - # For a newly-created file, set read/write permissions, as - # described in the youtube-dl documentation - os.chmod(netrc_path, stat.S_IREAD | stat.S_IWRITE) - - entry.set_text(netrc_path) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('.netrc file saved'), - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_reset_options_clicked(self, button): - - """Called by callback in self.setup_name_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Name' - ) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'This procedure cannot be reversed. Are you sure you want to' \ - + ' continue?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'reset_download_options', - # (Reset this edit window, if the user clicks 'yes') - 'data': [self], - }, - ) - - - def on_simple_options_clicked(self, button): - - """Called by callback in self.setup_general_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Name' - ) - - if not self.app_obj.simple_options_flag: - - self.app_obj.set_simple_options_flag(True) - - if not self.edit_dict: - # User has not changed any options, so redraw the window to - # show the same options.OptionsManager object - self.reset_with_new_edit_obj(self.edit_obj) - - else: - # User has already changed some options. We don't want to lose - # them, so wait for the window to close and be re-opened, - # before switching between simple/advanced options - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Fewer download options will be visible when you click' \ - + ' the \'Apply\' or \'Reset\' buttons (or when you' \ - + ' close and then re-open the window)', - ), - 'info', - 'ok', - self, # Parent window is this window - ) - - button.set_label( - _('Show advanced download options (when window re-opens)'), - ) - - else: - - self.app_obj.set_simple_options_flag(False) - - if not self.edit_dict: - self.reset_with_new_edit_obj(self.edit_obj) - - else: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'More download options will be visible when you click' \ - + ' the \'Apply\' or \'Reset\' buttons (or when you' \ - + ' close and then re-open the window)', - ), - 'info', - 'ok', - self, # Parent window is this window - ) - - button.set_label( - _('Hide advanced download options (when window re-opens)'), - ) - - - def on_sleep_button_changed(self, spinbutton, spinbutton2): - - """Called by callback in self.setup_advanced_workaround_tab(). - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - spinbutton2 (Gtk.SpinButton2): Another widget to update - - """ - - value = int(spinbutton.get_value()) - - self.edit_dict['min_sleep_interval'] = value - if value == 0: - spinbutton2.set_value(0) - spinbutton2.set_sensitive(False) - else: - spinbutton2.set_sensitive(True) - - - def on_subtitles_tab_add_clicked(self, button, treeview, other_liststore, - rev_dict): - - """Called by callback in self.setup_subtitles_options_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview on the left side of the tab - - other_liststore (Gtk.ListStore): The liststore belonging to the - treeview on the right side of the tab - - rev_dict (dict): A reversed formats.LANGUAGE_CODE_DICT - - """ - - selection = treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - name = model[tree_iter][0] - # From a string in the form 'English [en]', remove the language code to - # get a key in formats.LANGUAGE_CODE_DICT, e.g. 'English' - match = re.search(r'^(.*)\s\[', name) - if match: - - lang_name = match.groups()[0] - if lang_name in formats.LANGUAGE_CODE_DICT: - - lang_code = formats.LANGUAGE_CODE_DICT[lang_name] - - # Retrieve the existing list of languages - lang_code_list = self.retrieve_val('subs_lang_list') - if not lang_code in lang_code_list: - - lang_code_list.append(lang_code) - - # Sort by language name, not by language code - lang_list = [] - mod_code_list = [] - for this_code in lang_code_list: - lang_list.append(rev_dict[this_code]) - - lang_list.sort() - for this_lang in lang_list: - mod_code_list.append(formats.LANGUAGE_CODE_DICT[this_lang]) - - # Update the option... - self.edit_dict['subs_lang_list'] = mod_code_list - # ...and the treeview - other_liststore.clear() - for this_lang in lang_list: - other_liststore.append([ - this_lang + ' [' \ - + formats.LANGUAGE_CODE_DICT[this_lang] + ']', - ]) - - - def on_subtitles_tab_remove_clicked(self, button, other_treeview, - other_liststore, rev_dict): - - """Called by callback in self.setup_subtitles_options_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - other_treeview (Gtk.TreeView): The treeview on the right side of - the tab - - other_liststore (Gtk.ListStore): The liststore belonging to that - treeview - - rev_dict (dict): A reversed formats.LANGUAGE_CODE_DICT - - """ - - selection = other_treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - name = model[tree_iter][0] - # From a string in the form 'English [en]', remove the language name to - # get a value in formats.LANGUAGE_CODE_DICT, e.g. 'en' - match = re.search(r'^.*\s\[(.*)\]', name) - if match: - - lang_code = match.groups()[0] - - # Retrieve the existing list of languages - lang_code_list = self.retrieve_val('subs_lang_list') - if lang_code in lang_code_list: - - lang_code_list.remove(lang_code) - - # Sort by language name, not by language code - lang_list = [] - mod_code_list = [] - for this_code in lang_code_list: - lang_list.append(rev_dict[this_code]) - - lang_list.sort() - for this_lang in lang_list: - mod_code_list.append(formats.LANGUAGE_CODE_DICT[this_lang]) - - # Update the option... - self.edit_dict['subs_lang_list'] = mod_code_list - # ...and the treeview - other_liststore.clear() - for this_lang in lang_list: - other_liststore.append([ - this_lang + ' [' \ - + formats.LANGUAGE_CODE_DICT[this_lang] + ']', - ]) - - - def on_subtitles_toggled(self, radiobutton, button, button2, prop): - - """Called by callback in self.setup_subtitles_options_tab(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - button, button2 (Gtk.Button): Other widgets to be modified by this - function - - prop (str): The attribute in self.edit_dict to modify - - """ - - if radiobutton.get_active(): - - if prop == 'write_subs': - self.edit_dict['write_subs'] = False - self.edit_dict['write_auto_subs'] = False - self.edit_dict['write_all_subs'] = False - button.set_sensitive(False) - button2.set_sensitive(False) - - elif prop == 'write_auto_subs': - self.edit_dict['write_subs'] = True - self.edit_dict['write_auto_subs'] = True - self.edit_dict['write_all_subs'] = False - button.set_sensitive(False) - button2.set_sensitive(False) - - elif prop == 'write_all_subs': - self.edit_dict['write_subs'] = True - self.edit_dict['write_auto_subs'] = False - self.edit_dict['write_all_subs'] = True - button.set_sensitive(False) - button2.set_sensitive(False) - - elif prop == 'subs_lang': - self.edit_dict['write_subs'] = True - self.edit_dict['write_auto_subs'] = False - self.edit_dict['write_all_subs'] = False - button.set_sensitive(True) - button2.set_sensitive(True) - - - def on_video_format_mode_toggled(self, radiobutton, value): - - """Called by callback in self.setup_formats_advanced_tab(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - prop (str): The attribute in self.edit_dict to modify - - """ - - if radiobutton.get_active(): - self.edit_dict['video_format_mode'] = value - - - def on_ytdlp_output_add_button_clicked(self, button, liststore, combo, \ - entry): - - """Called from callback in self.setup_files_override_tab(). - - Adds a template to the 'output_format_list' option. - - Args: - - button (Gtk.Button): The widget clicked - - liststore (Gtk.ListStore): The treeview's model - - combo (Gtk.ComboBox): Widget providing the output type - - entry (Gtk.Entry): Widget providiing the output template - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - output_type = model[tree_iter][0] - - output_template = entry.get_text() - if output_template == '': - return - - # Items in the list are in the form TYPES:TEMPLATE - # However, every item should have a uniquE TYPES component. If an item - # with a TYPES component matching 'output_type' already exists, - # overwrite it. Otherwise, add a new item to the end of the list - output_format_list = self.retrieve_val('output_format_list') - mod_list = [] - match_flag = False - - for item in output_format_list: - - match = re.search(r'^([^\:]+)\:', item) - if match: - this_output_type = match.groups()[0] - - if this_output_type == output_type: - mod_list.append(output_type + ':' + output_template) - match_flag = True - - else: - mod_list.append(item) - - if not match_flag: - mod_list.append(output_type + ':' + output_template) - - # Update the option - self.edit_dict['output_format_list'] = mod_list - # Update the treeview - self.setup_files_override_tab_update_treeview(liststore) - - - def on_ytdlp_output_delete_button_clicked(self, button, treeview): - - """Called from a callback in self.setup_files_override_tab(). - - Deletes the selected template to the 'output_format_list' option. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the template list - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - return - - # Items in the list are in the form TYPES:TEMPLATE - output_type = model[this_iter][0] - output_template = model[this_iter][1] - item = output_type + ':' + output_template - # Walk the list, and delete the first matching group - output_format_list = self.retrieve_val('output_format_list') - mod_list = [] - match_flag = False - - for this_item in output_format_list: - - if not match_flag and this_item == item: - match_flag = True # Delete this one - else: - mod_list.append(this_item) - - # Update the option - self.edit_dict['output_format_list'] = mod_list - # Update the treeview - self.setup_files_override_tab_update_treeview(treeview.get_model()) - - - def on_ytdlp_output_refresnh_button_clicked(self, button, treeview): - - """Called from a callback in self.setup_files_override_tab(). - - Refreshes the treeview. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the template list - - """ - - # Update the treeview - self.setup_files_override_tab_update_treeview(treeview.get_model()) - - - def on_ytdlp_paths_add_button_clicked(self, button, liststore, combo, \ - entry): - - """Called from callback in self.setup_files_paths_tab(). - - Adds a path to the 'output_path_list' option. - - Args: - - button (Gtk.Button): The widget clicked - - liststore (Gtk.ListStore): The treeview's model - - combo (Gtk.ComboBox): Widget providing the output type - - entry (Gtk.Entry): Widget providiing the output path - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - output_type = model[tree_iter][0] - - output_path = entry.get_text() - if output_path == '': - return - - # Items in the list are in the form TYPES:PATH - # However, every item should have a uniquE TYPES component. If an item - # with a TYPES component matching 'output_type' already exists, - # overwrite it. Otherwise, add a new item to the end of the list - output_path_list = self.retrieve_val('output_path_list') - mod_list = [] - match_flag = False - - for item in output_path_list: - - match = re.search(r'^([^\:]+)\:', item) - if match: - this_output_type = match.groups()[0] - - if this_output_type == output_type: - mod_list.append(output_type + ':' + output_path) - match_flag = True - - else: - mod_list.append(item) - - if not match_flag: - mod_list.append(output_type + ':' + output_path) - - # Update the option - self.edit_dict['output_path_list'] = mod_list - # Update the treeview - self.setup_files_paths_tab_update_treeview(liststore) - - - def on_ytdlp_paths_delete_button_clicked(self, button, treeview): - - """Called from a callback in self.setup_files_paths_tab(). - - Deletes the selected path to the 'output_path_list' option. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the path list - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - return - - # Items in the list are in the form TYPES:PATH - output_type = model[this_iter][0] - output_path = model[this_iter][1] - item = output_type + ':' + output_path - # Walk the list, and delete the first matching group - output_path_list = self.retrieve_val('output_path_list') - mod_list = [] - match_flag = False - - for this_item in output_path_list: - - if not match_flag and this_item == item: - match_flag = True # Delete this one - else: - mod_list.append(this_item) - - # Update the option - self.edit_dict['output_path_list'] = mod_list - # Update the treeview - self.setup_files_paths_tab_update_treeview(treeview.get_model()) - - - def on_ytdlp_paths_refresh_button_clicked(self, button, treeview): - - """Called from a callback in self.setup_files_paths_tab(). - - Refreshes the treeview. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the path list - - """ - - # Update the treeview - self.setup_files_paths_tab_update_treeview(treeview.get_model()) - - - def on_ytdlp_paths_set_button_clicked(self, button, entry): - - """Called from a callback in self.setup_files_paths_tab(). - - Opens a file chooser dialogue to set the contents of the entry box. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to update - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Download options > Files > Paths' - ) - - # Prompt the user for a new file - if os.name == 'nt': - msg = _('Select the output folder') - else: - msg = _('Select the output directory') - - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - msg, - self, - 'folder', - ) - - current_dir = entry.get_text() - if current_dir: - dialogue_win.set_current_folder(current_dir) - - response = dialogue_win.run() - output_dir = dialogue_win.get_filename() - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and output_dir != '': - - entry.set_text(output_dir) - - -class FFmpegOptionsEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in an - ffmpeg_tartube.FFmpegOptionsManager object. - - Adapted from FFmpeg Command Line Wizard, by AndreKR - (https://github.com/AndreKR/ffmpeg-command-line-wizard). - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (ffmpeg_tartube.FFmpegOptionsManager): The object whose - attributes will be edited in this window - - video_list (list): An optional list of media.Video objects. If not - empty, when the edit window closes, a process operation will - start, using the FFmpeg options specified by 'edit_obj' to process - all the videos in the list. If empty, no operation is started; the - modified FFmpeg options are just updated as normal - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj, video_list=[]): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: FFmpeg options window starts here.' \ - + ' In the main window, in the Videos tab, right-click a video' \ - + ' and select Special > Process with FFmpeg...' - ) - - Gtk.Window.__init__(self, title=_('FFmpeg options')) - - if self.is_duplicate(app_obj, edit_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The ffmpeg_tartube.FFmpegOptionsManager object being edited - self.edit_obj = edit_obj - # An optional list of media.Video objects. If not empty, when the edit - # window closes, a process operation will start, using the FFmpeg - # options specified by 'edit_obj' to process all the videos in the - # list. If empty, no operation is started; the modified FFmpeg - # options are just updated as normal - self.video_list = video_list - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (Because of the need to (de)sensitise widgets so often, more of them - # than usual have their own IVs) - # (Name tab) - self.extra_cmd_string_textview = None # Gtk.TextView - self.extra_cmd_string_textbuffer = None # Gtk.TextBuffer - self.result_textview = None # Gtk.TextView - self.results_textbuffer = None # Gtk.TextBuffer - # (File tab) - self.add_end_filename_entry = None # Gtk.Entry - self.regex_match_filename_entry = None # Gtk.Entry - self.regex_apply_subst_entry = None # Gtk.Entry - self.rename_both_flag_checkbutton = None - # Gtk.CheckButton - self.change_file_ext_entry = None # Gtk.Entry - self.delete_original_flag_checkbutton = None - # Gtk.CheckButton - # (Settings tab) - self.input_mode_radiobutton = None # Gtk.RadioButton - self.input_mode_radiobutton2 = None # Gtk.RadioButton - self.audio_flag_checkbutton = None # Gtk.CheckButton - self.output_mode_radiobutton = None # Gtk.RadioButton - self.output_mode_radiobutton2 = None # Gtk.RadioButton - self.output_mode_radiobutton3 = None # Gtk.RadioButton - self.output_mode_radiobutton4 = None # Gtk.RadioButton - self.output_mode_radiobutton5 = None # Gtk.RadioButton - self.output_mode_radiobutton6 = None # Gtk.RadioButton - self.h264_grid = None # Gtk.Grid - self.gif_grid = None # Gtk.Grid - self.clip_grid = None # Gtk.Grid - self.slice_grid = None # Gtk.Grid - self.merge_grid = None # Gtk.Grid - self.thumb_grid = None # Gtk.Grid - # (Settings tab, H.264 grid) - self.audio_bitrate_spinbutton = None # Gtk.SpinButton - self.quality_mode_radiobutton = None # Gtk.RadioButton - self.quality_mode_radiobutton2 = None # Gtk.RadioButton - self.rate_factor_scale = None # Gtk.Scale - self.dummy_file_combo = None # Gtk.ComboBox - self.patience_preset_combo = None # Gtk.ComboBox - self.gpu_encoding_combo = None # Gtk.ComboBox - self.hw_accel_combo = None # Gtk.ComboBox - # (Settings tab, GIF grid) - self.palette_mode_radiobutton = None # Gtk.RadioButton - self.palette_mode_radiobutton2 = None # Gtk.RadioButton - # (Settings tab, split grid) - self.split_mode_radiobutton = None # Gtk.RadioButton - self.split_mode_radiobutton2 = None # Gtk.RadioButton - self.split_mode_liststore = None # Gtk.ListStore - self.start_stamp_entry = None # Gtk.Entry - self.stop_stamp_entry = None # Gtk.Entry - self.clip_title_entry = None # Gtk.Entry - self.add_timestamp_button = None # Gtk.Button - self.delete_timestamp_button = None # Gtk.Button - self.show_prefs_button = None # Gtk.Button - self.clear_timestamp_button = None # Gtk.Button - # (Settings tab, slice grid) - self.slice_mode_radiobutton = None # Gtk.RadioButton - self.slice_mode_radiobutton2 = None # Gtk.RadioButton - self.slice_mode_liststore = None # Gtk.ListStore - self.category_combo = None # Gtk.ComboBox - self.action_combo = None # Gtk.ComboBox - self.slice_start_entry = None # Gtk.Entry - self.slice_stop_entry = None # Gtk.Entry - self.add_slice_button = None # Gtk.Button - self.delete_slice_button = None # Gtk.Button - self.show_settings_button = None # Gtk.Button - self.clear_slice_button = None # Gtk.Button - # (Optimise tab) - self.seek_flag_checkbutton = None # Gtk.CheckButton - self.tuning_film_flag_checkbutton = None - # Gtk.CheckButton - self.tuning_animation_flag_checkbutton = None - # Gtk.CheckButton - self.tuning_grain_flag_checkbutton = None - # Gtk.CheckButton - self.tuning_still_image_flag_checkbutton = None - # Gtk.CheckButton - self.tuning_fast_decode_flag_checkbutton = None - # Gtk.CheckButton - self.profile_flag_checkbutton = None # Gtk.CheckButton - self.fast_start_flag_checkbutton = None # Gtk.CheckButton - self.tuning_zero_latency_flag_checkbutton = None - # Gtk.CheckButton - self.limit_flag_checkbutton = None # Gtk.CheckButton - self.limit_mbps_spinbutton = None # Gtk.SpinButton - self.limit_buffer_spinbutton = None # Gtk.SpinButton - # (Clips tab) - self.simple_split_mode_checkbutton = None - # Gtk.CheckButton - self.simple_split_mode_radiobutton = None - # Gtk.RadioButton - self.simple_split_mode_radiobutton2 = None - # Gtk.RadioButton - self.simple_split_mode_liststore = None # Gtk.ListStore - self.simple_start_stamp_entry = None - # Gtk.Entry - self.simple_stop_stamp_entry = None # Gtk.Entry - self.simple_clip_title_entry = None # Gtk.Entry - self.simple_add_timestamp_button = None # Gtk.Button - self.simple_delete_timestamp_button = None - # Gtk.Button - self.simple_show_prefs_button = None # Gtk.Button - self.simple_clear_timestamp_button = None - # Gtk.Button - # (Slices tab) - self.simple_slice_mode_checkbutton = None - # Gtk.CheckButton - self.simple_slice_mode_radiobutton = None - # Gtk.RadioButton - self.simple_slice_mode_radiobutton2 = None - # Gtk.RadioButton - self.simple_slice_mode_liststore = None # Gtk.ListStore - self.simple_category_combo = None # Gtk.ComboBox - self.simple_action_combo = None # Gtk.ComboBox - self.simple_slice_start_entry = None # Gtk.Entry - self.simple_slice_stop_entry = None # Gtk.Entry - self.simple_show_settings_button = None # Gtk.Button - # (Videox tab) - self.video_liststore = None # Gtk.ListStore - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK') are required, or False if just the 'OK' button is required - self.multi_button_flag = True - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # In this edit window, the key-value pairs directly correspond to those - # in ffmpeg_tartube.FFmpegOptionsManager.options_dict, rather than - # corresponding directly to attributes in the - # ffmpeg_tartube.FFmpegOptionsManager object - # Because of that, we use our own .apply_changes() and .retrieve_val() - # functions, rather than relying on the generic functions - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericConfigWin - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self, apply_button_flag=False): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - - In this edit window we apply changes to self.edit_obj.options_dict - (rather than to self.edit_obj's attributes directly, as in the generic - function.) - - Args: - - apply_button_flag (bool): True when self.apply_button was clicked, - False when self.ok_button was clicked. When True, we do not - start a process operation - - """ - - # For 'change_file_ext', remove the initial . (e.g. in '.mp4', if - # specified - if 'change_file_ext' in self.edit_dict: - self.edit_dict['change_file_ext'] = re.sub( - r'^\.', - '', - self.edit_dict['change_file_ext'], - ) - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - - if key in self.edit_obj.options_dict: - self.edit_obj.options_dict[key] = self.edit_dict[key] - - # The name can also be updated, if it has been changed (but it the - # entry was blank, keep the old name) - if 'name' in self.edit_dict \ - and self.edit_dict['name'] != '': - self.edit_obj.name = self.edit_dict['name'] - - # The changes can now be cleared - self.edit_dict = {} - - # If a list of videos was supplied, start a process operation - if self.video_list and not apply_button_flag: - - # Check that every media.Video object still exists, eliminating any - # that don't - mod_list = [] - for video_obj in self.video_list: - - # (Special case: 'dummy' video objects (those downloaded in the - # Classic Mode tab) use different IVs) - if video_obj.dummy_flag \ - or ( - video_obj.dbid in self.app_obj.media_reg_dict \ - and self.app_obj.media_reg_dict[video_obj.dbid] \ - == video_obj - ): - mod_list.append(video_obj) - - if mod_list: - - self.app_obj.process_manager_start( - self.edit_obj, - mod_list, - ) - - - def retrieve_val(self, name): - - """Can be called by anything. - - Any changes the user has made are temporarily stored in self.edit_dict. - - In the generic function, each key corresponds to an attribute in the - object being edited, self.edit_obj. In this window, it corresponds to a - key in self.edit_obj.options_dict. - - If 'name' exists as a key in that dictionary, retrieve the - corresponding value and return it. Otherwise, the user hasn't yet - modified the value, so retrieve directly from the attribute in the - object being edited. - - Args: - - name (str): The name of the attribute in the object being edited - - Return values: - - The original or modified value of that attribute - - """ - - if name in self.edit_dict: - - return self.edit_dict[name] - - elif name == 'uid' or name == 'name': - - return getattr(self.edit_obj, name) - - elif name in self.edit_obj.options_dict: - - value = self.edit_obj.options_dict[name] - if type(value) is list or type(value) is dict: - return value.copy() - else: - return value - - else: - - return self.app_obj.system_error( - 405, - 'Unrecognised property name \'' + name + '\'', - ) - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_name_tab() - self.setup_file_tab() - - if not self.app_obj.simple_ffmpeg_options_flag: - self.setup_settings_tab() - self.setup_optimise_tab() - else: - self.setup_clips_tab() - self.setup_slices_tab() - - self.setup_videos_tab() - - # Unusual step: if a list of media.Video objects to be processed has - # been supplied, use a different label for the OK button - if self.video_list: - - self.ok_button.set_label(_('Process files')) - self.ok_button.set_tooltip_text( - _('Process the files with FFmpeg'), - ) - self.ok_button.get_child().set_width_chars(15) - - - def setup_name_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Name' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: FFmpeg options > Name' - ) - - tab, grid = self.add_notebook_tab(_('_Name')) - grid_width = 4 - - self.add_label(grid, - _('Name for these FFmpeg options'), - 0, 0, 2, 1, - ) - - entry = self.add_entry(grid, - 'name', - 2, 0, 1, 1, - ) - entry.set_hexpand(True) - - entry2 = self.add_entry(grid, - None, - 3, 0, 1, 1, - ) - entry2.set_text('#' + str(self.edit_obj.uid)) - entry2.set_hexpand(False) - - self.add_label(grid, - _('Extra command line options (e.g. --help)'), - 0, 1, grid_width, 1, - ) - - self.extra_cmd_string_textview, \ - self.extra_cmd_string_textbuffer = self.add_textview(grid, - 'extra_cmd_string', - 0, 2, grid_width, 1, - ) - - self.add_label(grid, - _('System command, based on all FFmpeg options in this window:'), - 0, 3, grid_width, 1, - ) - - self.result_textview, self.results_textbuffer = self.add_textview(grid, - None, - 0, 4, grid_width, 1, - ) - self.result_textview.set_editable(False) - self.result_textview.set_wrap_mode(Gtk.WrapMode.WORD) - self.result_textview.set_can_focus(False) - # (Set the system command, as it stands) - self.update_system_cmd() - - if self.app_obj.simple_options_flag: - frame = self.add_pixbuf(grid, - 'hand_right_large', - 0, 5, 1, 1, - ) - frame.set_hexpand(False) - - else: - frame = self.add_pixbuf(grid, - 'hand_left_large', - 0, 5, 1, 1, - ) - frame.set_hexpand(False) - - button = Gtk.Button() - grid.attach(button, 1, 5, (grid_width - 1), 1) - if not self.app_obj.simple_ffmpeg_options_flag: - button.set_label(_('Show fewer FFmpeg options')) - else: - button.set_label(_('Show more FFmpeg options')) - button.connect('clicked', self.on_simple_options_clicked) - - frame2 = self.add_pixbuf(grid, - 'copy_large', - 0, 6, 1, 1, - ) - frame2.set_hexpand(False) - - button2 = Gtk.Button( - _('Import current FFmpeg options into this window'), - ) - grid.attach(button2, 1, 6, (grid_width - 1), 1) - button2.connect('clicked', self.on_clone_options_clicked) - if self.edit_obj == self.app_obj.ffmpeg_options_obj: - # No point cloning the current options manager into itself - button2.set_sensitive(False) - - frame3 = self.add_pixbuf(grid, - 'warning_large', - 0, 7, 1, 1, - ) - frame3.set_hexpand(False) - - button3 = Gtk.Button( - _('Completely reset all FFmpeg options to their default values'), - ) - grid.attach(button3, 1, 7, (grid_width - 1), 1) - button3.connect('clicked', self.on_reset_options_clicked) - - - def setup_file_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'File' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: FFmpeg options > File' - ) - - tab, grid = self.add_notebook_tab(_('_File')) - - self.add_label(grid, - _('Add to end of filename:'), - 0, 0, 1, 1, - ) - - self.add_end_filename_entry = self.add_entry(grid, - 'add_end_filename', - 1, 0, 1, 1, - ) - - self.add_label(grid, - _('If regex matches filename:'), - 0, 1, 1, 1, - ) - - self.regex_match_filename_entry = self.add_entry(grid, - None, - 1, 1, 1, 1, - ) - self.regex_match_filename_entry.set_text( - self.retrieve_val('regex_match_filename'), - ) - # (Signal connect appears below) - - self.add_label(grid, - _('...then apply substitution:'), - 0, 2, 1, 1, - ) - - self.regex_apply_subst_entry = self.add_entry(grid, - 'regex_apply_subst', - 1, 2, 1, 1, - ) - if self.retrieve_val('regex_match_filename') == '': - self.regex_apply_subst_entry.set_sensitive(False) - - self.rename_both_flag_checkbutton = self.add_checkbutton(grid, - _( - 'If the video/audio file is renamed, also rename the thumbnail' \ - + ' (but not vice-versa)', - ), - 'rename_both_flag', - 0, 3, 2, 1, - ) - if self.retrieve_val('add_end_filename') == '' \ - and self.retrieve_val('regex_match_filename') == '': - self.rename_both_flag_checkbutton.set_sensitive(False) - - self.add_label(grid, - _('Change file extension:'), - 0, 4, 1, 1, - ) - - self.change_file_ext_entry = self.add_entry(grid, - None, - 1, 4, 1, 1, - ) - self.change_file_ext_entry.set_text( - self.retrieve_val('change_file_ext'), - ) - # (Signal connect appears below) - - self.delete_original_flag_checkbutton = self.add_checkbutton(grid, - _('After changing the file extension, delete the original file'), - 'delete_original_flag', - 0, 5, 1, 1, - ) - if self.retrieve_val('change_file_ext') == '': - self.delete_original_flag_checkbutton.set_sensitive(False) - - # (Signal connects from above) - self.regex_match_filename_entry.connect( - 'changed', - self.on_regex_match_filename_entry_changed, - ) - - self.change_file_ext_entry.connect( - 'changed', - self.on_change_file_ext_entry_changed, - ) - - # (De)sensitise all of these widgets, depending on the value of the - # 'output_mode' setting - if self.retrieve_val('output_mode') == 'split': - self.setup_file_tab_set_sensitive(False) - else: - self.setup_file_tab_set_sensitive(True) - - - def setup_file_tab_set_sensitive(self, sens_flag): - - """Called by self.setup_file_tab() and various callbacks. - - (De)sensitises all widgets in the tab, as required. - - Args: - - sens_flag (bool): True to sensitise widgets, False to desensitise - them - - """ - - self.add_end_filename_entry.set_sensitive(sens_flag) - self.regex_match_filename_entry.set_sensitive(sens_flag) - - if self.retrieve_val('regex_match_filename') == '': - self.regex_apply_subst_entry.set_sensitive(False) - else: - self.regex_apply_subst_entry.set_sensitive(sens_flag) - - if self.retrieve_val('add_end_filename') == '' \ - and self.retrieve_val('regex_match_filename') == '': - self.rename_both_flag_checkbutton.set_sensitive(False) - else: - self.rename_both_flag_checkbutton.set_sensitive(sens_flag) - - if self.retrieve_val('output_mode') == 'gif': - self.change_file_ext_entry.set_sensitive(False) - else: - self.change_file_ext_entry.set_sensitive(sens_flag) - - if self.retrieve_val('change_file_ext') == '': - self.delete_original_flag_checkbutton.set_sensitive(False) - else: - self.delete_original_flag_checkbutton.set_sensitive(sens_flag) - - - def setup_settings_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Settings' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: FFmpeg options > Settings (to make hidden' \ - ' tabs visible, click the \'Show more FFmpeg options\' button' \ - ' in the Name tab' - ) - - tab, grid = self.add_notebook_tab(_('_Settings')) - grid_width = 7 - self.settings_grid = grid - - # Source - label = self.add_label(grid, - '' + _('Source') + '', - 0, 0, 1, 1, - ) - label.set_hexpand(False) - - self.input_mode_radiobutton = self.add_radiobutton(grid, - None, - _('Downloaded video/audio'), - None, - None, - 1, 0, 4, 1, - ) - self.input_mode_radiobutton.set_hexpand(False) - # (Signal connect appears below) - - self.audio_flag_checkbutton = self.add_checkbutton(grid, - _('with audio'), - None, - 5, 0, 1, 1, - ) - self.audio_flag_checkbutton.set_hexpand(False) - if self.retrieve_val('audio_flag'): - self.audio_flag_checkbutton.set_active(True) - if self.retrieve_val('input_mode') != 'video': - self.audio_flag_checkbutton.set_sensitive(False) - # (Signal connect appears below) - - self.input_mode_radiobutton2 = self.add_radiobutton(grid, - self.input_mode_radiobutton, - _('Thumbnail'), - None, - None, - 6, 0, 1, 1, - ) - self.input_mode_radiobutton2.set_hexpand(False) - if self.retrieve_val('input_mode') == 'thumb': - self.input_mode_radiobutton2.set_active(True) - # (Signal connect appears below) - - # Output - label2 = self.add_label(grid, - '' + _('Output') + '', - 0, 1, 1, 1, - ) - label2.set_hexpand(False) - - self.output_mode_radiobutton = self.add_radiobutton(grid, - None, - 'H.264', - None, - None, - 1, 1, 1, 1, - ) - self.output_mode_radiobutton.set_hexpand(False) - # (Signal connect appears below) - - self.output_mode_radiobutton2 = self.add_radiobutton(grid, - self.output_mode_radiobutton, - 'GIF', - None, - None, - 2, 1, 1, 1, - ) - self.output_mode_radiobutton2.set_hexpand(False) - if self.retrieve_val('output_mode') == 'gif': - self.output_mode_radiobutton2.set_active(True) - # (Signal connect appears below) - - self.output_mode_radiobutton3 = self.add_radiobutton(grid, - self.output_mode_radiobutton2, - _('Video clip'), - None, - None, - 3, 1, 1, 1, - ) - self.output_mode_radiobutton3.set_hexpand(False) - if self.retrieve_val('output_mode') == 'split': - self.output_mode_radiobutton3.set_active(True) - # (Signal connect appears below) - - self.output_mode_radiobutton4 = self.add_radiobutton(grid, - self.output_mode_radiobutton3, - _('Video slice'), - None, - None, - 4, 1, 1, 1, - ) - self.output_mode_radiobutton4.set_hexpand(False) - if self.retrieve_val('output_mode') == 'slice': - self.output_mode_radiobutton4.set_active(True) - # (Signal connect appears below) - - self.output_mode_radiobutton5 = self.add_radiobutton(grid, - self.output_mode_radiobutton4, - _('Merge video/audio'), - None, - None, - 5, 1, 1, 1, - ) - self.output_mode_radiobutton5.set_hexpand(False) - if self.retrieve_val('output_mode') == 'merge': - self.output_mode_radiobutton5.set_active(True) - # (Signal connect appears below) - - self.output_mode_radiobutton6 = self.add_radiobutton(grid, - self.output_mode_radiobutton5, - _('Thumbnail'), - None, - None, - 6, 1, 1, 1, - ) - self.output_mode_radiobutton6.set_hexpand(False) - if self.retrieve_val('output_mode') == 'thumb': - self.output_mode_radiobutton6.set_active(True) - # (Signal connect appears below) - - # Supplementary grids: one for each 'output_mode' - # Only one of them is visible at any time (this saves a lot of time - # (de)sensitising widgets) - self.h264_grid = self.setup_settings_tab_h264_grid(2, grid_width) - self.gif_grid = self.setup_settings_tab_gif_grid(2, grid_width) - self.clip_grid = self.setup_settings_tab_clip_grid(2, grid_width) - self.slice_grid = self.setup_settings_tab_slice_grid(2, grid_width) - self.merge_grid = self.setup_settings_tab_merge_grid(2, grid_width) - self.thumb_grid = self.setup_settings_tab_thumb_grid(2, grid_width) - - # (Signal connects from above) - self.input_mode_radiobutton.connect( - 'toggled', - self.on_input_mode_radiobutton_toggled, - ) - self.audio_flag_checkbutton.connect( - 'toggled', - self.on_audio_flag_checkbutton_toggled, - ) - self.input_mode_radiobutton2.connect( - 'toggled', - self.on_input_mode_radiobutton_toggled, - ) - - self.output_mode_radiobutton.connect( - 'toggled', - self.on_output_mode_radiobutton_toggled, - grid_width, - ) - self.output_mode_radiobutton2.connect( - 'toggled', - self.on_output_mode_radiobutton_toggled, - grid_width, - ) - self.output_mode_radiobutton3.connect( - 'toggled', - self.on_output_mode_radiobutton_toggled, - grid_width, - ) - self.output_mode_radiobutton4.connect( - 'toggled', - self.on_output_mode_radiobutton_toggled, - grid_width, - ) - self.output_mode_radiobutton5.connect( - 'toggled', - self.on_output_mode_radiobutton_toggled, - grid_width, - ) - self.output_mode_radiobutton6.connect( - 'toggled', - self.on_output_mode_radiobutton_toggled, - grid_width, - ) - - - def setup_settings_tab_h264_grid(self, row, outer_width): - - """Called by self.setup_settings_tab(). - - Creates a supplementary grid, within the tab's outer grid, which can be - swapped in and out as the 'output_mode' option is changed. - - This supplementary grid is visible when 'output_mode' is 'h264'. - - Args: - - row (int): The row on the tab's outer grid, on which the - supplementary grid is to be placed - - outer_width (int): The width of the tab's outer grid - - Return values: - - The new Gtk.Grid(). - - """ - - grid = Gtk.Grid() - if self.retrieve_val('output_mode') == 'h264': - self.settings_grid.attach(grid, 0, row, outer_width, 1) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - inner_width = 3 - - self.add_label(grid, - _('Audio bitrate'), - 0, 0, 1, 1, - ) - - self.audio_bitrate_spinbutton = self.add_spinbutton(grid, - 16, None, 16, - 'audio_bitrate', - 1, 0, 1, 1, - ) - if self.retrieve_val('input_mode') != 'video' \ - or not self.retrieve_val('audio_flag'): - self.audio_bitrate_spinbutton.set_sensitive(False) - - label = self.add_label(grid, - _('How to set the quality') + ' ⓘ', - 0, 1, 1, 1, - ) - label.set_tooltip_text( - _( - 'FFmpeg always encodes according to a Rate Factor that specifies' \ - + ' the quality of the result.', - ) + '\n\n' + _( - 'Instead of directly specifying the Rate Factor, an average bit' \ - + ' rate can be specified. FFmpeg will then determine the' \ - + ' optimal Rate Factor in a first pass.', - ) + '\n\n' + _( - 'In fact the first pass is only used for determining the Rate' \ - + ' Factor, no other data is carried over into the second pass.', - ) + '\n\n' + _( - 'Specifying an average bitrate but running only one pass is' \ - + ' possible, but not recommended. FFmpeg would then encode the' \ - + ' beginning of the video with a random Rate Factor and then' \ - + ' change it near the end of the video to eventually reach the' \ - + ' target bitrate.', - ), - ) - - # N.B. In the original 'FFmpeg command line wizard', the second of this - # pair of radiobuttons are disabled (for unknown reasons); here it is - # enabled - self.quality_mode_radiobutton = self.add_radiobutton(grid, - None, - _('Manual rate factor'), - None, - None, - 1, 1, (inner_width - 1), 1, - ) - # (Signal connect appears below) - - self.quality_mode_radiobutton2 = self.add_radiobutton(grid, - self.quality_mode_radiobutton, - _('Determine from target bitrate (2-Pass)'), - None, - None, - 1, 2, (inner_width - 1), 1, - ) - if self.retrieve_val('quality_mode') == 'abr': - self.quality_mode_radiobutton2.set_active(True) - # (Signal connect appears below) - - self.add_label(grid, - _('Rate factor'), - 0, 3, 1, 1, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 1, 3, (inner_width - 1), 1) - - label2 = self.add_label(grid2, - _('Lossless') + '\n' + _('Large file'), - 0, 0, 1, 1, - ) - label2.set_hexpand(False) - - self.rate_factor_scale = Gtk.Scale().new_with_range( - Gtk.Orientation.HORIZONTAL, - 0, - 51, - 1, - ) - grid2.attach(self.rate_factor_scale, 1, 0, 1, 1) - self.rate_factor_scale.set_draw_value(True) - self.rate_factor_scale.set_value( - self.retrieve_val('rate_factor'), - ) - self.rate_factor_scale.set_hexpand(True) - if self.retrieve_val('quality_mode') == 'abr': - self.rate_factor_scale.set_sensitive(False) - # (Signal connect appears below) - - label3 = self.add_label(grid2, - _('Bad quality') + '\n' + _('Small file'), - 2, 0, 1, 1, - ) - label3.set_hexpand(False) - # (End of the yet another grid) - - label4 = self.add_label(grid, - _('Name of dummy file') + ' ⓘ', - 0, 4, 1, 1, - ) - label4.set_tooltip_text( - _('A dummy file is created during the first pass.'), - ) - - combo_list = [ - [_('Use the output file'), 'output'], - [_('Dummy'), 'dummy'], - [_('/dev/null (Linux)'), '/dev/null'], - [_('NUL (MS Windows)'), 'NUL'], - ] - - self.dummy_file_combo = self.add_combo_with_data(grid, - combo_list, - 'dummy_file', - 1, 4, (inner_width - 1), 1, - ) - if self.retrieve_val('quality_mode') != 'abr': - self.dummy_file_combo.set_sensitive(False) - - self.add_label(grid, - _('Patience preset'), - 0, 5, 1, 1, - ) - - combo_list2 = [ - [_('Ultra fast'), 'ultrafast'], - [_('Super fast'), 'superfast'], - [_('Very fast'), 'veryfast'], - [_('Faster'), 'faster'], - [_('Fast'), 'fast'], - [_('Medium (default)'), 'medium'], - [_('Slow (file about 5-10% smaller than medium)'), 'slow'], - [_('Slower (file about 15% smaller than medium)'), 'slower'], - [_('Very slow (file about 17% smaller than medium)'), 'veryslow'], - ] - - self.patience_preset_combo = self.add_combo_with_data(grid, - combo_list2, - 'patience_preset', - 1, 5, (inner_width - 1), 1, - ) - self.patience_preset_combo.set_hexpand(False) - - self.add_label(grid, - _('GPU encoding'), - 0, 6, 1, 1, - ) - - combo_list3 = [ - 'libx264', 'libx265', 'h264_amf', 'hevc_amf', 'h264_nvenc', - 'hevc_nvenc', - ] - - self.gpu_encoding_combo = self.add_combo(grid, - combo_list3, - 'gpu_encoding', - 1, 6, (inner_width - 1), 1, - ) - - self.add_label(grid, - _('Hardware acceleration'), - 0, 7, 1, 1, - ) - - combo_list4 = ['none', 'auto', 'vdpau', 'dxva2', 'vaapi', 'qsv'] - - self.hw_accel_combo = self.add_combo(grid, - combo_list4, - 'hw_accel', - 1, 7, (inner_width - 1), 1, - ) - - # (Signal connects from above) - self.quality_mode_radiobutton.connect( - 'toggled', - self.on_quality_mode_radiobutton_toggled, - ) - self.quality_mode_radiobutton2.connect( - 'toggled', - self.on_quality_mode_radiobutton_toggled, - ) - - self.rate_factor_scale.connect( - 'value-changed', - self.on_rate_factor_scale_changed, - ) - - return grid - - - def setup_settings_tab_gif_grid(self, row, outer_width): - - """Called by self.setup_settings_tab(). - - Creates a supplementary grid, within the tab's outer grid, which can be - swapped in and out as the 'output_mode' option is changed. - - This supplementary grid is visible when 'output_mode' is 'gif'. - - Args: - - row (int): The row on the tab's outer grid, on which the - supplementary grid is to be placed - - outer_width (int): The width of the tab's outer grid - - Return values: - - The new Gtk.Grid(). - - """ - - grid = Gtk.Grid() - if self.retrieve_val('output_mode') == 'gif': - self.settings_grid.attach(grid, 0, row, outer_width, 1) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - self.add_label(grid, - _('Palette:'), - 0, 0, 1, 1, - ) - - self.palette_mode_radiobutton = self.add_radiobutton(grid, - None, - _('Faster') + '\n' \ - + _('Uses dithering to a standard palette provided by FFmpeg') \ - + '\n' + _('Can cause dithering artefacts and slight banding'), - None, - None, - 1, 0, 1, 1, - ) - # (Signal connect appears below) - - self.palette_mode_radiobutton2 = self.add_radiobutton(grid, - self.palette_mode_radiobutton, - _('Better') + '\n' \ - + _('Determines an optimized palette for the video') + '\n' \ - + _('Uses two passes and a temporary file for the palette'), - None, - None, - 1, 1, 1, 1, - ) - if self.retrieve_val('palette_mode') == 'better': - self.palette_mode_radiobutton2.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - self.palette_mode_radiobutton.connect( - 'toggled', - self.on_palette_mode_radiobutton_toggled, - ) - self.palette_mode_radiobutton2.connect( - 'toggled', - self.on_palette_mode_radiobutton_toggled, - ) - - return grid - - - def setup_settings_tab_clip_grid(self, row, outer_width): - - """Called by self.setup_settings_tab(). - - Creates a supplementary grid, within the tab's outer grid, which can be - swapped in and out as the 'output_mode' option is changed. - - This supplementary grid is visible when 'output_mode' is 'split'. - - Args: - - row (int): The row on the tab's outer grid, on which the - supplementary grid is to be placed - - outer_width (int): The width of the tab's outer grid - - Return values: - - The new Gtk.Grid(). - - """ - - grid = Gtk.Grid() - if self.retrieve_val('output_mode') == 'split': - self.settings_grid.attach(grid, 0, row, outer_width, 1) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - grid_width = 4 - - self.split_mode_radiobutton = self.add_radiobutton(grid, - None, - _('Split videos using their own timestamps'), - None, - None, - 0, 0, 1, 1, - ) - # (Signal connect appears below) - - self.split_mode_radiobutton2 = self.add_radiobutton(grid, - self.split_mode_radiobutton, - _('Split videos using these timestamps'), - None, - None, - 1, 0, 1, 1, - ) - if self.retrieve_val('split_mode') == 'custom': - self.split_mode_radiobutton2.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - self.split_mode_radiobutton.connect( - 'toggled', - self.on_split_mode_radiobutton_toggled, - ) - self.split_mode_radiobutton2.connect( - 'toggled', - self.on_split_mode_radiobutton_toggled, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Start'), _('Stop'), _('Clip title') ], - ): - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.split_mode_liststore = Gtk.ListStore(str, str, str) - treeview.set_model(self.split_mode_liststore) - - # Initialise the list - self.setup_settings_tab_update_clip_treeview() - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1) - - # Strip of widgets at the bottom - label = self.add_label(grid2, - _('Start timestamp (e.g. 15:29)'), - 0, 0, 1, 1, - ) - label.set_hexpand(False) - - if self.retrieve_val('split_mode') == 'video': - custom_flag = False - else: - custom_flag = True - - self.start_stamp_entry = self.add_entry(grid2, - None, - 1, 0, 1, 1, - ) - self.start_stamp_entry.set_width_chars(12) - self.start_stamp_entry.set_hexpand(False) - if not custom_flag: - self.start_stamp_entry.set_sensitive(False) - - label2 = self.add_label(grid2, - _('Stop timestamp (optional)'), - 2, 0, 1, 1, - ) - label2.set_hexpand(False) - - self.stop_stamp_entry = self.add_entry(grid2, - None, - 3, 0, 1, 1, - ) - self.stop_stamp_entry.set_width_chars(12) - self.stop_stamp_entry.set_hexpand(False) - if not custom_flag: - self.stop_stamp_entry.set_sensitive(False) - - label3 = self.add_label(grid2, - _('Clip title (optional)'), - 0, 1, 1, 1, - ) - label3.set_hexpand(False) - - self.clip_title_entry = self.add_entry(grid2, - None, - 1, 1, (grid_width - 1), 1, - ) - self.clip_title_entry.set_hexpand(True) - if not custom_flag: - self.clip_title_entry.set_sensitive(False) - - self.add_timestamp_button = Gtk.Button(_('Add timestamp')) - grid2.attach(self.add_timestamp_button, 0, 2, 1, 1) - self.add_timestamp_button.connect( - 'clicked', - self.on_add_timestamp_clicked, - ) - if not custom_flag: - self.add_timestamp_button.set_sensitive(False) - - self.delete_timestamp_button = Gtk.Button(_('Delete timestamp')) - grid2.attach(self.delete_timestamp_button, 1, 2, 1, 1) - self.delete_timestamp_button.connect( - 'clicked', - self.on_delete_timestamp_clicked, - treeview, - ) - if not custom_flag: - self.delete_timestamp_button.set_sensitive(False) - - self.show_prefs_button = Gtk.Button(_('Clip preferences')) - grid2.attach(self.show_prefs_button, 2, 2, 1, 1) - self.show_prefs_button.connect( - 'clicked', - self.on_clip_prefs_clicked, - ) - - self.clear_timestamp_button = Gtk.Button(_('Clear list')) - grid2.attach(self.clear_timestamp_button, 3, 2, 1, 1) - self.clear_timestamp_button.connect( - 'clicked', - self.on_clear_timestamp_clicked, - ) - if not custom_flag: - self.clear_timestamp_button.set_sensitive(False) - - return grid - - - def setup_settings_tab_slice_grid(self, row, outer_width): - - """Called by self.setup_settings_tab(). - - Creates a supplementary grid, within the tab's outer grid, which can be - swapped in and out as the 'output_mode' option is changed. - - This supplementary grid is visible when 'output_mode' is 'slice'. - - Args: - - row (int): The row on the tab's outer grid, on which the - supplementary grid is to be placed - - outer_width (int): The width of the tab's outer grid - - Return values: - - The new Gtk.Grid(). - - """ - - grid = Gtk.Grid() - if self.retrieve_val('output_mode') == 'slice': - self.settings_grid.attach(grid, 0, row, outer_width, 1) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - grid_width = 4 - - self.slice_mode_radiobutton = self.add_radiobutton(grid, - None, - _('Use the videos\' own slice data'), - None, - None, - 0, 0, 1, 1, - ) - # (Signal connect appears below) - - self.slice_mode_radiobutton2 = self.add_radiobutton(grid, - self.slice_mode_radiobutton, - _('Use this slice data'), - None, - None, - 1, 0, 1, 1, - ) - if self.retrieve_val('slice_mode') == 'custom': - self.slice_mode_radiobutton2.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - self.slice_mode_radiobutton.connect( - 'toggled', - self.on_slice_mode_radiobutton_toggled, - ) - self.slice_mode_radiobutton2.connect( - 'toggled', - self.on_slice_mode_radiobutton_toggled, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Category'), _('Action type'), _('Start'), _('Stop') ], - ): - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.slice_mode_liststore = Gtk.ListStore(str, str, str, str) - treeview.set_model(self.slice_mode_liststore) - - # Initialise the list - self.setup_settings_tab_update_slice_treeview() - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1) - - # Strip of widgets at the bottom - label = self.add_label(grid2, - _('Category'), - 0, 0, 1, 1, - ) - label.set_hexpand(False) - - if self.retrieve_val('slice_mode') == 'video': - custom_flag = False - else: - custom_flag = True - - self.category_combo = self.add_combo(grid2, - formats.SPONSORBLOCK_CATEGORY_LIST, - None, - 1, 0, 1, 1, - ) - self.category_combo.set_active(0) - if not custom_flag: - self.category_combo.set_sensitive(False) - - label2 = self.add_label(grid2, - _('Action type'), - 2, 0, 1, 1, - ) - label2.set_hexpand(False) - - self.action_combo = self.add_combo(grid2, - formats.SPONSORBLOCK_ACTION_LIST, - None, - 3, 0, 1, 1, - ) - self.action_combo.set_active(0) - if not custom_flag: - self.action_combo.set_sensitive(False) - - label3 = self.add_label(grid2, - _('Start (timestamp or seconds)'), - 0, 1, 1, 1, - ) - label3.set_hexpand(False) - - self.slice_start_entry = self.add_entry(grid2, - None, - 1, 1, 1, 1, - ) - if not custom_flag: - self.slice_start_entry.set_sensitive(False) - - label4 = self.add_label(grid2, - _('Stop (optional)'), - 2, 1, 1, 1, - ) - label4.set_hexpand(False) - - self.slice_stop_entry = self.add_entry(grid2, - None, - 3, 1, 1, 1, - ) - if not custom_flag: - self.slice_stop_entry.set_sensitive(False) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid3 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - self.add_slice_button = Gtk.Button(_('Add slice')) - grid3.attach(self.add_slice_button, 0, 0, 1, 1) - self.add_slice_button.set_hexpand(True) - self.add_slice_button.connect( - 'clicked', - self.on_add_slice_clicked, - ) - if not custom_flag: - self.add_slice_button.set_sensitive(False) - - self.delete_slice_button = Gtk.Button(_('Delete sliuce')) - grid3.attach(self.delete_slice_button, 1, 0, 1, 1) - self.delete_slice_button.set_hexpand(True) - self.delete_slice_button.connect( - 'clicked', - self.on_delete_slice_clicked, - treeview, - ) - if not custom_flag: - self.delete_slice_button.set_sensitive(False) - - self.show_settings_button = Gtk.Button(_('SponsorBlock settings')) - grid3.attach(self.show_settings_button, 2, 0, 1, 1) - self.show_settings_button.set_hexpand(True) - self.show_settings_button.connect( - 'clicked', - self.on_slice_settings_clicked, - ) - - self.clear_slice_button = Gtk.Button(_('Clear list')) - grid3.attach(self.clear_slice_button, 3, 0, 1, 1) - self.clear_slice_button.set_hexpand(True) - self.clear_slice_button.connect( - 'clicked', - self.on_clear_slice_clicked, - ) - if not custom_flag: - self.clear_slice_button.set_sensitive(False) - - return grid - - - def setup_settings_tab_update_clip_treeview(self): - - """ Called by self.setup_settings_tab_clip_grid(). - - Fills or updates the treeview. - """ - - self.split_mode_liststore.clear() - - # Add each timestamp/clip title to the treeview, one row at a time - for mini_list in self.retrieve_val('split_list'): - - start_stamp = mini_list[0] - - if mini_list[1] is None: - stop_stamp = '' - else: - stop_stamp = mini_list[1] - - if mini_list[2] is None: - clip_title = '' - else: - clip_title = mini_list[1] - - self.split_mode_liststore.append( - [ start_stamp, stop_stamp, clip_title ], - ) - - - def setup_settings_tab_update_slice_treeview(self): - - """ Called by self.setup_settings_tab_slice_grid(). - - Fills or updates the treeview. - """ - - self.slice_mode_liststore.clear() - - # Add slice data to the treeview, one row at a time - for mini_dict in self.retrieve_val('slice_list'): - - self.slice_mode_liststore.append( - [ - mini_dict['category'], - mini_dict['action'], - str(mini_dict['start_time']), - str(mini_dict['stop_time']), - ], - ) - - - def setup_settings_tab_merge_grid(self, row, outer_width): - - """Called by self.setup_settings_tab(). - - Creates a supplementary grid, within the tab's outer grid, which can be - swapped in and out as the 'output_mode' option is changed. - - This supplementary grid is visible when 'output_mode' is 'merge'. - - Args: - - row (int): The row on the tab's outer grid, on which the - supplementary grid is to be placed - - outer_width (int): The width of the tab's outer grid - - Return values: - - The new Gtk.Grid(). - - """ - - grid = Gtk.Grid() - if self.retrieve_val('output_mode') == 'merge': - self.settings_grid.attach(grid, 0, row, outer_width, 1) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - self.add_label(grid, - '' + _( - 'This merges a video and audio file with the same name' \ - + ' into a single video file,\nusing the extension' \ - + ' specified in the File tab', - ) + '', - 0, 0, 1, 1, - ) - - return grid - - - def setup_settings_tab_thumb_grid(self, row, outer_width): - - """Called by self.setup_settings_tab(). - - Creates a supplementary grid, within the tab's outer grid, which can be - swapped in and out as the 'output_mode' option is changed. - - This supplementary grid is visible when 'output_mode' is 'thumb'. - - Args: - - row (int): The row on the tab's outer grid, on which the - supplementary grid is to be placed - - outer_width (int): The width of the tab's outer grid - - Return values: - - The new Gtk.Grid(). - - """ - - grid = Gtk.Grid() - if self.retrieve_val('output_mode') == 'thumb': - self.settings_grid.attach(grid, 0, row, outer_width, 1) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - self.add_label(grid, - '' + _( - 'The thumbnail\'s format can be changed in the File tab', - ) + '', - 0, 0, 1, 1, - ) - - return grid - - - def setup_optimise_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Optimisations' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: FFmpeg options > Optimisations' - ) - - tab, grid = self.add_notebook_tab(_('_Optimisations')) - grid_width = 2 - - self.seek_flag_checkbutton = self.add_checkbutton(grid, - _( - 'Optimise for fast seeking (shorter keyframe interval, about' \ - + ' 10% larger file)', - ), - 'seek_flag', - 0, 0, grid_width, 1, - ) - - self.tuning_film_flag_checkbutton = self.add_checkbutton(grid, - _('Input video is a high-quality movie'), - 'tuning_film_flag', - 0, 1, grid_width, 1, - ) - - self.tuning_animation_flag_checkbutton = self.add_checkbutton(grid, - _('Input video is an animated movie'), - 'tuning_animation_flag', - 0, 2, grid_width, 1, - ) - - self.tuning_grain_flag_checkbutton = self.add_checkbutton(grid, - _('Input video contains film grain'), - 'tuning_grain_flag', - 0, 3, grid_width, 1, - ) - - self.tuning_still_image_flag_checkbutton = self.add_checkbutton(grid, - _('Input video is an image slideshow'), - 'tuning_still_image_flag', - 0, 4, grid_width, 1, - ) - - self.tuning_fast_decode_flag_checkbutton = self.add_checkbutton(grid, - _('Optimise for really weak CPU playback devices'), - 'tuning_fast_decode_flag', - 0, 5, grid_width, 1, - ) - - self.profile_flag_checkbutton = self.add_checkbutton(grid, - _( - 'Optimise for really old devices (requires rate factor' \ - + ' above 0)', - ), - 'profile_flag', - 0, 6, grid_width, 1, - ) - if not self.retrieve_val('rate_factor'): - self.profile_flag_checkbutton.set_sensitive(False) - - self.fast_start_flag_checkbutton = self.add_checkbutton(grid, - _( - 'Move headers to beginning of file (so it can play while' \ - + ' still downloading)', - ), - 'fast_start_flag', - 0, 7, grid_width, 1, - ) - - self.tuning_zero_latency_flag_checkbutton = self.add_checkbutton(grid, - _('Fast encoding and low latency streaming'), - 'tuning_zero_latency_flag', - 0, 8, grid_width, 1, - ) - - self.limit_flag_checkbutton = self.add_checkbutton(grid, - _('Limit bitrate (Mbit/s)'), - None, - 0, 9, 1, 1, - ) - if self.retrieve_val('limit_flag'): - self.limit_flag_checkbutton.set_active(True) - # (Signal connect appears below) - - self.limit_mbps_spinbutton = self.add_spinbutton(grid, - 0, None, 0.2, - 'limit_mbps', - 1, 9, 1, 1, - ) - if not self.retrieve_val('limit_flag'): - self.limit_mbps_spinbutton.set_sensitive(False) - - self.add_label(grid, - ' ' + _('Assuming a receiving buffer (seconds)'), - 0, 10, 1, 1, - ) - - self.limit_buffer_spinbutton = self.add_spinbutton(grid, - 0, None, 0.2, - 'limit_buffer', - 1, 10, 1, 1, - ) - if not self.retrieve_val('limit_flag'): - self.limit_buffer_spinbutton.set_sensitive(False) - - # (De)sensitise all of these widgets, depending on the value of the - # 'output_mode' setting - if self.retrieve_val('output_mode') == 'h264': - self.setup_optimise_tab_set_sensitive(True) - else: - self.setup_optimise_tab_set_sensitive(False) - - # (Signal connects from above) - self.limit_flag_checkbutton.connect( - 'toggled', - self.on_limit_flag_checkbutton_toggled, - ) - - - def setup_clips_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Clips' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: FFmpeg options > Clips' - ) - - tab, grid = self.add_notebook_tab(_('_Clips')) - grid_width = 2 - - output_mode = self.retrieve_val('output_mode') - split_mode = self.retrieve_val('split_mode') - - # N.B. I tried moving the equivalent code from - # self.setup_settings_tab_clip_grid() into a function, that could - # also be called from here, but that created too many complications - # However, both calling functions will use the same set of callbacks - - self.simple_split_mode_checkbutton = self.add_checkbutton(grid, - _('Split the video(s) to create video clips'), - None, - 0, 0, 1, 1, - ) - if output_mode == 'split': - self.simple_split_mode_checkbutton.set_active(True) - # (Signal connect appears below) - - self.simple_split_mode_radiobutton = self.add_radiobutton(grid, - None, - _('Split videos using their own timestamps'), - None, - None, - 1, 0, 1, 1, - ) - if output_mode != 'split': - self.simple_split_mode_radiobutton.set_sensitive(False) - # (Signal connect appears below) - - self.simple_split_mode_radiobutton2 = self.add_radiobutton(grid, - self.simple_split_mode_radiobutton, - _('Split videos using these timestamps'), - None, - None, - 1, 1, 1, 1, - ) - if output_mode != 'split': - self.simple_split_mode_radiobutton2.set_sensitive(False) - if split_mode == 'custom': - self.simple_split_mode_radiobutton2.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - self.simple_split_mode_checkbutton.connect( - 'toggled', - self.on_simple_split_toggled, - ) - self.simple_split_mode_radiobutton.connect( - 'toggled', - self.on_split_mode_radiobutton_toggled, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 2, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Start'), _('Stop'), _('Clip title') ], - ): - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.simple_split_mode_liststore = Gtk.ListStore(str, str, str) - treeview.set_model(self.simple_split_mode_liststore) - - # Initialise the list - self.setup_clips_tab_update_treeview() - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - # Strip of widgets at the bottom - label = self.add_label(grid2, - _('Start timestamp (e.g. 15:29)'), - 0, 0, 1, 1, - ) - label.set_hexpand(False) - - if split_mode == 'video': - custom_flag = False - else: - custom_flag = True - - self.simple_start_stamp_entry = self.add_entry(grid2, - None, - 1, 0, 1, 1, - ) - self.simple_start_stamp_entry.set_width_chars(12) - self.simple_start_stamp_entry.set_hexpand(False) - if not custom_flag: - self.simple_start_stamp_entry.set_sensitive(False) - - label2 = self.add_label(grid2, - _('Stop timestamp (optional)'), - 2, 0, 1, 1, - ) - label2.set_hexpand(False) - - self.simple_stop_stamp_entry = self.add_entry(grid2, - None, - 3, 0, 1, 1, - ) - self.simple_stop_stamp_entry.set_width_chars(12) - self.simple_stop_stamp_entry.set_hexpand(False) - if not custom_flag: - self.simple_stop_stamp_entry.set_sensitive(False) - - label3 = self.add_label(grid2, - _('Clip title (optional)'), - 0, 1, 1, 1, - ) - label3.set_hexpand(False) - - self.simple_clip_title_entry = self.add_entry(grid2, - None, - 1, 1, (grid_width - 1), 1, - ) - self.simple_clip_title_entry.set_hexpand(True) - if not custom_flag: - self.simple_clip_title_entry.set_sensitive(False) - - self.simple_add_timestamp_button = Gtk.Button(_('Add timestamp')) - grid2.attach(self.simple_add_timestamp_button, 0, 2, 1, 1) - self.simple_add_timestamp_button.connect( - 'clicked', - self.on_add_timestamp_clicked, - ) - if not custom_flag: - self.simple_add_timestamp_button.set_sensitive(False) - - self.simple_delete_timestamp_button = Gtk.Button(_('Delete timestamp')) - grid2.attach(self.simple_delete_timestamp_button, 1, 2, 1, 1) - self.simple_delete_timestamp_button.connect( - 'clicked', - self.on_delete_timestamp_clicked, - treeview, - ) - if not custom_flag: - self.simple_delete_timestamp_button.set_sensitive(False) - - self.simple_show_prefs_button = Gtk.Button(_('Clip preferences')) - grid2.attach(self.simple_show_prefs_button, 2, 2, 1, 1) - self.simple_show_prefs_button.connect( - 'clicked', - self.on_clip_prefs_clicked, - ) - - self.simple_clear_timestamp_button = Gtk.Button(_('Clear list')) - grid2.attach(self.simple_clear_timestamp_button, 3, 2, 1, 1) - self.simple_clear_timestamp_button.connect( - 'clicked', - self.on_clear_timestamp_clicked, - ) - if not custom_flag: - self.simple_clear_timestamp_button.set_sensitive(False) - - - def setup_clips_tab_update_treeview(self): - - """ Called by self.setup_clips_tab(). - - Fills or updates the treeview. - """ - - self.simple_split_mode_liststore.clear() - - # Add each timestamp/title to the treeview, one row at a time - for mini_list in self.retrieve_val('split_list'): - - start_stamp = mini_list[0] - - if mini_list[1] is None: - stop_stamp = '' - else: - stop_stamp = mini_list[1] - - if mini_list[2] is None: - clip_title = '' - else: - clip_title = mini_list[1] - - self.simple_split_mode_liststore.append( - [ start_stamp, stop_stamp, clip_title ], - ) - - - def setup_slices_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Slices' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: FFmpeg options > Slices' - ) - - tab, grid = self.add_notebook_tab(_('_Slices')) - grid_width = 2 - - output_mode = self.retrieve_val('output_mode') - slice_mode = self.retrieve_val('split_mode') - - self.simple_slice_mode_checkbutton = self.add_checkbutton(grid, - _('Remove slices from the video(s)'), - None, - 0, 0, 1, 1, - ) - if output_mode == 'slice': - self.simple_slice_mode_checkbutton.set_active(True) - # (Signal connect appears below) - - self.simple_slice_mode_radiobutton = self.add_radiobutton(grid, - None, - _('Use the videos\' own slice data'), - None, - None, - 1, 0, 1, 1, - ) - if output_mode != 'slice': - self.simple_slice_mode_radiobutton.set_sensitive(False) - # (Signal connect appears below) - - self.simple_slice_mode_radiobutton2 = self.add_radiobutton(grid, - self.simple_slice_mode_radiobutton, - _('Use this slice data'), - None, - None, - 1, 1, 1, 1, - ) - if output_mode != 'slice': - self.simple_slice_mode_radiobutton2.set_sensitive(False) - if slice_mode == 'custom': - self.simple_slice_mode_radiobutton2.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - self.simple_slice_mode_checkbutton.connect( - 'toggled', - self.on_simple_slice_toggled, - ) - self.simple_slice_mode_radiobutton.connect( - 'toggled', - self.on_slice_mode_radiobutton_toggled, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 2, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Category'), _('Action type'), _('Start'), _('Stop') ], - ): - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.simple_slice_mode_liststore = Gtk.ListStore(str, str, str, str) - treeview.set_model(self.simple_slice_mode_liststore) - - # Initialise the list - self.setup_slices_tab_update_treeview() - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - # Strip of widgets at the bottom - label = self.add_label(grid2, - _('Category'), - 0, 0, 1, 1, - ) - label.set_hexpand(False) - - if slice_mode == 'video': - custom_flag = False - else: - custom_flag = True - - self.simple_category_combo = self.add_combo(grid2, - formats.SPONSORBLOCK_CATEGORY_LIST, - None, - 1, 0, 1, 1, - ) - self.simple_category_combo.set_active(0) - if not custom_flag: - self.simple_category_combo.set_sensitive(False) - - label2 = self.add_label(grid2, - _('Action type'), - 2, 0, 1, 1, - ) - label2.set_hexpand(False) - - self.simple_action_combo = self.add_combo(grid2, - formats.SPONSORBLOCK_ACTION_LIST, - None, - 3, 0, 1, 1, - ) - self.simple_action_combo.set_active(0) - if not custom_flag: - self.simple_action_combo.set_sensitive(False) - - label3 = self.add_label(grid2, - _('Start (timestamp or seconds)'), - 0, 1, 1, 1, - ) - label3.set_hexpand(False) - - self.simple_slice_start_entry = self.add_entry(grid2, - None, - 1, 1, 1, 1, - ) - if not custom_flag: - self.simple_slice_start_entry.set_sensitive(False) - - label4 = self.add_label(grid2, - _('Stop (optional)'), - 2, 1, 1, 1, - ) - label4.set_hexpand(False) - - self.simple_slice_stop_entry = self.add_entry(grid2, - None, - 3, 1, 1, 1, - ) - if not custom_flag: - self.simple_slice_stop_entry.set_sensitive(False) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid3 = self.add_secondary_grid(grid, 0, 4, grid_width, 1) - - self.simple_add_slice_button = Gtk.Button(_('Add slice')) - grid3.attach(self.simple_add_slice_button, 0, 2, 1, 1) - self.simple_add_slice_button.set_hexpand(True) - self.simple_add_slice_button.connect( - 'clicked', - self.on_add_slice_clicked, - ) - if not custom_flag: - self.simple_add_slice_button.set_sensitive(False) - - self.simple_delete_slice_button = Gtk.Button(_('Delete slice')) - grid3.attach(self.simple_delete_slice_button, 1, 2, 1, 1) - self.simple_delete_slice_button.set_hexpand(True) - self.simple_delete_slice_button.connect( - 'clicked', - self.on_delete_slice_clicked, - treeview, - ) - if not custom_flag: - self.simple_delete_slice_button.set_sensitive(False) - - self.simple_show_settings_button \ - = Gtk.Button(_('SponsorBlock settings')) - grid3.attach(self.simple_show_settings_button, 2, 2, 1, 1) - self.simple_show_settings_button.set_hexpand(True) - self.simple_show_settings_button.connect( - 'clicked', - self.on_slice_settings_clicked, - ) - - self.simple_clear_slice_button = Gtk.Button(_('Clear list')) - grid3.attach(self.simple_clear_slice_button, 3, 2, 1, 1) - self.simple_clear_slice_button.set_hexpand(True) - self.simple_clear_slice_button.connect( - 'clicked', - self.on_clear_slice_clicked, - ) - if not custom_flag: - self.simple_clear_slice_button.set_sensitive(False) - - - def setup_slices_tab_update_treeview(self): - - """ Called by self.setup_slices_tab(). - - Fills or updates the treeview. - """ - - self.simple_slice_mode_liststore.clear() - - # Add slice data to the treeview, one row at a time - for mini_dict in self.retrieve_val('slice_list'): - - if 'category' in mini_dict: - category = mini_dict['category'] - else: - category = 'n/a' - - if 'action' in mini_dict: - action = mini_dict['action'] - else: - action = 'n/a' - - if 'start_time' in mini_dict: - start_time = mini_dict['start_time'] - else: - start_time = 'n/a' - - if 'stop_time' in mini_dict \ - and mini_dict['stop_time'] is not None: - stop_time = mini_dict['stop_time'] - else: - stop_time = 'n/a' - - self.simple_slice_mode_liststore.append( - [ category, action, str(start_time), str(stop_time) ], - ) - - - def setup_optimise_tab_set_sensitive(self, sens_flag): - - """Called by self.setup_optimise_tab() and various callbacks. - - (De)sensitises all widgets in the tab, as required. - - Args: - - sens_flag (bool): True to sensitise widgets, False to desensitise - them - - """ - - self.seek_flag_checkbutton.set_sensitive(sens_flag) - self.tuning_film_flag_checkbutton.set_sensitive(sens_flag) - self.tuning_animation_flag_checkbutton.set_sensitive(sens_flag) - self.tuning_grain_flag_checkbutton.set_sensitive(sens_flag) - self.tuning_still_image_flag_checkbutton.set_sensitive(sens_flag) - self.tuning_fast_decode_flag_checkbutton.set_sensitive(sens_flag) - - if not self.retrieve_val('rate_factor'): - self.profile_flag_checkbutton.set_sensitive(False) - else: - self.profile_flag_checkbutton.set_sensitive(sens_flag) - - self.fast_start_flag_checkbutton.set_sensitive(sens_flag) - - self.limit_flag_checkbutton.set_sensitive(sens_flag) - self.tuning_zero_latency_flag_checkbutton.set_sensitive(sens_flag) - - if not self.retrieve_val('limit_flag'): - self.limit_mbps_spinbutton.set_sensitive(False) - else: - self.limit_mbps_spinbutton.set_sensitive(sens_flag) - - if not self.retrieve_val('limit_flag'): - self.limit_buffer_spinbutton.set_sensitive(False) - else: - self.limit_buffer_spinbutton.set_sensitive(sens_flag) - - - def setup_videos_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Videos' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: FFmpeg options > Videos' - ) - - tab, grid = self.add_notebook_tab(_('_Videos')) - grid_width = 2 - - # List of videos to be processed - self.add_label(grid, - '' + _('List of videos to be processed') + '', - 0, 0, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ '#', _('Video'), _('Thumbnail'), _('Name') ] - ): - if i == 1 or i == 2: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.video_liststore = Gtk.ListStore(str, bool, bool, str) - treeview.set_model(self.video_liststore) - - # Allow drag and drop from the Video Catalogue, or an external - # application, hoping to receive full paths to a video/audio file - # and/or URLs, which are associated with a media.Video object - scrolled.connect( - 'drag-data-received', - self.on_video_drag_data_received, - ) - # (Without this line, we get Gtk warnings on some systems) - scrolled.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - # (Continuing) - scrolled.drag_dest_set_target_list(None) - scrolled.drag_dest_add_text_targets() - - # Initialise the list - self.setup_videos_tab_update_treeview() - - # Add editing buttons - button = Gtk.Button() - grid.attach(button, 0, 2, 1, 1) - button.set_label(_('Show video properties and timestamps')) - button.connect( - 'clicked', - self.on_video_show_button_clicked, - treeview, - ) - - button2 = Gtk.Button() - grid.attach(button2, 1, 2, 1, 1) - button2.set_label(_('Remove video from list')) - button2.connect( - 'clicked', - self.on_video_remove_button_clicked, - treeview, - ) - - - def setup_videos_tab_update_treeview(self): - - """Called by self.setup_videos_tab(). - - Fills or updates the treeview. - """ - - self.video_liststore.clear() - - # Sort the video list by .dbid (so the Videos tab looks nice) - self.video_list.sort(key=lambda x: x.dbid) - - # Add a row for each video in the list - for video_obj in self.video_list: - self.setup_videos_tab_add_row(video_obj) - - - def setup_videos_tab_add_row(self, video_obj): - - """Called by self.setup_videos_tab_update_treeview(). - - Adds a row to the treeview for a specified media.Video object. - - Args: - - video_obj (media.Video): The video to add - - """ - - if utils.find_thumbnail(self.app_obj, video_obj): - thumb_flag = True - else: - thumb_flag = True - - if video_obj.dummy_flag: - - # Special case: 'dummy' video objects (those downloaded in the - # Classic Mode tab) use different IVs - if video_obj.dummy_path is not None \ - and os.path.isfile(video_obj.dummy_path): - dl_flag = True - else: - dl_flag = False - - self.video_liststore.append( - [ - 'n/a', - dl_flag, - thumb_flag, - video_obj.dummy_path, - ], - ) - - else: - - # All other media.Video objects - self.video_liststore.append( - [ - str(video_obj.dbid), - video_obj.dl_flag, - thumb_flag, - video_obj.name, - ], - ) - - - # (Tab support functions) - - - def update_system_cmd(self): - - """Called after any widget is manipulated. - - Updates the contents of the textview showing a specimen system command, - incorporating the modified value. - """ - - # This particular call returns a list inside a tuple, for no obvious - # reason (and an identical call from ProcessManager.process_video() - # does not) - # Don't know why, but the FFmpeg system command, as a list, is at - # [0][2]) - result_list = self.edit_obj.get_system_cmd( - self.app_obj, - None, # Use a specimen source file - None, # ...and specimen timestamps - None, - None, - None, - self.edit_dict, - ), - - if not result_list: - self.results_textbuffer.set_text('') - - else: - text = ' '.join(result_list[0][2]) - if self.retrieve_val('output_mode') == 'slice': - - # Show the concatenation command on a second line - concat_list = [ - self.app_obj.ffmpeg_manager_obj.get_executable(), - '-safe', - '0', - '-f', - 'concat', - '-i', - 'clips.txt', - '-c', - 'copy', - result_list[0][2][-1], # Path to the output file - ] - - text += '\n' + ' '.join(concat_list) - - self.results_textbuffer.set_text(text) - - - # Callback class methods - - - def on_add_slice_clicked(self, button): - - """Called from a callback in self.setup_settings_tab_slice_grid(). - - In simple mode, called from a callback in self.setup_slices_tab(). - - Adds a new slice to the video's slice list. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' FFmpeg options > Slices' - ) - - if not self.app_obj.simple_ffmpeg_options_flag: - - tree_iter = self.category_combo.get_active_iter() - model = self.category_combo.get_model() - category = model[tree_iter][0] - - tree_iter2 = self.action_combo.get_active_iter() - model2 = self.action_combo.get_model() - action_type = model2[tree_iter2][0] - - start_time = utils.strip_whitespace( - self.slice_start_entry.get_text(), - ) - - stop_time = utils.strip_whitespace( - self.slice_stop_entry.get_text(), - ) - - else: - - tree_iter = self.simple_category_combo.get_active_iter() - model = self.simple_category_combo.get_model() - category = model[tree_iter][0] - - tree_iter2 = self.simple_action_combo.get_active_iter() - model2 = self.simple_action_combo.get_model() - action_type = model2[tree_iter2][0] - - start_time = utils.strip_whitespace( - self.simple_slice_start_entry.get_text(), - ) - - stop_time = utils.strip_whitespace( - self.simple_slice_stop_entry.get_text(), - ) - - # Do nothing if specified timestamps aren't valid ('stop_time' is NOT - # optional) - start_time = float( - utils.timestamp_convert_to_seconds(self.app_obj, start_time), - ) - - if stop_time == '': - stop_time = None - else: - stop_time = float( - utils.timestamp_convert_to_seconds(self.app_obj, stop_time), - ) - - try: - ignore = float(start_time) - if stop_time is not None: - ignore = float(stop_time) - - except: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid start/stop times'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - if stop_time is not None and stop_time <= start_time: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid start/stop times'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Compile the mini-dictionary in the format described by - # media.Video.__init__() - mini_dict = { - 'category': category, - 'action': action_type, - 'start_time': start_time, - 'stop_time': stop_time, - 'duration': 0, - } - - # Add it to the list - slice_list = self.retrieve_val('slice_list') - slice_list.append(mini_dict) - slice_list = list(sorted(slice_list, key=lambda x:x['start_time'])) - self.edit_dict['slice_list'] = slice_list - - # Show changes, and empty entry boxes - if not self.app_obj.simple_ffmpeg_options_flag: - - self.setup_settings_tab_update_slice_treeview() - self.slice_start_entry.set_text('') - self.slice_stop_entry.set_text('') - - else: - - self.setup_slices_tab_update_treeview() - self.simple_slice_start_entry.set_text('') - self.simple_slice_stop_entry.set_text('') - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_add_timestamp_clicked(self, button): - - """Called from a callback in self.setup_settings_tab_clip_grid(). - - In simple mode, called from a callback in self.setup_clips_tab(). - - Adds a new timestamp to the video's timestamp list, optionally with a - clip title. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' FFmpeg options > Clips' - ) - - if not self.app_obj.simple_ffmpeg_options_flag: - - start_stamp = utils.strip_whitespace( - self.start_stamp_entry.get_text(), - ) - stop_stamp = utils.strip_whitespace( - self.stop_stamp_entry.get_text(), - ) - clip_title = utils.strip_whitespace( - self.clip_title_entry.get_text(), - ) - - else: - - start_stamp = utils.strip_whitespace( - self.simple_start_stamp_entry.get_text(), - ) - stop_stamp = utils.strip_whitespace( - self.simple_stop_stamp_entry.get_text(), - ) - clip_title = utils.strip_whitespace( - self.simple_clip_title_entry.get_text(), - ) - - # (Values are stored as None, rather than empty strings) - if stop_stamp == '': - stop_stamp = None - - if clip_title == '': - clip_title = None - - # Do nothing if specified timestamps aren't valid ('stop_stamp' is - # optional) - regex = '^' + self.app_obj.timestamp_regex + '$' - if re.search(regex, start_stamp) \ - and (stop_stamp is None or re.search(regex, stop_stamp)) \ - and utils.timestamp_compare(self.app_obj, start_stamp, stop_stamp): - - # Add leading zeroes to the minutes and seconds components, so - # that .stamp_list gets sorted correctly (and doesn't look - # weird) - start_stamp = utils.timestamp_format(self.app_obj, start_stamp) - if stop_stamp is not None: - stop_stamp = utils.timestamp_format(self.app_obj, stop_stamp) - - # Timestamps stored in groups of three, in the form - # (start_stamp, stop_stamp, clip_title) - # If a group with the same 'start_stamp' timestamp already exists, - # don't replace it; allow duplicates (as the user may actually - # want that) - split_list = self.retrieve_val('split_list') - split_list.append([ start_stamp, stop_stamp, clip_title ]) - split_list.sort() - self.edit_dict['split_list'] = split_list - - # (Show changes, and update entry boxes. 'stop_stamp', if - # specified, becomes 'start_stamp' for the next group) - if not self.app_obj.simple_ffmpeg_options_flag: - - self.setup_settings_tab_update_clip_treeview() - - if stop_stamp is not None: - self.start_stamp_entry.set_text( - utils.timestamp_add_second(self.app_obj, stop_stamp), - ) - else: - self.start_stamp_entry.set_text('') - - self.stop_stamp_entry.set_text('') - self.clip_title_entry.set_text('') - - else: - - self.setup_clips_tab_update_treeview() - if stop_stamp is not None: - self.simple_start_stamp_entry.set_text( - utils.timestamp_add_second(self.app_obj, stop_stamp), - ) - else: - self.simple_start_stamp_entry.set_text('') - - self.simple_stop_stamp_entry.set_text('') - self.simple_clip_title_entry.set_text('') - - else: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid timestamp(s)'), - 'error', - 'ok', - self, # Parent window is this window - ) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_audio_flag_checkbutton_toggled(self, checkbutton): - - """Called by callback in self.setup_settings_tab(). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if not checkbutton.get_active(): - - self.edit_dict['audio_flag'] = False - - self.audio_bitrate_spinbutton.set_sensitive(False) - - else: - - self.edit_dict['audio_flag'] = True - - if self.retrieve_val('input_mode') == 'video': - self.audio_bitrate_spinbutton.set_sensitive(True) - else: - self.audio_bitrate_spinbutton.set_sensitive(False) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_change_file_ext_entry_changed(self, entry): - - """Called by callback in self.setup_file_tab(). - - Args: - - entry (Gtk.Entry): The widget clicked - - """ - - value = entry.get_text() - - self.edit_dict['change_file_ext'] = value - if value == '': - - self.delete_original_flag_checkbutton.set_active(False) - self.delete_original_flag_checkbutton.set_sensitive(False) - - else: - self.delete_original_flag_checkbutton.set_sensitive(True) - - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_clear_slice_clicked(self, button): - - """Called from a callback in self.setup_settings_tab_slice_grid(). - - Empties the slice list. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.edit_dict['slice_list'] = [] - if not self.app_obj.simple_ffmpeg_options_flag: - self.setup_settings_tab_update_slice_treeview() - else: - self.setup_slices_tab_update_treeview() - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_clear_timestamp_clicked(self, button): - - """Called from a callback in self.setup_settings_tab_clip_grid(). - - Empties the timestamp list. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.edit_dict['split_list'] = [] - if not self.app_obj.simple_ffmpeg_options_flag: - self.setup_settings_tab_update_clip_treeview() - else: - self.setup_clips_tab_update_treeview() - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_clip_prefs_clicked(self, button): - - """Called from a callback in self.setup_settings_tab_clip_grid() and - .setup_clips_tab(). - - Opens the preferences window to show clip settings. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - SystemPrefWin(self.app_obj, 'clips') - - - def on_clone_options_clicked(self, button): - - """Called by callback in self.setup_name_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' FFmpeg options > Name' - ) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'This procedure cannot be reversed. Are you sure you want to' \ - + ' continue?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'clone_ffmpeg_options_from_window', - 'data': [self, self.edit_obj], - }, - ) - - - def on_delete_slice_clicked(self, button, treeview): - - """Called from a callback in self.setup_settings_tab_slice_grid() and - .setup_slices_tab(). - - Deletes the selected slices from the slice list. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the slice list - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - category = model[this_iter][0] - action_type = model[this_iter][1] - start_time = float(model[this_iter][2]) - stop_time = float(model[this_iter][3]) - - # Slices are stored as a list of mini-dictionaries, in the form - # described by media.Video.__init__() - # Walk the list, and delete the first matching mini-dictionary - slice_list = self.retrieve_val('slice_list') - mod_list = [] - match_flag = False - - for mini_dict in slice_list: - - if not match_flag \ - and mini_dict['category'] == category \ - and mini_dict['action'] == action_type \ - and mini_dict['start_time'] == start_time \ - and mini_dict['stop_time'] == stop_time: - match_flag = True # Delete this one - else: - mod_list.append(mini_dict) - - self.edit_dict['slice_list'] = mod_list - - # (Show changes) - if not self.app_obj.simple_ffmpeg_options_flag: - self.setup_settings_tab_update_slice_treeview() - else: - self.setup_slices_tab_update_treeview() - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_delete_timestamp_clicked(self, button, treeview): - - """Called from a callback in self.setup_settings_tab_clip_grid() and - .setup_clips_tab(). - - Deletes the selected timestamps from the timestamp list. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the timestamp list - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - start_stamp = model[this_iter][0] - stop_stamp = model[this_iter][1] - clip_title = model[this_iter][2] - - # Timestamps stored in groups of three, in the form - # (start_stamp, stop_stamp, clip_title) - # Walk the list, and delete the first matchng group - split_list = self.retrieve_val('split_list') - mod_list = [] - match_flag = False - - for mini_list in split_list: - - if not match_flag \ - and mini_list[0] == start_stamp \ - and mini_list[1] == stop_stamp \ - and mini_list[2] == clip_title: - match_flag = True # Delete this one - else: - mod_list.append(mini_list) - - self.edit_dict['split_list'] = mod_list - - # (Show changes) - if not self.app_obj.simple_ffmpeg_options_flag: - self.setup_settings_tab_update_clip_treeview() - else: - self.setup_clips_tab_update_treeview() - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_input_mode_radiobutton_toggled(self, radiobutton): - - """Called by callback in self.setup_settings_tab(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if self.input_mode_radiobutton.get_active(): - - self.edit_dict['input_mode'] = 'video' - - self.output_mode_radiobutton.set_active(True) - self.output_mode_radiobutton.set_sensitive(True) - self.output_mode_radiobutton2.set_sensitive(True) - self.output_mode_radiobutton3.set_sensitive(True) - self.output_mode_radiobutton4.set_sensitive(True) - self.output_mode_radiobutton5.set_sensitive(False) - - self.audio_flag_checkbutton.set_sensitive(True) - if not self.retrieve_val('audio_flag'): - self.audio_bitrate_spinbutton.set_sensitive(False) - else: - self.audio_bitrate_spinbutton.set_sensitive(True) - - else: - - self.edit_dict['input_mode'] = 'thumb' - - self.output_mode_radiobutton4.set_active(True) - self.output_mode_radiobutton.set_sensitive(False) - self.output_mode_radiobutton2.set_sensitive(False) - self.output_mode_radiobutton3.set_sensitive(False) - self.output_mode_radiobutton4.set_sensitive(False) - self.output_mode_radiobutton5.set_sensitive(True) - - self.audio_flag_checkbutton.set_sensitive(False) - self.audio_bitrate_spinbutton.set_sensitive(False) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_limit_flag_checkbutton_toggled(self, checkbutton): - - """Called by callback in self.setup_optimise_tab(). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if not checkbutton.get_active(): - - self.edit_dict['limit_flag'] = False - - self.limit_mbps_spinbutton.set_sensitive(False) - self.limit_buffer_spinbutton.set_sensitive(False) - - else: - - self.edit_dict['audio_flag'] = True - - self.limit_mbps_spinbutton.set_sensitive(True) - self.limit_buffer_spinbutton.set_sensitive(True) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_output_mode_radiobutton_toggled(self, radiobutton, grid_width): - - """Called by callback in self.setup_settings_tab(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - grid_width (int): The width of self.settings_grid - - """ - - old_value = self.retrieve_val('output_mode') - if old_value == 'h264': - self.settings_grid.remove(self.h264_grid) - elif old_value == 'gif': - self.settings_grid.remove(self.gif_grid) - elif old_value == 'split': - self.settings_grid.remove(self.clip_grid) - elif old_value == 'slice': - self.settings_grid.remove(self.slice_grid) - elif old_value == 'merge': - self.settings_grid.remove(self.merge_grid) - else: - self.settings_grid.remove(self.thumb_grid) - - if self.output_mode_radiobutton.get_active(): - - self.edit_dict['output_mode'] = 'h264' - - self.settings_grid.attach(self.h264_grid, 0, 2, grid_width, 1) - self.setup_file_tab_set_sensitive(True) - self.setup_optimise_tab_set_sensitive(True) - - elif self.output_mode_radiobutton2.get_active(): - - self.edit_dict['output_mode'] = 'gif' - - self.settings_grid.attach(self.gif_grid, 0, 2, grid_width, 1) - self.setup_file_tab_set_sensitive(True) - self.setup_optimise_tab_set_sensitive(False) - - elif self.output_mode_radiobutton3.get_active(): - - self.edit_dict['output_mode'] = 'split' - - self.settings_grid.attach(self.clip_grid, 0, 2, grid_width, 1) - self.setup_file_tab_set_sensitive(False) - self.setup_optimise_tab_set_sensitive(False) - - elif self.output_mode_radiobutton4.get_active(): - - self.edit_dict['output_mode'] = 'slice' - - self.settings_grid.attach(self.slice_grid, 0, 2, grid_width, 1) - self.setup_file_tab_set_sensitive(False) - self.setup_optimise_tab_set_sensitive(False) - - elif self.output_mode_radiobutton5.get_active(): - - self.edit_dict['output_mode'] = 'merge' - - self.settings_grid.attach(self.merge_grid, 0, 2, grid_width, 1) - self.setup_file_tab_set_sensitive(True) - self.setup_optimise_tab_set_sensitive(False) - - elif self.output_mode_radiobutton6.get_active(): - - self.edit_dict['output_mode'] = 'thumb' - - self.settings_grid.attach(self.thumb_grid, 0, 2, grid_width, 1) - self.setup_file_tab_set_sensitive(True) - self.setup_optimise_tab_set_sensitive(False) - - self.show_all() - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_palette_mode_radiobutton_toggled(self, radiobutton): - - """Called by callback in self.setup_settings_tab_gif_grid(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if self.palette_mode_radiobutton.get_active(): - self.edit_dict['palette_mode'] = 'faster' - else: - self.edit_dict['palette_mode'] = 'better' - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_quality_mode_radiobutton_toggled(self, radiobutton): - - """Called by callback in self.setup_settings_tab_h264_grid(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if self.quality_mode_radiobutton.get_active(): - - self.edit_dict['quality_mode'] = 'crf' - - self.rate_factor_scale.set_sensitive(True) - self.dummy_file_combo.set_sensitive(False) - - else: - - self.edit_dict['quality_mode'] = 'abr' - - self.rate_factor_scale.set_sensitive(False) - self.dummy_file_combo.set_sensitive(True) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_rate_factor_scale_changed(self, scale): - - """Called by callback in self.setup_settings_tab_h264_grid(). - - Args: - - scale (Gtk.Scale): The widget clicked - - """ - - value = int(self.rate_factor_scale.get_value()) - - self.edit_dict['rate_factor'] = value - - if not value: - self.profile_flag_checkbutton.set_sensitive(False) - else: - self.profile_flag_checkbutton.set_sensitive(True) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_regex_match_filename_entry_changed(self, entry): - - """Called by callback in self.setup_file_tab(). - - Args: - - entry (Gtk.Entry): The widget clicked - - """ - - value = entry.get_text() - - self.edit_dict['regex_match_filename'] = value - if value == '': - - self.regex_apply_subst_entry.set_text('') - self.regex_apply_subst_entry.set_sensitive(False) - - else: - - self.regex_apply_subst_entry.set_sensitive(True) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_reset_options_clicked(self, button): - - """Called by callback in self.setup_name_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' FFmpeg options > Name' - ) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'This procedure cannot be reversed. Are you sure you want to' \ - + ' continue?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'reset_ffmpeg_options', - # (Reset this edit window, if the user clicks 'yes') - 'data': [self], - }, - ) - - - def on_simple_options_clicked(self, button): - - """Called by callback in self.setup_name_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' FFmpeg options > Name' - ) - - redraw_flag = False - if not self.app_obj.simple_ffmpeg_options_flag: - - self.app_obj.set_simple_ffmpeg_options_flag(True) - - if not self.edit_dict: - - # User has not changed any options, so redraw the window to - # show the same options.OptionsManager object - self.reset_with_new_edit_obj(self.edit_obj) - - else: - - # User has already changed some options. We don't want to lose - # them, so wait for the window to close and be re-opened, - # before switching between simple/advanced options - redraw_flag = True - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Fewer FFmpeg options will be visible when you click the' \ - + ' \'Apply\' or \'Reset\' buttons (or when you close' \ - + ' and then re-open the window)', - ), - 'info', - 'ok', - self, # Parent window is this window - ) - - button.set_label( - _('Show more FFmpeg options (when window re-opens)'), - ) - - else: - - self.app_obj.set_simple_ffmpeg_options_flag(False) - - if not self.edit_dict: - - self.reset_with_new_edit_obj(self.edit_obj) - - else: - - redraw_flag = True - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'More FFmpeg options will be visible when you click the' \ - + ' \'Apply\' or \'Reset\' buttons (or when you close' \ - + ' and then re-open the window)', - ), - 'info', - 'ok', - self, # Parent window is this window - ) - - button.set_label( - _('Show fewer FFmpeg options (when window re-opens)'), - ) - - if redraw_flag: - - # Discard the list of videos, so that this becomes an ordinary - # edit window, with an 'OK' button that stores changes (and no - # 'Process files' button that starts a process operation) - self.video_list = [] - - self.ok_button.set_label(_('OK')) - self.ok_button.get_child().set_width_chars(10) - self.ok_button.set_tooltip_text( - _('Apply changes'), - ) - - - def on_simple_slice_toggled(self, checkbutton): - - """Called by callback in self.setup_slices_tab(). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if not checkbutton.get_active(): - - self.edit_dict['output_mode'] = 'h264' - radio_sens_flag = False - sens_flag = False - - else: - - # (Update the corresponding checkbutton in the 'Clips' tab) - self.simple_split_mode_checkbutton.set_active(False) - - # (Respond to clicks on this checkbutton) - self.edit_dict['output_mode'] = 'slice' - radio_sens_flag = True - - if self.retrieve_val('slice_mode') == 'video': - sens_flag = False - else: - sens_flag = True - - # (De)sensitise widgets - self.simple_slice_mode_radiobutton.set_sensitive(radio_sens_flag) - self.simple_slice_mode_radiobutton2.set_sensitive(radio_sens_flag) - self.simple_category_combo.set_sensitive(sens_flag) - self.simple_action_combo.set_sensitive(sens_flag) - self.simple_slice_start_entry.set_sensitive(sens_flag) - self.simple_slice_stop_entry.set_sensitive(sens_flag) - - self.simple_add_slice_button.set_sensitive(sens_flag) - self.simple_delete_slice_button.set_sensitive(sens_flag) - self.simple_show_settings_button.set_sensitive(True) - self.simple_clear_slice_button.set_sensitive(sens_flag) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_simple_split_toggled(self, checkbutton): - - """Called by callback in self.setup_clips_tab(). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if not checkbutton.get_active(): - - self.edit_dict['output_mode'] = 'h264' - radio_sens_flag = False - sens_flag = False - - else: - - # (Update the corresponding checkbutton in the 'Slices' tab) - self.simple_slice_mode_checkbutton.set_active(False) - - # (Respond to clicks on this checkbutton) - self.edit_dict['output_mode'] = 'split' - radio_sens_flag = True - - if self.retrieve_val('split_mode') == 'video': - sens_flag = False - else: - sens_flag = True - - # (De)sensitise widgets - self.simple_split_mode_radiobutton.set_sensitive(radio_sens_flag) - self.simple_split_mode_radiobutton2.set_sensitive(radio_sens_flag) - self.simple_start_stamp_entry.set_sensitive(sens_flag) - self.simple_stop_stamp_entry.set_sensitive(sens_flag) - self.simple_clip_title_entry.set_sensitive(sens_flag) - self.simple_add_timestamp_button.set_sensitive(sens_flag) - self.simple_delete_timestamp_button.set_sensitive(sens_flag) - self.simple_show_prefs_button.set_sensitive(True) - self.simple_clear_timestamp_button.set_sensitive(sens_flag) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_slice_mode_radiobutton_toggled(self, radiobutton): - - """Called by callback in self.setup_settings_tab_slice_grid(). - - In simple mode, called from a callback in self.setup_slices_tab(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if not self.app_obj.simple_ffmpeg_options_flag: - - if self.slice_mode_radiobutton.get_active(): - - self.edit_dict['slice_mode'] = 'video' - sens_flag = False - - else: - - self.edit_dict['slice_mode'] = 'custom' - sens_flag = True - - # (De)sensitise widgets - self.category_combo.set_sensitive(sens_flag) - self.action_combo.set_sensitive(sens_flag) - self.slice_start_entry.set_sensitive(sens_flag) - self.slice_stop_entry.set_sensitive(sens_flag) - self.add_slice_button.set_sensitive(sens_flag) - self.delete_slice_button.set_sensitive(sens_flag) - self.show_settings_button.set_sensitive(True) - self.clear_slice_button.set_sensitive(sens_flag) - - else: - - if self.simple_slice_mode_radiobutton.get_active(): - - self.edit_dict['slice_mode'] = 'video' - sens_flag = False - - else: - - self.edit_dict['slice_mode'] = 'custom' - if self.retrieve_val('output_mode') == 'slice': - sens_flag = True - else: - sens_flag = False - - # (De)sensitise widgets - self.simple_category_combo.set_sensitive(sens_flag) - self.simple_action_combo.set_sensitive(sens_flag) - self.simple_slice_start_entry.set_sensitive(sens_flag) - self.simple_slice_stop_entry.set_sensitive(sens_flag) - self.simple_add_slice_button.set_sensitive(sens_flag) - self.simple_delete_slice_button.set_sensitive(sens_flag) - self.simple_show_settings_button.set_sensitive(True) - self.simple_clear_slice_button.set_sensitive(sens_flag) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_slice_settings_clicked(self, button): - - """Called from a callback in self.setup_settings_tab_slice_grid() and - .setup_slices_tab(). - - Opens the preferences window to show slice settings. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - SystemPrefWin(self.app_obj, 'slices') - - - def on_split_mode_radiobutton_toggled(self, radiobutton): - - """Called by callback in self.setup_settings_tab_clip_grid(). - - In simple mode, called from a callback in self.setup_clips_tab(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if not self.app_obj.simple_ffmpeg_options_flag: - - if self.split_mode_radiobutton.get_active(): - - self.edit_dict['split_mode'] = 'video' - sens_flag = False - - else: - - self.edit_dict['split_mode'] = 'custom' - sens_flag = True - - # (De)sensitise widgets - self.start_stamp_entry.set_sensitive(sens_flag) - self.stop_stamp_entry.set_sensitive(sens_flag) - self.clip_title_entry.set_sensitive(sens_flag) - self.add_timestamp_button.set_sensitive(sens_flag) - self.delete_timestamp_button.set_sensitive(sens_flag) - self.show_prefs_button.set_sensitive(True) - self.clear_timestamp_button.set_sensitive(sens_flag) - - else: - - if self.simple_split_mode_radiobutton.get_active(): - - self.edit_dict['split_mode'] = 'video' - sens_flag = False - - else: - - self.edit_dict['split_mode'] = 'custom' - if self.retrieve_val('output_mode') == 'split': - sens_flag = True - else: - sens_flag = False - - # (De)sensitise widgets - self.simple_start_stamp_entry.set_sensitive(sens_flag) - self.simple_stop_stamp_entry.set_sensitive(sens_flag) - self.simple_clip_title_entry.set_sensitive(sens_flag) - self.simple_add_timestamp_button.set_sensitive(sens_flag) - self.simple_delete_timestamp_button.set_sensitive(sens_flag) - self.simple_show_prefs_button.set_sensitive(True) - self.simple_clear_timestamp_button.set_sensitive(sens_flag) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_video_drag_data_received(self, widget, context, x, y, data, info, - time): - - """Called from callback in self.setup_videos_tab(). - - This function is required for detecting when the user drags and drops - data into the Videos tab. - - If the data contains full paths to a video/audio file and/or URLs, - then we can search the media data registry, looking for matching - media.Video objects. - - Those objects can then be added to self.video_list. - - Args: - - widget (mainwin.MainWin): The widget into which something has been - dragged - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - x, y (int): Where the drop happened - - data (Gtk.SelectionData): The object to be filled with drag data - - info (int): Info that has been registered with the target in the - Gtk.TargetList - - time (int): A timestamp - - """ - - text = None - if info == 0: - text = data.get_text() - - if text is not None: - - # Hopefully, 'text' contains one or more valid URLs or paths to - # video/audio files - line_list = text.splitlines() - mod_list = [] - - for line in line_list: - mod_line = utils.strip_whitespace(urllib.parse.unquote(line)) - if mod_line != '': - - # On Linux, URLs are received as expected, but paths to - # media data files are received as 'file://PATH' - match = re.search(r'^file\:\/\/(.*)', mod_line) - if match: - mod_list.append(match.group(1)) - else: - mod_list.append(mod_line) - - # The True argument means to include 'dummy' media.Videos from the - # Classic Mode tab in the search - video_list = self.app_obj.retrieve_videos_from_db(mod_list, True) - - # (Remember if the video list is currently empty, or not) - old_size = len(self.video_list) - - # Add videos to the list, but don't add duplicates - for video_obj in video_list: - - if not video_obj in self.video_list: - self.video_list.append(video_obj) - - # Redraw the whole video list by calling this function, which also - # sorts self.video_list nicely - self.setup_videos_tab_update_treeview() - - if old_size == 0 and self.video_list: - - # Replace the 'OK' button with a 'Process files' button - self.ok_button.set_label(_('Process files')) - self.ok_button.set_tooltip_text( - _('Process the files with FFmpeg'), - ) - self.ok_button.get_child().set_width_chars(15) - - # Without this line, the user's cursor is permanently stuck in drag - # and drop mode - context.finish(True, False, time) - - - def on_video_remove_button_clicked(self, button, treeview): - - """Called from callback in self.setup_videos_tab(). - - Removes a video from the list of videos to be processed by FFmpeg. - - If there are no videos left, this edit window reverts to its default - state, in which we just save any changes to the FFmpeg options. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview to be updated - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - dbid = int(model[this_iter][0]) - for video_obj in self.video_list: - - if video_obj.dbid == dbid: - self.video_list.remove(video_obj) - break - - # Update the visible list - self.setup_videos_tab_update_treeview() - # If all videos have been removed, restore the OK button - if not self.video_list: - - self.ok_button.set_label(_('OK')) - self.ok_button.get_child().set_width_chars(10) - self.ok_button.set_tooltip_text( - _('Apply changes'), - ) - - - def on_video_show_button_clicked(self, button, treeview): - - """Called from callback in self.setup_videos_tab(). - - Opens the video properties window. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview to be updated - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - dbid = int(model[this_iter][0]) - if dbid in self.app_obj.media_reg_dict: - VideoEditWin( - self.app_obj, - self.app_obj.media_reg_dict[dbid], - ) - - - # (Redefined button strip callbacks) - - - def on_button_apply_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Applies any changes made by the user and re-draws the window's tabs, - showing their new values. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Apply any changes the user has made. The True argument identifies - # this function as the caller, and prevents a process operation from - # starting - self.apply_changes(True) - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - # (Redefined generic callbacks) - - - def on_checkbutton_toggled(self, checkbutton, prop): - - """Modified form of the GenericEditWin callback, in which we - automatically update the system command visible in the 'Name' tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - if not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_combo_with_data_changed(self, combo, prop): - - """Modified form of the GenericEditWin callback, in which we - automatically update the system command visible in the 'Name' tab. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.edit_dict[prop] = model[tree_iter][1] - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_entry_changed(self, entry, prop): - - """Modified form of the GenericEditWin callback, in which we - automatically update the system command visible in the 'Name' tab. - - Args: - - entry (Gtk.Entry): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - self.edit_dict[prop] = entry.get_text() - - # (De)sensitise the checkbutton for 'rename_both_flag', if required - if entry == self.add_end_filename_entry \ - or entry == self.regex_match_filename_entry: - - if self.retrieve_val('add_end_filename') == '' \ - and self.retrieve_val('regex_match_filename') == '': - self.rename_both_flag_checkbutton.set_active(False) - self.rename_both_flag_checkbutton.set_sensitive(False) - else: - self.rename_both_flag_checkbutton.set_sensitive(True) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_spinbutton_changed(self, spinbutton, prop): - - """Modified form of the GenericEditWin callback, in which we - automatically update the system command visible in the 'Name' tab. - - Args: - - spinbutton (Gtk.SpinkButton): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - self.edit_dict[prop] = int(spinbutton.get_value()) - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - - def on_textview_changed(self, textbuffer, prop): - - """Modified form of the GenericEditWin callback, in which we - automatically update the system command visible in the 'Name' tab. - - Args: - - textbuffer (Gtk.TextBuffer): The widget modified - - prop (str): The attribute in self.edit_obj to modify - - """ - - text = textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - old_value = self.retrieve_val(prop) - - if type(old_value) is list: - self.edit_dict[prop] = text.split() - elif type(old_value) is tuple: - self.edit_dict[prop] = text.split() - else: - self.edit_dict[prop] = text - - # Update the system command in the 'Name' tab - self.update_system_cmd() - - -class VideoEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in a media.Video - object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (media.Video): The object whose attributes will be edited in - this window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video properties window starts here.' \ - + ' In the main window, in the Videos tab, right-click a video' \ - + ' and select Show video > Properties...' - ) - - Gtk.Window.__init__(self, title=_('Video properties')) - - if self.is_duplicate(app_obj, edit_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media.Video object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (Non-standard widgets) - self.apply_options_button = None # Gtk.Button - self.edit_options_button = None # Gtk.Button - self.remove_options_button = None # Gtk.Button - # (Widgets used in the Timestamps tab) - self.timestamp_liststore = None # Gtk.ListStore - # (Widgets used in the Slices tab) - self.slice_liststore = None # Gtk.ListStore - # (Widgets used in the Comments tab) - self.comment_scrolled = None # Gtk.ScrolledWindow - self.comment_treeview = None # Gtk.TreeView - self.comment_liststore = None # Gtk.ListStore - self.comment_listbox = None # Gtk.ListBox - self.filter_entry = None # Gtk.Entry - self.filter_togglebutton = None # Gtk.ToggleButon - self.filter_author_checkbutton = None # Gtk.CheckButton - self.filter_comment_checkbutton = None # Gtk.CheckButton - self.filter_uploader_checkbutton = None # Gtk.CheckButton - self.filter_apply_button = None # Gtk.ToolButton - self.filter_cancel_button = None # Gtk.ToolButton - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK') are required, or False if just the 'OK' button is required - self.multi_button_flag = False - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # The key-value pairs in the dictionary correspond directly to the - # names of attributes, and their values in self.edit_obj - # Key-value pairs are added to this dictionary whenever the user makes - # a change (so if no changes are made when the window is closed, the - # dictionary will still be empty) - self.edit_dict = {} - - # String identifying the media type - self.media_type = 'video' - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericConfigWin - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - """ - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - setattr(self.edit_obj, key, self.edit_dict[key]) - - # The changes can now be cleared - self.edit_dict = {} - - # Redraw this media.Video in the Video Catalogue, if it's visible - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.video_catalogue_update_video, - self.edit_obj, - ) - - -# def retrieve_val(): # Inherited from GenericConfigWin - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_general_tab() - self.setup_download_options_tab() - self.setup_livestream_tab() - self.setup_descrip_tab() - self.setup_timestamps_tab() - self.setup_slices_tab() - self.setup_comments_tab() - self.setup_errors_warnings_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video properties > General' - ) - - tab, grid = self.add_notebook_tab(_('_General')) - grid_width = 3 - - # General properties - self.add_label(grid, - '' + _('General properties') + '', - 0, 0, grid_width, 1, - ) - - # The first sets of widgets are shared by multiple edit windows - self.add_container_properties(grid) - self.add_source_properties(grid) - - label = self.add_label(grid, - _('File'), - 0, 5, 1, 1, - ) - label.set_hexpand(False) - - frame = self.add_image(grid, - self.app_obj.main_win_obj.icon_dict['stock_file'], - 1, 5, 1, 1, - ) - # (The frame looks cramped without this. The icon itself is 16x16) - frame.set_size_request( - 16 + (self.spacing_size * 2), - -1, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 2, 5, 1, 1) - - entry = self.add_entry(grid2, - None, - 0, 0, 1, 1, - ) - entry.set_editable(False) - if self.edit_obj.file_name: - entry.set_text(self.edit_obj.get_actual_path(self.app_obj)) - - if not self.app_obj.show_custom_icons_flag: - button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_FILE, - Gtk.IconSize.BUTTON, - ) - else: - button = Gtk.Button.new() - button.set_image( - Gtk.Image.new_from_pixbuf( - self.app_obj.main_win_obj.pixbuf_dict['stock_add'], - ), - ) - - grid2.attach(button, 1, 0, 1, 1) - button.set_tooltip_text(_('Set the file (if this is the wrong one)')) - if self.edit_obj.parent_obj.name \ - in self.app_obj.container_unavailable_dict: - button.set_sensitive(False) - # (Signal connect appears below) - - # (Back to the main grid) - label2 = self.add_label(grid, - _('Metadata file'), - 0, 6, 2, 1, - ) - label2.set_hexpand(False) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid3 = self.add_secondary_grid(grid, 2, 6, 1, 1) - - entry2 = self.add_entry(grid3, - None, - 0, 0, 1, 1, - ) - entry2.set_editable(False) - - metadata_path = None - if self.retrieve_val('file_name') is not None: - metadata_path = self.edit_obj.get_actual_path_by_ext( - self.app_obj, - '.info.json', - ) - if metadata_path: - entry2.set_text(metadata_path) - - if not self.app_obj.show_custom_icons_flag: - button2 = Gtk.Button.new_from_icon_name( - Gtk.STOCK_FILE, - Gtk.IconSize.BUTTON, - ) - else: - button2 = Gtk.Button.new() - button2.set_image( - Gtk.Image.new_from_pixbuf( - self.app_obj.main_win_obj.pixbuf_dict['stock_file'], - ), - ) - - grid3.attach(button2, 1, 0, 1, 1) - button2.set_tooltip_text( - _('Update database using the video\'s metadata file'), - ) - if not metadata_path: - button2.set_sensitive(False) - # (Signal connect appears below) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid4 = self.add_secondary_grid(grid, 0, 7, grid_width, 1) - - checkbutton = self.add_checkbutton(grid4, - _('Video downloaded'), - 'dl_flag', - 0, 0, 1, 1, - ) - checkbutton.set_sensitive(False) - - checkbutton2 = self.add_checkbutton(grid4, - _('Video unwatched'), - 'new_flag', - 1, 0, 1, 1, - ) - checkbutton2.set_sensitive(False) - - checkbutton3 = self.add_checkbutton(grid4, - _('Video has been split from an original'), - 'split_flag', - 0, 1, 2, 1, - ) - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid4, - _('Video is archived'), - 'archive_flag', - 0, 2, 1, 1, - ) - checkbutton4.set_sensitive(False) - - checkbutton5 = self.add_checkbutton(grid4, - _('Video is bookmarked'), - 'bookmark_flag', - 1, 2, 1, 1, - ) - checkbutton5.set_sensitive(False) - - checkbutton6 = self.add_checkbutton(grid4, - _('Video is favourite'), - 'fav_flag', - 0, 3, 1, 1, - ) - checkbutton6.set_sensitive(False) - - checkbutton7 = self.add_checkbutton(grid4, - _('Video is in waiting list'), - 'waiting_flag', - 1, 3, 1, 1, - ) - checkbutton7.set_sensitive(False) - - checkbutton8 = self.add_checkbutton(grid4, - _('Video is blocked/censored/age-restricted'), - 'block_flag', - 0, 4, 2, 1, - ) - checkbutton8.set_sensitive(False) - - checkbutton9 = self.add_checkbutton(grid4, - _('Always simulate download of this video'), - 'dl_sim_flag', - 0, 5, 2, 1, - ) - checkbutton9.set_sensitive(False) - - label3 = self.add_label(grid4, - _('Video ID'), - 2, 0, 1, 1, - ) - label3.set_hexpand(False) - - entry3 = self.add_entry(grid4, - None, - 3, 0, 1, 1, - ) - entry3.set_editable(False) - if self.edit_obj.vid is not None: - entry3.set_text(self.edit_obj.vid) - - label4 = self.add_label(grid4, - _('Duration'), - 2, 1, 1, 1, - ) - label4.set_hexpand(False) - - entry4 = self.add_entry(grid4, - None, - 3, 1, 1, 1, - ) - entry4.set_editable(False) - if self.edit_obj.duration is not None: - entry4.set_text( - utils.convert_seconds_to_string(self.edit_obj.duration), - ) - - label5 = self.add_label(grid4, - _('File size'), - 2, 2, 1, 1, - ) - label5.set_hexpand(False) - - entry5 = self.add_entry(grid4, - None, - 3, 2, 1, 1, - ) - entry5.set_editable(False) - if self.edit_obj.file_size is not None: - entry5.set_text(self.edit_obj.get_file_size_string()) - - label6 = self.add_label(grid4, - _('Upload time'), - 2, 3, 1, 1, - ) - label6.set_hexpand(False) - - entry6 = self.add_entry(grid4, - None, - 3, 3, 1, 1, - ) - entry6.set_editable(False) - if self.edit_obj.upload_time is not None: - entry6.set_text(self.edit_obj.get_upload_time_string()) - - label7 = self.add_label(grid4, - _('Receive time'), - 2, 4, 1, 1, - ) - label7.set_hexpand(False) - - entry7 = self.add_entry(grid4, - None, - 3, 4, 1, 1, - ) - entry7.set_editable(False) - if self.edit_obj.receive_time is not None: - entry7.set_text(self.edit_obj.get_receive_time_string()) - - label8 = self.add_label(grid4, - _('Subtitles'), - 2, 5, 1, 1, - ) - label8.set_hexpand(False) - - entry8 = self.add_entry(grid4, - None, - 3, 5, 1, 1, - ) - entry8.set_editable(False) - entry8.set_text(' '.join(self.edit_obj.subs_list)) - - # (Signal connect from above) - button.connect('clicked', self.on_file_button_clicked) - button2.connect('clicked', self.on_metadata_button_clicked) - - -# def setup_download_options_tab(): # Inherited from GenericConfigWin - - - def setup_livestream_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Livestream' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video properties > Live' - ) - - tab, grid = self.add_notebook_tab(_('_Live')) - grid_width = 2 - - # Livestream properties - self.add_label(grid, - '' + _('Livestream properties') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Livestream status'), - 0, 1, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - None, - 1, 1, 1, 1, - ) - entry.set_editable(False) - if self.edit_obj.live_mode == 1: - entry.set_text(_('Waiting to start')) - elif self.edit_obj.live_mode == 2: - entry.set_text(_('Livestream has started')) - elif self.edit_obj.was_live_flag: - entry.set_text(_('Livestream has finished')) - else: - entry.set_text(_('Not a livestream')) - - label2 = self.add_label(grid, - _('Livestream message'), - 0, 2, 1, 1, - ) - label2.set_hexpand(False) - - entry2 = self.add_entry(grid, - None, - 1, 2, 1, 1, - ) - entry2.set_text(self.edit_obj.live_msg) - entry2.set_editable(False) - - checkbutton = Gtk.CheckButton() - grid.attach(checkbutton, 0, 3, grid_width, 1) - checkbutton.set_label( - _('Video is pre-recorded'), - ) - if self.edit_obj.live_debut_flag: - checkbutton.set_active(True) - checkbutton.set_sensitive(False) - - if self.edit_obj.live_mode and not ( - self.edit_obj.parent_obj.dbid \ - in self.app_obj.container_unavailable_dict - ): - # Livestream actions - self.add_label(grid, - '' + _('Livestream actions') + '', - 0, 4, grid_width, 1, - ) - - checkbutton2 = Gtk.CheckButton() - grid.attach(checkbutton2, 0, 5, grid_width, 1) - checkbutton2.set_label( - _('When the livestream starts, show a desktop notification'), - ) - if self.edit_obj.dbid in self.app_obj.media_reg_auto_notify_dict: - checkbutton2.set_active(True) - checkbutton2.set_sensitive(False) - - checkbutton3 = Gtk.CheckButton() - grid.attach(checkbutton3, 0, 6, grid_width, 1) - checkbutton3.set_label( - _('When the livestream starts, play an alarm'), - ) - if self.edit_obj.dbid in self.app_obj.media_reg_auto_alarm_dict: - checkbutton3.set_active(True) - checkbutton3.set_sensitive(False) - - checkbutton4 = Gtk.CheckButton() - grid.attach(checkbutton4, 0, 7, grid_width, 1) - checkbutton4.set_label( - _( - 'When the livestream starts, open it in the system\'s web' \ - + ' browser', - ), - ) - if self.edit_obj.dbid in self.app_obj.media_reg_auto_open_dict: - checkbutton4.set_active(True) - checkbutton4.set_sensitive(False) - - checkbutton5 = Gtk.CheckButton() - grid.attach(checkbutton5, 0, 8, grid_width, 1) - checkbutton5.set_label( - _( - 'When the livestream starts, begin downloading it immediately', - ), - ) - if self.edit_obj.dbid in self.app_obj.media_reg_auto_dl_start_dict: - checkbutton5.set_active(True) - checkbutton5.set_sensitive(False) - - checkbutton6 = Gtk.CheckButton() - grid.attach(checkbutton6, 0, 9, grid_width, 1) - checkbutton6.set_label( - _( - 'When a livestream stops, download it (overwriting any' \ - + ' earlier file)', - ), - ) - if self.edit_obj.dbid in self.app_obj.media_reg_auto_dl_stop_dict: - checkbutton6.set_active(True) - checkbutton6.set_sensitive(False) - - - def setup_descrip_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Description' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video properties > Description' - ) - - tab, grid = self.add_notebook_tab(_('_Description')) - grid_width = 2 - - # Video description - self.add_label(grid, - '' + _('Video description') + '', - 0, 0, grid_width, 1, - ) - - textview, textbuffer = self.add_textview(grid, - 'descrip', - 0, 1, grid_width, 1, - ) - textview.set_editable(False) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_can_focus(False) - - button = Gtk.Button.new_with_label( - _('Update from the description file, and set the line length to:'), - ) - grid.attach(button, 0, 2, 1, 1) - # (Signal connect appears below) - - min_value = self.app_obj.main_win_obj.medium_string_max_len - max_value = self.app_obj.main_win_obj.descrip_line_max_len - if max_value < min_value: - max_value = min_value - - spinbutton = self.add_spinbutton(grid, - min_value, - max_value, - 1, - None, - 1, 2, 1, 1, - ) - spinbutton.set_value(self.app_obj.main_win_obj.descrip_line_max_len) - - button2 = Gtk.Button.new_with_label( - _('Clear the description (does not modify the file)'), - ) - grid.attach(button2, 0, 3, grid_width, 1) - # (Signal connect appears below) - - # (Signal connects from above) - button.connect( - 'clicked', - self.on_load_descrip_button_clicked, - spinbutton, - textbuffer, - ) - - button2.connect( - 'clicked', - self.on_clear_descrip_button_clicked, - textbuffer, - ) - - - def setup_timestamps_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Timestamps' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video properties > Timestamps' - ) - - tab, grid = self.add_notebook_tab(_('_Timestamps')) - grid_width = 4 - - # Timestamps - self.add_label(grid, - '' + _('Timestamps') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'Timestamps can be used to download or create video clips', - ) + '', - 0, 1, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 2, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Start'), _('Stop'), _('Clip title') ], - ): - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.timestamp_liststore = Gtk.ListStore(str, str, str) - treeview.set_model(self.timestamp_liststore) - - # Initialise the list - self.setup_timestamps_tab_update_treeview() - - # Strip of widgets at the bottom - label = self.add_label(grid, - _('Start timestamp (e.g. 15:29)'), - 0, 3, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - None, - 1, 3, 1, 1, - ) - entry.set_width_chars(12) - entry.set_hexpand(False) - - label2 = self.add_label(grid, - _('Stop timestamp (optional)'), - 2, 3, 1, 1, - ) - label2.set_hexpand(False) - - entry2 = self.add_entry(grid, - None, - 3, 3, 1, 1, - ) - entry2.set_width_chars(12) - entry2.set_hexpand(False) - - label3 = self.add_label(grid, - _('Clip title (optional)'), - 0, 4, 1, 1, - ) - label3.set_hexpand(False) - - entry3 = self.add_entry(grid, - None, - 1, 4, 3, 1, - ) - entry3.set_hexpand(True) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 5, grid_width, 1) - - button = Gtk.Button(_('Add timestamp')) - grid2.attach(button, 0, 0, 1, 1) - button.set_hexpand(True) - button.connect( - 'clicked', - self.on_add_stamp_button_clicked, - entry, - entry2, - entry3, - ) - - button2 = Gtk.Button(_('Delete timestamp')) - grid2.attach(button2, 1, 0, 1, 1) - button2.set_hexpand(True) - button2.connect( - 'clicked', - self.on_delete_stamp_button_clicked, - treeview, - ) - - button3 = Gtk.Button(_('Clip preferences')) - grid2.attach(button3, 2, 0, 1, 1) - button3.set_hexpand(True) - button3.connect( - 'clicked', - self.on_clip_prefs_clicked, - ) - - button4 = Gtk.Button(_('Clear list')) - grid2.attach(button4, 3, 0, 1, 1) - button4.set_hexpand(True) - button4.connect( - 'clicked', - self.on_clear_stamp_button_clicked, - ) - - button5 = Gtk.Button(_('Reset list using copied text')) - grid2.attach(button5, 0, 1, 2, 1) - button5.set_hexpand(True) - button5.connect( - 'clicked', - self.on_copy_stamp_button_clicked, - ) - - button6 = Gtk.Button(_('Reset list using video description')) - grid2.attach(button6, 2, 1, 2, 1) - button6.set_hexpand(True) - button6.connect( - 'clicked', - self.on_extract_stamp_button_clicked, - ) - - - def setup_timestamps_tab_update_treeview(self): - - """ Called by self.setup_timestamps_tab(). - - Fills or updates the treeview. - """ - - self.timestamp_liststore.clear() - - # Add each timestamp/title to the treeview, one row at a time - for mini_list in self.edit_obj.stamp_list: - - start_stamp = mini_list[0] - - if mini_list[1] is None: - stop_stamp = '' - else: - stop_stamp = mini_list[1] - - if mini_list[2] is None: - clip_title = '' - else: - clip_title = mini_list[2] - - self.timestamp_liststore.append( - [ start_stamp, stop_stamp, clip_title ], - ) - - - def setup_slices_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Slices' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video properties > Slices' - ) - - tab, grid = self.add_notebook_tab(_('_Slices')) - grid_width = 4 - - # Video slices - self.add_label(grid, - '' + _('Video slices') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'SponsorBlock provides a list of slices that can be' \ - + ' removed from a video', - ) + '', - 0, 1, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 2, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Category'), _('Action type'), _('Start'), _('Stop') ], - ): - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.slice_liststore = Gtk.ListStore(str, str, str, str) - treeview.set_model(self.slice_liststore) - - # Initialise the list - self.setup_slices_tab_update_treeview() - - # Strip of widgets at the bottom - self.add_label(grid, - _('Category'), - 0, 3, 1, 1 - ) - - combo = self.add_combo(grid, - formats.SPONSORBLOCK_CATEGORY_LIST, - None, - 1, 3, 1, 1, - ) - combo.set_active(0) - - self.add_label(grid, - _('Action type'), - 2, 3, 1, 1 - ) - - combo2 = self.add_combo(grid, - formats.SPONSORBLOCK_ACTION_LIST, - None, - 3, 3, 1, 1, - ) - combo2.set_active(0) - - label = self.add_label(grid, - _('Start (timestamp or seconds)'), - 0, 4, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - None, - 1, 4, 1, 1, - ) - entry.set_hexpand(False) - - label2 = self.add_label(grid, - _('Stop (optional)'), - 2, 4, 1, 1, - ) - label2.set_hexpand(False) - - entry2 = self.add_entry(grid, - None, - 3, 4, 1, 1, - ) - entry2.set_hexpand(False) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 5, grid_width, 1) - - button = Gtk.Button(_('Add slice')) - grid2.attach(button, 0, 0, 1, 1) - button.set_hexpand(True) - button.connect( - 'clicked', - self.on_add_slice_button_clicked, - combo, - combo2, - entry, - entry2, - ) - - button2 = Gtk.Button(_('Delete slice')) - grid2.attach(button2, 1, 0, 1, 1) - button2.set_hexpand(True) - button2.connect( - 'clicked', - self.on_delete_slice_button_clicked, - treeview, - ) - - button3 = Gtk.Button(_('SponsorBlock settings')) - grid2.attach(button3, 2, 0, 1, 1) - button3.set_hexpand(True) - button3.connect( - 'clicked', - self.on_block_prefs_clicked, - ) - - button4 = Gtk.Button(_('Clear list')) - grid2.attach(button4, 3, 0, 1, 1) - button4.set_hexpand(True) - button4.connect( - 'clicked', - self.on_clear_slice_button_clicked, - ) - - button5 = Gtk.Button(_('Contact SponsorBlock to reset list')) - grid2.attach(button5, 2, 1, 2, 1) - button5.set_hexpand(True) - button5.connect( - 'clicked', - self.on_contact_sblock_clicked, - ) - - - def setup_slices_tab_update_treeview(self): - - """ Called by self.setup_slices_tab(). - - Fills or updates the treeview. - """ - - self.slice_liststore.clear() - - # Add each slice to the treeview, one row at a time - for mini_dict in self.edit_obj.slice_list: - - if 'category' in mini_dict: - category = mini_dict['category'] - else: - category = 'n/a' - - if 'action' in mini_dict: - action = mini_dict['action'] - else: - action = 'n/a' - - if 'start_time' in mini_dict: - start_time = mini_dict['start_time'] - else: - start_time = 'n/a' - - if 'stop_time' in mini_dict \ - and mini_dict['stop_time'] is not None: - stop_time = mini_dict['stop_time'] - else: - stop_time = 'n/a' - - self.slice_liststore.append( - [ category, action, str(start_time), str(stop_time) ], - ) - - - def setup_comments_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Comments' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video properties > Comments' - ) - - tab, grid = self.add_notebook_tab(_('_Comments')) - grid_width = 3 - - # Comments (yt-dlp only) - label = self.add_label(grid, - '' + _('Comments') + '' + self.ytdlp_only(), - 0, 0, 1, 1, - ) - label.set_hexpand(True) - - label2 = self.add_label(grid, - _('Total comments:'), - 1, 0, 1, 1, - ) - label2.set_hexpand(False) - - entry = self.add_entry(grid, - None, - 2, 0, 1, 1, - ) - entry.set_hexpand(False) - entry.set_max_width_chars(8) - entry.set_text(str(len(self.edit_obj.comment_list))) - - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - self.comment_scrolled = Gtk.ScrolledWindow() - frame.add(self.comment_scrolled) - self.comment_scrolled.set_vexpand(True) - self.comment_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # For a flat list, use a Gtk.TreeView. For a formmated list, use a - # Gtk.ListBox - if not self.app_obj.comment_show_formatted_flag: - self.setup_comments_tab_add_treeview() - else: - self.setup_comments_tab_add_listbox() - - # (The list is initialised below) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1) - - # Editing widgets - checkbutton = self.add_checkbutton(grid2, - _('Show formatted list'), - None, - 0, 0, 1, 1, - ) - checkbutton.set_hexpand(False) - if self.app_obj.comment_show_formatted_flag: - checkbutton.set_active(True) - checkbutton.connect('toggled', self.on_format_checkbutton_toggled) - - radiobutton = self.add_radiobutton(grid2, - None, - _('Show comment times as text'), - None, - None, - 1, 0, 1, 1, - ) - radiobutton.set_hexpand(False) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton(grid2, - radiobutton, - _('Show comment timestamps'), - None, - None, - 2, 0, 1, 1, - ) - radiobutton2.set_hexpand(False) - if not self.app_obj.comment_show_text_time_flag: - radiobutton2.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_time_radiobutton_toggled, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid3 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - label3 = self.add_label(grid3, - _('Filter'), - 0, 0, 1, 1, - ) - label3.set_hexpand(False) - - self.filter_entry = self.add_entry(grid3, - None, - 1, 0, 1, 1, - ) - self.filter_entry.set_hexpand(False) - self.filter_entry.set_tooltip_text(_('Enter search text')) - self.filter_entry.set_width_chars(16) - - self.filter_togglebutton = Gtk.ToggleButton.new_with_label(_('Regex')) - grid3.attach(self.filter_togglebutton, 2, 0, 1, 1) - - self.filter_author_checkbutton = self.add_checkbutton(grid3, - _('Author'), - None, - 3, 0, 1, 1, - ) - self.filter_author_checkbutton.set_hexpand(False) - - self.filter_comment_checkbutton = self.add_checkbutton(grid3, - _('Comment'), - None, - 4, 0, 1, 1, - ) - self.filter_comment_checkbutton.set_hexpand(False) - self.filter_comment_checkbutton.set_active(True) - - self.filter_uploader_checkbutton = self.add_checkbutton(grid3, - _('Uploader'), - None, - 5, 0, 1, 1, - ) - self.filter_uploader_checkbutton.set_hexpand(False) - - if not self.app_obj.show_custom_icons_flag: - self.filter_apply_button = Gtk.ToolButton.new_from_stock( - Gtk.STOCK_FIND, - ) - else: - self.filter_apply_button = Gtk.ToolButton.new() - self.filter_apply_button.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.app_obj.main_win_obj.pixbuf_dict['stock_find'], - ), - ) - grid3.attach(self.filter_apply_button, 6, 0, 1, 1) - self.filter_apply_button.connect( - 'clicked', - self.on_apply_filter_button_clicked, - ) - - if not self.app_obj.show_custom_icons_flag: - self.filter_cancel_button = Gtk.ToolButton.new_from_stock( - Gtk.STOCK_CANCEL, - ) - else: - self.filter_cancel_button = Gtk.ToolButton.new() - self.filter_cancel_button.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.app_obj.main_win_obj.pixbuf_dict['stock_cancel'], - ), - ) - grid3.attach(self.filter_cancel_button, 7, 0, 1, 1) - self.filter_cancel_button.set_sensitive(False) - self.filter_cancel_button.connect( - 'clicked', - self.on_cancel_filter_button_clicked, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid4 = self.add_secondary_grid(grid, 0, 4, grid_width, 1) - - button = Gtk.Button.new_with_label( - _('Update from the metadata file'), - ) - grid4.attach(button, 0, 0, 1, 1) - button.set_hexpand(True) - button.connect( - 'clicked', - self.on_load_comments_button_clicked, - ) - # (The call to .check_actual_path_by_ext() requires a value of - # media.Video.file_name that is not None) - if self.edit_obj.file_name is None \ - or self.edit_obj.file_ext is None: - button.set_sensitive(False) - else: - json_path = self.edit_obj.check_actual_path_by_ext( - self.app_obj, - '.info.json', - ) - if json_path is None: - button.set_sensitive(False) - - button2 = Gtk.Button.new_with_label( - _('Clear comments (does not modify the file)'), - ) - grid4.attach(button2, 1, 0, 1, 1) - button2.set_hexpand(True) - button2.connect( - 'clicked', - self.on_clear_comments_button_clicked, - ) - - # Initialise the list - self.setup_comments_tab_update_list() - - - def setup_comments_tab_add_treeview(self): - - """Called by self.setup_comments_tab(). - - For a flat list, we use a Gtk.TreeView. - """ - - # (This treeview replaces the old treeview or Gtk.ListBox) - self.setup_comments_tab_remove_child() - - self.comment_treeview = Gtk.TreeView() - self.comment_scrolled.add(self.comment_treeview) - self.comment_treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ - _('Time'), _('Author'), _('Comment'), _('Likes'), - _('Favourite'), _('Uploader'), '', - ], - ): - if i < 4: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.comment_treeview.append_column(column_text) - column_text.set_resizable(True) - # (Employ a twin strategy to cope with spam: split long values - # into multiple lines, and limit the (default) column size) - if i == 1: - column_text.set_min_width(100) - column_text.set_max_width(200) - elif i == 2: - column_text.set_min_width(100) - else: - column_text.set_min_width(50) - elif i < 6: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - self.comment_treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - column_toggle.set_min_width(50) - else: - # (Prevent the 'Uploader' column expanding to fill available - # space, especially when the window is maximised) - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.comment_treeview.append_column(column_text) - column_text.set_resizable(False) - - self.comment_liststore = Gtk.ListStore( - str, str, str, str, bool, bool, - ) - self.comment_treeview.set_model(self.comment_liststore) - - - def setup_comments_tab_add_listbox(self): - - """Called by self.setup_comments_tab(). - - For a formatted list, we use a Gtk.ListBox. - """ - - # (This listbox replaces the old treeview or Gtk.ListBox) - self.setup_comments_tab_remove_child() - - self.comment_listbox = Gtk.ListBox() - self.comment_scrolled.add(self.comment_listbox) - self.comment_listbox.set_can_focus(False) - self.comment_listbox.set_vexpand(True) - - - def setup_comments_tab_remove_child(self): - - """Called by self.setup_comments_tab_add_treeview() and - self.setup_comments_tab_add_listbox(). - - Removes the containing Gtk.Frame's textview or listbox, before adding - a new child widget. - """ - - if self.comment_treeview is not None: - self.comment_scrolled.remove(self.comment_treeview) - self.comment_treeview = None - self.comment_liststore = None - elif self.comment_listbox is not None: - self.comment_scrolled.remove(self.comment_listbox) - self.comment_listbox = None - - - def setup_comments_tab_update_list(self): - - """Can be called by anything. - - Fills or updates either the treeview or the listbox, whichever is - visible at the moment. - """ - - if self.comment_treeview is not None: - self.setup_comments_tab_update_treeview() - else: - self.setup_comments_tab_update_listbox() - - # (The Gtk.ListBox won't appear filled without this line) - self.show_all() - - - def setup_comments_tab_update_treeview(self): - - """ Called by self.setup_comments_tab(). - - Fills or updates the treeview. - """ - - self.comment_liststore.clear() - - shorter = 30 - longer = 80 - - # Set up filtering - filter_dict = { - 'search_text': self.filter_entry.get_text(), - 'lower_text': self.filter_entry.get_text().lower(), - 'regex_flag': self.filter_togglebutton.get_active(), - 'author_flag': self.filter_author_checkbutton.get_active(), - 'comment_flag': self.filter_comment_checkbutton.get_active(), - 'uploader_flag': self.filter_uploader_checkbutton.get_active(), - } - - # Add each comment to the treeview, one row at a time - # (Employ a twin strategy to cope with spam: split long values into - # multiple lines, and limit the (default) column size) - longest = 0 - for mini_dict in self.edit_obj.comment_list: - - if not self.setup_comments_tab_check_filter( - mini_dict, - filter_dict, - ): - continue - - # (The keys 'id' and 'text' are compulsory) - if not self.app_obj.comment_show_text_time_flag \ - and 'timestamp' in mini_dict: - ts = datetime.datetime.fromtimestamp(mini_dict['timestamp']) - ts.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None) - time = ts.strftime('%Y-%m-%d %H:%M:%S') - - elif self.app_obj.comment_show_text_time_flag \ - and 'time' in mini_dict: - time = mini_dict['time'] - - else: - time = 'n/a' - - if 'author' in mini_dict: - author = mini_dict['author'] - else: - author = 'n/a' - - if 'likes' in mini_dict: - likes = mini_dict['likes'] - else: - likes = '0' - - if 'fav_flag' in mini_dict: - fav_flag = mini_dict['fav_flag'] - else: - fav_flag = False - - if 'ul_flag' in mini_dict: - ul_flag = mini_dict['ul_flag'] - else: - ul_flag = False - - self.comment_liststore.append([ - time, - utils.shorten_string(author, shorter), - utils.tidy_up_long_string(mini_dict['text'], longer), - str(likes), - fav_flag, - ul_flag, - ]) - - - def setup_comments_tab_update_listbox(self): - - """ Called by self.setup_comments_tab(). - - Fills or updates the listbox. - """ - - for child in self.comment_listbox.get_children(): - self.comment_listbox.remove(child) - - shorter = 30 - longer = 80 - # Import the main window (for convenience) - main_win_obj = self.app_obj.main_win_obj - - # The media.Video object's .comment_list is a flat list. Compile a - # dictionary, so we can find each commment's parents - check_dict = {} - for mini_dict in self.edit_obj.comment_list: - check_dict[mini_dict['id']] = mini_dict - - # Set up filtering - filter_dict = { - 'search_text': self.filter_entry.get_text(), - 'lower_text': self.filter_entry.get_text().lower(), - 'regex_flag': self.filter_togglebutton.get_active(), - 'author_flag': self.filter_author_checkbutton.get_active(), - 'comment_flag': self.filter_comment_checkbutton.get_active(), - 'uploader_flag': self.filter_uploader_checkbutton.get_active(), - } - - # Add each comment to the listbox, one row at a time - for mini_dict in self.edit_obj.comment_list: - - if not self.setup_comments_tab_check_filter( - mini_dict, - filter_dict, - ): - continue - - row = Gtk.ListBoxRow() - - hbox = Gtk.HBox() - row.add(hbox) - # (self.spacing_size is a little too big) - hbox.set_border_width(3) - - # Indent the comment, depending on how many parents this comment - # has - # The indentation is applied by adding a Gtk.Label of the right - # length, up to a sensible maximum - # (Don't indent comments when the filter is applied) - if not self.filter_cancel_button.get_sensitive(): - count = 0 - this_dict = mini_dict - while count <= 8 and this_dict['parent'] is not None: - this_dict = check_dict[this_dict['parent']] - count += 1 - - if count: - label = Gtk.Label.new() - hbox.pack_start(label, False, False, 0) - label.set_text(' ' * count) - - box = Gtk.Box() - hbox.add(box) - - vbox = Gtk.VBox() - box.add(vbox) - - hbox2 = Gtk.HBox() - vbox.pack_start(hbox2, False, False, 0) - - if mini_dict['ul_flag']: - image = Gtk.Image.new_from_pixbuf( - main_win_obj.pixbuf_dict['uploader_small'], - ) - hbox2.pack_start(image, False, False, 0) - - if 'author' in mini_dict: - msg = '' \ - + html.escape( - utils.shorten_string(mini_dict['author'], shorter), - quote=False, - ) + '' - else: - msg = 'Anonymous' - - if not self.app_obj.comment_show_text_time_flag \ - and 'timestamp' in mini_dict: - ts = datetime.datetime.fromtimestamp(mini_dict['timestamp']) - ts.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None) - time = ts.strftime('%Y-%m-%d %H:%M:%S') - - elif self.app_obj.comment_show_text_time_flag \ - and 'time' in mini_dict: - time = mini_dict['time'] - - else: - time = 'Unknown time' - - msg += ' ' + time + '' - - label2 = Gtk.Label() - hbox2.pack_start(label2, False, False, 0) - label2.set_markup(msg) - label2.set_alignment(0, 0.5) - - if mini_dict['likes']: - image = Gtk.Image.new_from_pixbuf( - main_win_obj.pixbuf_dict['likes_small'], - ) - hbox2.pack_start(image, False, False, self.spacing_size) - - label3 = Gtk.Label() - hbox2.pack_start(label3, False, False, 0) - label3.set_text(str(mini_dict['likes'])) - label3.set_alignment(0, 0.5) - - if mini_dict['fav_flag']: - image = Gtk.Image.new_from_pixbuf( - main_win_obj.pixbuf_dict['favourite_small'], - ) - hbox2.pack_start(image, False, False, self.spacing_size) - - hbox3 = Gtk.HBox() - vbox.pack_start(hbox3, False, False, 0) - - label4 = Gtk.Label() - hbox3.pack_start(label4, False, False, 0) - label4.set_text( - html.escape( - utils.tidy_up_long_string(mini_dict['text'], longer), - quote=False - , - ), - ) - label4.set_alignment(0, 0.5) - - self.comment_listbox.add(row) - - - def setup_comments_tab_check_filter(self, comment_dict, filter_dict): - - """Called by self.setup_comments_tab_update_treeview() and - self.setup_comments_tab_update_listbox(). - - Checks each comment against the filter, if it is active. - - Args: - - comment_dict (dict): An item in media.Video.comment_list, - supplying details about a single comment associated with this - window's video - - filter_dict (dict): Summary of the state of widgets on this tab, - supplying the keys 'search_text', 'lower_text', 'regex_flag', - 'author_flag', 'comment_flag', 'uploader_flag' - - Return values: - - True to display the comment, False to filter it out - - """ - - comment = comment_dict['text'] - - if not filter_dict['regex_flag']: - - if ( - filter_dict['author_flag'] \ - and comment_dict['author'].lower().find( - filter_dict['lower_text'] - ) > -1 - ) or ( - filter_dict['comment_flag'] \ - and comment_dict['text'].lower().find( - filter_dict['lower_text'] - ) > -1 - ) or ( - filter_dict['uploader_flag'] \ - and comment_dict['ul_flag'] - ): - return True - - else: - - if ( - filter_dict['author_flag'] \ - and re.search( - filter_dict['search_text'], - comment_dict['author'], - re.IGNORECASE, - ) - ) or ( - filter_dict['comment_flag'] \ - and re.search( - filter_dict['search_text'], - comment_dict['text'], - re.IGNORECASE - ) - ) or ( - filter_dict['uploader_flag'] \ - and comment_dict['ul_flag'] - ): - return True - - # No match - return False - - - def setup_errors_warnings_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Errors / Warnings' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video properties > Errors / Warnings' - ) - - tab, grid = self.add_notebook_tab(_('_Errors / Warnings')) - - # Errors / Warnings - self.add_label(grid, - '' + _('Errors / Warnings') + '', - 0, 0, 1, 1, - ) - - self.add_label(grid, - '' + _( - 'Error messages produced the last time this video was' \ - + ' checked/downloaded', - ) + '', - 0, 1, 1, 1, - ) - - textview, textbuffer = self.add_textview(grid, - 'error_list', - 0, 2, 1, 1, - ) - textview.set_editable(False) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_can_focus(False) - - self.add_label(grid, - '' + _( - 'Warning messages produced the last time this video was' \ - + ' checked/downloaded', - ) + '', - 0, 3, 1, 1, - ) - - textview2, textbuffer2 = self.add_textview(grid, - 'warning_list', - 0, 4, 1, 1, - ) - textview2.set_editable(False) - textview2.set_wrap_mode(Gtk.WrapMode.WORD) - textview2.set_can_focus(False) - - - # Callback class methods - - -# def on_button_apply_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_edit_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_remove_options_clicked(): # Inherited from GenericConfigWin - - - def on_add_slice_button_clicked(self, button, combo, combo2, entry, \ - entry2): - - """Called from a callback in self.setup_slicess_tab(). - - Adds a new slice to the video's slice list. - - Args: - - button (Gtk.Button): The widget clicked - - combo, combo2 (Gtk.Entry): Other widgets to modify - - entry, entry2 (Gtk.Entry): Other widgets to modify - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Video properties > Slices' - ) - - tree_iter = combo.get_active_iter() - model = combo.get_model() - category = model[tree_iter][0] - - tree_iter2 = combo2.get_active_iter() - model2 = combo2.get_model() - action_type = model2[tree_iter2][0] - - start_time = utils.strip_whitespace(entry.get_text()) - stop_time = utils.strip_whitespace(entry2.get_text()) - - start_time = float( - utils.timestamp_convert_to_seconds(self.app_obj, start_time), - ) - - if stop_time == '': - stop_time = None - else: - stop_time = float( - utils.timestamp_convert_to_seconds(self.app_obj, stop_time), - ) - - # Do nothing if specified timestamps aren't valid - try: - ignore = float(start_time) - if stop_time is not None: - ignore = float(stop_time) - - except: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid start/stop times'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - if stop_time is not None and stop_time <= start_time: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid start/stop times'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Compile the mini-dictionary in the format returned by SponsorBlock - mini_dict = { - 'category': category, - 'action': action_type, - 'start_time': start_time, - 'stop_time': stop_time, - 'duration': 0, - } - - # Add it to the list - slice_list = self.retrieve_val('slice_list') - slice_list.append(mini_dict) - - # (The called function will sort the list) - self.edit_obj.set_slices(slice_list) - - # (Show changes, and empty entry boxes) - self.setup_slices_tab_update_treeview() - entry.set_text('') - entry2.set_text('') - - - def on_add_stamp_button_clicked(self, button, entry, entry2, entry3): - - """Called from a callback in self.setup_timestamps_tab(). - - Adds a new timestamp to video's timestamp list, optionally with a - clip title. - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2, entry3 (Gtk.Entry): Other widgets to modify - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Video properties > Timestamps' - ) - - start_stamp = utils.strip_whitespace(entry.get_text()) - stop_stamp = utils.strip_whitespace(entry2.get_text()) - clip_title = utils.strip_whitespace(entry3.get_text()) - - # (Values are stored as None, rather than empty strings) - if stop_stamp == '': - stop_stamp = None - - if clip_title == '': - clip_title = None - - # Do nothing if specified timestamps aren't valid ('stop_stamp' is - # optional) - regex = '^' + self.app_obj.timestamp_regex + '$' - if re.search(regex, start_stamp) \ - and (stop_stamp is None or re.search(regex, stop_stamp)) \ - and utils.timestamp_compare(self.app_obj, start_stamp, stop_stamp): - - # Add leading zeroes to the minutes and seconds components, so - # that .stamp_list gets sorted correctly (and doesn't look - # weird) - start_stamp = utils.timestamp_format(self.app_obj, start_stamp) - if stop_stamp is not None: - stop_stamp = utils.timestamp_format(self.app_obj, stop_stamp) - - # Timestamps stored in groups of three, in the form - # (start_stamp, stop_stamp, clip_title) - # If a group with the same 'start_stamp' timestamp already exists, - # don't replace it; allow duplicates (as the user may actually - # want that) - stamp_list = self.retrieve_val('stamp_list') - stamp_list.append([ start_stamp, stop_stamp, clip_title ]) - - # (The called function will sort the list) - self.edit_obj.set_timestamps(stamp_list) - - # (Show changes, and empty entry boxes. The 'stop' timestamp, if - # specified, becomes the 'start' timestamp for the next group) - self.setup_timestamps_tab_update_treeview() - - if stop_stamp is None: - entry.set_text('') - else: - entry.set_text( - utils.timestamp_add_second(self.app_obj, stop_stamp), - ) - - entry2.set_text('') - entry3.set_text('') - - else: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid timestamp(s)'), - 'error', - 'ok', - self, # Parent window is this window - ) - - - def on_apply_filter_button_clicked(self, button): - - """Called from a callback in self.setup_comments_tab(). - - Filters the list of comments to those matching the search text. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if not self.filter_author_checkbutton.get_active() \ - and not self.filter_comment_checkbutton.get_active() \ - and not self.filter_uploader_checkbutton.get_active(): - self.filter_entry.set_text('') - self.filter_cancel_button.set_sensitive(False) - elif self.filter_entry.get_text() == '': - self.filter_cancel_button.set_sensitive(False) - else: - self.filter_cancel_button.set_sensitive(True) - - self.setup_comments_tab_update_list() - - - def on_block_prefs_clicked(self, button): - - """Called from a callback in self.setup_slices_tab(). - - Opens the preferences window to show (Sponsor)Block settings. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - SystemPrefWin(self.app_obj, 'slices') - - - def on_cancel_filter_button_clicked(self, button): - - """Called from a callback in self.setup_comments_tab(). - - Cancels the filter for comments. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.filter_entry.set_text('') - self.filter_cancel_button.set_sensitive(False) - self.setup_comments_tab_update_list() - - - def on_clear_comments_button_clicked(self, button): - - """Called from a callback in self.setup_descrip_tab(). - - Clears the video's .comment_list IV (but doesn't modify the - .info.json file itself). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.edit_obj.reset_comments() - self.setup_comments_tab_update_list() - - - def on_clear_descrip_button_clicked(self, button, textbuffer): - - """Called from a callback in self.setup_descrip_tab(). - - Clears the video's .descrip IV (but doesn't modify the .description - file itself). - - Args: - - button (Gtk.Button): The widget clicked - - textbuffer (Gtk.TextBuffer): The textview's textbuffer - - """ - - self.edit_obj.reset_video_descrip() - textbuffer.set_text('') - - - def on_clear_slice_button_clicked(self, button): - - """Called from a callback in self.setup_slices_tab(). - - Empties the video's slice list. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.edit_obj.reset_slices() - self.setup_slices_tab_update_treeview() - - - def on_clear_stamp_button_clicked(self, button): - - """Called from a callback in self.setup_timestamps_tab(). - - Empties the video's timestamp list. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.edit_obj.reset_timestamps() - self.setup_timestamps_tab_update_treeview() - - - def on_clip_prefs_clicked(self, button): - - """Called from a callback in self.setup_timestamps_tab(). - - Opens the preferences window to show clip settings. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - SystemPrefWin(self.app_obj, 'clips') - - - def on_contact_sblock_clicked(self, button): - - """Called from a callback in self.setup_slices_tab(). - - Contacts SponsorBlock to reset the video's slice list. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - utils.fetch_slice_data( - self.app_obj, - self.edit_obj, - ) - - self.setup_slices_tab_update_treeview() - - - def on_copy_stamp_button_clicked(self, button): - - """Called from a callback in self.setup_timestamps_tab(). - - Updates the video's timestamp list using text the user has copied and - pasted into a dialogue window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Open the dialogue window - dialogue_win = mainwin.AddStampDialogue( - self, - self.app_obj.main_win_obj, - ) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window - if response == Gtk.ResponseType.OK: - - text = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - # (Do not modify the existing list of timestampes, if no text was - # added to the dialogue window) - if text != '': - self.edit_obj.extract_timestamps_from_descrip( - self.app_obj, - text, - ) - - self.setup_timestamps_tab_update_treeview() - - # ...before destroying the dialogue window - dialogue_win.destroy() - - - def on_delete_slice_button_clicked(self, button, treeview): - - """Called from a callback in self.setup_slices_tab(). - - Deletes the selected slice from the video's slice list. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the slice list - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - category = model[this_iter][0] - action_type = model[this_iter][1] - start_time = float(model[this_iter][2]) - stop_time = float(model[this_iter][3]) - - # Slices are stored as a list of mini-dictionaries, in the form - # described by self.on_add_slice_button_clicked() - # Walk the list, and delete the first matching mini-dictionary - slice_list = self.retrieve_val('slice_list') - mod_list = [] - match_flag = False - - for mini_dict in slice_list: - - if not match_flag \ - and mini_dict['category'] == category \ - and mini_dict['action'] == action_type \ - and mini_dict['start_time'] == start_time \ - and mini_dict['stop_time'] == stop_time: - match_flag = True # Delete this one - else: - mod_list.append(mini_dict) - - # (The called function will sort the list) - self.edit_obj.set_slices(mod_list) - - # (Show changes) - self.setup_slices_tab_update_treeview() - - - def on_delete_stamp_button_clicked(self, button, treeview): - - """Called from a callback in self.setup_timestamps_tab(). - - Deletes the selected timestamp(s) from the video's timestamp list. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the timestamp list - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - return - - start_stamp = model[this_iter][0] - stop_stamp = model[this_iter][1] - clip_title = model[this_iter][2] - - # Timestamps stored in groups of three, in the form - # (start_stamp, stop_stamp, clip_title) - # Walk the list, and delete the first matchng group - stamp_list = self.retrieve_val('stamp_list') - mod_list = [] - match_flag = False - - for mini_list in stamp_list: - - if not match_flag \ - and mini_list[0] == start_stamp \ - and (mini_list[1] is None or mini_list[1] == stop_stamp) \ - and (mini_list[2] is None or mini_list[2] == clip_title): - match_flag = True # Delete this one - else: - mod_list.append(mini_list) - - # (The called function will sort the list) - self.edit_obj.set_timestamps(mod_list) - - # (Show changes) - self.setup_timestamps_tab_update_treeview() - - - def on_extract_stamp_button_clicked(self, button): - - """Called from a callback in self.setup_timestamps_tab(). - - Updates the video's timestamp list from its description, then displays - that list in the treeview. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.edit_obj.extract_timestamps_from_descrip(self.app_obj) - self.setup_timestamps_tab_update_treeview() - - - def on_file_button_clicked(self, button): - - """Called from a callback in self.setup_general_tab(). - - Prompts the user to choose a new video/audio file. If a valid one is - selected, update the media.Video object to use it - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Video properties > General' - ) - - # Prompt the user for a new file - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Select the correct video/audio file'), - self, - 'open', - ) - - if self.edit_obj.file_name is not None: - old_path = self.edit_obj.get_actual_path(self.app_obj) - old_dir, old_name = os.path.split(old_path) - dialogue_win.set_current_folder(old_dir) - - # Get the user's response - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = os.path.abspath(dialogue_win.get_filename()) - - dialogue_win.destroy() - if response == Gtk.ResponseType.OK: - - # The user must not set a video that's in a different directory - file_dir, file_name = os.path.split(new_path) - parent_obj = self.edit_obj.parent_obj - if file_dir != parent_obj.get_actual_dir(self.app_obj) \ - and file_dir != parent_obj.get_default_dir(self.app_obj): - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'The replacement video/audio file must be in the same' \ - + ' channel, playlist or folder', - ), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # The new file must be in a recognised video/audio format - file_name, file_ext = os.path.splitext(new_path) - short_ext = file_ext[1:] - - if not short_ext in formats.VIDEO_FORMAT_LIST \ - and not short_ext in formats.AUDIO_FORMAT_LIST: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('You must select a valid video/audio file'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Set the new file path - self.edit_obj.set_file_from_path(new_path) - - # Extract video statistics from the metadata file - self.app_obj.update_video_from_json(self.edit_obj) - - # Set the new file's size, duration, and so on. The True argument - # instructs the function to override existing values - if self.edit_obj.dl_flag: - self.app_obj.update_video_from_filesystem( - self.edit_obj, - new_path, - True, - ) - - # If the video exists, then we can mark it as downloaded - if not self.edit_obj.dl_flag: - self.app_obj.mark_video_downloaded(self.edit_obj, True) - - # Redraw the video in the Video Catalogue straight away - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.video_catalogue_update_video, - self.edit_obj, - ) - - # Reset this window by abusing the generic code - self.reset_with_new_edit_obj(self.edit_obj) - - - def on_format_checkbutton_toggled(self, checkbutton): - - """Called from callback in self.setup_comments_tab(). - - Updates the mainapp.TartubeApp IV, and redraws the treeview. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active(): - self.app_obj.set_comment_show_formatted_flag(False) - self.setup_comments_tab_add_treeview() - else: - self.app_obj.set_comment_show_formatted_flag(True) - self.setup_comments_tab_add_listbox() - - self.setup_comments_tab_update_list() - self.show_all() - - - def on_load_comments_button_clicked(self, button): - - """Called from a callback in self.setup_comments_tab(). - - Updates the video's comments from its .info.json file. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.app_obj.update_video_from_json(self.edit_obj, 'comments') - self.setup_comments_tab_update_list() - - - def on_load_descrip_button_clicked(self, button, spinbutton, textbuffer): - - """Called from a callback in self.setup_descrip_tab(). - - Updates the video's description from its .description file. - - Args: - - button (Gtk.Button): The widget clicked - - spinbutton (Gtk.SpinButton): Widget setting the maximum line - length - - textbuffer (Gtk.TextBuffer): The textview's textbuffer - - """ - - self.edit_obj.read_video_descrip( - self.app_obj, - int(spinbutton.get_value()), - ) - - textbuffer.set_text(self.edit_obj.descrip) - - - def on_metadata_button_clicked(self, button): - - """Called from a callback in self.setup_general_tab(). - - Prompts the user to choose a new metadata file. If a valid one is - selected, update the media.Video object to use it - - Args: - - button (Gtk.Button): The widget clicked - - """ - - metadata_path = self.edit_obj.get_actual_path_by_ext( - self.app_obj, - '.info.json', - ) - if metadata_path is not None: - - # Extract video statistics from the metadata file - self.app_obj.update_video_from_json(self.edit_obj) - - # Set the new file's size, duration, and so on. The True argument - # instructs the function to override existing values - if self.edit_obj.dl_flag: - self.app_obj.update_video_from_filesystem( - self.edit_obj, - self.edit_obj.get_actual_path(self.app_obj), - True, - ) - - # Redraw the video in the Video Catalogue straight away - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.video_catalogue_update_video, - self.edit_obj, - ) - - # Reset this window by abusing the generic code - self.reset_with_new_edit_obj(self.edit_obj) - - - def on_time_radiobutton_toggled(self, radiobutton): - - """Called from callback in self.setup_comments_tab(). - - Updates the mainapp.TartubeApp IV, and redraws the treeview. - - Args: - - radiobutton (Gtk.RadioButton): The clicked widget - - """ - - if radiobutton.get_active(): - self.app_obj.set_comment_show_text_time_flag(True) - else: - self.app_obj.set_comment_show_text_time_flag(False) - - self.setup_comments_tab_update_list() - - -class ChannelPlaylistEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in a media.Channel or - media.Playlist object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (media.Channel, media.Playlist): The object whose attributes - will be edited in this window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Channel/playlist properties window starts' \ - + ' here. In the main window, in the Videos tab, right-click' \ - + ' a channel or playlist and select Show > Channel' \ - + ' properties... of Show > Playlist properties...' - ) - - if isinstance(edit_obj, media.Channel): - media_type = 'channel' - win_title = _('Channel properties') - else: - media_type = 'playlist' - win_title = _('Playlist properties') - - Gtk.Window.__init__(self, title=win_title) - - if self.is_duplicate(app_obj, edit_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media.Channel or media.Playlist object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (Non-standard widgets) - self.apply_options_button = None # Gtk.Button - self.edit_options_button = None # Gtk.Button - self.remove_options_button = None # Gtk.Button - # (Widgets used in the Playlists tab) - self.playlists_liststore = None # Gtk.ListStore - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK') are required, or False if just the 'OK' button is required - self.multi_button_flag = False - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # The key-value pairs in the dictionary correspond directly to - # the names of attributes, and their balues in self.edit_obj - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - # String set to 'channel' or 'playlist' - self.media_type = media_type - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericConfigWin - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - """ - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - setattr(self.edit_obj, key, self.edit_dict[key]) - - # The changes can now be cleared - self.edit_dict = {} - - # Update this media.Channel/media.Playlist in the Video Index - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.video_index_update_row_text, - self.edit_obj, - ) - - -# def retrieve_val(): # Inherited from GenericConfigWin - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_general_tab() - self.setup_download_options_tab() - if self.edit_obj.enhanced: - self.setup_assoc_playlist_tab() - if mainapp.HAVE_MATPLOTLIB_FLAG: - self.setup_history_tab() - self.setup_rss_feed_tab() - self.setup_errors_warnings_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Channel properties > General' - ) - - tab, grid = self.add_notebook_tab(_('_General')) - - # General properties - self.add_label(grid, - '' + _('General properties') + '', - 0, 0, 3, 1, - ) - - # The first sets of widgets are shared by multiple edit windows - self.add_container_properties(grid) - self.add_source_properties(grid) - self.add_destination_properties(grid) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 7, 3, 1) - - if self.media_type == 'channel': - string = _( - 'Don\'t add videos in this channel to Tartube\'s database', - ) - else: - string = _( - 'Don\'t add videos in this playlist to Tartube\'s database', - ) - - checkbutton = self.add_checkbutton(grid2, - string, - 'dl_no_db_flag', - 0, 0, 1, 1, - ) - checkbutton.set_sensitive(False) - - if self.media_type == 'channel': - string = _('Always simulate download of videos in this channel') - else: - string = _('Always simulate download of videos in this playlist') - - checkbutton2 = self.add_checkbutton(grid2, - string, - 'dl_sim_flag', - 0, 1, 1, 1, - ) - checkbutton2.set_sensitive(False) - - if self.media_type == 'channel': - string = _('Disable checking/downloading for this channel') - else: - string = _('Disable checking/downloading for this playlist') - - checkbutton3 = self.add_checkbutton(grid2, - string, - 'dl_disable_flag', - 0, 2, 1, 1, - ) - checkbutton3.set_sensitive(False) - - if self.media_type == 'channel': - string = _('This channel is marked as a favourite') - else: - string = _('This playlist is marked as a favourite') - - checkbutton4 = self.add_checkbutton(grid2, - string, - 'fav_flag', - 0, 3, 1, 1, - ) - checkbutton4.set_sensitive(False) - - self.add_label(grid2, - _('Total videos'), - 1, 0, 1, 1, - ) - entry = self.add_entry(grid2, - 'vid_count', - 2, 0, 1, 1, - ) - entry.set_editable(False) - entry.set_width_chars(8) - entry.set_hexpand(False) - - self.add_label(grid2, - _('New videos'), - 1, 1, 1, 1, - ) - entry2 = self.add_entry(grid2, - 'new_count', - 2, 1, 1, 1, - ) - entry2.set_editable(False) - entry2.set_width_chars(8) - entry2.set_hexpand(False) - - self.add_label(grid2, - _('Favourite videos'), - 1, 2, 1, 1, - ) - entry3 = self.add_entry(grid2, - 'fav_count', - 2, 2, 1, 1, - ) - entry3.set_editable(False) - entry3.set_width_chars(8) - entry3.set_hexpand(False) - - self.add_label(grid2, - _('Downloaded videos'), - 1, 3, 1, 1, - ) - entry4 = self.add_entry(grid2, - 'dl_count', - 2, 3, 1, 1, - ) - entry4.set_editable(False) - entry4.set_width_chars(8) - entry4.set_hexpand(False) - - -# def setup_download_options_tab(): # Inherited from GenericConfigWin - - - def setup_assoc_playlist_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Associated Playlists' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Channel properties > Associated playlists.' \ - + ' Only visible for the compatible websites (e.g. YouTube)', - ) - - tab, grid = self.add_notebook_tab(_('Associated _Playlists')) - grid_width = 4 - - # Associated playlists - self.add_label(grid, - '' + _('Associated playlists') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'When a video is associated with a playlist, the playlist\'s' \ - + ' ID is stored here', - ) + '', - 0, 1, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 2, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Playlist ID'), _('Playlist Title') ], - ): - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.playlists_liststore = Gtk.ListStore(str, str) - treeview.set_model(self.playlists_liststore) - - # Initialise the list - self.setup_assoc_playlist_tab_update_treeview() - - # Strip of widgets at the bottom - checkbutton = self.add_checkbutton(grid, - _('Set the channel as the download destination'), - None, - 0, 3, 2, 1, - ) - if self.media_type != 'channel': - checkbutton.set_sensitive(False) - - button = Gtk.Button(_('Add selected playlist')) - grid.attach(button, 2, 3, 1, 1) - button.connect( - 'clicked', - self.on_add_assoc_playlist_button_clicked, - treeview, - checkbutton, - ) - - button2 = Gtk.Button(_('Add all playlists')) - grid.attach(button2, 3, 3, 1, 1) - button2.connect( - 'clicked', - self.on_all_assoc_playlist_button_clicked, - checkbutton, - ) - - button3 = Gtk.Button(_('Download preferences')) - grid.attach(button3, 0, 4, 1, 1) - button3.connect( - 'clicked', - self.on_assoc_playlist_prefs_button_clicked, - ) - - button4 = Gtk.Button(_('Clear list')) - grid.attach(button4, 1, 4, 1, 1) - button4.connect( - 'clicked', - self.on_clear_assoc_playlist_button_clicked, - ) - - # (This button works even if mainapp.TartubeApp.store_playlist_id_flag - # is False) - button5 = Gtk.Button(_('Reset list using metadata file')) - grid.attach(button5, 2, 4, 2, 1) - button5.connect( - 'clicked', - self.on_reset_assoc_playlist_button_clicked, - ) - - - def setup_assoc_playlist_tab_update_treeview(self): - - """ Called by self.setup_assoc_playlist_tab(). - - Fills or updates the treeview. - """ - - self.playlists_liststore.clear() - - # Add each playlist to the treeview, one row at a time - # (The treeview needs to be sorted by its second column, corresponding - # to values in the IV's key-value pairs) - sort_list = sorted( - self.edit_obj.playlist_id_dict.items(), key=lambda x:x[1], - ) - sort_dict = dict(sort_list) - - for playlist_id in sort_dict.keys(): - - playlist_title = self.edit_obj.playlist_id_dict[playlist_id] - - if playlist_title is None: - self.playlists_liststore.append( - [ playlist_id, '<' + _('Unnamed playlist') + '>' ], - ) - - else: - self.playlists_liststore.append( - [ playlist_id, playlist_title ], - ) - - - def setup_history_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'History' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Channel properties > History' - ) - - tab, grid = self.add_notebook_tab(_('_History')) - grid_width = 6 - - # Download history - self.add_label(grid, - '' + _('Download history') + '', - 0, 0, grid_width, 1, - ) - - # Add combos to customise the graph - combo, combo2, combo3, combo4, combo5 = self.add_combos_for_graphs( - grid, - 1, - ) - - # Add a button which, when clicked, draws the graph using the - # customisation options specified by the combos - button = Gtk.Button() - grid.attach(button, 5, 1, 1, 1) - button.set_label(_('Draw')) - # (Signal connect appears below) - - # Add a box, inside which we draw graphs - hbox = Gtk.HBox() - grid.attach(hbox, 0, 2, grid_width, 1) - hbox.set_hexpand(True) - hbox.set_vexpand(True) - - # (Signal connects from above) - button.connect( - 'clicked', self.on_button_draw_graph_clicked, - hbox, - combo, - combo2, - combo3, - combo4, - combo5, - ) - - - def setup_rss_feed_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'RSS feed' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Channel properties > RSS feed' - ) - - tab, grid = self.add_notebook_tab(_('_RSS feed')) - grid_width = 3 - - # RSS feed (used to detect livestreams) - self.add_label(grid, - '' + _('RSS feed (used to detect livestreams)') + '', - 0, 0, grid_width, 1, - ) - - if self.media_type == 'channel': - msg = _( - 'If Tartube cannot detect the channel\'s RSS feed, you' \ - + ' can enter the URL here', - ) - else: - msg = _( - 'If Tartube cannot detect the playlist\'s RSS feed, you' \ - + ' can enter the URL here', - ) - - self.add_label(grid, - '' + msg + '', - 0, 1, grid_width, 1, - ) - - entry = self.add_entry(grid, - None, - 0, 2, grid_width, 1, - ) - entry.set_editable(True) - entry.set_hexpand(True) - if self.edit_obj.rss: - entry.set_text(self.edit_obj.rss) - entry.connect('changed', self.on_rss_entry_changed) - - button = Gtk.Button(_('Open in web browser')) - grid.attach(button, 0, 3, 1, 1) - button.set_hexpand(False) - button.connect('clicked', self.on_open_feed_button_clicked) - - button2 = Gtk.Button(_('Set to default feed')) - grid.attach(button2, 1, 3, 1, 1) - button2.set_hexpand(False) - if not self.edit_obj.source or not self.edit_obj.enhanced: - button2.set_sensitive(False) - button2.connect('clicked', self.on_set_feed_button_clicked, entry) - - button3 = Gtk.Button(_('Reset feed')) - grid.attach(button3, 2, 3, 1, 1) - button3.set_hexpand(False) - button3.connect('clicked', self.on_reset_feed_button_clicked, entry) - - self.add_label(grid, - '' + _( - 'N.B. The Set to default feed button won\'t work if' \ - + ' the RSS feed was obtained from video metadata', - ) + '', - 0, 4, grid_width, 1, - ) - - - def setup_errors_warnings_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Errors / Warnings' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Channel properties > Errors / Warnings' - ) - - tab, grid = self.add_notebook_tab(_('_Errors / Warnings')) - - # Errors / Warnings - self.add_label(grid, - '' + _('Errors / Warnings') + '', - 0, 0, 1, 1, - ) - - if self.media_type == 'channel': - string = _( - 'Error messages produced the last time this channel was' \ - + ' checked/downloaded', - ) - else: - string = _( - 'Error messages produced the last time this playlist was' \ - + ' checked/downloaded', - ) - - self.add_label(grid, - '' + string + '', - 0, 1, 1, 1, - ) - - textview, textbuffer = self.add_textview(grid, - 'error_list', - 0, 2, 1, 1, - ) - textview.set_editable(False) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_can_focus(False) - - if self.media_type == 'channel': - string = _( - 'Warning messages produced the last time this channel was' \ - + ' checked/downloaded', - ) - else: - string = _( - 'Warning messages produced the last time this playlist was' \ - + ' checked/downloaded', - ) - - self.add_label(grid, - '' + string + '', - 0, 3, 1, 1, - ) - - textview2, textbuffer2 = self.add_textview(grid, - 'warning_list', - 0, 4, 1, 1, - ) - textview2.set_editable(False) - textview2.set_wrap_mode(Gtk.WrapMode.WORD) - textview2.set_can_focus(False) - - - # (Support functions) - - -# def add_combos_for_graphs(): # Inherited from GenericConfigWin - - -# def plot_graph(): # Inherited from GenericConfigWin - - - # Callback class methods - - - def on_add_assoc_playlist_button_clicked(self, button, treeview, \ - checkbutton): - - """Called from a callback in self.setup_assoc_playlist_tab(). - - The selected associated playlist is added to Tartube's database (if - possible). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the associated - playlist list - - checkbutton (Gtk.CheckButton): Another widget to check - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Channel properties > Associated playlists' - ) - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - playlist_id = model[this_iter][0] - playlist_title = model[this_iter][1] - - # If 'playlist_title' is specified, then only create a playlist if no - # other container has the same URL or name - # If 'playlist_title' is not specified, the just check for duplicate - # URLs; if a new playlist is created, create an artificial title - if playlist_title != '' \ - and not self.app_obj.is_container(playlist_title): - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The name \'{0}\' is already in use').format(playlist_title), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - url = utils.convert_enhanced_template_from_json( - 'convert_playlist_list', - self.edit_obj.enhanced, - { - 'playlist_id': playlist_id, - 'playlist_title': playlist_title, - }, - ) - if url is None: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Unable to extrapolate the URL for this playlist'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - for dbid in self.app_obj.container_reg_dict.keys(): - - media_data_obj = self.app_obj.media_reg_dict[dbid] - if not isinstance(media_data_obj, media.Folder) \ - and media_data_obj.source == url: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The playlist is already in Tartube\'s database'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Name an unnamed playlist - if playlist_title == '': - playlist_title \ - = utils.find_available_name(self, 'playlist', 1, -1) - - # Create the new playlist, with the same parent as this window's - # channel/playlist - playlist_obj = self.app_obj.add_playlist( - playlist_title, - self.edit_obj.parent_obj, # May be 'None' - url, - ) - if not playlist_obj: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Unable to create new playlist'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - else: - - # This window's channel is the download destination for the - # playlist, if required - if checkbutton.get_active(): - playlist_obj.set_master_dbid(self.app_obj, self.edit_obj.dbid) - - # Update the Video Index. The True argument tells the function not - # to select the new playlist - self.app_obj.main_win_obj.video_index_add_row(playlist_obj, True) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Added playlist \'{0}\' to Tartube\'s database', - ).format(playlist_title), - 'info', - 'ok', - self, # Parent window is this window - ) - - return - - - def on_all_assoc_playlist_button_clicked(self, button, checkbutton): - - """Called from a callback in self.setup_assoc_playlist_tab(). - - All associated playlists are added to Tartube's database (where - possible). - - Args: - - button (Gtk.Button): The widget clicked - - checkbutton (Gtk.CheckButton): Another widget to check - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' Channel properties > Associated playlists' - ) - - success_count = 0 - fail_count = 0 - - if not self.edit_obj.playlist_id_dict: - # No playlists to add - return - - # Go through the list of associated playlists, eliminating any which - # already exist (duplicate names or duplicate source URLs), and - # assigning a name to any unnamed playlist - for playlist_id in self.edit_obj.playlist_id_dict.keys(): - - playlist_title = self.edit_obj.playlist_id_dict[playlist_id] - - if playlist_title != '' \ - and not self.app_obj.is_container(playlist_title): - fail_count += 1 - continue - - url = utils.convert_enhanced_template_from_json( - 'convert_playlist_list', - self.edit_obj.enhanced, - { - 'playlist_id': playlist_id, - 'playlist_title': playlist_title, - }, - ) - if url is None: - continue - - match_flag = False - for dbid in self.app_obj.container_reg_dict.keys(): - - media_data_obj = self.app_obj.media_reg_dict[dbid] - if not isinstance(media_data_obj, media.Folder) \ - and media_data_obj.source == url: - match_flag = True - break - - if match_flag: - fail_count += 1 - continue - - # Name an unnamed playlist - if playlist_title == '': - playlist_title = utils.find_available_name( - self, - 'playlist', - 1, - -1, - ), - - # Create the new playlist, with the same parent as this window's - # channel/playlist - playlist_obj = self.app_obj.add_playlist( - playlist_title, - self.edit_obj.parent_obj, # May be 'None' - url, - ) - if not playlist_obj: - fail_count += 1 - - else: - # This window's channel is the download destination for the - # playlist, if required - if checkbutton.get_active(): - playlist_obj.set_master_dbid( - self.app_obj, - self.edit_obj.dbid, - ) - - # Update the Video Index. The True argument tells the function - # not to select the new playlist - self.app_obj.main_win_obj.video_index_add_row( - playlist_obj, - True, - ) - - success_count += 1 - - # Show confirmation - msg = _('Playlists added: {0}').format(success_count) \ - + '\n' + _('Playlists not added: {0}').format(fail_count) - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - return - - - def on_assoc_playlist_prefs_button_clicked(self, button): - - """Called from a callback in self.setup_assoc_playlist_tab(). - - Opens the preferences window to show associated playlist settings. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - SystemPrefWin(self.app_obj, 'downloads') - - -# def on_button_apply_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_edit_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_remove_options_clicked(): # Inherited from GenericConfigWin - - - def on_clear_assoc_playlist_button_clicked(self, button): - - """Called from a callback in self.setup_assoc_playlist_tab(). - - Empties the channel/playlist's associated playlist list. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.edit_obj.reset_playlist_id() - self.setup_assoc_playlist_tab_update_treeview() - - - def on_open_feed_button_clicked(self, button): - - """Called from a callback in self.setup_rss_feed_tab(). - - Opens the RSS feed in a web browser. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - rss = self.retrieve_val('rss') - if rss: - utils.open_file(self.app_obj, rss) - - - def on_reset_assoc_playlist_button_clicked(self, button): - - """Called from a callback in self.setup_assoc_playlist_tab(). - - Restores the channel/playlist's associated playlist list, extracting - playlist IDs from the metadata file of each child video. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.edit_obj.extract_playlist_id(self.app_obj) - self.setup_assoc_playlist_tab_update_treeview() - - - def on_rss_entry_changed(self, entry): - - """Called by callback in self.setup_rss_feed_tab(). - - Sets the RSS feed. - - Args: - entry (Gtk.Entry): The entry box modified - - """ - - rss = entry.get_text() - if rss == '': - self.edit_dict['rss'] = None - else: - self.edit_dict['rss'] = rss - - - def on_set_feed_button_clicked(self, button, entry): - - """Called from a callback in self.setup_rss_feed_tab(). - - Sets the RSS feed to its expected value. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to modify - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Channel properties > RSS feed' - ) - - rss = self.retrieve_val('rss') - # Update code won't work if the .rss IV is set - self.edit_obj.reset_rss() - - # Try to set the RSS feed using the channel/playlist URL - self.edit_obj.update_rss_from_url(self.retrieve_val('source')) - # Try to set the RSS feed using a child video, if any - if not self.edit_obj.rss and self.edit_obj.child_list: - - # Look for the first video whose URL is set, then give up - for child_obj in self.edit_obj.child_list: - if child_obj.source: - self.edit_obj.update_rss_from_url(child_obj.source) - break - - if not self.edit_obj.rss: - - # Failed. Restore the previous value - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Could not set the RSS feed'), - 'error', - 'ok', - self, # Parent window is this window - ) - - if rss: - entry.set_text(rss) - - else: - - # Succeeded - entry.set_text(self.edit_obj.rss) - - - def on_reset_feed_button_clicked(self, button, entry): - - """Called from a callback in self.setup_rss_feed_tab(). - - Resets the RSS feed. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to modify - - """ - - # (Updating the entry sets self.edit_dict) - entry.set_text('') - - -class FolderEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in a media.Folder - object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (media.Folder): The object whose attributes will be edited in - this window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Folder properties window starts here.' \ - + ' In the main window, in the Videos tab, right-click a folder' \ - + ' and select Show > Folder properties...' - ) - - Gtk.Window.__init__(self, title=_('Folder properties')) - - if self.is_duplicate(app_obj, edit_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media.Folder object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (Non-standard widgets) - self.apply_options_button = None # Gtk.Button - self.edit_options_button = None # Gtk.Button - self.remove_options_button = None # Gtk.Button - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK') are required, or False if just the 'OK' button is required - self.multi_button_flag = False - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # The key-value pairs in the dictionary correspond directly to - # the names of attributes, and their balues in self.edit_obj - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - # String identifying the media type - self.media_type = 'folder' - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericConfigWin - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - """ - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - setattr(self.edit_obj, key, self.edit_dict[key]) - - # The changes can now be cleared - self.edit_dict = {} - - # Update this media.Channel/media.Playlist in the Video Index - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.video_index_update_row_text, - self.edit_obj, - ) - - - -# def retrieve_val(): # Inherited from GenericConfigWin - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_general_tab() - self.setup_download_options_tab() - self.setup_statistics_tab() - if mainapp.HAVE_MATPLOTLIB_FLAG: - self.setup_history_tab() - if self.edit_obj == self.app_obj.fixed_recent_folder: - self.setup_recent_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Folder properties > General' - ) - - tab, grid = self.add_notebook_tab(_('_General')) - - # General properties - self.add_label(grid, - '' + _('General properties') + '', - 0, 0, 3, 1, - ) - - # The first sets of widgets are shared by multiple edit windows - self.add_container_properties(grid) - self.add_destination_properties(grid) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 7, 3, 1) - - checkbutton = self.add_checkbutton(grid2, - _('Don\'t add videos to Tartube\'s database'), - 'dl_no_db_flag', - 0, 0, 2, 1, - ) - checkbutton.set_sensitive(False) - - checkbutton2 = self.add_checkbutton(grid2, - _('Always simulate download of videos'), - 'dl_sim_flag', - 0, 1, 2, 1, - ) - checkbutton2.set_sensitive(False) - - checkbutton3 = self.add_checkbutton(grid2, - _('Disable checking/downloading'), - 'dl_disable_flag', - 0, 2, 2, 1, - ) - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid2, - _('This folder is marked as a favourite'), - 'fav_flag', - 0, 3, 2, 1, - ) - checkbutton4.set_sensitive(False) - - checkbutton5 = self.add_checkbutton(grid2, - _('This folder is hidden'), - 'hidden_flag', - 2, 0, 1, 1, - ) - checkbutton5.set_sensitive(False) - - checkbutton6 = self.add_checkbutton(grid2, - _('This folder can\'t be deleted by the user'), - 'fixed_flag', - 2, 1, 1, 1, - ) - checkbutton6.set_sensitive(False) - - checkbutton7 = self.add_checkbutton(grid2, - _('This is a system-controlled folder'), - 'priv_flag', - 2, 2, 1, 1, - ) - checkbutton7.set_sensitive(False) - - checkbutton8 = self.add_checkbutton(grid2, - _('All contents deleted when Tartube shuts down'), - 'temp_flag', - 2, 3, 1, 1, - ) - checkbutton8.set_sensitive(False) - - label = self.add_label(grid2, - _('Restrictions:'), - 0, 4, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid2, - None, - 1, 4, 1, 1, - ) - entry.set_editable(False) - if self.edit_obj.restrict_mode == 'full': - entry.set_text(_('Can only contain videos')) - elif self.edit_obj.restrict_mode == 'partial': - entry.set_text(_('Can contain folders and videos')) - else: - entry.set_text(_('Can contain anything')) - - - def setup_statistics_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Statistics' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Folder properties > Statistics' - ) - - tab, grid = self.add_notebook_tab(_('_Statistics')) - grid_width = 4 - - # Statistics - self.add_label(grid, - '' + _('Statistics') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - _('This folder contains:'), - 0, 1, grid_width, 1, - ) - - self.add_label(grid, - _('Videos'), - 0, 2, 1, 1, - ) - - entry = self.add_entry(grid, - None, - 1, 2, 1, 1, - ) - entry.set_editable(False) - - self.add_label(grid, - _('Downloaded'), - 0, 3, 1, 1, - ) - - entry2 = self.add_entry(grid, - None, - 1, 3, 1, 1, - ) - entry2.set_editable(False) - - self.add_label(grid, - _('Other'), - 0, 4, 1, 1, - ) - - entry3 = self.add_entry(grid, - None, - 1, 4, 1, 1, - ) - entry3.set_editable(False) - - self.add_label(grid, - _('Channels'), - 2, 2, 1, 1, - ) - - entry4 = self.add_entry(grid, - None, - 3, 2, 1, 1, - ) - entry4.set_editable(False) - - self.add_label(grid, - _('Playlists'), - 2, 3, 1, 1, - ) - - entry5 = self.add_entry(grid, - None, - 3, 3, 1, 1, - ) - entry5.set_editable(False) - - self.add_label(grid, - _('Sub-folders'), - 2, 4, 1, 1, - ) - - entry6 = self.add_entry(grid, - None, - 3, 4, 1, 1, - ) - entry6.set_editable(False) - - # Initialise the entries - self.setup_statistics_tab_recalculate( - entry, - entry2, - entry3, - entry4, - entry5, - entry6, - ) - - button = Gtk.Button() - grid.attach(button, 3, 5, 1, 1) - button.set_label(_('Recalculate')) - button.connect( - 'clicked', - self.on_button_recalculate_clicked, - entry, - entry2, - entry3, - entry4, - entry5, - entry6, - ) - - - def setup_statistics_tab_recalculate(self, entry, entry2, entry3, entry4, - entry5, entry6): - - """Called by self.setup_statistics_tab and - .on_recalculate_button_clicked(). - - Args: - - entry, entry2, entry3, entry4, entry5, entry6 (Gtk.Entry): The - entry boxes to update - - """ - - # Get number of videos, channels, playlists and sub-folders - total_count, video_count, channel_count, playlist_count, \ - folder_count = self.edit_obj.count_descendants( [0, 0, 0, 0, 0] ) - - # Calculate downloaded/not downloaded videos - dl_count = 0 - not_dl_count = 0 - child_list = self.edit_obj.compile_all_videos( [] ) - - for video_obj in child_list: - - if video_obj.dl_flag: - dl_count += 1 - else: - not_dl_count += 1 - - entry.set_text(str(video_count)) - entry2.set_text(str(dl_count)) - entry3.set_text(str(not_dl_count)) - entry4.set_text(str(channel_count)) - entry5.set_text(str(playlist_count)) - entry6.set_text(str(folder_count)) - - - def setup_history_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'History' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Folder properties > History' - ) - - tab, grid = self.add_notebook_tab(_('_History')) - grid_width = 6 - - # Download history - self.add_label(grid, - '' + _('Download history') + '', - 0, 0, grid_width, 1, - ) - - # Add combos to customise the graph - combo, combo2, combo3, combo4, combo5 = self.add_combos_for_graphs( - grid, - 1, - ) - - # Add a button which, when clicked, draws the graph using the - # customisation options specified by the combos - button = Gtk.Button() - grid.attach(button, 5, 1, 1, 1) - button.set_label(_('Draw')) - # (Signal connect appears below) - - # Add a box, inside which we draw graphs - hbox = Gtk.HBox() - grid.attach(hbox, 0, 2, grid_width, 1) - hbox.set_hexpand(True) - hbox.set_vexpand(True) - - # (Signal connects from above) - button.connect( - 'clicked', self.on_button_draw_graph_clicked, - hbox, - combo, - combo2, - combo3, - combo4, - combo5, - ) - - - def setup_recent_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Recent Videos' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Folder properties > Recent videos. Only' \ - + ' visible for the \'Recent videos\' folder' - ) - - tab, grid = self.add_notebook_tab(_('_Recent Videos')) - grid_width = 2 - - # Recent videos - self.add_label(grid, - '' + _('Recent videos') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' \ - + _('When videos are checked/downloaded, older videos are' \ - + ' removed from this folder', - ) + '', - 0, 1, grid_width, 1, - ) - - radiobutton = self.add_radiobutton(grid, - None, - _('Empty the whole folder'), - None, - None, - 0, 2, grid_width, 1, - ) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _('Remove videos after days'), - None, - None, - 0, 3, 1, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 1, - 14, - 1, - None, - 1, 3, 1, 1, - ) - # (Signal connect appears below) - - if not self.app_obj.fixed_recent_folder_days: - spinbutton.set_sensitive(False) - else: - radiobutton2.set_active(True) - spinbutton.set_value( - self.app_obj.fixed_recent_folder_days, - ) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_radiobutton_toggled, - spinbutton, - ) - - spinbutton.connect( - 'value-changed', - self.on_spinbutton_changed, - ) - - -# def setup_download_options_tab(): # Inherited from GenericConfigWin - - - # (Support functions) - - -# def add_combos_for_graphs(): # Inherited from GenericConfigWin - - -# def plot_graph(): # Inherited from GenericConfigWin - - - # Callback class methods - - -# def on_button_apply_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_edit_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_remove_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_draw_graph_clicked(): # Inherited from GenericConfigWin - - - def on_button_recalculate_clicked(self, button, entry, entry2, entry3, - entry4, entry5, entry6): - - """Called from callback in self.setup_statistics_tab(). - - Recalculates the number of child media data objects, and updates the - entry boxes. - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2, entry3, entry4, entry5, entry6 (Gtk.Entry): The - entry boxes to update - - """ - - self.setup_statistics_tab_recalculate( - entry, - entry2, - entry3, - entry4, - entry5, - entry6, - ) - - - def on_radiobutton_toggled(self, radiobutton, spinbutton): - - """Called from callback in self.setup_recent_tab(). - - (De)sensitises the spinbutton, depending on which radiobutton is - selected. Then updates the IV. - - Args: - - radiobutton (Gtk.RadioButton): The clicked widget - - spinbutton (Gtk.SpinButton): Another widget to modify - - """ - - if radiobutton.get_active(): - - spinbutton.set_sensitive(False) - self.app_obj.set_fixed_recent_folder_days(0) - - else: - - spinbutton.set_sensitive(True) - spinbutton.set_value(self.app_obj.fixed_recent_folder_days) - - - def on_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_recent_tab(). - - Sets the time after which videos are removed from the fixed 'Recent - Videos' folder. - - Args: - - spinbutton (Gtk.SpinButton): The clicked widget - - """ - - self.app_obj.set_fixed_recent_folder_days( - int(spinbutton.get_value()), - ) - - -class ScheduledEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in a media.Scheduled - object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (media.Scheduled): The object whose attributes will be edited - in this window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Scheduled downloads window starts here.' \ - + ' In the menu, click Edit > System preferences...' \ - + ' Scheduling > Start. In the \'Scheduled download name\'' \ - + ' box, add a name. Then click the Add button' - ) - - Gtk.Window.__init__(self, title=_('Scheduled download')) - - if self.is_duplicate(app_obj, edit_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media.Scheduled object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (IVs used to handle widget changes in the 'Media' tab) - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK') are required, or False if just the 'OK' button is required - self.multi_button_flag = False - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # The key-value pairs in the dictionary correspond directly to - # the names of attributes, and their balues in self.edit_obj - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - # String identifying the media type - self.media_type = 'scheduled' - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericConfigWin - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - """ - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - setattr(self.edit_obj, key, self.edit_dict[key]) - - # The changes can now be cleared - self.edit_dict = {} - - # Since the edit window opened, channels/playlists/folders may have - # been deleted. Check that any items in the .media_list IV still - # exist - for dbid in self.edit_obj.media_list: - - if not dbid in self.app_obj.container_reg_dict: - self.edit_obj.media_list.remove(dbid) - - # Update the parent preference window's list of scheduled downloads - for win_obj in self.app_obj.main_win_obj.config_win_list: - - if isinstance(win_obj, SystemPrefWin): - win_obj.setup_scheduling_start_tab_update_treeview() - - -# def retrieve_val(): # Inherited from GenericConfigWin - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_general_tab() - self.setup_start_tab() - self.setup_conflicts_tab() - self.setup_media_tab() - self.setup_limits_tab() - self.setup_other_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Scheduled downloads > General' - ) - - tab, grid = self.add_notebook_tab(_('_General')) - grid_width = 3 - - # General properties - self.add_label(grid, - '' + _('General properties') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - _('Scheduled download name'), - 0, 1, 1, 1, - ) - - entry = self.add_entry(grid, - None, - 1, 1, (grid_width - 1), 1, - ) - entry.set_text(self.edit_obj.name) - entry.set_editable(False) - - self.add_label(grid, - _('Download mode'), - 0, 2, 1, 1, - ) - - combo_list = [ - [_('Check channels, playlist and folders'), 'sim'], - [_('Download channels, playlists and folders'), 'real'], - [_('Perform a custom download'), 'custom_real'], - ] - - combo = self.add_combo_with_data(grid, - combo_list, - 'dl_mode', - 1, 2, (grid_width - 1), 1, - ) - combo.set_hexpand(True) - # (Signal connect appears below, overriding the generic one) - - self.add_label(grid, - _('Custom download name'), - 0, 3, 1, 1, - ) - - combo_list2 = [ - [ - '', - '', # self.on_custom_dl_combo_changed() converts it to None - ], - [ - self.app_obj.general_custom_dl_obj.name, - self.app_obj.general_custom_dl_obj.uid, - ], - ] - if self.app_obj.classic_custom_dl_obj is not None: - combo_list2.append([ - self.app_obj.classic_custom_dl_obj.name, - self.app_obj.classic_custom_dl_obj.uid, - ]) - - for custom_dl_obj in self.app_obj.custom_dl_reg_dict.values(): - if self.app_obj.general_custom_dl_obj != custom_dl_obj \ - and ( - not self.app_obj.classic_custom_dl_obj \ - or self.app_obj.classic_custom_dl_obj != custom_dl_obj - ): - combo_list2.append([custom_dl_obj.name, custom_dl_obj.uid]) - - combo2 = self.add_combo_with_data(grid, - combo_list2, - 'custom_dl_uid', - 1, 3, (grid_width - 1), 1, - ) - combo2.set_hexpand(True) - if self.edit_obj.dl_mode != 'custom_real': - combo2.set_sensitive(False) - # (Signal connect appears below, overriding the generic one) - - # (Signal connects from above) - combo.connect( - 'changed', - self.on_dl_mode_combo_changed, - combo2, - ) - combo2.connect( - 'changed', - self.on_custom_dl_combo_changed, - ) - - - def setup_start_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Start' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Scheduled downloads > Start' - ) - - tab, grid = self.add_notebook_tab(_('_Start')) - grid_width = 4 - - # Start conditions - self.add_label(grid, - '' + _('Start conditions') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - _('Start mode'), - 0, 1, 1, 1, - ) - - combo_list = [ - [_('Perform this download at regular intervals'), 'repeat'], - [_('Perform this download when Tartube starts'), 'start'], - [ - _('Perform this download some time after Tartube starts'), - 'start_after', - ], - [_('Perform this download at specified times'), 'timetable'], - [_('Disable this scheduled download'), 'disabled'], - ] - - combo = self.add_combo_with_data(grid, - combo_list, - None, - 1, 1, (grid_width - 1), 1, - ) - combo.set_hexpand(True) - # (Signal connect appears below) - - label = self.add_label(grid, - '', - 0, 2, 1, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 1, None, 1, - 'wait_value', - 1, 2, 1, 1, - ) - - combo2_list = [] - for unit in formats.TIME_METRIC_LIST: - if unit != 'seconds': - combo2_list.append( - [ formats.TIME_METRIC_TRANS_DICT[unit], unit ], - ) - - combo2 = self.add_combo_with_data(grid, - combo2_list, - 'wait_unit', - 2, 2, 2, 1, - ) - combo2.set_hexpand(True) - - label2 = self.add_label(grid, - '', - 0, 3, 1, 1, - ) - - treeview, liststore = self.add_treeview(grid, - 1, 3, 1, 10, - ) - self.setup_start_tab_update_treeview(liststore) - - combo3_list = [] - for key in formats.SPECIFIED_DAYS_LIST: - combo3_list.append( - [ formats.SPECIFIED_DAYS_DICT[key], key ], - ) - - combo3 = self.add_combo_with_data(grid, - combo3_list, - None, - 2, 3, 2, 1, - ) - combo3.set_hexpand(True) - combo3.set_active(0) - - label3 = self.add_label(grid, - _('Hours'), - 2, 4, 1, 1, - ) - label3.set_hexpand(False) - - spinbutton2 = self.add_spinbutton(grid, - 0, 23, 1, - None, - 3, 4, 1, 1, - ) - # (Signal connect appears below) - - label4 = self.add_label(grid, - _('Minutes'), - 2, 5, 1, 1, - ) - label4.set_hexpand(False) - - spinbutton3 = self.add_spinbutton(grid, - 0, 55, 5, - None, - 3, 5, 1, 1, - ) - # (Signal connect appears below) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 2, 6, 2, 1) - - button = Gtk.Button() - grid2.attach(button, 0, 0, 1, 1) - button.set_label(_('Add')) - button.set_hexpand(True) - # (Signal connect appears below) - - button2 = Gtk.Button() - grid2.attach(button2, 1, 0, 1, 1) - button2.set_label(_('Remove')) - button2.set_hexpand(True) - # (Signal connect appears below) - - # (Set up widgets in their initial state) - if self.edit_obj.start_mode == 'repeat': - combo.set_active(0) - elif self.edit_obj.start_mode == 'start': - combo.set_active(1) - elif self.edit_obj.start_mode == 'start_after': - combo.set_active(2) - elif self.edit_obj.start_mode == 'timetable': - combo.set_active(3) - else: - # .start_mode is 'disabled' - combo.set_active(4) - - self.setup_start_tab_update_widgets( - label, - spinbutton, - combo2, - label2, - combo3, - spinbutton2, - spinbutton3, - button, - button2, - ) - - # (Signal connects from above) - combo.connect( - 'changed', - self.on_start_mode_combo_changed, - label, - spinbutton, - combo2, - label2, - combo3, - spinbutton2, - spinbutton3, - button, - button2, - ) - - # (Signal for showing leading zeroes for both hours and minutes) - spinbutton2.connect('output', self.show_spinbutton_leading_zeroes, 2) - spinbutton3.connect('output', self.show_spinbutton_leading_zeroes, 2) - - button.connect( - 'clicked', - self.on_add_timetable_button_clicked, - liststore, - combo3, - spinbutton2, - spinbutton3, - ) - button2.connect( - 'clicked', - self.on_remove_timetable_button_clicked, - treeview, - liststore, - ) - - - def setup_start_tab_update_widgets(self, label, spinbutton, combo2, \ - label2, combo3, spinbutton2, spinbutton3, button, button2): - - """Called by self.setup_start_tab() and .on_start_mode_combo_changed(). - - Sets up widgets on opening, and when the first combo (marked 'Start - Mode') changes. - - Args: - - label (Gtk.Label): A widget to be modified - - spinbutton (Gtk.SpinButton): Another widget to be modified - - combo2 (Gtk.Combo): Another widget to be modified - - label2 (Gtk.Label): Another widget to be modified - - combo3 (Gtk.Combo): Another widget to be modified - - spinbutton2, spinbutton3 (Gtk.SpinButton): Other widgets to be - modified - - button, button (Gtk.Button): Other widgets to be modified - - """ - - label.set_markup('') - spinbutton.set_sensitive(False) - combo2.set_sensitive(False) - label2.set_markup('') - combo3.set_sensitive(False) - spinbutton2.set_sensitive(False) - spinbutton3.set_sensitive(False) - button.set_sensitive(False) - button2.set_sensitive(False) - - start_mode = self.retrieve_val('start_mode') - if start_mode == 'repeat': - label.set_markup(_('Interval time')) - spinbutton.set_sensitive(True) - combo2.set_sensitive(True) - elif start_mode == 'start': - pass - elif start_mode == 'start_after': - label.set_markup('Time after startup') - spinbutton.set_sensitive(True) - combo2.set_sensitive(True) - elif start_mode == 'timetable': - label2.set_markup(_('Start times')) - combo3.set_sensitive(True) - spinbutton2.set_sensitive(True) - spinbutton3.set_sensitive(True) - button.set_sensitive(True) - button2.set_sensitive(True) - else: - # 'start_mode' is 'disabled' - pass - - - def setup_start_tab_update_treeview(self, liststore): - - """Called by self.setup_start_tab(), .on_add_timetable_button_clicked() - and .on_remove_timetable_button_clicked(). - - Updates the treeview showing timetabled start times. - - Args: - - liststore (Gtk.ListStore): The treeview's model - - """ - - timetable_list = self.retrieve_val('timetable_list') - liststore.clear() - - # Each 'mini_list' is in the form [ day_string, time_string ] - for mini_list in timetable_list: - liststore.append([ - formats.SPECIFIED_DAYS_DICT[mini_list[0]] + ' ' + mini_list[1], - ]) - - - def setup_conflicts_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Conflicts' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Scheduled downloads > Conflicts' - ) - - tab, grid = self.add_notebook_tab(_('_Conflicts')) - - # Conflict settings - self.add_label(grid, - '' + _('Conflict settings') + '', - 0, 0, 1, 1, - ) - - self.add_label(grid, - _('If another scheduled download is running:'), - 0, 1, 1, 1, - ) - - combo4_list = [ - [ - _( - 'Add channels, playlists and folders to the end of the queue', - ), - 'join', - ], - [ - _( - 'Add channels, playlists and folders to the beginning of the' \ - + ' queue', - ), - 'priority', - ], - [ - _( - 'Do nothing, just wait until the next scheduled download' \ - + ' time', - ), - 'skip', - ], - ] - - combo4 = self.add_combo_with_data(grid, - combo4_list, - 'join_mode', - 0, 2, 1, 1, - ) - combo4.set_hexpand(True) - - self.add_checkbutton(grid, - _('This scheduled download takes priority over others') \ - + '\n' \ - + _( - 'Other scheduled downloads won\'t start until this one is' \ - + ' finished', - ), - 'exclusive_flag', - 0, 3, 1, 1, - ) - - - def setup_media_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Scheduled downloads > Media' - ) - - tab, grid = self.add_notebook_tab(_('_Media')) - grid_width = 4 - - # Media to download - self.add_label(grid, - '' + _('Media to download') + '', - 0, 0, grid_width, 1, - ) - - self.radiobutton = self.add_radiobutton(grid, - None, - _('Check/download everything'), - None, - None, - 0, 1, grid_width, 1, - ) - - self.radiobutton2 = self.add_radiobutton(grid, - self.radiobutton, - _('Only check/download the media below'), - None, - None, - 0, 2, grid_width, 1, - ) - if not self.edit_obj.all_flag: - self.radiobutton2.set_active(True) - self.radiobutton2.connect( - 'toggled', - self.on_all_flag_toggled, - ) - - self.add_label(grid, - '' + _( - 'Hint: you can drag and drop channels, playlists and your own' \ - + ' folders here', - ) + '', - 0, 3, grid_width, 1, - ) - - # Create a treeview, containing the .dbid (invisible) and .name - # (visible) for each media data object added - frame = Gtk.Frame() - grid.attach(frame, 0, 4, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treestore = Gtk.ListStore(int, str) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_model(treestore) - treeview.set_headers_visible(False) - treeview.set_hexpand(True) - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - '', - renderer_text, - text=1, - ) - treeview.append_column(column_text) - - # Initialise the treeview - self.setup_media_tab_update_treeview(treestore) - - # Set up drag and drop into the treeview - drag_target_list = [('text/plain', 0, 0)] - treeview.enable_model_drag_dest( - # Table of targets the drag procedure supports, and array length - drag_target_list, - # Bitmask of possible actions for a drag from this widget - Gdk.DragAction.DEFAULT, - ) - treeview.connect( - 'drag-drop', - self.on_video_index_drag_drop, - ) - treeview.connect( - 'drag-data-received', - self.on_video_index_drag_data_received, - ) - - # Editing widgets - obj_list = [] - for media_data_obj in self.app_obj.container_reg_dict.values(): - - if not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.fixed_flag: - obj_list.append(media_data_obj) - - obj_list.sort(key=lambda x: x.name.lower()) - - combostore = Gtk.ListStore(int, str) - for media_data_obj in obj_list: - combostore.append( [media_data_obj.dbid, media_data_obj.name] ) - - combo = Gtk.ComboBox.new_with_model(combostore) - grid.attach(combo, 0, 5, 1, 1) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 1) - - combo.set_entry_text_column(1) - combo.set_active(0) - - button = Gtk.Button() - grid.attach(button, 1, 5, 1, 1) - button.set_label(_('Add')) - button.connect( - 'clicked', - self.on_add_media_button_clicked, - combo, - treestore, - ) - - button2 = Gtk.Button() - grid.attach(button2, 2, 5, 1, 1) - button2.set_label(_('Remove')) - button2.connect( - 'clicked', - self.on_remove_media_button_clicked, - treeview, - ) - - button3 = Gtk.Button() - grid.attach(button3, 3, 5, 1, 1) - button3.set_label(_('Clear list')) - button3.connect( - 'clicked', - self.on_clear_media_button_clicked, - treestore, - ) - - - def setup_media_tab_update_treeview(self, liststore): - - """Called by self.setup_media_tab() and some callbacks. - - Updates the treeview to display the media.Scheduled object's - .media_list IV, first checking that any specified media data objects - still exist. - - Args: - - liststore (Gtk.ListStore): The treeview's model - - """ - - liststore.clear() - - media_list = self.retrieve_val('media_list') - for dbid in media_list: - - # This media data object may be deleted while the window is open - # (but the .media_list IV is checked, when the 'Save' or 'Apply' - # buttons are clicked) - if dbid in self.app_obj.container_reg_dict: - media_data_obj = self.app_obj.media_reg_dict[dbid] - liststore.append([media_data_obj.dbid, media_data_obj.name]) - - - def setup_limits_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Limits' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Scheduled downloads > Limits' - ) - - tab, grid = self.add_notebook_tab(_('_Limits')) - grid_width = 3 - - # Performance limits - self.add_label(grid, - '' + _('Performance limits') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' + _('Limits are applied when you start downloading a' \ - + ' video/channel/playlist') + '', - 0, 1, grid_width, 1, - ) - - self.add_label(grid, - '' + _('These limits override the default and alternative' \ - + ' limits specified elsewhere') + '', - 0, 2, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Limit simultaneous downloads to'), - 'scheduled_num_worker_apply_flag', - 0, 3, 1, 1, - ) - checkbutton.set_hexpand(False) - - spinbutton = self.add_spinbutton(grid, - self.app_obj.num_worker_min, - self.app_obj.num_worker_max, - 1, # Step - 'scheduled_num_worker', - 1, 3, 1, 1, - ) - - checkbutton2 = self.add_checkbutton(grid, - _('Limit download speed to'), - 'scheduled_bandwidth_apply_flag', - 0, 4, 1, 1, - ) - checkbutton2.set_hexpand(False) - - spinbutton2 = self.add_spinbutton(grid, - self.app_obj.bandwidth_min, - self.app_obj.bandwidth_max, - 1, # Step - 'scheduled_bandwidth', - 1, 4, 1, 1, - ) - - self.add_label(grid, - 'KiB/s', - 2, 3, 1, 1, - ) - - - def setup_other_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Other' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Scheduled downloads > Other' - ) - - tab, grid = self.add_notebook_tab(_('_Other')) - - # Other settings - self.add_label(grid, - '' + _('Other settings') + '', - 0, 0, 1, 1, - ) - - self.add_checkbutton(grid, - _( - 'Ignore time-saving preferences, and check/download the whole' \ - + ' channel/playlist/folder', - ), - 'ignore_limits_flag', - 0, 1, 1, 1, - ) - - self.add_checkbutton(grid, - _('Shut down Tartube when this scheduled download has finished'), - 'shutdown_flag', - 0, 2, 1, 1, - ) - - - # Callback class methods - - -# def on_button_apply_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_edit_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_remove_options_clicked(): # Inherited from GenericConfigWin - - - def on_add_media_button_clicked(self, button, combo, liststore): - - """Called by callback in self.setup_media_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - combo (Gtk.ComboBox): A combo in which the user has selected a new - media data object - - liststore (Gtk.ListStore): The treeview's model - - """ - - combo_iter = combo.get_active_iter() - combo_model = combo.get_model() - dbid = combo_model[combo_iter][0] - - # Check the media data object hasn't already been added to the list, - # and that is still exists in the media data registry - media_list = self.retrieve_val('media_list') - - if not dbid in media_list \ - and dbid in self.app_obj.container_reg_dict: - - media_list.append(dbid) - self.edit_dict['media_list'] = media_list - - self.radiobutton2.set_active(True) - - # Update the treeview - self.setup_media_tab_update_treeview(liststore) - - - def on_add_timetable_button_clicked(self, button, liststore, combo, \ - spinbutton, spinbutton2): - - """Called by callback in self.setup_start_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - liststore (Gtk.ListStore): The treeview's model - - combo (Gtk.ComboBox): A widget to modify - - spinbutton, spinbutton2 (Gtk.SpinButton): Other widgets to modify - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - day_str = model[tree_iter][1] - - hours = int(spinbutton.get_value()) - minutes = int(spinbutton2.get_value()) - - # Each 'mini_list' is in the form [ day_string, time_string ] - timetable_list = self.retrieve_val('timetable_list') - mini_list = [ - day_str, - '{:02d}'.format(hours) + ':' + '{:02d}'.format(minutes), - ] - # Check for duplicates - for other_list in timetable_list: - if other_list[0] == mini_list[0] \ - and other_list[1] == mini_list[1]: - return - - # No duplicates found - timetable_list.append(mini_list) - self.edit_dict['timetable_list'] = timetable_list - self.setup_start_tab_update_treeview(liststore) - - - def on_all_flag_toggled(self, radiobutton): - - """Called from callback in self.setup_media_tab(). - - Enables/disables checking/downloading all media. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.edit_obj.all_flag = False - else: - self.edit_obj.all_flag = True - - - def on_clear_media_button_clicked(self, button, liststore): - - """Called by callback in self.setup_media_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - liststore (Gtk.ListStore): The treeview's model - - """ - - # Update the IV - self.edit_dict['media_list'] = [] - # Update widgets - liststore.clear() - self.radiobutton.set_active(True) - - - def on_custom_dl_combo_changed(self, combo): - - """Called from callback in self.setup_general_tab(). - - Sets the IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - value = model[tree_iter][1] - - if value is None or value == '': - self.edit_dict['custom_dl_uid'] = None - else: - self.edit_dict['custom_dl_uid'] = int(value) - - - def on_dl_mode_combo_changed(self, combo, combo2): - - """Called from callback in self.setup_general_tab(). - - Sets the IV, and (de)sensitises other widgets. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - combo2 (Gtk.ComboBox): Another widget to update - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.edit_dict['dl_mode'] = model[tree_iter][1] - - if self.edit_dict['dl_mode'] == 'custom_real': - combo2.set_sensitive(True) - else: - combo2.set_active(0) - combo2.set_sensitive(False) - - - def on_remove_media_button_clicked(self, button, treeview): - - """Called by callback in self.setup_media_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The list of media data objects - - """ - - selection = treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - else: - - dbid = model[tree_iter][0] - - # Check the media data object exists in the list and in the media data - # registry - media_list = self.retrieve_val('media_list') - if dbid in media_list: - - media_list.remove(dbid) - self.edit_dict['media_list'] = media_list - - # Update widgets - self.setup_media_tab_update_treeview(treeview.get_model()) - if not media_list: - self.radiobutton.set_active(True) - - - def on_remove_timetable_button_clicked(self, button, treeview, liststore): - - """Called by callback in self.setup_start_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview to be updated - - liststore (Gtk.ListStore): The treeview's model - - """ - - selection = treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - # Nothing selected - return - - display_str = model[tree_iter][0] - - match = re.search(r'^(.*)\s(\d\d\:\d\d)', display_str) - if match: - - translated_str = match.groups()[0] - time_str = match.groups()[1] - - # Compile a reversed dictionary for lookup - rev_dict = {} - for key in formats.SPECIFIED_DAYS_DICT.keys(): - rev_dict[formats.SPECIFIED_DAYS_DICT[key]] = key - - if not translated_str in rev_dict: - return - - # Each 'mini_list' is in the form [ day_string, time_string ] - # 'display_str' contains 'time_string', and a translated version of - # 'day_string' - # Compile the 'mini_list' for the new entry - mini_list = [ rev_dict[translated_str], time_str ] - - # Look for a match in the IV - new_list = [] - for other_list in self.retrieve_val('timetable_list'): - if other_list[0] != mini_list[0] \ - or other_list[1] != mini_list[1]: - new_list.append(other_list) - - # Update the IV and the treeview - self.edit_dict['timetable_list'] = new_list - self.setup_start_tab_update_treeview(liststore) - - - def on_start_mode_combo_changed(self, combo, label, spinbutton, combo2, \ - label2, combo3, spinbutton2, spinbutton3, button, button2): - - """Called from callback in self.setup_start_tab(). - - Sets the IV, and (de)sensitises other widgets. - - Args: - - label (Gtk.Label): A widget to be modified - - spinbutton (Gtk.SpinButton): Another widget to be modified - - combo2 (Gtk.Combo): Another widget to be modified - - label2 (Gtk.Label): Another widget to be modified - - combo3 (Gtk.Combo): Another widget to be modified - - spinbutton2, spinbutton3 (Gtk.SpinButton): Other widgets to be - modified - - button, button (Gtk.Button): Other widgets to be modified - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.edit_dict['start_mode'] = model[tree_iter][1] - - self.setup_start_tab_update_widgets( - label, - spinbutton, - combo2, - label2, - combo3, - spinbutton2, - spinbutton3, - button, - button2, - ) - - - def on_video_index_drag_drop(self, treeview, drag_context, x, y, time): - - """Called from callback in self.setup_media_tab(). - - Override the usual Gtk handler, and allow - self.on_video_index_drag_data_received() to collect the results of the - drag procedure. - - Args: - - treeview (Gtk.TreeView): This tab's treeview - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - x, y (int): Cell coordinates in the treeview - - time (int): A timestamp - - """ - - # Must override the usual Gtk handler - treeview.stop_emission('drag_drop') - - # The second of these lines cause the 'drag-data-received' signal to be - # emitted - target_list = drag_context.list_targets() - treeview.drag_get_data(drag_context, target_list[-1], time) - - - def on_video_index_drag_data_received(self, treeview, drag_context, x, y, \ - selection_data, info, timestamp): - - """Called from callback in self.setup_media_tab(). - - Retrieve the media data object being dragged. update the - media.Scheduled object, and update the treeview itself. - - Args: - - treeview (Gtk.TreeView): This tab's treeview - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - x, y (int): Cell coordinates in the treeview - - selection_data (Gtk.SelectionData): Data from the dragged row - - info (int): Ignored - - timestamp (int): Ignored - - """ - - # Must override the usual Gtk handler - treeview.stop_emission('drag_data_received') - - # Get the dragged media data object - old_selection \ - = self.app_obj.main_win_obj.video_index_treeview.get_selection() - (model, start_iter) = old_selection.get_selected() - if start_iter is not None: - - drag_dbid = model[start_iter][0] - - # Check the media data object hasn't already been added to the - # list, and that is still exists in the media data registry - media_list = self.retrieve_val('media_list') - if not drag_dbid in media_list \ - and drag_dbid in self.app_obj.container_reg_dict: - - # (System folders can't be dragged here) - media_data_obj = self.app_obj.media_reg_dict[drag_dbid] - - if not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.fixed_flag: - - media_list.append(drag_dbid) - self.edit_dict['media_list'] = media_list - - self.radiobutton2.set_active(True) - - # Update the treeview - self.setup_media_tab_update_treeview(treeview.get_model()) - - -class SystemPrefWin(GenericPrefWin): - - """Python class for a 'preference window' to modify various system - settings. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - init_mode (str or None): If specified, a tab is automatically selected; - one of the values specified in the comments to self.select_tab() - - """ - - - # Standard class methods - - - def __init__(self, app_obj, init_mode=None): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences window starts here.' \ - + ' In the menu, click Edit > System preferences...' - ) - - Gtk.Window.__init__(self, title=_('System preferences')) - - if self.is_duplicate(app_obj, init_mode): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.files_inner_notebook = None # Gtk.Notebook - self.operations_inner_notebook = None # Gtk.Notebook - self.downloader_inner_notebook = None # Gtk.Notebook - self.ok_button = None # Gtk.Button - # (IVs used to handle widget changes in the 'General' tab) - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.radiobutton3 = None # Gtk.RadioButton - self.radiobutton4 = None # Gtk.RadioButton - self.radiobutton5 = None # Gtk.RadioButton - self.spinbutton = None # Gtk.SpinButton - self.spinbutton2 = None # Gtk.SpinButton - self.spinbutton3 = None # Gtk.SpinButton - self.spinbutton4 = None # Gtk.SpinButton - # (IVs used to handle widget changes in the 'Files' tab) - self.entry = None # Gtk.Entry - self.entry2 = None # Gtk.Entry - self.url_liststore = None # Gtk.ListStore - # (IVs used to handle widget changes in the 'Custom' tab) - self.custom_liststore = None # Gtk.ListStore - # (IVs used to handle widget changes in the 'Livestream' tab) - self.livestream_radiobutton = None # Gtk.RadioButton - self.livestream_radiobutton2 = None # Gtk.RadioButton - self.livestream_radiobutton3 = None # Gtk.RadioButton - self.livestream_radiobutton4 = None # Gtk.RadioButton - self.livestream_radiobutton5 = None # Gtk.RadioButton - # (IVs used to hanle widget changes in the 'Forks' tab) - self.forks_radiobutton = None # Gtk.RadioButton - self.forks_radiobutton2 = None # Gtk.RadioButton - self.forks_radiobutton3 = None # Gtk.RadioButton - self.forks_entry = None # Gtk.RadioButton - # (IVs used to hanle widget changes in the 'File paths' tab) - self.filepaths_combo = None # Gkt.ComboBox - # (IVs used to handle widget changes in the 'Downloaders' tab) - self.path_liststore = None # Gtk.ListStore - self.cmd_liststore = None # Gtk.ListStore - # (IVs used to handle widget changes in the 'Scheduling' tab) - self.schedule_liststore = None # Gtk.ListStore - # (IVs used to handle widget changes in the 'Options' tab) - self.options_liststore = None # Gtk.ListStore - self.ffmpeg_liststore = None # Gtk.ListStore - # (IVs used to open the window at a particular tab) - self.filesinner_notebook = None # Gtk.Notebook - self.operations_inner_notebook = None # Gtk.Notebook - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between preference window widgets - self.spacing_size = self.app_obj.default_spacing_size - - # Code - # ---- - - # Set up the preference window - self.setup() - - # Automatically open a particular tab, if required - self.select_tab(init_mode) - - - # Public class methods - - - def is_duplicate(self, app_obj, init_mode=None): - - """Called by self.__init__. - - Don't open this preference window, if another preference window of the - same class is already open. - - If 'init_mode' is specified, switch the visible tab in the existing - preference window (if any). - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - init_mode (str or None): One of the values specified in the - comments to self.select_tab() - - Return values: - - True if a duplicate is found, False if not - - """ - - for config_win_obj in app_obj.main_win_obj.config_win_list: - - if type(self) == type(config_win_obj): - - # Duplicate found. Make it prominent... - config_win_obj.present() - # ...and switch to a particular tab, if required - config_win_obj.select_tab(init_mode) - - return True - - # Not a duplicate - return False - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericPrefWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - def select_tab(self, init_mode=None): - - """Called by self.__init__(). - - On startup, automatically open a particular tab, if required. - - Args: - - init_mode (str or None): If specified: - - 'clips' - video clip preferences - - 'custom_dl' - custom download preferences - - 'db' - options for switching the Tartube database - - 'downloads' - Download operation preferences - - 'forks' - youtube-dl forks, - - 'live' - livestream options - - 'options' - the list of download options - - 'paths' - youtube-dl paths, - - 'slices' - video slice preferences - - (any other value is ignored) - - """ - - if init_mode is not None: - - if init_mode == 'clips': - self.select_clips_tab() - elif init_mode == 'custom_dl': - self.select_custom_dl_tab() - elif init_mode == 'db': - self.select_switch_db_tab() - elif init_mode == 'downloads': - self.select_downloads_tab() - elif init_mode == 'forks': - self.select_forks_tab() - elif init_mode == 'live': - self.select_livestream_tab() - elif init_mode == 'options': - self.select_options_tab() - elif init_mode == 'paths': - self.select_paths_tab() - elif init_mode == 'slices': - self.select_slices_tab() - - - def select_clips_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the video clip preferences are - displayed. - """ - - # Opens tab: self.setup_operations_clips_tab() - if not self.app_obj.simple_prefs_flag: - self.notebook.set_current_page(4) - self.operations_inner_notebook.set_current_page(8) - else: - self.notebook.set_current_page(4) - self.operations_inner_notebook.set_current_page(7) - - - def select_custom_dl_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the custom download preferences - are displayed. - """ - - # Opens tab: self.setup_operations_custom_dl_tab() - self.notebook.set_current_page(4) - self.operations_inner_notebook.set_current_page(4) - - - def select_switch_db_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the user can set Tartube's - data directory (which contains the Tartube database file). - """ - - # Opens tab: self.setup_files_database_tab() - if not self.app_obj.simple_prefs_flag: - self.notebook.set_current_page(1) - self.files_inner_notebook.set_current_page(2) - else: - self.notebook.set_current_page(1) - self.files_inner_notebook.set_current_page(0) - - - def select_downloads_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the custom download preferences - are displayed. - """ - - # Opens tab: self.setup_operations_downloads_tab() - self.notebook.set_current_page(4) - self.operations_inner_notebook.set_current_page(2) - - - def select_forks_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the user can set youtube-dl - forks. - """ - - # Opens tab: self.setup_downloader_forks_tab() - self.notebook.set_current_page(5) - self.downloader_inner_notebook.set_current_page(0) - - - def select_livestream_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the user can set livestream - options. - """ - - # Opens tab: self.setup_operations_livestreams_tab() - if not self.app_obj.simple_prefs_flag: - self.notebook.set_current_page(4) - self.operations_inner_notebook.set_current_page(6) - else: - self.notebook.set_current_page(4) - self.operations_inner_notebook.set_current_page(5) - - - def select_options_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the list of download options is - displayed. - """ - - # Opens tab: self.setup_options_dl_list_tab() - self.notebook.set_current_page(6) - - - def select_paths_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the user can set youtube-dl - paths. - """ - - # Opens tab: self.setup_downloader_paths_tab() - self.notebook.set_current_page(5) - self.downloader_inner_notebook.set_current_page(1) - - - def select_slices_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the video slice preferences are - displayed. - """ - - # Opens tab: self.setup_operations_slices_tab() - if not self.app_obj.simple_prefs_flag: - self.notebook.set_current_page(4) - self.operations_inner_notebook.set_current_page(9) - else: - self.notebook.set_current_page(4) - self.operations_inner_notebook.set_current_page(8) - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this preference window. - """ - - self.setup_general_tab() - self.setup_files_tab() - self.setup_windows_tab() - self.setup_scheduling_tab() - self.setup_operations_tab() - self.setup_downloader_tab() - self.setup_options_tab() - self.setup_output_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > General' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_General'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_general_application_tab(inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_general_modules_tab(inner_notebook) - - - def setup_general_application_tab(self, inner_notebook): - - """Called by self.setup_general_tab(). - - Sets up the 'Application' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > General > Application' - ) - - # Import the main window (for convenience) - main_win_obj = self.app_obj.main_win_obj - - tab, grid = self.add_inner_notebook_tab( - _('_Application'), - inner_notebook, - ) - grid_width = 3 - - # Application details - self.add_label(grid, - '' + _('Application details') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Version'), - 0, 1, 1, 1, - ) - label.set_hexpand(False) - - self.entry = self.add_entry(grid, - __main__.__version__, - False, - 1, 1, 1, 1, - ) - - self.entry = self.add_entry(grid, - __main__.__date__, - False, - 2, 1, 1, 1, - ) - - label = self.add_label(grid, - _('Locale override'), - 0, 2, 1, 1, - ) - label.set_hexpand(False) - - store = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str) - store.append( - [ - self.app_obj.main_win_obj.pixbuf_dict['slice_small'], - _('Use your system locale'), - '' - ] - ) - - # (This list is for a second combo, using the same list as the one - # above, but not clickable by the user) - label2 = self.add_label(grid, - _('Current locale'), - 0, 3, 1, 1, - ) - label2.set_hexpand(False) - - store2 = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str) - store2.append( - [ - self.app_obj.main_win_obj.pixbuf_dict['slice_small'], - _('Unrecognised locale'), - '' - ] - ) - - for this_locale in formats.LOCALE_LIST: - - # (Some locales are in format "en_GB", some in format "fr") - if not 'flag_' + this_locale in \ - self.app_obj.main_win_obj.pixbuf_dict: - flag_locale = current_locale[:2] - else: - flag_locale = this_locale - - pixbuf = \ - self.app_obj.main_win_obj.pixbuf_dict['flag_' + flag_locale] - - store.append( - [ pixbuf, formats.LOCALE_DICT[this_locale], this_locale ], - ) - store2.append( - [ pixbuf, formats.LOCALE_DICT[this_locale], this_locale ], - ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 1, 2, 1, 1) - combo.set_hexpand(False) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 1) - - if self.app_obj.override_locale is None: - combo.set_active(0) - else: - combo.set_active( - formats.LOCALE_LIST.index(self.app_obj.override_locale) + 1 - ) - combo.connect('changed', self.on_locale_combo_changed, grid) - - combo2 = Gtk.ComboBox.new_with_model(store2) - grid.attach(combo2, 1, 3, 1, 1) - combo2.set_hexpand(False) - combo2.set_sensitive(False) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo2.pack_start(renderer_pixbuf, False) - combo2.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo2.pack_start(renderer_text, True) - combo2.add_attribute(renderer_text, 'text', 1) - - # !!! DEBUG - # Git 518: User reports .current_locale is set to 'en_MY', but I can't - # reproduce it - if self.app_obj.current_locale is None \ - or not self.app_obj.current_locale in formats.LOCALE_LIST: - combo2.set_active(0) - else: - combo2.set_active( - formats.LOCALE_LIST.index(self.app_obj.current_locale) + 1 - ) - - # Preferences mode - self.add_label(grid, - '' + _('Preferences mode') + '', - 0, 4, grid_width, 1, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 5, grid_width, 1) - - if self.app_obj.simple_prefs_flag: - frame = self.add_pixbuf(grid2, - 'hand_right_large', - 0, 0, 1, 1, - ) - frame.set_hexpand(False) - frame.set_size_request(75, -1) - - else: - frame = self.add_pixbuf(grid2, - 'hand_left_large', - 0, 0, 1, 1, - ) - frame.set_hexpand(False) - frame.set_size_request(75, -1) - - button = Gtk.Button() - grid2.attach(button, 1, 0, 1, 1) - if not self.app_obj.simple_prefs_flag: - button.set_label(_('Hide advanced preferences')) - else: - button.set_label(_('Show advanced preferences')) - button.set_hexpand(True) - button.connect('clicked', self.on_simple_prefs_clicked) - - - def setup_general_modules_tab(self, inner_notebook): - - """Called by self.setup_general_tab(). - - Sets up the 'Modules' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > General > Modules' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Modules'), inner_notebook) - grid_width = 2 - - # Module availability - self.add_label(grid, - '' + _('Module availability') + '', - 0, 0, grid_width, 1, - ) - - self.add_checkbutton(grid, - _( - 'feedparser module is available (required for detecting' \ - + ' livestreams)', - ), - mainapp.HAVE_FEEDPARSER_FLAG, - False, # Can't be toggled by user - 0, 1, grid_width, 1, - ) - - self.add_checkbutton(grid, - _('matplotlib module is available (draws graphs)'), - mainapp.HAVE_MATPLOTLIB_FLAG, - False, # Can't be toggled by user - 0, 2, grid_width, 1, - ) - - self.add_checkbutton(grid, - _( - 'moviepy module is available (finds the length of videos, if' \ - + ' unknown)', - ), - mainapp.HAVE_MOVIEPY_FLAG, - False, # Can't be toggled by user - 0, 3, grid_width, 1, - ) - - self.add_checkbutton(grid, - _( - 'playsound module is available (sound an alarm when a livestream' \ - + ' starts)', - ), - mainapp.HAVE_PLAYSOUND_FLAG, - False, # Can't be toggled by user - 0, 4, grid_width, 1, - ) - - self.add_checkbutton(grid, - _( - 'XDG module is available (saves the config file in the standard' \ - + ' location)', - ), - mainapp.HAVE_XDG_FLAG, - False, # Can't be toggled by user - 0, 5, grid_width, 1, - ) - - self.add_checkbutton(grid, - _( - 'Notify module is available (shows desktop notifications; Linux/' \ - + '*BSD only)', - ), - mainapp.HAVE_NOTIFY_FLAG, - False, # Can't be toggled by user - 0, 6, grid_width, 1, - ) - - # Module preferences - self.add_label(grid, - '' + _('Module preferences') + '', - 0, 7, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Use \'moviepy\' module to get a video\'s duration, if not known' - + ' (may be slow)', - ), - self.app_obj.use_module_moviepy_flag, - True, # Can be toggled by user - 0, 8, grid_width, 1, - ) - checkbutton.connect('toggled', self.on_moviepy_button_toggled) - if not mainapp.HAVE_MOVIEPY_FLAG: - checkbutton.set_sensitive(False) - - self.add_label(grid, - _('Timeout applied when moviepy checks a video file'), - 0, 9, 1, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 0, - 60, - 1, # Step - self.app_obj.refresh_moviepy_timeout, - 1, 9, 1, 1, - ) - spinbutton.connect( - 'value-changed', - self.on_moviepy_timeout_spinbutton_changed, - ) - - - def setup_files_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Files' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > xxx' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Files'), 0) - - # ...and an inner notebook... - self.files_inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - if not self.app_obj.simple_prefs_flag: - self.setup_files_device_tab(self.files_inner_notebook) - self.setup_files_config_tab(self.files_inner_notebook) - self.setup_files_database_tab(self.files_inner_notebook) - self.setup_files_backups_tab(self.files_inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_files_videos_tab(self.files_inner_notebook) - self.setup_files_delete_tab(self.files_inner_notebook) - self.setup_files_update_tab(self.files_inner_notebook) - self.setup_files_urls_tab(self.files_inner_notebook) - self.setup_files_temp_folders_tab(self.files_inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_files_statistics_tab(self.files_inner_notebook) - if mainapp.HAVE_MATPLOTLIB_FLAG: - self.setup_files_history_tab(self.files_inner_notebook) - - - def setup_files_device_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Device' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Device' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Device'), inner_notebook) - grid_width = 3 - - # Device preferences - self.add_label(grid, - '' + _('Device preferences') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - _('Size of device'), - 0, 3, 1, 1, - ) - - self.entry = self.add_entry(grid, - str(utils.disk_get_total_space(self.app_obj.data_dir)), - False, - 1, 3, 1, 1, - ) - self.entry.set_sensitive(False) - - self.add_label(grid, - 'GiB', - 2, 3, 1, 1, - ) - - self.add_label(grid, - _('Free space on device'), - 0, 4, 1, 1, - ) - - self.entry2 = self.add_entry(grid, - str(utils.disk_get_free_space(self.app_obj.data_dir)), - False, - 1, 4, 1, 1, - ) - self.entry2.set_sensitive(False) - - self.add_label(grid, - 'GiB', - 2, 4, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Before checking/downloading videos, warn user if disk space' \ - + ' is less than', - ), - self.app_obj.disk_space_warn_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - # (Signal connect appears below) - - spinbutton = self.add_spinbutton(grid, - 0, None, - self.app_obj.disk_space_increment, - self.app_obj.disk_space_warn_limit, - 1, 6, 1, 1, - ) - if not self.app_obj.disk_space_warn_flag: - spinbutton.set_sensitive(False) - # (Signal connect appears below) - - self.add_label(grid, - 'GiB', - 2, 6, 1, 1, - ) - - checkbutton2 = self.add_checkbutton(grid, - _('Halt downloads if disk space is less than'), - self.app_obj.disk_space_stop_flag, - True, # Can be toggled by user - 0, 7, 1, 1, - ) - # (Signal connect appears below) - - spinbutton2 = self.add_spinbutton(grid, - 0, None, - self.app_obj.disk_space_increment, - self.app_obj.disk_space_stop_limit, - 1, 7, 1, 1, - ) - if not self.app_obj.disk_space_stop_flag: - spinbutton2.set_sensitive(False) - # (Signal connect appears below) - - self.add_label(grid, - 'GiB', - 2, 7, 1, 1, - ) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_disk_warn_button_toggled, - spinbutton, - ) - spinbutton.connect( - 'value-changed', - self.on_disk_warn_spinbutton_changed, - ) - checkbutton2.connect( - 'toggled', - self.on_disk_stop_button_toggled, - spinbutton2, - ) - spinbutton2.connect( - 'value-changed', - self.on_disk_stop_spinbutton_changed, - ) - - - def setup_files_config_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Config' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Config' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Config'), inner_notebook) - - # Configuration preferences - self.add_label(grid, - '' + _('Configuration preferences') + '', - 0, 0, 1, 1, - ) - - self.add_label(grid, - _('Tartube configuration file loaded from'), - 0, 1, 1, 1, - ) - - entry = self.add_entry(grid, - self.app_obj.get_config_path(), - False, - 0, 2, 1, 1, - ) - entry.set_sensitive(False) - - self.add_label(grid, - _('Default location for Tartube configuration'), - 0, 3, 1, 1, - ) - - entry2 = self.add_entry(grid, - self.app_obj.config_file_xdg_path, - False, - 0, 4, 1, 1, - ) - entry2.set_sensitive(False) - - self.add_label(grid, - _('Alternative location for Tartube configuration'), - 0, 5, 1, 1, - ) - - entry3 = self.add_entry(grid, - self.app_obj.config_file_path, - False, - 0, 6, 1, 1, - ) - entry3.set_sensitive(False) - - - def setup_files_database_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Database' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Database' - ) - - tab, grid = self.add_inner_notebook_tab(_('D_atabase'), inner_notebook) - grid_width = 2 - - # Database preferences - self.add_label(grid, - '' + _('Database preferences') + '', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - _('Current data folder'), - 0, 1, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - self.app_obj.data_dir, - False, - 1, 1, 1, 1, - ) - entry.set_sensitive(False) - - label2 = self.add_label(grid, - _('Current database'), - 0, 2, 1, 1, - ) - label2.set_hexpand(False) - - entry2 = self.add_entry(grid, - os.path.abspath( - os.path.join( - self.app_obj.data_dir, self.app_obj.db_file_name, - ), - ), - False, - 1, 2, 1, 1, - ) - entry2.set_sensitive(False) - - label3 = self.add_label(grid, - _('Recent data folders'), - 0, 3, 1, 1, - ) - label3.set_hexpand(False) - - treeview, liststore = self.add_treeview(grid, - 1, 3, 1, 1, - ) - treeview.set_vexpand(False) - for item in self.app_obj.data_dir_alt_list: - liststore.append([item]) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 4, grid_width, 1) - - button = Gtk.Button(_('Add new database')) - grid2.attach(button, 0, 0, 1, 1) - button.set_tooltip_text(_('Change to a different data folder')) - button.set_hexpand(True) - button.connect( - 'clicked', - self.on_data_dir_change_button_clicked, - ) - - button2 = Gtk.Button(_('Check and repair database')) - grid2.attach(button2, 1, 0, 2, 1) - button2.set_tooltip_text( - _('Check for inconsistencies, and repair them'), - ) - button2.set_hexpand(True) - button2.connect('clicked', self.on_data_check_button_clicked) - - button3 = Gtk.Button(_('Dump database to JSON')) - grid2.attach(button3, 3, 0, 2, 1) - button3.set_tooltip_text( - _('Convert databases to JSON, even when Tartube can\'t load them'), - ) - button3.set_hexpand(True) - button3.connect('clicked', self.on_dump_db_button_clicked, treeview) - - button4 = Gtk.Button(_('Switch to this database')) - grid2.attach(button4, 0, 1, 1, 1) - button4.set_tooltip_text(_('Switch to the selected data folder')) - button4.set_hexpand(True) - button4.set_sensitive(False) - button4.connect( - 'clicked', - self.on_data_dir_switch_button_clicked, - button, - treeview, - ) - - button5 = Gtk.Button(_('Forget')) - grid2.attach(button5, 1, 1, 1, 1) - button5.set_tooltip_text( - _('Remove the selected data folder from the list'), - ) - button5.set_hexpand(True) - button5.set_sensitive(False) - button5.connect( - 'clicked', - self.on_data_dir_forget_button_clicked, - treeview, - ) - - button6 = Gtk.Button(_('Forget all')) - grid2.attach(button6, 2, 1, 1, 1) - button6.set_tooltip_text( - _('Forget every folder in this list (except the current one)'), - ) - button6.set_hexpand(True) - if len(self.app_obj.data_dir_alt_list) <= 1: - button6.set_sensitive(False) - button6.connect( - 'clicked', - self.on_data_dir_forget_all_button_clicked, - treeview, - ) - - button7 = Gtk.Button(_('Move up')) - grid2.attach(button7, 3, 1, 1, 1) - button7.set_tooltip_text( - _('Move the selected folder up the list'), - ) - button7.set_hexpand(True) - button7.set_sensitive(False) - # (Signal connect appears below) - - button8 = Gtk.Button(_('Move down')) - grid2.attach(button8, 4, 1, 1, 1) - button8.set_tooltip_text( - _('Move the selected folder down the list'), - ) - button8.set_hexpand(True) - button8.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - button7.connect( - 'clicked', - self.on_data_dir_move_up_button_clicked, - treeview, - liststore, - button8, - ) - button8.connect( - 'clicked', - self.on_data_dir_move_down_button_clicked, - treeview, - liststore, - button7, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid3 = self.add_secondary_grid(grid, 0, 5, grid_width, 1) - - checkbutton = self.add_checkbutton(grid3, - _( - 'On startup, load the first database on the list (not the most' \ - + ' recently-use one)', - ), - self.app_obj.data_dir_use_first_flag, - True, # Can be toggled by user - 0, 0, 2, 1, - ) - checkbutton.connect('toggled', self.on_use_first_button_toggled) - - checkbutton2 = self.add_checkbutton(grid3, - _('If one database is in use, try to load others'), - self.app_obj.data_dir_use_list_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton2.connect('toggled', self.on_use_list_button_toggled) - - checkbutton3 = self.add_checkbutton(grid3, - _('New databases are added to this list'), - self.app_obj.data_dir_add_from_list_flag, - True, # Can be toggled by user - 1, 1, 1, 1, - ) - checkbutton3.connect('toggled', self.on_add_from_list_button_toggled) - - # Everything must be desensitised, if load/save is disabled - if self.app_obj.disable_load_save_flag: - button.set_sensitive(False) - button2.set_sensitive(False) - button3.set_sensitive(False) - button4.set_sensitive(False) - button5.set_sensitive(False) - button6.set_sensitive(False) - button7.set_sensitive(False) - button8.set_sensitive(False) - checkbutton.set_sensitive(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_sensitive(False) - - # (More signal connects from above) - treeview.connect( - 'cursor-changed', - self.on_data_dir_cursor_changed, - button4, # Switch - button5, # Forget - button6, # Forget all - button7, # Move up - button8, # Move down - ) - - - def setup_files_backups_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Backups' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Backups' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Backups'), inner_notebook) - grid_width = 3 - - # Backup preferences - self.add_label(grid, - '' + _('Backup preferences') + '', - 0, 0, grid_width, 1, - ) - self.add_label(grid, - '' + _( - 'When saving the database, Tartube makes a backup copy of' \ - + ' its database file', - ) + '', - 0, 1, grid_width, 1, - ) - - radiobutton = self.add_radiobutton(grid, - None, - _('Delete the backup as soon as the database has been saved'), - 0, 2, grid_width, 1, - ) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _('Keep the backup file, replacing any previous backup file'), - 0, 3, grid_width, 1, - ) - if self.app_obj.db_backup_mode == 'single': - radiobutton2.set_active(True) - # (Signal connect appears below) - - radiobutton3 = self.add_radiobutton(grid, - radiobutton2, - _('Make a new backup file once per day'), - 0, 4, grid_width, 1, - ) - if self.app_obj.db_backup_mode == 'daily': - radiobutton3.set_active(True) - # (Signal connect appears below) - - radiobutton4 = self.add_radiobutton(grid, - radiobutton3, - _('Make a new backup file every time the database is saved'), - 0, 5, grid_width, 1, - ) - if self.app_obj.db_backup_mode == 'always': - radiobutton4.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_backup_button_toggled, - 'default', - ) - radiobutton2.connect( - 'toggled', - self.on_backup_button_toggled, - 'single', - ) - radiobutton3.connect( - 'toggled', - self.on_backup_button_toggled, - 'daily', - ) - radiobutton4.connect( - 'toggled', - self.on_backup_button_toggled, - 'always', - ) - - if not self.app_obj.simple_prefs_flag: - - # Export preferences - self.add_label(grid, - '' + _('Export preferences') + '', - 0, 6, grid_width, 1, - ) - - label = self.add_label(grid, - _('Separator used in CSV exports'), - 0, 7, 1, 1, - ) - label.set_hexpand(False) - - # (At the moment, Tartube only offers two choices of CSV separator) - combo = self.add_combo(grid, - ['|', ','], - self.app_obj.export_csv_separator, - 1, 7, 1, 1, - ) - combo.set_hexpand(False) - combo.connect('changed', self.on_separator_combo_changed) - - # (Empty label for spacing) - label = self.add_label(grid, - '', - 2, 1, 1, 1, - ) - label.set_hexpand(True) - - - def setup_files_videos_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Videos' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Videos' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Videos'), - inner_notebook, - ) - grid_width = 3 - - # Video matching preferences - self.add_label(grid, - '' + _('Video matching preferences') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - _('When matching videos on the filesystem:'), - 0, 1, grid_width, 1, - ) - - self.radiobutton3 = self.add_radiobutton(grid, - None, - _('The video names must match exactly'), - 0, 2, 1, 1, - ) - # (Signal connect appears below) - - self.radiobutton4 = self.add_radiobutton(grid, - self.radiobutton3, - _('The first # characters must match exactly'), - 0, 3, 1, 1, - ) - # (Signal connect appears below) - - self.spinbutton3 = self.add_spinbutton(grid, - 1, 999, 1, self.app_obj.match_first_chars, - 1, 3, 2, 1, - ) - # (Signal connect appears below) - - self.radiobutton5 = self.add_radiobutton(grid, - self.radiobutton4, - _( - 'Ignore the last # characters; the remaining name must match' \ - + ' exactly', - ), - 0, 4, 1, 1, - ) - # (Signal connect appears below) - - self.spinbutton4 = self.add_spinbutton(grid, - 1, 999, 1, self.app_obj.match_ignore_chars, - 1, 4, 2, 1, - ) - # (Signal connect appears below) - - # (Widgets are sensitised/desensitised, based on the radiobutton) - if self.app_obj.match_method == 'exact_match': - self.spinbutton3.set_sensitive(False) - self.spinbutton4.set_sensitive(False) - elif self.app_obj.match_method == 'match_first': - self.radiobutton4.set_active(True) - self.spinbutton4.set_sensitive(False) - else: - self.radiobutton5.set_active(True) - self.spinbutton3.set_sensitive(False) - - # (Signal connects from above) - self.radiobutton3.connect('toggled', self.on_match_button_toggled) - self.radiobutton4.connect('toggled', self.on_match_button_toggled) - self.radiobutton5.connect('toggled', self.on_match_button_toggled) - self.spinbutton3.connect( - 'value-changed', - self.on_match_spinbutton_changed, - ) - self.spinbutton4.connect( - 'value-changed', - self.on_match_spinbutton_changed, - ) - - self.add_label(grid, - '' + _('Video matching recommended preferences') + '', - 0, 5, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Check the video\'s original name and the downloaded file' \ - + ' name', - ), - self.app_obj.match_nickname_flag, - True, # Can be toggled by user - 0, 6, grid_width, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect('toggled', self.on_match_nickname_button_toggled) - - self.add_label(grid, - '' + _( - 'N.B. If disabled, custom file templates will interfere' \ - + ' with video matching' - ) + '', - 0, 7, grid_width, 1, - ) - - - def setup_files_delete_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Delete' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Delete' - ) - - tab, grid = self.add_inner_notebook_tab( - _('D_elete'), - inner_notebook, - ) - grid_width = 3 - - # Automatic video deletion/removal preferences - self.add_label(grid, - '' + _('Automatic video deletion/removal preferences') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'Deleted videos are re-downloaded without an archive file.' \ - + ' See the Operations > Archive tab', - ) + '', - 0, 1, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Automatically delete downloaded videos'), - self.app_obj.auto_delete_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - # (Signal connect appears below) - - self.spinbutton = self.add_spinbutton(grid, - 0, 999, 1, self.app_obj.auto_delete_days, - 1, 2, 1, 1, - ) - if not self.app_obj.auto_delete_flag: - self.spinbutton.set_sensitive(False) - # (Signal connect appears below) - - self.add_label(grid, - _('days'), - 2, 2, 1, 1, - ) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Remove downloaded videos from the database (but don\'t' \ - + ' delete files)', - ), - self.app_obj.auto_remove_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - # (Signal connect appears below) - - self.spinbutton2 = self.add_spinbutton(grid, - 0, 999, 1, self.app_obj.auto_remove_days, - 1, 3, 1, 1, - ) - if not self.app_obj.auto_remove_flag: - self.spinbutton2.set_sensitive(False) - # (Signal connect appears below) - - self.add_label(grid, - _('days'), - 2, 3, 1, 1, - ) - - checkbutton3 = self.add_checkbutton(grid, - _('Only delete/remove videos which have been watched'), - self.app_obj.auto_delete_watched_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - # (Signal connect appears below) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 5, grid_width, 1) - - self.add_label(grid2, - _('Delete/remove files:'), - 0, 0, 1, 1, - ) - - self.radiobutton = self.add_radiobutton(grid2, - None, - _('When the database is loaded'), - 1, 0, 1, 1, - ) - # (Signal connect appears below) - - self.radiobutton2 = self.add_radiobutton(grid2, - self.radiobutton, - _('After every download operation'), - 2, 0, 1, 1, - ) - if self.app_obj.auto_delete_asap_flag: - self.radiobutton2.set_active(True) - - if not self.app_obj.auto_delete_flag \ - and not self.app_obj.auto_remove_flag: - checkbutton3.set_sensitive(False) - self.radiobutton.set_sensitive(False) - self.radiobutton2.set_sensitive(False) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_auto_delete_videos_button_toggled, - self.spinbutton, - checkbutton2, - checkbutton3, - self.radiobutton, - self.radiobutton2, - ) - self.spinbutton.connect( - 'value-changed', - self.on_auto_delete_videos_spinbutton_changed, - ) - checkbutton2.connect( - 'toggled', - self.on_auto_remove_videos_button_toggled, - self.spinbutton2, - checkbutton, - checkbutton3, - self.radiobutton, - self.radiobutton2, - ) - self.spinbutton2.connect( - 'value-changed', - self.on_auto_remove_videos_spinbutton_changed, - ) - checkbutton3.connect('toggled', self.on_delete_watched_button_toggled) - self.radiobutton.connect('toggled', self.on_delete_asap_button_toggled) - - # Manual video deletion/removal preferences - self.add_label(grid, - '' + _('Manual video deletion/removal preferences') + '', - 0, 6, grid_width, 1, - ) - - checkbutton4 = self.add_checkbutton(grid, - _('Show dialogue window before removing video(s)'), - self.app_obj.show_delete_video_dialogue_flag, - True, # Can be toggled by user - 0, 7, grid_width, 1, - ) - checkbutton4.connect( - 'toggled', - self.on_show_delete_video_button_toggled, - ) - - checkbutton5 = self.add_checkbutton(grid, - _('When removing videos, remove all files from the filesystem'), - self.app_obj.delete_video_files_flag, - True, # Can be toggled by user - 0, 8, grid_width, 1, - ) - checkbutton5.connect( - 'toggled', - self.on_remove_video_file_button_toggled, - ) - - checkbutton6 = self.add_checkbutton(grid, - _( - 'Show dialogue window before removing channels/playlists' \ - + '/folders', - ), - self.app_obj.show_delete_container_dialogue_flag, - True, # Can be toggled by user - 0, 9, grid_width, 1, - ) - checkbutton6.connect( - 'toggled', - self.on_show_delete_container_button_toggled, - ) - - checkbutton7 = self.add_checkbutton(grid, - _( - 'When removing containers, remove all files from the' \ - + ' filesystem', - ), - self.app_obj.delete_container_files_flag, - True, # Can be toggled by user - 0, 10, grid_width, 1, - ) - checkbutton7.connect( - 'toggled', - self.on_remove_container_file_button_toggled, - ) - - - def setup_files_update_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Update' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Update' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Update'), - inner_notebook, - ) - grid_width = 2 - - # Update video descriptions - self.add_label(grid, - '' + _('Update video descriptions') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' \ - + _( - 'These procedures might take a long time on a large database', - ) \ - + '', - 0, 1, grid_width, 1, - ) - - button = Gtk.Button.new_with_label( - _('Update from description files, and set the line lengths to:'), - ) - grid.attach(button, 0, 2, 1, 1) - # (Signal connect appears below) - - min_value = self.app_obj.main_win_obj.medium_string_max_len - max_value = self.app_obj.main_win_obj.descrip_line_max_len - if max_value < min_value: - max_value = min_value - - spinbutton = self.add_spinbutton(grid, - min_value, - max_value, - 1, - self.app_obj.main_win_obj.descrip_line_max_len, - 1, 2, 1, 1, - ) - - button2 = Gtk.Button.new_with_label( - _('Clear descriptions (does not modify the description files)'), - ) - grid.attach(button2, 0, 3, grid_width, 1) - # (Signal connect appears below) - - # (Signal connects from above) - button.connect( - 'clicked', - self.on_load_descrips_button_clicked, - spinbutton, - ) - - button2.connect( - 'clicked', - self.on_clear_descrips_button_clicked, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 4, grid_width, 1) - - # Video timestamps - self.add_label(grid2, - '' + _('Video timestamps') + '', - 0, 4, grid_width, 1, - ) - - button3 = Gtk.Button(_('Extract timestamps for all videos')) - grid2.attach(button3, 0, 5, 1, 1) - button3.set_hexpand(False) - button3.connect( - 'clicked', - self.on_extract_stamps_button_clicked, - ) - - button4 = Gtk.Button(_('Remove timestamps from all videos')) - grid2.attach(button4, 1, 5, 1, 1) - button4.set_hexpand(False) - button4.connect( - 'clicked', - self.on_remove_stamps_button_clicked, - ) - - # Video comments - self.add_label(grid2, - '' + _('Video comments') + '', - 0, 6, grid_width, 1, - ) - - button5 = Gtk.Button(_('Extract comments for all videos')) - grid2.attach(button5, 0, 7, 1, 1) - button5.set_hexpand(False) - button5.connect( - 'clicked', - self.on_extract_comments_button_clicked, - ) - - button6 = Gtk.Button(_('Remove comments from all videos')) - grid2.attach(button6, 1, 7, 1, 1) - button6.set_hexpand(False) - button6.connect( - 'clicked', - self.on_remove_comments_button_clicked, - ) - - self.add_label(grid, - '' + _( - 'Comments are extracted from each video\'s metadata file,' \ - + ' so this procedure may take a long time', - ) + '', - 0, 8, grid_width, 1, - ) - - - def setup_files_urls_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'URLs' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > URLs' - ) - - tab, grid = self.add_inner_notebook_tab( - _('U_RLs'), - inner_notebook, - ) - grid_width = 2 - - # Update channel/playlist URLs - self.add_label(grid, - '' + _('Update channel/playlist URLs') + '', - 0, 0, (grid_width - 1), 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Confirm every change'), - self.app_obj.url_change_confirm_flag, - True, # Can be toggled by user - (grid_width - 1), 0, 1, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect( - 'toggled', - self.on_confirm_url_button_toggled, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - # (Allow multiple selection) - treeview.set_can_focus(True) - selection = treeview.get_selection() - selection.set_mode(Gtk.SelectionMode.MULTIPLE) - - for i, column_title in enumerate( - [ 'hide', '', _('Name'), _('URL') ], - ): - if i == 1: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - column_title, - renderer_pixbuf, - pixbuf=i, - ) - treeview.append_column(column_pixbuf) - column_pixbuf.set_resizable(False) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - if i == 0: - column_text.set_visible(False) - elif i == 2: - renderer_text.set_property('editable', True) - renderer_text.connect( - 'edited', - self.on_container_name_edited, - treeview, - checkbutton, - ) - elif i == 3: - renderer_text.set_property('editable', True) - renderer_text.connect( - 'edited', - self.on_container_url_edited, - treeview, - checkbutton, - ) - - self.url_liststore = Gtk.ListStore( - int, GdkPixbuf.Pixbuf, str, str, - ) - treeview.set_model(self.url_liststore) - - # Initialise the list - self.setup_files_urls_tab_update_treeview() - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1) - - # Strip of widgets beneath the list - self.add_label(grid2, - _('Pattern'), - 0, 0, 1, 1, - ) - - entry = self.add_entry(grid2, - None, - True, - 1, 0, 1, 1, - ) - - self.add_label(grid2, - _('Substitution'), - 2, 0, 1, 1, - ) - - entry2 = self.add_entry(grid2, - None, - True, - 3, 0, 1, 1, - ) - - checkbutton2 = self.add_checkbutton(grid2, - _('This pattern is a regex'), - self.app_obj.url_change_regex_flag, - True, # Can be toggled by user - 0, 1, 2, 1, - ) - checkbutton2.set_hexpand(False) - checkbutton2.connect( - 'toggled', - self.on_url_regex_button_toggled, - ) - - button = Gtk.Button( - _('Search and replace text in the selected URLs'), - ) - grid2.attach(button, 2, 1, 2, 1) - button.set_hexpand(True) - button.connect( - 'clicked', - self.on_container_url_multiple_edited, - entry, - entry2, - treeview, - ) - - button2 = Gtk.Button() - grid2.attach(button2, 2, 2, 1, 1) - button2.set_hexpand(True) - button2.set_label(_('Open URLs')) - button2.connect( - 'clicked', - self.on_open_url_clicked, - treeview, - ) - - button3 = Gtk.Button() - grid2.attach(button3, 3, 2, 1, 1) - button3.set_hexpand(True) - button3.set_label(_('Refresh list')) - button3.connect( - 'clicked', - self.setup_files_urls_tab_update_treeview, - ) - - - def setup_files_urls_tab_update_treeview(self, button=None): - - """ Called by self.setup_files_urls_tab(). - - Fills or updates the treeview. - - Args: - - button (Gtk.Button): The widget clicked (if applicable) - - """ - - self.url_liststore.clear() - - # Prepare a sorted list of channels/playlists to display in the - # treeview - obj_list = [] - for media_data_obj in self.app_obj.container_reg_dict.values(): - - if isinstance(media_data_obj, media.Channel) \ - or isinstance(media_data_obj, media.Playlist): - obj_list.append(media_data_obj) - - obj_list.sort(key=lambda x: x.name.lower()) - - # Add each channel/playlist to the treeview, one row at a time - for media_data_obj in obj_list: - self.setup_files_urls_tab_add_row(media_data_obj) - - - def setup_files_urls_tab_add_row(self, media_data_obj): - - """Called by self.setup_scheduling_start_tab_update_treeview() and - .on_scheduled_add_button_clicked(). - - Adds a row to the treeview. - - Args: - - media_data_obj (media.Channel, media.Playlist): The media data - object to display on this row - - """ - - if isinstance(media_data_obj, media.Channel): - pixbuf = self.app_obj.main_win_obj.pixbuf_dict['channel_small'] - else: - pixbuf = self.app_obj.main_win_obj.pixbuf_dict['playlist_small'] - - row_list = [] - row_list.append(media_data_obj.dbid) - row_list.append(pixbuf) - row_list.append(media_data_obj.name) - row_list.append(media_data_obj.source) - - self.url_liststore.append(row_list) - - - def setup_files_temp_folders_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Temporary folders' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Temporary' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Temporary'), - inner_notebook, - ) - - # Temporary folder preferences - self.add_label(grid, - '' + _('Temporary folder preferences') + '', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Empty temporary folders when Tartube shuts down'), - self.app_obj.delete_on_shutdown_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - # (Signal connect appears below) - - self.add_label(grid, - '' + _( - '(N.B. Temporary folders are always emptied when Tartube' \ - + ' starts up)', - ) + '', - 0, 2, 1, 1, - ) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Open temporary folders (on the desktop) when Tartube shuts down', - ), - self.app_obj.open_temp_on_desktop_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton2.connect('toggled', self.on_open_desktop_button_toggled) - if self.app_obj.delete_on_shutdown_flag: - checkbutton2.set_sensitive(False) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_delete_shutdown_button_toggled, - checkbutton2, - ) - - - def setup_files_statistics_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Statistics' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > Statistics' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Statistics'), - inner_notebook, - ) - grid_width = 4 - - # Statistics - self.add_label(grid, - '' + _('Statistics') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - _('The Tartube database contains:'), - 0, 1, grid_width, 1, - ) - - self.add_label(grid, - _('Videos'), - 0, 2, 1, 1, - ) - - entry = self.add_entry(grid, - None, - False, - 1, 2, 1, 1, - ) - - self.add_label(grid, - _('Downloaded'), - 0, 3, 1, 1, - ) - - entry2 = self.add_entry(grid, - None, - False, - 1, 3, 1, 1, - ) - - self.add_label(grid, - _('Other'), - 0, 4, 1, 1, - ) - - entry3 = self.add_entry(grid, - None, - False, - 1, 4, 1, 1, - ) - - self.add_label(grid, - _('Channels'), - 2, 2, 1, 1, - ) - - entry4 = self.add_entry(grid, - None, - False, - 3, 2, 1, 1, - ) - - self.add_label(grid, - _('Playlists'), - 2, 3, 1, 1, - ) - - entry5 = self.add_entry(grid, - None, - False, - 3, 3, 1, 1, - ) - - self.add_label(grid, - _('Custom folders'), - 2, 4, 1, 1, - ) - - entry6 = self.add_entry(grid, - None, - False, - 3, 4, 1, 1, - ) - - # Initialise the entries. Commented out so that the preference window - # will still appear quickly for enormous databases -# self.setup_files_statistics_tab_recalculate( -# entry, -# entry2, -# entry3, -# entry4, -# entry5, -# entry6, -# ) - - button = Gtk.Button() - grid.attach(button, 3, 5, 1, 1) - button.set_label(_('Calculate')) - button.connect( - 'clicked', - self.on_recalculate_stats_button_clicked, - entry, - entry2, - entry3, - entry4, - entry5, - entry6, - ) - - - def setup_files_statistics_tab_recalculate(self, entry, entry2, entry3, - entry4, entry5, entry6): - - """Called by self.setup_files_statistics_tab and - .on_recalculate_stats_button_clicked(). - - Args: - - entry, entry2, entry3, entry4, entry5, entry6 (Gtk.Entry): The - entry boxes to update - - """ - - video_count = 0 - dl_count = 0 - not_dl_count = 0 - channel_count = 0 - playlist_count = 0 - folder_count = 0 - - # Get number of videos, channels, playlists and sub-folders, and also - # downloaded/not downloaded videos - # Ignore fixed (system) folders - for media_data_obj in self.app_obj.media_reg_dict.values(): - - if isinstance(media_data_obj, media.Video): - - video_count += 1 - - if media_data_obj.dl_flag: - dl_count += 1 - else: - not_dl_count += 1 - - elif isinstance(media_data_obj, media.Channel): - - channel_count += 1 - - elif isinstance(media_data_obj, media.Playlist): - - playlist_count += 1 - - elif isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag: - - folder_count += 1 - - entry.set_text(str(video_count)) - entry2.set_text(str(dl_count)) - entry3.set_text(str(not_dl_count)) - entry4.set_text(str(channel_count)) - entry5.set_text(str(playlist_count)) - entry6.set_text(str(folder_count)) - - - def setup_files_history_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'History' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Files > History' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_History'), - inner_notebook, - ) - grid_width = 6 - - # Download history - self.add_label(grid, - '' + _('Download history') + '', - 0, 0, grid_width, 1, - ) - - # Add combos to customise the graph - combo, combo2, combo3, combo4, combo5 = self.add_combos_for_graphs( - grid, - 1, - ) - - # Add a button which, when clicked, draws the graph using the - # customisation options specified by the combos - button = Gtk.Button() - grid.attach(button, 5, 1, 1, 1) - button.set_label(_('Draw')) - # (Signal connect appears below) - - # Add a box, inside which we draw graphs - hbox = Gtk.HBox() - grid.attach(hbox, 0, 2, grid_width, 1) - hbox.set_hexpand(True) - hbox.set_vexpand(True) - - # (Signal connects from above) - button.connect( - 'clicked', self.on_button_draw_graph_clicked, - hbox, - combo, - combo2, - combo3, - combo4, - combo5, - ) - - - def setup_windows_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Window' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Windows'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_windows_main_window_tab(inner_notebook) - self.setup_windows_videos_tab(inner_notebook) - self.setup_windows_drag_tab(inner_notebook) - self.setup_windows_system_tray_tab(inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_windows_dialogues_tab(inner_notebook) - self.setup_windows_colours_tab(inner_notebook) - self.setup_windows_errors_warnings_tab(inner_notebook) - self.setup_windows_websites_tab(inner_notebook) - - - def setup_windows_main_window_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Main Window' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows > Main Window' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Main window'), - inner_notebook, - ) - grid_width = 3 - - # Main window preferences - self.add_label(grid, - '' + _('Main window preferences') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Remember size of the main window'), - self.app_obj.main_win_save_size_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - # (Signal connect appears below) - - checkbutton2 = self.add_checkbutton(grid, - _('Remember slider positions'), - self.app_obj.main_win_save_slider_flag, - True, # Can be toggled by user - 1, 1, 1, 1, - ) - checkbutton2.connect('toggled', self.on_remember_slider_button_toggled) - if not self.app_obj.main_win_save_size_flag: - checkbutton2.set_sensitive(False) - - button = Gtk.Button(_('Reset both')) - grid.attach(button, 2, 1, 1, 1) - button.set_hexpand(True) - button.connect( - 'clicked', - self.on_reset_size_clicked, - ) - - # (Signal connect from above) - checkbutton.connect( - 'toggled', - self.on_remember_size_button_toggled, - checkbutton2, - ) - - checkbutton3 = self.add_checkbutton(grid, - _('Don\'t show the main window toolbar'), - self.app_obj.toolbar_hide_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - # (Signal connect appears below) - - if not self.app_obj.simple_prefs_flag: - - checkbutton4 = self.add_checkbutton(grid, - _('Don\'t show labels in the main window toolbar'), - self.app_obj.toolbar_squeeze_flag, - True, # Can be toggled by user - 1, 2, 2, 1, - ) - checkbutton4.connect('toggled', self.on_squeeze_button_toggled) - if self.app_obj.toolbar_hide_flag: - checkbutton4.set_sensitive(False) - - # (Signal connect from above) - checkbutton3.connect( - 'toggled', - self.on_hide_toolbar_button_toggled, - checkbutton4, - ) - - checkbutton5 = self.add_checkbutton(grid, - _( - 'Replace stock icons with custom icons (in case stock icons' \ - + ' are not visible)', - ), - self.app_obj.show_custom_icons_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton5.connect('toggled', self.on_show_custom_icons_toggled) - - if not self.app_obj.simple_prefs_flag: - - checkbutton6 = self.add_checkbutton(grid, - _('Show tooltips for videos, channels, playlists and folders'), - self.app_obj.show_tooltips_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - # (Signal connect appears below) - - checkbutton7 = self.add_checkbutton(grid, - _( - 'Show errors/warnings in tooltips (but not in the Videos' \ - + ' tab)', - ), - self.app_obj.show_tooltips_extra_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - checkbutton7.connect( - 'toggled', - self.on_show_tooltips_extra_toggled, - ) - if not self.app_obj.show_tooltips_flag: - checkbutton7.set_sensitive(False) - - # (Signal connect from above) - checkbutton6.connect( - 'toggled', - self.on_show_tooltips_toggled, - checkbutton7, - ) - - checkbutton8 = self.add_checkbutton(grid, - _( - 'Disable the download buttons in the toolbar and the Videos tab', - ), - self.app_obj.disable_dl_all_flag, - True, # Can be toggled by user - 0, 6, grid_width, 1, - ) - checkbutton8.connect('toggled', self.on_disable_dl_all_toggled) - - checkbutton9 = self.add_checkbutton(grid, - _( - 'In the Progress tab, hide finished downloads', - ), - self.app_obj.progress_list_hide_flag, - True, # Can be toggled by user - 0, 7, 1, 1, - ) - checkbutton9.connect('toggled', self.on_hide_button_toggled) - - checkbutton10 = self.add_checkbutton(grid, - _('Show downloads in reverse order'), - self.app_obj.results_list_reverse_flag, - True, # Can be toggled by user - 1, 7, 2, 1, - ) - checkbutton10.connect('toggled', self.on_reverse_button_toggled) - - checkbutton11 = self.add_checkbutton(grid, - _( - 'In the Progress/Classic Mode tabs, remember the width of' \ - + ' (some) columns', - ), - self.app_obj.progress_list_remember_width_flag, - True, # Can be toggled by user - 0, 8, grid_width, 1, - ) - checkbutton11.connect('toggled', self.on_remember_width_button_toggled) - - checkbutton12 = self.add_checkbutton(grid, - _('When Tartube starts, automatically open the Classic Mode tab'), - self.app_obj.show_classic_tab_on_startup_flag, - True, # Can be toggled by user - 0, 9, grid_width, 1, - ) - checkbutton12.connect( - 'toggled', - self.on_show_classic_mode_button_toggled, - ) - if __main__.__pkg_no_download_flag__: - checkbutton12.set_sensitive(False) - - if not self.app_obj.simple_prefs_flag: - - checkbutton13 = self.add_checkbutton(grid, - _( - 'In the Classic Mode tab, when adding URLs, remove' \ - + ' duplicates rather than retaining them', - ), - self.app_obj.classic_duplicate_remove_flag, - True, # Can be toggled by user - 0, 10, grid_width, 1, - ) - checkbutton13.connect( - 'toggled', - self.on_remove_duplicate_button_toggled, - ) - - checkbutton14 = self.add_checkbutton(grid, - _( - 'In the Errors/Warnings tab, don\'t reset the tab title when' \ - + ' it is clicked', - ), - self.app_obj.system_msg_keep_totals_flag, - True, # Can be toggled by user - 0, 11, grid_width, 1, - ) - checkbutton14.connect( - 'toggled', - self.on_system_keep_button_toggled, - ) - - - def setup_windows_videos_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Tabs' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows > Videos' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Videos'), inner_notebook) - grid_width = 2 - - # Video Index (left side of the Videos tab) - self.add_label(grid, - '' + _('Video Index (left side of the Videos tab)') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Show a \'Custom download all\' button'), - self.app_obj.show_custom_dl_button_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.connect('toggled', self.on_show_custom_dl_button_toggled) - - if not self.app_obj.simple_prefs_flag: - - checkbutton2 = self.add_checkbutton(grid, - _('While checking/downloading videos, show free disk space'), - self.app_obj.show_free_space_flag, - True, # Can be toggled by user - 0, 2, grid_width, 1, - ) - checkbutton2.connect( - 'toggled', - self.on_show_free_space_button_toggled, - ) - - checkbutton3 = self.add_checkbutton(grid, - _('Allow each row to be marked for checking/downloading'), - self.app_obj.show_marker_in_index_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton3.connect( - 'toggled', - self.on_show_selector_button_toggled, - ) - - checkbutton4 = self.add_checkbutton(grid, - _('Show smaller icons'), - self.app_obj.show_small_icons_in_index_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - checkbutton4.connect('toggled', self.on_show_small_icons_toggled) - - checkbutton5 = self.add_checkbutton(grid, - _( - 'Show detailed statistics about the videos in each channel' \ - + ' / playlist / folder', - ), - self.app_obj.complex_index_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - checkbutton5.connect('toggled', self.on_complex_button_toggled) - - checkbutton6 = self.add_checkbutton(grid, - _( - 'After clicking on a folder, automatically expand/collapse the' \ - + ' tree around it', - ), - self.app_obj.auto_expand_video_index_flag, - True, # Can be toggled by user - 0, 6, grid_width, 1, - ) - # (Signal connect appears below) - - checkbutton7 = self.add_checkbutton(grid, - _( - 'Expand the whole tree, not just the level beneath the clicked' \ - + ' folder', - ), - self.app_obj.full_expand_video_index_flag, - True, # Can be toggled by user - 0, 7, grid_width, 1, - ) - if not self.app_obj.auto_expand_video_index_flag: - checkbutton7.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - checkbutton6.connect( - 'toggled', - self.on_expand_tree_toggled, - checkbutton7, - ) - checkbutton7.connect('toggled', self.on_expand_full_tree_toggled) - - # Video Catalogue (right side of the Videos tab) - self.add_label(grid, - '' + _('Video Catalogue (right side of the Videos tab)') \ - + '', - 0, 8, grid_width, 1, - ) - - checkbutton8 = self.add_checkbutton(grid, - _('Show \'today\' and \'yesterday\' as the date, when possible'), - self.app_obj.show_pretty_dates_flag, - True, # Can be toggled by user - 0, 9, grid_width, 1, - ) - checkbutton8.connect('toggled', self.on_pretty_date_button_toggled) - - checkbutton9 = self.add_checkbutton(grid, - _('Show livestreams with a different background colour'), - self.app_obj.livestream_use_colour_flag, - True, # Can be toggled by user - 0, 10, grid_width, 1, - ) - # (Signal connect appears below) - - checkbutton10 = self.add_checkbutton(grid, - _('Use same background colours for livestream and debut videos'), - self.app_obj.livestream_simple_colour_flag, - True, # Can be toggled by user - 0, 11, grid_width, 1, - ) - if not self.app_obj.livestream_use_colour_flag: - checkbutton10.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - checkbutton9.connect( - 'toggled', - self.on_livestream_colour_button_toggled, - checkbutton10, - ) - checkbutton10.connect( - 'toggled', - self.on_livestream_simple_button_toggled, - ) - - if not self.app_obj.simple_prefs_flag: - - checkbutton11 = self.add_checkbutton(grid, - _('Channel and playlist names are clickable (grid mode only)'), - self.app_obj.catalogue_clickable_container_flag, - True, # Can be toggled by user - 0, 12, grid_width, 1, - ) - checkbutton11.connect('toggled', self.on_clickable_button_toggled) - - checkbutton12 = self.add_checkbutton(grid, - _('Show nicknames (not video file names)'), - self.app_obj.catalogue_show_nickname_flag, - True, # Can be toggled by user - 0, 13, grid_width, 1, - ) - checkbutton12.connect('toggled', self.on_nickname_button_toggled) - - - def setup_windows_drag_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Drag' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows > Drag' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Drag'), inner_notebook) - - # Drag and drop preferences - self.add_label(grid, - '' + _('Drag and drop preferences') + '', - 0, 0, 1, 1, - ) - - self.add_label(grid, - '' + _( - 'When dragging and dropping videos to an external application...', - ) + '', - 0, 1, 2, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Transfer the video\'s full file path'), - self.app_obj.drag_video_path_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton.connect('toggled', self.on_drag_path_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _('Transfer the video\'s source URL'), - self.app_obj.drag_video_source_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton2.connect('toggled', self.on_drag_source_button_toggled) - - checkbutton3 = self.add_checkbutton(grid, - _('Transfer the video\'s name'), - self.app_obj.drag_video_name_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - checkbutton3.connect('toggled', self.on_drag_name_button_toggled) - - checkbutton4 = self.add_checkbutton(grid, - _('Transfer error/warning messages'), - self.app_obj.drag_video_msg_flag, - True, # Can be toggled by user - 1, 2, 1, 1, - ) - checkbutton4.connect('toggled', self.on_drag_msg_button_toggled) - - checkbutton5 = self.add_checkbutton(grid, - _('Transfer the thumbnail\'s full file path'), - self.app_obj.drag_thumb_path_flag, - True, # Can be toggled by user - 1, 3, 1, 1, - ) - checkbutton5.connect('toggled', self.on_drag_thumb_button_toggled) - - checkbutton6 = self.add_checkbutton(grid, - _('Add a text separator before each item'), - self.app_obj.drag_video_separator_flag, - True, # Can be toggled by user - 1, 4, 1, 1, - ) - checkbutton6.connect('toggled', self.on_drag_separator_button_toggled) - - self.add_label(grid, - '' + _( - 'When dragging and dropping messages from the Errors/Warnings' \ - + ' tab to an external application...', - ) + '', - 0, 5, 2, 1, - ) - - checkbutton7 = self.add_checkbutton(grid, - _('Transfer the video/channel/playlist file path'), - self.app_obj.drag_error_path_flag, - True, # Can be toggled by user - 0, 6, 1, 1, - ) - checkbutton7.connect('toggled', self.on_drag_error_path_button_toggled) - - checkbutton8 = self.add_checkbutton(grid, - _('Transfer the video/channel/playlist URL'), - self.app_obj.drag_error_source_flag, - True, # Can be toggled by user - 0, 7, 1, 1, - ) - checkbutton8.connect( - 'toggled', - self.on_drag_error_source_button_toggled, - ) - - checkbutton9 = self.add_checkbutton(grid, - _('Transfer the video/channel/playlist name'), - self.app_obj.drag_error_name_flag, - True, # Can be toggled by user - 0, 8, 1, 1, - ) - checkbutton9.connect( - 'toggled', - self.on_drag_error_name_button_toggled, - ) - - checkbutton10 = self.add_checkbutton(grid, - _('Transfer error/warning messages'), - self.app_obj.drag_error_msg_flag, - True, # Can be toggled by user - 1, 6, 1, 1, - ) - checkbutton10.connect( - 'toggled', - self.on_drag_error_msg_button_toggled, - ) - - checkbutton11 = self.add_checkbutton(grid, - _('Add a text separator before each item'), - self.app_obj.drag_error_separator_flag, - True, # Can be toggled by user - 1, 7, 1, 1, - ) - checkbutton11.connect( - 'toggled', - self.on_drag_error_separator_button_toggled, - ) - - - def setup_windows_system_tray_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'System tray' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows > Tray' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Tray'), - inner_notebook, - ) - - # System tray preferences - self.add_label(grid, - '' + _('System tray preferences') + '', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Show Tartube in the system tray'), - self.app_obj.show_status_icon_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.set_hexpand(False) - # (Signal connect appears below) - - checkbutton2 = self.add_checkbutton(grid, - _('Start Tartube in the system tray'), - self.app_obj.open_in_tray_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.set_hexpand(False) - # (Signal connect appears below) - if not self.app_obj.show_status_icon_flag: - checkbutton2.set_sensitive(False) - - checkbutton3 = self.add_checkbutton(grid, - _('Close to the tray, rather than closing the application'), - self.app_obj.close_to_tray_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton3.set_hexpand(False) - # (Signal connect appears below) - if not self.app_obj.show_status_icon_flag: - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid, - _( - 'After closing to the tray, restore the window\'s position' \ - + ' (does not work on Wayland)', - ), - self.app_obj.restore_posn_from_tray_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - checkbutton4.set_hexpand(False) - # (Signal connect appears below) - if not self.app_obj.show_status_icon_flag \ - or not self.app_obj.close_to_tray_flag: - checkbutton4.set_sensitive(False) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_show_status_icon_toggled, - checkbutton2, - checkbutton3, - checkbutton4, - ) - checkbutton2.connect('toggled', self.on_open_in_tray_toggled) - checkbutton3.connect( - 'toggled', - self.on_close_to_tray_toggled, - checkbutton4, - ) - checkbutton4.connect('toggled', self.on_restore_from_tray_toggled) - - - def setup_windows_dialogues_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Dialogues' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows > Dialogues' - ) - - tab, grid = self.add_inner_notebook_tab( - _('D_ialogues'), - inner_notebook, - ) - - # Dialogue window preferences - self.add_label(grid, - '' + _('Dialogue window preferences') + '', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('When adding channels/playlists, keep the dialogue window open'), - self.app_obj.dialogue_keep_open_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.set_hexpand(False) - # (Signal connect appears below) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'When the dialogue window opens, add URLs from the system' \ - + ' clipboard', - ), - self.app_obj.dialogue_copy_clipboard_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.set_hexpand(False) - checkbutton2.connect('toggled', self.on_clipboard_button_toggled) - if self.app_obj.dialogue_keep_open_flag: - checkbutton2.set_sensitive(False) - - # (Signal connect from above) - checkbutton.connect( - 'toggled', - self.on_keep_open_button_toggled, - checkbutton2, - ) - - checkbutton3 = self.add_checkbutton(grid, - _( - 'When adding YouTube channels, remind the user to copy the' \ - + ' correct URL', - ), - self.app_obj.dialogue_yt_remind_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton3.set_hexpand(False) - checkbutton3.connect('toggled', self.on_yt_remind_button_toggled) - - # Debugging preferences - self.add_label(grid, - '' + _('Debugging preferences') + '', - 0, 4, 1, 1, - ) - - checkbutton4 = self.add_checkbutton(grid, - _( - 'Temporarily disable message dialogue windows (display messages' \ - + ' in terminal instead)', - ), - self.app_obj.dialogue_disable_msg_flag, - True, # Can be toggled by user - 0, 5, 1, 1, - ) - checkbutton4.set_hexpand(False) - checkbutton4.connect('toggled', self.on_dialogue_disable_toggled) - - self.add_label(grid, - '' + _( - 'N.B. Tartube shows a dialogue window after checking or' \ - + ' downloading videos', - ) + '', - 0, 7, 1, 1, - ) - - self.add_label(grid, - '' + _( - 'That dialogue window can be disabled in the Operations >' \ - + ' Actions tab', - ) + '', - 0, 8, 1, 1, - ) - - - def setup_windows_colours_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Colours' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows > Colours' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Colours'), - inner_notebook, - ) - grid_width = 5 - - # Video catalogue colour preferences - self.add_label(grid, - '' + _('Video catalogue colour preferences') + '', - 0, 0, grid_width, 1, - ) - - self.setup_windows_colours_tab_add_row(grid, - 1, - # Key in mainapp.TartubeApp.custom_bg_table - 'live_wait', - _('Waiting livestreams'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 2, - 'live_now', - _('Broadcasting livestreams'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 3, - 'debut_wait', - _('Waiting debut videos'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 4, - 'debut_now', - _('Broadcasting debut videos'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 5, - 'select', - _('Selected videos'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 6, - 'select_wait', - _('Selected waiting videos'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 7, - 'select_live', - _('Selected broadcasting videos'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 8, - 'drag_drop_notify', - _('Drag and Drop notification'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 9, - 'drag_drop_odd', - _('Drag and Drop background 1'), - ) - - self.setup_windows_colours_tab_add_row(grid, - 10, - 'drag_drop_even', - _('Drag and Drop background 2'), - ) - - - def setup_windows_colours_tab_add_row(self, grid, row_num, key, descrip): - - """Called by self.setup_windows_colours_tab_add_row(). - - Sets up a single row of widgets corresponding to a single key in - mainapp.TartubeApp.custom_bg_table. - - Args: - - grid (Gtk.Grid): The grid on which widgets are attached - - row_num (int): Coordinates on the grid on which these widgets are - place - - key (str): A key in mainapp.TartubeApp.custom_bg_table - - descrip (str): The label to use for this row - - """ - - label = self.add_label(grid, - descrip, - 0, row_num, 1, 1, - ) - label.set_hexpand(False) - - label2 = self.add_label(grid, - '' + _('Custom colour:') + '', - 1, row_num, 1, 1, - ) - label2.set_hexpand(False) - - colorbutton = Gtk.ColorButton.new() - grid.attach(colorbutton, 2, row_num, 1, 1) - colorbutton.connect( - 'color-set', - self.on_custom_colour_button_clicked, - key, - ) - - mini_list = self.app_obj.custom_bg_table[key] - custom_rgba_obj = Gdk.RGBA( - mini_list[0], - mini_list[1], - mini_list[2], - mini_list[3], - ) - colorbutton.set_rgba(custom_rgba_obj) - - label3 = self.add_label(grid, - '' + _('Default colour:') + '', - 3, row_num, 1, 1, - ) - label3.set_hexpand(False) - - colorbutton2 = Gtk.ColorButton.new() - grid.attach(colorbutton2, 4, row_num, 1, 1) - colorbutton2.connect( - 'button-press-event', - self.on_default_colour_button_clicked, - colorbutton, - key, - ) - - mini_list2 = self.app_obj.default_bg_table[key] - default_rgba_obj = Gdk.RGBA( - mini_list2[0], - mini_list2[1], - mini_list2[2], - mini_list[3], - ) - colorbutton2.set_rgba(default_rgba_obj) - - - def setup_windows_errors_warnings_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Errors/Warnings' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows >' \ - + ' Errors/Warnings' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Errors/Warnings'), - inner_notebook, - ) - - # Errors/Warnings tab preferences - self.add_label(grid, - '' + _('Errors/Warnings tab preferences') + '', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Show Tartube errors'), - self.app_obj.system_error_show_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.connect('toggled', self.on_system_error_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _('Show Tartube warnings'), - self.app_obj.system_warning_show_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.connect('toggled', self.on_system_warning_button_toggled) - - checkbutton3 = self.add_checkbutton(grid, - _('Show operation errors'), - self.app_obj.operation_error_show_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton3.connect( - 'toggled', - self.on_operation_error_button_toggled, - ) - - checkbutton4 = self.add_checkbutton(grid, - _('Show operation warnings'), - self.app_obj.operation_warning_show_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - checkbutton4.connect( - 'toggled', - self.on_operation_warning_button_toggled, - ) - - checkbutton5 = self.add_checkbutton(grid, - _('Show dates'), - self.app_obj.system_msg_show_date_flag, - True, # Can be toggled by user - 0, 5, 1, 1, - ) - checkbutton5.connect( - 'toggled', - self.on_system_date_button_toggled, - ) - - checkbutton6 = self.add_checkbutton(grid, - _('Show channel/playlist/folder names'), - self.app_obj.system_msg_show_container_flag, - True, # Can be toggled by user - 0, 6, 1, 1, - ) - checkbutton6.connect( - 'toggled', - self.on_system_container_button_toggled, - ) - - checkbutton7 = self.add_checkbutton(grid, - _('Show video names'), - self.app_obj.system_msg_show_video_flag, - True, # Can be toggled by user - 0, 7, 1, 1, - ) - checkbutton7.connect( - 'toggled', - self.on_system_video_button_toggled, - ) - - checkbutton8 = self.add_checkbutton(grid, - _('Show full messages'), - self.app_obj.system_msg_show_multi_line_flag, - True, # Can be toggled by user - 0, 8, 1, 1, - ) - checkbutton8.connect( - 'toggled', - self.on_system_multi_line_button_toggled, - ) - - - def setup_windows_websites_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Websites' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Windows > Websites' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Websites'), - inner_notebook, - ) - grid_width = 2 - - # YouTube error/warning preferences - self.add_label(grid, - '' + _('YouTube error/warning preferences') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Ignore YouTube copyright errors'), - self.app_obj.ignore_yt_copyright_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.connect('toggled', self.on_copyright_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _('Ignore YouTube age-restriction errors'), - self.app_obj.ignore_yt_age_restrict_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.connect('toggled', self.on_age_restrict_button_toggled) - - checkbutton3 = self.add_checkbutton(grid, - _('Ignore YouTube deletion by uploader errors'), - self.app_obj.ignore_yt_uploader_deleted_flag, - True, # Can be toggled by user - 1, 1, 1, 1, - ) - checkbutton3.connect('toggled', self.on_uploader_button_toggled) - - checkbutton4 = self.add_checkbutton(grid, - _('Ignore YouTube payment errors'), - self.app_obj.ignore_yt_payment_flag, - True, # Can be toggled by user - 1, 2, 1, 1, - ) - checkbutton4.connect('toggled', self.on_payment_button_toggled) - - # General preferences - self.add_label(grid, - '' + _('General preferences') + '', - 0, 4, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'Ignore any errors/warnings which match lines in this list' \ - + ' (applies to all websites)', - ) + '', - 0, 5, grid_width, 1, - ) - - textview, textbuffer = self.add_textview(grid, - self.app_obj.ignore_custom_msg_list, - 0, 6, grid_width, 1 - ) - # (Signal connect appears below) - - radiobutton = self.add_radiobutton(grid, - None, - _('These are ordinary strings'), - 0, 7, 1, 1, - ) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _('These are regular expressions (regexes)'), - 1, 7, 1, 1, - ) - if self.app_obj.ignore_custom_regex_flag: - radiobutton2.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - textbuffer.connect('changed', self.on_custom_textview_changed) - radiobutton.connect( - 'toggled', - self.on_regex_button_toggled, - False, - ) - radiobutton2.connect( - 'toggled', - self.on_regex_button_toggled, - True, - ) - - - def setup_scheduling_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Scheduling' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Scheduling' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Scheduling'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_scheduling_start_tab(inner_notebook) - self.setup_scheduling_stop_tab(inner_notebook) - - - def setup_scheduling_start_tab(self, inner_notebook): - - """Called by self.setup_scheduling_tab(). - - Sets up the 'Start' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Scheduling > Start' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Start'), inner_notebook) - grid_width = 5 - - # Scheduled download preferences - self.add_label(grid, - '' + _('Scheduled download preferences') + '', - 0, 0, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ - _('Name'), _('Download'), _('Start mode'), _('Time'), - _('Priority'), _('Whole'), _('Shutdown'), _('D/L All'), - _('Join mode'), - ] - ): - if i >= 4 and i <= 7: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.schedule_liststore = Gtk.ListStore( - str, str, str, str, bool, bool, bool, bool, str, - ) - treeview.set_model(self.schedule_liststore) - - # Initialise the list - self.setup_scheduling_start_tab_update_treeview() - - # Add editing widgets - label = self.add_label(grid, - _('Scheduled download name'), - 0, 2, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - None, - True, - 1, 2, (grid_width - 2), 1, - ) - - button = Gtk.Button() - grid.attach(button, (grid_width - 1), 2, 1, 1) - button.set_label(_('Add')) - button.connect( - 'clicked', - self.on_scheduled_add_button_clicked, - entry, - ) - - button2 = Gtk.Button() - grid.attach(button2, 1, 3, 1, 1) - button2.set_label(_('Edit')) - button2.connect( - 'clicked', - self.on_scheduled_edit_button_clicked, - treeview, - ) - - button3 = Gtk.Button() - grid.attach(button3, 2, 3, 1, 1) - button3.set_label(_('Move up')) - button3.connect( - 'clicked', - self.on_scheduled_move_up_button_clicked, - treeview, - ) - - button4 = Gtk.Button() - grid.attach(button4, 3, 3, 1, 1) - button4.set_label(_('Move down')) - button4.connect( - 'clicked', - self.on_scheduled_move_down_button_clicked, - treeview, - ) - - button5 = Gtk.Button() - grid.attach(button5, 4, 3, 1, 1) - button5.set_label(_('Delete')) - button5.connect( - 'clicked', - self.on_scheduled_delete_button_clicked, - treeview, - ) - - - def setup_scheduling_start_tab_update_treeview(self): - - """ Called by self.setup_scheduling_start_tab() and - mainapp.TartubeApp.del_scheduled_list(). - - Fills or updates the treeview. - """ - - self.schedule_liststore.clear() - - for scheduled_obj in self.app_obj.scheduled_list: - self.setup_scheduling_start_tab_add_row(scheduled_obj) - - - def setup_scheduling_start_tab_add_row(self, scheduled_obj): - - """Called by self.setup_scheduling_start_tab_update_treeview() and - .on_scheduled_add_button_clicked(). - - Adds a row to the treeview. - - Args: - - scheduled_obj (media.Scheduled) - The scheduled download object to - display on this row - - """ - - row_list = [] - - row_list.append(scheduled_obj.name) - - if scheduled_obj.dl_mode == 'sim': - row_list.append(_('Check')) - elif scheduled_obj.dl_mode == 'real': - row_list.append(_('Download')) - else: - row_list.append(_('Custom')) - - row_list.append(scheduled_obj.start_mode) - - if scheduled_obj.start_mode != 'timetable': - - row_list.append( - str(scheduled_obj.wait_value) + ' ' + scheduled_obj.wait_unit - ) - - elif scheduled_obj.timetable_list: - - # (Show the first day/time combination only) - mini_list = scheduled_obj.timetable_list[0] - row_list.append( - formats.SPECIFIED_DAYS_DICT[mini_list[0]] + ' ' + mini_list[1], - ) - - else: - - row_list.append('') - - row_list.append(scheduled_obj.exclusive_flag) - row_list.append(scheduled_obj.ignore_limits_flag) - row_list.append(scheduled_obj.shutdown_flag) - row_list.append(scheduled_obj.all_flag) - row_list.append(scheduled_obj.join_mode) - - self.schedule_liststore.append(row_list) - - - def setup_scheduling_stop_tab(self, inner_notebook): - - """Called by self.setup_scheduling_tab(). - - Sets up the 'Stop' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Scheduling > Stop' - ) - - tab, grid = self.add_inner_notebook_tab(_('S_top'), inner_notebook) - grid_width = 3 - - # Scheduled stop preferences - self.add_label(grid, - '' + _('Scheduled stop preferences') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Stop all download operations after this much time'), - self.app_obj.autostop_time_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - # (Signal connect appears below) - - spinbutton = self.add_spinbutton(grid, - 1, None, 1, self.app_obj.autostop_time_value, - 1, 1, 1, 1, - ) - if not self.app_obj.autostop_time_flag: - spinbutton.set_sensitive(False) - - store = Gtk.ListStore(str, str) - for string in formats.TIME_METRIC_LIST: - store.append( [string, formats.TIME_METRIC_TRANS_DICT[string]] ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 2, 1, 1, 1) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 1) - combo.set_entry_text_column(1) - combo.set_active( - formats.TIME_METRIC_LIST.index( - self.app_obj.autostop_time_unit, - ) - ) - if not self.app_obj.autostop_time_flag: - combo.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_autostop_time_button_toggled, - spinbutton, - combo, - ) - spinbutton.connect( - 'value-changed', - self.on_autostop_time_spinbutton_changed, - ) - combo.connect('changed', self.on_autostop_time_combo_changed) - - checkbutton2 = self.add_checkbutton(grid, - _('Stop all download operations after this many videos'), - self.app_obj.autostop_videos_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - # (Signal connect appears below) - - spinbutton2 = self.add_spinbutton(grid, - 1, None, 1, self.app_obj.autostop_videos_value, - 1, 2, 1, 1, - ) - if not self.app_obj.autostop_videos_flag: - spinbutton2.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - checkbutton2.connect( - 'toggled', - self.on_autostop_videos_button_toggled, - spinbutton2, - ) - spinbutton2.connect( - 'value-changed', - self.on_autostop_videos_spinbutton_changed, - ) - - checkbutton3 = self.add_checkbutton(grid, - _('Stop all download operations after this much disk space'), - self.app_obj.autostop_size_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - # (Signal connect appears below) - - spinbutton3 = self.add_spinbutton(grid, - 1, None, 1, self.app_obj.autostop_size_value, - 1, 3, 1, 1, - ) - if not self.app_obj.autostop_size_flag: - spinbutton3.set_sensitive(False) - - combo3 = self.add_combo(grid, - formats.FILESIZE_METRIC_LIST, - None, - 2, 3, 1, 1, - ) - combo3.set_active( - formats.FILESIZE_METRIC_LIST.index( - self.app_obj.autostop_size_unit, - ) - ) - if not self.app_obj.autostop_size_flag: - combo3.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - checkbutton3.connect( - 'toggled', - self.on_autostop_size_button_toggled, - spinbutton3, - combo3, - ) - spinbutton3.connect( - 'value-changed', - self.on_autostop_size_spinbutton_changed, - ) - combo3.connect('changed', self.on_autostop_size_combo_changed) - - self.add_label(grid, - '' + _( - 'N.B. Disk space is estimated. This setting does not apply' \ - + ' to simulated downloads', - ) + '', - 0, 4, grid_width, 1, - ) - - - def setup_operations_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Operations' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('_Operations'), 0) - - # ...and an inner notebook... - self.operations_inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_operations_limits_tab(self.operations_inner_notebook) - self.setup_operations_stop_tab(self.operations_inner_notebook) - self.setup_operations_downloads_tab(self.operations_inner_notebook) - self.setup_operations_ignore_tab(self.operations_inner_notebook) - self.setup_operations_custom_dl_tab(self.operations_inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_operations_archive_tab(self.operations_inner_notebook) - self.setup_operations_livestreams_tab(self.operations_inner_notebook) - self.setup_operations_actions_tab(self.operations_inner_notebook) - self.setup_operations_clips_tab(self.operations_inner_notebook) - self.setup_operations_slices_tab(self.operations_inner_notebook) - self.setup_operations_comments_tab(self.operations_inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_operations_mirrors_tab(self.operations_inner_notebook) - self.setup_operations_proxies_tab(self.operations_inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_operations_prefs_tab(self.operations_inner_notebook) - else: - self.setup_operations_missing_tab(self.operations_inner_notebook) - - - def setup_operations_limits_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Limits' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Limits' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Limits'), - inner_notebook, - ) - grid_width = 3 - - # Performance limits - self.add_label(grid, - '' + _('Performance limits') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '' + _('Limits are applied when you start downloading a' \ - + ' video/channel/playlist') + '', - 0, 1, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Limit simultaneous downloads to'), - self.app_obj.num_worker_apply_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect('toggled', self.on_worker_button_toggled) - - spinbutton = self.add_spinbutton(grid, - self.app_obj.num_worker_min, - self.app_obj.num_worker_max, - 1, # Step - self.app_obj.num_worker_default, - 1, 2, 1, 1, - ) - spinbutton.connect('value-changed', self.on_worker_spinbutton_changed) - - checkbutton2 = self.add_checkbutton(grid, - _('Limit download speed to'), - self.app_obj.bandwidth_apply_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton2.set_hexpand(False) - checkbutton2.connect('toggled', self.on_bandwidth_button_toggled) - - spinbutton2 = self.add_spinbutton(grid, - self.app_obj.bandwidth_min, - self.app_obj.bandwidth_max, - 1, # Step - self.app_obj.bandwidth_default, - 1, 3, 1, 1, - ) - spinbutton2.connect( - 'value-changed', - self.on_bandwidth_spinbutton_changed, - ) - - self.add_label(grid, - 'KiB/s', - 2, 3, 1, 1, - ) - - checkbutton3 = self.add_checkbutton(grid, - _('Overriding video format options, limit video resolution to'), - self.app_obj.video_res_apply_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - checkbutton3.set_hexpand(False) - checkbutton3.connect('toggled', self.on_video_res_button_toggled) - - combo = self.add_combo(grid, - formats.VIDEO_RESOLUTION_LIST, - None, - 1, 4, 1, 1, - ) - combo.set_active( - formats.VIDEO_RESOLUTION_LIST.index( - self.app_obj.video_res_default, - ) - ) - combo.connect('changed', self.on_video_res_combo_changed) - - # Alternative performance limits - self.add_label(grid, - '' + _('Alternative performance limits') + '', - 0, 5, grid_width, 1, - ) - - checkbutton4 = self.add_checkbutton(grid, - _('Limit simultaneous downloads to'), - self.app_obj.alt_num_worker_apply_flag, - True, # Can be toggled by user - 0, 6, 1, 1, - ) - checkbutton4.set_hexpand(False) - checkbutton4.connect('toggled', self.on_worker_button_toggled, True) - - spinbutton3 = self.add_spinbutton(grid, - self.app_obj.num_worker_min, - self.app_obj.num_worker_max, - 1, # Step - self.app_obj.alt_num_worker, - 1, 6, 1, 1, - ) - spinbutton3.connect( - 'value-changed', - self.on_worker_spinbutton_changed, - True, - ) - - checkbutton5 = self.add_checkbutton(grid, - _('Limit download speed to'), - self.app_obj.alt_bandwidth_apply_flag, - True, # Can be toggled by user - 0, 7, 1, 1, - ) - checkbutton5.set_hexpand(False) - checkbutton5.connect('toggled', self.on_bandwidth_button_toggled, True) - - spinbutton4 = self.add_spinbutton(grid, - self.app_obj.bandwidth_min, - self.app_obj.bandwidth_max, - 1, # Step - self.app_obj.alt_bandwidth, - 1, 7, 1, 1, - ) - spinbutton4.connect( - 'value-changed', - self.on_bandwidth_spinbutton_changed, - True, - ) - - self.add_label(grid, - 'KiB/s', - 2, 7, 1, 1, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 8, (grid_width - 1), 1) - - label = self.add_label(grid2, - _('Alternative limits apply between') + ' ', - 0, 0, 1, 1, - ) - - # (Hours in format '00' to '23') - start_time = self.app_obj.alt_start_time - stop_time = self.app_obj.alt_stop_time - - hour_list = [] - for h in range(24): - hour_list.append('{:02d}'.format(h)) - - # (Minutes in format '00', '05', 10' .. '55') - minute_list = [] - for n in range(12): - minute_list.append('{:02d}'.format(n*5)) - - combo2 = self.add_combo(grid2, - hour_list, - None, - 1, 0, 1, 1, - ) - combo2.set_active(hour_list.index(start_time[0:2])) - combo2.connect('changed', self.on_alt_time_combo_changed, 'start_hour') - - label2 = self.add_label(grid2, - ' : ', - 2, 0, 1, 1, - ) - label2.set_hexpand(False) - - combo3 = self.add_combo(grid2, - minute_list, - None, - 3, 0, 1, 1, - ) - combo3.set_active(minute_list.index(start_time[3:5])) - combo3.connect('changed', self.on_alt_time_combo_changed, 'start_min') - - label3 = self.add_label(grid2, - ' ' + _('and') + ' ', - 4, 0, 1, 1, - ) - label3.set_hexpand(False) - - combo4 = self.add_combo(grid2, - hour_list, - None, - 5, 0, 1, 1, - ) - combo4.set_active(hour_list.index(stop_time[0:2])) - combo4.connect('changed', self.on_alt_time_combo_changed, 'stop_hour') - - label4 = self.add_label(grid2, - ' : ', - 6, 0, 1, 1, - ) - label4.set_hexpand(False) - - combo5 = self.add_combo(grid2, - minute_list, - None, - 7, 0, 1, 1, - ) - combo5.set_active(minute_list.index(stop_time[3:5])) - combo5.connect('changed', self.on_alt_time_combo_changed, 'stop_min') - - label5 = self.add_label(grid2, - _('On days') + ' ', - 0, 1, 1, 1, - ) - - combo6_list = [] - for s in formats.SPECIFIED_DAYS_LIST: - combo6_list.append( [formats.SPECIFIED_DAYS_DICT[s], s] ) - - combo6 = self.add_combo_with_data(grid2, - combo6_list, - None, - 1, 1, 7, 1, - ) - combo6.set_hexpand(False) - combo6.set_active( - formats.SPECIFIED_DAYS_LIST.index(self.app_obj.alt_day_string), - ) - combo6.connect('changed', self.on_alt_days_combo_changed) - - - def setup_operations_stop_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Stop' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Stop' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Stop'), - inner_notebook, - ) - grid_width = 2 - - # Time-saving settings - self.add_label(grid, - '' + _('Time-saving settings') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Stop checking/downloading a channel/playlist when it finds' \ - + ' videos you already have', - ), - self.app_obj.operation_limit_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.set_hexpand(False) - # (Signal connect appears below) - - self.add_label(grid, - _('Stop after this many videos (when checking)'), - 0, 2, 1, 1, - ) - - entry = self.add_entry(grid, - self.app_obj.operation_check_limit, - True, - 1, 2, 1, 1, - ) - entry.set_width_chars(4) - entry.connect('changed', self.on_check_limit_changed) - if not self.app_obj.operation_limit_flag: - entry.set_sensitive(False) - - self.add_label(grid, - _('Stop after this many videos (when downloading)'), - 0, 3, 1, 1, - ) - - entry2 = self.add_entry(grid, - self.app_obj.operation_download_limit, - True, - 1, 3, 1, 1, - ) - entry2.set_width_chars(4) - entry2.connect('changed', self.on_dl_limit_changed) - if not self.app_obj.operation_limit_flag: - entry2.set_sensitive(False) - - # (Signal connect from above) - checkbutton.connect( - 'toggled', - self.on_limit_button_toggled, - entry, - entry2, - ) - - - def setup_operations_downloads_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Downloads' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Downloads' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Downloads'), - inner_notebook, - ) - grid_width = 2 - - # Download operation preferences - self.add_label(grid, - '' + _('Download operation preferences') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Automatically update downloader before every download operation', - ), - self.app_obj.operation_auto_update_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.connect('toggled', self.on_auto_update_button_toggled) - if __main__.__pkg_strict_install_flag__: - checkbutton.set_sensitive(False) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Automatically save files at the end of all operations', - ), - self.app_obj.operation_save_flag, - True, # Can be toggled by user - 0, 2, grid_width, 1, - ) - checkbutton2.connect('toggled', self.on_save_button_toggled) - - if not self.app_obj.simple_prefs_flag: - - checkbutton3 = self.add_checkbutton(grid, - _( - 'For simulated downloads, don\'t check a video in a folder' \ - + ' more than once', - ), - self.app_obj.operation_sim_shortcut_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton3.connect( - 'toggled', - self.on_operation_sim_button_toggled, - ) - - checkbutton4 = self.add_checkbutton(grid, - _( - 'If a download stalls, restart it after this many minutes', - ), - self.app_obj.operation_auto_restart_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - # (Signal connect appears below) - - spinbutton = self.add_spinbutton(grid, - 1, - None, - 1, # Step - self.app_obj.operation_auto_restart_time, - 1, 4, 1, 1, - ) - # (Signal connect appears below) - if not self.app_obj.operation_auto_restart_flag: - spinbutton.set_sensitive(False) - - self.add_label(grid, - ' ' \ - + _( - 'Maximum restarts after a stalled download (0 for no maximum)', - ), - 0, 5, 1, 1, - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, - None, - 1, # Step - self.app_obj.operation_auto_restart_max, - 1, 5, 1, 1, - ) - # (Signal connect appears below) - if not self.app_obj.operation_auto_restart_flag: - spinbutton2.set_sensitive(False) - - # (Signal connects from above) - checkbutton4.connect( - 'toggled', - self.on_auto_restart_button_toggled, - spinbutton, - spinbutton2, - ) - spinbutton.connect( - 'value-changed', - self.on_auto_restart_time_spinbutton_changed, - ) - spinbutton2.connect( - 'value-changed', - self.on_auto_restart_max_spinbutton_changed, - ) - - checkbutton5 = self.add_checkbutton(grid, - _('Apply a timeout (in minutes) when checking a video'), - self.app_obj.apply_json_timeout_flag, - True, # Can be toggled by user - 0, 6, grid_width, 1, - ) - # (Signal connect appears below) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 7, grid_width, 1) - - label = self.add_label(grid2, - _('Without comments'), - 0, 0, 1, 1, - ) - - spinbutton3 = self.add_spinbutton(grid2, - 1, - None, - 1, # Step - self.app_obj.json_timeout_no_comments_time, - 1, 0, 1, 1, - ) - # (Signal connect appears below) - if not self.app_obj.apply_json_timeout_flag: - spinbutton3.set_sensitive(False) - - label2 = self.add_label(grid2, - _('With comments'), - 2, 0, 1, 1, - ) - - spinbutton4 = self.add_spinbutton(grid2, - 1, - None, - 1, # Step - self.app_obj.json_timeout_with_comments_time, - 3, 0, 1, 1, - ) - # (Signal connect appears below) - if not self.app_obj.apply_json_timeout_flag: - spinbutton4.set_sensitive(False) - - # (Signal connects from above) - checkbutton5.connect( - 'toggled', - self.on_json_button_toggled, - spinbutton3, - spinbutton4, - ) - spinbutton3.connect( - 'value-changed', - self.on_timeout_no_comments_spinbutton_changed, - ) - spinbutton4.connect( - 'value-changed', - self.on_timeout_with_comments_spinbutton_changed, - ) - - checkbutton6 = self.add_checkbutton(grid, - _( - 'Assign anonymous error/warning messages to the most' \ - + ' probable video', - ), - self.app_obj.auto_assign_errors_warnings_flag, - True, # Can be toggled by user - 0, 8, grid_width, 1, - ) - checkbutton6.connect('toggled', self.on_auto_assign_button_toggled) - - checkbutton7 = self.add_checkbutton(grid, - _( - 'Add censored, age-restricted and other blocked videos to the' \ - + ' database', - ), - self.app_obj.add_blocked_videos_flag, - True, # Can be toggled by user - 0, 9, grid_width, 1, - ) - checkbutton7.connect('toggled', self.on_add_blocked_button_toggled) - - if not self.app_obj.simple_prefs_flag: - - checkbutton8 = self.add_checkbutton(grid, - _( - 'Extract playlist IDs from each video, and store them in the' \ - + ' parent channel/playlist', - ), - self.app_obj.store_playlist_id_flag, - True, # Can be toggled by user - 0, 10, grid_width, 1, - ) - checkbutton8.connect('toggled', self.on_store_playlist_id_toggled) - - checkbutton9 = self.add_checkbutton(grid, - _( - 'Convert .webp thumbnails into .jpg thumbnails (using' \ - + ' FFmpeg) after downloading them', - ), - self.app_obj.ffmpeg_convert_webp_flag, - True, # Can be toggled by user - 0, 11, grid_width, 1, - ) - # (Signal connect appears below) - - checkbutton10 = self.add_checkbutton(grid, - _( - '...but don\'t delete the original thumbnails (enable' \ - + ' before embedding thumbnails in videos)', - ), - self.app_obj.ffmpeg_retain_webp_flag, - True, # Can be toggled by user - 0, 12, grid_width, 1, - ) - if not self.app_obj.ffmpeg_convert_webp_flag: - checkbutton.set_sensitive(False) - checkbutton10.connect( - 'toggled', - self.on_ffmpeg_retain_flag_toggled, - ) - - # (Signal connects from above) - checkbutton9.connect( - 'toggled', - self.on_ffmpeg_convert_flag_toggled, - checkbutton10, - ) - - - def setup_operations_ignore_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Ignore' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Ignore' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Ignore'), - inner_notebook, - ) - grid_width = 2 - - # Ignore downloader errors/warnings - self.add_label(grid, - '' + _('Ignore downloader errors/warnings') + '', - 0, 0, grid_width, 1, - ) - - ignore_me = _( - 'TRANSLATOR\'S NOTE: These error messages are always in English', - ) - - checkbutton = self.add_checkbutton(grid, - _('Ignore \'Child process exited with non-zero code\' errors'), - self.app_obj.ignore_child_process_exit_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.connect('toggled', self.on_child_process_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Ignore \'Unable to download video data\' and \'Unable to' \ - + ' extract video data\' errors', - ), - self.app_obj.ignore_http_404_error_flag, - True, # Can be toggled by user - 0, 2, grid_width, 1, - ) - checkbutton2.connect('toggled', self.on_http_404_button_toggled) - - checkbutton3 = self.add_checkbutton(grid, - _('Ignore \'Did not get any data blocks\' errors'), - self.app_obj.ignore_data_block_error_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton3.connect('toggled', self.on_data_block_button_toggled) - - checkbutton4 = self.add_checkbutton(grid, - _( - 'Ignore \'Requested formats are incompatible for merge\' warnings', - ), - self.app_obj.ignore_merge_warning_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - checkbutton4.connect('toggled', self.on_merge_button_toggled) - - checkbutton5 = self.add_checkbutton(grid, - _('Ignore \'No video formats found\' errors'), - self.app_obj.ignore_missing_format_error_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - checkbutton5.connect('toggled', self.on_missing_format_button_toggled) - - checkbutton6 = self.add_checkbutton(grid, - _('Ignore \'There are no annotations to write\' warnings'), - self.app_obj.ignore_no_annotations_flag, - True, # Can be toggled by user - 0, 6, grid_width, 1, - ) - checkbutton6.connect('toggled', self.on_no_annotations_button_toggled) - - checkbutton7 = self.add_checkbutton(grid, - _('Ignore \'Video doesn\'t have subtitles\' warnings'), - self.app_obj.ignore_no_subtitles_flag, - True, # Can be toggled by user - 0, 7, grid_width, 1, - ) - checkbutton7.connect('toggled', self.on_no_subtitles_button_toggled) - - checkbutton8 = self.add_checkbutton(grid, - _('Ignore \'A channel/user page was given\' warnings'), - self.app_obj.ignore_page_given_flag, - True, # Can be toggled by user - 0, 8, grid_width, 1, - ) - checkbutton8.connect('toggled', self.on_page_given_button_toggled) - - checkbutton9 = self.add_checkbutton(grid, - _('Ignore \'There\'s no playlist description to write\' warnings'), - self.app_obj.ignore_no_descrip_flag, - True, # Can be toggled by user - 0, 9, grid_width, 1, - ) - checkbutton9.connect('toggled', self.on_no_descrip_button_toggled) - - checkbutton10 = self.add_checkbutton(grid, - _( - 'Ignore \'Unable to download video thumbnail: HTTP Error 404:' \ - + ' Not Fuund\' warnings', - ), - self.app_obj.ignore_thumb_404_flag, - True, # Can be toggled by user - 0, 10, grid_width, 1, - ) - checkbutton10.connect('toggled', self.on_thumb_404_button_toggled) - - - def setup_operations_custom_dl_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Custom' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Custom' - ) - - tab, grid = self.add_inner_notebook_tab(_('_Custom'), inner_notebook) - grid_width = 4 - - # Custom downloads - self.add_label(grid, - '' + _('Custom downloads') + '', - 0, 0, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - # (The final column is deliberately empty, so that the previous column - # doesn't expand to fill the whole available area) - for i, column_title in enumerate( - [ '#', _('Name'), _('Default'), _('Classic Mode'), ''] - ): - if i == 2 or i == 3: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.custom_liststore = Gtk.ListStore(int, str, bool, bool, str) - treeview.set_model(self.custom_liststore) - - # Initialise the list - self.setup_operations_custom_dl_tab_update_treeview() - - # Add editing buttons - self.add_label(grid, - 'Name', - 0, 2, 1, 1, - ) - - entry = self.add_entry(grid, - None, - True, - 1, 2, 1, 1, - ) - - button = Gtk.Button() - grid.attach(button, 2, 2, 1, 1) - button.set_label(_('Add')) - button.connect( - 'clicked', - self.on_custom_dl_add_button_clicked, - entry, - ) - - button2 = Gtk.Button() - grid.attach(button2, 3, 2, 1, 1) - button2.set_label(_('Import')) - button2.connect( - 'clicked', - self.on_custom_dl_import_button_clicked, - entry, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - button3 = Gtk.Button() - grid2.attach(button3, 0, 0, 1, 1) - button3.set_label(_('Edit')) - button3.connect( - 'clicked', - self.on_custom_dl_edit_button_clicked, - treeview, - ) - - button4 = Gtk.Button() - grid2.attach(button4, 1, 0, 1, 1) - button4.set_label(_('Export')) - button4.connect( - 'clicked', - self.on_custom_dl_export_button_clicked, - treeview, - ) - - button5 = Gtk.Button() - grid2.attach(button5, 2, 0, 1, 1) - button5.set_label(_('Clone')) - button5.connect( - 'clicked', - self.on_custom_dl_clone_button_clicked, - treeview, - ) - - button6 = Gtk.Button() - grid2.attach(button6, 3, 0, 1, 1) - button6.set_label(_('Use in Classic Mode tab')) - button6.connect( - 'clicked', - self.on_custom_dl_use_classic_button_clicked, - treeview, - ) - - button7 = Gtk.Button() - grid2.attach(button7, 4, 0, 1, 1) - button7.set_label(_('Delete')) - button7.connect( - 'clicked', - self.on_custom_dl_delete_button_clicked, - treeview, - ) - - # (Use an empty label for spacing) - label = self.add_label(grid2, - '', - 5, 0, 1, 1, - ) - label.set_hexpand(True) - - button8 = Gtk.Button() - grid2.attach(button8, 6, 0, 1, 1) - button8.set_label(_('Refresh list')) - button8.connect( - 'clicked', - self.setup_operations_custom_dl_tab_update_treeview, - ) - - - def setup_operations_custom_dl_tab_update_treeview(self): - - """Can be called by anything. - - Fills or updates the treeview. - - """ - - self.custom_liststore.clear() - - for uid in sorted(self.app_obj.custom_dl_reg_dict): - self.setup_operations_custom_dl_tab_add_row( - self.app_obj.custom_dl_reg_dict[uid], - ) - - - def setup_operations_custom_dl_tab_add_row(self, custom_dl_obj): - - """Can be called by anything. - - Adds a row to the treeview. - - Args: - - custom_dl_obj (downloads.CustomDLManager): The custom download - manager object to display on this row - - """ - - row_list = [] - - row_list.append(custom_dl_obj.uid) - row_list.append( - utils.tidy_up_long_string( - custom_dl_obj.name, - self.app_obj.main_win_obj.short_string_max_len, - ), - ) - - if self.app_obj.general_custom_dl_obj \ - and self.app_obj.general_custom_dl_obj == custom_dl_obj: - row_list.append(True) - else: - row_list.append(False) - - if self.app_obj.classic_custom_dl_obj \ - and self.app_obj.classic_custom_dl_obj == custom_dl_obj: - row_list.append(True) - else: - row_list.append(False) - - # (The final column is deliberately empty, so that the previous column - # doesn't expand to fill the whole available area) - row_list.append('') - - self.custom_liststore.append(row_list) - - - def setup_operations_archive_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Archive' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Archive' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Archive'), - inner_notebook, - ) - - grid_width = 4 - - # Archive file preferences - self.add_label(grid, - '' + _('Archive file preferences') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Allow downloader to create its own archive file (so deleted' \ - + ' videos are not re-downloaded)', - ), - self.app_obj.allow_ytdl_archive_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - # (Signal connect appears below) - - # (Empty label for spacing) - label = self.add_label(grid, - ' ', - 0, 2, 1, 1, - ) - label.set_hexpand(False) - - radiobutton = self.add_radiobutton(grid, - None, - _( - 'Store the archive file in the same location as the video', - ), - 1, 2, (grid_width - 1), 1, - ) - # (Signal connect appears below) - - self.add_label(grid, - '' + _( - 'N.B. Archive files are never stored in system folders like' \ - + ' \'Unsorted Videos\'', - ) + '', - 1, 3, (grid_width - 1), 1, - ) - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _( - 'Store the archive file in Tartube\'s data directory', - ), - 1, 4, (grid_width - 1), 1, - ) - if self.app_obj.allow_ytdl_archive_mode == 'top': - radiobutton2.set_active(True) - # (Signal connect appears below) - - radiobutton3 = self.add_radiobutton(grid, - radiobutton2, - _( - 'Store the archive file at this location:', - ), - 1, 5, (grid_width - 1), 1, - ) - if self.app_obj.allow_ytdl_archive_mode == 'custom': - radiobutton3.set_active(True) - # (Signal connect appears below) - - entry = self.add_entry(grid, - None, - True, - 1, 6, 1, 1, - ) - if self.app_obj.allow_ytdl_archive_path != None: - entry.set_text(self.app_obj.allow_ytdl_archive_path) - entry.set_hexpand(True) - entry.set_editable(False) - - button = Gtk.Button(_('Set')) - grid.attach(button, 2, 6, 1, 1) - # (Signal connect appears below) - - button2 = Gtk.Button(_('Reset')) - grid.attach(button2, 3, 6, 1, 1) - # (Signal connect appears below) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_archive_button_toggled, - radiobutton, - radiobutton2, - radiobutton3, - button, - button2, - ) - radiobutton.connect( - 'toggled', - self.on_archive_radiobutton_toggled, - radiobutton, - radiobutton2, - radiobutton3, - entry, - button, - button2, - ) - radiobutton2.connect( - 'toggled', - self.on_archive_radiobutton_toggled, - radiobutton, - radiobutton2, - radiobutton3, - entry, - button, - button2, - ) - radiobutton3.connect( - 'toggled', - self.on_archive_radiobutton_toggled, - radiobutton, - radiobutton2, - radiobutton3, - entry, - button, - button2, - ) - button.connect('clicked', self.on_set_archive_button_clicked, entry) - button2.connect('clicked', self.on_reset_archive_button_clicked, entry) - - if not self.app_obj.allow_ytdl_archive_flag: - radiobutton.set_sensitive(False) - radiobutton2.set_sensitive(False) - radiobutton3.set_sensitive(False) - if not self.app_obj.allow_ytdl_archive_flag \ - or self.app_obj.allow_ytdl_archive_mode != 'custom': - button.set_sensitive(False) - button2.set_sensitive(False) - - # Classic Mode tab preferences - self.add_label(grid, - '' + _('Classic Mode tab preferences') + '', - 0, 7, grid_width, 1, - ) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Create an archive file when downloading from the Classic Mode' \ - + ' tab', - ), - self.app_obj.classic_ytdl_archive_flag, - True, # Can be toggled by user - 0, 8, grid_width, 1, - ) - checkbutton2.connect('toggled', self.on_archive_classic_button_toggled) - - self.add_label(grid, - '' + _( - 'This setting should only be enabled when downloading' \ - + ' channels and playlists', - ) + '', - 0, 9, grid_width, 1, - ) - - - def setup_operations_livestreams_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Streams' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Livestreams' - ) - - tab, grid = self.add_inner_notebook_tab( - _('Li_vestreams'), - inner_notebook, - ) - grid_width = 2 - - # Livestream preferences (compatible websites only) - self.add_label(grid, - '' + _( - 'Livestream preferences (compatible websites only)', - ) + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Do not check/download any livestream [yt-dlp only]'), - self.app_obj.block_livestreams_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.connect( - 'toggled', - self.on_block_livestreams_button_toggled, - ) - - checkbutton2 = self.add_checkbutton(grid, - _('Detect livestreams announced within this many days'), - self.app_obj.enable_livestreams_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - # (Signal connect appears below) - spinbutton = self.add_spinbutton(grid, - 0, None, 1, self.app_obj.livestream_max_days, - 1, 2, 1, 1, - ) - if not self.app_obj.enable_livestreams_flag: - spinbutton.set_sensitive(False) - # (Signal connect appears below) - - checkbutton3 = self.add_checkbutton(grid, - _('How often to check the status of livestreams (in minutes)'), - self.app_obj.scheduled_livestream_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - if not self.app_obj.enable_livestreams_flag: - checkbutton3.set_sensitive(False) - # (Signal connect appears below) - - spinbutton2 = self.add_spinbutton(grid, - 1, None, 1, self.app_obj.scheduled_livestream_wait_mins, - 1, 3, 1, 1, - ) - if not self.app_obj.enable_livestreams_flag \ - or not self.app_obj.scheduled_livestream_flag: - spinbutton2.set_sensitive(False) - # (Signal connect appears below) - - checkbutton4 = self.add_checkbutton(grid, - _('Check more frequently when a livestream is due to start'), - self.app_obj.scheduled_livestream_extra_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - if not self.app_obj.enable_livestreams_flag \ - or not self.app_obj.scheduled_livestream_flag: - checkbutton4.set_sensitive(False) - checkbutton4.connect( - 'toggled', - self.on_extra_livestreams_button_toggled, - ) - - # (Signal connects from above) - checkbutton2.connect( - 'toggled', - self.on_enable_livestreams_button_toggled, - checkbutton3, - checkbutton4, - spinbutton, - spinbutton2, - ) - - spinbutton.connect( - 'value-changed', - self.on_livestream_max_days_spinbutton_changed, - ) - - checkbutton3.connect( - 'toggled', - self.on_scheduled_livestreams_button_toggled, - checkbutton4, - spinbutton2, - ) - - spinbutton2.connect( - 'value-changed', - self.on_scheduled_livestreams_spinbutton_changed, - ) - - # Broadcast preferences (compatible websites only) - self.add_label(grid, - '' + _( - 'Broadcasting livestream preferences (compatible websites' \ - + ' only)', - ) + '', - 0, 5, grid_width, 1, - ) - - self.add_label(grid, - '' + _( - 'These settings apply when downloading videos individually,' \ - + ' for example with a custom download', - ) + '', - 0, 6, grid_width, 1, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 7, grid_width, 1) - - label = self.add_label(grid2, - _('Download using:'), - 0, 0, 1, 1, - ) - label.set_hexpand(False) - - self.livestream_radiobutton = self.add_radiobutton(grid2, - None, - '', - 1, 0, 1, 1, - ) - self.livestream_radiobutton.set_hexpand(False) - # (Signal connect appears below) - - self.livestream_radiobutton2 = self.add_radiobutton(grid2, - self.livestream_radiobutton, - _('.m3u manifest'), - 2, 0, 1, 1, - ) - self.livestream_radiobutton2.set_hexpand(False) - if self.app_obj.livestream_dl_mode == 'default_m3u': - self.livestream_radiobutton2.set_active(True) - # (Signal connect appears below) - - self.livestream_radiobutton3 = self.add_radiobutton(grid2, - self.livestream_radiobutton2, - 'streamlink', - 3, 0, 1, 1, - ) - self.livestream_radiobutton3.set_hexpand(False) - if self.app_obj.livestream_dl_mode == 'streamlink': - self.livestream_radiobutton3.set_active(True) - # (Signal connect appears below) - - # (Set labels for those widgets, and replace them every time the - # downloader changes) - self.setup_operations_livestreams_tab_update() - - # (Signal connects from above) - self.livestream_radiobutton.connect( - 'toggled', - self.on_livestream_mode_button_toggled, - 'default', - ) - self.livestream_radiobutton2.connect( - 'toggled', - self.on_livestream_mode_button_toggled, - 'default_m3u', - ) - self.livestream_radiobutton3.connect( - 'toggled', - self.on_livestream_mode_button_toggled, - 'streamlink', - ) - - if not self.app_obj.simple_prefs_flag: - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid3 = self.add_secondary_grid(grid, 0, 8, grid_width, 1) - - self.livestream_radiobutton4 = self.add_radiobutton(grid3, - None, - _('Replace a partially-downloaded livestream'), - 1, 0, 1, 1, - ) - self.livestream_radiobutton4.set_hexpand(False) - - self.livestream_radiobutton5 = self.add_radiobutton(grid3, - self.livestream_radiobutton4, - _('Resume a partially-downloaded livestream'), - 2, 0, 1, 1, - ) - self.livestream_radiobutton5.set_hexpand(False) - if not self.app_obj.livestream_replace_flag: - self.livestream_radiobutton5.set_active(True) - if self.app_obj.livestream_dl_mode == 'streamlink': - self.livestream_radiobutton5.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - self.livestream_radiobutton4.connect( - 'toggled', - self.on_livestream_replace_button_toggled, - ) - - # (More widgets) - checkbutton5 = self.add_checkbutton(grid, - _( - 'Bypass usual limits on simultaneous downloads, so that' \ - + ' all livestreams can be downloaded', - ), - self.app_obj.num_worker_bypass_flag, - True, # Can be toggled by user - 0, 9, grid_width, 1, - ) - checkbutton5.connect( - 'toggled', - self.on_worker_bypass_button_toggled, - ) - - self.add_label(grid, - _('Timeout after this many minutes of inactivity'), - 0, 10, 1, 1, - ) - - spinbutton3 = self.add_spinbutton(grid, - 1, None, 0.2, - self.app_obj.livestream_dl_timeout, - 1, 10, 1, 1, - ) - spinbutton3.connect( - 'value-changed', - self.on_livestream_timeout_spinbutton_changed, - ) - - checkbutton6 = self.add_checkbutton(grid, - _( - 'When the livestream download is stopped manually, mark the' \ - + ' video as downloaded', - ), - self.app_obj.livestream_stop_is_final_flag, - True, # Can be toggled by user - 0, 11, grid_width, 1, - ) - checkbutton6.connect( - 'toggled', - self.on_livestream_stop_button_toggled, - ) - - checkbutton7 = self.add_checkbutton(grid, - _( - 'Check a video before the livestream download (ensures' \ - + ' metadata is downloaded)', - ), - self.app_obj.livestream_force_check_flag, - True, # Can be toggled by user - 0, 12, grid_width, 1, - ) - checkbutton7.connect( - 'toggled', - self.on_livestream_force_check_button_toggled, - ) - - self.add_label(grid, - ' ' + _( - 'N.B. This setting is ignored in the Classic Mode tab', - ) + '', - 0, 13, grid_width, 1, - ) - - - def setup_operations_livestreams_tab_update(self): - - """Called initially by self.setup_operations_livestreams_tab, and - subsequently by self.update_ytdl_combos(). - - Updates labels in that tab to show the current downloader. - """ - - downloader = self.app_obj.get_downloader() - - self.livestream_radiobutton.set_label( - downloader + ' (' + _('not recommended') + ')', - ) - - - def setup_operations_actions_tab(self, inner_notebook): - - """Called by self.setup_scheduling_tab(). - - Sets up the 'Actions' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Actions' - ) - - tab, grid = self.add_inner_notebook_tab( - _('Ac_tions'), - inner_notebook, - ) - grid_width = 3 - - # Livestream actions (can be toggled for individual videos) - self.add_label(grid, - '' + _( - 'Livestream actions (can be toggled for individual videos)', - ) + '', - 0, 0, grid_width, 1, - ) - - # Currently disabled on MS Windows - if os.name == 'nt': - string = ' ' + _('(currently disabled on MS Windows)') - else: - string = '' - - checkbutton = self.add_checkbutton(grid, - _('When a livestream starts, show a desktop notification') \ - + string, - self.app_obj.livestream_auto_notify_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.connect( - 'toggled', - self.on_livestream_auto_notify_button_toggled, - ) - if os.name == 'nt': - checkbutton.set_sensitive(False) - - checkbutton2 = self.add_checkbutton(grid, - _('When a livestream starts, sound an alarm'), - self.app_obj.livestream_auto_alarm_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - if not mainapp.HAVE_PLAYSOUND_FLAG \ - or self.app_obj.sound_dir is None \ - or not self.app_obj.sound_list: - checkbutton2.set_sensitive(False) - checkbutton2.connect( - 'toggled', - self.on_livestream_auto_alarm_button_toggled, - ) - - combo = self.add_combo(grid, - self.app_obj.sound_list, - self.app_obj.sound_custom, - 1, 2, 1, 1, - ) - combo.connect('changed', self.on_sound_custom_changed) - if not mainapp.HAVE_PLAYSOUND_FLAG \ - or self.app_obj.sound_dir is None \ - or not self.app_obj.sound_list: - combo.set_visible(False) - - button = Gtk.Button(_('Test')) - grid.attach(button, 2, 2, 1, 1) - button.set_tooltip_text(_('Plays the selected sound effect')) - button.connect('clicked', self.on_test_sound_clicked, combo) - if not mainapp.HAVE_PLAYSOUND_FLAG \ - or self.app_obj.sound_dir is None \ - or not self.app_obj.sound_list: - button.set_sensitive(False) - - checkbutton3 = self.add_checkbutton(grid, - _( - 'When a livestream starts, open it in the system\'s web browser', - ), - self.app_obj.livestream_auto_open_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton3.connect( - 'toggled', - self.on_livestream_auto_open_button_toggled, - ) - - checkbutton4 = self.add_checkbutton(grid, - _('When a livestream starts, begin downloading it immediately'), - self.app_obj.livestream_auto_dl_start_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - checkbutton4.connect( - 'toggled', - self.on_livestream_auto_dl_start_button_toggled, - ) - - checkbutton5 = self.add_checkbutton(grid, - _( - 'When a livestream stops, download it (overwriting any earlier' \ - + ' file)', - ), - self.app_obj.livestream_auto_dl_stop_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - checkbutton5.connect( - 'toggled', - self.on_livestream_auto_dl_stop_button_toggled, - ) - - if not self.app_obj.simple_prefs_flag: - - # Desktop notification preferences - self.add_label(grid, - '' + _('Desktop notification preferences') + '', - 0, 6, 1, 1, - ) - - radiobutton = self.add_radiobutton(grid, - None, - _('Show a dialogue window at the end of an operation'), - 0, 7, 1, 1, - ) - # (Signal connect appears below) - - if platform.system() != 'Windows' \ - and platform.system() != 'Darwin': - text = 'Show a desktop notification at the end of an operation' - else: - text = 'Show a desktop notification (Linux/*BSD only)' - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _(text), - 0, 8, 1, 1, - ) - if self.app_obj.operation_dialogue_mode == 'desktop': - radiobutton2.set_active(True) - if platform.system() == 'Windows' or platform.system() == 'Darwin': - radiobutton2.set_sensitive(False) - # (Signal connect appears below) - - radiobutton3 = self.add_radiobutton(grid, - radiobutton2, - _('Don\'t notify the user at the end of an operation'), - 0, 9, 1, 1, - ) - if self.app_obj.operation_dialogue_mode == 'default': - radiobutton3.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_dialogue_button_toggled, - 'dialogue', - ) - radiobutton2.connect( - 'toggled', - self.on_dialogue_button_toggled, - 'desktop', - ) - radiobutton3.connect( - 'toggled', - self.on_dialogue_button_toggled, - 'default', - ) - - - def setup_operations_clips_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Clips' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Clips' - ) - - tab, grid = self.add_inner_notebook_tab( - _('Cli_ps'), - inner_notebook, - ) - grid_width = 2 - - # Timestamps - self.add_label(grid, - '' + _('Timestamps') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'When a video is checked/downloaded, automatically extract' \ - + ' timestamps from its metadata file', - ), - self.app_obj.video_timestamps_extract_json_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.connect('toggled', self.on_extract_json_flag_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'When a video is checked/downloaded, automatically extract' \ - + ' timestamps from its description', - ), - self.app_obj.video_timestamps_extract_descrip_flag, - True, # Can be toggled by user - 0, 2, grid_width, 1, - ) - checkbutton2.connect('toggled', self.on_extract_descrip_flag_toggled) - - if not self.app_obj.simple_prefs_flag: - - checkbutton3 = self.add_checkbutton(grid, - _('If timestamps have already been extracted, replace them'), - self.app_obj.video_timestamps_replace_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton3.connect( - 'toggled', - self.on_replace_stamps_flag_toggled, - ) - - checkbutton4 = self.add_checkbutton(grid, - _( - 'If no timestamps have been extracted, try again before' \ - + ' splitting a video', - ), - self.app_obj.video_timestamps_re_extract_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - checkbutton4.connect( - 'toggled', - self.on_reextract_stamps_flag_toggled, - ) - - # Video clips (requires FFmpeg) - self.add_label(grid, - '' + _('Video clips (requires FFmpeg or yt-dlp)') + '', - 0, 5, grid_width, 1, - ) - - # (N.B. This setting can be more conveniently changed in - # mainwin.PrepareClipDialogue) - if not self.app_obj.simple_prefs_flag: - - radiobutton = self.add_radiobutton(grid, - None, - _('Download video clips using FFmpeg'), - 0, 6, 1, 1, - ) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _('Download video clips using yt-dlp'), - 1, 6, 1, 1, - ) - if self.app_obj.video_timestamps_dl_mode == 'downloader': - radiobutton2.set_active(True) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_clips_dl_mode_button_toggled, - ) - - self.add_label(grid, - _('Format of video clip filenames'), - 0, 7, 1, 1, - ) - - combo_list = [ - _('Number'), 'num', - _('Clip Title'), 'clip', - _('Number + Clip Title'), 'num_clip', - _('Clip Title + Number'), 'clip_num', - _('Original Title'), 'orig', - _('Original Title + Number'), 'orig_num', - _('Original Title + Clip Title'), 'orig_clip', - _('Original Title + Number + Clip Title'), 'orig_num_clip', - _('Original Title + Clip Title + Number'), 'orig_clip_num', - ] - - store = Gtk.ListStore(str, str) - count = -1 - line_num = 0 - while combo_list: - - count += 1 - descrip = combo_list.pop(0) - mode = combo_list.pop(0) - if mode == self.app_obj.split_video_name_mode: - line_num = count - - store.append([ descrip, mode]) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 1, 7, 1, 1) - combo.set_hexpand(True) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_active(line_num) - combo.connect('changed', self.on_split_mode_combo_changed) - - self.add_label(grid, - _('Generic title for video clips'), - 0, 8, 1, 1, - ) - - entry = self.add_entry(grid, - None, - True, - 1, 8, 1, 1, - ) - entry.set_text(self.app_obj.split_video_custom_title) - entry.connect( - 'changed', - self.on_custom_title_changed, - ) - - radiobutton3 = self.add_radiobutton(grid, - None, - _('Move clips to the Video Clips folder'), - 0, 9, 1, 1, - ) - # (Signal connect appears below) - - radiobutton4 = self.add_radiobutton(grid, - radiobutton3, - _('Keep clips with their original video'), - 1, 9, 1, 1, - ) - if not self.app_obj.split_video_clips_dir_flag: - radiobutton4.set_active(True) - - # (Signal connects from above) - radiobutton3.connect( - 'toggled', - self.on_clips_dir_button_toggled, - ) - - checkbutton5 = self.add_checkbutton(grid, - _('...but place new files inside a sub-directory'), - self.app_obj.split_video_subdir_flag, - True, # Can be toggled by user - 0, 10, grid_width, 1, - ) - checkbutton5.connect( - 'toggled', - self.on_split_subdir_flag_toggled, - ) - - checkbutton6 = self.add_checkbutton(grid, - _('Add new files to Tartube\'s database'), - self.app_obj.split_video_add_db_flag, - True, # Can be toggled by user - 0, 11, 1, 1, - ) - checkbutton6.connect( - 'toggled', - self.on_add_db_flag_toggled, - ) - - checkbutton7 = self.add_checkbutton(grid, - _('Use the original video\'s thumbnail'), - self.app_obj.split_video_copy_thumb_flag, - True, # Can be toggled by user - 1, 11, 1, 1, - ) - checkbutton7.connect('toggled', self.on_copy_thumb_flag_toggled) - - if not self.app_obj.simple_prefs_flag: - - checkbutton8 = self.add_checkbutton(grid, - _( - 'Force keyframes at cuts (slower, but fewer video' \ - + ' artefacts before and after each cut)', - ), - self.app_obj.split_video_force_keyframe_flag, - True, # Can be toggled by user - 0, 12, grid_width, 1, - ) - checkbutton8.connect( - 'toggled', - self.on_split_keyframe_flag_toggled, - ) - - if os.name == 'nt': - msg = _('After splitting a video, open the destination folder') - else: - msg = _('After splitting a video, open the destination directory') - - checkbutton9 = self.add_checkbutton(grid, - msg, - self.app_obj.split_video_auto_open_flag, - True, # Can be toggled by user - 0, 13, grid_width, 1, - ) - checkbutton9.connect('toggled', self.on_auto_open_flag_toggled) - - checkbutton10 = self.add_checkbutton(grid, - _( - 'After splitting a video, delete the original (ignored for' \ - + ' videos in channels/playlists)', - ), - self.app_obj.split_video_auto_delete_flag, - True, # Can be toggled by user - 0, 14, grid_width, 1, - ) - checkbutton10.connect('toggled', self.on_auto_delete_flag_toggled) - - - def setup_operations_slices_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Slices' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Slices' - ) - - tab, grid = self.add_inner_notebook_tab( - _('Slic_es'), - inner_notebook, - ) - grid_width = 1 - - # Video slices (requires FFmpeg) - self.add_label(grid, - '' + _('Video slices (requires FFmpeg)') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'While checking/downloading videos, check each video against' \ - + ' the SponsorBlock server', - ), - self.app_obj.sblock_fetch_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.connect('toggled', self.on_sblock_fetch_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'When contacting the server, obfuscate each video\'s ID' \ - + ' (recommended)', - ), - self.app_obj.sblock_obfuscate_flag, - True, # Can be toggled by user - 0, 2, grid_width, 1, - ) - checkbutton2.connect( - 'toggled', - self.on_sblock_obfuscate_button_toggled, - ) - - if not self.app_obj.simple_prefs_flag: - - checkbutton3 = self.add_checkbutton(grid, - _( - 'If slices have already been extracted, replace the old list', - ), - self.app_obj.sblock_replace_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton3.connect( - 'toggled', - self.on_sblock_replace_button_toggled, - ) - - checkbutton4 = self.add_checkbutton(grid, - _( - 'If slices have been extracted, contact the server again' \ - + ' before removing more slices from the video', - ), - self.app_obj.sblock_re_extract_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - checkbutton4.connect( - 'toggled', - self.on_sblock_re_extract_button_toggled, - ) - - checkbutton5 = self.add_checkbutton(grid, - _( - 'Force keyframes at cuts (slower, but fewer video artefacts' \ - + ' before and after each cut)', - ), - self.app_obj.slice_video_force_keyframe_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - checkbutton5.connect( - 'toggled', - self.on_slice_keyframe_flag_toggled, - ) - - checkbutton6 = self.add_checkbutton(grid, - _( - 'After removing slices from a video, reset all timestamp and' \ - + ' slice data (recommended)', - ), - self.app_obj.slice_video_cleanup_flag, - True, # Can be toggled by user - 0, 6, grid_width, 1, - ) - checkbutton6.connect( - 'toggled', - self.on_slice_cleanup_button_toggled, - ) - - - def setup_operations_comments_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Comments' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Comments' - ) - - tab, grid = self.add_inner_notebook_tab( - _('C_omments'), - inner_notebook, - ) - grid_width = 1 - - # Video comments (yt-dlp only) - self.add_label(grid, - '' + _('Video comments') + '' + self.ytdlp_only(), - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('When checking videos, store comments in the metadata file'), - self.app_obj.check_comment_fetch_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - # (Signal connect appears below) - - checkbutton2 = self.add_checkbutton(grid, - _('When downloading videos, store comments in the metadata file'), - self.app_obj.dl_comment_fetch_flag, - True, # Can be toggled by user - 0, 2, grid_width, 1, - ) - # (Signal connect appears below) - - self.add_label(grid, - '' + _('Warning: fetching comments will increase the download' \ - + ' time, perhaps by a lot!') + '', - 0, 3, grid_width, 1, - ) - - checkbutton3 = self.add_checkbutton(grid, - _('Also store comments in the Tartube database'), - self.app_obj.comment_store_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - # (Signal connect appears below) - if not self.app_obj.check_comment_fetch_flag \ - and not self.app_obj.dl_comment_fetch_flag: - checkbutton3.set_sensitive(False) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_check_comment_fetch_button_toggled, - checkbutton3, - ) - checkbutton2.connect( - 'toggled', - self.on_dl_comment_fetch_button_toggled, - checkbutton3, - ) - checkbutton3.connect('toggled', self.on_comment_store_button_toggled) - - self.add_label(grid, - '' + _( - 'Warning: storing comments will increase the size of' \ - + ' Tartube\'s datbase, perhaps by a lot!', - ) + '', - 0, 5, grid_width, 1, - ) - - - def setup_operations_mirrors_tab(self, inner_notebook): - - """Called by self.setup_scheduling_tab(). - - Sets up the 'Mirrors' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Mirrors' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Mirrors'), - inner_notebook, - ) - grid_width = 2 - - # Invidious mirror - self.add_label(grid, - '' + _('Invidious mirror') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - _( - 'To find an updated list of Invidious mirrors, use any' \ - + ' search engine!', - ), - 0, 1, grid_width, 1, - ) - - entry = self.add_entry(grid, - self.app_obj.custom_invidious_mirror, - True, - 0, 2, 1, 1, - ) - entry.connect('changed', self.on_invidious_mirror_changed) - - button = Gtk.Button(_('Reset')) - grid.attach(button, 1, 2, 1, 1) - button.set_tooltip_text(_('Use the default Invidious mirror')) - button.set_hexpand(False) - button.connect('clicked', self.on_reset_invidious_clicked, entry) - - msg = _('Type the exact text that replaces www.youtube.com e.g.') - msg = re.sub('www.youtube.com', ' www.youtube.com ', msg) - - self.add_label(grid, - '' + msg + ' ' + self.app_obj.default_invidious_mirror \ - + '', - 0, 3, grid_width, 1, - ) - - # SponsorBlock API mirror - self.add_label(grid, - '' + _('SponsorBlock API mirror') + '', - 0, 4, grid_width, 1, - ) - - entry2 = self.add_entry(grid, - self.app_obj.custom_sblock_mirror, - True, - 0, 5, 1, 1, - ) - entry2.connect('changed', self.on_sblock_mirror_changed) - - button2 = Gtk.Button(_('Reset')) - grid.attach(button2, 1, 5, 1, 1) - button2.set_tooltip_text(_('Use the default SponsorBlock URL')) - button2.connect('clicked', self.on_reset_sblock_clicked, entry2) - - - def setup_operations_proxies_tab(self, inner_notebook): - - """Called by self.setup_scheduling_tab(). - - Sets up the 'Proxies' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Proxies' - ) - - tab, grid = self.add_inner_notebook_tab( - _('P_roxies'), - inner_notebook, - ) - - # Proxies - self.add_label(grid, - '' + _('Proxies') + '', - 0, 0, 1, 1, - ) - - self.add_label(grid, - '' \ - + _( - 'During a download operation, Tartube will cycle betwween the' \ - + ' proxies in this list', - ) + '', - 0, 1, 1, 1, - ) - - textview, textbuffer = self.add_textview(grid, - self.app_obj.dl_proxy_list, - 0, 2, 1, 1 - ) - textbuffer.connect('changed', self.on_proxy_textview_changed) - - - def setup_operations_prefs_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Preferences' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Preferences' - ) - - tab, grid = self.add_inner_notebook_tab( - _('Pre_ferences'), - inner_notebook, - ) - grid_width = 3 - - # URL flexibility preferences - self.add_label(grid, - '' + _('URL flexibility preferences') + '', - 0, 0, grid_width, 1, - ) - - radiobutton = self.add_radiobutton(grid, - None, - _( - 'If a video\'s URL represents a channel/playlist, not a video,' \ - + ' don\'t download it', - ), - 0, 1, grid_width, 1, - ) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - _('...or, download multiple videos into the containing folder'), - 0, 2, grid_width, 1, - ) - if self.app_obj.operation_convert_mode == 'multi': - radiobutton2.set_active(True) - # (Signal connect appears below) - - radiobutton3 = self.add_radiobutton(grid, - radiobutton2, - _( - '...or, create a new channel, and download the videos into that', - ), - 0, 3, grid_width, 1, - ) - if self.app_obj.operation_convert_mode == 'channel': - radiobutton3.set_active(True) - # (Signal connect appears below) - - radiobutton4 = self.add_radiobutton(grid, - radiobutton3, - _( - '...or, create a new playlist, and download the videos into that', - ), - 0, 4, grid_width, 1, - ) - if self.app_obj.operation_convert_mode == 'playlist': - radiobutton4.set_active(True) - # (Signal connect appears below) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_convert_from_button_toggled, - 'disable', - ) - radiobutton2.connect( - 'toggled', - self.on_convert_from_button_toggled, - 'multi', - ) - radiobutton3.connect( - 'toggled', - self.on_convert_from_button_toggled, - 'channel', - ) - radiobutton4.connect( - 'toggled', - self.on_convert_from_button_toggled, - 'playlist', - ) - - # Missing video preferences - self.add_label(grid, - '' + _('Missing video preferences') + '', - 0, 5, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Add videos which have been removed from a channel/playlist to' \ - + ' the Missing Videos folder', - ), - self.app_obj.track_missing_videos_flag, - True, # Can be toggled by user - 0, 6, grid_width, 1, - ) - # (Signal connect appears below) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Only add videos that were uploaded within this many days', - ), - self.app_obj.track_missing_time_flag, - True, # Can be toggled by user - 0, 7, 1, 1, - ) - if not self.app_obj.track_missing_videos_flag: - checkbutton2.set_sensitive(False) - # (Signal connect appears below) - - spinbutton = self.add_spinbutton(grid, - 0, - 365, - 1, # Step - self.app_obj.track_missing_time_days, - 1, 7, 2, 1, - ) - spinbutton.set_hexpand(True) - if not self.app_obj.track_missing_videos_flag \ - or not self.app_obj.track_missing_time_flag: - spinbutton.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_missing_videos_button_toggled, - checkbutton2, - spinbutton, - ) - checkbutton2.connect( - 'toggled', - self.on_missing_time_button_toggled, - spinbutton, - ) - spinbutton.connect( - 'value-changed', - self.on_missing_time_spinbutton_changed, - ) - - - def setup_operations_missing_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Missing' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Operations > Missing' - ) - - tab, grid = self.add_inner_notebook_tab( - _('Missi_ng'), - inner_notebook, - ) - grid_width = 3 - - # Missing video preferences - self.add_label(grid, - '' + _('Missing video preferences') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'Add videos which have been removed from a channel/playlist to' \ - + ' the Missing Videos folder', - ), - self.app_obj.track_missing_videos_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - # (Signal connect appears below) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'Only add videos that were uploaded within this many days', - ), - self.app_obj.track_missing_time_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - if not self.app_obj.track_missing_videos_flag: - checkbutton2.set_sensitive(False) - # (Signal connect appears below) - - spinbutton = self.add_spinbutton(grid, - 0, - 365, - 1, # Step - self.app_obj.track_missing_time_days, - 1, 2, 2, 1, - ) - spinbutton.set_hexpand(True) - if not self.app_obj.track_missing_videos_flag \ - or not self.app_obj.track_missing_time_flag: - spinbutton.set_sensitive(False) - # (Signal connect appears below) - - # (Signal connects from above) - checkbutton.connect( - 'toggled', - self.on_missing_videos_button_toggled, - checkbutton2, - spinbutton, - ) - checkbutton2.connect( - 'toggled', - self.on_missing_time_button_toggled, - spinbutton, - ) - spinbutton.connect( - 'value-changed', - self.on_missing_time_spinbutton_changed, - ) - - - def setup_downloader_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'youtube-dl' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Downloaders' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab('_Downloaders') - - # ...and an inner notebook... - self.downloader_inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_downloader_forks_tab(self.downloader_inner_notebook) - self.setup_downloader_paths_tab(self.downloader_inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_downloader_ffmpeg_tab(self.downloader_inner_notebook) - self.setup_downloader_streamlink_tab( - self.downloader_inner_notebook, - ) - - - def setup_downloader_forks_tab(self, inner_notebook): - - """Called by self.setup_downloader_tab(). - - Sets up the 'Forks' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Downloaders > Forks' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Forks'), - inner_notebook, - ) - - # Forks of youtube-dl - self.add_label(grid, - '' + _('Forks of youtube-dl') + '', - 0, 0, 1, 1, - ) - - # yt-dlp. Use an event box so the downloader can be selected by - # clicking anywhere in the frame - event_box = Gtk.EventBox() - grid.attach(event_box, 0, 1, 1, 1) - # (Signal connect appears below) - - frame = Gtk.Frame() - event_box.add(frame) - frame.set_border_width(self.spacing_size) - - grid2 = Gtk.Grid() - frame.add(grid2) - grid2.set_border_width(self.spacing_size) - - self.add_label(grid2, - utils.tidy_up_long_string( - 'yt-dlp: ' \ - + self.app_obj.ytdl_fork_descrip_dict['yt-dlp'] \ - + '', - ), - 0, 0, 2, 1, - ) - - self.forks_radiobutton = self.add_radiobutton(grid2, - None, - ' ' + _('Use yt-dlp'), - 0, 1, 1, 1, - ) - # (Signal connect appears below) - - checkbutton = self.add_checkbutton(grid2, - _('Install without dependencies') + '\n' \ - + _('(recommended on MS Windows)'), - self.app_obj.ytdl_fork_no_dependency_flag, - True, # Can be toggled by user - 1, 1, 1, 1, - ) - # (Signal connect appears below) - - # youtube-dl - event_box2 = Gtk.EventBox() - grid.attach(event_box2, 0, 2, 1, 1) - # (Signal connect appears below) - - frame2 = Gtk.Frame() - event_box2.add(frame2) - frame2.set_border_width(self.spacing_size) - - grid3 = Gtk.Grid() - frame2.add(grid3) - grid3.set_border_width(self.spacing_size) - - self.add_label(grid3, - utils.tidy_up_long_string( - 'youtube-dl: ' \ - + self.app_obj.ytdl_fork_descrip_dict['youtube-dl'] \ - + '', - ), - 0, 0, 1, 1, - ) - - self.forks_radiobutton2 = self.add_radiobutton(grid3, - self.forks_radiobutton, - ' ' + _('Use youtube-dl'), - 0, 1, 1, 1, - ) - # (Signal connect appears below) - - # Any other fork - event_box3 = Gtk.EventBox() - grid.attach(event_box3, 0, 3, 1, 1) - # (Signal connect appears below) - - frame3 = Gtk.Frame() - event_box3.add(frame3) - frame3.set_border_width(self.spacing_size) - - grid4 = Gtk.Grid() - frame3.add(grid4) - grid4.set_border_width(self.spacing_size) - grid4.set_row_spacing(self.spacing_size) - - self.add_label(grid4, - '' + utils.tidy_up_long_string( - '' + _('Other forks') + ': ' \ - + self.app_obj.ytdl_fork_descrip_dict['custom'], - ) + '', - 0, 0, 2, 1, - ) - - self.forks_radiobutton3 = self.add_radiobutton(grid4, - self.forks_radiobutton2, - ' ' + _('Use this fork (e.g. youtube-dlc):'), - 0, 1, 1, 1, - ) - # (Signal connect appears below) - self.forks_radiobutton3.set_hexpand(False) - - self.forks_entry = self.add_entry(grid4, - None, - True, - 1, 1, 1, 1, - ) - self.forks_entry.set_sensitive(True) - self.forks_entry.set_max_length(32) - self.forks_entry.set_hexpand(False) - self.forks_entry.set_icon_from_stock( - Gtk.EntryIconPosition.PRIMARY, - 'gtk-yes', - ) - # (Signal connect appears below) - - # Set widgets' initial states - if self.app_obj.ytdl_fork is None \ - or self.app_obj.ytdl_fork == 'youtube-dl': - self.forks_radiobutton2.set_active(True) - checkbutton.set_sensitive(False) - self.forks_entry.set_sensitive(False) - elif self.app_obj.ytdl_fork == 'yt-dlp': - self.forks_radiobutton.set_active(True) - checkbutton.set_sensitive(True) - self.forks_entry.set_sensitive(False) - else: - self.forks_radiobutton3.set_active(True) - if self.app_obj.ytdl_fork is not None: - self.forks_entry.set_text(self.app_obj.ytdl_fork) - else: - self.forks_entry.set_text('') - checkbutton.set_sensitive(False) - self.forks_entry.set_sensitive(True) - - # (Signal connects from above) - event_box.connect( - 'button-press-event', - self.on_ytdl_fork_frame_clicked, - self.forks_radiobutton, - ) - event_box2.connect( - 'button-press-event', - self.on_ytdl_fork_frame_clicked, - self.forks_radiobutton2, - ) - event_box3.connect( - 'button-press-event', - self.on_ytdl_fork_frame_clicked, - self.forks_radiobutton3, - ) - self.forks_radiobutton.connect( - 'toggled', - self.on_ytdl_fork_button_toggled, - checkbutton, - 'yt-dlp', - ) - checkbutton.connect('toggled', self.on_ytdlp_install_button_toggled) - self.forks_radiobutton2.connect( - 'toggled', - self.on_ytdl_fork_button_toggled, - checkbutton, - 'youtube-dl', - ) - self.forks_radiobutton3.connect( - 'toggled', - self.on_ytdl_fork_button_toggled, - checkbutton, - ) - self.forks_entry.connect('changed', self.on_ytdl_fork_changed) - - # Bottom section (always sensitised) - checkbutton2 = self.add_checkbutton(grid, - _( - 'When using other downloaders, filter out yt-dlp download' \ - + ' options', - ), - self.app_obj.ytdlp_filter_options_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - - - def setup_downloader_paths_tab(self, inner_notebook): - - """Called by self.setup_downloader_tab(). - - Sets up the 'File Paths' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Downloaders > File paths' - ) - - tab, grid = self.add_inner_notebook_tab( - _('File _paths'), - inner_notebook, - ) - grid_width = 3 - - # Downloader file paths - self.add_label(grid, - '' + _('Downloader file paths') + '', - 0, 0, grid_width, 1, - ) - - # youtube-dl file paths - self.add_label(grid, - _('Path to the executable'), - 0, 1, 1, 1, - ) - - combo_list = [ - [ - _('Use default path') + ' (' + self.app_obj.ytdl_path_default \ - + ')', - self.app_obj.ytdl_path_default, - ], - ] - - if os.name != 'nt': - - combo_list.append( - [ - _('Use local path') + ' (' + self.app_obj.ytdl_bin + ')', - self.app_obj.ytdl_bin, - ], - ) - - if os.name == 'nt': - msg = _('Use custom path (not recommended on MS Windows)') - else: - msg = _('Use custom path') - combo_list.append( - [ - msg, - None, # Set by the callback - ], - ) - - if os.name != 'nt': - - combo_list.append( - [ - _('Use PyPI path') + ' (' + self.app_obj.ytdl_path_pypi \ - + ')', - self.app_obj.ytdl_path_pypi, - ], - ) - - self.path_liststore = Gtk.ListStore(str, str) - for mini_list in combo_list: - self.path_liststore.append( [ mini_list[0], mini_list[1] ] ) - - self.filepaths_combo = Gtk.ComboBox.new_with_model(self.path_liststore) - grid.attach(self.filepaths_combo, 1, 1, (grid_width - 1), 1) - renderer_text = Gtk.CellRendererText() - self.filepaths_combo.pack_start(renderer_text, True) - self.filepaths_combo.add_attribute(renderer_text, 'text', 0) - self.filepaths_combo.set_entry_text_column(0) - # (Signal connect appears below) - - entry = self.add_entry(grid, - None, - False, - 1, 2, 1, 1, - ) - - button = Gtk.Button(_('Set')) - grid.attach(button, 2, 2, 1, 1) - # (Signal connect appears below) - - # Set up those widgets - if os.name == 'nt': - - if self.app_obj.ytdl_path_custom_flag: - self.filepaths_combo.set_active(1) - else: - self.filepaths_combo.set_active(0) - - else: - - if self.app_obj.ytdl_path_custom_flag: - self.filepaths_combo.set_active(2) - elif self.app_obj.ytdl_path == self.app_obj.ytdl_path_default: - self.filepaths_combo.set_active(0) - elif self.app_obj.ytdl_path == self.app_obj.ytdl_path_pypi: - self.filepaths_combo.set_active(3) - else: - self.filepaths_combo.set_active(1) - - if self.app_obj.ytdl_path_custom_flag: - - # (If this window is loaded due to - # mainapp.TartubeApp.debug_open_pref_win_flag, this value will be - # None) - if self.app_obj.ytdl_path: - entry.set_text(self.app_obj.ytdl_path) - - else: - button.set_sensitive(False) - - # Now set up the next combo - self.add_label(grid, - _('Command for update operations'), - 0, 3, 1, 1, - ) - - self.cmd_liststore = Gtk.ListStore(str, str) - for item in self.app_obj.ytdl_update_list: - self.cmd_liststore.append( [item, formats.YTDL_UPDATE_DICT[item]] ) - - combo2 = Gtk.ComboBox.new_with_model(self.cmd_liststore) - grid.attach(combo2, 1, 3, (grid_width - 1), 1) - - renderer_text = Gtk.CellRendererText() - combo2.pack_start(renderer_text, True) - combo2.add_attribute(renderer_text, 'text', 1) - combo2.set_entry_text_column(1) - - combo2.set_active( - self.app_obj.ytdl_update_list.index( - self.app_obj.ytdl_update_current, - ), - ) - if __main__.__pkg_strict_install_flag__: - combo2.set_sensitive(False) - # (Signal connect appears below) - - # Update the combos, so that the youtube-dl fork, rather than - # youtube-dl itself, is visible (if applicable) - self.update_ytdl_combos() - - # (Signal connects from above) - self.filepaths_combo.connect( - 'changed', - self.on_ytdl_path_combo_changed, - entry, - button, - ) - button.connect('clicked', self.on_ytdl_path_button_clicked, entry) - combo2.connect('changed', self.on_update_combo_changed) - - - def setup_downloader_ffmpeg_tab(self, inner_notebook): - - """Called by self.setup_downloader_tab(). - - Sets up the 'FFmpeg / AVConv' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Downloaders' \ - + ' > FFmpeg / AVConv' - ) - - tab, grid = self.add_inner_notebook_tab( - _('FF_mpeg / AVConv'), - inner_notebook, - ) - grid_width = 4 - - # Post-processor file paths - self.add_label(grid, - '' + _('Post-processor file paths') + '', - 0, 0, grid_width, 1, - ) - self.add_label(grid, - '' + _( - 'You only need to set these paths if Tartube cannot find' \ - + ' FFmpeg / AVConv automatically' - ) + '', - 0, 1, grid_width, 1, - ) - - self.add_label(grid, - _('Path to the FFmpeg executable'), - 0, 2, 1, 1, - ) - - button = Gtk.Button(_('Set')) - grid.attach(button, 1, 2, 1, 1) - # (Signal connect appears below) - - button2 = Gtk.Button(_('Reset')) - grid.attach(button2, 2, 2, 1, 1) - # (Signal connect appears below) - - button3 = Gtk.Button(_('Use default path')) - grid.attach(button3, 3, 2, 1, 1) - # (Signal connect appears below) - - entry = self.add_entry(grid, - self.app_obj.ffmpeg_path, - False, - 0, 3, grid_width, 1, - ) - entry.set_sensitive(False) - entry.set_editable(False) - entry.set_hexpand(True) - - if os.name == 'nt': - entry.set_sensitive(False) - entry.set_text(_('Install from main menu')) - button.set_sensitive(False) - button2.set_sensitive(False) - button3.set_sensitive(False) - - # (Signal connects from above) - button.connect('clicked', self.on_set_ffmpeg_button_clicked, entry) - button2.connect('clicked', self.on_reset_ffmpeg_button_clicked, entry) - button3.connect( - 'clicked', - self.on_default_ffmpeg_button_clicked, entry, - ) - - self.add_label(grid, - _('Path to the AVConv executable'), - 0, 4, 1, 1, - ) - - button4 = Gtk.Button(_('Set')) - grid.attach(button4, 1, 4, 1, 1) - # (Signal connect appears below) - - button5 = Gtk.Button(_('Reset')) - grid.attach(button5, 2, 4, 1, 1) - # (Signal connect appears below) - - button6 = Gtk.Button(_('Use default path')) - grid.attach(button6, 3, 4, 1, 1) - # (Signal connect appears below) - - entry2 = self.add_entry(grid, - self.app_obj.ffmpeg_path, - False, - 0, 5, grid_width, 1, - ) - entry2.set_sensitive(False) - entry2.set_editable(False) - entry2.set_hexpand(True) - - if os.name == 'nt': - entry2.set_sensitive(False) - entry2.set_text(_('Not supported on MS Windows')) - button4.set_sensitive(False) - button5.set_sensitive(False) - button6.set_sensitive(False) - - # (Signal connects from above) - button4.connect('clicked', self.on_set_avconv_button_clicked, entry2) - button5.connect('clicked', self.on_reset_avconv_button_clicked, entry2) - button6.connect( - 'clicked', - self.on_default_avconv_button_clicked, - entry2, - ) - - - def setup_downloader_streamlink_tab(self, inner_notebook): - - """Called by self.setup_downloader_tab(). - - Sets up the 'streamlink' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Downloaders > streamlink' - ) - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'streamlink\' is the name of a Python' \ - + ' module' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_streamlink'), - inner_notebook, - ) - grid_width = 3 - - # streamlink file path - self.add_label(grid, - '' + _('streamlink file path') + '', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - _('Path to the streamlink executable'), - 0, 1, 1, 1, - ) - - button = Gtk.Button(_('Set')) - grid.attach(button, 1, 1, 1, 1) - # (Signal connect appears below) - - button2 = Gtk.Button(_('Reset')) - grid.attach(button2, 2, 1, 1, 1) - # (Signal connect appears below) - - entry = self.add_entry(grid, - self.app_obj.streamlink_path, - False, - 0, 2, grid_width, 1, - ) - entry.set_sensitive(False) - entry.set_editable(False) - entry.set_hexpand(True) - - if os.name == 'nt': - entry.set_sensitive(False) - entry.set_text(_('Install from main menu')) - button.set_sensitive(False) - button2.set_sensitive(False) - - # (Signal connects from above) - button.connect( - 'clicked', - self.on_set_streamlink_button_clicked, - entry, - ) - button2.connect( - 'clicked', - self.on_reset_streamlink_button_clicked, - entry, - ) - - - def setup_options_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Options' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Options' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab('O_ptions') - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_options_dl_list_tab(inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_options_dl_prefs_tab(inner_notebook) - self.setup_options_ffmpeg_list_tab(inner_notebook) - - - def setup_options_dl_list_tab(self, inner_notebook): - - """Called by self.setup_options_tab(). - - Sets up the 'Download options' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Options' \ - + ' > Download options' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Download options'), - inner_notebook, - ) - grid_width = 4 - - # List of download options - self.add_label(grid, - '' + _('List of download options') + '', - 0, 0, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ - '#', _('Name'), _('Videos tab'), _('Classic Mode'), - _('Dropzone'), _('Applied to media'), - ] - ): - if i >= 2 and i <= 4: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.options_liststore = Gtk.ListStore(int, str, bool, bool, bool, str) - treeview.set_model(self.options_liststore) - - # Initialise the list - self.setup_options_dl_list_tab_update_treeview() - - # Add editing buttons - self.add_label(grid, - 'Manager name', - 0, 2, 1, 1, - ) - - entry = self.add_entry(grid, - None, - True, - 1, 2, 1, 1, - ) - - button = Gtk.Button() - grid.attach(button, 2, 2, 1, 1) - button.set_label(_('Add')) - button.connect( - 'clicked', - self.on_options_add_button_clicked, - entry, - ) - - button2 = Gtk.Button() - grid.attach(button2, 3, 2, 1, 1) - button2.set_label(_('Import')) - button2.connect( - 'clicked', - self.on_options_import_button_clicked, - entry, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - button3 = Gtk.Button() - grid2.attach(button3, 0, 0, 1, 1) - button3.set_label(_('Edit')) - button3.connect( - 'clicked', - self.on_options_edit_button_clicked, - treeview, - ) - - button4 = Gtk.Button() - grid2.attach(button4, 1, 0, 1, 1) - button4.set_label(_('Export')) - button4.connect( - 'clicked', - self.on_options_export_button_clicked, - treeview, - ) - - button5 = Gtk.Button() - grid2.attach(button5, 2, 0, 1, 1) - button5.set_label(_('Clone')) - button5.connect( - 'clicked', - self.on_options_clone_button_clicked, - treeview, - ) - - button6 = Gtk.Button() - grid2.attach(button6, 3, 0, 1, 1) - button6.set_label(_('Use in Classic Mode tab')) - button6.connect( - 'clicked', - self.on_options_use_classic_button_clicked, - treeview, - ) - - button7 = Gtk.Button() - grid2.attach(button7, 4, 0, 1, 1) - button7.set_label(_('Delete')) - button7.connect( - 'clicked', - self.on_options_delete_button_clicked, - treeview, - ) - - # (Use an empty label for spacing) - label = self.add_label(grid2, - '', - 5, 0, 1, 1, - ) - label.set_hexpand(True) - - # !!! DEBUG: Clicking the button creates a positional argument error - # !!! for no obvious reason - button8 = Gtk.Button() - grid2.attach(button8, 6, 0, 1, 1) - button8.set_label(_('Refresh list')) - button8.connect( - 'clicked', - self.setup_options_dl_list_tab_update_treeview, - ) - - - def setup_options_dl_list_tab_update_treeview(self): - - """Can be called by anything. - - Fills or updates the treeview. - """ - - self.options_liststore.clear() - - for uid in sorted(self.app_obj.options_reg_dict): - self.setup_options_dl_list_tab_add_row( - self.app_obj.options_reg_dict[uid], - ) - - - def setup_options_dl_list_tab_add_row(self, options_obj): - - """Can be called by anything. - - Adds a row to the treeview. - - Args: - - options_obj (options.OptionsManager) - The options manager object - to display on this row - - """ - - row_list = [] - - row_list.append(options_obj.uid) - row_list.append( - utils.tidy_up_long_string( - options_obj.name, - self.app_obj.main_win_obj.short_string_max_len, - ), - ) - - if self.app_obj.general_options_obj \ - and self.app_obj.general_options_obj == options_obj: - row_list.append(True) - else: - row_list.append(False) - - if self.app_obj.classic_options_obj \ - and self.app_obj.classic_options_obj == options_obj: - row_list.append(True) - else: - row_list.append(False) - - if options_obj.uid in self.app_obj.classic_dropzone_list: - row_list.append(True) - else: - row_list.append(False) - - if not options_obj.dbid_list: - row_list.append('') - else: - row_list.append(self.get_options_applied_text(options_obj)) - - self.options_liststore.append(row_list) - - - def setup_options_dl_prefs_tab(self, inner_notebook): - - """Called by self.setup_downloader_tab(). - - Sets up the 'Preferences' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Options > Preferences' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Preferences'), - inner_notebook, - ) - grid_width = 2 - - # Download options preferences - self.add_label(grid, - '' + _('Download options preferences') + '', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _( - 'When applying download options to something, clone the general' \ - + ' download options', - ), - self.app_obj.auto_clone_options_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.connect('toggled', self.on_auto_clone_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _( - 'After downloading a video, destroy its download options', - ), - self.app_obj.auto_delete_options_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.connect('toggled', self.on_auto_delete_button_toggled) - - - def setup_options_ffmpeg_list_tab(self, inner_notebook): - - """Called by self.setup_options_tab(). - - Sets up the 'FFmpeg options' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Options > FFmpeg options' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_FFmpeg options'), - inner_notebook, - ) - grid_width = 4 - - # List of FFmpeg options managers - self.add_label(grid, - '' + _('List of FFmpeg options managers') + '', - 0, 0, grid_width, 1, - ) - - # (GenericConfigWin.add_treeview() doesn't support multiple columns, so - # we'll do everything ourselves) - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - # (Fourth column is empty, to keep the 3rd column at a minimum width) - for i, column_title in enumerate( - [ '#', _('Name'), _('Current'), '' ] - ): - if i == 2: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.ffmpeg_liststore = Gtk.ListStore(int, str, bool, str) - treeview.set_model(self.ffmpeg_liststore) - - # Initialise the list - self.setup_options_ffmpeg_list_tab_update_treeview() - - # Add editing buttons - self.add_label(grid, - 'Manager name', - 0, 2, 1, 1, - ) - - entry = self.add_entry(grid, - None, - True, - 1, 2, 1, 1, - ) - - button = Gtk.Button() - grid.attach(button, 2, 2, 1, 1) - button.set_label(_('Add')) - button.connect( - 'clicked', - self.on_ffmpeg_add_button_clicked, - entry, - ) - - button2 = Gtk.Button() - grid.attach(button2, 3, 2, 1, 1) - button2.set_label(_('Import')) - button2.connect( - 'clicked', - self.on_ffmpeg_import_button_clicked, - entry, - ) - - # (To avoid messing up the neat format of the rows above, add a - # secondary grid, and put the next set of widgets inside it) - grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1) - - button3 = Gtk.Button() - grid2.attach(button3, 0, 0, 1, 1) - button3.set_label(_('Edit')) - button3.connect( - 'clicked', - self.on_ffmpeg_edit_button_clicked, - treeview, - ) - - button4 = Gtk.Button() - grid2.attach(button4, 1, 0, 1, 1) - button4.set_label(_('Export')) - button4.connect( - 'clicked', - self.on_ffmpeg_export_button_clicked, - treeview, - ) - - button5 = Gtk.Button() - grid2.attach(button5, 2, 0, 1, 1) - button5.set_label(_('Clone')) - button5.connect( - 'clicked', - self.on_ffmpeg_clone_button_clicked, - treeview, - ) - - button6 = Gtk.Button() - grid2.attach(button6, 3, 0, 1, 1) - button6.set_label(_('Use these options')) - button6.connect( - 'clicked', - self.on_ffmpeg_use_button_clicked, - treeview, - ) - - button7 = Gtk.Button() - grid2.attach(button7, 4, 0, 1, 1) - button7.set_label(_('Delete')) - button7.connect( - 'clicked', - self.on_ffmpeg_delete_button_clicked, - treeview, - ) - - # (Use an empty label for spacing) - label = self.add_label(grid2, - '', - 5, 0, 1, 1, - ) - label.set_hexpand(True) - - button8 = Gtk.Button() - grid2.attach(button8, 6, 0, 1, 1) - button8.set_label(_('Refresh list')) - button8.connect( - 'clicked', - self.setup_options_ffmpeg_list_tab_update_treeview, - ) - - - def setup_options_ffmpeg_list_tab_update_treeview(self): - - """Can be called by anything. - - Fills or updates the treeview. - """ - - self.ffmpeg_liststore.clear() - - for uid in sorted(self.app_obj.ffmpeg_reg_dict): - self.setup_options_ffmpeg_list_tab_add_row( - self.app_obj.ffmpeg_reg_dict[uid], - ) - - - def setup_options_ffmpeg_list_tab_add_row(self, options_obj): - - """Can be called by anything. - - Adds a row to the treeview. - - Args: - - options_obj (ffmpeg_tartube.FFmpegOptionsManager): The FFmpeg - options manager object to display on this row - - """ - - row_list = [] - - row_list.append(options_obj.uid) - row_list.append( - utils.tidy_up_long_string( - options_obj.name, - self.app_obj.main_win_obj.short_string_max_len, - ), - ) - - if self.app_obj.ffmpeg_options_obj \ - and self.app_obj.ffmpeg_options_obj == options_obj: - row_list.append(True) - else: - row_list.append(False) - - # (Fourth column is empty, to keep the 3rd column at a minimum width) - row_list.append('') - - self.ffmpeg_liststore.append(row_list) - - - def setup_output_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Output' tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Output' - ) - - # Add this tab... - tab, grid = self.add_notebook_tab(_('O_utput'), 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_output_general_tab(inner_notebook) - self.setup_output_outputtab_tab(inner_notebook) - if not self.app_obj.simple_prefs_flag: - self.setup_output_terminal_tab(inner_notebook) - self.setup_output_log_tab(inner_notebook) - - - def setup_output_general_tab(self, inner_notebook): - - """Called by self.setup_output_tab(). - - Sets up the 'General' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Output > General' - ) - - tab, grid = self.add_inner_notebook_tab(_('_General'), inner_notebook) - - # General preferences (applies to both the Output tab, terminal window - # and downloader log) - self.add_label(grid, - '' + _('General preferences') + '', - 0, 0, 1, 1, - ) - - self.add_label(grid, - '' + _( - 'Applies to Output tab, terminal window and downloader log', - ) + '', - 0, 1, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Downloader writes verbose output (youtube-dl debugging mode)'), - self.app_obj.ytdl_write_verbose_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect('toggled', self.on_ytdl_verbose_button_toggled) - - - def setup_output_outputtab_tab(self, inner_notebook): - - """Called by self.setup_output_tab(). - - Sets up the 'Output tab' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Output > Output tab' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Output tab'), - inner_notebook, - ) - grid_width = 2 - - # Output tab preferences - self.add_label(grid, - '' + _('Output tab preferences') + '', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Display downloader system commands in the Output tab'), - self.app_obj.ytdl_output_system_cmd_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect('toggled', self.on_output_system_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _('Display output from downloader\'s STDOUT in the Output tab'), - self.app_obj.ytdl_output_stdout_flag, - True, # Can be toggled by user - 0, 2, grid_width, 1, - ) - checkbutton2.set_hexpand(False) - # (Signal connect appears below) - - checkbutton3 = self.add_checkbutton(grid, - _('...but don\'t write each video\'s JSON data'), - self.app_obj.ytdl_output_ignore_json_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton3.set_hexpand(False) - checkbutton3.connect('toggled', self.on_output_json_button_toggled) - if not self.app_obj.ytdl_output_stdout_flag: - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid, - _('...but don\'t write each video\'s download progress'), - self.app_obj.ytdl_output_ignore_progress_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - checkbutton4.set_hexpand(False) - checkbutton4.connect('toggled', self.on_output_progress_button_toggled) - if not self.app_obj.ytdl_output_stdout_flag: - checkbutton4.set_sensitive(False) - - # (Signal connect from above) - checkbutton2.connect( - 'toggled', - self.on_output_stdout_button_toggled, - checkbutton3, - checkbutton4, - ) - - checkbutton5 = self.add_checkbutton(grid, - _('Display output from downloader\'s STDERR in the Output tab'), - self.app_obj.ytdl_output_stderr_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - checkbutton5.set_hexpand(False) - checkbutton5.connect('toggled', self.on_output_stderr_button_toggled) - - checkbutton6 = self.add_checkbutton(grid, - _('Limit the size of Output tab pages to'), - self.app_obj.output_size_apply_flag, - True, # Can be toggled by user - 0, 6, 1, 1, - ) - checkbutton6.set_hexpand(False) - checkbutton6.connect('toggled', self.on_output_size_button_toggled) - - spinbutton = self.add_spinbutton(grid, - self.app_obj.output_size_min, - self.app_obj.output_size_max, - 1, # Step - self.app_obj.output_size_default, - 1, 6, 1, 1, - ) - spinbutton.connect( - 'value-changed', - self.on_output_size_spinbutton_changed, - ) - - if not self.app_obj.simple_prefs_flag: - - checkbutton7 = self.add_checkbutton(grid, - _( - 'Empty pages in the Output tab at the start of every' \ - + ' operation', - ), - self.app_obj.ytdl_output_start_empty_flag, - True, # Can be toggled by user - 0, 7, grid_width, 1, - ) - checkbutton7.set_hexpand(False) - checkbutton7.connect( - 'toggled', - self.on_output_empty_button_toggled, - ) - - checkbutton8 = self.add_checkbutton(grid, - _( - 'Show a summary of active threads (changes are applied when' \ - + ' Tartube restarts)', - ), - self.app_obj.ytdl_output_show_summary_flag, - True, # Can be toggled by user - 0, 8, grid_width, 1, - ) - checkbutton8.set_hexpand(False) - checkbutton8.connect( - 'toggled', - self.on_output_summary_button_toggled, - ) - - checkbutton9 = self.add_checkbutton(grid, - _( - 'During update/info operations, automatically switch to the' \ - + ' Output tab', - ), - self.app_obj.auto_switch_output_flag, - True, # Can be toggled by user - 0, 9, grid_width, 1, - ) - checkbutton9.connect('toggled', self.on_auto_switch_button_toggled) - - checkbutton10 = self.add_checkbutton(grid, - _( - 'During a refresh operation, show all matching videos in the' \ - + ' Output tab', - ), - self.app_obj.refresh_output_videos_flag, - True, # Can be toggled by user - 0, 10, grid_width, 1, - ) - checkbutton10.set_hexpand(False) - # (Signal connect appears below) - - checkbutton11 = self.add_checkbutton(grid, - _('...also show all non-matching videos'), - self.app_obj.refresh_output_verbose_flag, - True, # Can be toggled by user - 0, 11, grid_width, 1, - ) - checkbutton11.set_hexpand(False) - checkbutton11.connect( - 'toggled', - self.on_refresh_verbose_button_toggled, - ) - if not self.app_obj.refresh_output_videos_flag: - checkbutton10.set_sensitive(False) - - # (Signal connect from above) - checkbutton10.connect( - 'toggled', - self.on_refresh_videos_button_toggled, - checkbutton11, - ) - - - def setup_output_terminal_tab(self, inner_notebook): - - """Called by self.setup_output_tab(). - - Sets up the 'Terminal window' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Output > Terminal window' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Terminal window'), - inner_notebook, - ) - - # Terminal window preferences - self.add_label(grid, - '' + _('Terminal window preferences') + '', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Write downloader system commands to the terminal window'), - self.app_obj.ytdl_write_system_cmd_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect('toggled', self.on_terminal_system_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _('Write output from downloader\'s STDOUT to the terminal window'), - self.app_obj.ytdl_write_stdout_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.set_hexpand(False) - # (Signal connect appears below) - - checkbutton3 = self.add_checkbutton(grid, - _('...but don\'t write each video\'s JSON data'), - self.app_obj.ytdl_write_ignore_json_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton3.set_hexpand(False) - checkbutton3.connect('toggled', self.on_terminal_json_button_toggled) - if not self.app_obj.ytdl_write_stdout_flag: - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid, - _('...but don\'t write each video\'s download progress'), - self.app_obj.ytdl_write_ignore_progress_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - checkbutton4.set_hexpand(False) - checkbutton4.connect( - 'toggled', - self.on_terminal_progress_button_toggled, - ) - if not self.app_obj.ytdl_write_stdout_flag: - checkbutton4.set_sensitive(False) - - # (Signal connect from above) - checkbutton2.connect( - 'toggled', - self.on_terminal_stdout_button_toggled, - checkbutton3, - checkbutton4, - ) - - checkbutton5 = self.add_checkbutton(grid, - _('Write output from downloader\'s STDERR to the terminal window'), - self.app_obj.ytdl_write_stderr_flag, - True, # Can be toggled by user - 0, 5, 1, 1, - ) - checkbutton5.set_hexpand(False) - checkbutton5.connect( - 'toggled', - self.on_terminal_stderr_button_toggled, - ) - - - def setup_output_log_tab(self, inner_notebook): - - """Called by self.setup_output_tab(). - - Sets up the 'Log' inner notebook tab. - - Args: - - inner_notebook (Gtk.Notebook): The container for this tab - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Output > Log' - ) - - tab, grid = self.add_inner_notebook_tab( - _('_Downloader log'), - inner_notebook, - ) - - # Downloader log preferences - self.add_label(grid, - '' + _('Downloader log preferences') + '', - 0, 0, 1, 1, - ) - - self.add_label(grid, - '' + _( - 'If enabled, the file \'{0}\' is written to Tartube\'s' \ - + ' data folder', - ).format(self.app_obj.ytdl_log_name) + '', - 0, 1, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - _('Write downloader system commands to the log'), - self.app_obj.ytdl_log_system_cmd_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect('toggled', self.on_log_system_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - _('Write output from downloader\'s STDOUT to the log'), - self.app_obj.ytdl_log_stdout_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton2.set_hexpand(False) - # (Signal connect appears below) - - checkbutton3 = self.add_checkbutton(grid, - _('...but don\'t write each video\'s JSON data'), - self.app_obj.ytdl_log_ignore_json_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - checkbutton3.set_hexpand(False) - checkbutton3.connect('toggled', self.on_log_json_button_toggled) - if not self.app_obj.ytdl_log_stdout_flag: - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid, - _('...but don\'t write each video\'s download progress'), - self.app_obj.ytdl_log_ignore_progress_flag, - True, # Can be toggled by user - 0, 5, 1, 1, - ) - checkbutton4.set_hexpand(False) - checkbutton4.connect( - 'toggled', - self.on_log_progress_button_toggled, - ) - if not self.app_obj.ytdl_log_stdout_flag: - checkbutton4.set_sensitive(False) - - # (Signal connect from above) - checkbutton2.connect( - 'toggled', - self.on_log_stdout_button_toggled, - checkbutton3, - checkbutton4, - ) - - checkbutton5 = self.add_checkbutton(grid, - _('Write output from downloader\'s STDERR to the log'), - self.app_obj.ytdl_log_stderr_flag, - True, # Can be toggled by user - 0, 6, 1, 1, - ) - checkbutton5.set_hexpand(False) - checkbutton5.connect( - 'toggled', - self.on_log_stderr_button_toggled, - ) - - - # Callback class methods - - - def on_add_blocked_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables adding blocked videos to the database. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.add_blocked_videos_flag: - self.app_obj.set_add_blocked_videos_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.add_blocked_videos_flag: - self.app_obj.set_add_blocked_videos_flag(False) - - - def on_add_db_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables adding split files to Tartube's database. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.split_video_add_db_flag: - self.app_obj.set_split_video_add_db_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.split_video_add_db_flag: - self.app_obj.set_split_video_add_db_flag(False) - - - def on_add_from_list_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_database_tab(). - - Enables/disables automatic adding of new Tartube data directories to - the list of recent directories. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.data_dir_add_from_list_flag: - self.app_obj.set_data_dir_add_from_list_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.data_dir_add_from_list_flag: - self.app_obj.set_data_dir_add_from_list_flag(False) - - - def on_age_restrict_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_websites_tab(). - - Enables/disables ignoring of YouTube age-restriction error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_yt_age_restrict_flag: - self.app_obj.set_ignore_yt_age_restrict_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_yt_age_restrict_flag: - self.app_obj.set_ignore_yt_age_restrict_flag(False) - - - def on_alt_time_combo_changed(self, combo, type_str): - - """Called from a callback in self.setup_operations_limits_tab(). - - Sets the hours or minutes portion of the start or stop time for - alternative performance limits. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - type_str (str): 'start_hour', 'start_min', 'stop_hour', 'stop_min' - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - value = model[tree_iter][0] - - if type_str == 'start_hour' or type_str == 'start_min': - - if type_str == 'start_hour': - start_time = value + self.app_obj.alt_start_time[2:5] - else: - start_time = self.app_obj.alt_start_time[0:3] + value - - self.app_obj.set_alt_start_time(start_time) - - elif type_str == 'stop_hour' or type_str == 'stop_min': - - if type_str == 'stop_hour': - stop_time = value + self.app_obj.alt_stop_time[2:5] - else: - stop_time = self.app_obj.alt_stop_time[0:3] + value - - self.app_obj.set_alt_stop_time(stop_time) - - - def on_alt_days_combo_changed(self, combo): - - """Called from a callback in self.setup_operations_limits_tab(). - - Sets the day(s) on which alternative performance limits apply. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - type_str (str): One of the values in formats.SPECIFIED_DAYS_LIST, - e.g. 'every_day' - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_alt_day_string(model[tree_iter][1]) - - - def on_archive_button_toggled(self, checkbutton, radiobutton, - radiobutton2, radiobutton3, button, button2): - - """Called from callback in self.setup_operations_archive_tab(). - - Enables/disables creation of youtube-dl's archive file, - ytdl-archive.txt. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - radiobutton, radiobutton2, radiobutton3 (Gtk.RadioButton): Other - widgets to modify - - button, button2 (Gtk.Button): Other widgets to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.allow_ytdl_archive_flag: - self.app_obj.set_allow_ytdl_archive_flag(True) - radiobutton.set_sensitive(True) - radiobutton2.set_sensitive(True) - radiobutton3.set_sensitive(True) - if self.app_obj.allow_ytdl_archive_mode == 'custom': - button.set_sensitive(True) - button2.set_sensitive(True) - else: - button.set_sensitive(False) - button2.set_sensitive(False) - - elif not checkbutton.get_active() \ - and self.app_obj.allow_ytdl_archive_flag: - self.app_obj.set_allow_ytdl_archive_flag(False) - radiobutton.set_sensitive(False) - radiobutton2.set_sensitive(False) - radiobutton3.set_sensitive(False) - button.set_sensitive(False) - button2.set_sensitive(False) - - - def on_archive_classic_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_archive_tab(). - - Enables/disables creation of youtube-dl's archive file, - ytdl-archive.txt, when downloading from the Classic Mode tab. Toggling - the corresponding Gtk.ToggleButton in the Classic Mode tab sets the IV - (and makes sure the two buttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - - other_flag = main_win_obj.classic_archive_button.get_active() - if (checkbutton.get_active() and not other_flag): - main_win_obj.classic_archive_button.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.classic_archive_button.set_active(False) - - - def on_archive_radiobutton_toggled(self, widget, radiobutton, \ - radiobutton2, radiobutton3, entry, button, button2): - - """Called from callback in self.setup_operations_archive_tab(). - - Enables/disables creation of youtube-dl's archive file, - ytdl-archive.txt. - - Args: - - widget (Gtk.RadioButton): The widget clicked - - radiobutton, radiobutton2, radiobutton3 (Gtk.RadioButton): Other - widgets to check - - entry (Gtk.Entry): A widget to modify - - button, button2 (Gtk.Button): Other widgets to modify - - """ - - if radiobutton.get_active(): - self.app_obj.set_allow_ytdl_archive_mode('default') - entry.set_text('') - self.app_obj.set_allow_ytdl_archive_path(None) - button.set_sensitive(False) - button2.set_sensitive(False) - - elif radiobutton2.get_active(): - self.app_obj.set_allow_ytdl_archive_mode('top') - entry.set_text('') - self.app_obj.set_allow_ytdl_archive_path(None) - button.set_sensitive(False) - button2.set_sensitive(False) - - elif radiobutton3.get_active(): - self.app_obj.set_allow_ytdl_archive_mode('custom') - button.set_sensitive(True) - button2.set_sensitive(True) - - - def on_auto_assign_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables auto-assigning anonymous youtube-dl error/warning - messages to the most probable video. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_assign_errors_warnings_flag: - self.app_obj.set_auto_assign_errors_warnings_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_assign_errors_warnings_flag: - self.app_obj.set_auto_assign_errors_warnings_flag(False) - - - def on_auto_clone_button_toggled(self, checkbutton): - - """Called from callback in self.setup_options_prefs(). - - Enables/disables auto-cloning of the General Options Manager. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_clone_options_flag: - self.app_obj.set_auto_clone_options_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_clone_options_flag: - self.app_obj.set_auto_clone_options_flag(False) - - - def on_auto_delete_button_toggled(self, checkbutton): - - """Called from callback in self.setup_options_prefs(). - - Enables/disables auto-deleting of download options applied to a - media.Video, after it has been downloaded. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_delete_options_flag: - self.app_obj.set_auto_delete_options_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_delete_options_flag: - self.app_obj.set_auto_delete_options_flag(False) - - - def on_auto_delete_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables auto-deleting the original video after splitting it - into smaller pieces. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.split_video_auto_delete_flag: - self.app_obj.set_split_video_auto_delete_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.split_video_auto_delete_flag: - self.app_obj.set_split_video_auto_delete_flag(False) - - - def on_auto_delete_videos_button_toggled(self, checkbutton, spinbutton, - checkbutton2, checkbutton3, radiobutton, radiobutton2): - - """Called from callback in self.setup_files_delete_tab(). - - Enables/disables automatic deletion of downloaded videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): A widget to be (de)sensitised - - checkbutton2, checkbutton3 (Gtk.CheckButton): Other widgets to be - (de)sensitised - - radiobutton, radiobutton2 (Gtk.RadioButton): Other widgets to be - (de)sensitised - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_delete_flag: - self.app_obj.set_auto_delete_flag(True) - spinbutton.set_sensitive(True) - checkbutton3.set_sensitive(True) - radiobutton.set_sensitive(True) - radiobutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.auto_delete_flag: - self.app_obj.set_auto_delete_flag(False) - spinbutton.set_sensitive(False) - if checkbutton2.get_active(): - checkbutton3.set_sensitive(True) - radiobutton.set_sensitive(True) - radiobutton2.set_sensitive(True) - else: - checkbutton3.set_active(False) - checkbutton3.set_sensitive(False) - radiobutton.set_active(True) - radiobutton.set_sensitive(False) - radiobutton2.set_sensitive(False) - - - def on_auto_delete_videos_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_files_delete_tab(). - - Sets the number of days after which downloaded videos should be - deleted. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_auto_delete_days(spinbutton.get_value()) - - - def on_auto_remove_videos_button_toggled(self, checkbutton, spinbutton, - checkbutton2, checkbutton3, radiobutton, radiobutton2): - - """Called from callback in self.setup_files_delete_tab(). - - Enables/disables automatic removal of downloaded videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): A widget to be (de)sensitised - - checkbutton2, checkbutton3 (Gtk.CheckButton): Other widgets to be - (de)sensitised - - radiobutton, radiobutton2 (Gtk.RadioButton): Other widgets to be - (de)sensitised - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_remove_flag: - self.app_obj.set_auto_remove_flag(True) - spinbutton.set_sensitive(True) - checkbutton3.set_sensitive(True) - radiobutton.set_sensitive(True) - radiobutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.auto_remove_flag: - self.app_obj.set_auto_remove_flag(False) - spinbutton.set_sensitive(False) - if checkbutton2.get_active(): - checkbutton3.set_sensitive(True) - radiobutton.set_sensitive(True) - radiobutton2.set_sensitive(True) - else: - checkbutton3.set_active(False) - checkbutton3.set_sensitive(False) - radiobutton.set_active(True) - radiobutton.set_sensitive(False) - radiobutton2.set_sensitive(False) - - - def on_auto_remove_videos_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_files_delete_tab(). - - Sets the number of days after which downloaded videos should be - removed. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_auto_remove_days(spinbutton.get_value()) - - - def on_auto_open_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables auto-opening the destination directory after splitting - a video. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.split_video_auto_open_flag: - self.app_obj.set_split_video_auto_open_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.split_video_auto_open_flag: - self.app_obj.set_split_video_auto_open_flag(False) - - - def on_auto_restart_button_toggled(self, checkbutton, spinbutton, - spinbutton2): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables restarting a stalled download operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton, spinbutton2 (Gtk.SpinButton): Other widgets to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.operation_auto_restart_flag: - self.app_obj.set_operation_auto_restart_flag(True) - spinbutton.set_sensitive(True) - spinbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.operation_auto_restart_flag: - self.app_obj.set_operation_auto_restart_flag(False) - spinbutton.set_sensitive(False) - spinbutton2.set_sensitive(False) - - - def on_auto_restart_max_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Sets the maximum number of restarts after a stalled download. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_operation_auto_restart_max(spinbutton.get_value()) - - - def on_auto_restart_time_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Sets the time after which a stalled download job is restarted. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_operation_auto_restart_time(spinbutton.get_value()) - - - def on_auto_switch_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables automatically switching to the Output tab when an - update operation starts. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_switch_output_flag: - self.app_obj.set_auto_switch_output_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_switch_output_flag: - self.app_obj.set_auto_switch_output_flag(False) - - - def on_auto_update_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables automatic update operation before every download - operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.operation_auto_update_flag: - self.app_obj.set_operation_auto_update_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.operation_auto_update_flag: - self.app_obj.set_operation_auto_update_flag(False) - - - def on_autostop_size_button_toggled(self, checkbutton, spinbutton, combo): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Enables/disables auto-stopping a download operation after a certain - amount of disk space. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to modify - - combo (Gtk.ComboBox): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.autostop_size_flag: - self.app_obj.set_autostop_size_flag(True) - spinbutton.set_sensitive(True) - combo.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.autostop_size_flag: - self.app_obj.set_autostop_size_flag(False) - spinbutton.set_sensitive(False) - combo.set_sensitive(False) - - - def on_autostop_size_combo_changed(self, combo): - - """Called from a callback in self.setup_scheduling_stop_tab(). - - Sets the disk space unit at which a download operation is auto-stopped. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_autostop_size_unit(model[tree_iter][0]) - - - def on_autostop_size_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Sets the disk space value at which a download operation is - auto-stopped. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_autostop_size_value(spinbutton.get_value()) - - - def on_autostop_time_button_toggled(self, checkbutton, spinbutton, combo): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Enables/disables auto-stopping a download operation after a certain - time. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to modify - - combo (Gtk.ComboBox): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.autostop_time_flag: - self.app_obj.set_autostop_time_flag(True) - spinbutton.set_sensitive(True) - combo.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.autostop_time_flag: - self.app_obj.set_autostop_time_flag(False) - spinbutton.set_sensitive(False) - combo.set_sensitive(False) - - - def on_autostop_time_combo_changed(self, combo): - - """Called from a callback in self.setup_scheduling_stop_tab(). - - Sets the time unit at which a download operation is auto-stopped. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_autostop_time_unit(model[tree_iter][0]) - - - def on_autostop_time_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Sets the time value at which a download operation is auto-stopped. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_autostop_time_value(spinbutton.get_value()) - - - def on_autostop_videos_button_toggled(self, checkbutton, spinbutton): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Enables/disables auto-stopping a download operation after a certain - number of videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.autostop_videos_flag: - self.app_obj.set_autostop_videos_flag(True) - spinbutton.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.autostop_videos_flag: - self.app_obj.set_autostop_videos_flag(False) - spinbutton.set_sensitive(False) - - - def on_autostop_videos_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Sets the number of videos at which a download operation is - auto-stopped. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_autostop_videos_value(spinbutton.get_value()) - - - def on_backup_button_toggled(self, radiobutton, value): - - """Called from callback in self.setup_files_backups_tab(). - - Updates IVs in the main application. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - value (str): The new value of the IV - - """ - - if radiobutton.get_active(): - self.app_obj.set_db_backup_mode(value) - - - def on_bandwidth_button_toggled(self, checkbutton, alt_flag=False): - - """Called from callback in self.setup_operations_limits_tab(). - - Enables/disables the download speed limit. Toggling the corresponding - Gtk.CheckButton in the Progress tab sets the IV (and makes sure the two - checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - alt_flag (bool): If True, the alternative limit is toggled - - """ - - main_win_obj = self.app_obj.main_win_obj - - if not alt_flag: - - other_flag = main_win_obj.bandwidth_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.bandwidth_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.bandwidth_checkbutton.set_active(False) - - else: - - # Alternative limits. There is no second widget to toggle - if checkbutton.get_active() \ - and not self.app_obj.alt_bandwidth_apply_flag: - self.app_obj.set_alt_bandwidth_apply_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.alt_bandwidth_apply_flag: - self.app_obj.set_alt_bandwidth_apply_flag(False) - - - def on_bandwidth_spinbutton_changed(self, spinbutton, alt_flag=False): - - """Called from callback in self.setup_operations_limits_tab(). - - Sets the simultaneous download limit. Setting the value of the - corresponding Gtk.SpinButton in the Progress tab sets the IV (and - makes sure the two spinbuttons have the same value). - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - alt_flag (bool): If True, the alternative limit is set - - """ - - if not alt_flag: - - self.app_obj.main_win_obj.bandwidth_spinbutton.set_value( - spinbutton.get_value(), - ) - - else: - - # Alternative limits. There is no second widget to toggle - self.app_obj.set_alt_bandwidth(int(spinbutton.get_value())) - - - def on_block_livestreams_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Enables/disables checking/downloading livestreams by yt-dlp - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.block_livestreams_flag: - self.app_obj.set_block_livestreams_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.block_livestreams_flag: - self.app_obj.set_block_livestreams_flag(False) - - - def on_check_comment_fetch_button_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_operations_comments_tab(). - - Enables/disables fetching video comments while checking videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.check_comment_fetch_flag: - self.app_obj.set_check_comment_fetch_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.check_comment_fetch_flag: - self.app_obj.set_check_comment_fetch_flag(False) - - if not self.app_obj.dl_comment_fetch_flag: - checkbutton2.set_active(False) - checkbutton2.set_sensitive(False) - else: - checkbutton2.set_sensitive(True) - - - def on_check_limit_changed(self, entry): - - """Called from callback in self.setup_operations_block_tab(). - - Sets the limit at which a download operation will stop checking a - channel or playlist. - - Args: - - entry (Gtk.Entry): The widget changed - - """ - - text = entry.get_text() - if text.isdigit() and int(text) >= 0: - self.app_obj.set_operation_check_limit(int(text)) - - - def on_clickable_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables clickable channel/playlist names in the Video - Catalogue. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.catalogue_clickable_container_flag: - self.app_obj.set_catalogue_clickable_container_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.catalogue_clickable_container_flag: - self.app_obj.set_catalogue_clickable_container_flag(False) - - - def on_child_process_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of child process exit error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_child_process_exit_flag: - self.app_obj.set_ignore_child_process_exit_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_child_process_exit_flag: - self.app_obj.set_ignore_child_process_exit_flag(False) - - - def on_clear_descrips_button_clicked(self, button): - - """Called from a callback in self.setup_files_update_tab(). - - For every video in the database, clears the description (but doesn't - modify the description file itself). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Update' - ) - - video_count = 0 - update_count = 0 - - for media_data_obj in self.app_obj.media_reg_dict.values(): - - if isinstance(media_data_obj, media.Video): - - video_count += 1 - - if media_data_obj.descrip is not None \ - and media_data_obj.descrip != '': - update_count += 1 - - media_data_obj.reset_video_descrip() - - # Confirm the result - msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \ - + _('Videos updated:') + ' ' + str(update_count) - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_clipboard_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_dialogues_tab(). - - Enables/disables copying from the system clipboard in various dialogue - windows. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.dialogue_copy_clipboard_flag: - self.app_obj.set_dialogue_copy_clipboard_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.dialogue_copy_clipboard_flag: - self.app_obj.set_dialogue_copy_clipboard_flag(False) - - - def on_clips_dir_button_toggled(self, radiobutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Toggles between moving clips to the Video Clips folder. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.app_obj.set_split_video_clips_dir_flag(True) - else: - self.app_obj.set_split_video_clips_dir_flag(False) - - - def on_clips_dl_mode_button_toggled(self, radiobutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Toggles between download clips with FFmpeg and yt-dlp. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.app_obj.set_video_timestamps_dl_mode('ffmpeg') - else: - self.app_obj.set_video_timestamps_dl_mode('downloader') - - - def on_close_to_tray_toggled(self, checkbutton, checkbutton2): - - """Called from a callback in self.setup_windows_system_tray_tab(). - - Enables/disables closing to the system tray, rather than closing the - application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.close_to_tray_flag: - self.app_obj.set_close_to_tray_flag(True) - checkbutton2.set_sensitive(True) - elif not checkbutton.get_active() \ - and self.app_obj.close_to_tray_flag: - self.app_obj.set_close_to_tray_flag(False) - checkbutton2.set_active(False) - checkbutton2.set_sensitive(False) - - - def on_comment_store_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_comments_tab(). - - Enables/disables storing video comments in the Tartube database. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.comment_store_flag: - self.app_obj.set_comment_store_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.comment_store_flag: - self.app_obj.set_comment_store_flag(False) - - - def on_complex_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_videos_tab(). - - Switches between simple/complex views in the Video Index. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - redraw_flag = False - if checkbutton.get_active() and not self.app_obj.complex_index_flag: - self.app_obj.set_complex_index_flag(True) - redraw_flag = True - elif not checkbutton.get_active() and self.app_obj.complex_index_flag: - self.app_obj.set_complex_index_flag(False) - redraw_flag = True - - if redraw_flag: - # Redraw the Video Index and the Video Catalogue (since nothing in - # the Video Index will be selected) - self.app_obj.main_win_obj.video_index_catalogue_reset() - - - def on_confirm_url_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_urls_tab(). - - Enables/disables prompting user for confirmation before modifying a - URL. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.url_change_confirm_flag: - self.app_obj.set_url_change_confirm_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.url_change_confirm_flag: - self.app_obj.set_url_change_confirm_flag(False) - - - def on_container_name_edited(self, widget, path, text, treeview, \ - checkbutton): - - """Called from callback in self.setup_files_urls_tab(). - - Updates the name of a channel/playlist, prompting the user for - confirmation first, if required. - - Args: - - widget (Gtk.CellRendererText): The widget clicked - - path (int): Path to the treeview line that was edited - - text (str): The new contents of the cell - - treeview (Gtk.TreeView): The parent treeview - - checkbutton (Gtk.CheckButton): If active, prompt the user before - updating URLs - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > URLs' - ) - - # Check the entered text is a valid name - if text == '' \ - or re.search(r'^\s*$', text) \ - or not self.app_obj.check_container_name_is_legal(text): - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The name \'{0}\' is not allowed').format(text), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Get the dbid for the selected line's channel/playlist - model = treeview.get_model() - tree_iter = model.get_iter(path) - if tree_iter is not None: - - dbid = model[tree_iter][0] - media_data_obj = self.app_obj.media_reg_dict[dbid] - - if media_data_obj.name == text: - # No change - return - - # Check that the parent folder doesn't already have a container - # with the same name - if ( - media_data_obj.parent_obj is not None \ - and self.app_obj.find_duplicate_name_in_container( - media_data_obj.parent_obj, - text, - ) - ) or ( - media_data_obj.parent_obj is None \ - and self.app_obj.find_duplicate_name_in_container(None, text) - ): - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'There is already a channel, playlist or folder' \ - + ' called \'{0}\'', - ).format(text), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - if not checkbutton.get_active(): - - # Rename without confirmation - if self.app_obj.rename_container_silently( - media_data_obj, - text, - ): - model[tree_iter][3] = text - - else: - - # Seek confirmation before renaming - if isinstance(media_data_obj, media.Channel): - msg = _('Are you sure you want to rename this channel?') - elif isinstance(media_data_obj, media.Playlist): - msg = _('Are you sure you want to rename this playlist?') - elif isinstance(media_data_obj, media.Folder): - msg = _('Are you sure you want to rename this folder?') - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - msg, - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'update_container_name', - 'data': [model, tree_iter, media_data_obj, text], - }, - ) - - - def on_container_url_edited(self, widget, path, text, treeview, \ - checkbutton): - - """Called from callback in self.setup_files_urls_tab(). - - Updates the URL for a channel/playlist, prompting the user for - confirmation first, if required. - - Args: - - widget (Gtk.CellRendererText): The widget clicked - - path (int): Path to the treeview line that was edited - - text (str): The new contents of the cell - - treeview (Gtk.TreeView): The parent treeview - - checkbutton (Gtk.CheckButton): If active, prompt the user before - updating URLs - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > URLs' - ) - - # Check the entered text is a valid URL - if not utils.check_url(text): - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('That is not a valid URL'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Get the dbid for the selected line's channel/playlist - model = treeview.get_model() - tree_iter = model.get_iter(path) - if tree_iter is not None: - - dbid = model[tree_iter][0] - media_data_obj = self.app_obj.media_reg_dict[dbid] - - if not checkbutton.get_active(): - media_data_obj.set_source(text) - model[tree_iter][3] = text - - else: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Are you sure you want to update the URL?'), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'update_container_url', - 'data': [model, tree_iter, media_data_obj, text], - }, - ) - - - def on_container_url_multiple_edited(self, button, entry, entry2, \ - treeview): - - """Called from callback in self.setup_files_urls_tab(). - - Search and replace in the source URLs of the selected channels/ - playlists. - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2 (Gtk.Entry): Widgets containing the search/replace - text - - treeview (Gtk.TreeView): The parent treeview - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + 'System preferences > Files > URLs' - ) - - # Check the pattern (in 'entry') is valid ('entry2' can contain any - # text, including no text at all) - pattern = entry.get_text() - subst = entry2.get_text() - - if not self.app_obj.url_change_regex_flag: - - if pattern == '': - return - - else: - - try: - re.compile(pattern) - - except re.error(): - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The regex is invalid'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Get the media data objects for each selected line - media_list = [] - mod_path_list = [] - - selection = treeview.get_selection() - this_tuple = selection.get_selected_rows() - # (Confusingly, first item in the tuple is the Gtk.ListStore) - model = this_tuple[0] - for path in this_tuple[1]: - - tree_iter = model.get_iter(path) - if tree_iter is not None: - - dbid = model[tree_iter][0] - if dbid in self.app_obj.media_reg_dict: - - media_data_obj = self.app_obj.media_reg_dict[dbid] - if media_data_obj.source is not None: - media_list.append(media_data_obj) - mod_path_list.append(path) - - if not media_list: - # Nothing selected (or channels/playlists removed) - return - - # Get confirmation, before proceeding - if not self.app_obj.url_change_confirm_flag: - - self.app_obj.update_container_url_multiple( - [ - self, - model, - mod_path_list, - media_list, - pattern, - subst, - ], - ) - - else: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Are you sure you want to update these URLs?'), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'update_container_url_multiple', - 'data': \ - [ - self, - model, - mod_path_list, - media_list, - pattern, - subst, - ], - }, - ) - - - def on_convert_from_button_toggled(self, radiobutton, mode): - - """Called from callback in self.setup_operations_prefs_tab(). - - Set what happens when downloading a media.Video object whose URL - represents a channel/playlist. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - mode (str): The new value for the IV: 'disable', 'multi', - 'channel' or 'playlist' - - """ - - if radiobutton.get_active(): - self.app_obj.set_operation_convert_mode(mode) - - - def on_copy_thumb_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables copying the original video's thumbnail after splitting - a video. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.split_video_copy_thumb_flag: - self.app_obj.set_split_video_copy_thumb_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.split_video_copy_thumb_flag: - self.app_obj.set_split_video_copy_thumb_flag(False) - - - def on_copyright_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_websites_tab(). - - Enables/disables ignoring of YouTube copyright errors messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_yt_copyright_flag: - self.app_obj.set_ignore_yt_copyright_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_yt_copyright_flag: - self.app_obj.set_ignore_yt_copyright_flag(False) - - - def on_custom_colour_button_clicked(self, colorbutton, key): - - """Called by self.setup_windows_colours_tab_add_row() - - After the colour selection dialogue has closed, update the custom - background colour used in the Video Catalogue. - - Args: - - colorbutton (Gtk.ColorButton): The widget clicked - - key (str): One of the keys in mainapp.TartubeApp.custom_bg_table - - """ - - rgba_obj = colorbutton.get_color() - - # Update IVs. Insert a standard value for alpha, so the user doesn't - # have to think about it - self.app_obj.set_custom_bg( - key, - (rgba_obj.red / 65536), - (rgba_obj.green / 65536), - (rgba_obj.blue / 65536), - 0.20, - ) - - # Update the custom colour button to show the colour with its true - # alpha value - mini_list = self.app_obj.custom_bg_table[key] - custom_rgba_obj = Gdk.RGBA( - mini_list[0], - mini_list[1], - mini_list[2], - mini_list[3], - ) - colorbutton.set_rgba(custom_rgba_obj) - - # Update the Video Catalogue to show the new colour - if self.app_obj.main_win_obj.video_index_current_dbid is not None: - self.app_obj.main_win_obj.video_catalogue_redraw_all( - self.app_obj.main_win_obj.video_index_current_dbid, - ) - - - def on_custom_dl_add_button_clicked(self, button, entry): - - """Called from callback in self.setup_operations_custom_dl_tab(). - - Adds a new downloads.CustomDLManager object. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Widget providiing a name for the new object - - """ - - name = entry.get_text() - if name == '': - return - - new_obj = self.app_obj.create_custom_dl_manager(name) - - # Update the treeview - self.setup_operations_custom_dl_tab_update_treeview() - # Empty the entry box - entry.set_text('') - - # All other widgets for creating an custom download manager object open - # its edit window, so we'll do the same here - CustomDLEditWin(self.app_obj, new_obj) - - - def on_custom_dl_clone_button_clicked(self, button, treeview): - - """Called from callback in self.setup_operations_custom_dl_tab(). - - Clones the selected downloads.CustomDLManager object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.custom_dl_reg_dict: - - new_obj = self.app_obj.clone_custom_dl_manager( - self.app_obj.custom_dl_reg_dict[uid], - ) - - # Open an edit window, so the user can set the cloned - # object's name - CustomDLEditWin(self.app_obj, new_obj) - - - def on_custom_dl_delete_button_clicked(self, button, treeview): - - """Called from callback in self.setup_operations_custom_dl_tab(). - - Deletes the selected downloads.CustomDLManager object, if allowed. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Operations > Custom' - ) - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - uid = model[this_iter][0] - if not uid in self.app_obj.custom_dl_reg_dict: - return - - custom_dl_obj = self.app_obj.custom_dl_reg_dict[uid] - if self.app_obj.general_custom_dl_obj \ - and self.app_obj.general_custom_dl_obj == custom_dl_obj: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The default custom download manager cannot be deleted'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Prompt for confirmation - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Are you sure you want to delete this custom download manager?'), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'delete_custom_dl_manager', - # Specified manager - 'data': custom_dl_obj, - }, - ) - - - def on_custom_dl_edit_button_clicked(self, button, treeview): - - """Called from callback in self.setup_operations_custom_dl_tab(). - - Opens an edit window for the selected downloads.CustomDLManager object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.custom_dl_reg_dict: - - CustomDLEditWin( - self.app_obj, - self.app_obj.custom_dl_reg_dict[uid], - ) - - - def on_custom_dl_export_button_clicked(self, button, treeview): - - """Called from callback in self.setup_operations_custom_dl_tab(). - - Exports the selected downloads.CustomDLManager object to a JSON file. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.custom_dl_reg_dict: - - self.app_obj.export_custom_dl_manager( - self.app_obj.custom_dl_reg_dict[uid], - ) - - - def on_custom_dl_import_button_clicked(self, button, entry): - - """Called from callback in self.setup_operations_custom_dl_tab(). - - Imports a JSON file and creates a new downloads.CustomDLManager for it. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Widget providiing a name for the new object. - If the entry is empty, then the name specified by the JSON file - itself is used - - """ - - self.app_obj.import_custom_dl_manager(entry.get_text()) - - # Update the treeview - self.setup_operations_custom_dl_tab_update_treeview() - # Empty the entry box - entry.set_text('') - - - def on_custom_dl_use_classic_button_clicked(self, button, treeview): - - """Called from callback in self.setup_operations_custom_dl_tab(). - - Applies the selected downloads.CustomDLManager object to the Classic - Mode tab. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.custom_dl_reg_dict: - - custom_dl_obj = self.app_obj.custom_dl_reg_dict[uid] - - # If this is already the custom download manager for the - # Classic Mode tab, then remove it (but don't delete the - # object itself) - if self.app_obj.classic_custom_dl_obj \ - and self.app_obj.classic_custom_dl_obj == custom_dl_obj: - - self.app_obj.disapply_classic_custom_dl_manager() - - else: - - self.app_obj.apply_classic_custom_dl_manager( - self.app_obj.custom_dl_reg_dict[uid], - ) - - # Update the treeview - self.setup_operations_custom_dl_tab_update_treeview() - - - def on_custom_textview_changed(self, textbuffer): - - """Called from callback in self.setup_windows_websites_tab(). - - Sets the custom of list of ignorable error messages. - - Args: - - textbuffer (Gtk.TextBuffer): The buffer belonging to the textview - whose contents has been modified - - """ - - text = textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - # Filter out empty lines - line_list = text.splitlines() - mod_list = [] - for line in line_list: - if re.search(r'\S', line): - mod_list.append(line) - - # Apply the changes - self.app_obj.set_ignore_custom_msg_list(mod_list) - - - def on_custom_title_changed(self, entry): - - """Called from callback in self.setup_operations_clips_tab(). - - Sets the custom title for split videos. - - Args: - - entry (Gtk.Entry): The widget clicked - - """ - - text = entry.get_text() - if text == '': - self.app_obj.set_split_video_custom_title( - self.app_obj.split_video_generic_title, - ) - else: - self.app_obj.set_split_video_custom_title(text) - - entry.set_text(self.app_obj.split_video_custom_title) - - - def on_data_block_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of data block error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_data_block_error_flag: - self.app_obj.set_ignore_data_block_error_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_data_block_error_flag: - self.app_obj.set_ignore_data_block_error_flag(False) - - - def on_data_check_button_clicked(self, button): - - """Called from callback in self.setup_files_database_tab(). - - Checks the Tartube database for inconsistencies, and fixes them. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.app_obj.check_integrity_db( - False, # Don't run silently; prompt the user before repairing - self, # This window, not the main window, is the parent - ) - - - def on_data_dir_change_button_clicked(self, button): - - """Called from callback in self.setup_files_database_tab(). - - Opens a window in which the user can select Tartube's data directoy. - If the user actually selects it, call the main application to take - action. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Database' - ) - - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Please select Tartube\'s data folder'), - self, - 'folder', - ) - - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - dialogue_manager_obj = self.app_obj.dialogue_manager_obj - - # In the past, I accidentally created a new database directory - # just inside an existing one, rather than switching to the - # existing one - # If no database file exists, prompt the user to create a new one - db_path = os.path.abspath( - os.path.join(new_path, self.app_obj.db_file_name), - ) - - if not os.path.isfile(db_path): - - dialogue_manager_obj.show_msg_dialogue( - _( - 'Are you sure you want to create a new database at' \ - + ' this location?', - ) + '\n\n' + new_path, - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'switch_db', - 'data': [new_path, self], - }, - ) - - else: - - # Database file already exists, so try to load it now - self.try_switch_db(new_path, button) - - - def on_data_dir_cursor_changed(self, treeview, button2, button3, button4, - button5, button6): - - """Called by self.setup_files_database_tab(). - - When a data directory in the list is selected, (de)sensitise buttons - in response. - - Args: - - treeview (Gtk.TreeView): The widget in which a line was selected. - - button2, button3, button4, button5, button6 (Gtk.Button): Other - widgets to be modified - - """ - - selection = treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is not None and not self.app_obj.disable_load_save_flag: - - data_dir = model[tree_iter][0] - - if data_dir != self.app_obj.data_dir: - button2.set_sensitive(True) - button3.set_sensitive(True) - else: - button2.set_sensitive(False) - button3.set_sensitive(False) - - posn = self.app_obj.data_dir_alt_list.index(data_dir) - if posn > 0: - button5.set_sensitive(True) - else: - button5.set_sensitive(False) - - if posn < (len(self.app_obj.data_dir_alt_list) - 1): - button6.set_sensitive(True) - else: - button6.set_sensitive(False) - - else: - - button2.set_sensitive(False) - button3.set_sensitive(False) - button5.set_sensitive(False) - button6.set_sensitive(False) - - if len(self.app_obj.data_dir_alt_list) <= 1 \ - or self.app_obj.disable_load_save_flag: - button4.set_sensitive(False) - else: - button4.set_sensitive(True) - - - def on_data_dir_forget_button_clicked(self, button, treeview): - - """Called from callback in self.setup_files_database_tab(). - - Removes the selected the data directory from the list of alternative - data directories. - - Args: - - button (Gtk.Button): The widget that was clicked - - treeview (Gtk.TreeView): The widget in which a line was selected. - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Database' - ) - - selection = treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - else: - - data_dir = model[tree_iter][0] - - # Should not be possible to click the button, when the current - # directory is selected, but we'll check anyway - if data_dir == self.app_obj.data_dir: - return - - # Prompt the user for confirmation. If the user confirms, this window - # is reset to update the treeview - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Are you sure you want to forget this database?'), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'forget_db', - 'data': [data_dir, self], - }, - ) - - - def on_data_dir_forget_all_button_clicked(self, button, treeview): - - """Called from callback in self.setup_files_database_tab(). - - Removes all data directories from the list of alternatives, except for - the current one. - - Args: - - button (Gtk.Button): The widget that was clicked - - treeview (Gtk.TreeView): The widget in which a line was selected. - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Database' - ) - - # Should not be possible to click the button, when the list contains - # no alternatives but the current one, but we'll check anyway - if len(self.app_obj.data_dir_alt_list) <= 1: - return - - # Prompt the user for confirmation. If the user confirms, this window - # is reset to update the treeview - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Are you sure you want to forget all databases except the' \ - + ' current one?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'forget_all_db', - 'data': self, - }, - ) - - - def on_data_dir_move_down_button_clicked(self, button, treeview, \ - liststore, button2): - - """Called from callback in self.setup_files_database_tab(). - - Moves the selected data directory down one position in the list of - alternative data directories. - - Args: - - button (Gtk.Button): The widget that was clicked (the down button) - - treeview (Gtk.TreeView): The widget in which a line was selected - - liststore (Gtk.ListStore): The treeview's liststore - - button2 (Gtk.Button): The up button - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # (Keeping track of the first/last selected items helps us to - # (de)sensitise buttons, in a moment) - first_item = None - last_item = None - - path_list.reverse() - - for path in path_list: - - this_iter = model.get_iter(path) - last_item = model[this_iter][0] - if first_item is None: - first_item = model[this_iter][0] - - if model.iter_next(this_iter): - - liststore.move_after( - this_iter, - model.iter_next(this_iter), - ) - - else: - - # If the first item won't move up, then successive items will - # be moved above this one (which is not what we want) - break - - # Update the IV - dir_list = [] - for row in liststore: - dir_list.append(row[0]) - - self.app_obj.set_data_dir_alt_list(dir_list) - - # (De)sensitise the button(s), if required - if dir_list.index(first_item) == 0: - button2.set_sensitive(False) - else: - button2.set_sensitive(True) - - if dir_list.index(last_item) == (len(dir_list) - 1): - button.set_sensitive(False) - else: - button.set_sensitive(True) - - - def on_data_dir_move_up_button_clicked(self, button, treeview, liststore, - button2): - - """Called from callback in self.setup_files_database_tab(). - - Moves the selected data directory up one position in the list of - alternative data directories. - - Args: - - button (Gtk.Button): The widget that was clicked (the up button) - - treeview (Gtk.TreeView): The widget in which a line was selected - - liststore (Gtk.ListStore): The treeview's liststore - - button2 (Gtk.Button): The down button - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # (Keeping track of the first/last selected items helps us to - # (de)sensitise buttons, in a moment) - first_item = None - last_item = None - - # Move the selected items up - for path in path_list: - - this_iter = model.get_iter(path) - last_item = model[this_iter][0] - if first_item is None: - first_item = model[this_iter][0] - - if model.iter_previous(this_iter): - - liststore.move_before( - this_iter, - model.iter_previous(this_iter), - ) - - else: - - # If the first item won't move up, then successive items will - # be moved above this one (which is not what we want) - break - - # Update the IV - dir_list = [] - for row in liststore: - dir_list.append(row[0]) - - self.app_obj.set_data_dir_alt_list(dir_list) - - # (De)sensitise the button(s), if required - if dir_list.index(first_item) == 0: - button.set_sensitive(False) - else: - button.set_sensitive(True) - - if dir_list.index(last_item) == (len(dir_list) - 1): - button2.set_sensitive(False) - else: - button2.set_sensitive(True) - - - def on_data_dir_switch_button_clicked(self, button, button2, treeview): - - """Called from callback in self.setup_files_database_tab(). - - Changes the Tartube data directory to the one selected in the - textview. - - Args: - - button (Gtk.Button): The widget clicked - - button2 (Gtk.Button): Another button to be possibly desensitised - - treeview (Gtk.TreeView): A widget in which one file path is - selected (maybe) - - entry, entry2 (Gtk.Entry): Other widgets to be modified - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Database' - ) - - selection = treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - else: - - data_dir = model[tree_iter][0] - - # Should not be possible to click the button, when the current - # directory is selected, but we'll check anyway - if data_dir == self.app_obj.data_dir: - return - - # If no database file exists, prompt the user to create a new one - db_path = os.path.abspath( - os.path.join(data_dir, self.app_obj.db_file_name), - ) - - if not os.path.isfile(db_path): - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - _( - 'No database exists at this location:', - ) + '\n\n' + data_dir + '\n\n' + _( - 'Do you want to create a new one?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'switch_db', - 'data': [data_dir, self], - }, - ) - - else: - - # Database file already exists, so try to load it now - self.try_switch_db(data_dir, button2) - - - def on_delete_asap_button_toggled(self, radiobutton): - - """Called from callback in self.setup_files_delete_tab(). - - Enables/disables automatic deletion/removal of videos after every - download operation. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if not radiobutton.get_active() \ - and not self.app_obj.auto_delete_asap_flag: - self.app_obj.set_auto_delete_asap_flag(True) - elif radiobutton.get_active() \ - and self.app_obj.auto_delete_asap_flag: - self.app_obj.set_auto_delete_asap_flag(False) - - - def on_default_avconv_button_clicked(self, button, entry): - - """Called from callback in self.setup_downloader_ffmpeg_tab(). - - Sets the path to the avconv binary to the default path. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - self.app_obj.set_avconv_path(self.app_obj.default_avconv_path) - entry.set_text(self.app_obj.avconv_path) - - - def on_default_colour_button_clicked(self, colorbutton, event_button, \ - colorbutton2, key): - - """Called by self.setup_windows_colours_tab_add_row() - - After clicking the default colour button, don't allow the usual - colour selection dialogue to open. Instead, update the button showing - the custom colour. - - Args: - - colorbutton (Gtk.ColorButton): The widget clicked - - event_button (Gdk.EventButton): Ignored - - colorbutton2 (Gtk.ColorButton): Another widget to u pdate - - key (str): One of the keys in mainapp.TartubeApp.custom_bg_table - - """ - - # Update IVs - self.app_obj.reset_custom_bg(key) - - # Update the custom colour button - mini_list = self.app_obj.custom_bg_table[key] - custom_rgba_obj = Gdk.RGBA( - mini_list[0], - mini_list[1], - mini_list[2], - mini_list[3], - ) - colorbutton2.set_rgba(custom_rgba_obj) - - # Update the Video Catalogue to show the new colour - if self.app_obj.main_win_obj.video_index_current_dbid is not None: - self.app_obj.main_win_obj.video_catalogue_redraw_all( - self.app_obj.main_win_obj.video_index_current_dbid, - ) - - # Return True so the colour selection dialogue is not opened - return True - - - def on_default_ffmpeg_button_clicked(self, button, entry): - - """Called from callback in self.setup_downloader_ffmpeg_tab(). - - Sets the path to the ffmpeg binary to the default path. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - self.app_obj.set_ffmpeg_path(self.app_obj.default_ffmpeg_path) - entry.set_text(self.app_obj.ffmpeg_path) - - - def on_delete_shutdown_button_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_files_temp_folders_tab(). - - Enables/disables emptying temporary folders when Tartube shuts down. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to be modified - - """ - - if checkbutton.get_active() \ - and not self.app_obj.delete_on_shutdown_flag: - self.app_obj.set_delete_on_shutdown_flag(True) - checkbutton2.set_sensitive(False) - - elif not checkbutton.get_active() \ - and self.app_obj.delete_on_shutdown_flag: - self.app_obj.set_delete_on_shutdown_flag(False) - checkbutton2.set_sensitive(True) - - - def on_delete_watched_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_delete_tab(). - - Enables/disables automatic deletion/removal of videos, but only those - that have been watched. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_delete_watched_flag: - self.app_obj.set_auto_delete_watched_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_delete_watched_flag: - self.app_obj.set_auto_delete_watched_flag(False) - - - def on_dialogue_button_toggled(self, radiobutton, mode): - - """Called from callback in self.setup_operations_actions_tab(). - - Sets whether a desktop notification, dialogue window or neither should - be shown to the user at the end of a download/update/refresh/info/tidy - operation. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - mode (str): The new value for the IV: 'default', 'desktop' or - 'dialogue' - - """ - - if radiobutton.get_active(): - self.app_obj.set_operation_dialogue_mode(mode) - - - def on_dialogue_disable_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_dialogues_tab(). - - Enables/disables message dialogue windows (for testing purposes). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.dialogue_disable_msg_flag: - self.app_obj.set_dialogue_disable_msg_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.dialogue_disable_msg_flag: - self.app_obj.set_dialogue_disable_msg_flag(False) - - - def on_disable_dl_all_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables the 'Download all' buttons in the main window toolbar - and in the Videos tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.disable_dl_all_flag: - self.app_obj.set_disable_dl_all_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.disable_dl_all_flag: - self.app_obj.set_disable_dl_all_flag(False) - - - def on_disk_stop_button_toggled(self, checkbutton, spinbutton): - - """Called from a callback in self.setup_files_device_tab(). - - Enables/disables halting a download operation when the system is - running out of disk space. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.CheckButton): Another widget to be (de)sensitised - - """ - - if checkbutton.get_active() \ - and not self.app_obj.disk_space_stop_flag: - self.app_obj.set_disk_space_stop_flag(True) - spinbutton.set_sensitive(True) - elif not checkbutton.get_active() \ - and self.app_obj.disk_space_stop_flag: - self.app_obj.set_disk_space_stop_flag(False) - spinbutton.set_sensitive(False) - - - def on_disk_stop_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_files_device_tab(). - - Sets the amount of free disk space below which download operations - will be halted. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_disk_space_stop_limit(spinbutton.get_value()) - - - def on_disk_warn_button_toggled(self, checkbutton, spinbutton): - - """Called from a callback in self.setup_files_device_tab(). - - Enables/disables warnings when the system is running out of disk space. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.CheckButton): Another widget to be (de)sensitised - - """ - - if checkbutton.get_active() \ - and not self.app_obj.disk_space_warn_flag: - self.app_obj.set_disk_space_warn_flag(True) - spinbutton.set_sensitive(True) - elif not checkbutton.get_active() \ - and self.app_obj.disk_space_warn_flag: - self.app_obj.set_disk_space_warn_flag(False) - spinbutton.set_sensitive(False) - - - def on_disk_warn_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_files_device_tab(). - - Sets the amount of free disk space below which a warning will be - issued. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_disk_space_warn_limit(spinbutton.get_value()) - - - def on_dl_comment_fetch_button_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_operations_comments_tab(). - - Enables/disables fetching video comments while downloading videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.dl_comment_fetch_flag: - self.app_obj.set_dl_comment_fetch_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.dl_comment_fetch_flag: - self.app_obj.set_dl_comment_fetch_flag(False) - - if not self.app_obj.check_comment_fetch_flag: - checkbutton2.set_active(False) - checkbutton2.set_sensitive(False) - else: - checkbutton2.set_sensitive(True) - - - def on_dl_limit_changed(self, entry): - - """Called from callback in self.setup_operations_block_tab(). - - Sets the limit at which a download operation will stop downloading a - channel or playlist. - - Args: - - entry (Gtk.Entry): The widget changed - - """ - - text = entry.get_text() - if text.isdigit() and int(text) >= 0: - self.app_obj.set_operation_download_limit(int(text)) - - - def on_drag_error_msg_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the error/warning message when dragging - and dropping from the Errors/Warnings tab to an external application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_error_msg_flag: - self.app_obj.set_drag_error_msg_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_error_msg_flag: - self.app_obj.set_drag_error_msg_flag(False) - - - def on_drag_error_name_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the video/channel/playlist name when - dragging and dropping from the Errors/Warnings tab to an external - application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_error_name_flag: - self.app_obj.set_drag_error_name_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_error_name_flag: - self.app_obj.set_drag_error_name_flag(False) - - - def on_drag_error_path_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the video/channel/playlist path when - dragging and dropping from the Errors/Warnings tab to an external - application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_error_path_flag: - self.app_obj.set_drag_error_path_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_error_path_flag: - self.app_obj.set_drag_error_path_flag(False) - - - def on_drag_error_separator_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables adding a separator before each video/channel/playlsit - when dragging and dropping from the Errors/Warnings tab to an external - application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_error_separator_flag: - self.app_obj.set_drag_error_separator_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_error_separator_flag: - self.app_obj.set_drag_error_separator_flag(False) - - - def on_drag_error_source_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the video/channel/playlist URL when - dragging and dropping from the Errors/Warnings tab to an external - application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_error_source_flag: - self.app_obj.set_drag_error_source_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_error_source_flag: - self.app_obj.set_drag_error_source_flag(False) - - - def on_drag_msg_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring of any errors/warnings associated with - the video when dragging and dropping to an external application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_video_msg_flag: - self.app_obj.set_drag_video_msg_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_video_msg_flag: - self.app_obj.set_drag_video_msg_flag(False) - - - def on_drag_name_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the video's name when dragging and - dropping to an external application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_video_name_flag: - self.app_obj.set_drag_video_name_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_video_name_flag: - self.app_obj.set_drag_video_name_flag(False) - - - def on_drag_name_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the video's name when dragging and - dropping to an external application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_video_name_flag: - self.app_obj.set_drag_video_name_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_video_name_flag: - self.app_obj.set_drag_video_name_flag(False) - - - def on_drag_path_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the video's path when dragging and - dropping to an external application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_video_path_flag: - self.app_obj.set_drag_video_path_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_video_path_flag: - self.app_obj.set_drag_video_path_flag(False) - - - def on_drag_separator_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables adding a separator before each video, when dragging - video data into an external application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_video_separator_flag: - self.app_obj.set_drag_video_separator_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_video_separator_flag: - self.app_obj.set_drag_video_separator_flag(False) - - - def on_drag_source_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the video's source URL when dragging and - dropping to an external application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_video_source_flag: - self.app_obj.set_drag_video_source_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_video_source_flag: - self.app_obj.set_drag_video_source_flag(False) - - - def on_drag_thumb_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_drag_tab(). - - Enables/disables transferring the video thumbnail's path when dragging - and dropping to an external application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.drag_thumb_path_flag: - self.app_obj.set_drag_thumb_path_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.drag_thumb_path_flag: - self.app_obj.set_drag_thumb_path_flag(False) - - - def on_dump_db_button_clicked(self, button, treeview): - - """Called from callback in self.setup_files_database_tab(). - - Prompts the user to select a database, then dumps it to JSON. - - The code is here, rather than in mainapp.TartubeApp, so that dialogue - windows can use this preferences window as the parent. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview showing recent data folders - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Database' - ) - - # Prompt the user to select a database file - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Select a Tartube database file'), - self, - 'open', - ) - - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - db_path = dialogue_win.get_filename() - - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK or not db_path: - return - - # Try to load it, ignoring any lockfiles that might be in place - try: - fh = open(db_path, 'rb') - load_dict = pickle.load(fh) - fh.close() - - except: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('This file is not loadable (might be corrupted)'), - 'error', - 'ok', - self, - ) - - return - - if not 'container_reg_dict' in load_dict \ - or not 'media_reg_dict' in load_dict: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('This file does is not compatible with Tartube'), - 'error', - 'ok', - self, - ) - - return - - elif len(load_dict['container_reg_dict']) == 0: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('This file was loaded, but is empty'), - 'error', - 'ok', - self, - ) - - return - - # Prepare a dictionary in the same form exported by - # mainapp.TartubeApp.export_from_db() - container_reg_dict = load_dict['container_reg_dict'] - local = utils.get_local_time() - db_dict = {} - count = 0 - - for dbid in self.app_obj.container_reg_dict.keys(): - - if not dbid in load_dict['media_reg_dict']: - continue - - media_data_obj = load_dict['media_reg_dict'][dbid] - # (Don't export folders; just export channels/playlists as a flat - # dictionary) - if isinstance(media_data_obj, media.Channel) \ - or isinstance(media_data_obj, media.Playlist): - - count += 1 - mini_dict = { - 'dbid': count, - 'vid': None, - 'name': media_data_obj.name, - 'nickname': media_data_obj.nickname, - 'file': None, - 'source': media_data_obj.source, - 'db_dict': {}, - } - - if isinstance(media_data_obj, media.Channel): - mini_dict['type'] = 'channel' - else: - mini_dict['type'] = 'playlist' - - db_dict[count] = mini_dict - - export_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(local.strftime('%d %b %Y')), - 'save_time': str(local.strftime('%H:%M:%S')), - 'file_type': 'db_export', - # Data - 'db_dict': db_dict, - } - - # Try to save the export file - export_path = os.path.abspath( - os.path.join( - os.path.dirname(os.path.realpath(db_path)), - self.app_obj.export_json_file_name, - ), - ) - - try: - with open(export_path, 'w') as outfile: - json.dump(export_dict, outfile, indent=4) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Database export file saved to:') \ - + '\n\n' + export_path, - 'info', - 'ok', - self, - ) - - except Exception as e: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Failed to save the database export file:') \ - + '\n\n' + str(e), - 'error', - 'ok', - self, - ) - - - def on_enable_livestreams_button_toggled(self, checkbutton, checkbutton2, - checkbutton3, spinbutton, spinbutton2): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Enables/disables livestream detection. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2, checkbutton3 (Gtk.CheckButton): Other widgets to - sensitise/desensitise, according to the new value of the flag - - spinbutton, spinbutton2 (Gtk.SpinButton): Another widget to - sensitise/desensitise, according to the new value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.enable_livestreams_flag: - self.app_obj.set_enable_livestreams_flag(True) - checkbutton2.set_sensitive(True) - checkbutton3.set_sensitive(True) - spinbutton.set_sensitive(True) - spinbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.enable_livestreams_flag: - self.app_obj.set_enable_livestreams_flag(False) - checkbutton2.set_active(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_active(False) - checkbutton3.set_sensitive(False) - spinbutton.set_sensitive(False) - spinbutton2.set_sensitive(False) - - - def on_expand_tree_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables auto-expansion of the Video Index after a folder is - selected (clicked). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget that must be - modified - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_expand_video_index_flag: - self.app_obj.set_auto_expand_video_index_flag(True) - checkbutton2.set_sensitive(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_expand_video_index_flag: - self.app_obj.set_auto_expand_video_index_flag(False) - checkbutton2.set_sensitive(False) - - - def on_expand_full_tree_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables full auto-expansion of the Video Index after a folder - is selected (clicked). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.full_expand_video_index_flag: - self.app_obj.set_full_expand_video_index_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.full_expand_video_index_flag: - self.app_obj.set_full_expand_video_index_flag(False) - - - def on_extra_livestreams_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Enables/disables performing more frequent livestream operations when a - livestream is due to start. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.scheduled_livestream_extra_flag: - self.app_obj.set_scheduled_livestream_extra_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.scheduled_livestream_extra_flag: - self.app_obj.set_scheduled_livestream_extra_flag(False) - - - def on_extract_comments_button_clicked(self, button): - - """Called from callback in self.setup_files_update_tab(). - - Extracts timestamps from the description of every video in the - database. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Update' - ) - - video_count = 0 - attempt_count = 0 - success_count = 0 - - for media_data_obj in self.app_obj.media_reg_dict.values(): - - if isinstance(media_data_obj, media.Video): - - video_count += 1 - - if not media_data_obj.comment_list: - - attempt_count += 1 - self.app_obj.update_video_from_json( - media_data_obj, - 'comments', - ) - - if media_data_obj.comment_list: - success_count += 1 - - # Redraw the Video Catalogue, at its current page - main_win_obj = self.app_obj.main_win_obj - main_win_obj.video_catalogue_redraw_all( - main_win_obj.video_index_current_dbid, - main_win_obj.catalogue_toolbar_current_page, - ) - - # Confirm the result - msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \ - + _('Videos checked:') + ' ' + str(attempt_count) + '\n' \ - + _('Videos updated:') + ' ' + str(success_count) - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_extract_stamps_button_clicked(self, button): - - """Called from callback in self.setup_files_update_tab(). - - Extracts timestamps from the description of every video in the - database. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Update' - ) - - video_count = 0 - attempt_count = 0 - success_count = 0 - - for media_data_obj in self.app_obj.media_reg_dict.values(): - - if isinstance(media_data_obj, media.Video): - - video_count += 1 - - if not media_data_obj.stamp_list \ - and media_data_obj.descrip is not None \ - and media_data_obj.descrip != '': - - attempt_count += 1 - - media_data_obj.extract_timestamps_from_descrip( - self.app_obj, - ) - - if media_data_obj.stamp_list: - - success_count += 1 - - # Redraw the Video Catalogue, at its current page - main_win_obj = self.app_obj.main_win_obj - main_win_obj.video_catalogue_redraw_all( - main_win_obj.video_index_current_dbid, - main_win_obj.catalogue_toolbar_current_page, - ) - - # Confirm the result - msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \ - + _('Videos checked:') + ' ' + str(attempt_count) + '\n' \ - + _('Videos updated:') + ' ' + str(success_count) - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_extract_descrip_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables automatically extracting timestamps from the video's - description file. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.video_timestamps_extract_descrip_flag: - self.app_obj.set_video_timestamps_extract_descrip_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.video_timestamps_extract_descrip_flag: - self.app_obj.set_video_timestamps_extract_descrip_flag(False) - - - def on_extract_json_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables automatically extracting timestamps from the video's - metadata file. - - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.video_timestamps_extract_json_flag: - self.app_obj.set_video_timestamps_extract_json_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.video_timestamps_extract_json_flag: - self.app_obj.set_video_timestamps_extract_json_flag(False) - - - def on_ffmpeg_add_button_clicked(self, button, entry): - - """Called from callback in self.setup_options_ffmpeg_list_tab(). - - Adds a new ffmpeg_tartube.FFmpegOptionsManager object. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Widget providiing a name for the new object - - """ - - name = entry.get_text() - if name == '': - return - - new_obj = self.app_obj.create_ffmpeg_options(name) - - # Update the treeview - self.setup_options_ffmpeg_list_tab_update_treeview() - # Empty the entry box - entry.set_text('') - - # All other widgets for creating an options manager object open its - # edit window, so we'll do the same here - FFmpegOptionsEditWin(self.app_obj, new_obj) - - - def on_ffmpeg_clone_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_ffmpeg_list_tab(). - - Clones the selected offmpeg_tartube.FFmpegOptionsManager object . - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.ffmpeg_reg_dict: - - new_obj = self.app_obj.clone_ffmpeg_options( - self.app_obj.ffmpeg_reg_dict[uid], - ) - - # Open an edit window, so the user can set the cloned - # object's name - FFmpegOptionsEditWin(self.app_obj, new_obj) - - - def on_ffmpeg_convert_flag_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables conversion of .webp thumbnails into .jpg thumbnails. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to be updated - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ffmpeg_convert_webp_flag: - self.app_obj.set_ffmpeg_convert_webp_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.ffmpeg_convert_webp_flag: - self.app_obj.set_ffmpeg_convert_webp_flag(False) - checkbutton2.set_active(False) - checkbutton2.set_sensitive(False) - - - def on_ffmpeg_delete_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_ffmpeg_list_tab(). - - Deletes the selected ffmpeg_tartube.FFmpegOptionsManager object, if - allowed. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Options > FFmpeg options' - ) - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - uid = model[this_iter][0] - if not uid in self.app_obj.ffmpeg_reg_dict: - return - - options_obj = self.app_obj.ffmpeg_reg_dict[uid] - if self.app_obj.ffmpeg_options_obj \ - and self.app_obj.ffmpeg_options_obj == options_obj: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The current options manager cannot be deleted'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Prompt for confirmation - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Are you sure you want to delete this options manager?'), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'delete_ffmpeg_options', - # Specified options - 'data': options_obj, - }, - ) - - - def on_ffmpeg_edit_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_ffmpeg_list_tab(). - - Opens an edit window for the selected - ffmpeg_tartube.FFmpegOptionsManager. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.ffmpeg_reg_dict: - - FFmpegOptionsEditWin( - self.app_obj, - self.app_obj.ffmpeg_reg_dict[uid], - ) - - - def on_ffmpeg_export_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_ffmpeg_list_tab(). - - Exports the selected offmpeg_tartube.FFmpegOptionsManager object to a - JSON file. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.ffmpeg_reg_dict: - - self.app_obj.export_ffmpeg_options( - self.app_obj.ffmpeg_reg_dict[uid], - ) - - - def on_ffmpeg_import_button_clicked(self, button, entry): - - """Called from callback in self.setup_options_ffmpeg_list_tab(). - - Imports a JSON file and creates a new - ffmpeg_tartube.FFmpegOptionsManager for it. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Widget providiing a name for the new object. - If the entry is empty, then the name specified by the JSON file - itself is used - - """ - - self.app_obj.import_ffmpeg_options(entry.get_text()) - - # Update the treeview - self.setup_options_ffmpeg_list_tab_update_treeview() - # Empty the entry box - entry.set_text('') - - - def on_ffmpeg_retain_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables conversion of .webp thumbnails into .jpg thumbnails. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ffmpeg_retain_webp_flag: - self.app_obj.set_ffmpeg_retain_webp_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ffmpeg_retain_webp_flag: - self.app_obj.set_ffmpeg_retain_webp_flag(False) - - - def on_ffmpeg_use_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_ffmpeg_list_tab(). - - Sets the selected ffmpeg_tartube.FFmpegOptionsManager object as the - current one. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.ffmpeg_reg_dict: - - self.app_obj.set_ffmpeg_options_obj( - self.app_obj.ffmpeg_reg_dict[uid], - ) - - # Update the treeview - self.setup_options_ffmpeg_list_tab_update_treeview() - - - def on_hide_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables hiding finishe media data objects in the Progress - List. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.hide_finished_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.hide_finished_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.hide_finished_checkbutton.set_active(False) - - - def on_hide_toolbar_button_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables hiding the main window's main toolbar. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another checkbutton to modify - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Windows > Main Window' - ) - - if checkbutton.get_active() \ - and not self.app_obj.toolbar_hide_flag: - self.app_obj.set_toolbar_hide_flag(True) - checkbutton2.set_sensitive(False) - - elif not checkbutton.get_active() \ - and self.app_obj.toolbar_hide_flag: - self.app_obj.set_toolbar_hide_flag(False) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The new setting will be applied when Tartube restarts'), - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_http_404_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of HTTP 404 error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_http_404_error_flag: - self.app_obj.set_ignore_http_404_error_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_http_404_error_flag: - self.app_obj.set_ignore_http_404_error_flag(False) - - - def on_invidious_mirror_changed(self, entry): - - """Called from callback in self.on_invidious_mirror_changed(). - - Sets the Invidious mirror to use. - - Args: - - entry (Gtk.Entry): The widget changed - - """ - - self.app_obj.set_custom_invidious_mirror(entry.get_text()) - - - def on_json_button_toggled(self, checkbutton, spinbutton, spinbutton2): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables applying a timeout when fetching a video's JSON data. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton, spinbutton2 (Gtk.SpinButton): Other widgets to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.apply_json_timeout_flag: - self.app_obj.set_apply_json_timeout_flag(True) - spinbutton.set_sensitive(True) - spinbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.apply_json_timeout_flag: - self.app_obj.set_apply_json_timeout_flag(False) - spinbutton.set_sensitive(False) - spinbutton2.set_sensitive(False) - - - def on_keep_open_button_toggled(self, checkbutton, checkbutton2): - - """Called from a callback in self.setup_windows_dialogues_tab(). - - Enables/disables keeping the dialogue window open when adding channels/ - playlists/folders. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another checkbutton to sensitise/ - desensitise, according to the new value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.dialogue_keep_open_flag: - self.app_obj.set_dialogue_keep_open_flag(True) - checkbutton2.set_sensitive(False) - - elif not checkbutton.get_active() \ - and self.app_obj.dialogue_keep_open_flag: - self.app_obj.set_dialogue_keep_open_flag(False) - checkbutton2.set_sensitive(True) - - - def on_limit_button_toggled(self, checkbutton, entry, entry2): - - """Called from callback in self.setup_operations_block_tab(). - - Sets the limit at which a download operation will stop downloading a - channel or playlist. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - entry, entry2 (Gtk.Entry): The entry boxes which must be - sensitised/desensitised, according to the new setting of the IV - - """ - - if checkbutton.get_active() and not self.app_obj.operation_limit_flag: - self.app_obj.set_operation_limit_flag(True) - entry.set_sensitive(True) - entry2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.operation_limit_flag: - self.app_obj.set_operation_limit_flag(False) - entry.set_sensitive(False) - entry2.set_sensitive(False) - - - def on_livestream_auto_alarm_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_actions_tab(). - - Enables/disables sounding an alarm when a livestream starts. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_auto_alarm_flag: - self.app_obj.set_livestream_auto_alarm_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.livestream_auto_alarm_flag: - self.app_obj.set_livestream_auto_alarm_flag(False) - - - def on_livestream_auto_dl_start_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_actions_tab(). - - Enables/disables downloading a livestream as soon as it starts. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_auto_dl_start_flag: - self.app_obj.set_livestream_auto_dl_start_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.livestream_auto_dl_start_flag: - self.app_obj.set_livestream_auto_dl_start_flag(False) - - - def on_livestream_auto_dl_stop_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_actions_tab(). - - Enables/disables downloading a livestream as soon as it stops. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_auto_dl_stop_flag: - self.app_obj.set_livestream_auto_dl_stop_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.livestream_auto_dl_stop_flag: - self.app_obj.set_livestream_auto_dl_stop_flag(False) - - - def on_livestream_auto_notify_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_actions_tab(). - - Enables/disables desktop notifications when a livestream starts. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_auto_notify_flag: - self.app_obj.set_livestream_auto_notify_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.livestream_auto_notify_flag: - self.app_obj.set_livestream_auto_notify_flag(False) - - - def on_livestream_auto_open_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_actions_tab(). - - Enables/disables opening a livestream in the system's web browser when - it starts. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_auto_open_flag: - self.app_obj.set_livestream_auto_open_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.livestream_auto_open_flag: - self.app_obj.set_livestream_auto_open_flag(False) - - - def on_livestream_colour_button_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables coloured backgrounds for livestream videos in the - Video Catalogue. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_use_colour_flag: - self.app_obj.set_livestream_use_colour_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.livestream_use_colour_flag: - self.app_obj.set_livestream_use_colour_flag(False) - checkbutton2.set_sensitive(False) - - # Redraw the Video Catalogue, at its current page, to update the - # backgrounds - main_win_obj = self.app_obj.main_win_obj - main_win_obj.video_catalogue_redraw_all( - main_win_obj.video_index_current_dbid, - main_win_obj.catalogue_toolbar_current_page, - ) - - - def on_livestream_force_check_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Enables/disables force check of a video, before downloading it as a - livestream. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_force_check_flag: - self.app_obj.set_livestream_force_check_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.livestream_force_check_flag: - self.app_obj.set_livestream_force_check_flag(False) - - - def on_livestream_max_days_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Sets the time (in days) at which Tartube stops looking for livestreams. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_livestream_max_days( - spinbutton.get_value(), - ) - - - def on_livestream_mode_button_toggled(self, radiobutton, value): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Sets the livestream download mode. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - radiobutton2, radiobutton3 (Gtk.RadioButton): Other widgets to - modify - - value (str): The new value of the IV - - """ - - if radiobutton.get_active(): - self.app_obj.set_livestream_dl_mode(value) - - if self.app_obj.livestream_dl_mode == 'streamlink': - self.livestream_radiobutton4.set_active(True) - self.livestream_radiobutton5.set_sensitive(False) - else: - self.livestream_radiobutton5.set_sensitive(True) - - - def on_livestream_replace_button_toggled(self, radiobutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Enables/disables replacing a previously-downloaded livestream. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.app_obj.set_livestream_replace_flag(True) - else: - self.app_obj.set_livestream_replace_flag(False) - - - def on_livestream_simple_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables using the same background colour for livestream and - debut videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_simple_colour_flag: - self.app_obj.set_livestream_simple_colour_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.livestream_simple_colour_flag: - self.app_obj.set_livestream_simple_colour_flag(False) - - # Redraw the Video Catalogue, at its current page, to update the - # backgrounds - main_win_obj = self.app_obj.main_win_obj - main_win_obj.video_catalogue_redraw_all( - main_win_obj.video_index_current_dbid, - main_win_obj.catalogue_toolbar_current_page, - ) - - - def on_livestream_stop_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Enables/disables marking a stopped livestream as downloaded. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.livestream_stop_is_final_flag: - self.app_obj.set_livestream_stop_is_final_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.livestream_stop_is_final_flag: - self.app_obj.set_livestream_stop_is_final_flag(False) - - - def on_livestream_timeout_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Sets the timeout (in minutes) for livestream downloads. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_livestream_dl_timeout( - spinbutton.get_value(), - ) - - - def on_load_descrips_button_clicked(self, button, spinbutton): - - """Called from a callback in self.setup_files_update_tab(). - - For every video in the database, updates the description from the - .description file. - - Args: - - button (Gtk.Button): The widget clicked - - spinbutton (Gtk.SpinButton): Widget setting the maximum line - length - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Update' - ) - - video_count = 0 - update_count = 0 - - for media_data_obj in self.app_obj.media_reg_dict.values(): - - if isinstance(media_data_obj, media.Video): - - video_count += 1 - - old_descrip = media_data_obj.descrip - - media_data_obj.read_video_descrip( - self.app_obj, - int(spinbutton.get_value()), - ) - - if old_descrip != media_data_obj.descrip: - update_count += 1 - - # Confirm the result - msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \ - + _('Videos updated:') + ' ' + str(update_count) - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_locale_combo_changed(self, combo, grid): - - """Called from a callback in self.setup_general_language_tab(). - - Sets the override locale for Tartube. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - grid (Gtk.Grid): The grid on which this tab's widgets are - arranged - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > General > Application' - ) - - tree_iter = combo.get_active_iter() - model = combo.get_model() - locale = model[tree_iter][2] - - if locale == '': - self.app_obj.reset_override_local() - else: - self.app_obj.set_override_local(locale) - - # Add some more widgets to tell the user to restart Tartube - # As the user might not know the language, show an icon as well as - # some text - # Use an extra grid to avoid messing up the layout of widgets above - grid2 = self.add_secondary_grid(grid, 0, 4, 3, 1) - grid2.set_border_width(self.spacing_size * 2) - - frame = self.add_image(grid2, - self.app_obj.main_win_obj.icon_dict['warning_large'], - 0, 0, 1, 1, - ) - # (The frame looks cramped without this. The icon itself is 32x32) - frame.set_size_request( - 32 + (self.spacing_size * 2), - 32 + (self.spacing_size * 2), - ) - - self.add_label(grid2, - '' + _( - 'The new setting will be applied when Tartube' \ - + ' restarts', - ) + '', - 1, 0, 1, 1, - ) - - self.show_all() - - - def on_log_json_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_log_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - downloader log. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_log_ignore_json_flag: - self.app_obj.set_ytdl_log_ignore_json_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_log_ignore_json_flag: - self.app_obj.set_ytdl_log_ignore_json_flag(False) - - - def on_log_progress_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_log_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - downloader log. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_log_ignore_progress_flag: - self.app_obj.set_ytdl_log_ignore_progress_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_log_ignore_progress_flag: - self.app_obj.set_ytdl_log_ignore_progress_flag(False) - - - def on_log_stderr_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_log_tab(). - - Enables/disables writing output from youtube-dl's STDERR to the - downloader log. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_log_stderr_flag: - self.app_obj.set_ytdl_log_stderr_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_log_stderr_flag: - self.app_obj.set_ytdl_log_stderr_flag(False) - - - def on_log_stdout_button_toggled(self, checkbutton, checkbutton2, \ - checkbutton3): - - """Called from a callback in self.setup_output_log_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - downloader log. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2, checkbutton3 (Gtk.CheckButton): Additional - checkbuttons to sensitise/desensitise, according to the new - value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_log_stdout_flag: - self.app_obj.set_ytdl_log_stdout_flag(True) - checkbutton2.set_sensitive(True) - checkbutton3.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_log_stdout_flag: - self.app_obj.set_ytdl_log_stdout_flag(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_sensitive(False) - - - def on_log_system_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_log_tab(). - - Enables/disables writing youtube-dl system commands to the downloader - log. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_log_system_cmd_flag: - self.app_obj.set_ytdl_log_system_cmd_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_log_system_cmd_flag: - self.app_obj.set_ytdl_log_system_cmd_flag(False) - - - def on_match_button_toggled(self, radiobutton): - - """Called from callback in self.setup_files_video_deletion_tab(). - - Updates IVs in the main application and sensities/desensities widgets. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - default_val = self.app_obj.match_default_chars - - if radiobutton.get_active(): - - if radiobutton == self.radiobutton3: - self.app_obj.set_match_method('exact_match') - # (Changing the contents of the widgets automatically updates - # mainapp.TartubeApp IVs) - self.spinbutton3.set_value(default_val) - self.spinbutton3.set_sensitive(False) - self.spinbutton4.set_value(default_val) - self.spinbutton4.set_sensitive(False) - - elif radiobutton == self.radiobutton4: - self.app_obj.set_match_method('match_first') - self.spinbutton3.set_sensitive(True) - self.spinbutton4.set_value(default_val) - self.spinbutton4.set_sensitive(False) - - else: - self.app_obj.set_match_method('ignore_last') - self.spinbutton3.set_value(default_val) - self.spinbutton3.set_sensitive(False) - self.spinbutton4.set_sensitive(True) - - - def on_match_nickname_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_videos_tab(). - - Enables/disables matching against both media.Video.name and - media.Video.nickname. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to be modified - - """ - - if checkbutton.get_active() \ - and not self.app_obj.match_nickname_flag: - self.app_obj.set_match_nickname_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.match_nickname_flag: - self.app_obj.set_match_nickname_flag(False) - - - def on_match_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_files_video_deletion_tab(). - - Updates IVs in the main application and sensities/desensities widgets. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - if spinbutton == self.spinbutton3: - self.app_obj.set_match_first_chars(spinbutton.get_value()) - else: - self.app_obj.set_match_ignore_chars(spinbutton.get_value()) - - - def on_merge_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of 'Requested formats are incompatible for - merge and will be merged into mkv' warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_merge_warning_flag: - self.app_obj.set_ignore_merge_warning_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_merge_warning_flag: - self.app_obj.set_ignore_merge_warning_flag(False) - - - def on_missing_format_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of missing format error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_missing_format_error_flag: - self.app_obj.set_ignore_missing_format_error_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_missing_format_error_flag: - self.app_obj.set_ignore_missing_format_error_flag(False) - - - def on_missing_time_button_toggled(self, checkbutton, spinbutton): - - """Called from callback in self.setup_operations_prefs_tab(). - - Enables/disables a time limit when tracking videos missing from a - channel/playlist. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.track_missing_time_flag: - - self.app_obj.set_track_missing_time_flag(True) - spinbutton.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.track_missing_time_flag: - self.app_obj.set_track_missing_time_flag(False) - spinbutton.set_sensitive(False) - - - def on_missing_time_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_prefs_tab(). - - Sets a time limit when tracking videos missing from a channel/playlist. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_track_missing_time_days( - spinbutton.get_value(), - ) - - - def on_missing_videos_button_toggled(self, checkbutton, checkbutton2, \ - spinbutton): - - """Called from callback in self.setup_operations_prefs_tab(). - - Enables/disables tracking videos missing from a channel/playlist. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to modify - - spinbutton (Gtk.SpinButton): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.track_missing_videos_flag: - - self.app_obj.set_track_missing_videos_flag(True) - checkbutton2.set_sensitive(True) - if self.app_obj.track_missing_time_flag: - spinbutton.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.track_missing_videos_flag: - checkbutton2.set_active(False) - self.app_obj.set_track_missing_videos_flag(False) - checkbutton2.set_sensitive(False) - spinbutton.set_sensitive(False) - - - def on_moviepy_button_toggled(self, checkbutton): - - """Called from callback in self.setup_general_modules_tab(). - - Enables/disables use of the moviepy.editor module. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.use_module_moviepy_flag: - self.app_obj.set_use_module_moviepy_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.use_module_moviepy_flag: - self.app_obj.set_use_module_moviepy_flag(False) - - - def on_moviepy_timeout_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_general_modules_tab(). - - Sets the timeout to apply to threads using the moviepy module. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_refresh_moviepy_timeout( - spinbutton.get_value(), - ) - - - def on_nickname_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables showing video nicknames in the Video Catalogue. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.catalogue_show_nickname_flag: - self.app_obj.set_catalogue_show_nickname_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.catalogue_show_nickname_flag: - self.app_obj.set_catalogue_show_nickname_flag(False) - - - def on_no_annotations_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of the 'no annotations' warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_no_annotations_flag: - self.app_obj.set_ignore_no_annotations_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_no_annotations_flag: - self.app_obj.set_ignore_no_annotations_flag(False) - - - def on_no_subtitles_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of the 'no subtitles' warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_no_subtitles_flag: - self.app_obj.set_ignore_no_subtitles_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_no_subtitles_flag: - self.app_obj.set_ignore_no_subtitles_flag(False) - - - def on_no_descrip_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of the 'no playlist description to write' - warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_no_descrip_flag: - self.app_obj.set_ignore_no_descrip_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_no_descrip_flag: - self.app_obj.set_ignore_no_descrip_flag(False) - - - def on_open_desktop_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_temp_folders_tab(). - - Enables/disables opening temporary folders on the desktop when Tartube - shuts down. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.open_temp_on_desktop_flag: - self.app_obj.set_open_temp_on_desktop_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.open_temp_on_desktop_flag: - self.app_obj.set_open_temp_on_desktop_flag(False) - - - def on_open_in_tray_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_system_tray_tab(). - - Enables/disables opening Tartube in the system tray. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.open_in_tray_flag: - self.app_obj.set_open_in_tray_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.open_in_tray_flag: - self.app_obj.set_open_in_tray_flag(False) - - - def on_open_url_clicked(self, button, treeview): - - """Called from a callback in self.setup_files_urls_tab(). - - Opens the URL in the selected line. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The parent treeview - - """ - - # Get the media data objects for each selected line - selection = treeview.get_selection() - this_tuple = selection.get_selected_rows() - # (Confusingly, first item in the tuple is the Gtk.ListStore) - model = this_tuple[0] - for path in this_tuple[1]: - - tree_iter = model.get_iter(path) - if tree_iter is not None: - - dbid = model[tree_iter][0] - if dbid in self.app_obj.media_reg_dict: - - media_data_obj = self.app_obj.media_reg_dict[dbid] - if media_data_obj.source is not None: - - utils.open_file(self.app_obj, media_data_obj.source) - - - def on_operation_error_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables operation errors in the 'Errors/Warnings' tab. - Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab - sets the IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.show_operation_error_checkbutton.get_active() - - main_win_obj = self.app_obj.main_win_obj - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_operation_error_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_operation_error_checkbutton.set_active(False) - - - def on_operation_sim_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables ignoring already-checked videos whose parent is a - media.Folder, if the videos have already been checked. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.operation_sim_shortcut_flag: - self.app_obj.set_operation_sim_shortcut_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.operation_sim_shortcut_flag: - self.app_obj.set_operation_sim_shortcut_flag(False) - - - def on_operation_warning_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables operation warnings in the 'Errors/Warnings' tab. - Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab - sets the IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag \ - = main_win_obj.show_operation_warning_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_operation_warning_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_operation_warning_checkbutton.set_active(False) - - - def on_options_add_button_clicked(self, button, entry): - - """Called from callback in self.setup_options_dl_list_tab(). - - Adds a new options.OptionsManager object. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Widget providiing a name for the new object - - """ - - name = entry.get_text() - if name == '': - return - - new_obj = self.app_obj.create_download_options(name) - - # If required, clone download options from the General Options Manager - # into the new object - if self.app_obj.auto_clone_options_flag: - new_obj.clone_options(self.app_obj.general_options_obj) - - # On the assumption that objects created here will be applied (mostly) - # in the Classic Mode tab, disable downloading the description, - # annotations (etc) files - new_obj.set_classic_mode_options() - - # Update the treeview - self.setup_options_dl_list_tab_update_treeview() - # Empty the entry box - entry.set_text('') - - # All other widgets for creating an options manager object open its - # edit window, so we'll do the same here - OptionsEditWin(self.app_obj, new_obj) - - - def on_options_clone_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_dl_list_tab(). - - Clones the selected options.OptionsManager object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.options_reg_dict: - - new_obj = self.app_obj.clone_download_options( - self.app_obj.options_reg_dict[uid], - ) - - # Open an edit window, so the user can set the cloned - # object's name - OptionsEditWin(self.app_obj, new_obj) - - - def on_options_delete_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_dl_list_tab(). - - Deletes the selected options.OptionsManager object, if allowed. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Options > Download options' - ) - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - - return - - uid = model[this_iter][0] - if not uid in self.app_obj.options_reg_dict: - return - - options_obj = self.app_obj.options_reg_dict[uid] - if self.app_obj.general_options_obj \ - and self.app_obj.general_options_obj == options_obj: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The default options manager cannot be deleted'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Prompt for confirmation - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Are you sure you want to delete this options manager?'), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'delete_download_options', - # Specified options - 'data': options_obj, - }, - ) - - - def on_options_edit_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_dl_list_tab(). - - Opens an edit window for the selected options.OptionsManager object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.options_reg_dict: - - OptionsEditWin( - self.app_obj, - self.app_obj.options_reg_dict[uid], - ) - - - def on_options_export_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_dl_list_tab(). - - Exports the selected options.OptionsManager object to a JSON file. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.options_reg_dict: - - self.app_obj.export_download_options( - self.app_obj.options_reg_dict[uid], - ) - - - def on_options_import_button_clicked(self, button, entry): - - """Called from callback in self.setup_options_dl_list_tab(). - - Imports a JSON file and creates a new options.OptionsManager for it. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Widget providiing a name for the new object. - If the entry is empty, then the name specified by the JSON file - itself is used - - """ - - self.app_obj.import_download_options(entry.get_text()) - - # Update the treeview - self.setup_options_dl_list_tab_update_treeview() - # Empty the entry box - entry.set_text('') - - - def on_options_use_classic_button_clicked(self, button, treeview): - - """Called from callback in self.setup_options_dl_list_tab(). - - Applies the selected options.OptionsManager object to the Classic Mode - tab. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if path_list: - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is not None: - - uid = model[this_iter][0] - if uid in self.app_obj.options_reg_dict: - - options_obj = self.app_obj.options_reg_dict[uid] - - # If this is already the options manager for the Classic - # Mode tab, then remove it (but don't delete the object - # itself) - if self.app_obj.classic_options_obj \ - and self.app_obj.classic_options_obj == options_obj: - - self.app_obj.remove_classic_download_options() - - else: - - self.app_obj.apply_classic_download_options( - self.app_obj.options_reg_dict[uid], - ) - - # Update the treeview - self.setup_options_dl_list_tab_update_treeview() - - - def on_output_empty_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables emptying pages in the Output tab at the start of every - operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_start_empty_flag: - self.app_obj.set_ytdl_output_start_empty_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_start_empty_flag: - self.app_obj.set_ytdl_output_start_empty_flag(False) - - - def on_output_json_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the Output - tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_ignore_json_flag: - self.app_obj.set_ytdl_output_ignore_json_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_ignore_json_flag: - self.app_obj.set_ytdl_output_ignore_json_flag(False) - - - def on_output_progress_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the Output - tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_ignore_progress_flag: - self.app_obj.set_ytdl_output_ignore_progress_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_ignore_progress_flag: - self.app_obj.set_ytdl_output_ignore_progress_flag(False) - - - def on_output_stderr_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing output from youtube-dl's STDERR to the Output - tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_stderr_flag: - self.app_obj.set_ytdl_output_stderr_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_stderr_flag: - self.app_obj.set_ytdl_output_stderr_flag(False) - - - def on_output_stdout_button_toggled(self, checkbutton, checkbutton2, \ - checkbutton3): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the Output - tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2, checkbutton3 (Gtk.CheckButton): Additional - checkbuttons to sensitise/desensitise, according to the new - value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_stdout_flag: - self.app_obj.set_ytdl_output_stdout_flag(True) - checkbutton2.set_sensitive(True) - checkbutton3.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_stdout_flag: - self.app_obj.set_ytdl_output_stdout_flag(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_sensitive(False) - - - def on_output_summary_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables displaying a summary page in the Output tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_show_summary_flag: - self.app_obj.set_ytdl_output_show_summary_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_show_summary_flag: - self.app_obj.set_ytdl_output_show_summary_flag(False) - - - def on_output_size_button_toggled(self, checkbutton): - - """Called from callback in self.setup_output_outputtab_tab(). - - Enables/disables applying a maximum size to the Output tab pages. - Toggling the corresponding Gtk.CheckButton in the Output tab sets the - IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - other_flag \ - = self.app_obj.main_win_obj.output_size_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - self.app_obj.main_win_obj.output_size_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - self.app_obj.main_win_obj.output_size_checkbutton.set_active(False) - - - def on_output_size_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_output_outputtab_tab(). - - Sets the maximum size of the Output tab pages. Setting the value of the - corresponding Gtk.SpinButton in the Output tab sets the IV (and - makes sure the two spinbuttons have the same value). - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.main_win_obj.output_size_spinbutton.set_value( - spinbutton.get_value(), - ) - - - def on_output_system_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing youtube-dl system commands to the Output tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_system_cmd_flag: - self.app_obj.set_ytdl_output_system_cmd_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_system_cmd_flag: - self.app_obj.set_ytdl_output_system_cmd_flag(False) - - - def on_page_given_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of the 'a channel/user page was given' - warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_page_given_flag: - self.app_obj.set_ignore_page_given_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_page_given_flag: - self.app_obj.set_ignore_page_given_flag(False) - - - def on_payment_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_websites_tab(). - - Enables/disables ignoring of payment required error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_yt_payment_flag: - self.app_obj.set_ignore_yt_payment_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_yt_payment_flag: - self.app_obj.set_ignore_yt_payment_flag(False) - - - def on_pretty_date_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables 'today' and 'yesterday' rather than a numerical date - in the Videos tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_pretty_dates_flag: - self.app_obj.set_show_pretty_dates_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_pretty_dates_flag: - self.app_obj.set_show_pretty_dates_flag(False) - - - def on_proxy_textview_changed(self, textbuffer): - - """Called from callback in self.setup_operations_proxies_tab(). - - Sets the list of proxies. - - Args: - - textbuffer (Gtk.TextBuffer): The buffer belonging to the textview - whose contents has been modified - - """ - - text = textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - # Filter out empty lines - line_list = text.splitlines() - mod_list = [] - for line in line_list: - if re.search(r'\S', line): - mod_list.append(line) - - # Apply the changes - self.app_obj.set_dl_proxy_list(mod_list) - - - def on_recalculate_stats_button_clicked(self, button, entry, entry2, - entry3, entry4, entry5, entry6): - - """Called from callback in self.setup_files_statistics_tab(). - - Recalculates the number of media data objects in the Tartube database, - and updates the entry boxes. - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2, entry3, entry4, entry5, entry6 (Gtk.Entry): The - entry boxes to update - - """ - - self.setup_files_statistics_tab_recalculate( - entry, - entry2, - entry3, - entry4, - entry5, - entry6, - ) - - - def on_reextract_stamps_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables re-extracting timestamps just before splitting a - video. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.video_timestamps_re_extract_flag: - self.app_obj.set_video_timestamps_re_extract_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.video_timestamps_re_extract_flag: - self.app_obj.set_video_timestamps_re_extract_flag(False) - - - def on_refresh_verbose_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables displaying non-matching videos in the Output tab - during a refresh operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.refresh_output_verbose_flag: - self.app_obj.set_refresh_output_verbose_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.refresh_output_verbose_flag: - self.app_obj.set_refresh_output_verbose_flag(False) - - - def on_refresh_videos_button_toggled(self, checkbutton, checkbutton2): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables displaying matching videos in the Output tab during a - refresh operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): A different checkbutton to - sensitise/desensitise, according to the new value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.refresh_output_videos_flag: - self.app_obj.set_refresh_output_videos_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.refresh_output_videos_flag: - self.app_obj.set_refresh_output_videos_flag(False) - checkbutton2.set_sensitive(False) - - - def on_regex_button_toggled(self, radiobutton, flag): - - """Called from callback in self.setup_windows_websites_tab(). - - Sets whether mainapp.TartubeApp.ignore_custom_msg_list contains - ordinary strings or regexes. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - flag (bool): False for ordinary strings, True for regexes - - """ - - if radiobutton.get_active(): - self.app_obj.set_ignore_custom_regex_flag(flag) - - - def on_remember_size_button_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables remembering the size of the main window. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to be modified - - """ - - if checkbutton.get_active() \ - and not self.app_obj.main_win_save_size_flag: - self.app_obj.set_main_win_save_size_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.main_win_save_size_flag: - self.app_obj.set_main_win_save_size_flag(False) - checkbutton2.set_sensitive(False) - checkbutton2.set_active(False) - - - def on_remember_slider_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables remembering the position of sliders in the main - window. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.main_win_save_slider_flag: - self.app_obj.set_main_win_save_slider_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.main_win_save_slider_flag: - self.app_obj.set_main_win_save_slider_flag(False) - - - def on_remember_width_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_main_window_tab(). - - Enables/disables remembering the width of some columns in the Progress - and Classic Mode tabs. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.progress_list_remember_width_flag: - self.app_obj.set_progress_list_remember_width_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.progress_list_remember_width_flag: - self.app_obj.set_progress_list_remember_width_flag(False) - - - def on_remove_comments_button_clicked(self, button): - - """Called from callback in self.setup_files_update_tab(). - - Clears comments from every video in the database. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Update' - ) - - video_count = 0 - success_count = 0 - - for media_data_obj in self.app_obj.media_reg_dict.values(): - - if isinstance(media_data_obj, media.Video): - - video_count += 1 - if media_data_obj.comment_list: - - success_count += 1 - media_data_obj.reset_comments() - - - # Redraw the Video Catalogue, at its current page - main_win_obj = self.app_obj.main_win_obj - main_win_obj.video_catalogue_redraw_all( - main_win_obj.video_index_current_dbid, - main_win_obj.catalogue_toolbar_current_page, - ) - - # Confirm the result - msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \ - + _('Videos updated:') + ' ' + str(success_count) - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_remove_container_file_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_delete_tab(). - - Enables/disables removing all files from the filesystem when deleting - containers manually. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.delete_container_files_flag: - self.app_obj.set_delete_container_files_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.delete_container_files_flag: - self.app_obj.set_delete_container_files_flag(False) - - - def on_remove_duplicate_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables removeing duplicate URLs from the Classic Mode tab, - after the 'Add URLs' button is clicked. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.classic_duplicate_remove_flag: - self.app_obj.set_classic_duplicate_remove_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.classic_duplicate_remove_flag: - self.app_obj.set_classic_duplicate_remove_flag(False) - - - def on_remove_stamps_button_clicked(self, button): - - """Called from callback in self.setup_files_update_tab(). - - Clears timestamps from every video in the database. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Update' - ) - - video_count = 0 - success_count = 0 - - for media_data_obj in self.app_obj.media_reg_dict.values(): - - if isinstance(media_data_obj, media.Video): - - video_count += 1 - if media_data_obj.stamp_list: - - success_count += 1 - media_data_obj.reset_timestamps() - - - # Redraw the Video Catalogue, at its current page - main_win_obj = self.app_obj.main_win_obj - main_win_obj.video_catalogue_redraw_all( - main_win_obj.video_index_current_dbid, - main_win_obj.catalogue_toolbar_current_page, - ) - - # Confirm the result - msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \ - + _('Videos updated:') + ' ' + str(success_count) - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_remove_video_file_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_delete_tab(). - - Enables/disables removing all files from the filesystem when deleting - videos manually. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.delete_video_files_flag: - self.app_obj.set_delete_video_files_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.delete_video_files_flag: - self.app_obj.set_delete_video_files_flag(False) - - - def on_replace_stamps_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables replacing timestamps. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.video_timestamps_replace_flag: - self.app_obj.set_video_timestamps_replace_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.video_timestamps_replace_flag: - self.app_obj.set_video_timestamps_replace_flag(False) - - - def on_reset_archive_button_clicked(self, button, entry): - - """Called from callback in self.setup_operations_archive_tab(). - - Resets the path to the youtube-dl archive file. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - self.app_obj.set_allow_ytdl_archive_path(None) - entry.set_text('') - - - def on_reset_avconv_button_clicked(self, button, entry): - - """Called from callback in self.setup_ytdl_avconv_tab(). - - Resets the path to the avconv binary. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - self.app_obj.set_avconv_path(None) - entry.set_text('') - - - def on_reset_ffmpeg_button_clicked(self, button, entry): - - """Called from callback in self.setup_downloader_ffmpeg_tab(). - - Resets the path to the FFmpeg binary. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - self.app_obj.set_ffmpeg_path(None) - entry.set_text('') - - - def on_reset_invidious_clicked(self, button, entry): - - """Called from callback in self.setup_operations_mirrors_tab(). - - Resets the Invidious mirror - - Args: - - entry (Gtk.Entry): The widget changed - - entry (Gtk.Entry): Another widget to update - - """ - - self.app_obj.reset_custom_invidious_mirror() - entry.set_text(self.app_obj.custom_invidious_mirror) - - - def on_reset_sblock_clicked(self, button, entry): - - """Called from callback in self.setup_operations_mirrors_tab(). - - Resets the URL of the SponsorBlock API. - - Args: - - button (Gtk.Button): The widget that was clicked - - entry (Gtk.Entry): Another widget to update - - """ - - self.app_obj.reset_custom_sblock_mirror() - entry.set_text(self.app_obj.custom_sblock_mirror) - - - def on_reset_size_clicked(self, button): - - """Called from a callback in self.setup_windows_main_window_tab(). - - Resets the main window to its default size, and repositions sliders to - their default positions. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.app_obj.main_win_obj.resize_self( - self.app_obj.main_win_width, - self.app_obj.main_win_height, - ) - - self.app_obj.main_win_obj.reset_sliders() - - # Because of Gtk issues, the slider's position must be reset twice, the - # second time by mainapp.TartubeApp.script_fast_timer_callback() - self.app_obj.set_main_win_slider_reset_flag(True) - - - def on_reset_streamlink_button_clicked(self, button, entry): - - """Called from callback in self.setup_downloader_streamlink_tab(). - - Resets the path to the streamlink binary. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - self.app_obj.set_streamlink_path(None) - entry.set_text('') - - - def on_restore_from_tray_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_system_tray_tab(). - - Enables/disables restoring the window's position after closing it to - the system tray. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.restore_posn_from_tray_flag: - self.app_obj.set_restore_posn_from_tray_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.restore_posn_from_tray_flag: - self.app_obj.set_restore_posn_from_tray_flag(False) - - - def on_reverse_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables display of videos in the Results List in the reverse - order. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.reverse_results_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.reverse_results_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.reverse_results_checkbutton.set_active(False) - - - def on_save_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables automatic saving of files at the end of a download/ - update/refresh/info/tidy operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() and not self.app_obj.operation_save_flag: - self.app_obj.set_operation_save_flag(True) - elif not checkbutton.get_active() and self.app_obj.operation_save_flag: - self.app_obj.set_operation_save_flag(False) - - - def on_sblock_fetch_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_slices_tab(). - - Enables/disables contacting the SponsorBlock server. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.sblock_fetch_flag: - self.app_obj.set_sblock_fetch_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.sblock_fetch_flag: - self.app_obj.set_sblock_fetch_flag(False) - - - def on_sblock_obfuscate_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_slices_tab(). - - Enables/disables obfuscating video IDs in requests to the SponsorBlock - server. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.sblock_obfuscate_flag: - self.app_obj.set_sblock_obfuscate_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.sblock_obfuscate_flag: - self.app_obj.set_sblock_obfuscate_flag(False) - - - def on_sblock_replace_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_slices_tab(). - - Enables/disables replacing any previous set of video slices. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.sblock_replace_flag: - self.app_obj.set_sblock_replace_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.sblock_replace_flag: - self.app_obj.set_sblock_replace_flag(False) - - - def on_sblock_re_extract_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_slices_tab(). - - Enables/disables re-extracting SponsorBlock before removing slices - from a video. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.sblock_re_extract_flag: - self.app_obj.set_sblock_re_extract_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.sblock_re_extract_flag: - self.app_obj.set_sblock_re_extract_flag(False) - - - def on_slice_cleanup_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_slices_tab(). - - Enables/disables clearing timestamp/slice data from a video, after it - has had its video slices removed. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.slice_video_cleanup_flag: - self.app_obj.set_slice_video_cleanup_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.slice_video_cleanup_flag: - self.app_obj.set_slice_video_cleanup_flag(False) - - - def on_sblock_mirror_changed(self, entry): - - """Called from callback in self.setup_operations_mirrors_tab(). - - Sets the SponsorBlock API mirror to use. - - Args: - - entry (Gtk.Entry): The widget changed - - """ - - self.app_obj.set_custom_sblock_mirror(entry.get_text()) - - - def on_scheduled_add_button_clicked(self, button, entry): - - """Called from callback in self.setup_scheduling_start_tab(). - - Adds a new media.Scheduled object, adds it to the treeview, and opens - an edit window for it. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): An entry containing the new object's name - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Scheduling > Start' - ) - - # Check the specified name is valid - name = entry.get_text() - if name == '': - return - - for this_obj in self.app_obj.scheduled_list: - if this_obj.name == name: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('There is already a scheduled download with that name'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Create a new scheduled download object - new_obj = media.Scheduled(name, 'real', 'repeat') - self.app_obj.add_scheduled_list(new_obj) - - # Add it to the treeview - self.setup_scheduling_start_tab_add_row(new_obj) - # Open an edit window for it - ScheduledEditWin(self.app_obj, new_obj) - # Reset the entry - entry.set_text('') - - - def on_scheduled_delete_button_clicked(self, button, treeview): - - """Called from callback in self.setup_scheduling_start_tab(). - - Prompts the user, and then deletes the selected media.Scheduled object. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview with a selected line - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Scheduling > Start' - ) - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for path in path_list: - - this_iter = model.get_iter(path) - name = model[this_iter][0] - - for scheduled_obj in self.app_obj.scheduled_list: - if scheduled_obj.name == name: - - # Prompt the user - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Are you sure you want to delete this scheduled' \ - + ' download?', - ), - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'del_scheduled_list', - 'data': [scheduled_obj, self], - }, - ) - - - def on_scheduled_edit_button_clicked(self, button, treeview): - - """Called from callback in self.setup_scheduling_start_tab(). - - Opens an edit window for the selected media.Scheduled object. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview with a selected line - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for path in path_list: - - this_iter = model.get_iter(path) - name = model[this_iter][0] - - for scheduled_obj in self.app_obj.scheduled_list: - if scheduled_obj.name == name: - ScheduledEditWin(self.app_obj, scheduled_obj) - break - - - def on_scheduled_move_down_button_clicked(self, button, treeview): - - """Called from callback in self.setup_scheduling_start_tab(). - - Moves the selected media.Scheduled object down one position in the - list. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview with a selected line - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for path in path_list: - - this_iter = model.get_iter(path) - if model.iter_next(this_iter): - - name = model[this_iter][0] - self.app_obj.move_scheduled_list(name, True) - - model.move_after( - this_iter, - model.iter_next(this_iter), - ) - - - def on_scheduled_move_up_button_clicked(self, button, treeview): - - """Called from callback in self.setup_scheduling_start_tab(). - - Moves the selected media.Scheduled object up one position in the list. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview with a selected line - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for path in path_list: - - this_iter = model.get_iter(path) - if this_iter is not None: - - name = model[this_iter][0] - self.app_obj.move_scheduled_list(name, False) - - model.move_before( - this_iter, - model.iter_previous(this_iter), - ) - - - def on_scheduled_livestreams_button_toggled(self, checkbutton, - checkbutton2, spinbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Enables starting the livestream task periodically to check videos - marked as livestreams. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to sensitise/ - desensitise, according to the new value of the flag - - spinbutton (Gtk.SpinButton): Another widget to sensitise/ - desensitise, according to the new value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.scheduled_livestream_flag: - self.app_obj.set_scheduled_livestream_flag(True) - spinbutton.set_sensitive(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.scheduled_livestream_flag: - self.app_obj.set_scheduled_livestream_flag(False) - spinbutton.set_sensitive(False) - checkbutton2.set_sensitive(False) - checkbutton2.set_active(False) - - - def on_scheduled_livestreams_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Sets the time (in minutes) between scheduled livestream operations. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_scheduled_livestream_wait_mins( - spinbutton.get_value(), - ) - - - def on_separator_combo_changed(self, combo): - - """Called from a callback in self.setup_files_backups_tab(). - - Sets the CSV export separator. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_export_csv_separator(model[tree_iter][0]) - - - def on_set_archive_button_clicked(self, button, entry): - - """Called from callback in self.setup_operations_archive_tab(). - - Opens a window in which the user can select the path to the directory - in which the youtube-dl archive file should be stored. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Operations > Archive' - ) - - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Select the location of the archive file'), - self, - 'folder', - ) - - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and new_path: - - self.app_obj.set_allow_ytdl_archive_path(new_path) - entry.set_text(self.app_obj.allow_ytdl_archive_path) - - - def on_set_avconv_button_clicked(self, button, entry): - - """Called from callback in self.setup_downloader_ffmpeg_tab(). - - Opens a window in which the user can select the avconv binary, if it is - installed (and if the user wants it). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Downloaders > FFmpeg / AVConv' - ) - - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Please select the AVConv executable'), - self, - 'open', - ) - - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and new_path: - - self.app_obj.set_avconv_path(new_path) - entry.set_text(self.app_obj.avconv_path) - - - def on_set_ffmpeg_button_clicked(self, button, entry): - - """Called from callback in self.setup_downloader_ffmpeg_tab(). - - Opens a window in which the user can select the FFmpeg binary, if it is - installed (and if the user wants it). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Downloaders > FFmpeg / AVConv' - ) - - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Please select the FFmpeg executable'), - self, - 'open', - ) - - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and new_path: - - self.app_obj.set_ffmpeg_path(new_path) - entry.set_text(self.app_obj.ffmpeg_path) - - - def on_set_streamlink_button_clicked(self, button, entry): - - """Called from callback in self.setup_downloader_streamlink_tab(). - - Opens a window in which the user can select the streamlink binary, if - it is installed (and if the user wants it). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Downloaders > streamlink' - ) - - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Please select the streamlink executable'), - self, - 'open', - ) - - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and new_path: - - self.app_obj.set_streamlink_path(new_path) - entry.set_text(self.app_obj.streamlink_path) - - - def on_show_classic_mode_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_main_window_tab(). - - Enables/disables automatically opening the Classic Mode tab on startup. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_classic_tab_on_startup_flag: - self.app_obj.set_show_classic_tab_on_startup_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_classic_tab_on_startup_flag: - self.app_obj.set_show_classic_tab_on_startup_flag(False) - - - def on_show_custom_dl_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables showing the 'Custom download all' button in the Videos - tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_custom_dl_button_flag: - self.app_obj.set_show_custom_dl_button_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_custom_dl_button_flag: - self.app_obj.set_show_custom_dl_button_flag(False) - - # Abuse the main window code to either show or hide the button - self.app_obj.main_win_obj.hide_progress_bar(True) - - - def on_show_custom_icons_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables replacing stock icons with custom icons. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Windows > Main Window' - ) - - if checkbutton.get_active() \ - and not self.app_obj.show_custom_icons_flag: - self.app_obj.set_show_custom_icons_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_custom_icons_flag: - self.app_obj.set_show_custom_icons_flag(False) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The new setting will be applied when Tartube restarts'), - 'info', - 'ok', - self, # Parent window is this window - ) - - - def on_show_delete_container_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_delete_tab(). - - Enables/disables prompting the user before deleting containers. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_delete_container_dialogue_flag: - self.app_obj.set_show_delete_container_dialogue_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_delete_container_dialogue_flag: - self.app_obj.set_show_delete_container_dialogue_flag(False) - - - def on_show_delete_video_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_delete_tab(). - - Enables/disables prompting the user before deleting videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_delete_video_dialogue_flag: - self.app_obj.set_show_delete_video_dialogue_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_delete_video_dialogue_flag: - self.app_obj.set_show_delete_video_dialogue_flag(False) - - - def on_show_free_space_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables showing free disk space in the Videos tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_free_space_flag: - self.app_obj.set_show_free_space_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_free_space_flag: - self.app_obj.set_show_free_space_flag(False) - - - def on_show_selector_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables showing the selector button in each row of the Video - Index. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_marker_in_index_flag: - self.app_obj.set_show_marker_in_index_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_marker_in_index_flag: - self.app_obj.set_show_marker_in_index_flag(False) - - - def on_show_small_icons_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables smaller icons in the Video Index. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_small_icons_in_index_flag: - self.app_obj.set_show_small_icons_in_index_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_small_icons_in_index_flag: - self.app_obj.set_show_small_icons_in_index_flag(False) - - - def on_show_status_icon_toggled(self, checkbutton, checkbutton2, - checkbutton3, checkbutton4): - - """Called from a callback in self.setup_windows_system_tray_tab(). - - Shows/hides the status icon in the system tray. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2, checkbutton3, checkbutton4 (Gtk.CheckButton): Other - widgets to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_status_icon_flag: - self.app_obj.set_show_status_icon_flag(True) - checkbutton2.set_sensitive(True) - checkbutton3.set_sensitive(True) - if self.app_obj.close_to_tray_flag: - checkbutton4.set_sensitive(True) - else: - checkbutton4.set_sensitive(False) - - elif not checkbutton.get_active() \ - and self.app_obj.show_status_icon_flag: - self.app_obj.set_show_status_icon_flag(False) - checkbutton2.set_active(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_active(False) - checkbutton3.set_sensitive(False) - checkbutton4.set_active(False) - checkbutton4.set_sensitive(False) - - - def on_show_tooltips_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables tooltips for videos/channels/playlists/folders. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to be modified - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_tooltips_flag: - self.app_obj.set_show_tooltips_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.show_tooltips_flag: - self.app_obj.set_show_tooltips_flag(False) - checkbutton2.set_sensitive(False) - checkbutton2.set_active(False) - - - def on_show_tooltips_extra_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_videos_tab(). - - Enables/disables errors/warnings in tooltips. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_tooltips_extra_flag: - self.app_obj.set_show_tooltips_extra_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_tooltips_extra_flag: - self.app_obj.set_show_tooltips_extra_flag(False) - - - def on_simple_prefs_clicked(self, button): - - """Called by callback in self.setup_general_application_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if not self.app_obj.simple_prefs_flag: - self.app_obj.set_simple_prefs_flag(True) - else: - self.app_obj.set_simple_prefs_flag(False) - - self.reset_window() - - - def on_slice_keyframe_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_slices_tab(). - - Enables/disables forced keyframes at cuts, when removing slices from - videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.slice_video_force_keyframe_flag: - self.app_obj.set_slice_video_force_keyframe_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.slice_video_force_keyframe_flag: - self.app_obj.set_slice_video_force_keyframe_flag(False) - - - def on_sound_custom_changed(self, combo): - - """Called from callback in self.setup_operations_actions_tab(). - - Sets the user's preferred sound effect for livestream alarms. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_sound_custom(model[tree_iter][0]) - - - def on_split_keyframe_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables forced keyframes at cuts, when splitting videos into - clips. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.split_video_force_keyframe_flag: - self.app_obj.set_split_video_force_keyframe_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.split_video_force_keyframe_flag: - self.app_obj.set_split_video_force_keyframe_flag(False) - - - def on_split_mode_combo_changed(self, combo): - - """Called from a callback in self.setup_operations_clips_tab(). - - Sets the mode for naming split video files. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_split_video_name_mode(model[tree_iter][1]) - - - def on_split_subdir_flag_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_clips_tab(). - - Enables/disables moving split files into a sub-directory. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.split_video_subdir_flag: - self.app_obj.set_split_video_subdir_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.split_video_subdir_flag: - self.app_obj.set_split_video_subdir_flag(False) - - - def on_squeeze_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables labels in the main window's main toolbar. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.toolbar_squeeze_flag: - self.app_obj.set_toolbar_squeeze_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.toolbar_squeeze_flag: - self.app_obj.set_toolbar_squeeze_flag(False) - - - def on_store_playlist_id_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables storing video's playlist IDs in the parent channel/ - playlist. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.store_playlist_id_flag: - self.app_obj.set_store_playlist_id_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.store_playlist_id_flag: - self.app_obj.set_store_playlist_id_flag(False) - - - def on_system_container_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables showing container names in the Errors/Warnings tab. - Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab - sets the IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag \ - = main_win_obj.show_system_container_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_system_container_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_system_container_checkbutton.set_active(False) - - - def on_system_date_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables showing dates (as well as times) in the Errors/ - Warnings tab. Toggling the corresponding Gtk.CheckButton in the Errors/ - Warnings tab sets the IV (and makes sure the two checkbuttons have the - same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag \ - = main_win_obj.show_system_date_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_system_date_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_system_date_checkbutton.set_active(False) - - - def on_system_multi_line_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables showing full error/warning messages in the Errors/ - Warnings tab. Toggling the corresponding Gtk.CheckButton in the Errors/ - Warnings tab sets the IV (and makes sure the two checkbuttons have the - same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag \ - = main_win_obj.show_system_multi_line_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_system_multi_line_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_system_multi_line_checkbutton.set_active(False) - - - def on_system_video_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables showing video names in the Errors/Warnings tab. - Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab - sets the IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag \ - = main_win_obj.show_system_video_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_system_video_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_system_video_checkbutton.set_active(False) - - - def on_system_error_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables system errors in the 'Errors/Warnings' tab. Toggling - the corresponding Gtk.CheckButton in the Errors/Warnings tab sets the - IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.show_system_error_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_system_error_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_system_error_checkbutton.set_active(False) - - - def on_system_keep_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables keeping the total number of system messages in the tab - label until the clear button is explicitly clicked. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.system_msg_keep_totals_flag: - self.app_obj.set_system_msg_keep_totals_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.system_msg_keep_totals_flag: - self.app_obj.set_system_msg_keep_totals_flag(False) - - - def on_system_warning_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables system warnings in the 'Errors/Warnings' tab. Toggling - the corresponding Gtk.CheckButton in the Errors/Warnings tab sets the - IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.show_system_warning_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_system_warning_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_system_warning_checkbutton.set_active(False) - - - def on_terminal_json_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_terminal_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_ignore_json_flag: - self.app_obj.set_ytdl_write_ignore_json_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_ignore_json_flag: - self.app_obj.set_ytdl_write_ignore_json_flag(False) - - - def on_terminal_progress_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_terminal_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_ignore_progress_flag: - self.app_obj.set_ytdl_write_ignore_progress_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_ignore_progress_flag: - self.app_obj.set_ytdl_write_ignore_progress_flag(False) - - - def on_terminal_stderr_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_terminal_tab(). - - Enables/disables writing output from youtube-dl's STDERR to the - terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_stderr_flag: - self.app_obj.set_ytdl_write_stderr_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_stderr_flag: - self.app_obj.set_ytdl_write_stderr_flag(False) - - - def on_terminal_stdout_button_toggled(self, checkbutton, checkbutton2, \ - checkbutton3): - - """Called from a callback in self.setup_output_terminal_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2, checkbutton3 (Gtk.CheckButton): Additional - checkbuttons to sensitise/desensitise, according to the new - value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_stdout_flag: - self.app_obj.set_ytdl_write_stdout_flag(True) - checkbutton2.set_sensitive(True) - checkbutton3.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_stdout_flag: - self.app_obj.set_ytdl_write_stdout_flag(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_sensitive(False) - - - def on_terminal_system_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_terminal_tab(). - - Enables/disables writing youtube-dl system commands to the terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_system_cmd_flag: - self.app_obj.set_ytdl_write_system_cmd_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_system_cmd_flag: - self.app_obj.set_ytdl_write_system_cmd_flag(False) - - - def on_test_sound_clicked(self, button, combo): - - """Called from callback in self.setup_operations_actions_tab(). - - Plays the sound effect selected in the combobox. - - Args: - - button (Gtk.Button): The widget that was clicked - - combo (Gtk.ComboBox): The widget in which a sound effect is - selected - - """ - - self.app_obj.play_sound() - - - def on_thumb_404_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_ignore_tab(). - - Enables/disables ignoring of the 'Unable to download video thumbnail' - warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_thumb_404_flag: - self.app_obj.set_ignore_thumb_404_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_thumb_404_flag: - self.app_obj.set_ignore_thumb_404_flag(False) - - - def on_timeout_no_comments_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Sets the JSON timeout when not fetching comments. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_json_timeout_no_comments_time(spinbutton.get_value()) - - - def on_timeout_with_comments_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Sets the JSON timeout when fetching comments. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_json_timeout_with_comments_time( - spinbutton.get_value(), - ) - - - def on_update_combo_changed(self, combo): - - """Called from a callback in self.setup_downloader_paths_tab(). - - Extracts the value visible in the combobox, converts it into another - value, and uses that value to update the main application's IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_ytdl_update_current(model[tree_iter][0]) - - - def on_uploader_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_websites_tab(). - - Enables/disables ignoring of deletion by uploader error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_yt_uploader_deleted_flag: - self.app_obj.set_ignore_yt_uploader_deleted_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_yt_uploader_deleted_flag: - self.app_obj.set_ignore_yt_uploader_deleted_flag(False) - - - def on_url_regex_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_urls_tab(). - - Enables/disables treating the pattern as a regex, when searching/ - replacing URLs. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.url_change_regex_flag: - self.app_obj.set_url_change_regex_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.url_change_regex_flag: - self.app_obj.set_url_change_regex_flag(False) - - - def on_use_first_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_database_tab(). - - Enables/disables automatic loading of the first database file in the - list. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.data_dir_use_first_flag: - self.app_obj.set_data_dir_use_first_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.data_dir_use_first_flag: - self.app_obj.set_data_dir_use_first_flag(False) - - - def on_use_list_button_toggled(self, checkbutton): - - """Called from callback in self.setup_files_database_tab(). - - Enables/disables automatic loading of an alternative database file, if - the default one is locked. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.data_dir_use_list_flag: - self.app_obj.set_data_dir_use_list_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.data_dir_use_list_flag: - self.app_obj.set_data_dir_use_list_flag(False) - - - def on_video_res_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_limits_tab(). - - Enables/disables the video resolution limit. Toggling the corresponding - Gtk.CheckButton in the Progress tab sets the IV (and makes sure the two - checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - other_flag \ - = self.app_obj.main_win_obj.video_res_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - self.app_obj.main_win_obj.video_res_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - self.app_obj.main_win_obj.video_res_checkbutton.set_active(False) - - - def on_video_res_combo_changed(self, combo): - - """Called from a callback in self.setup_operations_limits_tab(). - - Extracts the value visible in the combobox, converts it into another - value, and uses that value to update the main application's IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.main_win_obj.set_video_res(model[tree_iter][0]) - - - def on_worker_button_toggled(self, checkbutton, alt_flag=False): - - """Called from callback in self.setup_operations_limits_tab(). - - Enables/disables the simultaneous download limit. Toggling the - corresponding Gtk.CheckButton in the Progress tab sets the IV (and - makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - alt_flag (bool): If True, the alternative limit is toggled - - """ - - main_win_obj = self.app_obj.main_win_obj - - if not alt_flag: - - other_flag = main_win_obj.num_worker_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.num_worker_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.num_worker_checkbutton.set_active(False) - - else: - - # Alternative limits. There is no second widget to toggle - if checkbutton.get_active() \ - and not self.app_obj.alt_num_worker_apply_flag: - self.app_obj.set_alt_num_worker_apply_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.alt_num_worker_apply_flag: - self.app_obj.set_alt_num_worker_apply_flag(False) - - - def on_worker_bypass_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_livestreams_tab(). - - Enables/disables bypassing the maximum simultaneous downloads limit - when downloading broadcasting livestreams. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.num_worker_bypass_flag: - self.app_obj.set_num_worker_bypass_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.num_worker_bypass_flag: - self.app_obj.set_num_worker_bypass_flag(False) - - - def on_worker_spinbutton_changed(self, spinbutton, alt_flag=False): - - """Called from callback in self.setup_operations_limits_tab(). - - Sets the simultaneous download limit. Setting the value of the - corresponding Gtk.SpinButton in the Progress tab sets the IV (and - makes sure the two spinbuttons have the same value). - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - alt_flag (bool): If True, the alternative limit is set - - """ - - if not alt_flag: - - self.app_obj.main_win_obj.num_worker_spinbutton.set_value( - spinbutton.get_value(), - ) - - else: - - # Alternative limits. There is no second widget to toggle - self.app_obj.set_alt_num_worker(int(spinbutton.get_value())) - - - def on_yt_remind_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_dialogues_tab(). - - Enables/disables reminding the user about the correct URL when adding - YouTube channels. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.dialogue_yt_remind_flag: - self.app_obj.set_dialogue_yt_remind_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.dialogue_yt_remind_flag: - self.app_obj.set_dialogue_yt_remind_flag(False) - - - def on_ytdl_fork_button_toggled(self, radiobutton, checkbutton, \ - fork_type=None): - - """Called from callback in self.setup_downloader_forks_tab(). - - Sets the youtube-dl fork to be used. See also - self.on_ytdl_fork_changed(). - - Args: - - radiobutton (Gtk.Radiobutton): The widget clicked - - checkbutton (Gtk.CheckButton): Another widget to be updated - - fork_type (str): 'yt-dlp', 'youtube-dl', or None for any other fork - - """ - - if radiobutton.get_active(): - - if fork_type is None: - - fork_name = self.forks_entry.get_text() - # (If the 'other fork' option is selected, but nothing is - # entered in the entry box, use youtube-dl as the downloader) - if fork_name == '': - self.app_obj.set_ytdl_fork(None) - else: - self.app_obj.set_ytdl_fork(fork_name) - - checkbutton.set_sensitive(False) - self.forks_entry.set_sensitive(True) - - elif fork_type == 'youtube-dl': - - fork_name = fork_type - self.app_obj.set_ytdl_fork(None) - checkbutton.set_sensitive(False) - self.forks_entry.set_text('') - self.forks_entry.set_sensitive(False) - - elif fork_type == 'yt-dlp': - - fork_name = fork_type - self.app_obj.set_ytdl_fork(fork_type) - checkbutton.set_sensitive(True) - self.forks_entry.set_text('') - self.forks_entry.set_sensitive(False) - - # If the user has set a custom path to the youtube-dl executable - # that does not match 'fork_type', then the path must be reset - if self.app_obj.ytdl_path_custom_flag: - - directory, fullname = os.path.split(self.app_obj.ytdl_path) - filename, extension = os.path.splitext(fullname) - if filename != fork_name: - self.filepaths_combo.set_active(0) - - - self.update_ytdl_combos() - - - def on_ytdl_fork_changed(self, entry): - - """Called from callback in self.setup_downloader_forks_tab(). - - Sets the youtube-dl fork to be used. See also - self.on_ytdl_fork_button_toggled(). - - Args: - - entry (Gtk.Entry): The widget changed - - """ - - if self.forks_radiobutton3.get_active(): - - text = utils.strip_whitespace(entry.get_text()) - if text == '': - - self.app_obj.set_ytdl_fork(None) - entry.set_icon_from_stock( - Gtk.EntryIconPosition.PRIMARY, - 'gtk-yes', - ) - - else: - - # Git 466 - prevent the user from adding an absolute path; - # the value we're expecting is omething like 'youtube-dlc' - if re.search(r'[^\w\-]', text): - - entry.set_icon_from_stock( - Gtk.EntryIconPosition.PRIMARY, - 'gtk-no', - ) - - else: - - self.app_obj.set_ytdl_fork(text) - entry.set_icon_from_stock( - Gtk.EntryIconPosition.PRIMARY, - 'gtk-yes', - ) - - self.update_ytdl_combos() - - - def on_ytdl_fork_frame_clicked(self, event_box, event_button, radiobutton): - - """Called from a callback in self.setup_downloader_forks_tab(). - - Enables/disables selecting a downloader by clicking anywhere in its - containing frame. - - Args: - - event_box (Gtk.EventBox): Ignored - - event_button (Gdk.EventButton): Ignored - - radiobutton (Gtk.RadioButton): The radiobutton inside the clicked - frame, which should be made active - - """ - - if not radiobutton.get_active(): - radiobutton.set_active(True) - - - def on_ytdl_path_button_clicked(self, button, entry): - - """Called from callback in self.setup_downloader_paths_tab(). - - Sets a custom path to the youtube-dl executable. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to update - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Downloaders > File paths' - ) - - # Prompt the user for the new youtube-dl executable - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - _('Select the youtube-dl-compatible executable'), - self, - 'open', - ) - - # (When the user first selects 'Use custom path', using the combobox, - # the default youtube-dl path continues to be used until they have - # specified a new path) - if self.app_obj.ytdl_path != self.app_obj.ytdl_path_default: - dialogue_win.set_current_folder(self.app_obj.ytdl_path) - - # Get the user's response - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - if response == Gtk.ResponseType.OK: - - # Update the name of the fork to match the path - directory, fullname = os.path.split(new_path) - filename, extension = os.path.splitext(fullname) - self.app_obj.set_ytdl_fork(filename) - - # Update widgets in the calling ('File paths') tab - entry.set_text(new_path) - # Update widgets in the 'Forks' tab - if filename == 'youtube-dl': - self.forks_radiobutton.set_active(True) - elif filename == 'yt-dlp': - self.forks_radiobutton2.set_active(True) - else: - self.forks_radiobutton3.set_active(True) - self.forks_entry.set_text(filename) - - # Update the path to the executable (this must be done last, - # otherwise widget updates will override each other) - self.app_obj.set_ytdl_path(new_path) - self.app_obj.ytdl_update_dict['ytdl_update_custom_path'] \ - = ['python3', self.app_obj.ytdl_path, '-U'] - - - def on_ytdl_path_combo_changed(self, combo, entry, button): - - """Called from a callback in self.setup_downloader_paths_tab(). - - Extracts the value visible in the combobox, converts it into another - value, and uses that value to update the main application's IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - entry (Gtk.Entry): Another entry to check - - button (Gtk.Button): Another widget to modify - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - ytdl_path = model[tree_iter][1] - - if ytdl_path is not None: - - self.app_obj.set_ytdl_path(ytdl_path) - self.app_obj.set_ytdl_path_custom_flag(False) - entry.set_text('') - button.set_sensitive(False) - - else: - - # Custom youtube-dl path, set by the entry/button - # Until the user has selected their own executable, use the default - # one - self.app_obj.set_ytdl_path(self.app_obj.ytdl_path_default) - self.app_obj.ytdl_update_dict['ytdl_update_custom_path'] \ - = ['python3', self.app_obj.ytdl_path, '-U'] - self.app_obj.set_ytdl_path_custom_flag(True) - - entry.set_text(self.app_obj.ytdl_path) - button.set_sensitive(True) - - - def on_ytdl_verbose_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_general_tab(). - - Enables/disables writing verbose output (youtube-dl debugging mode). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_verbose_flag: - self.app_obj.set_ytdl_write_verbose_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_verbose_flag: - self.app_obj.set_ytdl_write_verbose_flag(False) - - - def on_ytdlp_install_button_toggled(self, checkbutton): - - """Called from callback in self.setup_downloader_forks_tab(). - - Sets the flag to install yt-dlp with or without dependencies. - - Args: - - checkbutton (Gtk.Checkbutton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_fork_no_dependency_flag: - self.app_obj.set_ytdl_fork_no_dependency_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_fork_no_dependency_flag: - self.app_obj.set_ytdl_fork_no_dependency_flag(False) - - - # (Callback support functions) - - - def try_switch_db(self, data_dir, button): - - """Called by self.on_data_dir_change_button_clicked() and - .on_data_dir_switch_button_clicked(). - - Having confirmed that a database directory specified by the user - actually exists, attempt to load the database file inside it. - - Args: - - data_dir (str): The full path to the data directory - - button (Gtk.Button): A button to be possibly desensitised - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \ - + ' System preferences > Files > Database' - ) - - dialogue_manager_obj = self.app_obj.dialogue_manager_obj - - # Database file already exists, so try to load it now - if not self.app_obj.switch_db([data_dir, self]): - - # Load failed, and the user chose to shut down Tartube - if self.app_obj.disable_load_save_lock_flag: - - return self.app_obj.stop() - - # Load failed for any other reason - elif self.app_obj.disable_load_save_flag: - - button.set_sensitive(False) - - if not self.app_obj.disable_load_save_lock_flag: - - if self.app_obj.disable_load_save_msg is not None: - - dialogue_win = dialogue_manager_obj.show_msg_dialogue( - self.app_obj.disable_load_save_msg, - 'error', - 'ok', - self, # Parent window is this window - ) - - else: - - dialogue_win = dialogue_manager_obj.show_msg_dialogue( - _('Database file not loaded'), - 'error', - 'ok', - self, # Parent window is this window - ) - - # When load/save is disabled, this preference window can't be - # opened - # Therefore, if load/save has just been disabled, close this - # window after the dialogue window closes - dialogue_win.set_modal(True) - dialogue_win.run() - dialogue_win.destroy() - if self.app_obj.disable_load_save_flag: - self.destroy() - - # Load not attempted - else: - - dialogue_win = dialogue_manager_obj.show_msg_dialogue( - _('Did not try to load the database file'), - 'error', - 'ok', - self, # Parent window is this window - ) - - else: - - # Load succeeded. Redraw the preference window, opening it at the - # same tab - self.reset_window() - self.select_switch_db_tab() - - if self.app_obj.disable_load_save_msg is not None: - - dialogue_manager_obj.show_msg_dialogue( - self.app_obj.disable_load_save_msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - else: - - dialogue_manager_obj.show_msg_dialogue( - _('Database file loaded'), - 'info', - 'ok', - self, # Parent window is this window - ) - - - def update_ytdl_combos(self): - - """Called initially by self.setup_downloader_paths_tab(), then by - self.on_ytdl_fork_changed(). - - Updates the contents of the two comboboxes in the tab, so that the - youtube-dl fork is visible, rather than yotube-dl itself (if - applicable). - - Also updates labels in the Operations > Livestreams tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: System preferences > Downloaders > Forks' - ) - - fork = standard = 'youtube-dl' - if self.app_obj.ytdl_fork is not None: - fork = self.app_obj.ytdl_fork - - ytdl_path_default = re.sub( - standard, - fork, - self.app_obj.ytdl_path_default, - ) - - # First combo: Path to the youtube-dl executable - self.path_liststore.set( - self.path_liststore.get_iter(Gtk.TreePath(0)), - 0, - _('Use default path') + ' (' + ytdl_path_default + ')', - ) - - ytdl_bin = re.sub( - standard, - fork, - self.app_obj.ytdl_bin, - ) - - if os.name != 'nt': - - self.path_liststore.set( - self.path_liststore.get_iter(Gtk.TreePath(1)), - 0, - _('Use local path') + ' (' + ytdl_bin + ')', - ) - - ytdl_path_pypi = re.sub( - standard, - fork, - self.app_obj.ytdl_path_pypi, - ) - - self.path_liststore.set( - self.path_liststore.get_iter(Gtk.TreePath(3)), - 0, - _('Use PyPI path') + ' (' + ytdl_path_pypi + ')', - ) - - # Second combo: Command for update operations - count = -1 - for item in self.app_obj.ytdl_update_list: - - count += 1 - descrip = re.sub(standard, fork, formats.YTDL_UPDATE_DICT[item]) - self.cmd_liststore.set( - self.cmd_liststore.get_iter(Gtk.TreePath(count)), - 1, - descrip, - ) - - # Update labels in the Operations > Livestreams tab - self.setup_operations_livestreams_tab_update() - diff --git a/build/lib/tartube/dialogue.py b/build/lib/tartube/dialogue.py deleted file mode 100644 index 61dc2688..00000000 --- a/build/lib/tartube/dialogue.py +++ /dev/null @@ -1,522 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Dialogue manager classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, GdkPixbuf - - -# Import other modules -import os -import re -import threading - - -# Import our modules -import utils - - -# Classes - - -class DialogueManager(threading.Thread): - - """Called by mainapp.TartubeApp.start(). - - Python class to manage message dialogue windows safely (i.e. without - causing a Gtk crash). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - main_win_obj (mainwin.MainWin): The main window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, main_win_obj): - - super(DialogueManager, self).__init__() - - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The main window - self.main_win_obj = main_win_obj - - - # Public class methods - - - def show_msg_dialogue(self, msg, msg_type, button_type, - parent_win_obj=None, response_dict=None): - - """Can be called by anything. - - Creates a standard Gtk.MessageDialog window. - - Args: - - msg (str): The text to display in the dialogue window - - msg_type (str): The icon to display in the dialogue window: 'info', - 'warning', 'question', 'error' - - button_type (str): The buttons to use in the dialogue window: 'ok', - 'ok-cancel', 'yes-no' - - parent_win_obj (mainwin.MainWin, config.GenericConfigWin or None): - The parent window for the dialogue window. If None, the main - window is used as the parent window - - response_dict (dict or None): A dictionary specified if the calling - code needs a response (e.g., needs to know whether the user - clicked the 'yes' or 'no' button). If specified, the keys are - 0, 1 or more of the values 'ok', 'cancel', 'yes', 'no'. The - corresponding values are the mainapp.TartubeApp function called - if the user clicks that button. (f the value begins with - 'main_win_', then the rest of the value is the mainwin.MainWin - function called). The dictionary can also contain the key - 'data'. If it does, the corresponding value is passed to the - mainapp.TartubeApp function as an argument - - Return values: - - Gtk.MessageDialog window - - """ - - if self.app_obj.dialogue_disable_msg_flag: - print(msg) - return - - if parent_win_obj is None: - parent_win_obj = self.main_win_obj - - # Rationalise the message. First, split the string into a list of - # lines, preserving \n\n (but not a standalone \n) - line_list = msg.split('\n\n') - # In each line, convert any standalone \n characters to whitespace. - # Then add new newline characters, if required, to give a maximum - # length per line - mod_list = [] - for line in line_list: - mod_list.append(utils.tidy_up_long_string(line, 40)) - - # Finally combine everything into a single string, as before - double = '\n\n' - msg = double.join(mod_list) - - # ...and display the message dialogue - dialogue_win = MessageDialogue( - self, - msg, - msg_type, - button_type, - parent_win_obj, - response_dict, - ) - - dialogue_win.create_dialogue() - - return dialogue_win - - - def show_simple_msg_dialogue(self, msg, msg_type, button_type, - parent_win_obj=None, response_dict=None): - - """Can be called by anything. - - A modified version of self.show_msg_dialogue(). Behaves in the same - way, but does not format msg, as the original function does. - - Args: - - msg (str): The text to display in the dialogue window - - msg_type (str): The icon to display in the dialogue window: 'info', - 'warning', 'question', 'error' - - button_type (str): The buttons to use in the dialogue window: 'ok', - 'ok-cancel', 'yes-no' - - parent_win_obj (mainwin.MainWin, config.GenericConfigWin or None): - The parent window for the dialogue window. If None, the main - window is used as the parent window - - response_dict (dict or None): A dictionary specified if the calling - code needs a response (e.g., needs to know whether the user - clicked the 'yes' or 'no' button). If specified, the keys are - 0, 1 or more of the values 'ok', 'cancel', 'yes', 'no'. The - corresponding values are the mainapp.TartubeApp function called - if the user clicks that button. (f the value begins with - 'main_win_', then the rest of the value is the mainwin.MainWin - function called). The dictionary can also contain the key - 'data'. If it does, the corresponding value is passed to the - mainapp.TartubeApp function as an argument - - Return values: - - Gtk.MessageDialog window - - """ - - if self.app_obj.dialogue_disable_msg_flag: - print(msg) - return - - if parent_win_obj is None: - parent_win_obj = self.main_win_obj - - # ...Display the message dialogue - dialogue_win = MessageDialogue( - self, - msg, - msg_type, - button_type, - parent_win_obj, - response_dict, - ) - - dialogue_win.create_dialogue() - - return dialogue_win - - - def show_file_chooser(self, msg, parent_win_obj, action=None, - file_path=None): - - """Can be called by anything. - - Creates a standard Gtk.FileChooserAction window. - - Gtk (who knows why) likes to create file chooser dialogues bigger than - the size of the observable universe. If it's too big, resize it - automatically. - - Args: - - msg (str): The text to display in the dialogue window - - parent_win_obj (mainwin.MainWin, config.GenericConfigWin or None): - The parent window for the dialogue window. If None, the main - window is used as the parent window - - action (str or None): The type of fille chooser to create: 'open' - to set a file for opening, 'save' to save a file, or 'folder' - to select a folder - - file_path (str or None): The file path to suggest to the user. If - not specified, then the file chooser is opened in Tartube's - data directory - - Return values: - - Gtk.MessageDialog window - - """ - - if parent_win_obj is None: - parent_win_obj = self.main_win_obj - - if action is None: - action = Gtk.FileChooserAction.SAVE - elif action == 'open': - action = Gtk.FileChooserAction.OPEN - elif action == 'folder': - action = Gtk.FileChooserAction.SELECT_FOLDER - else: - action = Gtk.FileChooserAction.SAVE - - # Create the file chooser dialogue - dialogue_win = FileChooserDialogue( - msg, - parent_win_obj, - action, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.OK, - ), - ) - - if file_path is not None: - - dialogue_win.set_current_name(file_path) - - elif self.app_obj.data_dir: - - # Yes, Gtk discourages this, but they aren't the ones who have to - # click through a million folders to find a suitable one - # The calling code can override this call with its own one, if - # necessary - dialogue_win.set_current_folder(self.app_obj.data_dir) - - # Save the universe - dialogue_win.set_default_size( - self.app_obj.config_win_width, - self.app_obj.config_win_height, - ) - - dialogue_win.connect( - 'size-allocate', - dialogue_win.on_size_allocate, - self.app_obj, - ) - - return dialogue_win - - -class MessageDialogue(Gtk.MessageDialog): - - """Called by dialogue.DialogueManager.show_msg_dialogue(). - - Creates a standard Gtk.MessageDialog window, and optionally returns a - response. - - Args: - - manager_obj (dialogue.DialogueManager): The parent dialogue manager - - msg (str): The text to display in the dialogue window - - msg_type (str): The icon to display in the dialogue window: 'info', - 'warning', 'question', 'error' - - button_type (str): The buttons to use in the dialogue window: 'ok', - 'ok-cancel', 'yes-no' - - parent_win_obj (mainwin.MainWin, config.GenericConfigWin): The parent - window for the dialogue window - - response_dict (dict or None): A dictionary specified if the calling - code needs a response (e.g., needs to know whether the user clicked - the 'yes' or 'no' button). If specified, the keys are 0, 1 or more - of the values 'ok', 'cancel', 'yes', 'no'. The corresponding values - are the mainapp.TartubeApp function called if the user clicks that - button. (f the value begins with 'main_win_', then the rest of the - value is the mainwin.MainWin function called). The dictionary can - also contain the key 'data'. If it does, the corresponding value is - passed to the mainapp.TartubeApp function as an argument - - """ - - - # Standard class methods - - - def __init__(self, manager_obj, msg, msg_type, button_type, parent_win_obj, - response_dict): - - # Prepare arguments - if msg_type == 'warning': - gtk_msg_type = Gtk.MessageType.WARNING - elif msg_type == 'question': - gtk_msg_type = Gtk.MessageType.QUESTION - elif msg_type == 'error': - gtk_msg_type = Gtk.MessageType.ERROR - else: - gtk_msg_type = Gtk.MessageType.INFO - - if button_type == 'ok-cancel': - gtk_button_type = Gtk.ButtonsType.OK_CANCEL - default_response = Gtk.ResponseType.OK - elif button_type == 'yes-no': - gtk_button_type = Gtk.ButtonsType.YES_NO - default_response = Gtk.ResponseType.YES - else: - gtk_button_type = Gtk.ButtonsType.OK - default_response = Gtk.ResponseType.OK - - # Set up the dialogue window - Gtk.MessageDialog.__init__( - self, - parent_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - gtk_msg_type, - gtk_button_type, - msg, - ) - - spacing_size = manager_obj.app_obj.default_spacing_size - - # Set up responses - self.set_default_response(default_response) - self.connect( - 'response', - self.on_clicked, - manager_obj.app_obj, - response_dict, - ) - - - # Public class methods - - - def create_dialogue(self): - - """Called by dialogue.DialogueManager.show_msg_dialogue(). - - Creating the message dialogue window using a Glib timeout keeps this - code thread-safe. - """ - - GObject.timeout_add(0, self.show_dialogue) - - - def show_dialogue(self): - - """Called by the timer created in self.create_dialogue(). - - Creating the message dialogue window using a Glib timeout keeps this - code thread-safe. - """ - - self.show_all() - return False - - - # (Callbacks) - - - def on_clicked(self, widget, response, app_obj, response_dict): - - """Called from a callback in self.__init__(). - - Destroy the dialogue window. If the calling code requires a response, - call the specified function in mainapp.TartubeApp. - - Args: - - widget (Gtk.MessageDialog): This dialogue window - - response (int): The response, matching a Gtk.ResponseType - - app_obj: The mainapp.TartubeApp object - - response_dict (dict or None): A dictionary specified if the calling - code needs a response (e.g., needs to know whether the user - clicked the 'yes' or 'no' button). If specified, the keys are - 0, 1 or more of the values 'ok', 'cancel', 'yes', 'no'. The - corresponding values are the mainapp.TartubeApp function called - if the user clicks that button. (f the value begins with - 'main_win_', then the rest of the value is the mainwin.MainWin - function called). The dictionary can also contain the key - 'data'. If it does, the corresponding value is passed to the - mainapp.TartubeApp function as an argument - - """ - - # Destroy the window - self.destroy() - - # If the calling code requires a response, provide it - if response_dict is not None: - - func = None - if response == Gtk.ResponseType.OK and 'ok' in response_dict: - func = response_dict['ok'] - elif response == Gtk.ResponseType.CANCEL \ - and 'cancel' in response_dict: - func = response_dict['cancel'] - elif response == Gtk.ResponseType.YES and 'yes' in response_dict: - func = response_dict['yes'] - elif response == Gtk.ResponseType.NO and 'no' in response_dict: - func = response_dict['no'] - - if func is not None: - - # Is it a mainapp.TartubeApp function or a mainwin.MainWin - # function? - if re.search('^main_win_', func): - - # We will call the specified mainwin.MainWin function - method = getattr(app_obj.main_win_obj, func[9::]) - - else: - - # We will call the specified mainapp.TartubeApp function - method = getattr(app_obj, func) - - # If the dictionary contains a key called 'data', use its - # corresponding value as an argument in the call - if 'data' in response_dict: - method(response_dict['data']) - else: - method() - - -class FileChooserDialogue(Gtk.FileChooserDialog): - - """Called by dialogue.DialogueManager.show_file_chooser(). - - Sub-class for the standard Gtk file chooser dialogue, with a callback to - reduce its size below the the size of the observable universe. - """ - - # Standard class methods - - - # Public class methods - - - # (Callbacks) - - - def on_size_allocate(self, widget, rect, app_obj): - - """Called from callback in DialogueManager.show_file_chooser(). - - If the window is bigger than the size of the observable universe, then - resize it. - - Args: - - widget (mainwin.MainWin): The widget the has been resized - - rect (Gdk.Rectangle): Object describing the window's new size - - app_obj (mainapp.TartubeApp): The main application object - - """ - - resize_flag = False - - width = rect.width - if width > app_obj.config_win_width: - width = app_obj.config_win_width - resize_flag = True - - height = rect.height - if height > app_obj.config_win_height: - height = app_obj.config_win_height - resize_flag = True - - if resize_flag: - widget.resize(width, height) - diff --git a/build/lib/tartube/downloads.py b/build/lib/tartube/downloads.py deleted file mode 100644 index d7f73318..00000000 --- a/build/lib/tartube/downloads.py +++ /dev/null @@ -1,11434 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Download and livestream operation classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import GObject - - -# Import other modules -import datetime -import json -import __main__ -import os -import queue -import random -import re -import requests -import shutil -import signal -import subprocess -import sys -import threading -import time - - -# Import our modules -import formats -import mainapp -import media -import options -import utils -# Use same gettext translations -from mainapp import _ - -if mainapp.HAVE_FEEDPARSER_FLAG: - import feedparser - - -# Decorator to add thread synchronisation to some functions in the -# downloads.DownloadList object -_SYNC_LOCK = threading.RLock() - -def synchronise(lock): - def _decorator(func): - def _wrapper(*args, **kwargs): - lock.acquire() - ret_value = func(*args, **kwargs) - lock.release() - return ret_value - return _wrapper - return _decorator - - -# Classes -class DownloadManager(threading.Thread): - - """Called by mainapp.TartubeApp.download_manager_continue(). - - Based on the DownloadManager class in youtube-dl-gui. - - Python class to manage a download operation. - - Creates one or more downloads.DownloadWorker objects, each of which handles - a single download. - - This object runs on a loop, looking for available workers and, when one is - found, assigning them something to download. The worker completes that - download and then waits for another assignment. - - Args: - - app_obj: The mainapp.TartubeApp object - - operation_type (str): 'sim' if channels/playlists should just be - checked for new videos, without downloading anything. 'real' if - videos should be downloaded (or not) depending on each media data - object's .dl_sim_flag IV - - 'custom_real' is like 'real', but with additional options applied - (specified by a downloads.CustomDLManager object). A 'custom_real' - operation is sometimes preceded by a 'custom_sim' operation (which - is the same as a 'sim' operation, except that it is always followed - by a 'custom_real' operation) - - For downloads launched from the Classic Mode tab, 'classic_real' - for an ordinary download, or 'classic_custom' for a custom - download. A 'classic_custom' operation is always preceded by a - 'classic_sim' operation (which is the same as a 'sim' operation, - except that it is always followed by a 'classic_custom' operation) - - download_list_obj (downloads.DownloadManager): An ordered list of - media data objects to download, each one represented by a - downloads.DownloadItem object - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that applies to this download operation. Only specified - when 'operation_type' is 'custom_sim', 'custom_real', 'classic_sim' - or 'classic_real' - - For 'custom_real' and 'classic_real', not specified if - mainapp.TartubeApp.temp_stamp_buffer_dict or - .temp_slice_buffer_dict are specified (because those values take - priority) - - """ - - - # Standard class methods - - - def __init__(self, app_obj, operation_type, download_list_obj, \ - custom_dl_obj): - - super(DownloadManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # Each instance of this object, which represents a single download - # operation, creates its own options.OptionsParser object. That - # object convert the download options stored in - # downloads.DownloadWorker.options_list into a list of youtube-dl - # command line options - self.options_parser_obj = None - # An ordered list of media data objects to download, each one - # represented by a downloads.DownloadItem object - self.download_list_obj = download_list_obj - # The custom download manager (downloads.CustomDLManager) that applies - # to this download operation. Only specified when 'operation_type' is - # 'custom_sim', 'custom_real', 'classic_sim' or 'classic_real' - # For 'custom_real' and 'classic_real', not specified if - # mainapp.TartubeApp.temp_stamp_buffer_dict or - # .temp_slice_buffer_dict are specified (because those values take - # priority) - self.custom_dl_obj = custom_dl_obj - # List of downloads.DownloadWorker objects, each one handling one of - # several simultaneous downloads - self.worker_list = [] - - - # IV list - other - # --------------- - # 'sim' if channels/playlists should just be checked for new videos, - # without downloading anything. 'real' if videos should be downloaded - # (or not) depending on each media data object's .dl_sim_flag IV - # 'custom_real' is like 'real', but with additional options applied - # (specified by a downloads.CustomDLManager object). A 'custom_real' - # operation is sometimes preceded by a 'custom_sim' operation (which - # is the same as a 'sim' operation, except that it is always followed - # by a 'custom_real' operation) - # For downloads launched from the Classic Mode tab, 'classic_real' for - # an ordinary download, or 'classic_custom' for a custom download. A - # 'classic_custom' operation is always preceded by a 'classic_sim' - # operation (which is the same as a 'sim' operation, except that it - # is always followed by a 'classic_custom' operation) - # This is the default value for the download operation, when it starts. - # If the user wants to add new download.DownloadItem objects during - # an operation, the code can call - # downloads.DownloadList.create_item() with a non-default value of - # operation_type - self.operation_type = operation_type - # Shortcut flag to test the operation type; True for 'classic_sim', - # 'classic_real' and 'classic_custom'; False for all other values - self.operation_classic_flag = False # (Set below) - - # The time at which the download operation began (in seconds since - # epoch) - self.start_time = int(time.time()) - # The time at which the download operation completed (in seconds since - # epoch) - self.stop_time = None - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # Flag set to False if self.stop_download_operation() is called - # The False value halts the main loop in self.run() - self.running_flag = True - # Flag set to True if the operation has been stopped manually by the - # user (via a call to self.stop_download_operation() or - # .stop_download_operation_soon() - self.manual_stop_flag = False - - # Number of download jobs started (number of downloads.DownloadItem - # objects which have been allocated to a worker) - self.job_count = 0 - # The current downloads.DownloadItem being handled by self.run() - # (stored in this IV so that anything can update the main window's - # progress bar, at any time, by calling self.nudge_progress_bar() ) - self.current_item_obj = None - - # On-going counts of how many videos have been downloaded (real and - # simulated, and including videos from which one or more clips have - # been extracted), how many clips have been extracted, how many video - # slices have been removed, and how much disc space has been consumed - # (in bytes), so that the operation can be auto-stopped, if required - self.total_video_count = 0 - self.total_dl_count = 0 - self.total_sim_count = 0 - self.total_clip_count = 0 - self.total_slice_count = 0 - self.total_size_count = 0 - # Special count for media.Video objects which have already been - # checked/downloaded, and are being checked again (directly, for - # example after right-clicking the video) - # If non-zero, prevents mainwin.NewbieDialogue from opening - self.other_video_count = 0 - - # If mainapp.TartubeApp.operation_convert_mode is set to any value - # other than 'disable', then a media.Video object whose URL - # represents a channel/playlist is converted into multiple new - # media.Video objects, one for each video actually downloaded - # The original media.Video object is added to this list, via a call to - # self.mark_video_as_doomed(). At the end of the whole download - # operation, any media.Video object in this list is destroyed - self.doomed_video_list = [] - - # When the self.operation_type is 'classic_sim', we just compile a list - # of all videos detected. (A single URL may produce multiple videos) - # A second download operation is due to be launched when this one - # finishes, with self.operation_type set to 'classic_custom'. During - # that operation, each of these video will be downloaded individually - # The list is in groups of two, in the form - # [ parent_obj, json_dict ] - # ...where 'parent_obj' is a 'dummy' media.Video object representing a - # video, channel or playlist, from which the metedata for a single - # video, 'json_dict', has been extracted - self.classic_extract_list = [] - - # Flag set to True when alternative performance limits currently apply, - # False when not. By checking the previous value (stored here) - # against the new one, we can see whether the period of alternative - # limits has started (or stopped) - self.alt_limits_flag = self.check_alt_limits() - # Alternative limits are checked every five minutes. The time (in - # minutes past the hour) at which the next check should be performed - self.alt_limits_check_time = None - - - # Code - # ---- - - # Set the flag - if operation_type == 'classic_sim' \ - or operation_type == 'classic_real' \ - or operation_type == 'classic_custom': - self.operation_classic_flag = True - - # Create an object for converting download options stored in - # downloads.DownloadWorker.options_list into a list of youtube-dl - # command line options - self.options_parser_obj = options.OptionsParser(self.app_obj) - - # Create a list of downloads.DownloadWorker objects, each one handling - # one of several simultaneous downloads - # Note that if a downloads.DownloadItem was created by a - # media.Scheduled object that specifies more (or fewer) workers, - # then self.change_worker_count() will be called - if self.alt_limits_flag: - worker_count = self.app_obj.alt_num_worker - elif self.app_obj.num_worker_apply_flag: - worker_count = self.app_obj.num_worker_default - else: - worker_count = self.app_obj.num_worker_max - - for i in range(1, worker_count + 1): - self.worker_list.append(DownloadWorker(self)) - - # Set the time at which the first check for alternative limits is - # performed - local = utils.get_local_time() - self.alt_limits_check_time \ - = (int(int(local.strftime('%M')) / 5) * 5) + 5 - if self.alt_limits_check_time > 55: - self.alt_limits_check_time = 0 - # (Also update the icon in the Progress tab) - self.app_obj.main_win_obj.toggle_alt_limits_image(self.alt_limits_flag) - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - On a continuous loop, passes downloads.DownloadItem objects to each - downloads.DownloadWorker object, as they become available, until the - download operation is complete. - """ - - manager_string = _('D/L Manager:') + ' ' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - manager_string + _('Starting download operation'), - ) - - # (Monitor changes to the number of workers, and number of available - # workers, so that we can display a running total in the Output tab's - # summary page) - local_worker_available_count = 0 - local_worker_total_count = 0 - - # Perform the download operation until there is nothing left to - # download, or until something has called - # self.stop_download_operation() - while self.running_flag: - - # Send a message to the Output tab's summary page, if required. - # The number of workers shown doesn't include those dedicated to - # broadcasting livestreams - available_count = 0 - total_count = 0 - for worker_obj in self.worker_list: - if not worker_obj.broadcast_flag: - total_count += 1 - if worker_obj.available_flag: - available_count += 1 - - if local_worker_available_count != available_count \ - or local_worker_total_count != total_count: - local_worker_available_count = available_count - local_worker_total_count = total_count - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - manager_string + _('Workers: available:') + ' ' \ - + str(available_count) + ', ' + _('total:') + ' ' \ - + str(total_count), - ) - - # Auto-stop the download operation, if required - if self.app_obj.autostop_time_flag: - - # Calculate the current time limit, in seconds - time_limit = self.app_obj.autostop_time_value \ - * formats.TIME_METRIC_DICT[self.app_obj.autostop_time_unit] - - if (time.time() - self.start_time) > time_limit: - break - - # Every five minutes, check whether the period of alternative - # performance limits has started (or stopped) - local = utils.get_local_time() - if int(local.strftime('%M')) >= self.alt_limits_check_time: - - self.alt_limits_check_time += 5 - if self.alt_limits_check_time > 55: - self.alt_limits_check_time = 0 - - new_flag = self.check_alt_limits() - if new_flag != self.alt_limits_flag: - - self.alt_limits_flag = new_flag - if not new_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - _( - 'Alternative performance limits no longer apply', - ), - ) - - else: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Alternative performance limits now apply'), - ) - - # Change the number of workers. Bandwidth changes are - # applied by OptionsParser.build_limit_rate() - if self.app_obj.num_worker_default \ - != self.app_obj.alt_num_worker: - - if not new_flag: - - self.change_worker_count( - self.app_obj.num_worker_default, - ) - - else: - - self.change_worker_count( - self.app_obj.alt_num_worker, - ) - - # (Also update the icon in the Progress tab) - self.app_obj.main_win_obj.toggle_alt_limits_image( - self.alt_limits_flag, - ) - - # Fetch information about the next media data object to be - # downloaded (and store it in an IV, so the main window's - # progress bar can be updated at any time, by any code) - self.current_item_obj = self.download_list_obj.fetch_next_item() - - # Exit this loop when there are no more downloads.DownloadItem - # objects whose .status is formats.MAIN_STAGE_QUEUED, and when - # all workers have finished their downloads - # Otherwise, wait for an available downloads.DownloadWorker, and - # then assign the next downloads.DownloadItem to it - if not self.current_item_obj: - if self.check_workers_all_finished(): - - # Send a message to the Output tab's summary page - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - manager_string + _('All threads finished'), - ) - - break - - else: - worker_obj = self.get_available_worker( - self.current_item_obj.media_data_obj, - ) - - # If the worker has been marked as doomed (because the number - # of simultaneous downloads allowed has decreased) then we - # can destroy it now - if worker_obj and worker_obj.doomed_flag: - - worker_obj.close() - self.remove_worker(worker_obj) - - # Otherwise, initialise the worker's IVs for the next job - elif worker_obj: - - # Send a message to the Output tab's summary page - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Thread #') + str(worker_obj.worker_id) \ - + ': ' + _('Downloading:') + ' \'' \ - + self.current_item_obj.media_data_obj.name + '\'', - ) - - # Initialise IVs - worker_obj.prepare_download(self.current_item_obj) - # Change the download stage for that downloads.DownloadItem - self.download_list_obj.change_item_stage( - self.current_item_obj.item_id, - formats.MAIN_STAGE_ACTIVE, - ) - # Update the main window's progress bar (but not for - # workers dedicated to broadcasting livestreams) - if not worker_obj.broadcast_flag: - self.job_count += 1 - - # Throughout the downloads.py code, instead of calling a - # mainapp.py or mainwin.py function directly (which is - # not thread-safe), set a Glib timeout to handle it - if not self.operation_classic_flag: - self.nudge_progress_bar() - - # If this downloads.DownloadItem was marked (while it was - # still in the queue) as being the last one that should - # be checked/downloaded, we can prevent any more items - # being fetched from the downloads.DownloadList - if self.download_list_obj.final_item_id is not None \ - and self.download_list_obj.final_item_id \ - == self.current_item_obj.item_id: - self.download_list_obj.prevent_fetch_new_items() - - # Pause a moment, before the next iteration of the loop (don't want - # to hog resources) - time.sleep(self.sleep_time) - - # Download operation complete (or has been stopped). Send messages to - # the Output tab's summary page - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - manager_string + _('Downloads complete (or stopped)'), - ) - - # Close all the workers - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - manager_string + _('Halting all workers'), - ) - - for worker_obj in self.worker_list: - worker_obj.close() - - # Join and collect - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - manager_string + _('Join and collect threads'), - ) - - for worker_obj in self.worker_list: - worker_obj.join() - - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - manager_string + _('Operation complete'), - ) - - # Set the stop time - self.stop_time = int(time.time()) - - # Tell the Progress List (or Classic Progress List) to display any - # remaining download statistics immediately - if not self.operation_classic_flag: - - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.progress_list_display_dl_stats, - ) - - else: - - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.classic_mode_tab_display_dl_stats, - ) - - # Any media.Video objects which have been marked as doomed, can now be - # destroyed - for video_obj in self.doomed_video_list: - self.app_obj.delete_video( - video_obj, - True, # Delete any files associated with the video - True, # Don't update the Video Index yet - True, # Don't update the Video Catalogue yet - ) - - # (Also update the icon in the Progress tab) - self.app_obj.main_win_obj.toggle_alt_limits_image(False) - - # When youtube-dl reports it is finished, there is a short delay before - # the final downloaded video(s) actually exist in the filesystem - # Therefore, mainwin.MainWin.progress_list_display_dl_stats() may not - # have marked the final video(s) as downloaded yet - # Let the timer run for a few more seconds to allow those videos to be - # marked as downloaded (we can stop before that, if all the videos - # have been already marked) - if not self.operation_classic_flag: - - GObject.timeout_add( - 0, - self.app_obj.download_manager_halt_timer, - ) - - else: - - # For download operations launched from the Classic Mode tab, we - # don't need to wait at all - GObject.timeout_add( - 0, - self.app_obj.download_manager_finished, - ) - - - def apply_ignore_limits(self): - - """Called by mainapp>TartubeApp.script_slow_timer_callback(), after - starting a download operation to check/download everything. - - One of the media.Scheduled objects specified that operation limits - should be ignored, so apply that setting to everything in the download - list. - - (Doing things this way is a lot simpler than the alternatives.) - """ - - for item_id in self.download_list_obj.download_item_list: - - download_item_obj \ - = self.download_list_obj.download_item_dict[item_id] - download_item_obj.set_ignore_limits_flag() - - - def check_alt_limits(self): - - """Called by self.__init__() and .run(). - - Checks whether alternative performance limits apply right now, or not. - - Return values: - - True if alternative limits apply, False if not - - """ - - if not self.app_obj.alt_num_worker_apply_flag: - return False - - # Get the current time and day of the week - local = utils.get_local_time() - current_hours = int(local.strftime('%H')) - current_minutes = int(local.strftime('%M')) - # 0=Monday, 6=Sunday - current_day = local.today().weekday() - target_day_str = self.app_obj.alt_day_string - - # The period of alternative performance limits have a start and stop - # time, stored as strings in the form '21:00' - start_hours = int(self.app_obj.alt_start_time[0:2]) - start_minutes = int(self.app_obj.alt_start_time[3:5]) - stop_hours = int(self.app_obj.alt_stop_time[0:2]) - stop_minutes = int(self.app_obj.alt_stop_time[3:5]) - - # Is the current time before or after the start/stop times? - if current_hours < start_hours \ - or (current_hours == start_hours and current_minutes < start_minutes): - start_before_flag = True - else: - start_before_flag = False - - if current_hours < stop_hours \ - or (current_hours == stop_hours and current_minutes < stop_minutes): - stop_before_flag = True - else: - stop_before_flag = False - - # If the start time is earlier than the stop time, we assume they're on - # the same day - if start_hours < stop_hours \ - or (start_hours == stop_hours and start_minutes < stop_minutes): - - if not utils.check_day(current_day, target_day_str) \ - or start_before_flag \ - or (not stop_before_flag): - return False - else: - return True - - # Otherwise, we assume the stop time occurs the following day (e.g. - # 21:00 to 07:00) - else: - - prev_day = current_day - 1 - if prev_day < 0: - prev_day = 6 - - if ( - self.utils.check_day(current_day, target_day_str) \ - and (not start_before_flag) - ) or ( - self.utils.check_day(prev_day, target_day_str) \ - and stop_before_flag - ): - return True - else: - return False - - - def change_worker_count(self, number): - - """Called by mainapp.TartubeApp.set_num_worker_default(). Can also be - called by self.run() when the period of alternative performances limits - begins or ends. - - When the number of simultaneous downloads allowed is changed during a - download operation, this function responds. - - If the number has increased, creates an extra download worker object. - - If the number has decreased, marks the worker as doomed. When its - current download is completed, the download manager destroys it. - - Args: - - number (int): The new number of simultaneous downloads allowed - - """ - - # How many workers do we have already? - current = len(self.worker_list) - # If this object hasn't set up its worker pool yet, let the setup code - # proceed as normal - # Sanity check: if the specified value is less than 1, or hasn't - # changed, take no action - if not current or number < 1 or current == number: - return - - # Usually, the number of workers goes up or down by one at a time, but - # we'll check for larger leaps anyway - for i in range(1, (abs(current-number) + 1)): - - if number > current: - - # The number has increased. If any workers have marked as - # doomed, they can be unmarked, allowing them to continue - match_flag = False - - for worker_obj in self.worker_list: - if worker_obj.doomed_flag: - worker_obj.set_doomed_flag(True) - match_flag = True - break - - if not match_flag: - # No workers were marked doomed, so create a brand new - # download worker - self.worker_list.append(DownloadWorker(self)) - - else: - - # The number has decreased. The first worker in the list is - # marked as doomed - that is, when it has finished its - # current job, it closes (rather than being given another - # job, as usual) - for worker_obj in self.worker_list: - if not worker_obj.doomed_flag: - worker_obj.set_doomed_flag(True) - break - - - def check_master_slave(self, media_data_obj): - - """Called by VideoDownloader.do_download(). - - When two channels/playlists/folders share a download destination, we - don't want to download both of them at the same time. - - This function is called when media_data_obj is about to be - downloaded. - - Every worker is checked, to see if it's downloading to the same - destination. If so, this function returns True, and - VideoDownloader.do_download() waits a few seconds, before trying - again. - - Otherwise, this function returns False, and - VideoDownloader.do_download() is free to start its download. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): - The media data object that the calling function wants to - download - - Return values: - - True or False, as described above - - """ - - for worker_obj in self.worker_list: - - if not worker_obj.available_flag \ - and worker_obj.download_item_obj: - - other_obj = worker_obj.download_item_obj.media_data_obj - - if other_obj.dbid != media_data_obj.dbid: - - if ( - not isinstance(other_obj, media.Video) - and other_obj.external_dir is not None - ): - if other_obj.external_dir \ - == media_data_obj.external_dir: - return True - - # (Alternative download destinations only apply when no - # external directory is specified) - elif other_obj.dbid == media_data_obj.master_dbid: - return True - - return False - - - def check_workers_all_finished(self): - - """Called by self.run(). - - Based on DownloadManager._jobs_done(). - - Return values: - - True if all downloads.DownloadWorker objects have finished their - jobs, otherwise returns False - - """ - - for worker_obj in self.worker_list: - if not worker_obj.available_flag: - return False - - return True - - - def create_bypass_worker(self): - - """Called by downloads.DownloadList.create_item(). - - For a broadcasting livestream, we create additional workers if - required, possibly bypassing the limit specified by - mainapp.TartubeApp.num_worker_default. - """ - - # How many workers do we have already? - current = len(self.worker_list) - # If this object hasn't set up its worker pool yet, let the setup code - # proceed as normal - if not current: - return - - # If we don't already have the maximum number of workers (or if no - # limit currently applies), then we don't need to create any more - if not self.app_obj.num_worker_apply_flag \ - or current < self.app_obj.num_worker_default: - return - - # Check the existing workers, in case one is already available - for worker_obj in self.worker_list: - if worker_obj.available_flag: - return - - # Bypass the worker limit to create an additional worker, to be used - # only for broadcasting livestreams - self.worker_list.append(DownloadWorker(self, True)) - # Create an additional page in the main window's Output tab, if - # required - self.app_obj.main_win_obj.output_tab_setup_pages() - - - def get_available_worker(self, media_data_obj): - - """Called by self.run(). - - Based on DownloadManager._get_worker(). - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist or - media.Folder): The media data object which is the next to be - downloaded - - Return values: - - The first available downloads.DownloadWorker, or None if there are - no available workers - - """ - - # Some workers are only available when media_data_obj is media.Video - # that's a broadcasting livestream - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.live_mode == 2: - broadcast_flag = True - else: - broadcast_flag = False - - for worker_obj in self.worker_list: - - if worker_obj.available_flag \ - and (broadcast_flag or not worker_obj.broadcast_flag): - return worker_obj - - return None - - - def mark_video_as_doomed(self, video_obj): - - """Called by VideoDownloader.check_dl_is_correct_type(). - - When youtube-dl reports the URL associated with a download item - object contains multiple videos (or potentially contains multiple - videos), then the URL represents a channel or playlist, not a video. - - If the channel/playlist was about to be downloaded into a media.Video - object, then the calling function takes action to prevent it. - - It then calls this function to mark the old media.Video object to be - destroyed, once the download operation is complete. - - Args: - - video_obj (media.Video): The video object whose URL is not a video, - and which must be destroyed - - """ - - if isinstance(video_obj, media.Video) \ - and not video_obj in self.doomed_video_list: - self.doomed_video_list.append(video_obj) - - - def nudge_progress_bar(self): - - """Can be called by anything. - - Called by self.run() during the download operation. - - Also called by code in other files, just after that code adds a new - media data object to our download list. - - Updates the main window's progress bar. - """ - - if self.current_item_obj: - - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - self.current_item_obj.media_data_obj.name, - self.job_count, - len(self.download_list_obj.download_item_list), - ) - - - def register_classic_url(self, parent_obj, json_dict): - - """Called by VideoDownloader.extract_stdout_data(). - - When the self.operation_type is 'classic_sim', we just compile a list - of all videos detected. (A single URL may produce multiple videos). - - A second download operation is due to be launched when this one - finishes, with self.operation_type set to 'classic_custom'. During that - operation, each of these URLs will be downloaded individually. - - Args: - - parent_obj (media.Video, media.Channel, media.Playlist): The - media data object from which the URL was extracted - - json_dict (dict): Metadata extracted from a single video, - stored as a dictionary - - """ - - self.classic_extract_list.append(parent_obj) - self.classic_extract_list.append(json_dict) - - - def register_clip(self): - - """Called by ClipDownloader.confirm_video(). - - A shorter version of self.register_video(). Clips do not count - towards video limits, but we still keep track of them. - - When all of the clips for a video have been extracted, a further call - to self.register_video() must be made. - """ - - self.total_clip_count += 1 - - - def register_slice(self): - - """Called by ClipDownloader.do_download_remove_slices(). - - A shorter version of self.register_video(). Video slices removed from - videos do not count towards video limits, but we still keep track of - them. - - When all of the video sliceshave been removed, a further call to - self.register_video() must be made. - """ - - self.total_slice_count += 1 - - - def register_video(self, dl_type): - - """Called by VideoDownloader.confirm_new_video(), when a video is - downloaded, or by .confirm_sim_video(), when a simulated download finds - a new video. - - Can also be called by .confirm_old_video() when downloading from the - Classic Mode tab. - - Furthermore, called by ClipDownloader.do_download() when all clips for - a video have been extracted, at least one of them successfully. - - This function adds the new video to its ongoing total and, if a limit - has been reached, stops the download operation. - - Args: - - dl_type (str): 'new', 'sim', 'old', 'clip' or 'other', depending on - the calling function - - """ - - if dl_type == 'other': - # Special count for already checked/downloaded media.Videos, in - # order to prevent mainwin.NewbieDialogue opening - self.other_video_count += 1 - - else: - self.total_video_count += 1 - if dl_type == 'new': - self.total_dl_count += 1 - elif dl_type == 'sim': - self.total_sim_count += 1 - - if self.app_obj.autostop_videos_flag \ - and self.total_video_count >= self.app_obj.autostop_videos_value: - self.stop_download_operation() - - - def register_video_size(self, size=None): - - """Called by mainapp.TartubeApp.update_video_when_file_found(). - - Called with the size of a video that's just been downloaded. This - function adds the size to its ongoing total and, if a limit has been - reached, stops the download operation. - - Args: - - size (int): The size of the downloaded video (in bytes) - - """ - - # (In case the filesystem didn't detect the file size, for whatever - # reason, we'll check for a None value) - if size is not None: - - self.total_size_count += size - - if self.app_obj.autostop_size_flag: - - # Calculate the current limit - limit = self.app_obj.autostop_size_value \ - * formats.FILESIZE_METRIC_DICT[self.app_obj.autostop_size_unit] - - if self.total_size_count >= limit: - self.stop_download_operation() - - - def remove_worker(self, worker_obj): - - """Called by self.run(). - - When a worker marked as doomed has completed its download job, this - function is called to remove it from self.worker_list. - - Args: - - worker_obj (downloads.DownloadWorker): The worker object to remove - - """ - - new_list = [] - - for other_obj in self.worker_list: - if other_obj != worker_obj: - new_list.append(other_obj) - - self.worker_list = new_list - - - def stop_download_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .dl_timer_callback(), .on_button_stop_operation(). - - Also called by mainwin.StatusIcon.on_stop_menu_item(). - - Also called by self.register_video() and .register_video_size(). - - Based on DownloadManager.stop_downloads(). - - Stops the download operation. On the next iteration of self.run()'s - loop, the downloads.DownloadWorker objects are cleaned up. - """ - - self.running_flag = False - self.manual_stop_flag = True - - # In the Progress List, change the status of remaining items from - # 'Waiting' to 'Not started' - self.download_list_obj.abandon_remaining_items() - - - def stop_download_operation_soon(self): - - """Called by mainwin.MainWin.on_progress_list_stop_all_soon(), after - the user clicks the 'Stop after these videos' option in the Progress - List. - - Stops the download operation, but only after any videos which are - currently being downloaded have finished downloading. - """ - - self.manual_stop_flag = True - - self.download_list_obj.prevent_fetch_new_items() - for worker_obj in self.worker_list: - if worker_obj.running_flag \ - and worker_obj.downloader_obj is not None: - worker_obj.downloader_obj.stop_soon() - - # In the Progress List, change the status of remaining items from - # 'Waiting' to 'Not started' - self.download_list_obj.abandon_remaining_items() - - -class DownloadWorker(threading.Thread): - - """Called by downloads.DownloadManager.__init__(). - - Based on the Worker class in youtube-dl-gui. - - Python class for managing simultaneous downloads. The parent - downloads.DownloadManager object can create one or more workers, each of - which handles a single download. - - The download manager runs on a loop, looking for available workers and, - when one is found, assigns them something to download. - - After the download is completely, the worker optionally checks a channel's - or a playlist's RSS feed, looking for livestreams. - - When all tasks are completed, the worker waits for another assignment. - - Args: - - download_manager_obj (downloads.DownloadManager): The parent download - manager object - - broadcast_flag (bool): True if this worker has been created - specifically to handle broadcasting livestreams (see comments - below); False if not - - """ - - - # Standard class methods - - - def __init__(self, download_manager_obj, broadcast_flag=False): - - super(DownloadWorker, self).__init__() - - # IV list - class objects - # ----------------------- - # The parent downloads.DownloadManager object - self.download_manager_obj = download_manager_obj - # The downloads.DownloadItem object for the current job - self.download_item_obj = None - # The downloads.VideoDownloader, downloads.ClipDownloader or - # downloads.StreamDownloader object for the current job (if it - # exists) - self.downloader_obj = None - # The downloads.JSONFetcher object for the current job (if it exists) - self.json_fetcher_obj = None - # The options.OptionsManager object for the current job - self.options_manager_obj = None - - - # IV list - other - # --------------- - # A number identifying this worker, matching the number of the page - # in the Output tab (so the first worker created is #1) - self.worker_id = len(download_manager_obj.worker_list) + 1 - - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # Flag set to False if self.close() is called - # The False value halts the main loop in self.run() - self.running_flag = True - # Flag set to True when the parent downloads.DownloadManager object - # wants to destroy this worker, having called self.set_doomed_flag() - # to do that - # The worker is not destroyed until its current download is complete - self.doomed_flag = False - # Downloads of broadcasting livestreams must start as soon as possible. - # If the worker limit (mainapp.TartubeApp.num_worker_default) has - # been reached, additional workers are created to handle them - # If True, this worker can only be used for broadcasting livestreams. - # If False, it can be used for anything - self.broadcast_flag = broadcast_flag - - # Options list (used by downloads.VideoDownloader) - # Initialised in the call to self.prepare_download() - self.options_list = [] - # Flag set to True when the worker is available for a new job, False - # when it is already occupied with a job - self.available_flag = True - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Waits until this worker has been assigned a job, at which time we - create a new downloads.VideoDownloader or downloads.StreamDownloader - object and wait for the result. - """ - - # Import the main application and custom download manager (for - # convenience) - app_obj = self.download_manager_obj.app_obj - custom_dl_obj = self.download_manager_obj.custom_dl_obj - - # Handle a job, or wait for the downloads.DownloadManager to assign - # this worker a job - while self.running_flag: - - # If this worker is currently assigned a job... - if not self.available_flag: - - # Import the media data object (for convenience) - media_data_obj = self.download_item_obj.media_data_obj - - # If the downloads.DownloadItem was created by a scheduled - # download (media.Scheduled), then change the number of - # workers, if necessary - if self.download_item_obj.scheduled_obj: - - scheduled_obj = self.download_item_obj.scheduled_obj - if scheduled_obj.scheduled_num_worker_apply_flag \ - and scheduled_obj.scheduled_num_worker \ - != len(self.download_manager_obj.worker_list): - - self.download_manager_obj.change_worker_count( - scheduled_obj.scheduled_num_worker, - ) - - # When downloading a livestream that's broadcasting now, we - # call StreamDownloader rather than VideoDownloader - # When downloading video clips, use youtube-dl with FFmpeg as - # its external downloader - # Otherwise, use youtube-dl with an argument list determined by - # the download options applied - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.live_mode == 2 \ - and self.download_item_obj.operation_type != 'sim' \ - and self.download_item_obj.operation_type != 'custom_sim' \ - and self.download_item_obj.operation_type != 'classic_sim': - self.run_stream_downloader(media_data_obj) - - elif isinstance(media_data_obj, media.Video) \ - and not media_data_obj.live_mode \ - and ( - ( - ( - self.download_item_obj.operation_type \ - == 'custom_real' \ - or self.download_item_obj.operation_type \ - == 'classic_custom' - ) and ( - ( - custom_dl_obj \ - and custom_dl_obj.dl_by_video_flag \ - and custom_dl_obj.split_flag - and media_data_obj.stamp_list - ) or ( - custom_dl_obj \ - and custom_dl_obj.dl_by_video_flag \ - and not custom_dl_obj.split_flag \ - and custom_dl_obj.slice_flag - and media_data_obj.slice_list - ) or media_data_obj.dbid in \ - app_obj.temp_stamp_buffer_dict \ - or media_data_obj.dbid in \ - app_obj.temp_slice_buffer_dict \ - ) - ) or ( - self.download_item_obj.operation_type \ - == 'classic_real' \ - and media_data_obj.dbid in \ - app_obj.temp_stamp_buffer_dict - ) - ): - self.run_clip_slice_downloader(media_data_obj) - - else: - self.run_video_downloader(media_data_obj) - - # Send a message to the Output tab's summary page - app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Thread #') + str(self.worker_id) \ - + ': ' + _('Job complete') + ' \'' \ - + self.download_item_obj.media_data_obj.name + '\'', - ) - - # This worker is now available for a new job - self.available_flag = True - - # Send a message to the Output tab's summary page - app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Thread #') + str(self.worker_id) \ - + ': ' + _('Worker now available again'), - ) - - # During (real, not simulated) custom downloads, apply a delay - # if one has been specified - if ( - self.download_item_obj.operation_type == 'custom_real' \ - or self.download_item_obj.operation_type \ - == 'classic_custom' - ) and custom_dl_obj \ - and custom_dl_obj.delay_flag: - - # Set the delay (in seconds), a randomised value if - # required - if custom_dl_obj.delay_min: - delay = random.randint( - int(custom_dl_obj.delay_min * 60), - int(custom_dl_obj.delay_max * 60), - ) - else: - delay = int(custom_dl_obj.delay_max * 60) - - time.sleep(delay) - - # Pause a moment, before the next iteration of the loop (don't want - # to hog resources) - time.sleep(self.sleep_time) - - - def run_video_downloader(self, media_data_obj): - - """Called by self.run() - - Creates a new downloads.VideoDownloader to handle the download(s) for - this job, and destroys it when it's finished. - - If possible, checks the channel/playlist RSS feed for videos we don't - already have, and mark them as livestreams - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object being downloaded. When the - download operation was launched from the Classic Mode tab, a - dummy media.Video object - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # If the download stalls, the VideoDownloader may need to be replaced - # with a new one. Use a while loop for that - first_flag = True - restart_count = 0 - - while True: - - # Set up the new downloads.VideoDownloader object - self.downloader_obj = VideoDownloader( - self.download_manager_obj, - self, - self.download_item_obj, - ) - - if first_flag: - - first_flag = False - # Send a message to the Output tab's summary page - app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Thread #') + str(self.worker_id) \ - + ': ' + _('Assigned job:') + ' \'' \ - + self.download_item_obj.media_data_obj.name \ - + '\'', - ) - - # Execute the assigned job - return_code = self.downloader_obj.do_download() - - # Any youtube-dl error/warning messages which have not yet been - # passed to their media.Video objects can now be processed - for vid in self.downloader_obj.video_msg_buffer_dict.keys(): - self.downloader_obj.process_error_warning(vid) - - # Unless the download was stopped manually (return code 5), any - # 'dummy' media.Video objects can be set, so that their URLs are - # not remembered in the next Tartube session - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.dummy_flag \ - and return_code < 5: - media_data_obj.set_dummy_dl_flag(True) - - # If the download stalled, -1 is returned. If we're allowed to - # restart a stalled download, do that; otherwise give up - if return_code > -1 \ - or ( - app_obj.operation_auto_restart_max != 0 - and restart_count >= app_obj.operation_auto_restart_max - ): - break - - else: - restart_count += 1 - msg = _('Tartube is restarting a stalled download') - - # Show confirmation of the restart - if app_obj.ytdl_output_stdout_flag: - app_obj.main_win_obj.output_tab_write_stdout( - self.worker_id, - msg, - ) - - if app_obj.ytdl_write_stdout_flag: - print(msg) - - if app_obj.ytdl_log_stdout_flag: - app_obj.write_downloader_log(msg) - - # If the downloads.VideoDownloader object collected any youtube-dl - # error/warning messages, display them in the Error List - if media_data_obj.error_list or media_data_obj.warning_list: - GObject.timeout_add( - 0, - app_obj.main_win_obj.errors_list_add_operation_msg, - media_data_obj, - ) - - # In the event of an error, nothing updates the video's row in the - # Video Catalogue, and therefore the error icon won't be visible - # Do that now (but don't if mainwin.ComplexCatalogueItem objects aren't - # being used in the Video Catalogue) - if not self.download_item_obj.operation_classic_flag \ - and return_code == VideoDownloader.ERROR \ - and isinstance(media_data_obj, media.Video) \ - and app_obj.catalogue_mode_type != 'simple': - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - media_data_obj, - ) - - # Call the destructor function of VideoDownloader object - self.downloader_obj.close() - - # If possible, check the channel/playlist RSS feed for videos we don't - # already have, and mark them as livestreams - if self.running_flag \ - and mainapp.HAVE_FEEDPARSER_FLAG \ - and app_obj.enable_livestreams_flag \ - and ( - isinstance(media_data_obj, media.Channel) \ - or isinstance(media_data_obj, media.Playlist) - ) and not media_data_obj.dl_no_db_flag \ - and media_data_obj.child_list \ - and media_data_obj.rss: - - # Send a message to the Output tab's summary page - app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Thread #') + str(self.worker_id) \ - + ': ' + _('Checking RSS feed'), - ) - - # Check the RSS feed for the media data object - self.check_rss(media_data_obj) - - - def run_clip_slice_downloader(self, media_data_obj): - - """Called by self.run() - - Creates a new downloads.ClipDownloader to handle the download(s) for - this job, and destroys it when it's finished. - - Args: - - media_data_obj (media.Video): The media data object being - downloaded. When the download operation was launched from the - Classic Mode tab, a dummy media.Video object - - """ - - # Import the main application and custom download manager (for - # convenience) - app_obj = self.download_manager_obj.app_obj - custom_dl_obj = self.download_manager_obj.custom_dl_obj - - # Set up the new downloads.ClipDownloader object - self.downloader_obj = ClipDownloader( - self.download_manager_obj, - self, - self.download_item_obj, - ) - - # Send a message to the Output tab's summary page - app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Thread #') + str(self.worker_id) \ - + ': ' + _('Assigned job:') + ' \'' \ - + self.download_item_obj.media_data_obj.name \ - + '\'', - ) - - # Execute the assigned job - # ClipDownloader handles two related operations. Both start by - # downloading the video as clips. The second operation concatenates - # the clips back together, which has the effect of removing one or - # more slices from a video - if ( - custom_dl_obj \ - and custom_dl_obj.split_flag \ - and media_data_obj.stamp_list - ) or media_data_obj.dbid in app_obj.temp_stamp_buffer_dict: - return_code = self.downloader_obj.do_download_clips() - else: - return_code = self.downloader_obj.do_download_remove_slices() - - # In the event of an error, nothing updates the video's row in the - # Video Catalogue, and therefore the error icon won't be visible - # Do that now (but don't if mainwin.ComplexCatalogueItem objects aren't - # being used in the Video Catalogue) - if not self.download_item_obj.operation_classic_flag \ - and return_code == ClipDownloader.ERROR \ - and app_obj.catalogue_mode_type != 'simple': - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - media_data_obj, - ) - - # Call the destructor function of ClipDownloader object - self.downloader_obj.close() - - - def run_stream_downloader(self, media_data_obj): - - """Called by self.run() - - A modified version of self.run_video_downloader(), used when - downloading a media.Video object that's a livestream broadcasting now. - - First creates a new downloads.VideoDownloader to check the video, if - it hasn't already been checked (which fetches the thumbnail, - description, annotations and metadata files). - - Then creates a new downloads.StreamDownloader to handle the download - for this job. - - Args: - - media_data_obj (media.Video): The media data object being - downloaded - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Checking a livestream (simulated download), before downloading it - # (real download) makes sure that the thumbnail and other metadata - # files are downloaded, if required - # Assume that the video has not been checked if its path or name are - # not set - # If mainapp.TartubeApp.livestream_dl_mode is 'default', meaning that - # youtube-dl is downloading the livestream directly, then there is - # no need to check the video first - if app_obj.livestream_dl_mode != 'default' \ - and not self.download_manager_obj.operation_classic_flag \ - and ( - app_obj.livestream_force_check_flag \ - or media_data_obj.file_name is None \ - or media_data_obj.file_ext is None - ): - # Set up the new downloads.VideoDownloader object. The True - # argument forces it to do a simulated download - self.downloader_obj = VideoDownloader( - self.download_manager_obj, - self, - self.download_item_obj, - True, - ) - - # Send a message to the Output tab's summary page - app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Thread #') + str(self.worker_id) \ - + ': ' + _('Assigned job:') + ' \'' \ - + self.download_item_obj.media_data_obj.name \ - + '\'', - ) - - # Execute the assigned job (but regardless of success or failure, - # we press on with the livestream download below) - return_code = self.downloader_obj.do_download() - - # Any youtube-dl error/warning messages which have not yet been - # passed to their media.Video objects can now be processed - for vid in self.downloader_obj.video_msg_buffer_dict.keys(): - self.downloader_obj.process_error_warning(vid) - - # If the downloads.VideoDownloader object collected any youtube-dl - # error/warning messages, display them in the Error List - if media_data_obj.error_list or media_data_obj.warning_list: - GObject.timeout_add( - 0, - app_obj.main_win_obj.errors_list_add_operation_msg, - media_data_obj, - ) - - # In the event of an error, nothing updates the video's row in the - # Video Catalogue, and therefore the error icon won't be visible - # Do that now (but don't if mainwin.ComplexCatalogueItem objects - # aren't being used in the Video Catalogue) - if not self.download_item_obj.operation_classic_flag \ - and return_code == VideoDownloader.ERROR \ - and isinstance(media_data_obj, media.Video) \ - and app_obj.catalogue_mode_type != 'simple': - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - media_data_obj, - ) - - # Call the destructor function of VideoDownloader object - self.downloader_obj.close() - - # In the event of an error during the checking stage, don't - # proceed with the download - if return_code >= VideoDownloader.ERROR: - return - - # Reset our IVs, ready for the call to StreamDownloader - self.prepare_download(self.download_item_obj) - - # Now proceed with the livestream download. Set up the new - # downloads.StreamDownloader object - self.downloader_obj = StreamDownloader( - self.download_manager_obj, - self, - self.download_item_obj, - ) - - # Send a message to the Output tab's summary page - app_obj.main_win_obj.output_tab_write_stdout( - 0, - _('Thread #') + str(self.worker_id) \ - + ': ' + _('Assigned job:') + ' \'' \ - + self.download_item_obj.media_data_obj.name \ - + '\'', - ) - - # Execute the assigned job - return_code = self.downloader_obj.do_download() - - # In the event of an error, nothing updates the video's row in the - # Video Catalogue, and therefore the error icon won't be visible - # Do that now (but don't if mainwin.ComplexCatalogueItem objects aren't - # being used in the Video Catalogue) - if not self.download_item_obj.operation_classic_flag \ - and return_code == StreamDownloader.ERROR \ - and app_obj.catalogue_mode_type != 'simple': - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - media_data_obj, - ) - - # Call the destructor function of StreamDownloader object - self.downloader_obj.close() - - - def close(self): - - """Called by downloads.DownloadManager.run(). - - This worker object is closed when: - - 1. The download operation is complete (or has been stopped) - 2. The worker has been marked as doomed, and the calling function - is now ready to destroy it - - Tidy up IVs and stop any child processes. - """ - - self.running_flag = False - - if self.downloader_obj: - self.downloader_obj.stop() - - if self.json_fetcher_obj: - self.json_fetcher_obj.stop() - - - def check_rss(self, container_obj): - - """Called by self.run(), after the VideoDownloader has finished. - - If possible, check the channel/playlist RSS feed for videos we don't - already have, and mark them as livestreams. - - This process works on YouTube (each media.Channel and media.Playlist - has the URL for its RSS feed set automatically). - - It might work on other compatible websites (the user must set the - channel's/playlist's RSS feed manually). - - On a compatible website, when youtube-dl fetches a list of videos in - the channel/playlist, it won't fetch any that are livestreams (either - waiting to start, or currently broadcasting). - - However, livestreams (both waiting and broadcasting) do appear in the - RSS feed. We can compare the RSS feed against the channel's/playlist's - list of child media.Video objects (which has just been updated), in - order to detect livestreams (with reasonably good accuracy). - - Args: - - container_obj (media.Channel, media.Playlist): The channel or - playlist which the VideoDownloader has just checked/downloaded. - (This function is not called for media.Folders or for - individual media.Video objects) - - """ - - app_obj = self.download_manager_obj.app_obj - - # Livestreams are usually the first entry in the RSS feed, having not - # started yet (or being currently broadcast), but there's no - # gurantee of that - # In addition, although RSS feeds are normally quite short (with - # dozens of entries, not thousands), there is no guarantee of this - # mainapp.TartubeApp.livestream_max_days specifies how many days of - # videos we should check, looking for livestreams - # Implement this by stopping when an entry in the RSS feed matches a - # particular media.Video object - # (If we can't decide which video to match, the default to searching - # the whole RSS feed) - time_limit_video_obj = None - check_source_list = [] - check_name_list = [] - - if app_obj.livestream_max_days: - - # Stop checking the RSS feed at the first matching video that's - # older than the specified time - # (Of course, the 'first video' must not itself be a livestream) - older_time = int( - time.time() - (app_obj.livestream_max_days * 86400), - ) - - for child_obj in container_obj.child_list: - - # An entry in the RSS feed is a new livestream, if it doesn't - # match one of the videos in these lists - # (We don't need to check each RSS entry against the entire - # contents of the channel/playlist - which might be thousands - # of videos - just those up to the time limit) - if child_obj.source: - check_source_list.append(child_obj.source) - if child_obj.name != app_obj.default_video_name: - check_name_list.append(child_obj.name) - - # The time limit will apply to this video, when found - for child_obj in container_obj.child_list: - if child_obj.source \ - and not child_obj.live_mode \ - and child_obj.upload_time is not None \ - and child_obj.upload_time < older_time: - time_limit_video_obj = child_obj - break - - else: - - # Stop checking the RSS feed at the first matching video, no matter - # how old - for child_obj in container_obj.child_list: - if child_obj.source: - check_source_list.append(child_obj.source) - if child_obj.name != app_obj.default_video_name: - check_name_list.append(child_obj.name) - - for child_obj in container_obj.child_list: - if child_obj.source \ - and not time_limit_video_obj \ - and not child_obj.live_mode: - time_limit_video_obj = child_obj - break - - # Fetch the RSS feed - try: - feed_dict = feedparser.parse(container_obj.rss) - except: - return - - # Check each entry in the feed, stopping at the first one which matches - # the selected media.Video object - for entry_dict in feed_dict['entries']: - - if time_limit_video_obj \ - and entry_dict['link'] == time_limit_video_obj.source: - - # Found a matching media.Video object, so we can stop looking - # for livestreams now - break - - elif not entry_dict['link'] in check_source_list \ - and not entry_dict['title'] in check_name_list: - - # New livestream detected. Create a new JSONFetcher object to - # fetch its JSON data - # If the data is received, the livestream is live. If the data - # is not received, the livestream is waiting to go live - self.json_fetcher_obj = JSONFetcher( - self.download_manager_obj, - self, - container_obj, - entry_dict, - ) - - # Then execute the assigned job - self.json_fetcher_obj.do_fetch() - - # Call the destructor function of the JSONFetcher object - self.json_fetcher_obj.close() - self.json_fetcher_obj = None - - - def prepare_download(self, download_item_obj): - - """Called by downloads.DownloadManager.run(). - - Also called by self.run_stream_downloader() after the checking phase, - just before downloading the broadcasting livestream for real. - - Based on Worker.download(). - - Updates IVs for a new job, so that self.run can initiate the download. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object describing the URL from which youtube-dl should download - video(s). - - """ - - self.download_item_obj = download_item_obj - self.options_manager_obj = download_item_obj.options_manager_obj - - self.options_list = self.download_manager_obj.options_parser_obj.parse( - self.download_item_obj.media_data_obj, - self.options_manager_obj, - self.download_item_obj.operation_type, - self.download_item_obj.scheduled_obj, - ) - - self.available_flag = False - - - def set_doomed_flag(self, flag): - - """Called by downloads.DownloadManager.change_worker_count().""" - - self.doomed_flag = flag - - - # Callback class methods - - - def data_callback(self, dl_stat_dict, last_flag=False): - - """Called by downloads.VideoDownloader.read_child_process() and - .last_data_callback(). - - Based on Worker._data_hook() and ._talk_to_gui(). - - 'dl_stat_dict' holds a dictionary of statistics in a standard format - specified by downloads.VideoDownloader.extract_stdout_data(). - - This callback receives that dictionary and passes it on to the main - window, so the statistics can be displayed there. - - Args: - - dl_stat_dict (dict): The dictionary of statistics described above - - last_flag (bool): True when called by .last_data_callback(), - meaning that the VideoDownloader object has finished, and is - sending this function the final set of statistics - - """ - - main_win_obj = self.download_manager_obj.app_obj.main_win_obj - - if not self.download_item_obj.operation_classic_flag: - - GObject.timeout_add( - 0, - main_win_obj.progress_list_receive_dl_stats, - self.download_item_obj, - dl_stat_dict, - last_flag, - ) - - # If downloading a video individually, need to update the tooltips - # in the Results List to show any errors/warnings (which won't - # show up if the video was not downloaded) - if last_flag \ - and isinstance(self.download_item_obj.media_data_obj, media.Video): - - GObject.timeout_add( - 0, - main_win_obj.results_list_update_tooltip, - self.download_item_obj.media_data_obj, - ) - - else: - - GObject.timeout_add( - 0, - main_win_obj.classic_mode_tab_receive_dl_stats, - self.download_item_obj, - dl_stat_dict, - last_flag, - ) - - -class DownloadList(object): - - """Called by mainapp.TartubeApp.download_manager_continue(). - - Based on the DownloadList class in youtube-dl-gui. - - Python class to keep track of all the media data objects to be downloaded - (for real or in simulation) during a downloaded operation. - - This object contains an ordered list of downloads.DownloadItem objects. - Each of those objects represents a media data object to be downloaded - (media.Video, media.Channel, media.Playlist or media.Folder). - - Videos are downloaded in the order specified by the list. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - operation_type (str): 'sim' if channels/playlists should just be - checked for new videos, without downloading anything. 'real' if - videos should be downloaded (or not) depending on each media data - object's .dl_sim_flag IV - - 'custom_real' is like 'real', but with additional options applied - (specified by a downloads.CustomDLManager object). A 'custom_real' - operation is sometimes preceded by a 'custom_sim' operation (which - is the same as a 'sim' operation, except that it is always followed - by a 'custom_real' operation) - - For downloads launched from the Classic Mode tab, 'classic_real' - for an ordinary download, or 'classic_custom' for a custom - download. A 'classic_custom' operation is always preceded by a - 'classic_sim' operation (which is the same as a 'sim' operation, - except that it is always followed by a 'classic_custom' operation) - - media_data_list (list): List of media.Video, media.Channel, - media.Playlist and/or media.Folder objects. Can also be a list of - (exclusively) media.Scheduled objects. If not an empty list, only - the specified media data objects (and their children) are - checked/downloaded. If an empty list, all media data objects are - checked/downloaded. If operation_type is 'classic', then the - media_data_list contains a list of dummy media.Video objects from a - previous call to this function. If an empty list, all - dummy media.Video objects are downloaded - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that applies to this download operation. Only specified - when 'operation_type' is 'custom_sim', 'custom_real', 'classic_sim' - or 'classic_real' - - For 'custom_real' and 'classic_real', not specified if - mainapp.TartubeApp.temp_stamp_buffer_dict or - .temp_slice_buffer_dict are specified (because those values take - priority) - - """ - - - # Standard class methods - - - def __init__(self, app_obj, operation_type, media_data_list, \ - custom_dl_obj): - - # IV list - class objects - # ----------------------- - self.app_obj = app_obj - - # The custom download manager (downloads.CustomDLManager) that applies - # to this download operation. Only specified when 'operation_type' is - # 'custom_sim', 'custom_real', 'classic_sim' or 'classic_real' - # For 'custom_real' and 'classic_real', not specified if - # mainapp.TartubeApp.temp_stamp_buffer_dict or - # .temp_slice_buffer_dict are specified (because those values take - # priority) - self.custom_dl_obj = custom_dl_obj - - # IV list - other - # --------------- - # 'sim' if channels/playlists should just be checked for new videos, - # without downloading anything. 'real' if videos should be downloaded - # (or not) depending on each media data object's .dl_sim_flag IV - # 'custom_real' is like 'real', but with additional options applied - # (specified by a downloads.CustomDLManager object). A 'custom_real' - # operation is sometimes preceded by a 'custom_sim' operation (which - # is the same as a 'sim' operation, except that it is always followed - # by a 'custom_real' operation) - # For downloads launched from the Classic Mode tab, 'classic_real' for - # an ordinary download, or 'classic_custom' for a custom download. A - # 'classic_custom' operation is always preceded by a 'classic_sim' - # operation (which is the same as a 'sim' operation, except that it - # is always followed by a 'classic_custom' operation) - # This IV records the default setting for this operation. Once the - # download operation starts, new download.DownloadItem objects can - # be added to the list in a call to self.create_item(), and that call - # can specify a value that overrides the default value, just for that - # call - # Overriding the default value is not possible for download operations - # initiated from the Classic Mode tab - self.operation_type = operation_type - # Shortcut flag to test the operation type; True for 'classic_sim', - # 'classic_real' and 'classic_custom'; False forall other values - self.operation_classic_flag = False # (Set below) - # Flag set to True in a call to self.prevent_fetch_new_items(), in - # which case subsequent calls to self.fetch_next_item() return - # nothing, preventing any further downloads - self.prevent_fetch_flag = False - - # Number of download.DownloadItem objects created (used to give each a - # unique ID) - self.download_item_count = 0 - - # An ordered list of downloads.DownloadItem objects, one for each - # media.Video, media.Channel, media.Playlist or media.Folder object - # (including dummy media.Video objects used by download operations - # launched from the Classic Mode tab) - # This list stores each item's .item_id - self.download_item_list = [] - # A supplementary list of downloads.DownloadItem objects - # Suppose self.download_item_list already contains items A B C, and - # some of part of the code wants to add items X Y Z to the beginning - # of the list, producing the list X Y Z A B C (and not Z Y X A B C) - # The new items are added (one at a time) to this temporary list, and - # then added to the beginning/end of self.download_item_list at the - # end of this function (or in the next call to - # self.fetch_next_item() ) - self.temp_item_list = [] - - # We preserve the 'media_data_list' argument (which may be an empty - # list). Used by mainapp.TartubeApp.download_manager_finished during - # a 'custom_sim' operation, in order to initiate the subsequent - # 'custom_real' operation - self.orig_media_data_list = media_data_list - - # Corresponding dictionary of downloads.DownloadItem items for quick - # lookup, containing items from both self.download_item_list and - # self.temp_item_list - # Dictionary in the form - # key = download.DownloadItem.item_id - # value = the download.DownloadItem object itself - self.download_item_dict = {} - # The .item_id of a download.DownloadItem.item_id, which is set (if - # required) by a call to self.set_final_item() - # When self.fetch_next_item() fetches this item, that item is the last - # item to be fetched: self.download_item_list() and - # self.temp_item_list() are emptied, and any items they contained are - # not checked/downloaded - self.final_item_id = None - - - # Code - # ---- - - # Set the flag - if operation_type == 'classic_sim' \ - or operation_type == 'classic_real' \ - or operation_type == 'classic_custom': - self.operation_classic_flag = True - - # Compile the list - - # Scheduled downloads - if media_data_list and isinstance(media_data_list[0], media.Scheduled): - - # media_data_list is a list of media.Scheduled objects, each one - # handling a scheduled download - all_obj = False - ignore_limits_flag = False - - for scheduled_obj in media_data_list: - if scheduled_obj.all_flag: - all_obj = scheduled_obj - if scheduled_obj.ignore_limits_flag: - ignore_limits_flag = True - if all_obj: - break - - - if all_obj: - - # Use all media data objects - for dbid in self.app_obj.container_top_level_list: - obj = self.app_obj.media_reg_dict[dbid] - self.create_item( - obj, - all_obj, # media.Scheduled object - None, # override_operation_type - False, # priority_flag - ignore_limits_flag, - ) - - else: - - # Use only media data objects specified by the media.Scheduled - # objects - # Don't add the same media data object twice - check_dict = {} - - for scheduled_obj in media_data_list: - - if scheduled_obj.join_mode == 'priority': - priority_flag = True - else: - priority_flag = False - - for dbid in scheduled_obj.media_list: - if not dbid in check_dict: - - obj = self.app_obj.media_reg_dict[dbid] - - self.create_item( - obj, - scheduled_obj, - scheduled_obj.dl_mode, - priority_flag, - scheduled_obj.ignore_limits_flag, - ) - - check_dict[dbid] = None - - # Normal downloads - elif not self.operation_classic_flag: - - # For each media data object to be downloaded, create a - # downloads.DownloadItem object, and update the IVs above - if not media_data_list: - - # Use all media data objects - for dbid in self.app_obj.container_top_level_list: - obj = self.app_obj.media_reg_dict[dbid] - self.create_item( - obj, - None, # media.Scheduled object - None, # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - else: - - for media_data_obj in media_data_list: - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: - - # Videos in a private folder's .child_list can't be - # downloaded (since they are also a child of a - # channel, playlist or a public folder) - GObject.timeout_add( - 0, - app_obj.system_error, - 301, - _('Cannot download videos in a private folder'), - ) - - else: - - # Use the specified media data object - self.create_item( - media_data_obj, - None, # media.Scheduled object - None, # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - # Some media data objects have an alternate download destination, - # for example, a playlist ('slave') might download its videos - # into the directory used by a channel ('master') - # This can increase the length of the operation, because a 'slave' - # won't start until its 'master' is finished - # Make sure all designated 'masters' are handled before 'slaves' (a - # media data object can't be both a master and a slave) - self.reorder_master_slave() - - # Downloads from the Classic Mode tab - else: - - # The download operation was launched from the Classic Mode tab. - # Each URL to be downloaded is represented by a dummy media.Video - # object (one which is not in the media data registry) - main_win_obj = self.app_obj.main_win_obj - - # The user may have rearranged rows in the Classic Mode tab, so - # get a list of (all) dummy media.Videos in the rearranged order - # (It should be safe to assume that the Gtk.Liststore contains - # exactly the same number of rows, as dummy media.Video objects - # in mainwin.MainWin.classic_media_dict) - dbid_list = [] - for row in main_win_obj.classic_progress_liststore: - dbid_list.append(row[0]) - - # Compile a list of dummy media.Video objects in the correct order - obj_list = [] - if not media_data_list: - - # Use all of them - for dbid in dbid_list: - obj_list.append(main_win_obj.classic_media_dict[dbid]) - - else: - - # Use a subset of them - for dbid in dbid_list: - - dummy_obj = main_win_obj.classic_media_dict[dbid] - if dummy_obj in media_data_list: - obj_list.append(dummy_obj) - - - # For each dummy media.Video object, create a - # downloads.DownloadItem object, and update the IVs above - # Don't re-download a video already marked as downloaded (if the - # user actually wants to re-download a video, then - # mainapp.TartubeApp.on_button_classic_redownload() has reset the - # flag) - for dummy_obj in obj_list: - - if not dummy_obj.dl_flag: - self.create_dummy_item(dummy_obj) - - # We can now merge the two DownloadItem lists - if self.temp_item_list: - - self.download_item_list \ - = self.temp_item_list + self.download_item_list - self.temp_item_list = [] - - - # Public class methods - - - @synchronise(_SYNC_LOCK) - def abandon_remaining_items(self): - - """Called by downloads.DownloadManager.stop_download_operation() and - .stop_download_operation_soon(). - - When the download operation has been stopped by the user, any rows in - the main window's Progress List (or Classic Progress List) currently - marked as 'Waiting' should be marked as 'Not started'. - """ - - main_win_obj = self.app_obj.main_win_obj - download_manager_obj = self.app_obj.download_manager_obj - - # In case of any recent calls to self.create_item(), which want to - # place new DownloadItems at the beginning of the queue, then - # merge the temporary queue into the main one - if self.temp_item_list: - self.download_item_list \ - = self.temp_item_list + self.download_item_list - - # 'dl_stat_dict' holds a dictionary of statistics in a standard format - # specified by downloads.VideoDownloader.extract_stdout_data() - # Prepare the dictionary to be passed on to the main window, so the - # statistics can be displayed there for every 'Waiting' item - dl_stat_dict = {} - dl_stat_dict['status'] = formats.MAIN_STAGE_NOT_STARTED - - for item_id in self.download_item_list: - this_item = self.download_item_dict[item_id] - - if this_item.stage == formats.MAIN_STAGE_QUEUED: - this_item.stage = formats.MAIN_STAGE_NOT_STARTED - - if not download_manager_obj.operation_classic_flag: - - GObject.timeout_add( - 0, - main_win_obj.progress_list_receive_dl_stats, - this_item, - dl_stat_dict, - True, # Final set of statistics for this item - ) - - else: - - GObject.timeout_add( - 0, - main_win_obj.classic_mode_tab_receive_dl_stats, - this_item, - dl_stat_dict, - True, # Final set of statistics for this item - ) - - - @synchronise(_SYNC_LOCK) - def change_item_stage(self, item_id, new_stage): - - """Called by downloads.DownloadManager.run(). - - Based on DownloadList.change_stage(). - - Changes the download stage for the specified downloads.DownloadItem - object. - - Args: - - item_id (int): The specified item's .item_id - - new_stage: The new download stage, one of the values imported from - formats.py (e.g. formats.MAIN_STAGE_QUEUED) - - """ - - self.download_item_dict[item_id].stage = new_stage - - - def create_item(self, media_data_obj, scheduled_obj=None, - override_operation_type=None, priority_flag=False, - ignore_limits_flag=False, recursion_flag=False): - - """Called initially by self.__init__() (or by many other functions, - for example in mainapp.TartubeApp). - - Subsequently called by this function recursively. - - Creates a downloads.DownloadItem object for media data objects in the - media data registry. - - Doesn't create a download item object for: - - media.Video, media.Channel and media.Playlist objects whose - .source is None - - media.Video objects whose parent is not a media.Folder (i.e. - whose parent is a media.Channel or a media.Playlist) - - media.Video objects in any restricted folder - - media.Video objects in the fixed 'Unsorted Videos' folder which - are already marked as downloaded - - media.Video objects which have an ancestor (e.g. a parent - media.Channel) for which checking/downloading is disabled - - media.Video objects whose parent is a media.Folder, and whose - file IVs are set, and for which a thumbnail exists, if - mainapp.TartubeApp.operation_sim_shortcut_flag is set, and if - the operation_type is 'sim' - - media.Channel and media.Playlist objects for which checking/ - downloading are disabled, or which have an ancestor (e.g. a - parent media.folder) for which checking/downloading is disabled - - media.Channel, media.Playlist and media.Folder objects whose - .dl_no_db_flag is set, during simulated downloads - - media.Channel and media.Playlist objects during custom downloads - in which videos are to be downloaded independently - - media.Channel and media.Playlist objects which are disabled - because their external directory is not available - - media.Video objects whose parent channel/playlist/folder is - marked unavailable because its external directory is not - accessible - - media.Folder objects - - Adds the resulting downloads.DownloadItem object to this object's IVs. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): A media data object - - scheduled_obj (media.Scheduled): The scheduled download object - which wants to download media_data_obj (None if no scheduled - download applies in this case) - - override_operation_type (str): After the download operation has - started, any code can call this function to add new - downloads.DownloadItem objects to this downloads.DownloadList, - specifying a value that overrides the default value of - self.operation_type. Note that this is not allowed when - self.operation_type is 'classic_real', 'classic_sim' or - 'classic_custom', and will cause an error. The value is always - None when called by self.__init__(). Otherwise, the value can - be None, 'sim', 'real', 'custom_sim' or 'custom_real' - - priority_flag (bool): True if media_data_obj is to be added to the - beginning of the list, False if it is to be added to the end - of the list - - ignore_limits_flag (bool): True if operation limits - (mainapp.TartubeApp.operation_limit_flag) should be ignored - - recursion_flag (bool): True when called by this function - recursively, False when called (for the first time) by anything - else. If False and media_data_obj is a media.Video object, we - download it even if its parent is a channel or a playlist - - Return values: - - The downloads.DownloadItem object created (or None if no object is - created; only required by calls from - mainapp.TartubeApp.download_watch_videos() ) - - """ - - # Sanity check - if no URL is specified, then there is nothing to - # download - if not isinstance(media_data_obj, media.Folder) \ - and media_data_obj.source is None: - return None - - # Apply the operation_type override, if specified - if override_operation_type is not None: - - if self.operation_classic_flag: - - GObject.timeout_add( - 0, - self.app_obj.system_error, - 302, - 'Invalid argument in Classic Mode tab download operation', - ) - - return None - - else: - - operation_type = override_operation_type - - else: - - operation_type = self.operation_type - - if operation_type == 'custom_real' \ - or operation_type == 'classic_custom': - custom_flag = True - else: - custom_flag = False - - # Get the options.OptionsManager object that applies to this media - # data object - # (The manager might be specified by obj itself, or it might be - # specified by obj's parent, or we might use the default - # options.OptionsManager) - if not self.operation_classic_flag: - - options_manager_obj = utils.get_options_manager( - self.app_obj, - media_data_obj, - ) - - else: - - # Classic Mode tab - if self.app_obj.classic_options_obj is not None: - options_manager_obj = self.app_obj.classic_options_obj - else: - options_manager_obj = self.app_obj.general_options_obj - - # Ignore private folders, and don't download any of their children - # (because they are all children of some other non-private folder) - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: - return None - - # Don't download videos that we already have - # Don't download videos if they're in a channel or playlist (since - # downloading the channel/playlist downloads the videos it contains) - # (Exception: download a single video if that's what the calling code - # has specifically requested) - # (Exception: for custom downloads, do get videos independently of - # their channel/playlist, if allowed) - # Don't download videos in a folder, if this is a simulated download, - # and the video has already been checked (exception: if the video - # has been passed to the download operation directly, for example by - # right-clicking the video and selecting 'Check video') - # (Exception: do download videos in a folder if they're marked as - # livestreams, in case the livestream has finished) - # During custom downloads that required a subtitled video, don't - # download an un-subtitles video - if isinstance(media_data_obj, media.Video): - - if media_data_obj.dl_flag \ - and not media_data_obj.dbid \ - in self.app_obj.temp_stamp_buffer_dict \ - and not media_data_obj.dbid in self.app_obj.temp_slice_buffer_dict: - return - - if ( - not isinstance(media_data_obj.parent_obj, media.Folder) \ - and recursion_flag - and ( - not custom_flag - or ( - self.custom_dl_obj \ - and not self.custom_dl_obj.dl_by_video_flag - ) or media_data_obj.dl_flag - ) - ): - return None - - if isinstance(media_data_obj.parent_obj, media.Folder) \ - and ( - operation_type == 'sim' \ - or operation_type == 'custom_sim' \ - or operation_type == 'classic_sim' - ) and self.app_obj.operation_sim_shortcut_flag \ - and recursion_flag \ - and media_data_obj.file_name \ - and not media_data_obj.live_mode \ - and utils.find_thumbnail(self.app_obj, media_data_obj): - return None - - if custom_flag \ - and self.custom_dl_obj \ - and self.custom_dl_obj.dl_by_video_flag: - - if self.custom_dl_obj.dl_precede_flag \ - and self.custom_dl_obj.dl_if_subs_flag \ - and ( - not media_data_obj.subs_list \ - or ( - self.custom_dl_obj.dl_if_subs_list \ - and not utils.match_subs( - self.custom_dl_obj, - media_data_obj.subs_list, - ) - ) - ): - return None - - elif ( - self.custom_dl_obj.ignore_stream_flag \ - and media_data_obj.live_mode - ) or ( - self.custom_dl_obj.ignore_old_stream_flag \ - and media_data_obj.was_live_flag - ) or ( - self.custom_dl_obj.dl_if_stream_flag \ - and not media_data_obj.live_mode - ) or ( - self.custom_dl_obj.dl_if_old_stream_flag \ - and not media_data_obj.was_live_flag - ): - return - - # Don't download videos in channels/playlists/folders which have been - # marked unavailable, because their external directory is not - # accessible - if isinstance(media_data_obj, media.Video): - if media_data_obj.parent_obj.dbid \ - in self.app_obj.container_unavailable_dict: - return None - - elif not isinstance(media_data_obj, media.Video) \ - and media_data_obj.dbid in self.app_obj.container_unavailable_dict: - return None - - # Don't simulated downloads of video in channels/playlists/folders - # whose whose .dl_no_db_flag is set - if (operation_type == 'sim' or operation_type == 'custom_sim') \ - and ( - ( - isinstance(media_data_obj, media.Video) \ - and media_data_obj.parent_obj.dl_no_db_flag - ) or ( - not isinstance(media_data_obj, media.Video) \ - and media_data_obj.dl_no_db_flag - ) - ): - return None - - # Don't create a download.DownloadItem object if checking/download is - # disabled for the media data object - if not isinstance(media_data_obj, media.Video) \ - and media_data_obj.dl_disable_flag: - return None - - # Don't create a download.DownloadItem object for a media.Folder, - # obviously - # Don't create a download.DownloadItem object for a media.Channel or - # media.Playlist during a custom download in which videos are to be - # downloaded independently - download_item_obj = None - - if ( - isinstance(media_data_obj, media.Video) - and custom_flag - and ( - (self.custom_dl_obj and self.custom_dl_obj.dl_by_video_flag) \ - or media_data_obj.dbid in self.app_obj.temp_stamp_buffer_dict \ - or media_data_obj.dbid in self.app_obj.temp_slice_buffer_dict - ) - ) or ( - isinstance(media_data_obj, media.Video) - and ( - not custom_flag \ - or ( - self.custom_dl_obj \ - and not self.custom_dl_obj.dl_by_video_flag - ) - ) - ) or ( - ( - isinstance(media_data_obj, media.Channel) \ - or isinstance(media_data_obj, media.Playlist) - ) and ( - not custom_flag \ - or ( - self.custom_dl_obj \ - and not self.custom_dl_obj.dl_by_video_flag - ) - ) - ): - # (Broadcasting livestreams should always take priority over - # everything else) - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.live_mode == 2: - - broadcast_flag = True - # For a broadcasting livestream, we create additional workers - # if required, possibly bypassing the limit specified by - # mainapp.TartubeApp.num_worker_default - if self.app_obj.download_manager_obj: - self.app_obj.download_manager_obj.create_bypass_worker() - - else: - - broadcast_flag = False - - # Create a new download.DownloadItem object... - self.download_item_count += 1 - download_item_obj = DownloadItem( - self.download_item_count, - media_data_obj, - scheduled_obj, - options_manager_obj, - operation_type, - ignore_limits_flag, - ) - - # ...and add it to our list - if broadcast_flag: - self.download_item_list.insert(0, download_item_obj.item_id) - elif priority_flag: - self.temp_item_list.append(download_item_obj.item_id) - else: - self.download_item_list.append(download_item_obj.item_id) - - self.download_item_dict[download_item_obj.item_id] \ - = download_item_obj - - # Call this function recursively for any child media data objects in - # the following situations: - # 1. A media.Folder object has children - # 2. A media.Channel/media.Playlist object has child media.Video - # objects, and this is a custom download in which videos are to - # be downloaded independently of their channel/playlist - if isinstance(media_data_obj, media.Folder) \ - or ( - not isinstance(media_data_obj, media.Video) - and custom_flag - and self.custom_dl_obj - and self.custom_dl_obj.dl_by_video_flag - ): - for child_obj in media_data_obj.child_list: - self.create_item( - child_obj, - scheduled_obj, - operation_type, - priority_flag, - ignore_limits_flag, - True, # Recursion - ) - - # Procedure complete - return download_item_obj - - - def create_dummy_item(self, media_data_obj): - - """Called by self.__init__() only, when the download operation was - launched from the Classic Mode tab (this function is not called - recursively). - - Creates a downloads.DownloadItem object for each dummy media.Video - object. - - Adds the resulting downloads.DownloadItem object to this object's IVs. - - Args: - - media_data_obj (media.Video): A media data object - - Return values: - - The downloads.DownloadItem object created - - """ - - if media_data_obj.options_obj is not None: - # (Download options specified by the Drag and Drop tab) - options_manager_obj = media_data_obj.options_obj - elif self.app_obj.classic_options_obj is not None: - options_manager_obj = self.app_obj.classic_options_obj - else: - options_manager_obj = self.app_obj.general_options_obj - - # Create a new download.DownloadItem object... - self.download_item_count += 1 - download_item_obj = DownloadItem( - media_data_obj.dbid, - media_data_obj, - None, # media.Scheduled object - options_manager_obj, - self.operation_type, # 'classic_real'. 'classic_sim' or - # 'classic_custom' - False, # ignore_limits_flag - ) - - # ...and add it to our list - self.download_item_list.append(download_item_obj.item_id) - self.download_item_dict[download_item_obj.item_id] = download_item_obj - - # Procedure complete - return download_item_obj - - - @synchronise(_SYNC_LOCK) - def fetch_next_item(self): - - """Called by downloads.DownloadManager.run(). - - Based on DownloadList.fetch_next(). - - Return values: - - The next downloads.DownloadItem object, or None if there are none - left - - """ - - if not self.prevent_fetch_flag: - - # In case of any recent calls to self.create_item(), which want to - # place new DownloadItems at the beginning of the queue, then - # merge the temporary queue into the main one - if self.temp_item_list: - self.download_item_list \ - = self.temp_item_list + self.download_item_list - - for item_id in self.download_item_list: - this_item = self.download_item_dict[item_id] - - # Don't return an item that's marked as - # formats.MAIN_STAGE_ACTIVE - if this_item.stage == formats.MAIN_STAGE_QUEUED: - return this_item - - return None - - - @synchronise(_SYNC_LOCK) - def is_queuing(self, item_id): - - """Called by mainwin.MainWin.progress_list_popup_menu(), etc. - - Checks whether the specified DownloadItem object is waiting in the - queue (i.e. waiting to start checking/downloading). - - Args: - - item_id (int): The .item_id of a downloads.DownloadItem object; - should be a key in self.download_item_dict - - """ - - if item_id in self.download_item_dict: - item_obj = self.download_item_dict[item_id] - if item_obj.stage == formats.MAIN_STAGE_QUEUED: - return True - - return False - - - @synchronise(_SYNC_LOCK) - def move_item_to_bottom(self, download_item_obj): - - """Called by mainwin.MainWin.on_progress_list_dl_last(). - - Moves the specified DownloadItem object to the end of - self.download_item_list, so it is assigned a DownloadWorker last - (after all other DownloadItems). - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object to move - - """ - - # Move the item to the bottom (end) of the list - if download_item_obj is None \ - or not download_item_obj.item_id in self.download_item_list: - return - else: - self.download_item_list.append( - self.download_item_list.pop( - self.download_item_list.index(download_item_obj.item_id), - ), - ) - - - @synchronise(_SYNC_LOCK) - def move_item_to_top(self, download_item_obj): - - """Called by mainwin.MainWin.on_progress_list_dl_next(). - - Moves the specified DownloadItem object to the start of - self.download_item_list, so it is the next item to be assigned a - DownloadWorker. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object to move - - """ - - # Move the item to the top (beginning) of the list - if download_item_obj is None \ - or not download_item_obj.item_id in self.download_item_list: - return - else: - self.download_item_list.insert( - 0, - self.download_item_list.pop( - self.download_item_list.index(download_item_obj.item_id), - ), - ) - - - @synchronise(_SYNC_LOCK) - def prevent_fetch_new_items(self): - - """Called by DownloadManager.stop_download_operation_soon(). - - Sets the flag that prevents calls to self.fetch_next_item() from - fetching anything new, which allows the download operation to stop as - soon as any ongoing video downloads have finished. - """ - - self.prevent_fetch_flag = True - - - @synchronise(_SYNC_LOCK) - def set_final_item(self, item_id): - - """Called by mainwin.MainWin.on_progress_list_stop_soon(), etc. - - After the specified DownloadItem object is assigned to a worker, no - more DownloadItem objects are assigned to a worker (i.e. do not start - to check/download). - """ - - if item_id in self.download_item_dict: - self.final_item_id = item_id - - else: - GObject.timeout_add( - 0, - app_obj.system_error, - 318, - _('Unrecognised download item ID'), - ) - - - def reorder_master_slave(self): - - """Called by self.__init__() after the calls to self.create_item() are - finished. - - Some media data objects have an alternate download destination, for - example, a playlist ('slave') might download its videos into the - directory used by a channel ('master'). - - This can increase the length of the operation, because a 'slave' won't - start until its 'master' is finished. - - Make sure all designated 'masters' are handled before 'slaves' (a media - media data object can't be both a master and a slave). - - Even if this doesn't reduce the time the 'slaves' spend waiting to - start, it at least makes the download order predictable. - """ - - master_list = [] - other_list = [] - for item_id in self.download_item_list: - download_item_obj = self.download_item_dict[item_id] - - if isinstance(download_item_obj.media_data_obj, media.Video) \ - or not download_item_obj.media_data_obj.slave_dbid_list: - other_list.append(item_id) - else: - master_list.append(item_id) - - self.download_item_list = [] - self.download_item_list.extend(master_list) - self.download_item_list.extend(other_list) - - -class DownloadItem(object): - - """Called by downloads.DownloadList.create_item() and - .create_dummy_item(). - - Based on the DownloadItem class in youtube-dl-gui. - - Python class used to track the download status of a media data object - (media.Video, media.Channel, media.Playlist or media.Folder), one of many - in a downloads.DownloadList object. - - Args: - - item_id (int): The number of downloads.DownloadItem objects created, - used to give each one a unique ID - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object to be downloaded. When the - download operation was launched from the Classic Mode tab, a dummy - media.Video object - - scheduled_obj (media.Scheduled): The scheduled download object which - wants to download media_data_obj (None if no scheduled download - applies in this case) - - options_manager_obj (options.OptionsManager): The object which - specifies download options for the media data object - - operation_type (str): The value that applies to this DownloadItem only - (might be different from the default value stored in - DownloadManager.operation_type) - - ignore_limits_flag (bool): Flag set to True if operation limits - (mainapp.TartubeApp.operation_limit_flag) should be ignored - - """ - - - # Standard class methods - - - def __init__(self, item_id, media_data_obj, scheduled_obj, - options_manager_obj, operation_type, ignore_limits_flag): - - # IV list - class objects - # ----------------------- - # The media data object to be downloaded. When the download operation - # was launched from the Classic Mode tab, a dummy media.Video object - self.media_data_obj = media_data_obj - # The scheduled download object which wants to download media_data_obj - # (None if no scheduled download applies in this case) - self.scheduled_obj = scheduled_obj - # The object which specifies download options for the media data object - self.options_manager_obj = options_manager_obj - - # IV list - other - # --------------- - # A unique ID for this object - self.item_id = item_id - # The current download stage - self.stage = formats.MAIN_STAGE_QUEUED - - # The value that applies to this DownloadItem only (might be different - # from the default value stored in DownloadManager.operation_type) - self.operation_type = operation_type - # Shortcut flag to test the operation type; True for 'classic_sim', - # 'classic_real' and 'classic_custom'; False for all other values - self.operation_classic_flag = False # (Set below) - - # Flag set to True if operation limits - # (mainapp.TartubeApp.operation_limit_flag) should be ignored - self.ignore_limits_flag = ignore_limits_flag - - - # Code - # ---- - - # Set the flag - if operation_type == 'classic_sim' \ - or operation_type == 'classic_real' \ - or operation_type == 'classic_custom': - self.operation_classic_flag = True - - - # Set accessors - - - def set_ignore_limits_flag(self): - - """Called by DownloadManager.apply_ignore_limits(), following a call - from mainapp>TartubeApp.script_slow_timer_callback().""" - - self.ignore_limits_flag = True - - -class VideoDownloader(object): - - """Called by downloads.DownloadWorker.run_video_downloader() or - .run_stream_downloader(). - - Based on the YoutubeDLDownloader class in youtube-dl-gui. - - Python class to create a system child process. Uses the child process to - instruct youtube-dl to download all videos associated with the URL - described by a downloads.DownloadItem object (which might be an individual - video, or a channel or playlist). - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - Sets self.return_code to a value in the range 0-5, described below. The - parent downloads.DownloadWorker object checks that return code once this - object's child process has finished. - - Args: - - download_manager_obj (downloads.DownloadManager): The download manager - object handling the entire download operation - - download_worker_obj (downloads.DownloadWorker): The parent download - worker object. The download manager uses multiple workers to - implement simultaneous downloads. The download manager checks for - free workers and, when it finds one, assigns it a - download.DownloadItem object. When the worker is assigned a - download item, it creates a new instance of this object to - interface with youtube-dl, and waits for this object to return a - return code - - download_item_obj (downloads.DownloadItem): The download item object - describing the URL from which youtube-dl should download video(s) - - force_sim_flag (bool): Set to True when called by - .run_stream_downloader(), in which case a simulated download rather - than a real download is performed, regardless of other settings - - Warnings: - - The calling function is responsible for calling the close() method - when it's finished with this object, in order for this object to - properly close down. - - """ - - - # Attributes - - - # Valid values for self.return_code. The larger the number, the higher in - # the hierarchy of return codes. - # Codes lower in the hierarchy (with a smaller number) cannot overwrite - # higher in the hierarchy (with a bigger number) - # - # 0 - The download operation completed successfully - OK = 0 - # 1 - A warning occured during the download operation - WARNING = 1 - # 2 - An error occured during the download operation - ERROR = 2 - # 3 - The corresponding url video file was larger or smaller from the given - # filesize limit - FILESIZE_ABORT = 3 - # 4 - The video(s) for the specified URL have already been downloaded - ALREADY = 4 - # 5 - The download operation was stopped by the user - STOPPED = 5 - # 6 - The download operation has stalled. The parent worker can restart it, - # if required - STALLED = -1 - - - # Standard class methods - - - def __init__(self, download_manager_obj, download_worker_obj, \ - download_item_obj, force_sim_flag=False): - - # IV list - class objects - # ----------------------- - # The downloads.DownloadManager object handling the entire download - # operation - self.download_manager_obj = download_manager_obj - # The parent downloads.DownloadWorker object - self.download_worker_obj = download_worker_obj - # The downloads.DownloadItem object describing the URL from which - # youtube-dl should download video(s) - self.download_item_obj = download_item_obj - - # The child process created by self.create_child_process() - self.child_process = None - - # Read from the child process STDOUT (i.e. self.child_process.stdout) - # and STDERR (i.e. self.child_process.stderr) in an asynchronous way - # by polling this queue.PriorityQueue object - self.queue = queue.PriorityQueue() - self.stdout_reader = PipeReader(self.queue, 'stdout') - self.stderr_reader = PipeReader(self.queue, 'stderr') - - - # IV list - other - # --------------- - # The current return code, using values in the range 0-5, as described - # above - # The value remains set to self.OK unless we encounter any problems - # The larger the number, the higher in the hierarchy of return codes. - # Codes lower in the hierarchy (with a smaller number) cannot - # overwrite higher in the hierarchy (with a bigger number) - self.return_code = self.OK - # The time (in seconds) between iterations of the loop in - # self.do_download() - self.sleep_time = 0.1 - # The time (in seconds) to wait for an existing download, which shares - # a common download destination with this media data object, to - # finish downloading - self.long_sleep_time = 10 - - # The time (matches time.time() ) at which the first youtube-dl - # network error was detected. Reset back to None when the download - # resumes. If the download does not resume quickly enough - # (according to settings), then this download is marked as stalled, - # and can be restarted, if settings require that - self.network_error_time = None - - # Flag set to True if we are simulating downloads for this media data - # object, or False if we actually downloading videos (updated below) - self.dl_sim_flag = force_sim_flag - # Flag set to True if this download operation was launched from the - # Classic Mode tab, False if not (set below) - self.dl_classic_flag = False - - # Flag set to True by a call from any function to self.stop_soon() - # After being set to True, this VideoDownloader should give up after - # the next call to self.confirm_new_video(), .confirm_old_video() - # .confirm_sim_video() - self.stop_soon_flag = False - # Exception: after the FFmpeg "Merging formats into..." message, wait - # for the merge to complete before giving up - self.stop_after_merge_flag = False - # When self.stop_soon_flag is True, the next call to - # self.confirm_new_video(), .confirm_old_video() or - # .confirm_sim_video() sets this flag to True, informing - # self.do_download() that it can stop the child process - self.stop_now_flag = False - - # youtube-dl is passed a URL, which might represent an individual - # video, a channel or a playlist - # Assume it's an individual video unless youtube-dl reports a - # channel or playlist (in which case, we can update these IVs later) - # For real downloads, self.video_num is the current video number, and - # self.video_total is the number of videos in the channel/playlist - # For simulated downloads, both IVs are set to the number of - # videos actually found - self.video_num = 0 - self.video_total = 0 - # When the 'Downloading webpage' message is detected, denoting the - # start of a real (not simulated) download, this IV is set to the - # video's ID. The value is reset when self.confirm_new_video() etc - # is called, or when an error/warning with a different video ID is - # detected - # When set, any youtube-dl errors/warnings which do not specify their - # own video ID can be assumed to belong to this video - self.probable_video_id = None - # self.extract_stdout_data() detects the completion of a download job - # in one of several ways - # The first time it happens for each individual video, - # self.extract_stdout_data() takes action. It calls - # self.confirm_new_video(), self.confirm_old_video() or - # self.confirm_sim_video() when required - # On subsequent occasions, the completion message is ignored (as - # youtube-dl may pass us more than one completion message for a - # single video) - # There is one exception: in calls to self.confirm_new_video, a - # subsequent call to self.confirm_new_video() updates the file - # extension of the media.Video. (yt-dlp and/or FFmpeg may send - # several completion messages as it converts one file format to - # another; the final one is the one we want) - # Dictionary of videos, used to check for the first completion message - # for each unique video - # Dictionary in the form - # key = the video number (matches self.video_num) - # value = the media.Video object created - self.video_check_dict = {} - # The code imported from youtube-dl-gui doesn't recognise a downloaded - # video, if FFmpeg isn't used to extract it (because FFmpeg is not - # installed, or because the website doesn't support it, or whatever) - # In this situation, youtube-dl's STDOUT messages don't definitively - # establish when it has finished downloading a video - # When a file destination is announced; it is temporarily stored in - # these IVs. When STDOUT receives a message in the form - # [download] 100% of 2.06MiB in 00:02 - # ...and the filename isn't one that FFmpeg would use (e.g. - # 'myvideo.f136.mp4' or 'myvideo.f136.m4a', then assume that the - # video has finished downloading - self.temp_path = None - self.temp_filename = None - self.temp_extension = None - - # When checking a channel/playlist, this number is incremented every - # time youtube-dl gives us the details of a video which the Tartube - # database already contains (with a minimum number of IVs already - # set) - # When downloading a channel/playlist, this number is incremented every - # time youtube-dl gives us a 'video already downloaded' message - # (unless the Tartube database hasn't actually marked the video as - # downloaded) - # Every time the value is incremented, we check the limits specified by - # mainapp.TartubeApp.operation_check_limit or - # .operation_download_limit. If the limit has been reached, we stop - # checking/downloading the channel/playlist - # No check is carried out if self.download_item_obj represents an - # individual media.Video object (and not a whole channel or playlist) - self.video_limit_count = 0 - # Git issue #9 describes youtube-dl failing to download the video's - # JSON metadata. We can't do anything about the youtube-dl code, but - # we can apply our own timeout - # This IV is set whenever self.confirm_sim_video() is called. After - # being set, if a certain time has passed without another call to - # self.confirm_sim_video, self.do_download() halts the child process - # The time to wait is specified by mainapp.TartubeApp IVs - # .json_timeout_no_comments_time and .json_timeout_with_comments_time - self.last_sim_video_check_time = None - - # If mainapp.TartubeApp.operation_convert_mode is set to any value - # other than 'disable', then a media.Video object whose URL - # represents a channel/playlist is converted into multiple new - # media.Video objects, one for each video actually downloaded - # Flag set to True when self.download_item_obj.media_data_obj is a - # media.Video object, but a channel/playlist is detected (regardless - # of the value of mainapp.TartubeApp.operation_convert_mode) - self.url_is_not_video_flag = False - - # Buffer for youtube-dl error/warning messages that can be associated - # with a particular video ID - # The corresponding media.Video object might not exist at the time the - # error/warning is processed, so it is temporarily stored here, so - # that the parent downloads.DownloadWorker can retrieve it - # Dictionary in the form - # key = The video ID (corresponds to media.Video.vid) - # value = A list in the form - # [ [type, message], [type, message] ... ] - # ...where 'type' is the string 'error' or 'warning', and 'message' - # is the error/warning generated - self.video_msg_buffer_dict = {} - # Errors/warnings for individual media.Video objects requires special - # handling. We can't predict where, in the check/download process, - # the first error/warning will occur - # Dictionary of videos which have been assigned an error/warning - # by this instance of the VideoDownloader. The first error/warning - # removes any errors/warnings generated by previous operations. - # The call to self.confirm_new_video(), .confirm_old_video() and - # .confirm_sim_video() removes any errors/warnings generated by - # previous operations by consulting this dictionary - # Dictionary in the form - # key = The video ID (corresponds to media.Video.vid) - # value = True (not required) - self.video_error_warning_dict = {} - - # For channels/playlists, a list of child media.Video objects, used to - # track missing videos (when required) - self.missing_video_check_list = [] - # Flag set to True (for convenience) if the list is populated - self.missing_video_check_flag = False - - - # Code - # ---- - # Initialise IVs depending on whether this is a real or simulated - # download - media_data_obj = self.download_item_obj.media_data_obj - - # All media data objects can be marked as simulate downloads only - # (except when the download operation was launched from the Classic - # Mode tab) - # The setting applies not just to the media data object, but all of its - # descendants - if not self.download_item_obj.operation_classic_flag: - - if ( - force_sim_flag \ - or self.download_item_obj.operation_type == 'sim' \ - or self.download_item_obj.operation_type == 'custom_sim' - ): - dl_sim_flag = True - else: - dl_sim_flag = media_data_obj.dl_sim_flag - parent_obj = media_data_obj.parent_obj - - while not dl_sim_flag and parent_obj is not None: - dl_sim_flag = parent_obj.dl_sim_flag - parent_obj = parent_obj.parent_obj - - if dl_sim_flag: - self.dl_sim_flag = True - else: - self.dl_sim_flag = False - - else: - - self.dl_classic_flag = True - if self.download_item_obj.operation_type == 'classic_sim': - self.dl_sim_flag = True - - # If the user wants to detect missing videos in channels/playlists - # (those that have been downloaded by the user, but since removed - # from the website by the creator), set that up - if ( - isinstance(media_data_obj, media.Channel) \ - or isinstance(media_data_obj, media.Playlist) - ) and ( - self.download_item_obj.operation_type == 'real' \ - or self.download_item_obj.operation_type == 'sim' - ) and download_manager_obj.app_obj.track_missing_videos_flag: - - # Compile a list of child videos. Videos can be removed from the - # list as they are detected - self.missing_video_check_list = media_data_obj.child_list.copy() - if self.missing_video_check_list: - self.missing_video_check_flag = True - - - # Public class methods - - - def do_download(self): - - """Called by downloads.DownloadWorker.run_video_downloader(). - - Based on YoutubeDLDownloader.download(). - - Downloads video(s) from a URL described by self.download_item_obj. - - Return values: - - The final return code, a value in the range 0-5 (as described - above) - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Set the default return code. Everything is OK unless we encounter - # any problems - self.set_return_code(self.OK) - - if not self.dl_classic_flag: - - # Reset the errors/warnings stored in the media data object, the - # last time it was checked/downloaded - self.download_item_obj.media_data_obj.reset_error_warning() - if isinstance( - self.download_item_obj.media_data_obj, - media.Video, - ): - self.download_item_obj.media_data_obj.set_block_flag(False) - - else: - # If two channels/playlists/folders share a download - # destination, we don't want to download both of them at the - # same time - # If this media data obj shares a download destination with - # another downloads.DownloadWorker, wait until that download - # has finished before starting this one - while self.download_manager_obj.check_master_slave( - self.download_item_obj.media_data_obj, - ): - time.sleep(self.long_sleep_time) - - # Prepare a system command... - options_obj = self.download_worker_obj.options_manager_obj - if options_obj.options_dict['direct_cmd_flag']: - - cmd_list = utils.generate_direct_system_cmd( - app_obj, - self.download_item_obj.media_data_obj, - options_obj, - ) - - else: - - divert_mode = None - if ( - self.download_item_obj.operation_type == 'custom_real' \ - or self.download_item_obj.operation_type == 'classic_custom' - ) and isinstance( - self.download_item_obj.media_data_obj, - media.Video, - ) and self.download_manager_obj.custom_dl_obj: - divert_mode \ - = self.download_manager_obj.custom_dl_obj.divert_mode - - cmd_list = utils.generate_ytdl_system_cmd( - app_obj, - self.download_item_obj.media_data_obj, - self.download_worker_obj.options_list, - self.dl_sim_flag, - self.dl_classic_flag, - self.missing_video_check_flag, - self.download_manager_obj.custom_dl_obj, - divert_mode, - ) - - # ...display it in the Output tab (if required)... - if app_obj.ytdl_output_system_cmd_flag: - app_obj.main_win_obj.output_tab_write_system_cmd( - self.download_worker_obj.worker_id, - ' '.join(cmd_list), - ) - - # ...and the terminal (if required) - if app_obj.ytdl_write_system_cmd_flag: - print(' '.join(cmd_list)) - - # ...and the downloader log (if required) - if app_obj.ytdl_log_system_cmd_flag: - app_obj.write_downloader_log(' '.join(cmd_list)) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # While downloading the media data object, update the callback function - # with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # Apply the JSON timeout, if required - if app_obj.apply_json_timeout_flag \ - and self.last_sim_video_check_time is not None \ - and self.last_sim_video_check_time < time.time(): - # Halt the child process, which stops checking this channel/ - # playlist - self.stop() - - GObject.timeout_add( - 0, - app_obj.system_error, - 303, - 'Enforced timeout because downloader took too long to' \ - + ' fetch a video\'s JSON data', - ) - - # If a download has stalled (there has been no activity for some - # time), halt the child process (allowing the parent worker to - # restart the stalled download, if required) - if app_obj.operation_auto_restart_flag \ - and self.network_error_time is not None: - - restart_time = app_obj.operation_auto_restart_time * 60 - if (self.network_error_time + restart_time) < time.time(): - - # Stalled download. Stop the child process - self.stop() - - # Pass a dictionary of values to downloads.DownloadWorker, - # confirming the result of the job. The values are passed - # on to the main window - self.set_return_code(self.STALLED) - self.last_data_callback() - - return self.return_code - - # Stop this video downloader, if required to do so, having just - # finished checking/downloading a video - if self.stop_now_flag: - self.stop() - - # The child process has finished - # We also set the return code to self.ERROR if the download didn't - # start or if the child process return code is greater than 0 - # Original notes from youtube-dl-gui: - # NOTE: In Linux if the called script is just empty Python exits - # normally (ret=0), so we can't detect this or similar cases - # using the code below - # NOTE: In Unix a negative return code (-N) indicates that the child - # was terminated by signal N (e.g. -9 = SIGKILL) - internal_msg = None - if self.child_process is None: - self.set_return_code(self.ERROR) - internal_msg = _('Download did not start') - - elif self.child_process.returncode > 0: - self.set_return_code(self.ERROR) - if not app_obj.ignore_child_process_exit_flag: - internal_msg = _( - 'Child process exited with non-zero code: {}', - ).format(self.child_process.returncode) - - if internal_msg: - - # (The message must be visible in the Errors/Warnings tab, the - # Output tab, terminal and/or downloader log) - self.set_error( - self.download_item_obj.media_data_obj, - internal_msg, - ) - - if app_obj.ytdl_output_stderr_flag: - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - internal_msg, - ) - - if app_obj.ytdl_write_stderr_flag: - print(internal_msg) - - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(internal_msg) - - # For channels/playlists, detect missing videos (those downloaded by - # the user, but since deleted from the website by the creator) - # We only perform the check if the process completed without errors, - # and was not halted early by the user (or halted by the download - # manager, because too many videos have been downloaded) - # We also ignore livestreams - detected_list = [] - - if app_obj.track_missing_videos_flag \ - and self.missing_video_check_list \ - and self.download_manager_obj.running_flag \ - and not self.stop_soon_flag \ - and not self.stop_now_flag \ - and self.return_code <= self.WARNING \ - and self.video_num > 0: - for check_obj in self.missing_video_check_list: - if check_obj.dbid in app_obj.media_reg_dict \ - and check_obj.dl_flag \ - and not check_obj.live_mode: - - # Filter out videos that are too old - if ( - app_obj.track_missing_time_flag \ - and app_obj.track_missing_time_days > 0 - ): - # Convert the video's upload time from seconds to days - days = check_obj.upload_time / (60 * 60 * 24) - if days <= app_obj.track_missing_time_days: - - # Mark this video as missing - detected_list.append(check_obj) - - else: - - # Mark this video as missing - detected_list.append(check_obj) - - for detected_obj in detected_list: - app_obj.mark_video_missing( - detected_obj, - True, # Video is missing - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the parent channel/playlist - ) - - # Pass a dictionary of values to downloads.DownloadWorker, confirming - # the result of the job. The values are passed on to the main - # window - self.last_data_callback() - - # Pass the result back to the parent downloads.DownloadWorker object - return self.return_code - - - def check_dl_is_correct_type(self): - - """Called by self.extract_stdout_data(). - - When youtube-dl reports the URL associated with the download item - object contains multiple videos (or potentially contains multiple - videos), then the URL represents a channel or playlist, not a video. - - This function checks whether a channel/playlist is about to be - downloaded into a media.Video object. If so, it takes action to prevent - that from happening. - - The action taken depends on the value of - mainapp.TartubeApp.operation_convert_mode. - - Return values: - - False if a channel/playlist was about to be downloaded into a - media.Video object, which has since been replaced by a new - media.Channel/media.Playlist object - - True in all other situations (including when a channel/playlist was - about to be downloaded into a media.Video object, which was - not replaced by a new media.Channel/media.Playlist object) - - """ - - # Special case: if the download operation was launched from the - # Classic Mode tab, there is no need to do anything - if self.dl_classic_flag: - return True - - # Otherwise, import IVs (for convenience) - app_obj = self.download_manager_obj.app_obj - media_data_obj = self.download_item_obj.media_data_obj - - if isinstance(self.download_item_obj.media_data_obj, media.Video): - - # If the mode is 'disable', or if it the original media.Video - # object is contained in a channel or a playlist, then we must - # stop downloading this URL immediately - if app_obj.operation_convert_mode == 'disable' \ - or not isinstance( - self.download_item_obj.media_data_obj.parent_obj, - media.Folder, - ): - self.url_is_not_video_flag = True - - # Stop downloading this URL - self.stop() - self.set_error( - media_data_obj, - '\'' + media_data_obj.name + '\' ' + _( - 'This video has a URL that points to a channel or a' \ - + ' playlist, not a video', - ), - ) - - # Don't allow self.confirm_sim_video() to be called - return False - - # Otherwise, we can create new media.Video objects for each - # video downloaded/checked. The new objects may be placd into a - # new media.Channel or media.Playlist object - elif not self.url_is_not_video_flag: - - self.url_is_not_video_flag = True - - # Mark the original media.Video object to be destroyed at the - # end of the download operation - self.download_manager_obj.mark_video_as_doomed(media_data_obj) - - if app_obj.operation_convert_mode != 'multi': - - # Create a new media.Channel or media.Playlist object and - # add it to the download manager - # Then halt this job, so the new channel/playlist object - # can be downloaded - self.convert_video_to_container() - - # Don't allow self.confirm_sim_video() to be called - return False - - # Do allow self.confirm_sim_video() to be called - return True - - - def close(self): - - """Can be called by anything. - - Destructor function for this object. - """ - - # Tell the PipeReader objects to shut down, thus joining their threads - self.stdout_reader.join() - self.stderr_reader.join() - - - def confirm_archived_video(self, filename): - - """Called by self.extract_stdout_data(). - - A modified version of self.confirm_old_video(), called when - youtube-dl's 'has already been recorded in archive' message is detected - (but only when checking for missing videos). - - Tries to find a match for the video name and, if one is found, marks it - as not missing. - - Args: - - filename (str): The video name, which should match the .name of a - media.Video object in self.missing_video_check_list - - """ - - # Create shortcut variables (for convenience) - app_obj = self.download_manager_obj.app_obj - media_data_obj = self.download_item_obj.media_data_obj - - # media_data_obj is a media.Channel or media.Playlist object. Check its - # child objects, looking for a matching video - match_obj = media_data_obj.find_matching_video(app_obj, filename) - if match_obj and match_obj in self.missing_video_check_list: - self.missing_video_check_list.remove(match_obj) - - - def confirm_new_video(self, dir_path, filename, extension, \ - merge_flag=False): - - """Called by self.extract_stdout_data(). - - A successful download is announced in one of several ways. - - When an announcement is detected, this function is called. Use the - first announcement to update self.video_check_dict. For subsequent - announcements, only a media.Video's file extension is updated (see the - comments in self.__init__() ). - - Args: - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - merge_flag (bool): True if this function was called as the result - of a 'Merging formats into...' message - - """ - - # Create shortcut variables (for convenience) - app_obj = self.download_manager_obj.app_obj - media_data_obj = self.download_item_obj.media_data_obj - - # Error/warning handling for individual videos - video_obj = None - - # Special case: don't add videos to the Tartube database at all - if not isinstance(media_data_obj, media.Video) \ - and media_data_obj.dl_no_db_flag: - - # Deal with the video description, JSON data and thumbnail, - # according to the settings in options.OptionsManager - utils.handle_files_after_download( - app_obj, - self.download_worker_obj.options_manager_obj, - dir_path, - filename, - ) - - # Register the download with DownloadManager, so that download - # limits can be applied, if required - self.download_manager_obj.register_video('new') - - # Special case: if the download operation was launched from the - # Classic Mode tab, then we only need to update the dummy - # media.Video object, and to move/remove description/metadata/ - # thumbnail files, as appropriate - elif self.dl_classic_flag: - - self.confirm_new_video_classic_mode(dir_path, filename, extension) - - # All other cases - elif not self.video_num in self.video_check_dict: - - # Create a new media.Video object for the video - if self.url_is_not_video_flag: - - video_obj = app_obj.convert_video_from_download( - self.download_item_obj.media_data_obj.parent_obj, - self.download_item_obj.options_manager_obj, - dir_path, - filename, - extension, - True, # Don't sort parent containers yet - ) - - else: - - video_obj = app_obj.create_video_from_download( - self.download_item_obj, - dir_path, - filename, - extension, - True, # Don't sort parent containers yet - ) - - # If downloading from a channel/playlist, remember the video's - # index. (The server supplies an index even for a channel, and - # the user might want to convert a channel to a playlist) - if self.video_num > 0 and ( - isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist) - ): - video_obj.set_index(self.video_num) - - # Contact SponsorBlock server to fetch video slice data - if app_obj.custom_sblock_mirror != '' \ - and app_obj.sblock_fetch_flag \ - and video_obj.vid != None \ - and (not video_obj.slice_list or app_obj.sblock_replace_flag): - utils.fetch_slice_data( - app_obj, - video_obj, - self.download_worker_obj.worker_id, - True, # Write to terminal/log, if allowed - ) - - # Update the main window - GObject.timeout_add( - 0, - app_obj.announce_video_download, - self.download_item_obj, - video_obj, - utils.compile_mini_options_dict( - self.download_worker_obj.options_manager_obj, - ), - ) - - # Register the download with DownloadManager, so that download - # limits can be applied, if required - self.download_manager_obj.register_video('new') - - # Update the checklist - if self.video_num > 0: - self.video_check_dict[self.video_num] = video_obj - - elif self.video_num > 0: - - # Update the video's file extension, in case one file format has - # been converted to another (with a new call to this function - # each time) - video_obj = self.video_check_dict[self.video_num] - - if video_obj.file_ext is None \ - or (extension is not None and video_obj.file_ext != extension): - video_obj.set_file_ext(extension) - - # The probable video ID, if captured, can now be reset - self.probable_video_id = None - - if video_obj: - - # If no errors/warnings were received during this operation, - # errors/warnings that already exist (from previous operations) - # can now be cleared - if not video_obj.dbid in self.video_error_warning_dict: - video_obj.reset_error_warning() - - # This confirmation clears a video marked as blocked - video_obj.set_block_flag(False) - - # This VideoDownloader can now stop, if required to do so after a video - # has been checked/downloaded - if self.stop_soon_flag: - if merge_flag and not self.stop_now_flag: - self.stop_after_merge_flag = True - else: - self.stop_now_flag = True - - - def confirm_new_video_classic_mode(self, dir_path, filename, extension): - - """Called by self.confirm_new_video() when a download operation was - launched from the Classic Mode tab. - - Handles the download. - - Args: - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Update the dummy media.Video object - dummy_obj = self.download_item_obj.media_data_obj - - dummy_obj.set_dl_flag(True) - dummy_obj.set_dummy_path( - os.path.abspath(os.path.join(dir_path, filename + extension)), - ) - - # Contact SponsorBlock server to fetch video slice data - if app_obj.custom_sblock_mirror != '' \ - and app_obj.sblock_fetch_flag \ - and dummy_obj.vid != None \ - and (not dummy_obj.slice_list or app_obj.sblock_replace_flag): - utils.fetch_slice_data( - app_obj, - dummy_obj, - self.download_worker_obj.worker_id, - True, # Write to terminal/log, if allowed - ) - - # Deal with the video description, JSON data and thumbnail, according - # to the settings in options.OptionsManager - utils.handle_files_after_download( - app_obj, - self.download_worker_obj.options_manager_obj, - dir_path, - filename, - ) - - # Register the download with DownloadManager, so that download limits - # can be applied, if required - self.download_manager_obj.register_video('new') - - # The probable video ID, if captured, can now be reset - self.probable_video_id = None - - - def confirm_old_video(self, dir_path, filename, extension): - - """Called by self.extract_stdout_data(). - - When youtube-dl reports a video has already been downloaded, make sure - the media.Video object is marked as downloaded, and upate the main - window if necessary. - - Args: - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - """ - - # Create shortcut variables (for convenience) - app_obj = self.download_manager_obj.app_obj - media_data_obj = self.download_item_obj.media_data_obj - - # Error/warning handling for individual videos - if isinstance(media_data_obj, media.Video): - video_obj = media_data_obj - else: - video_obj = None - - # Special case: don't add videos to the Tartube database at all - if video_obj is None and media_data_obj.dl_no_db_flag: - - # Register the download with DownloadManager, so that download - # limits can be applied, if required - self.download_manager_obj.register_video('old') - - # Special case: if the download operation was launched from the - # Classic Mode tab, then we only need to update the dummy - # media.Video object - elif self.dl_classic_flag: - - media_data_obj.set_dl_flag(True) - media_data_obj.set_dummy_path( - os.path.abspath(os.path.join(dir_path, filename + extension)), - ) - - # Register the download with DownloadManager, so that download - # limits can be applied, if required - self.download_manager_obj.register_video('old') - - # All other cases - elif video_obj: - - if not media_data_obj.dl_flag: - - GObject.timeout_add( - 0, - app_obj.mark_video_downloaded, - video_obj, - True, # Video is downloaded - True, # Video is not new - ) - - else: - - # media_data_obj is a media.Channel or media.Playlist object. Check - # its child objects, looking for a matching video - match_obj = media_data_obj.find_matching_video(app_obj, filename) - if match_obj: - - # This video will not be marked as a missing video - if match_obj in self.missing_video_check_list: - self.missing_video_check_list.remove(match_obj) - - if not match_obj.dl_flag: - - GObject.timeout_add( - 0, - app_obj.mark_video_downloaded, - match_obj, - True, # Video is downloaded - True, # Video is not new - ) - - else: - - # Register the download with DownloadManager, so that - # download limits can be applied, if required - self.download_manager_obj.register_video('old') - - # This video applies towards the limit (if any) specified - # by mainapp.TartubeApp.operation_download_limit - self.video_limit_count += 1 - - if not isinstance( - self.download_item_obj.media_data_obj, - media.Video, - ) \ - and not self.download_item_obj.ignore_limits_flag \ - and app_obj.operation_limit_flag \ - and app_obj.operation_download_limit \ - and self.video_limit_count >= \ - app_obj.operation_download_limit: - # Limit reached; stop downloading videos in this - # channel/playlist - self.stop() - - else: - - # No match found, so create a new media.Video object for the - # video file that already exists on the user's filesystem - video_obj = app_obj.create_video_from_download( - self.download_item_obj, - dir_path, - filename, - extension, - ) - - if self.video_num > 0: - self.video_check_dict[self.video_num] = video_obj - - # Update the main window - if media_data_obj.external_dir is not None \ - and media_data_obj.master_dbid != media_data_obj.dbid: - - # The container is storing its videos in another - # container's sub-directory, which (probably) explains - # why we couldn't find a match. Don't add anything to the - # Results List - GObject.timeout_add( - 0, - app_obj.announce_video_clone, - video_obj, - ) - - else: - - # Do add an entry to the Results List (as well as updating - # the Video Catalogue, as normal) - GObject.timeout_add( - 0, - app_obj.announce_video_download, - self.download_item_obj, - video_obj, - utils.compile_mini_options_dict( - self.download_worker_obj.options_manager_obj, - ), - ) - - # Register the download with DownloadManager, so that - # download limits can be applied, if required - self.download_manager_obj.register_video('new') - - # The probable video ID, if captured, can now be reset - self.probable_video_id = None - - if video_obj: - - # If no errors/warnings were received during this operation, - # errors/warnings that already exist (from previous operations) - # can now be cleared - if not video_obj.dbid in self.video_error_warning_dict: - video_obj.reset_error_warning() - - # This confirmation clears a video marked as blocked - video_obj.set_block_flag(False) - - # This VideoDownloader can now stop, if required to do so after a video - # has been checked/downloaded - if self.stop_soon_flag: - self.stop_now_flag = True - - - def confirm_sim_video(self, json_dict): - - """Called by self.extract_stdout_data(). - - After a successful simulated download, youtube-dl presents us with JSON - data for the video. Use that data to update everything. - - Args: - - json_dict (dict): JSON data from STDOUT, converted into a python - dictionary - - """ - - # Import the main application and download list (for convenience) - app_obj = self.download_manager_obj.app_obj - dl_list_obj = self.download_manager_obj.download_list_obj - # Call self.stop(), if the limit described in the comments for - # self.__init__() have been reached - stop_flag = False - - # Set the time at which a JSON timeout should be applied, if no more - # calls to this function have been made - if app_obj.apply_json_timeout_flag: - - if (self.dl_sim_flag and app_obj.check_comment_fetch_flag) \ - or (not self.dl_sim_flag and app_obj.dl_comment_fetch_flag): - wait_secs = app_obj.json_timeout_with_comments_time * 60 - else: - wait_secs = app_obj.json_timeout_no_comments_time * 60 - - self.last_sim_video_check_time = int(time.time()) + wait_secs - - # From the JSON dictionary, extract the data we need - # Git #177 reports that this value might be 'None', so check for that - if '_filename' in json_dict \ - and json_dict['_filename'] is not None: - full_path = json_dict['_filename'] - path, filename, extension = self.extract_filename(full_path) - else: - GObject.timeout_add( - 0, - app_obj.system_error, - 304, - 'Missing filename in JSON data', - ) - - return - - # (Git #322, 'upload_date' might be None) - if 'upload_date' in json_dict \ - and json_dict['upload_date'] is not None: - - try: - # date_string in form YYYYMMDD - date_string = json_dict['upload_date'] - dt_obj = datetime.datetime.strptime(date_string, '%Y%m%d') - upload_time = dt_obj.timestamp() - except: - upload_time = None - - else: - upload_time = None - - if 'duration' in json_dict: - duration = json_dict['duration'] - else: - duration = None - - if 'title' in json_dict: - name = json_dict['title'] - else: - name = None - - if 'id' in json_dict: - vid = json_dict['id'] - - chapter_list = [] - if 'chapters' in json_dict: - chapter_list = json_dict['chapters'] - - if 'description' in json_dict: - descrip = json_dict['description'] - else: - descrip = None - - if 'thumbnail' in json_dict: - thumbnail = json_dict['thumbnail'] - else: - thumbnail = None - -# if 'webpage_url' in json_dict: -# source = json_dict['webpage_url'] -# else: -# source = None - # !!! DEBUG: yt-dlp Git #119: filter out the extraneous characters at - # the end of the URL, if present - if 'webpage_url' in json_dict: - - source = re.sub( - r'\&has_verified\=.*\&bpctr\=.*', - '', - json_dict['webpage_url'], - ) - - else: - source = None - - if 'playlist_index' in json_dict: - playlist_index = json_dict['playlist_index'] - else: - playlist_index = None - - if 'is_live' in json_dict: - if json_dict['is_live']: - live_flag = True - else: - live_flag = False - else: - live_flag = False - - if 'was_live' in json_dict: - if json_dict['was_live']: - was_live_flag = True - else: - was_live_flag = False - else: - was_live_flag = False - - if 'comments' in json_dict: - comment_list = json_dict['comments'] - else: - comment_list = [] - - if 'playlist_id' in json_dict: - playlist_id = json_dict['playlist_id'] - if 'playlist_title' in json_dict: - playlist_title = json_dict['playlist_title'] - else: - playlist_title = None - else: - playlist_id = None - - if 'subtitles' in json_dict and json_dict['subtitles']: - subs_flag = True - else: - subs_flag = False - - # Does an existing media.Video object match this video? - media_data_obj = self.download_item_obj.media_data_obj - video_obj = None - - if self.url_is_not_video_flag: - - # media_data_obj has a URL which represents a channel or playlist, - # but media_data_obj itself is a media.Video object - # media_data_obj's parent is a media.Folder object. Check its - # child objects, looking for a matching video - # (video_obj is set to None, if no match is found) - video_obj = media_data_obj.parent_obj.find_matching_video( - app_obj, - filename, - ) - - if not video_obj: - video_obj = media_data_obj.parent_obj.find_matching_video( - app_obj, - name, - ) - - elif isinstance(media_data_obj, media.Video): - - # media_data_obj is a media.Video object - video_obj = media_data_obj - - else: - - # media_data_obj is a media.Channel or media.Playlist object. Check - # its child objects, looking for a matching video - # (video_obj is set to None, if no match is found) - video_obj = media_data_obj.find_matching_video(app_obj, filename) - if not video_obj: - video_obj = media_data_obj.find_matching_video(app_obj, name) - - new_flag = False - update_results_flag = False - if not video_obj: - - # Special case: during the checking phase of a custom download, - # don't create a new media.Video object if the video has no - # available subtitles (if that's what the settings in the - # CustomDLManager specify) - if ( - self.download_item_obj.operation_type == 'custom_sim' \ - or self.download_item_obj.operation_type == 'classic_sim' - ) and dl_list_obj.custom_dl_obj \ - and dl_list_obj.custom_dl_obj.dl_by_video_flag \ - and dl_list_obj.custom_dl_obj.dl_precede_flag \ - and dl_list_obj.custom_dl_obj.dl_if_subs_flag \ - and dl_list_obj.custom_dl_obj.ignore_if_no_subs_flag \ - and ( - not subs_flag \ - or ( - dl_list_obj.custom_dl_obj.dl_if_subs_list \ - and not utils.match_subs( - dl_list_obj.custom_dl_obj, - list(json_dict['subtitles'].keys), - ) - ) - ): - return - - # No matching media.Video object found, so create a new one - new_flag = True - update_results_flag = True - - if self.url_is_not_video_flag: - - video_obj = app_obj.convert_video_from_download( - self.download_item_obj.media_data_obj.parent_obj, - self.download_item_obj.options_manager_obj, - path, - filename, - extension, - # Don't sort parent container objects yet; wait for - # mainwin.MainWin.results_list_update_row() to do it - True, - ) - - else: - - video_obj = app_obj.create_video_from_download( - self.download_item_obj, - path, - filename, - extension, - True, - ) - - # Update its IVs with the JSON information we extracted - if filename is not None: - video_obj.set_name(filename) - - if name is not None: - video_obj.set_nickname(name) - elif filename is not None: - video_obj.set_nickname(filename) - - if vid is not None: - video_obj.set_vid(vid) - - if upload_time is not None: - video_obj.set_upload_time(upload_time) - - if duration is not None: - video_obj.set_duration(duration) - - if source is not None: - video_obj.set_source(source) - - if chapter_list: - video_obj.extract_timestamps_from_chapters( - app_obj, - chapter_list, - ) - - if descrip is not None: - video_obj.set_video_descrip( - app_obj, - descrip, - app_obj.main_win_obj.descrip_line_max_len, - ) - - if was_live_flag: - video_obj.set_was_live_flag(True) - - if comment_list and app_obj.comment_store_flag: - video_obj.set_comments(comment_list) - - if app_obj.store_playlist_id_flag \ - and playlist_id is not None \ - and not isinstance(video_obj.parent_obj, media.Folder): - video_obj.parent_obj.set_playlist_id( - playlist_id, - playlist_title, - ) - - if subs_flag: - video_obj.extract_subs_list(json_dict['subtitles']) - - app_obj.extract_parent_name_from_metadata( - video_obj, - json_dict, - ) - - if isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist): - # 'Enhanced' websites only: set the channel/playlist RSS feed, - # if not already set - video_obj.parent_obj.update_rss_from_json(json_dict) - - # If downloading from a channel/playlist, remember the video's - # index. (The server supplies an index even for a channel, - # and the user might want to convert a channel to a playlist) - video_obj.set_index(playlist_index) - - # Now we can sort the parent containers - video_obj.parent_obj.sort_children(app_obj) - app_obj.fixed_all_folder.sort_children(app_obj) - if video_obj.bookmark_flag: - app_obj.fixed_bookmark_folder.sort_children(app_obj) - if video_obj.fav_flag: - app_obj.fixed_fav_folder.sort_children(app_obj) - if video_obj.live_mode: - app_obj.fixed_live_folder.sort_children(app_obj) - if video_obj.missing_flag: - app_obj.fixed_missing_folder.sort_children(app_obj) - if video_obj.new_flag: - app_obj.fixed_new_folder.sort_children(app_obj) - if video_obj in app_obj.fixed_recent_folder.child_list: - app_obj.fixed_recent_folder.sort_children(app_obj) - if video_obj.waiting_flag: - app_obj.fixed_waiting_folder.sort_children(app_obj) - - else: - - # This video will not be marked as a missing video - if video_obj in self.missing_video_check_list: - self.missing_video_check_list.remove(video_obj) - - if video_obj.file_name \ - and video_obj.name != app_obj.default_video_name: - - # This video must not be displayed in the Results List, and - # counts towards the limit (if any) specified by - # mainapp.TartubeApp.operation_check_limit - self.video_limit_count += 1 - - if not isinstance( - self.download_item_obj.media_data_obj, - media.Video, - ) \ - and not self.download_item_obj.ignore_limits_flag \ - and app_obj.operation_limit_flag \ - and app_obj.operation_check_limit \ - and self.video_limit_count >= app_obj.operation_check_limit: - # Limit reached. When we reach the end of this function, - # stop checking videos in this channel/playlist - stop_flag = True - - # The call to DownloadManager.register_video() below doesn't - # take account of this situation, so make our own call - self.download_manager_obj.register_video('other') - - else: - - # This video must be displayed in the Results List, and counts - # towards the limit (if any) specified by - # mainapp.TartubeApp.autostop_videos_value - update_results_flag = True - - # If the 'Add videos' button was used, the path/filename/extension - # won't be set yet - if not video_obj.file_name and full_path: - video_obj.set_file(filename, extension) - - # Update any video object IVs that are not set - if video_obj.name == app_obj.default_video_name \ - and filename is not None: - video_obj.set_name(filename) - - if video_obj.nickname == app_obj.default_video_name: - if name is not None: - video_obj.set_nickname(name) - elif filename is not None: - video_obj.set_nickname(filename) - - if not video_obj.vid and vid is not None: - video_obj.set_vid(vid) - - if not video_obj.upload_time and upload_time is not None: - video_obj.set_upload_time(upload_time) - - if not video_obj.duration and duration is not None: - video_obj.set_duration(duration) - - if not video_obj.source and source is not None: - video_obj.set_source(source) - - if chapter_list: - video_obj.extract_timestamps_from_chapters( - app_obj, - chapter_list, - ) - - if not video_obj.descrip and descrip is not None: - video_obj.set_video_descrip( - app_obj, - descrip, - app_obj.main_win_obj.descrip_line_max_len, - ) - - if was_live_flag: - video_obj.set_was_live_flag(True) - - if not video_obj.comment_list \ - and comment_list \ - and app_obj.comment_store_flag: - video_obj.set_comments(comment_list) - - if app_obj.store_playlist_id_flag \ - and playlist_id is not None \ - and not isinstance(video_obj.parent_obj, media.Folder): - video_obj.parent_obj.set_playlist_id( - playlist_id, - playlist_title, - ) - - if subs_flag: - video_obj.extract_subs_list(json_dict['subtitles']) - - app_obj.extract_parent_name_from_metadata( - video_obj, - json_dict, - ) - - if isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist): - # 'Enhanced' websites only: set the channel/playlist RSS feed, - # if not already set - video_obj.parent_obj.update_rss_from_json(json_dict) - - # If downloading from a channel/playlist, remember the video's - # index. (The server supplies an index even for a channel, - # and the user might want to convert a channel to a playlist) - video_obj.set_index(playlist_index) - - # Deal with livestreams - if video_obj.live_mode != 2 and live_flag: - - GObject.timeout_add( - 0, - app_obj.mark_video_live, - video_obj, - 2, # Livestream is broadcasting - {}, # No livestream data - True, # Don't update Video Index yet - True, # Don't update Video Catalogue yet - ) - - elif video_obj.live_mode != 0 and not live_flag: - - GObject.timeout_add( - 0, - app_obj.mark_video_live, - video_obj, - 0, # Livestream has finished - {}, # Reset any livestream data - True, # Don't update Video Index yet - True, # Don't update Video Catalogue yet - ) - - # Deal with the video description, JSON data and thumbnail, according - # to the settings in options.OptionsManager - options_dict \ - = self.download_worker_obj.options_manager_obj.options_dict - - if descrip and options_dict['write_description']: - - descrip_path = os.path.abspath( - os.path.join(path, filename + '.description'), - ) - - if not options_dict['sim_keep_description']: - - descrip_path = utils.convert_path_to_temp( - app_obj, - descrip_path, - ) - - # (Don't replace a file that already exists, and obviously don't - # do anything if the call returned None because of a filesystem - # error) - if descrip_path is not None and not os.path.isfile(descrip_path): - - try: - fh = open(descrip_path, 'wb') - fh.write(descrip.encode('utf-8')) - fh.close() - - if options_dict['move_description']: - utils.move_metadata_to_subdir( - app_obj, - video_obj, - '.description', - ) - - except: - pass - - if options_dict['write_info']: - - json_path = os.path.abspath( - os.path.join(path, filename + '.info.json'), - ) - - if not options_dict['sim_keep_info']: - json_path = utils.convert_path_to_temp(app_obj, json_path) - - if json_path is not None and not os.path.isfile(json_path): - - try: - with open(json_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - - if options_dict['move_info']: - utils.move_metadata_to_subdir( - app_obj, - video_obj, - '.info.json', - ) - - except: - pass - - # v2.1.101 - Annotations were removed by YouTube in 2019, so this - # feature is not available, and will not be available until the - # authors have some annotations to test -# if options_dict['write_annotations']: -# -# xml_path = os.path.abspath( -# os.path.join(path, filename + '.annotations.xml'), -# ) -# -# if not options_dict['sim_keep_annotations']: -# xml_path = utils.convert_path_to_temp(app_obj, xml_path) - - if thumbnail and options_dict['write_thumbnail']: - - # Download the thumbnail, if we don't already have it - # The thumbnail's URL is something like - # 'https://i.ytimg.com/vi/abcdefgh/maxresdefault.jpg' - # When saved to disc by youtube-dl, the file is given the same name - # as the video (but with a different extension) - # Get the thumbnail's extension... - remote_file, remote_ext = os.path.splitext(thumbnail) - # Fix for Odysee videos, whose thumbnail extension is not specified - # in the .info.json file - if remote_ext == '': - remote_ext = '.webp' - - # ...and thus get the filename used by youtube-dl when storing the - # thumbnail locally - thumb_path = video_obj.get_actual_path_by_ext(app_obj, remote_ext) - - if not options_dict['sim_keep_thumbnail']: - thumb_path = utils.convert_path_to_temp(app_obj, thumb_path) - - if thumb_path is not None and not os.path.isfile(thumb_path): - - # v2.0.013 The requests module fails if the connection drops - # v1.2.006 Writing the file fails if the directory specified - # by thumb_path doesn't exist - # Use 'try' so that neither problem is fatal - try: - request_obj = requests.get( - thumbnail, - timeout = app_obj.request_get_timeout, - ) - - with open(thumb_path, 'wb') as outfile: - outfile.write(request_obj.content) - - except: - pass - - # Convert .webp thumbnails to .jpg, if required - thumb_path = utils.find_thumbnail_webp_intact_or_broken( - app_obj, - video_obj, - ) - if thumb_path is not None \ - and not app_obj.ffmpeg_fail_flag \ - and app_obj.ffmpeg_convert_webp_flag \ - and not app_obj.ffmpeg_manager_obj.convert_webp(thumb_path): - - app_obj.set_ffmpeg_fail_flag(True) - GObject.timeout_add( - 0, - app_obj.system_error, - 305, - app_obj.ffmpeg_fail_msg, - ) - - # Move to the sub-directory, if required - if options_dict['move_thumbnail']: - - utils.move_thumbnail_to_subdir(app_obj, video_obj) - - # Contact SponsorBlock server to fetch video slice data - if app_obj.custom_sblock_mirror != '' \ - and app_obj.sblock_fetch_flag \ - and video_obj.vid != None \ - and (not video_obj.slice_list or app_obj.sblock_replace_flag): - utils.fetch_slice_data( - app_obj, - video_obj, - self.download_worker_obj.worker_id, - True, # Write to terminal/log, if allowed - ) - - # If a new media.Video object was created (or if a video whose name is - # unknown, now has a name), add a line to the Results List, as well - # as updating the Video Catalogue - # The True argument passes on the download options 'move_description', - # etc, but not 'keep_description', etc - if update_results_flag: - - GObject.timeout_add( - 0, - app_obj.announce_video_download, - self.download_item_obj, - video_obj, - # No call to utils.compile_mini_options_dict(), because this - # function deals with download options like - # 'move_description' by itself - {}, - ) - - else: - - # Otherwise, just update the Video Catalogue - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # For simulated downloads, self.do_download() has not displayed - # anything in the Output tab/terminal window/downloader log; so do - # that now (if required) - if app_obj.ytdl_output_stdout_flag: - - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - '[' + video_obj.parent_obj.name + '] <' \ - + _('Simulated download of:') + ' \'' + filename + '\'>', - ) - - if app_obj.ytdl_write_stdout_flag: - - # v2.2.039 Partial fix for Git #106, #115 and #175, for which we - # get a Python error when print() receives unicode characters - filename = filename.encode().decode( - utils.get_encoding(), - 'replace', - ) - - try: - - print( - '[' + video_obj.parent_obj.name + '] <' \ - + _('Simulated download of:') + ' \'' + filename + '\'>', - ) - - except: - - print( - '[' + video_obj.parent_obj.name + '] <' \ - + _( - 'Simulated download of video with unprintable characters', - ) + '>', - ) - - if app_obj.ytdl_log_stdout_flag: - - app_obj.write_downloader_log( - '[' + video_obj.parent_obj.name + '] <' \ - + _('Simulated download of:') + ' \'' + filename + '\'>', - ) - - # If a new media.Video object was created (or if a video whose name is - # unknown, now has a name), register the simulated download with - # DownloadManager, so that download limits can be applied, if - # required - if update_results_flag: - self.download_manager_obj.register_video('sim') - - if video_obj: - - # If no errors/warnings were received during this operation, - # errors/warnings that already exist (from previous operations) - # can now be cleared - if not video_obj.dbid in self.video_error_warning_dict: - video_obj.reset_error_warning() - - # This confirmation clears a video marked as blocked - video_obj.set_block_flag(False) - - # Stop checking videos in this channel/playlist, if a limit has been - # reached - if stop_flag: - self.stop() - - # This VideoDownloader can now stop, if required to do so after a video - # has been checked/downloaded - elif self.stop_soon_flag: - self.stop_now_flag = True - - - def convert_video_to_container(self): - - """Called by self.check_dl_is_correct_type(). - - Creates a new media.Channel or media.Playlist object to replace an - existing media.Video object. The new object is given some of the - properties of the old one. - - This function doesn't destroy the old object; DownloadManager.run() - handles that. - """ - - app_obj = self.download_manager_obj.app_obj - old_video_obj = self.download_item_obj.media_data_obj - container_obj = old_video_obj.parent_obj - - # Some media.Folder objects cannot contain channels or playlists (for - # example, the 'Unsorted Videos' folder) - # If that is the case, the new channel/playlist is created without a - # parent. Otherwise, it is created at the same location as the - # original media.Video object - if container_obj.restrict_mode != 'open': - container_obj = None - - # Decide on a name for the new channel/playlist, e.g. 'channel_1' or - # 'playlist_4'. The name must not already be in use. The user can - # customise the name when they're ready - name = utils.find_available_name( - app_obj, - # e.g. 'channel' - app_obj.operation_convert_mode, - # Allow 'channel_1', if available - 1, - ) - - # (Prevent any possibility of an infinite loop by giving up after some - # thousands of attempts) - name = None - new_container_obj = None - - for n in range (1, 9999): - test_name = app_obj.operation_convert_mode + '_' + str(n) - if not app_obj.is_container(test_name): - name = test_name - break - - if name is not None: - - # Create the new channel/playlist. Very unlikely that the old - # media.Video object has its .dl_sim_flag set, but we'll use it - # nonetheless - if app_obj.operation_convert_mode == 'channel': - - new_container_obj = app_obj.add_channel( - name, - container_obj, # May be None - source = old_video_obj.source, - dl_sim_flag = old_video_obj.dl_sim_flag, - ) - - else: - - new_container_obj = app_obj.add_playlist( - name, - container_obj, # May be None - source = old_video_obj.source, - dl_sim_flag = old_video_obj.dl_sim_flag, - ) - - if new_container_obj is None: - - # New channel/playlist could not be created (for some reason), so - # stop downloading from this URL - self.stop() - self.set_error( - media_data_obj, - '\'' + media_data_obj.name + '\' ' + _( - 'This video has a URL that points to a channel or a' \ - + ' playlist, not a video', - ), - ) - - else: - - # Update IVs for the new channel/playlist object - new_container_obj.set_options_obj(old_video_obj.options_obj) - new_container_obj.set_source(old_video_obj.source) - - # Add the new channel/playlist to the Video Index (but don't - # select it) - app_obj.main_win_obj.video_index_add_row(new_container_obj, True) - - # Add the new channel/playlist to the download manager's list of - # things to download... - new_download_item_obj \ - = self.download_manager_obj.download_list_obj.create_item( - new_container_obj, - self.download_item_obj.scheduled_obj, - self.download_item_obj.operation_type, - False, # priority_flag - self.download_item_obj.ignore_limits_flag, - ) - # ...and add a row the Progress List - app_obj.main_win_obj.progress_list_add_row( - new_download_item_obj.item_id, - new_download_item_obj.media_data_obj, - ) - - # Stop this download job, allowing the replacement one to start - self.stop() - - - def create_child_process(self, cmd_list): - - """Called by self.do_download() immediately after the call to - utils.generate_ytdl_system_cmd(). - - Based on YoutubeDLDownloader._create_process(). - - Executes the system command, creating a new child process which - executes youtube-dl. - - Sets self.return_code in the event of an error. - - Args: - - cmd_list (list): Python list that contains the command to execute - - """ - - # Strip double quotes from arguments - # (Since we're sending the system command one argument at a time, we - # don't need to retain the double quotes around any single argument - # and, in fact, doing so would cause an error) - cmd_list = utils.strip_double_quotes(cmd_list) - - # Create the child process - info = preexec = None - - if os.name == 'nt': - # Hide the child process window that MS Windows helpfully creates - # for us - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - # Make this child process the process group leader, so that we can - # later kill the whole process group with os.killpg - preexec = os.setsid - - try: - self.child_process = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=preexec, - startupinfo=info, - ) - - except (ValueError, OSError) as error: - # (There is no need to update the media data object's error list, - # as the code in self.do_download() will notice the child - # process didn't start, and set its own error message) - self.set_return_code(self.ERROR) - - - def extract_filename(self, input_data): - - """Called by self.confirm_sim_video() and .extract_stdout_data(). - - Based on the extract_data() function in youtube-dl-gui's - downloaders.py. - - Extracts various components of a filename. - - Args: - - input_data (str): Full path to a file which has been downloaded - and saved to the filesystem - - Return values: - - Returns the path, filename and extension components of the full - file path. - - """ - - path, fullname = os.path.split(input_data.strip("\"")) - filename, extension = os.path.splitext(fullname) - - return path, filename, extension - - - def extract_stdout_data(self, stdout): - - """Called by self.read_child_process(). - - Based on the extract_data() function in youtube-dl-gui's - downloaders.py. - - Extracts youtube-dl statistics from the child process. - - Args: - - stdout (str): String that contains a line from the child process - STDOUT (i.e., a message from youtube-dl) - - Return values: - - Python dictionary in a standard format also used by the main window - code. Dictionaries in this format are generally called - 'dl_stat_dict' (or some variation of it). - - The returned dictionary can be empty if there is no data to - extract, otherwise it contains one or more of the following keys: - - 'status' : Contains the status of the download - 'path' : Destination path - 'filename' : The filename without the extension - 'extension' : The file extension - 'percent' : The percentage of the video being downloaded - 'eta' : Estimated time for the completion of the - download - 'speed' : Download speed - 'filesize' : The size of the video file being downloaded - 'playlist_index' : The playlist index of the current video file - being downloaded - 'playlist_size' : The number of videos in the playlist - 'dl_sim_flag' : Flag set to True if we are simulating downloads - for this media data object, or False if we - actually downloading videos (set below) - - """ - - # Import the media data object (for convenience) - media_data_obj = self.download_item_obj.media_data_obj - - # Initialise the dictionary with default key-value pairs for the main - # window to display, to be overwritten (if possible) with new key- - # value pairs as this function interprets the STDOUT message - dl_stat_dict = { - 'playlist_index': self.video_num, - 'playlist_size': self.video_total, - 'dl_sim_flag': self.dl_sim_flag, - } - - # If STDOUT has not been received by this function, then the main - # window can be passed just the default key-value pairs - if not stdout: - return dl_stat_dict - - # In some cases, we want to preserve the multiple successive whitespace - # characters in the STDOUT message, in order to extract filenames - # in their original form - # In other cases, we just eliminate multiple successive whitespace - # characters - stdout_with_spaces_list = stdout.split(' ') - stdout_list = stdout.split() - - # The '[download] XXX has already been recorded in the archive' - # message does not cause a call to self.confirm_new_video(), etc, - # so we must handle it here - # (Note that the first word might be '[download]', or '[Youtube]', etc) - if self.missing_video_check_flag: - - match = re.search( - r'^\[\w+\]\s(.*)\shas already been recorded in (the )?' \ - + 'archive$', - stdout, - ) - - if match: - self.confirm_archived_video(match.group(1)) - self.download_manager_obj.register_video('other') - return dl_stat_dict - - # Likewise for the frame messages from youtube-dl direct downloads - match = re.search( - r'^frame.*size\=\s*([\S]+).*bitrate\=\s*([\S]+)', - stdout, - ) - if match: - dl_stat_dict['filesize'] = match.groups()[0] - dl_stat_dict['speed'] = match.groups()[1] - return dl_stat_dict - - # Extract the data - stdout_list[0] = stdout_list[0].lstrip('\r') - if stdout_list[0] == '[download]': - - dl_stat_dict['status'] = formats.ACTIVE_STAGE_DOWNLOAD - if self.network_error_time is not None: - self.network_error_time = None - - # Get path, filename and extension - if stdout_list[1] == 'Destination:': - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[2:]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - - # v2.3.013 - the path to the subtitles file is being mistaken - # for the path to the video file here. Only use the - # destination if the path is a recognised video/audio format - # (and if we don't already have it) - short_ext = extension[1:] - if self.temp_path is None \ - and ( - short_ext in formats.VIDEO_FORMAT_LIST \ - or short_ext in formats.AUDIO_FORMAT_LIST - ): - self.set_temp_destination(path, filename, extension) - - # Get progress information - if '%' in stdout_list[1]: - if stdout_list[1] != '100%': - - # Old format, e.g. - # [download] 27.0% of 7.55MiB at 73.63KiB/s ETA 01:16 - if stdout_list[3] != '~': - dl_stat_dict['percent'] = stdout_list[1] - dl_stat_dict['eta'] = stdout_list[7] - dl_stat_dict['speed'] = stdout_list[5] - dl_stat_dict['filesize'] = stdout_list[3] - # New format (approx December 2022), e.g. - # [download] 8.5% of ~ 19.87MiB at 2.35MiB/s ETA 00:07 - # (frag 8/94) - else: - dl_stat_dict['percent'] = stdout_list[1] - dl_stat_dict['eta'] = stdout_list[8] - dl_stat_dict['speed'] = stdout_list[6] - dl_stat_dict['filesize'] = stdout_list[4] - - else: - dl_stat_dict['percent'] = '100%' - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - dl_stat_dict['filesize'] = stdout_list[3] - - # If the most recently-received filename isn't one used by - # FFmpeg, then this marks the end of a video download - # (See the comments in self.__init__) - if len(stdout_list) > 4 \ - and stdout_list[4] == 'in' \ - and self.temp_filename is not None \ - and not re.search(r'^.*\.f\d{1,3}$', self.temp_filename): - - self.confirm_new_video( - self.temp_path, - self.temp_filename, - self.temp_extension, - ) - - self.reset_temp_destination() - - # Get playlist information (when downloading a channel or a - # playlist, this line is received once per video) - # youtube-dl 'Downloading video n of n' - # yt-dlp: 'Downloading item n of n' - if stdout_list[1] == 'Downloading' \ - and (stdout_list[2] == 'video' or stdout_list[2] == 'item') \ - and stdout_list[4] == 'of': - dl_stat_dict['playlist_index'] = int(stdout_list[3]) - self.video_num = int(stdout_list[3]) - dl_stat_dict['playlist_size'] = int(stdout_list[5]) - self.video_total = int(stdout_list[5]) - - # If youtube-dl is about to download a channel or playlist into - # a media.Video object, decide what to do to prevent it - if not self.dl_classic_flag: - self.check_dl_is_correct_type() - - # Remove the 'and merged' part of the STDOUT message when using - # FFmpeg to merge the formats - if stdout_list[-3] == 'downloaded' and stdout_list[-1] == 'merged': - stdout_list = stdout_list[:-2] - stdout_with_spaces_list = stdout_with_spaces_list[:-2] - - dl_stat_dict['percent'] = '100%' - - # Get file already downloaded status - if stdout_list[-1] == 'downloaded': - - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[1:-4]), - ) - - # v2.3.013 - same problem as above - short_ext = extension[1:] - if short_ext in formats.VIDEO_FORMAT_LIST \ - or short_ext in formats.AUDIO_FORMAT_LIST: - - dl_stat_dict['status'] = formats.COMPLETED_STAGE_ALREADY - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.reset_temp_destination() - - self.confirm_old_video(path, filename, extension) - - # Get filesize abort status - if stdout_list[-1] == 'Aborting.': - dl_stat_dict['status'] = formats.ERROR_STAGE_ABORT - - elif stdout_list[0] == '[hlsnative]': - - # Get information from the native HLS extractor (see - # https://github.com/rg3/youtube-dl/blob/master/youtube_dl/ - # downloader/hls.py#L54 - dl_stat_dict['status'] = formats.ACTIVE_STAGE_DOWNLOAD - - if len(stdout_list) == 7: - segment_no = float(stdout_list[6]) - current_segment = float(stdout_list[4]) - - # Get the percentage - percent = '{0:.1f}%'.format(current_segment / segment_no * 100) - dl_stat_dict['percent'] = percent - - # youtube-dl uses [ffmpeg], yt-dlp uses [Merger] or [ExtractAudio] - elif stdout_list[0] == '[ffmpeg]' \ - or stdout_list[0] == '[Merger]' \ - or stdout_list[0] == '[ExtractAudio]': - - # Using FFmpeg, not the the native HLS extractor - # A successful video download is announced in one of several ways. - # Use the first announcement to update self.video_check_dict, and - # ignore subsequent announcements - dl_stat_dict['status'] = formats.ACTIVE_STAGE_POST_PROCESS - - # Get the final file extension after the merging process has - # completed - if stdout_list[1] == 'Merging': - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[4:]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.reset_temp_destination() - - self.confirm_new_video(path, filename, extension, True) - - # Get the final file extension after simple FFmpeg post-processing - # (i.e. not after a file merge) - elif stdout_list[1] == 'Destination:': - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[2:]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.reset_temp_destination() - - self.confirm_new_video(path, filename, extension) - - # Get final file extension after the recoding process - elif stdout_list[1] == 'Converting': - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[8:]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.reset_temp_destination() - - self.confirm_new_video(path, filename, extension) - - elif ( - isinstance(media_data_obj, media.Channel) - and not media_data_obj.rss \ - and stdout_list[0] == '[youtube:channel]' \ - ) or ( - isinstance(media_data_obj, media.Playlist) \ - and not media_data_obj.rss \ - and stdout_list[0] == '[youtube:playlist]' \ - and stdout_list[2] == 'Downloading' \ - and stdout_list[3] == 'webpage' - ): - # YouTube only: set the channel/playlist RSS feed, if not already - # set, first removing the final colon that should be there - # (This is the old method of setting the RSS; no longer necessary - # as of v2.3.602, but retained in case it is useful in the - # future) - container_id = re.sub(r'\:*$', '', stdout_list[1]) - media_data_obj.update_rss_from_id(container_id) - - elif ( - not self.dl_sim_flag \ - and stdout_list[2] == 'Downloading' \ - and stdout_list[3] == 'webpage' \ - and re.search(r'^\[[^\]\:]+\]', stdout_list[0]) \ - ): - # (The re.search() above excludes [youtube:channel] and - # [youtube:playlist], etc) - self.probable_video_id = re.sub(r'\:*$', '', stdout_list[1]) - - elif ( - stdout_list[0] == 'Deleting' \ - and stdout_list[1] == 'original' \ - and stdout_list[2] == 'file' \ - and self.stop_after_merge_flag \ - ): - # (We were waiting for an FFmpeg to finish, before stopping the - # download) - self.stop_now_flag = True - self.stop_after_merge_flag = False - return dl_stat_dict - - elif stdout_list[0][0] == '{': - - # JSON data, the result of a simulated download. Convert to a - # python dictionary - if self.dl_sim_flag: - - # (Try/except to check for invalid JSON) - try: - json_dict = json.loads(stdout) - - except: - GObject.timeout_add( - 0, - self.download_manager_obj.app_obj.system_error, - 306, - 'Invalid JSON data received from server', - ) - - return dl_stat_dict - - if json_dict: - - # For some Classic Mode custom downloads, Tartube performs - # two consecutive download operations: one simulated - # download to fetch URLs of individual videos, and - # another to download each video separately - # If we're on the first operation, the dummy media.Video - # object's URL may represent an individual video, or a - # channel or playlist - # In both cases, we simply make a list of each video - # detected, along with its metadata, ready for the - # second operation - if self.download_item_obj.operation_type == 'classic_sim': - - # (If the URL can't be retrieved for any reason, then - # just ignore this batch of JSON) - if 'webpage_url' in json_dict: - self.download_manager_obj.register_classic_url( - self.download_item_obj.media_data_obj, - json_dict, - ) - - # If youtube-dl is about to download a channel or playlist - # into a media.Video object, decide what to do to prevent - # that - # The called function returns a True/False value, - # specifically to allow this code block to call - # self.confirm_sim_video when required - # v1.3.063 At this point, self.video_num can be 0 for a URL - # that's an individual video, but > 0 for a URL that's - # actually a channel/playlist - elif not self.video_num \ - or self.check_dl_is_correct_type(): - self.confirm_sim_video(json_dict) - - self.video_num += 1 - dl_stat_dict['playlist_index'] = self.video_num - self.video_total += 1 - dl_stat_dict['playlist_size'] = self.video_total - - dl_stat_dict['status'] = formats.ACTIVE_STAGE_CHECKING - - elif stdout_list[0][0] != '[' or stdout_list[0] == '[debug]': - - # (Just ignore this output) - return dl_stat_dict - - else: - - # The download has started - dl_stat_dict['status'] = formats.ACTIVE_STAGE_PRE_PROCESS - - return dl_stat_dict - - - def extract_stdout_status(self, dl_stat_dict): - - """Called by self.read_child_process() immediately after a call to - self.extract_stdout_data(). - - Based on YoutubeDLDownloader._extract_info(). - - If the job's status is formats.COMPLETED_STAGE_ALREADY or - formats.ERROR_STAGE_ABORT, translate that into a new value for the - return code, and then use that value to actually set self.return_code - (which halts the download). - - Args: - - dl_stat_dict (dict): The Python dictionary returned by the call to - self.extract_stdout_data(), in the standard form described by - the comments for that function - - """ - - if 'status' in dl_stat_dict: - if dl_stat_dict['status'] == formats.COMPLETED_STAGE_ALREADY: - self.set_return_code(self.ALREADY) - dl_stat_dict['status'] = None - - if dl_stat_dict['status'] == formats.ERROR_STAGE_ABORT: - self.set_return_code(self.FILESIZE_ABORT) - dl_stat_dict['status'] = None - - - def is_blocked(self, stderr): - - """Called by self.register_error_warning(). - - See if a STDERR message indicates a video that is censored, age- - restricted or otherwise unavailable for download. - - Args: - - stderr (str): A message from the child process STDERR - - Return values: - - True if the video is blocked, False if not - - """ - - # N.B. These strings also appear in self.is_ignorable() - regex_list = [ - 'Content Warning', - 'This video may be inappropriate for some users', - 'Sign in to confirm your age', - 'This video contains content from.*copyright grounds', - 'This video requires payment to watch', - 'The uploader has not made this video available', - ] - - for regex in regex_list: - - if re.search(r'\s*(\S*)\:\s' + regex, stderr): - return True - - # Not blocked - return None - - - def is_child_process_alive(self): - - """Called by self.do_download() and self.stop(). - - Based on YoutubeDLDownloader._proc_is_alive(). - - Called continuously during the self.do_download() loop to check whether - the child process has finished or not. - - Return values: - - True if the child process is alive, otherwise returns False - - """ - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def is_debug(self, stderr): - - """Called by self.do_download(). - - Based on YoutubeDLDownloader._is_warning(). - - After the child process has terminated with an error of some kind, - checks the STERR message to see if it's an error or just a debug - message (generated then youtube-dl verbose output is turned on). - - Args: - - stderr (str): A message from the child process STDERR - - Return values: - - True if the STDERR message is a youtube-dl debug message, False if - it's an error - - """ - - return stderr.split(' ')[0] == '[debug]' - - - def is_ignorable(self, stderr): - - """Called by self.register_error_warning(). - - Before testing a STDERR message, see if it's one of the frequent - messages which the user has opted to ignore (if any). - - Args: - - stderr (str): A message from the child process STDERR - - Return values: - - True if the STDERR message is ignorable, False if it should be - tested further - - """ - - app_obj = self.download_manager_obj.app_obj - media_data_obj = self.download_item_obj.media_data_obj - - if ( - app_obj.ignore_http_404_error_flag \ - and ( - re.search( - r'unable to download video data\: HTTP Error 404', - stderr, - ) or re.search( - r'Unable to extract video data', - stderr, - ) - ) - ) or ( - app_obj.ignore_data_block_error_flag \ - and re.search(r'Did not get any data blocks', stderr) - ) or ( - app_obj.ignore_merge_warning_flag \ - and re.search( - r'Requested formats are incompatible for merge', - stderr, - ) - ) or ( - app_obj.ignore_missing_format_error_flag \ - and re.search( - r'No video formats found; please report this issue', - stderr, - ) - ) or ( - app_obj.ignore_no_annotations_flag \ - and re.search( - r'There are no annotations to write', - stderr, - ) - ) or ( - app_obj.ignore_no_subtitles_flag \ - and re.search( - r'video doesn\'t have subtitles', - stderr, - ) - ) or ( - app_obj.ignore_page_given_flag \ - and re.search( - r'A channel.user page was given', - stderr, - ) - ) or ( - app_obj.ignore_no_descrip_flag \ - and re.search( - r'There.s no playlist description to write', - stderr, - ) - ) or ( - app_obj.ignore_thumb_404_flag \ - and re.search( - r'Unable to download video thumbnail.*HTTP Error 404', - stderr, - ) - ) or ( - app_obj.ignore_yt_age_restrict_flag \ - and ( - re.search( - r'Content Warning', - stderr, - ) or re.search( - r'This video may be inappropriate for some users', - stderr, - ) or re.search( - r'Sign in to confirm your age', - stderr, - ) - ) - ) or ( - app_obj.ignore_yt_copyright_flag \ - and ( - re.search( - r'This video contains content from.*copyright grounds', - stderr, - ) or re.search( - r'Sorry about that\.', - stderr, - ) - ) - ) or ( - app_obj.ignore_yt_payment_flag \ - and re.search( - r'This video requires payment to watch', - stderr, - ) - - ) or ( - app_obj.ignore_yt_uploader_deleted_flag \ - and ( - re.search( - r'The uploader has not made this video available', - stderr, - ) - ) - ): - # This message is ignorable - return True - - # Check the custom list of messages - for item in app_obj.ignore_custom_msg_list: - if ( - (not app_obj.ignore_custom_regex_flag) \ - and stderr.find(item) > -1 - ) or ( - app_obj.ignore_custom_regex_flag and re.search(item, stderr) - ): - # This message is ignorable - return True - - # This message is not ignorable - return False - - - def is_network_error(self, stderr): - - """Called by self.read_child_process(). - - Try to detect network errors, indicating a stalled download. - - youtube-dl's output is system-dependent, so this function may not - detect every type of network error. - - Args: - - stderr (str): A message from the child process STDERR - - Return values: - - True if the STDERR message seems to be a network error, False if it - should be tested further - - """ - - if re.search(r'[Uu]nable to download video data', stderr) \ - or re.search(r'[Uu]nable to download webpage', stderr) \ - or re.search(r'[Nn]ame or service not known', stderr) \ - or re.search(r'urlopen error', stderr) \ - or re.search(r'Got server HTTP error', stderr): - return True - else: - return False - - - def is_warning(self, stderr): - - """Called by self.do_download(). - - Based on YoutubeDLDownloader._is_warning(). - - After the child process has terminated with an error of some kind, - checks the STERR message to see if it's an error or just a warning. - - Args: - - stderr (str): A message from the child process STDERR - - Return values: - - True if the STDERR message is a warning, False if it's an error - - """ - - return stderr.split(':')[0] == 'WARNING' - - - def last_data_callback(self): - - """Called by self.read_child_process(). - - Based on YoutubeDLDownloader._last_data_hook(). - - After the child process has finished, creates a new Python dictionary - in the standard form described by self.extract_stdout_data(). - - Sets key-value pairs in the dictonary, then passes it to the parent - downloads.DownloadWorker object, confirming the result of the child - process. - - The new key-value pairs are used to update the main window. - """ - - dl_stat_dict = {} - - if self.return_code == self.OK: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_FINISHED - elif self.return_code == self.ERROR: - dl_stat_dict['status'] = formats.MAIN_STAGE_ERROR - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - elif self.return_code == self.WARNING: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_WARNING - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - elif self.return_code == self.STOPPED: - dl_stat_dict['status'] = formats.ERROR_STAGE_STOPPED - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - elif self.return_code == self.ALREADY: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_ALREADY - elif self.return_code == self.STALLED: - dl_stat_dict['status'] = formats.MAIN_STAGE_STALLED - else: - dl_stat_dict['status'] = formats.ERROR_STAGE_ABORT - - # Use some empty values in dl_stat_dict so that the Progress tab - # doesn't show arbitrary data from the last file downloaded - # Exception: in Classic Mode, don't do that for self.ALREADY, otherwise - # the filename will never be visible - if not self.dl_classic_flag or self.return_code != self.ALREADY: - dl_stat_dict['filename'] = '' - dl_stat_dict['extension'] = '' - dl_stat_dict['percent'] = '' - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - dl_stat_dict['filesize'] = '' - - # The True argument shows that this function is the caller - self.download_worker_obj.data_callback(dl_stat_dict, True) - - - def match_vid_or_url(self, media_data_obj, vid, url=None): - - """Called by self.register_error_warning(). - - Tests whether a media.Video object has a specified video ID, or a the - URL expected from that video ID. - - Args: - - media_data_obj (media.Video): The video to test - - vid (str): The video ID - - url (str or None): A URL expected from that video ID, or None if - we don't know how to convert the video ID into a URL - - Return values: - - True if the video matches the video ID or URL, False otherwise - - """ - - if ( - media_data_obj.vid is not None \ - and media_data_obj.vid == vid - ) or ( - media_data_obj.source is not None \ - and media_data_obj.source == url - ): - return True - else: - return False - - - def process_error_warning(self, vid): - - """Called by downloads.DownloadWorker.run_video_downloader() or by any - other code. - - When a youtube-dl error/warning message is received with an - identifiable video ID, the corresponding media.Video object might not - yet exist. - - The error/warning is stored temporarily in self.video_msg_buffer_dict() - until it can be passed on to the media.Video. (If the media.Video still - does not exist, pass it on to the parent channel/playlist instead.) - - Args: - - vid (str): The video ID, a key in self.video_msg_buffer_dict - - """ - - if not vid in self.video_msg_buffer_dict: - - GObject.timeout_add( - 0, - app_obj.system_error, - 307, - 'Missing VID in video error/warning buffer', - ) - - return - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Search for a matching media.Video - video_obj = None - - if isinstance(self.download_item_obj.media_data_obj, media.Video): - - # This should not happen, but handle it anyway - video_obj = self.download_item_obj.media_data_obj - - else: - - for child_obj in self.download_item_obj.media_data_obj.child_list: - - if isinstance(child_obj, media.Video) \ - and child_obj.vid is not None \ - and child_obj.vid == vid: - - video_obj = child_obj - break - - # mini_list is in the form [ msg_type, data ] - for mini_list in self.video_msg_buffer_dict[vid]: - - if video_obj is None: - - # No matching media.Video found; assign the error/warning to - # the parent channel/playlist instead - if mini_list[0] == 'warning': - self.set_return_code(self.WARNING) - self.download_item_obj.media_data_obj.set_warning( - mini_list[1], - ) - - else: - self.set_return_code(self.ERROR) - self.download_item_obj.media_data_obj.set_error( - mini_list[1], - ) - - else: - - if mini_list[0] == 'warning': - self.set_warning(video_obj, mini_list[1]) - else: - self.set_error(video_obj, mini_list[1]) - - # Code in downloads.DownloadWorker.run_video_downloader() - # calls mainwin.MainWin.errors_list_add_operation_msg() for - # the main downloads.DownloadItem and its errors/warnings; - # but for a child video, we have to call it directly - # The True argument means 'display the last error/warning only' - # in case the same video generates several errors - app_obj.main_win_obj.errors_list_add_operation_msg( - video_obj, - True, - ) - - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - - def read_child_process(self): - - """Called by self.do_download(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Return values: - - True if either STDOUT or STDERR were read. None if both queues were - empty, or if STDERR was read and a network error was detected - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.download_manager_obj.app_obj.system_error, - 308, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # On MS Windows we use cp1252, so that Tartube can communicate with the - # Windows console - data = data.decode(utils.get_encoding(), 'replace') - - # youtube-dl livestream downloads are normally handles by - # downloads.StreamDownloader, but in certain circumstances this - # VideoDownloader might be asked to handle them - # When youtube-dl is downloading the livestream directly (i.e. without - # .m3u), it produces a lot of output in STDERR, most of which can be - # ignored, but some of which should be converted to STDOUT - if mini_list[1] == 'stderr': - mod_data = utils.stream_output_is_ignorable(data) - if mod_data is None: - # Ignore whole line - self.queue.task_done() - - return True - elif mod_data != data: - # Ignore the unmatched portion of the line, and convert STDERR - # to STDOUT (so self.extract_stdout_data() can process it as - # normal) - data = mod_data - mini_list[1] = 'stdout' - - # STDOUT - if mini_list[1] == 'stdout': - - # Look out for network errors that indicate a stalled download - # (I'm not sure why this message does not appear in STDERR; - # self.is_network_error() checks for the same pattern) - if app_obj.operation_auto_restart_flag \ - and self.network_error_time is None \ - and re.search('Got server HTTP error', data): - - self.network_error_time = time.time() - - else: - - # Convert download statistics into a python dictionary in a - # standard format, specified in the comments for - # self.extract_stdout_data() - dl_stat_dict = self.extract_stdout_data(data) - # If the job's status is formats.COMPLETED_STAGE_ALREADY or - # formats.ERROR_STAGE_ABORT, set our self.return_code IV - self.extract_stdout_status(dl_stat_dict) - # Pass the dictionary on to self.download_worker_obj so the - # main window can be updated - self.download_worker_obj.data_callback(dl_stat_dict) - - # Show output in the Output tab (if required). For simulated - # downloads, a message is displayed by self.confirm_sim_video() - # instead - if app_obj.ytdl_output_stdout_flag \ - and ( - not app_obj.ytdl_output_ignore_progress_flag \ - or not re.search( - r'^\[download\]\s+[0-9\.]+\%\sof\s.*\sat\s.*\sETA', - data, - ) - ) and ( - not app_obj.ytdl_output_ignore_json_flag \ - or data[:1] != '{' - ): - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - data, - ) - - # Show output in the terminal (if required). For simulated - # downloads, a message is displayed by - # self.confirm_sim_video() instead - if app_obj.ytdl_write_stdout_flag \ - and ( - not app_obj.ytdl_write_ignore_progress_flag \ - or not re.search( - r'^\[download\]\s+[0-9\.]+\%\sof\s.*\sat\s.*\sETA', - data, - ) - ) and ( - not app_obj.ytdl_write_ignore_json_flag \ - or data[:1] != '{' - ): - # Git #175, Japanese text may produce a codec error here, - # despite the .decode() call above - try: - print( - data.encode(utils.get_encoding(), 'replace'), - ) - except: - print('STDOUT text with unprintable characters') - - # Write output to the download log (if required). For simulated - # downloads, a message is displayed by - # self.confirm_sim_video() instead - if app_obj.ytdl_log_stdout_flag \ - and ( - not app_obj.ytdl_log_ignore_progress_flag \ - or not re.search( - r'^\[download\]\s+[0-9\.]+\%\sof\s.*\sat\s.*\sETA', - data, - ) - ) and ( - not app_obj.ytdl_log_ignore_json_flag \ - or data[:1] != '{' - ): - app_obj.write_downloader_log(data) - - # STDERR (ignoring any empty error messages) - elif data != '': - - # Look out for network errors that indicate a stalled download - if app_obj.operation_auto_restart_flag \ - and self.network_error_time is None \ - and self.is_network_error(data): - - self.network_error_time = time.time() - - else: - - # Check for recognised errors/warnings, and update the - # appropriate media data object (immediately, if possible, or - # later otherwise) - self.register_error_warning(data) - - # Show output in the Output tab (if required) - if app_obj.ytdl_output_stderr_flag: - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - data, - ) - - # Show output in the terminal (if required) - if app_obj.ytdl_write_stderr_flag: - # Git #175, Japanese text may produce a codec error here, - # despite the .decode() call above - try: - print(data.encode(utils.get_encoding(), 'replace')) - except: - print('STDERR text with unprintable characters') - - # Write output to the downloader log (if required) - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(data) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def register_error_warning(self, data): - - """Called by self.read_child_process() - - When youtube-dl produces an error or warning (in its STDERR), pass that - error/warning on to the appropriate media data object: the video - responsible, if possible, or the parent channel/playlist if not. - - Args: - - data (str): The error/warning message from the child process STDERR - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Try to identify the video ID that produced the error/warning. As of - # v2.3.453, some error/warning messages contain the video ID, others - # do not - msg_type = None - vid = None - site_name = None - - if self.is_warning(data): - - self.set_return_code(self.WARNING) - msg_type = 'warning' - - # e.g. WARNING: [youtube] abcdefgh: here are no annotations - match = re.search( - r'^WARNING\:\s*\[([^\]]+)\]\s*(\S+)\s*\:', - data, - ) - - if match: - site_name = match.groups()[0] - vid = match.groups()[1] - - elif not self.is_debug(data): - - self.set_return_code(self.ERROR) - msg_type = 'error' - - # e.g. ERROR: [youtube] abcdefgh: Sign in to confirm your age - match = re.search( - r'^ERROR\:\s*\[([^\]]+)\]\s*(\S+)\s*\:', - data, - ) - - if match: - site_name = match.groups()[0] - vid = match.groups()[1] - - if not msg_type: - # Not an error/warning - return - - # If the error/warning marks the video as blocked, we can add it to - # the database (to alert the user about its existence) - new_obj = None - if app_obj.add_blocked_videos_flag \ - and vid is not None \ - and self.is_blocked(data): - - # Check this video is not already in the parent channel/playlist/ - # folder - media_data_obj = self.download_item_obj.media_data_obj - url = utils.convert_enhanced_template_from_json( - 'convert_video_list', - site_name, - { 'video_id': vid }, - ) - - if isinstance(media_data_obj, media.Video): - - if self.match_vid_or_url(media_data_obj, vid, url): - media_data_obj.set_block_flag(True) - - else: - - match_flag = False - for child_obj in media_data_obj.child_list: - - if self.match_vid_or_url(child_obj, vid, url): - match_flag = True - break - - if not match_flag: - - # Video is not in the database, so add it - new_obj = app_obj.add_video(media_data_obj, url) - if new_obj: - new_obj.set_block_flag(True) - new_obj.set_vid(vid) - - # For some reason, YouTube messages giving the (approximate) start time - # of a livestream are written to STDERR - # If the video's ID is recognised, we can update the media.Video - # object. However, we won't add a new video to the database; - # JSONFetcher can do that - if new_obj is None \ - and app_obj.enable_livestreams_flag \ - and vid is not None: - - live_data_dict = utils.extract_livestream_data(data) - if live_data_dict: - - # Check this video is not already in the parent channel/ - # playlist/folder - media_data_obj = self.download_item_obj.media_data_obj - url = utils.convert_enhanced_template_from_json( - 'convert_video_list', - site_name, - { 'video_id': vid }, - ) - - if isinstance(media_data_obj, media.Video): - - if self.match_vid_or_url(media_data_obj, vid, url): - GObject.timeout_add( - 0, - app_obj.mark_video_live, - media_data_obj, - 1, - live_data_dict, - ) - - # (We don't want mainwin.NewbieDialogue to appear in - # this situation) - self.download_manager_obj.register_video('other') - - else: - - for child_obj in media_data_obj.child_list: - - if self.match_vid_or_url(child_obj, vid, url): - GObject.timeout_add( - 0, - app_obj.mark_video_live, - child_obj, - 1, - live_data_dict, - ) - - self.download_manager_obj.register_video('other') - - break - - # Assign the error/warning to a media data object - if not self.is_ignorable(data): - - # If the error/warning is anonymous (does not contain the video - # ID), then we can use the most probable video ID - if vid is None \ - and app_obj.auto_assign_errors_warnings_flag \ - and self.probable_video_id is not None: - vid = self.probable_video_id - - # Decide which media data object should have this error/warning - # assigned to it - if self.dl_classic_flag: - - # During Classic Mode downloads, no point trying to assign - # errors/warnings to dummy media.Video objects in a channel/ - # playlist - if msg_type == 'warning': - self.set_warning( - self.download_item_obj.media_data_obj, - data, - ) - else: - self.set_error( - self.download_item_obj.media_data_obj, - data, - ) - - elif new_obj: - - # We created a new media.Video object just a moment ago, so - # assign the error/warning to it directly - if msg_type == 'warning': - self.set_warning(new_obj, data) - else: - self.set_error(new_obj, data) - - # Code in downloads.DownloadWorker.run_video_downloader() - # calls mainwin.MainWin.errors_list_add_operation_msg() for - # the main downloads.DownloadItem and its errors/warnings; - # but for a child video, we have to call it directly - # The True argument means 'display the last error/warning only' - # in case the same video generates several errors - app_obj.main_win_obj.errors_list_add_operation_msg( - new_obj, - True, - ) - - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - new_obj, - ) - - elif isinstance( - self.download_item_obj.media_data_obj, - media.Video, - ): - # We are downloading a single video, so we don't need the video - # ID (in which case, the error/warning can be assigned to it - # directly) - if msg_type == 'warning': - self.set_warning( - self.download_item_obj.media_data_obj, - data, - ) - else: - self.set_error( - self.download_item_obj.media_data_obj, - data, - ) - - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - self.download_item_obj.media_data_obj, - ) - - elif vid is None: - - # We are downloading a channel/playlist and the video ID is not - # known, so assign the error/warning to the channel/playlist - if msg_type == 'warning': - self.set_warning( - self.download_item_obj.media_data_obj, - data, - ) - else: - self.set_error( - self.download_item_obj.media_data_obj, - data, - ) - - else: - - # The corresponding media.Video object might not exist yet. - # Temporarily store the error/warning in a buffer, so that - # the parent downloads.DownloadWorker can retrieve it - if vid in self.video_msg_buffer_dict: - self.video_msg_buffer_dict[vid].append( [msg_type, data] ) - else: - self.video_msg_buffer_dict[vid] = [ [msg_type, data] ] - - - def set_error(self, media_data_obj, msg): - - """Wrapper for media.Video.set_error(). - - Args: - - media_data_obj (media.Video, media.Channel or media.Playlist): - The media data object to update. Only videos are updated by - this function - - msg (str): The error message for this video - - """ - - if isinstance(media_data_obj, media.Video): - - if not media_data_obj.dbid in self.video_error_warning_dict: - - # The new error is the first error/warning generated during - # this operation; remove any errors/warnings from previous - # operations - media_data_obj.reset_error_warning() - self.video_error_warning_dict[media_data_obj.dbid] = True - - # Set the new error - media_data_obj.set_error(msg) - - - def set_warning(self, media_data_obj, msg): - - """Wrapper for media.Video.set_warning(). - - Args: - - media_data_obj (media.Video, media.Channel or media.Playlist): - The media data object to update. Only videos are updated by - this function - - msg (str): The warning message for this video - - """ - - if isinstance(media_data_obj, media.Video): - - if not media_data_obj.dbid in self.video_error_warning_dict: - - # The new warning is the first error/warning generated during - # this operation; remove any errors/warnings from previous - # operations - media_data_obj.reset_error_warning() - self.video_error_warning_dict[media_data_obj.dbid] = True - - # Set the new warning - media_data_obj.set_warning(msg) - - - def set_return_code(self, code): - - """Called by self.do_download(), .create_child_process(), - .extract_stdout_status() and .stop(). - - Based on YoutubeDLDownloader._set_returncode(). - - After the child process has terminated with an error of some kind, - sets a new value for self.return_code, but only if the new return code - is higher in the hierarchy of return codes than the current value. - - Args: - - code (int): A return code in the range 0-5 - - """ - - # (The code -1, STALLED, overrules everything else) - if code == -1 or code >= self.return_code: - self.return_code = code - - - def set_temp_destination(self, path, filename, extension): - - """Called by self.extract_stdout_data().""" - - self.temp_path = path - self.temp_filename = filename - self.temp_extension = extension - - - def reset_temp_destination(self): - - """Called by self.extract_stdout_data().""" - - self.temp_path = None - self.temp_filename = None - self.temp_extension = None - - - def stop(self): - - """Called by DownloadWorker.close() and also by - mainwin.MainWin.on_progress_list_stop_now(). - - Terminates the child process and sets this object's return code to - self.STOPPED. - """ - - if self.is_child_process_alive(): - - if os.name == 'nt': - # os.killpg is not available on MS Windows (see - # https://bugs.python.org/issue5115 ) - self.child_process.kill() - - # When we kill the child process on MS Windows the return code - # gets set to 1, so we want to reset the return code back to - # 0 - self.child_process.returncode = 0 - - else: - os.killpg(self.child_process.pid, signal.SIGKILL) - - self.set_return_code(self.STOPPED) - - - def stop_soon(self): - - """Can be called by anything. Currently called by - mainwin.MainWin.on_progress_list_stop_soon(). - - Sets the flag that causes this VideoDownloader to stop after the - current video. - """ - - self.stop_soon_flag = True - - -class ClipDownloader(object): - - """Called by downloads.DownloadWorker.run_clip_slice_downloader(). - - A modified VideoDownloader to download one or more video clips from a - specified video (rather than downloading the complete video). - - Optionally concatenates the clips back together, which has the effect of - removing one or more slices from a video. - - Python class to create multiple system child processes, one for each clip. - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - Sets self.return_code to a value in the range 0-5, described below. The - parent downloads.DownloadWorker object checks that return code once this - object's child process has finished. - - Args: - - download_manager_obj (downloads.DownloadManager): The download manager - object handling the entire download operation - - download_worker_obj (downloads.DownloadWorker): The parent download - worker object. The download manager uses multiple workers to - implement simultaneous downloads. The download manager checks for - free workers and, when it finds one, assigns it a - download.DownloadItem object. When the worker is assigned a - download item, it creates a new instance of this object to - interface with youtube-dl, and waits for this object to return a - return code - - download_item_obj (downloads.DownloadItem): The download item object - describing the URL from which youtube-dl should download clip(s) - - Warnings: - - The calling function is responsible for calling the close() method - when it's finished with this object, in order for this object to - properly close down. - - """ - - - # Attributes (the same set used by VideoDownloader; not all of them are - # used by ClipDownloader) - - - # Valid values for self.return_code. The larger the number, the higher in - # the hierarchy of return codes. - # Codes lower in the hierarchy (with a smaller number) cannot overwrite - # higher in the hierarchy (with a bigger number) - # - # 0 - The download operation completed successfully - OK = 0 - # 1 - A warning occured during the download operation - WARNING = 1 - # 2 - An error occured during the download operation - ERROR = 2 - # 3 - The corresponding url video file was larger or smaller from the given - # filesize limit - FILESIZE_ABORT = 3 - # 4 - The video(s) for the specified URL have already been downloaded - ALREADY = 4 - # 5 - The download operation was stopped by the user - STOPPED = 5 - # 6 - The download operation has stalled. The parent worker can restart it, - # if required - STALLED = -1 - - - # Standard class methods - - - def __init__(self, download_manager_obj, download_worker_obj, \ - download_item_obj): - - # IV list - class objects - # ----------------------- - # The downloads.DownloadManager object handling the entire download - # operation - self.download_manager_obj = download_manager_obj - # The parent downloads.DownloadWorker object - self.download_worker_obj = download_worker_obj - # The downloads.DownloadItem object describing the URL from which - # youtube-dl should download video(s) - self.download_item_obj = download_item_obj - - # The child process created by self.create_child_process() - self.child_process = None - - # Read from the child process STDOUT (i.e. self.child_process.stdout) - # and STDERR (i.e. self.child_process.stderr) in an asynchronous way - # by polling this queue.PriorityQueue object - self.queue = queue.PriorityQueue() - self.stdout_reader = PipeReader(self.queue, 'stdout') - self.stderr_reader = PipeReader(self.queue, 'stderr') - - - # IV list - other - # --------------- - # The current return code, using values in the range 0-5, as described - # above - # The value remains set to self.OK unless we encounter any problems - # The larger the number, the higher in the hierarchy of return codes. - # Codes lower in the hierarchy (with a smaller number) cannot - # overwrite higher in the hierarchy (with a bigger number) - self.return_code = self.OK - # The time (in seconds) between iterations of the loop in - # self.do_download_clips() - self.sleep_time = 0.1 - - # Flag set to True if this download operation was launched from the - # Classic Mode tab, False if not (set below) - self.dl_classic_flag = False - # Flag set to True if an attempt to copy an original videos' thumbnail - # fails (in which case, don't try again) - self.thumb_copy_fail_flag = False - - # Flag set to True by a call from any function to self.stop_soon() - # After being set to True, this ClipDownloader should give up after - # the next clip has been downloaded - self.stop_soon_flag = False - # When self.stop_soon_flag is True, the next call to - # self.extract_stdout_data() for a downloaded clip sets this flag to - # True, informing self.do_download_clips() that it can stop the child - # process - self.stop_now_flag = False - - # Named for compatibility with VideoDownloader, both IVs are set to the - # number of clips that have been downloaded - self.video_num = 0 - self.video_total = 0 - - # The type of download, depending on which function is called: - # 'chapters': self.do_download_clips_with_chapters() - # 'downloader': self.do_download_clips_with_downloader() - # 'ffmpeg': self.do_download_clips_with_ffmpeg() - # 'slices': self.do_download_remove_slices() - self.dl_type = None - - # Used for 'ffmpeg' and 'slices': - # Output generated by youtube-dl/FFmpeg may vary, depending on the - # file format specified. We have to record every file path - # we receive; the last path received is the one that remains on the - # filesystem (earlier ones are generally deleted). - # These two variables are reset at the beginning/end of every clip - # The file path currently being downloaded/processed - self.dl_path = None - # Flag set to True when youtube-dl/FFmpeg appears to have finished - # downloading/post-processing the clip - self.dl_confirm_flag = False - - # Used for self.dl_type = 'chapters': - self.chapter_dest_obj = None - self.chapter_dest_dir = None - self.chapter_orig_video_obj = None - - # Used for self.dl_type = 'downloader': - self.downloader_path_list = [] - - # Dictionary of clip titles used during this operation (i.e. when - # splitting a video into clips), used to re-name duplicates - # Not used when removing video slices - self.clip_title_dict = {} - - # Code - # ---- - # Initialise IVs - if self.download_item_obj.operation_classic_flag: - self.dl_classic_flag = True - - - # Public class methods - - - def do_download_clips(self): - - """Called by downloads.DownloadWorker.run_clip_slice_downloader(). - - Using the URL described by self.download_item_obj (which must - represent a media.Video object, during a 'custom_real' or - 'classic_custom' download operation), downloads a series of one or more - clips, using the timestamps specified by the media.Video itself. - - Return values: - - The final return code, a value in the range 0-5 (as described - above) - - """ - - # Import the main application and video object (for convenience) - app_obj = self.download_manager_obj.app_obj - orig_video_obj = self.download_item_obj.media_data_obj - - # Set the default return code. Everything is OK unless we encounter any - # problems - self.return_code = self.OK - - if not self.dl_classic_flag: - - # Reset the errors/warnings stored in the media data object, the - # last time it was checked/downloaded - orig_video_obj.reset_error_warning() - - if orig_video_obj.dbid in app_obj.temp_stamp_buffer_dict: - - # Retrieve the entry from the main application's temporary - # timestamp buffer, if it exists - stamp_list = app_obj.temp_stamp_buffer_dict[orig_video_obj.dbid] - # (The temporary buffer, once used, must be emptied immediately) - app_obj.del_temp_stamp_buffer_dict(orig_video_obj.dbid) - - # The first entry in 'stamp_list' is one of the values 'chapters', - # 'downloader' or 'ffmpeg'; extract it - dl_mode = stamp_list.pop(0) - if dl_mode != 'chapters' \ - and dl_mode != 'downloader' \ - and dl_mode != 'ffmpeg': - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('Invalid timestamps in temporary buffer'), - ) - - self.stop() - return self.ERROR - - else: - - # Otherwise, re-extract timestamps from the video's .info.json or - # description file, if allowed - if app_obj.video_timestamps_re_extract_flag \ - and not orig_video_obj.stamp_list: - app_obj.update_video_from_json(orig_video_obj, 'chapters') - - if app_obj.video_timestamps_re_extract_flag \ - and not orig_video_obj.stamp_list: - orig_video_obj.extract_timestamps_from_descrip(app_obj) - - # Check that at least one timestamp now exists - if not orig_video_obj.stamp_list: - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('No timestamps defined in video\'s timestamp list'), - ) - - self.stop() - return self.ERROR - - else: - stamp_list = orig_video_obj.stamp_list.copy() - dl_mode = 'default' - - # Set the containing folder, creating a media.Folder object and/or a - # sub-directory for the video clips, if required - parent_obj, parent_dir, dest_obj, dest_dir \ - = utils.clip_set_destination(app_obj, orig_video_obj) - - if parent_obj is None: - - # Duplicate media.Folder name, this is a fatal error - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _( - 'FAILED: Can\'t create the destination folder either because' \ - + ' a folder with the same name already exists, or because' \ - + ' new folders can\'t be added to the parent folder', - ), - ) - - self.stop() - - return self.ERROR - - # Download the clips - if dl_mode == 'chapters': - return self.do_download_clips_with_chapters( - orig_video_obj, - parent_obj, - parent_dir, - dest_obj, - dest_dir, - ) - - elif dl_mode == 'downloader': - - return self.do_download_clips_with_downloader( - orig_video_obj, - stamp_list, - parent_obj, - parent_dir, - dest_obj, - dest_dir, - ) - - else: - - # (dl_mode == 'ffmpeg') - return self.do_download_clips_with_ffmpeg( - orig_video_obj, - stamp_list, - parent_obj, - parent_dir, - dest_obj, - dest_dir, - ) - - - def do_download_clips_with_chapters(self, orig_video_obj, parent_obj, - parent_dir, dest_obj, dest_dir): - - """Called by self.do_download_clips(). - - Downloads video clips using yt-dlp's --split-chapters. A single - system command is used to download all requested video clips together. - - Args: - - orig_video_obj (media.Video): The video whose clips are being - downloaded - - parent_obj (media.Folder): orig_video_obj's containing folder - - parent_dir (str): Path to the containing folder's directory in - Tartube's data folder - - dest_obj (media.Folder): The actual folder to which video clips are - downloaded, which might be different from 'parent_obj' - - dest_dir (str): Path to the destination folder - - Return values: - - The final return code, a value in the range 0-5 (as described - above) - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Set the download type and its associated IVs - self.dl_type = 'chapters' - self.chapter_dest_obj = dest_obj - self.chapter_dest_dir = dest_dir - self.chapter_orig_video_obj = orig_video_obj - - # Get an output template for these clip(s) - if self.dl_classic_flag: - output_template = utils.clip_prepare_chapter_output_template( - app_obj, - orig_video_obj, - orig_video_obj.dummy_dir, - ) - - else: - output_template = utils.clip_prepare_chapter_output_template( - app_obj, - orig_video_obj, - dest_dir, - ) - - # Create a temporary directory to which the full video is downloaded - temp_dir = self.create_temp_dir_for_chapters(orig_video_obj) - if temp_dir is None: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Cannot create temporary directory'), - ) - - return - - # Prepare a system command... - if self.download_manager_obj.custom_dl_obj is not None: - divert_mode = self.download_manager_obj.custom_dl_obj.divert_mode - else: - divert_mode = None - - cmd_list = utils.generate_chapters_split_system_cmd( - app_obj, - orig_video_obj, - self.download_worker_obj.options_list.copy(), - dest_dir, - temp_dir, - output_template, - self.download_manager_obj.custom_dl_obj, - divert_mode, - self.dl_classic_flag, - ) - - # ...display it in the Output tab (if required)... - if app_obj.ytdl_output_system_cmd_flag: - app_obj.main_win_obj.output_tab_write_system_cmd( - self.download_worker_obj.worker_id, - ' '.join(cmd_list), - ) - - # ...and the terminal (if required) - if app_obj.ytdl_write_system_cmd_flag: - print(' '.join(cmd_list)) - - # ...and the downloader log (if required) - if app_obj.ytdl_log_system_cmd_flag: - app_obj.write_downloader_log(' '.join(cmd_list)) - - # Write an additional message in the Output tab, in the same style - # as those produced by youtube-dl/FFmpeg (and therefore not - # translated) - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - '[' + __main__.__packagename__ + '] Downloading chapters', - ) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child - # process STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # Pass data on to self.download_worker_obj so the main window can be - # updated. We don't know for sure how many chapters there will be, so - # just use default values - self.download_worker_obj.data_callback({ - 'playlist_index': 1, - 'playlist_size': 1, - 'status': formats.ACTIVE_STAGE_DOWNLOAD, - 'filename': '', - # This guarantees the the Classic Progress List shows the clip - # title, not the original filename - 'clip_flag': True, - }) - - # While downloading the media data object(s), update the callback - # function with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # Stop this clip downloader, if required to do so - if self.stop_now_flag: - self.stop() - - # The child process has finished - # We also set the return code to self.ERROR if the download didn't - # start or if the child process return code is greater than 0 - # Original notes from youtube-dl-gui: - # NOTE: In Linux if the called script is just empty Python exits - # normally (ret=0), so we can't detect this or similar cases - # using the code below - # NOTE: In Unix a negative return code (-N) indicates that the child - # was terminated by signal N (e.g. -9 = SIGKILL) - if self.child_process is None: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Clip download did not start'), - ) - - elif self.child_process.returncode > 0: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _( - 'FAILED: Child process exited with non-zero code: {}' - ).format(self.child_process.returncode), - ) - - # If at least one clip was extracted... - if self.video_total: - - # ...then the number of video downloads must be incremented - self.download_manager_obj.register_video('clip') - - # Delete the original video, if required, and if it's not inside a - # channel/playlist - # (Don't bother trying to delete a 'dummy' media.Video object, for - # download operations launched from the Classic Mode tab) - if app_obj.split_video_auto_delete_flag \ - and not isinstance(orig_video_obj.parent_obj, media.Channel) \ - and not isinstance(orig_video_obj.parent_obj, media.Playlist) \ - and not orig_video_obj.dummy_flag: - - app_obj.delete_video( - orig_video_obj, - True, # Delete all files - True, # Don't update Video Index yet - True, # Don't update Video Catalogue yet - ) - - - # Open the destination directory, if required to do so - if dest_dir is not None \ - and app_obj.split_video_auto_open_flag: - utils.open_file(app_obj, dest_dir) - - # Pass a dictionary of values to downloads.DownloadWorker, confirming - # the result of the job. The values are passed on to the main - # window - self.last_data_callback() - - # Pass the result back to the parent downloads.DownloadWorker object - return self.return_code - - - def do_download_clips_with_downloader(self, orig_video_obj, stamp_list, - parent_obj, parent_dir, dest_obj, dest_dir): - - """Called by self.do_download_clips(). - - Downloads video clips using yt-dlp's download-sections. A single - system command is used to download all requested video clips together. - - Args: - - orig_video_obj (media.Video): The video whose clips are being - downloaded - - stamp_list (list): List in groups of three, in the form - [start_timestamp, stop_timestamp, clip_title] - - parent_obj (media.Folder): orig_video_obj's containing folder - - parent_dir (str): Path to the containing folder's directory in - Tartube's data folder - - dest_obj (media.Folder): The actual folder to which video clips are - downloaded, which might be different from 'parent_obj' - - dest_dir (str): Path to the destination folder - - Return values: - - The final return code, a value in the range 0-5 (as described - above) - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Set the download type - self.dl_type = 'downloader' - - # Create a temporary directory to which the full video is downloaded - temp_dir = self.create_temp_dir_for_chapters(orig_video_obj) - if temp_dir is None: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Cannot create temporary directory'), - ) - - return - - # Prepare a system command... - if self.download_manager_obj.custom_dl_obj is not None: - divert_mode = self.download_manager_obj.custom_dl_obj.divert_mode - else: - divert_mode = None - - cmd_list = utils.generate_downloader_split_system_cmd( - app_obj, - orig_video_obj, - self.download_worker_obj.options_list.copy(), - dest_dir, - temp_dir, - stamp_list, - self.download_manager_obj.custom_dl_obj, - divert_mode, - self.dl_classic_flag, - ) - - # ...display it in the Output tab (if required)... - if app_obj.ytdl_output_system_cmd_flag: - app_obj.main_win_obj.output_tab_write_system_cmd( - self.download_worker_obj.worker_id, - ' '.join(cmd_list), - ) - - # ...and the terminal (if required) - if app_obj.ytdl_write_system_cmd_flag: - print(' '.join(cmd_list)) - - # ...and the downloader log (if required) - if app_obj.ytdl_log_system_cmd_flag: - app_obj.write_downloader_log(' '.join(cmd_list)) - - # Write an additional message in the Output tab, in the same style - # as those produced by youtube-dl/FFmpeg (and therefore not - # translated) - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - '[' + __main__.__packagename__ + '] Downloading sections', - ) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child - # process STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # Pass data on to self.download_worker_obj so the main window can be - # updated. We don't know for sure how many chapters there will be, so - # just use default values - self.download_worker_obj.data_callback({ - 'playlist_index': 1, - 'playlist_size': 1, - 'status': formats.ACTIVE_STAGE_DOWNLOAD, - 'filename': '', - # This guarantees the the Classic Progress List shows the clip - # title, not the original filename - 'clip_flag': True, - }) - - # While downloading the media data object(s), update the callback - # function with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # Stop this clip downloader, if required to do so - if self.stop_now_flag: - self.stop() - - # The child process has finished - # We also set the return code to self.ERROR if the download didn't - # start or if the child process return code is greater than 0 - # Original notes from youtube-dl-gui: - # NOTE: In Linux if the called script is just empty Python exits - # normally (ret=0), so we can't detect this or similar cases - # using the code below - # NOTE: In Unix a negative return code (-N) indicates that the child - # was terminated by signal N (e.g. -9 = SIGKILL) - if self.child_process is None: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Clip download did not start'), - ) - - elif self.child_process.returncode > 0: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _( - 'FAILED: Child process exited with non-zero code: {}' - ).format(self.child_process.returncode), - ) - - # Set the destination directory, which is different from the current - # value, in downloads from the Classic Mode tab - if self.dl_classic_flag: - dest_dir = orig_video_obj.dummy_dir - - # self.downloader_path_list contains a list of paths that yt-dlp - # attempted to download, hopefully in the same order as 'stamp_list' - if self.downloader_path_list: - - for i in range(len(self.downloader_path_list)): - - old_path = self.downloader_path_list[i] - - if not os.path.isfile(old_path): - continue - elif i >= len(stamp_list): - break - - # List in groups of 3, in the form - # [start_stamp, optional_stop_stamp, optional_clip_title] - mini_list = stamp_list[i] - - # Rename the clip, ready for it to be added to the Tartube - # database - directory, filename, extension \ - = utils.extract_path_components(old_path) - # (This is a scaled-down version of code in - # utils.clip_prepare_title() ) - orig_name = orig_video_obj.file_name - if orig_name is None \ - and orig_video_obj.dummy_flag \ - and orig_video_obj.nickname != app_obj.default_video_name: - orig_name = orig_video_obj.nickname - - this_title = mini_list[2] - if this_title is None: - this_title = 'Clip' - - if app_obj.split_video_name_mode == 'num': - mod_title = str(i + 1) - elif app_obj.split_video_name_mode == 'clip': - mod_title = this_title - elif app_obj.split_video_name_mode == 'num_clip': - mod_title = str(i + 1) + ' ' + this_title - elif app_obj.split_video_name_mode == 'clip_num': - mod_title = this_title + ' ' + str(i + 1) - - elif app_obj.split_video_name_mode == 'orig' \ - or app_obj.split_video_name_mode == 'orig_num': - - # N.B. We must have a unique clip name, so these two - # settings are combined - if orig_name is None: - mod_title = str(i + 1) - else: - mod_title = orig_name + ' ' + str(i + 1) - - elif app_obj.split_video_name_mode == 'orig_clip': - - if orig_name is None: - mod_title = this_title - else: - mod_title = orig_name + ' ' + this_title - - elif app_obj.split_video_name_mode == 'orig_num_clip': - - if orig_name is None: - mod_title = str(i + 1) + ' ' + this_title - else: - mod_title = orig_name + ' ' + str(i + 1) + ' ' \ - + this_title - - elif app_obj.split_video_name_mode == 'orig_clip_num': - - if orig_name is None: - mod_title = this_title + ' ' + str(i + 1) - else: - mod_title = orig_name + ' ' + this_title + ' ' \ - + str(i + 1) - - # Failsafe - if mod_title is None: - mod_title = str(i + 1) - - new_path = os.path.abspath( - os.path.join(dest_dir, mod_title + extension), - ) - - utils.rename_file(app_obj, old_path, new_path) - - if os.path.isfile(new_path): - self.confirm_video_clip( - dest_obj, - dest_dir, - orig_video_obj, - mod_title, - new_path, - ) - - # If at least one clip was extracted... - if self.video_total: - - # ...then the number of video downloads must be incremented - self.download_manager_obj.register_video('clip') - - # Delete the original video, if required, and if it's not inside a - # channel/playlist - # (Don't bother trying to delete a 'dummy' media.Video object, for - # download operations launched from the Classic Mode tab) - if app_obj.split_video_auto_delete_flag \ - and not isinstance(orig_video_obj.parent_obj, media.Channel) \ - and not isinstance(orig_video_obj.parent_obj, media.Playlist) \ - and not orig_video_obj.dummy_flag: - - app_obj.delete_video( - orig_video_obj, - True, # Delete all files - True, # Don't update Video Index yet - True, # Don't update Video Catalogue yet - ) - - # Open the destination directory, if required to do so - if dest_dir is not None \ - and app_obj.split_video_auto_open_flag: - utils.open_file(app_obj, dest_dir) - - # Pass a dictionary of values to downloads.DownloadWorker, confirming - # the result of the job. The values are passed on to the main - # window - self.last_data_callback() - - # Pass the result back to the parent downloads.DownloadWorker object - return self.return_code - - - def do_download_clips_with_ffmpeg(self, orig_video_obj, stamp_list, - parent_obj, parent_dir, dest_obj, dest_dir): - - """Called by self.do_download_clips(). - - Downloads video clips using FFmpeg, on clip at a time. - - Args: - - orig_video_obj (media.Video): The video whose clips are being - downloaded - - stamp_list (list): List in groups of three, in the form - [start_timestamp, stop_timestamp, clip_title] - - parent_obj (media.Folder): orig_video_obj's containing folder - - parent_dir (str): Path to the containing folder's directory in - Tartube's data folder - - dest_obj (media.Folder): The actual folder to which video clips are - downloaded, which might be different from 'parent_obj' - - dest_dir (str): Path to the destination folder - - Return values: - - The final return code, a value in the range 0-5 (as described - above) - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Set the download type - self.dl_type = 'ffmpeg' - - # Download the clips, one at a time - list_size = len(stamp_list) - for i in range(list_size): - - # Reset detection variables - self.dl_path = None - self.dl_confirm_flag = False - - # List in the form [start_stamp, stop_stamp, clip_title] - # If 'stop_stamp' is not specified, then 'start_stamp' of the next - # clip is used. If there are no more clips, then this clip will - # end at the end of the video - start_stamp, stop_stamp, clip_title \ - = utils.clip_extract_data(stamp_list, i) - - # Set a (hopefully unique) clip title - clip_title = utils.clip_prepare_title( - app_obj, - orig_video_obj, - self.clip_title_dict, - clip_title, - i + 1, - list_size, - ) - - self.clip_title_dict[clip_title] = None - - # Prepare a system command... - if self.download_manager_obj.custom_dl_obj is not None: - divert_mode \ - = self.download_manager_obj.custom_dl_obj.divert_mode - else: - divert_mode = None - - cmd_list = utils.generate_ffmpeg_split_system_cmd( - app_obj, - orig_video_obj, - self.download_worker_obj.options_list.copy(), - dest_dir, - clip_title, - start_stamp, - stop_stamp, - self.download_manager_obj.custom_dl_obj, - divert_mode, - self.dl_classic_flag, - ) - - # ...display it in the Output tab (if required)... - if app_obj.ytdl_output_system_cmd_flag: - app_obj.main_win_obj.output_tab_write_system_cmd( - self.download_worker_obj.worker_id, - ' '.join(cmd_list), - ) - - # ...and the terminal (if required) - if app_obj.ytdl_write_system_cmd_flag: - print(' '.join(cmd_list)) - - # ...and the downloader log (if required) - if app_obj.ytdl_log_system_cmd_flag: - app_obj.write_downloader_log(' '.join(cmd_list)) - - # Write an additional message in the Output tab, in the same style - # as those produced by youtube-dl/FFmpeg (and therefore not - # translated) - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - '[' + __main__.__packagename__ + '] Downloading clip ' \ - + str(i + 1) + '/' + str(list_size), - ) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child - # process STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # Pass data on to self.download_worker_obj so the main window can - # be updated - self.download_worker_obj.data_callback({ - 'playlist_index': i + 1, - 'playlist_size': list_size, - 'status': formats.ACTIVE_STAGE_DOWNLOAD, - 'filename': clip_title, - # This guarantees the the Classic Progress List shows the clip - # title, not the original filename - 'clip_flag': True, - }) - - # While downloading the media data object, update the callback - # function with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't - # want to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # Stop this clip downloader, if required to do so, having just - # finished downloading a clip - if self.stop_now_flag: - self.stop() - - # The child process has finished - # We also set the return code to self.ERROR if the download didn't - # start or if the child process return code is greater than 0 - # Original notes from youtube-dl-gui: - # NOTE: In Linux if the called script is just empty Python exits - # normally (ret=0), so we can't detect this or similar cases - # using the code below - # NOTE: In Unix a negative return code (-N) indicates that the - # child was terminated by signal N (e.g. -9 = SIGKILL) - if self.child_process is None: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Clip download did not start'), - ) - - elif self.child_process.returncode > 0: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _( - 'FAILED: Child process exited with non-zero code: {}' - ).format(self.child_process.returncode), - ) - - # General error handling - if self.return_code != self.OK: - break - - # Deal with a confirmed download (if any) - if self.dl_path is not None and self.dl_confirm_flag: - - self.confirm_video_clip( - dest_obj, - dest_dir, - orig_video_obj, - clip_title - ) - - # If at least one clip was extracted... - if self.video_total: - - # ...then the number of video downloads must be incremented - self.download_manager_obj.register_video('clip') - - # Delete the original video, if required, and if it's not inside a - # channel/playlist - # (Don't bother trying to delete a 'dummy' media.Video object, for - # download operations launched from the Classic Mode tab) - if app_obj.split_video_auto_delete_flag \ - and not isinstance(orig_video_obj.parent_obj, media.Channel) \ - and not isinstance(orig_video_obj.parent_obj, media.Playlist) \ - and not orig_video_obj.dummy_flag: - - app_obj.delete_video( - orig_video_obj, - True, # Delete all files - True, # Don't update Video Index yet - True, # Don't update Video Catalogue yet - ) - - # Open the destination directory, if required to do so - if dest_dir is not None \ - and app_obj.split_video_auto_open_flag: - utils.open_file(app_obj, dest_dir) - - # Pass a dictionary of values to downloads.DownloadWorker, confirming - # the result of the job. The values are passed on to the main - # window - self.last_data_callback() - - # Pass the result back to the parent downloads.DownloadWorker object - return self.return_code - - - def do_download_remove_slices(self): - - """Called by downloads.DownloadWorker.run_clip_slice_downloader(). - - Modified version of self.do_download_clips(). - - The media.Video object specifies one or more video slices that must be - removed. We start by downloading the video in clips, as before. The - clips are the portions of the video that we want to keep. - - Then, we concatenate the clips back together, which has the effect of - 'downloading' a video with the specified slices removed. - - Return values: - - The final return code, a value in the range 0-5 (as described - above) - - """ - - # Import the main application and video object (for convenience) - app_obj = self.download_manager_obj.app_obj - orig_video_obj = self.download_item_obj.media_data_obj - - # Set the download type - self.dl_type = 'slices' - - # Set the default return code. Everything is OK unless we encounter any - # problems - self.return_code = self.OK - - if not self.dl_classic_flag: - - # Reset the errors/warnings stored in the media data object, the - # last time it was checked/downloaded - self.download_item_obj.media_data_obj.reset_error_warning() - - # Contact the SponsorBlock server to update the video's slice data, if - # allowed - # (No point doing it, if the temporary buffer is set) - if not orig_video_obj.dbid in app_obj.temp_slice_buffer_dict: - - if app_obj.sblock_re_extract_flag \ - and not orig_video_obj.slice_list: - utils.fetch_slice_data( - app_obj, - orig_video_obj, - self.download_worker_obj.worker_id, - ) - - # Check that at least one slice now exists - if not orig_video_obj.slice_list: - - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('No slices defined in video\'s slice list'), - ) - - self.stop() - return self.ERROR - - # Create a temporary directory for this video so we don't accidentally - # overwrite anything - parent_dir = orig_video_obj.parent_obj.get_actual_dir(app_obj) - temp_dir = self.create_temp_dir_for_slices(orig_video_obj) - if temp_dir is None: - return self.ERROR - - # If the temporary buffer specifies a slice list, use it; otherwise - # use the video's actual slice list - if not orig_video_obj.dbid in app_obj.temp_slice_buffer_dict: - slice_list = orig_video_obj.slice_list.copy() - temp_flag = False - - else: - slice_list = app_obj.temp_slice_buffer_dict[orig_video_obj.dbid] - # The first entry in 'slice_list' is the value 'default'; remove it - slice_list.pop(0) - - # (The temporary buffer, once used, must be emptied immediately) - app_obj.del_temp_slice_buffer_dict(orig_video_obj.dbid) - temp_flag = True - - # Convert this list from a list of video slices to be removed, to a - # list of video clips to be retained - # The returned list is in groups of two, in the form - # [start_time, stop_time] - # ...where 'start_time' and 'stop_time' are floating-point values in - # seconds. 'stop_time' can be None to signify the end of the video, - # but 'start_time' is 0 to signify the start of the video - clip_list = utils.convert_slices_to_clips( - app_obj, - self.download_manager_obj.custom_dl_obj, - slice_list, - temp_flag, - ) - - # Download the clips, one at a time - confirmed_list = [] - count = 0 - list_size = len(clip_list) - for mini_list in clip_list: - - count += 1 - start_time = mini_list[0] - stop_time = mini_list[1] - - # Reset detection variables - self.dl_path = None - self.dl_confirm_flag = False - - # Prepare a system command... - if self.download_manager_obj.custom_dl_obj is not None: - divert_mode \ - = self.download_manager_obj.custom_dl_obj.divert_mode - else: - divert_mode = None - - cmd_list = utils.generate_slice_system_cmd( - app_obj, - orig_video_obj, - self.download_worker_obj.options_list.copy(), - temp_dir, - count, - start_time, - stop_time, - self.download_manager_obj.custom_dl_obj, - divert_mode, - self.dl_classic_flag, - ) - - # ...display it in the Output tab (if required)... - if app_obj.ytdl_output_system_cmd_flag: - app_obj.main_win_obj.output_tab_write_system_cmd( - self.download_worker_obj.worker_id, - ' '.join(cmd_list), - ) - - # ...and the terminal (if required) - if app_obj.ytdl_write_system_cmd_flag: - print(' '.join(cmd_list)) - - # ...and the downloader log (if required) - if app_obj.ytdl_log_system_cmd_flag: - app_obj.write_downloader_log(' '.join(cmd_list)) - - # Write an additional message in the Output tab, in the same style - # as those produced by youtube-dl/FFmpeg (and therefore not - # translated) - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - '[' + __main__.__packagename__ + '] Downloading clip ' \ - + str(count) + '/' + str(list_size), - ) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child - # process STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # Pass data on to self.download_worker_obj so the main window can - # be updated - if stop_time is not None: - clip = 'Clip ' + str(start_time) + 's - ' + str(stop_time) \ - + 's' - else: - clip = 'Clip ' + str(start_time) + 's - end' - - self.download_worker_obj.data_callback({ - 'playlist_index': count, - 'playlist_size': list_size, - 'status': formats.ACTIVE_STAGE_DOWNLOAD, - 'filename': clip, - }) - - # While downloading the media data object, update the callback - # function with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't - # want to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # Stop this clip downloader, if required to do so, having just - # finished downloading a clip - if self.stop_now_flag: - self.stop() - - # The child process has finished - # We also set the return code to self.ERROR if the download didn't - # start or if the child process return code is greater than 0 - # Original notes from youtube-dl-gui: - # NOTE: In Linux if the called script is just empty Python exits - # normally (ret=0), so we can't detect this or similar cases - # using the code below - # NOTE: In Unix a negative return code (-N) indicates that the - # child was terminated by signal N (e.g. -9 = SIGKILL) - if self.child_process is None: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Clip download did not start'), - ) - - elif self.child_process.returncode > 0: - self.set_return_code(self.ERROR) - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _( - 'FAILED: Child process exited with non-zero code: {}' - ).format(self.child_process.returncode), - ) - - # General error handling - if self.return_code != self.OK: - - break - - # Add a confirmed download to the list - if self.dl_path is not None and self.dl_confirm_flag: - - confirmed_list.append(self.dl_path) - self.video_num += 1 - self.video_total += 1 - - # If fewer clips than expected were downloaded, then don't use any of - # them - if len(confirmed_list) != len(clip_list): - - self.set_return_code(self.ERROR) - - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: One or more clips were not downloaded'), - ) - - else: - - # Otherwise, get the video's (original) file extension from the - # first clip - file_path, file_ext = os.path.splitext(confirmed_list[0]) - - # Ordinarily, the user will check a video before custom downloading - # it. If not, the media.Video object won't have a .file_name set, - # which breaks the code below; in that case, we'll have to - # generate a name ourselves - if orig_video_obj.file_name is None: - fallback_name = _('Video') + ' ' + str(orig_video_obj.dbid) - orig_video_obj.set_name(fallback_name) - orig_video_obj.set_nickname(fallback_name) - orig_video_obj.set_file(fallback_name, file_ext) - - # If there is more than one clip, they must be concatenated to - # produce a single video (like the original video, from which the - # video slices have been removed) - if len(confirmed_list) == 1: - output_path = confirmed_list[0] - - else: - # For FFmpeg's benefit, write a text file listing every clip - line_list = [] - clips_file = os.path.abspath( - os.path.join(temp_dir, 'clips.txt'), - ) - - for confirmed_path in confirmed_list: - line_list.append('file \'' + confirmed_path + '\'') - - with open(clips_file, 'w') as fh: - fh.write('\n'.join(line_list)) - - # Prepare the FFmpeg command to concatenate the clips together - output_path = os.path.abspath( - os.path.join( - temp_dir, - orig_video_obj.file_name + file_ext, - ), - ) - - cmd_list = [ - app_obj.ffmpeg_manager_obj.get_executable(), - '-safe', - '0', - '-f', - 'concat', - '-i', - clips_file, - '-c', - 'copy', - output_path, - ] - - # ...display it in the Output tab (if required)... - if app_obj.ytdl_output_system_cmd_flag: - app_obj.main_win_obj.output_tab_write_system_cmd( - self.download_worker_obj.worker_id, - ' '.join(cmd_list), - ) - - # ...and the terminal (if required) - if app_obj.ytdl_write_system_cmd_flag: - print(' '.join(cmd_list)) - - # ...and the downloader log (if required) - if app_obj.ytdl_log_system_cmd_flag: - app_obj.write_downloader_log(' '.join(cmd_list)) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child - # process STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # Pass data on to self.download_worker_obj so the main window - # can be updated - self.download_worker_obj.data_callback({ - 'playlist_index': self.video_total, - 'playlist_size': self.video_total, - 'status': formats.ACTIVE_STAGE_CONCATENATE, - 'filename': '', - }) - - # Wait for the concatenation to finish. We are not bothered - # about reading the child process STDOUT/STDERR, since we can - # just test for the existence of the output file - while self.is_child_process_alive(): - time.sleep(self.sleep_time) - - if not os.path.isfile(output_path): - - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Can\'t concatenate clips'), - ) - - return self.ERROR - - # Move the single video file back into the parent directory, - # replacing any file of the same name that's already there - moved_path = os.path.abspath( - os.path.join( - parent_dir, - orig_video_obj.file_name + file_ext, - ), - ) - - if os.path.isfile(moved_path): - app_obj.remove_file(moved_path) - - if not app_obj.move_file_or_directory(output_path, moved_path): - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _( - 'FAILED: Clips were concatenated, but could not move' \ - + ' the output file out of the temporary directory', - ), - ) - - return self.ERROR - - # Also move metadata files, if they don't already exist in the - # parent directory (or its /.data and ./thumbs sub-directories) - self.move_metadata_files(orig_video_obj, temp_dir, parent_dir) - - # Update media.Video IVs (in particular, in some circumstances, - # FFmpeg may have switched the file extension to a different one) - orig_video_obj.set_file(orig_video_obj.file_name, file_ext) - - # downloads.DownloadManager tracks the number of video slices - # removed - for i in range(len(slice_list)): - self.download_manager_obj.register_slice() - - # Update Tartube's database - self.confirm_video_remove_slices(orig_video_obj, moved_path) - - # Delete the temporary directory - app_obj.remove_directory(temp_dir) - - # Pass a dictionary of values to downloads.DownloadWorker, confirming - # the result of the job. The values are passed on to the main - # window - self.last_data_callback() - - # Pass the result back to the parent downloads.DownloadWorker object - return self.return_code - - - def close(self): - - """Can be called by anything. - - Destructor function for this object. - """ - - # Tell the PipeReader objects to shut down, thus joining their threads - self.stdout_reader.join() - self.stderr_reader.join() - - - def confirm_video_clip(self, dest_obj, dest_dir, orig_video_obj, \ - clip_title, clip_path=None): - - """Called by self.do_download_clips_with_ffmpeg(), - self.extract_stdout_data(), etc, when a video clip is confirmed as - having been downloaded. - - Args: - - dest_obj (media.Folder): The folder object into which the new video - object is to be created - - dest_dir (str): The path to that folder on the filesystem - - orig_video_obj (media.Video): The original video, from which the - video clip has been split - - clip_title (str): The clip title for the new video, matching its - filename - - clip_path (str or None): Full path to the video clip; specified - only when required - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Download confirmed - self.video_num += 1 - self.video_total += 1 - self.download_manager_obj.register_clip() - - if dest_obj \ - and app_obj.split_video_add_db_flag \ - and not orig_video_obj.dummy_flag: - - # Add the clip to Tartube's database - if self.dl_type == 'ffmpeg': - - clip_video_obj = utils.clip_add_to_db( - app_obj, - dest_obj, - orig_video_obj, - clip_title, - self.dl_path, - ) - - elif self.dl_type == 'chapters' or self.dl_type == 'downloader': - - clip_video_obj = utils.clip_add_to_db( - app_obj, - dest_obj, - orig_video_obj, - clip_title, - clip_path, - ) - - if clip_video_obj and not orig_video_obj.dummy_flag: - - # Update the Results List (unless the download operation was - # launched from the Classic Mode tab) - app_obj.main_win_obj.results_list_add_row( - self.download_item_obj, - clip_video_obj, - {}, # No 'mini_options_dict' to apply - ) - - elif app_obj.split_video_copy_thumb_flag \ - and not self.thumb_copy_fail_flag: - - # The call to utils.clip_add_to_db() copies the original thumbnail, - # when required - # Since we're not going to call that, copy the thumbnail here - thumb_path = utils.find_thumbnail(app_obj, orig_video_obj) - if thumb_path is not None: - - _, thumb_ext = os.path.splitext(thumb_path) - new_path = os.path.abspath( - os.path.join(dest_dir, clip_title + thumb_ext), - ) - - try: - - shutil.copyfile(thumb_path, new_path) - - except: - - GObject.timeout_add( - 0, - app_obj.system_error, - 309, - _( - 'Failed to copy the original video\'s' \ - + ' thumbnail', - ), - ) - - # Don't try to copy orig_video_obj's thumbnail again - self.thumb_copy_fail_flag = True - - # This ClipDownloader can now stop, if required to do so after a clip - # has been downloaded - if self.stop_soon_flag: - self.stop_now_flag = True - - - def confirm_video_remove_slices(self, orig_video_obj, output_path): - - """Called by self.do_download_remove_slices(). - - Once a video has been downloaded as a sequence of clips, then - concatenated into a single video file (thereby removing one or more - video slices), make sure the medai.Video object is marked as - downloaded, and update the main window. - - Args: - - orig_video_obj (media.Video): The video to be downloaded - - output_path (str): Full path to the concatenated video - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Special case: don't add videos to the Tartube database - if orig_video_obj.parent_obj.dl_no_db_flag: - # (Do nothing, in this case) - pass - - # Special case: if the download operation was launched from the - # Classic Mode tab, then we only need to update the dummy - # media.Video object, and to move/remove description/metadata/ - # thumbnail files, as appropriate - elif self.dl_classic_flag: - - orig_video_obj.set_dl_flag(True) - orig_video_obj.set_dummy_path(output_path) - - elif not orig_video_obj.dl_flag: - - # Mark the video as downloaded - GObject.timeout_add( - 0, - app_obj.mark_video_downloaded, - orig_video_obj, - True, # Video is downloaded - ) - - # Do add an entry to the Results List (as well as updating the - # Video Catalogue, as normal) - GObject.timeout_add( - 0, - app_obj.announce_video_download, - self.download_item_obj, - orig_video_obj, - # No call to utils.compile_mini_options_dict(), because this - # function deals with download options like - # 'move_description' by itself - {}, - ) - - # Try to detect the video's new length. The TRUE argument tells - # the function to override the existing length, if set - app_obj.update_video_from_filesystem( - orig_video_obj, - output_path, - True, - ) - - # Register the download with DownloadManager, so that download limits - # can be applied, if required - self.download_manager_obj.register_video('new') - - # Timestamp and slice information is now obsolete for this video, and - # can be removed, if required - if app_obj.slice_video_cleanup_flag: - orig_video_obj.reset_timestamps() - orig_video_obj.reset_slices() - - - def create_child_process(self, cmd_list): - - """Called by self.do_download_clips() shortly after the call to - utils.generate_ffmpeg_split_system_cmd(), etc. - - Based on YoutubeDLDownloader._create_process(). - - Executes the system command, creating a new child process which - executes youtube-dl. - - Sets self.return_code in the event of an error. - - Args: - - cmd_list (list): Python list that contains the command to execute - - """ - - # Strip double quotes from arguments - # (Since we're sending the system command one argument at a time, we - # don't need to retain the double quotes around any single argument - # and, in fact, doing so would cause an error) - cmd_list = utils.strip_double_quotes(cmd_list) - - # Create the child process - info = preexec = None - - if os.name == 'nt': - # Hide the child process window that MS Windows helpfully creates - # for us - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - # Make this child process the process group leader, so that we can - # later kill the whole process group with os.killpg - preexec = os.setsid - - try: - self.child_process = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=preexec, - startupinfo=info, - ) - - except (ValueError, OSError) as error: - # (There is no need to update the media data object's error list, - # as the code in self.do_download_clips() will notice the child - # process didn't start, and set its own error message) - self.set_return_code(self.ERROR) - - - def create_temp_dir_for_chapters(self, orig_video_obj): - - """Called by self.do_download_clips_with_chapters(). - - Create a temporary directory for files used while yt-dlp downloads - video chapters. - - Args: - - orig_video_obj (media.Video): The video to be downloaded - - Return values: - - The temporary directory created on success, None on failure - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Work out where the temporary directory should be... - temp_dir = os.path.abspath( - os.path.join( - app_obj.temp_dir, - '.clips_' + str(orig_video_obj.dbid) - ), - ) - - # ...then create it - try: - if os.path.isdir(temp_dir): - app_obj.remove_directory(temp_dir) - - app_obj.make_directory(temp_dir) - - return temp_dir - - except: - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Can\'t create a temporary folder for video clips'), - ) - - self.stop() - - return None - - - def create_temp_dir_for_slices(self, orig_video_obj): - - """Called by self.do_download_remove_slices(). - - Before downloading a video in clips, and then concatenating the clips, - create a temporary directory for the clips so we don't accidentally - overwrite anything. - - Args: - - orig_video_obj (media.Video): The video to be downloaded - - Return values: - - The temporary directory created on success, None on failure - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Work out where the temporary directory should be... - temp_dir = os.path.abspath( - os.path.join( - app_obj.temp_dir, - '.slices_' + str(orig_video_obj.dbid) - ), - ) - - # ...then create it - try: - if os.path.isdir(temp_dir): - app_obj.remove_directory(temp_dir) - - app_obj.make_directory(temp_dir) - - return temp_dir - - except: - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - _('FAILED: Can\'t create a temporary folder for video slices'), - ) - - self.stop() - - return None - - - def extract_stdout_data(self, stdout): - - """Called by self.read_child_process(). - - Extracts output from the child process. - - Output generated by youtube-dl/FFmpeg may vary, depending on the file - format specified. We have to record every file path we receive; the - last path received is the one that remains on the filesystem (earlier - ones are generally deleted). - - Args: - - stdout (str): String that contains a line from the child process - STDOUT (i.e. a message from youtube-dl) - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Output received from self.do_download_clips_with_ffmpeg() and - # self.do_download_remove_slices() - if self.dl_type == 'ffmpeg' or self.dl_type == 'slices': - - # Check for a media file being downloaded - match = re.search(r'^\[download\] Destination\:\s(.*)$', stdout) - if match: - - self.dl_path = match.group(1) - return - - match = re.search(r'^\[ffmpeg\] Destination\:\s(.*)$', stdout) - if match: - - self.dl_path = match.group(1) - self.dl_confirm_flag = True - return - - # Check for completion of a media file download - match = re.search(r'^\[download\] 100% of .* in', stdout) - if match: - - self.dl_confirm_flag = True - return - - # Check for confirmation of post-processing - match = re.search( - r'^\[ffmpeg\] Merging formats into \"(.*)\"$', - stdout - ) - if match: - - self.dl_path = match.group(1) - self.dl_confirm_flag = True - - return - - elif self.dl_type == 'chapters': - - # !!! DEBUG v2.4.306 - # Would like to extract download progress here, but yt-dlp is - # sending all progress updates from 0.1% to 100% in a single line - # and not in a consistent way - - # Check for completion of a media file download - match = re.search( - r'^\[SplitChapters\] Chapter \d+\; Destination\: (.*)$', - stdout, - ) - if match: - - _, name, _ = utils.extract_path_components(match.group(1)) - - self.confirm_video_clip( - self.chapter_dest_obj, - self.chapter_dest_dir, - self.chapter_orig_video_obj, - name, - match.group(1), - ) - - elif self.dl_type == 'downloader': - - # Check for the start of a media file download, storing the path - # in the list. Hopefully, yt-dlp announces a list of paths that - # is in the same order as the sections we specified - match = re.search( - r'^\[download\] Destination\: (.*)$', - stdout, - ) - if match: - self.downloader_path_list.append(match.group(1)) - - - def is_child_process_alive(self): - - """Called by self.do_download_clips(), .do_download_remove_slices and - .stop(). - - Based on YoutubeDLDownloader._proc_is_alive(). - - Called continuously during the loop to check whether the child process - has finished or not. - - Return values: - - True if the child process is alive, otherwise returns False - - """ - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def is_network_error(self, stderr): - - """Called by self.do_download_clips(); an exact copy of the function in - VideoDownloader. - - Try to detect network errors, indicating a stalled download. - - youtube-dl's output is system-dependent, so this function may not - detect every type of network error. - - Args: - - stderr (str): A message from the child process STDERR - - Return values: - - True if the STDERR message seems to be a network error, False if it - should be tested further - - """ - - # v2.3.012, this error is seen on MS Windows: - # unable to download video data: - # Don't know yet what the equivalent on other operating systems is, so - # we'll detect the first part, which is a string generated by - # youtube-dl itself - - if re.search(r'unable to download video data', stderr): - return True - else: - return False - - - def last_data_callback(self): - - """Called by self.read_child_process(). - - Based on VideoDownloader.last_data_callback(). - - After the child process has finished, creates a new Python dictionary - in the standard form described by self.extract_stdout_data(). - - Sets key-value pairs in the dictonary, then passes it to the parent - downloads.DownloadWorker object, confirming the result of the child - process. - - The new key-value pairs are used to update the main window. - """ - - dl_stat_dict = {} - - # (Some of these statuses are not actually used, but the code - # references them, in case they are added in future) - if self.return_code == self.OK: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_FINISHED - elif self.return_code == self.ERROR: - dl_stat_dict['status'] = formats.MAIN_STAGE_ERROR - elif self.return_code == self.WARNING: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_WARNING - elif self.return_code == self.STOPPED: - dl_stat_dict['status'] = formats.ERROR_STAGE_STOPPED - elif self.return_code == self.ALREADY: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_ALREADY - elif self.return_code == self.STALLED: - dl_stat_dict['status'] = formats.MAIN_STAGE_STALLED - else: - dl_stat_dict['status'] = formats.ERROR_STAGE_ABORT - - # In the Classic Progress List, the 'Incoming File' column showed - # clipped names. Replace that with the full video name - dl_stat_dict['filename'] = self.download_item_obj.media_data_obj.name - dl_stat_dict['clip_flag'] = True - - # The True argument shows that this function is the caller - self.download_worker_obj.data_callback(dl_stat_dict, True) - - - def move_metadata_files(self, orig_video_obj, temp_dir, parent_dir): - - """Called by self.do_download_remove_slices(). - - After moving the (concatenated) video file from its temporary directory - into the parent container's directory, do the same to the metadata - files. - - Depending on settings in the options.OptionsManager, they may be - moved into a sub-directory of the parent cotainer's directory instead. - - Args: - - orig_video_obj (media.Video): The video that was downloaded as a - sequence of clips - - temp_dir (str): Full path to the temporary directory into which the - video and its metadata files was downloaded - - parent_dir (str): Full path to the parent container's directory - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Handle the description file - options_obj = self.download_worker_obj.options_manager_obj - if options_obj.options_dict['keep_description']: - - descrip_path = os.path.abspath( - os.path.join(temp_dir, 'clip_1.description'), - ) - - if os.path.isfile(descrip_path): - - moved_path = os.path.abspath( - os.path.join( - parent_dir, - orig_video_obj.file_name + '.description', - ), - ) - - if options_obj.options_dict['move_description']: - final_path = os.path.abspath( - os.path.join( - parent_dir, - '.data', - orig_video_obj.file_name + '.description', - ), - ) - else: - final_path = moved_path - - if not os.path.isfile(moved_path) \ - and not os.path.isfile(final_path): - app_obj.move_file_or_directory(descrip_path, moved_path) - - # Further move the file into its sub-directory, if - # required, first creating that sub-directory if it - # doesn't exist - if options_obj.options_dict['move_description']: - utils.move_metadata_to_subdir( - app_obj, - orig_video_obj, - '.description', - ) - - # Handle the .info.json file - if options_obj.options_dict['keep_info']: - - json_path = os.path.abspath( - os.path.join(temp_dir, 'clip_1.info.json'), - ) - - if os.path.isfile(json_path): - - moved_path = os.path.abspath( - os.path.join( - parent_dir, - orig_video_obj.file_name + '.info.json', - ), - ) - - if options_obj.options_dict['move_info']: - final_path = os.path.abspath( - os.path.join( - parent_dir, - '.data', - orig_video_obj.file_name + '.info.json', - ), - ) - else: - final_path = moved_path - - if not os.path.isfile(moved_path) \ - and not os.path.isfile(final_path): - app_obj.move_file_or_directory(json_path, moved_path) - - if options_obj.options_dict['move_info']: - utils.move_metadata_to_subdir( - app_obj, - orig_video_obj, - '.info.json', - ) - - # v2.1.101 - Annotations were removed by YouTube in 2019, so this - # feature is not available, and will not be available until the - # authors have some annotations to test -# if options_obj.options_dict['keep_annotations']: -# -# xml_path = os.path.abspath( -# os.path.join(temp_dir, 'clip_1.annotations.xml'), -# ) -# -# if os.path.isfile(xml_path): -# -# moved_path = os.path.abspath( -# os.path.join( -# parent_dir, -# orig_video_obj.file_name + '.annotations.xml', -# ), -# ) -# -# if options_obj.options_dict['move_annotations']: -# final_path = os.path.abspath( -# os.path.join( -# parent_dir, -# '.data', -# orig_video_obj.file_name + '.annotations.xml', -# ), -# ) -# else: -# final_path = moved_path -# -# if not os.path.isfile(moved_path) \ -# and not os.path.isfile(final_path): -# app_obj.move_file_or_directory(xml_path, moved_path) -# -# if options_obj.options_dict['move_annotations']: -# utils.move_metadata_to_subdir( -# app_obj, -# orig_video_obj, -# '.annotations.xml', -# ) - - # Handle the thumbnail - if options_obj.options_dict['keep_thumbnail']: - - thumb_path = utils.find_thumbnail_from_filename( - app_obj, - temp_dir, - 'clip_1', - ) - - if thumb_path is not None and os.path.isfile(thumb_path): - - name, ext = os.path.splitext(thumb_path) - - moved_path = os.path.abspath( - os.path.join( - parent_dir, - orig_video_obj.file_name + ext, - ), - ) - - if not os.path.isfile(moved_path): - app_obj.move_file_or_directory(thumb_path, moved_path) - - # Convert .webp thumbnails to .jpg, if required - convert_path = utils.find_thumbnail_webp_intact_or_broken( - app_obj, - orig_video_obj, - ) - if convert_path is not None \ - and not app_obj.ffmpeg_fail_flag \ - and app_obj.ffmpeg_convert_webp_flag \ - and not app_obj.ffmpeg_manager_obj.convert_webp( - convert_path, - ): - app_obj.set_ffmpeg_fail_flag(True) - GObject.timeout_add( - 0, - app_obj.system_error, - 310, - app_obj.ffmpeg_fail_msg, - ) - - # Move to the sub-directory, if required - if options_obj.options_dict['move_thumbnail']: - utils.move_thumbnail_to_subdir( - app_obj, - orig_video_obj, - ) - - - def read_child_process(self): - - """Called by self.do_download_clips() and - self.do_download_remove_slices(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Return values: - - True if either STDOUT or STDERR were read, None if both queues were - empty - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.download_manager_obj.app_obj.system_error, - 311, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # On MS Windows we use cp1252, so that Tartube can communicate with the - # Windows console - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - # Remove weird carriage returns that insert empty lines into the - # Output tab - data = re.sub(r'[\r]+', '', data) - - # Extract output from STDOUT - self.extract_stdout_data(data) - - # Show output in the Output tab (if required) - if app_obj.ytdl_output_stdout_flag: - - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - data, - ) - - # Show output in the terminal (if required) - if app_obj.ytdl_write_stdout_flag: - - # Git #175, Japanese text may produce a codec error - # here, despite the .decode() call above - try: - print( - data.encode( - utils.get_encoding(), - 'replace', - ), - ) - except: - print( - 'STDOUT text with unprintable characters' - ) - - # Write output in the downloader log (if required) - if app_obj.ytdl_output_stdout_flag: - app_obj.write_downloader_log(data) - - # STDERR (ignoring any empty error messages) - elif data != '': - - # v2.3.168 I'm not sure that any detectable errors are actually - # produced, but nevertheless this section can handle any such - # errors - - # After a network error, stop trying to download clips - if self.is_network_error(data): - - self.stop() - self.last_data_callback() - self.set_return_code(self.STALLED) - - self.queue.task_done() - return None - - # Show output in the Output tab (if required) - if app_obj.ytdl_output_stderr_flag: - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - data, - ) - - # Show output in the terminal (if required) - if app_obj.ytdl_write_stderr_flag: - # Git #175, Japanese text may produce a codec error here, - # despite the .decode() call above - try: - print(data.encode(utils.get_encoding(), 'replace')) - except: - print('STDERR text with unprintable characters') - - # Write output to the downloader log (if required) - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(data) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def set_return_code(self, code): - - """Called by self.do_download_clips(), .do_download_remove_slices(), - .create_child_process() and .stop(). - - Based on YoutubeDLDownloader._set_returncode(). - - After the child process has terminated with an error of some kind, - sets a new value for self.return_code, but only if the new return code - is higher in the hierarchy of return codes than the current value. - - Args: - - code (int): A return code in the range 0-5 - - """ - - if code >= self.return_code: - self.return_code = code - - - def stop(self): - - """Called by DownloadWorker.close() and also by - mainwin.MainWin.on_progress_list_stop_now(). - - Terminates the child process and sets this object's return code to - self.STOPPED. - """ - - if self.is_child_process_alive(): - - if os.name == 'nt': - # os.killpg is not available on MS Windows (see - # https://bugs.python.org/issue5115 ) - self.child_process.kill() - - # When we kill the child process on MS Windows the return code - # gets set to 1, so we want to reset the return code back to - # 0 - self.child_process.returncode = 0 - - else: - os.killpg(self.child_process.pid, signal.SIGKILL) - - self.set_return_code(self.STOPPED) - - - def stop_soon(self): - - """Can be called by anything. Currently called by - mainwin.MainWin.on_progress_list_stop_soon(). - - Sets the flag that causes this ClipDownloader to stop after the - current video. - """ - - self.stop_soon_flag = True - - -class StreamDownloader(object): - - """Called by downloads.DownloadWorker.run_stream_downloader(). - - Python class to create a system child process. Uses the child process to - download a currently broadcasting livestream, using the URL described by a - downloads.DownloadItem object. - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - Sets self.return_code to a value in the range 0-5, described below. The - parent downloads.DownloadWorker object checks that return code once this - object's child process has finished. - - Args: - - download_manager_obj (downloads.DownloadManager): The download manager - object handling the entire download operation - - download_worker_obj (downloads.DownloadWorker): The parent download - worker object. The download manager uses multiple workers to - implement simultaneous downloads. The download manager checks for - free workers and, when it finds one, assigns it a - download.DownloadItem object. When the worker is assigned a - download item, it creates a new instance of this object to - interface with youtube-dl, and waits for this object to return a - return code - - download_item_obj (downloads.DownloadItem): The download item object - describing the URL with which the livestream must be downloaded - - Warnings: - - The calling function is responsible for calling the close() method - when it's finished with this object, in order for this object to - properly close down. - - """ - - # Attributes - - - # Valid vlues for self.return_code, following the model established by - # downloads.VideoDownloader (but with a smaller set of values) - # 0 - The download operation completed successfully - OK = 0 - # 2 - An error occured during the download operation - ERROR = 2 - # 5 - The download operation was stopped by the user - STOPPED = 5 - - - # Standard class methods - - - def __init__(self, download_manager_obj, download_worker_obj, \ - download_item_obj): - - # IV list - class objects - # ----------------------- - # The downloads.DownloadManager object handling the entire download - # operation - self.download_manager_obj = download_manager_obj - # The parent downloads.DownloadWorker object - self.download_worker_obj = download_worker_obj - # The downloads.DownloadItem object describing the URL of the - # broadcasting livestream - self.download_item_obj = download_item_obj - - # The child process created by self.create_child_process() - self.child_process = None - - # Read from the child process STDOUT (i.e. self.child_process.stdout) - # and STDERR (i.e. self.child_process.stderr) in an asynchronous way - # by polling this queue.PriorityQueue object - self.queue = queue.PriorityQueue() - self.stdout_reader = PipeReader(self.queue, 'stdout') - self.stderr_reader = PipeReader(self.queue, 'stderr') - - - # IV list - other - # --------------- - # The current return code, using values in the range 0-5, as described - # above - # The value remains set to self.OK unless we encounter any problems - # The larger the number, the higher in the hierarchy of return codes. - # Codes lower in the hierarchy (with a smaller number) cannot - # overwrite higher in the hierarchy (with a bigger number) - self.return_code = self.OK - # The time (in seconds) between iterations of the loop in - # self.do_download_basic() - self.sleep_time = 0.5 - # The time (in seconds) between iterations of the loop in - # self.do_download_m3u() and .do_download_streamlink() - self.longer_sleep_time = 0.25 - # Flag set to True after the first error message processed - self.first_error_flag = False - - # Shortcut to the livestream download mode: 'default', 'default_mu3' or - # 'streamlink' - self.dl_mode = self.download_manager_obj.app_obj.livestream_dl_mode - - # Flag set to True when we're expecting the .m3u manifest in STDOUT, - # set back to False when it is received - self.m3u_waiting_flag = False - # The text of the .m3u manifest, when received. Stored here so that - # self.do_download_m3u() can retrieve it - self.m3u_manifest = None - - # The actual (video) output path, set when intercepted (and used to - # update the Progress List) - # (YouTube and other sites add a date/time to the video title, which - # chnages every minutes; so the output path may not be the one we - # were expecting) - self.actual_output_path = None - # ...and its components (for quick lookup) - self.actual_output_dir = None - self.actual_output_filename = None - self.actual_output_ext = None - # The expected output path. In some modes, it is passed directly to the - # downloader - self.expect_output_path = self.choose_path() - - # Flag set to True for downloads in 'streamlink' mode, when the - # download started message is detected. The actual output path is on - # the next line of STDOU; when this flag is True, that output path - # can be intercepted - self.streamlink_start_flag = False - - # Number of segments downloaded so far - self.segment_count = 0 - # The time at which we should stop waiting for the next segment - # (matches time.time()) - self.check_time = 0 - # Size of the output file, and the time (matches time.time()) at which - # this value was set - # (These IVs are only use by 'default_m3u' and 'streamlink' modes - self.output_size = 0 - self.output_size_time = 0 - - - # Public class methods - - - def do_download(self): - - """Called by downloads.DownloadWorker.run_stream_downloader(). - - Downloads a broadcasting livestream using the URL described by - self.download_item_obj. - - Return values: - - The final return code, a value in the range 0-5 (as described - above) - - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - video_obj = self.download_item_obj.media_data_obj - - # Set the default return code. Everything is OK unless we encounter any - # problems - self.set_return_code(self.OK) - - if not self.download_item_obj.operation_classic_flag: - - # Reset the errors/warnings stored in the media data object, the - # last time it was checked/downloaded - video_obj.reset_error_warning() - video_obj.set_block_flag(False) - - # If the file already exists (indicating an incomplete livestream - # download), we can replace it before re-starting the download, if - # required - if app_obj.livestream_replace_flag \ - and video_obj.file_name is not None: - - if os.path.isfile(self.expect_output_path): - app_obj.remove_file(self.expect_output_path) - - part_path = self.expect_output_path + '.part' - if os.path.isfile(part_path): - app_obj.remove_file(part_path) - - # There are currently three download methods, specified by self.dl_mode - msg = _('Tartube is starting the livestream download') - if self.dl_mode == 'default': - - self.show_msg(msg + ' (' + app_obj.get_downloader() + ')...') - self.do_download_basic() - - elif self.dl_mode == 'default_m3u': - - self.show_msg( - msg + ' (' + app_obj.get_downloader() + '/FFmpeg/.m3u)...', - ) - self.do_download_m3u() - - elif self.dl_mode == 'streamlink': - - self.show_msg(msg + ' (streamlink)...') - self.do_download_streamlink() - - else: - - GObject.timeout_add( - 0, - app_obj.system_error, - 312, - _('Invalid livestream download mode'), - ) - - self.set_return_code(self.ERROR) - - # If the file described by the output path actually exists, we can mark - # the video as downloaded - if self.return_code == self.ERROR \ - or self.actual_output_path is None \ - or ( - not os.path.isfile(self.actual_output_path) \ - and not os.path.isfile(self.actual_output_path + '.part') - ): - # Video is not marked as downloaded - self.show_error('Livestream download failed') - self.set_error(video_obj, 'Livestream download failed') - - else: - - # Because of YouTube's delightful habit of appending the date/time - # to a livestream video's title, the media.Video's .file_name - # may be different to the name of the file actually downloaded - # Rectify the situation by renaming the video and/or the .part file - if not os.path.isfile(self.expect_output_path) \ - and os.path.isfile(self.actual_output_path): - - app_obj.move_file_or_directory( - self.actual_output_path, - self.expect_output_path, - ) - - if not os.path.isfile(self.expect_output_path + '.part') \ - and os.path.isfile(self.actual_output_path + '.part'): - - app_obj.move_file_or_directory( - self.actual_output_path + '.part', - self.expect_output_path + '.part', - ) - - # If we have a .part file instead of a video file, we can - # optionally salvage the download by converting it (e.g. - # convert output.mp4.part to output.mp4, and hope it works) - if not os.path.isfile(self.expect_output_path) \ - and os.path.isfile(self.expect_output_path + '.part'): - - if app_obj.livestream_stop_is_final_flag: - - self.show_msg( - _( - 'Incomplete livestream download detected;' \ - + ' removing the .part component from the' \ - + ' output file', - ), - ) - - app_obj.move_file_or_directory( - part_path, - self.expect_output_path, - ) - - elif not self.download_item_obj.operation_classic_flag: - - self.show_msg( - _( - 'Incomplete livestream download detected;' \ - + ' to complete the download, right-click the' \ - + ' video and select \'Finalise livestream\'', - ), - ) - - # Update IVs and the main window - if self.return_code == self.STOPPED \ - and not app_obj.livestream_stop_is_final_flag: - - # Video is not marked as downloaded - self.show_msg('Livestream download stopped') - - else: - - # Video is marked as downloaded - if self.return_code == self.STOPPED: - self.show_msg('Livestream download stopped') - else: - self.show_msg('Livestream download complete') - - if not self.download_item_obj.operation_classic_flag: - - # Download from the Videos tab - GObject.timeout_add( - 0, - app_obj.mark_video_downloaded, - video_obj, - True, # Video is downloaded - ) - GObject.timeout_add( - 0, - app_obj.mark_video_live, - video_obj, - 0, # Not live - ) - - else: - - # Download from the Classic Mode tab - video_obj.set_dl_flag(True) - if os.path.isfile(self.expect_output_path): - video_obj.set_dummy_path( - self.expect_output_path, - ) - else: - video_obj.set_dummy_path( - self.expect_output_path + '.part', - ) - - # Update the main window - GObject.timeout_add( - 0, - app_obj.announce_video_download, - self.download_item_obj, - video_obj, - utils.compile_mini_options_dict( - self.download_worker_obj.options_manager_obj, - ), - ) - - # Register the download with DownloadManager, so that download - # limits can be applied, if required - # (Use 'new' rather than 'old', even though the media.Video - # object already exists; the download operation's - # confirmation window will be less confusing that way) - self.download_manager_obj.register_video('new') - - # Pass a dictionary of values to downloads.DownloadWorker, confirming - # the result of the job. The values are passed on to the main window - self.last_data_callback() - - # Pass the result back to the parent downloads.DownloadWorker object - return self.return_code - - - def do_download_basic(self): - - """Called by self.do_download() when self.dl_mode is set to 'default'. - - Downloads a broadcasting livestream using youtube-dl alone. - - This function is based on VideoDownload.do_download() (but simplified, - because self.download_item_obj.media_data_obj is always a media.Video). - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Prepare a system command - options_obj = self.download_worker_obj.options_manager_obj - if options_obj.options_dict['direct_cmd_flag']: - - cmd_list = utils.generate_direct_system_cmd( - app_obj, - self.download_item_obj.media_data_obj, - options_obj, - ) - - else: - - cmd_list = utils.generate_ytdl_system_cmd( - app_obj, - self.download_item_obj.media_data_obj, - self.download_worker_obj.options_list, - ) - - # Display the (modified) command in the Output tab and/or terminal (if - # required)... - if app_obj.ytdl_output_system_cmd_flag: - self.show_cmd(' '.join(cmd_list)) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # While downloading the media data object, update the callback function - # with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # Perform a timeout, if necessary - if self.check_time > 0 and self.check_time < time.time(): - - # Halt the child process - self.stop() - self.show_msg('Download timed out') - return - - # The child process has finished - # We also set the return code to self.ERROR if the download didn't - # start or if the child process return code is greater than 0 - # Original notes from youtube-dl-gui: - # NOTE: In Linux if the called script is just empty Python exits - # normally (ret=0), so we can't detect this or similar cases - # using the code below - # NOTE: In Unix a negative return code (-N) indicates that the child - # was terminated by signal N (e.g. -9 = SIGKILL) - internal_msg = None - if self.child_process is None: - self.set_return_code(self.ERROR) - internal_msg = _('Download did not start') - - elif self.child_process.returncode > 0: - self.set_return_code(self.ERROR) - if not app_obj.ignore_child_process_exit_flag: - internal_msg = _( - 'Child process exited with non-zero code: {}', - ).format(self.child_process.returncode) - - if internal_msg: - - # (The message must be visible in the Errors/Warnings tab, the - # Output tab and/or the terminal) - self.set_error( - self.download_item_obj.media_data_obj, - internal_msg, - ) - - self.show_error(internal_msg) - - return - - - def do_download_m3u(self): - - """Called by self.do_download() when self.dl_mode is set to - 'default_m3u'. - - Downloads a broadcasting livestream, instructing youtube-dl to fetch - the .m3u manifest first. - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Prepare a system command to fetch the .m3u manifest... - cmd_list = utils.generate_m3u_system_cmd( - app_obj, - self.download_item_obj.media_data_obj, - ) - - # ...and display it in the Output tab and/or terminal, if required - self.show_cmd(' '.join(cmd_list)) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # The first message in STDOUT should be the .m3u manifest - self.m3u_waiting_flag = True - - # While downloading the media data object, update the callback function - # with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.longer_sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # !!! DEBUG: Any stall/timeout code goes here - pass - - # Reset the child process, ready for the next one - self.reset() - - # Check the .m3u manifest was fetched - if self.m3u_manifest is None or self.m3u_manifest == '': - - msg = _('Failed to download the .m3u manifest') - - self.set_error( - self.download_item_obj.media_data_obj, - msg, - ) - self.show_error(msg) - - self.set_return_code(self.ERROR) - return - - # Prepare a system command to download the livestream using the .m3u - # manifest... - cmd_list = [ - app_obj.ffmpeg_manager_obj.get_executable(), - '-i', - self.m3u_manifest, - '-c', - 'copy', - self.expect_output_path, - ] - - # ...and display it in the Output tab and/or terminal, if required - self.show_cmd(' '.join(cmd_list)) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # !!! DEBUG: Because STDOUT/STDERR messages are missing, artificially - # !!! update the Progress List, as if the start of the download had - # !!! been detected (in self.read_child_process() code) - self.set_actual_output_path(self.expect_output_path) - - # While downloading the media data object, update the callback function - # with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # Check on our progress. As of v2.3.618, youtube-dl output on lines - # without a newline character cannot be retrieved by - # downloads.PipeReader; this is the next best thing - if self.actual_output_path \ - and os.path.isfile(self.actual_output_path): - - current_size = os.path.getsize(self.actual_output_path) - if current_size != self.output_size: - - self.output_size = current_size - self.output_size_time = time.time() - self.segment_count += 1 - - # (Convert to, e.g. '27.5 MiB') - converted_size = utils.convert_bytes_to_string( - current_size, - ) - - self.download_data_callback('', str(converted_size)) - self.show_msg( - ('Downloaded segment #{0}, size {1}').format( - self.segment_count, - converted_size, - ), - ) - - self.check_time = (app_obj.livestream_dl_timeout * 60) \ - + time.time() - - # Perform a timeout, if necessary - if self.check_time > 0 and self.check_time < time.time(): - - # Halt the child process - self.stop() - self.show_msg('Download timed out') - return - - - def do_download_streamlink(self): - - """Called by self.do_download() when self.dl_mode is set to - 'streamlink'. - - Downloads a broadcasting livestream using streamlink. - """ - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Prepare a system command to download the livestream... - cmd_list = utils.generate_streamlink_system_cmd( - app_obj, - self.download_item_obj.media_data_obj, - self.expect_output_path, - ) - - # ...and display it in the Output tab and/or terminal, if required - self.show_cmd(' '.join(cmd_list)) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # While downloading the media data object, update the callback function - # with the status of the current job - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.longer_sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # Check on our progress. As of v2.3.618, youtube-dl output on lines - # without a newline character cannot be retrieved by - # downloads.PipeReader; this is the next best thing - if self.actual_output_path \ - and os.path.isfile(self.actual_output_path): - - current_size = os.path.getsize(self.actual_output_path) - if current_size != self.output_size: - - self.output_size = current_size - self.output_size_time = time.time() - self.segment_count += 1 - - # (Convert to, e.g. '27.5 MiB') - converted_size = utils.convert_bytes_to_string( - current_size, - ) - - self.download_data_callback('', str(converted_size)) - self.show_msg( - ('Downloaded segment #{0}, size {1}').format( - self.segment_count, - converted_size, - ), - ) - - # Streamlink has been passed a --stream-timeout argument; - # so add a few seconds to the usual timeout value, hoping - # that streamlink's own timeout will happen first - self.check_time = (app_obj.livestream_dl_timeout * 60) \ - + time.time() + 30 - - # Perform a timeout, if necessary - if self.check_time > 0 and self.check_time < time.time(): - - # Halt the child process - self.stop() - self.show_msg('Download timed out') - return - - - def choose_path(self): - - """Called by self.__init__(). - - When downloading from the Classic Mode tab, we don't know the video's - name, so we have to choose an arbitrary one. - - Otherwise, the video's name (and file path) is already known, so we - can just use the normal media.Video function to get it. - - Return values: - - The new value of self.expect_output_path - - """ - - video_obj = self.download_item_obj.media_data_obj - if not video_obj.dummy_flag: - return video_obj.get_actual_path(self.download_manager_obj.app_obj) - - else: - - # Retrieve the user's preferred file extension - if video_obj.dummy_format is not None: - convert_flag, file_ext, resolution \ - = utils.extract_dummy_format(video_obj.dummy_format) - - if file_ext is None: - file_ext = 'mp4' - - # Use an arbitrary filename in the form 'livestream_N.EXT' - count = 0 - while 1: - count += 1 - path = os.path.abspath( - os.path.join( - video_obj.dummy_dir, 'livestream_' + str(count) \ - + '.' + file_ext, - ), - ) - - if not os.path.isfile(path): - return path - - - def close(self): - - """Can be called by anything. - - Destructor function for this object. - """ - - # Tell the PipeReader objects to shut down, thus joining their threads - self.stdout_reader.join() - self.stderr_reader.join() - - - def create_child_process(self, cmd_list): - - """Called by self.do_download_basic(), .do_download_m3u() and - .do_download_streamlink(). - - Based on YoutubeDLDownloader._create_process(). - - Executes the system command, creating a new child process which - executes youtube-dl. - - Args: - - cmd_list (list): Python list that contains the command to execute - - Return values: - - True on success, False on an error - - """ - - # Strip double quotes from arguments - # (Since we're sending the system command one argument at a time, we - # don't need to retain the double quotes around any single argument - # and, in fact, doing so would cause an error) - cmd_list = utils.strip_double_quotes(cmd_list) - - # Create the child process - info = preexec = None - - if os.name == 'nt': - # Hide the child process window that MS Windows helpfully creates - # for us - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - # Make this child process the process group leader, so that we can - # later kill the whole process group with os.killpg - preexec = os.setsid - - try: - self.child_process = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=preexec, - startupinfo=info, - ) - - return True - - except (ValueError, OSError) as error: - # (Errors are expected and frequent) - return False - - - def download_data_callback(self, speed='', filesize=''): - - """Called by self.read_child_process() and .set_actual_output_path(). - - Passes a dictionary of values to self.download_worker_obj so the main - window can be updated. - - The dictionary is based on the one created by downloads.VideoDownloader - (but with far fewer values included). - - This function is only called when a download is in progress; - self.last_data_callback() is called at the end of it. - """ - - if self.segment_count == 0: - percent = '?' - else: - percent = str(self.segment_count) + '/?' - - dl_stat_dict = { - 'status': formats.ACTIVE_STAGE_DOWNLOAD, - 'path': self.actual_output_dir, - 'filename': self.actual_output_filename, - 'extension': self.actual_output_ext, - 'percent': percent, - 'speed': speed, - 'filesize': filesize, - 'playlist_index': 1, - 'dl_sim_flag': False, - } - - self.download_worker_obj.data_callback(dl_stat_dict) - - - def is_child_process_alive(self): - - """Called by self.do_download_basic(), .do_download_m3u(), - .do_download_streamlink() and .stop(). - - Based on YoutubeDLDownloader._proc_is_alive(). - - Called continuously during the self.do_fetch() loop to check whether - the child process has finished or not. - - Return values: - - True if the child process is alive, otherwise returns False - - """ - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def last_data_callback(self): - - """Called by self.do_download(). - - Based on YoutubeDLDownloader._last_data_hook(). - - After the child process has finished, creates a new Python dictionary - in the standard form described by - downloads.VideoDownloader.extract_stdout_data(). - - Sets key-value pairs in the dictonary, then passes it to the parent - downloads.DownloadWorker object, confirming the result of the child - process. - - The new key-value pairs are used to update the main window. - """ - - dl_stat_dict = {} - - if self.return_code == self.OK: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_FINISHED - elif self.return_code == self.ERROR: - dl_stat_dict['status'] = formats.MAIN_STAGE_ERROR - elif self.return_code == self.STOPPED: - dl_stat_dict['status'] = formats.ERROR_STAGE_STOPPED - - # Use some empty values in dl_stat_dict so that the Progress tab - # doesn't show arbitrary data from the most recent call to - # self.download_data_callback() - dl_stat_dict['path'] = '' - dl_stat_dict['filename'] = '' - dl_stat_dict['extension'] = '' - dl_stat_dict['percent'] = '' - dl_stat_dict['speed'] = '' - dl_stat_dict['filesize'] = '' - dl_stat_dict['playlist_index'] = 1 - dl_stat_dict['dl_sim_flag'] = False - - # The True argument shows that this function is the caller - self.download_worker_obj.data_callback(dl_stat_dict, True) - - - def read_child_process(self): - - """Called by self.do_download_basic(), .do_download_m3u() and - .do_download_streamlink(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Return values: - - True if either STDOUT or STDERR were read. None if both queues were - empty, or if STDERR was read and a network error was detected - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.download_manager_obj.app_obj.system_error, - 313, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # On MS Windows we use cp1252, so that Tartube can communicate with the - # Windows console - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - if self.m3u_waiting_flag: - - # Assume the first line of text received in STDOUT is the .mu3 - # manifest - self.m3u_waiting_flag = False - self.m3u_manifest = data - # (The manifest will be visible in the next system command, so - # don't show it here) - self.show_msg( - _( - 'Downloaded the .m3u manifest, now downloading the' \ - + ' livestream...'), - ) - - self.queue.task_done() - return True - - # Capture the actual output path provided by the downloader - if self.actual_output_path is None: - - output_path = None - if self.dl_mode != 'streamlink': - - match = re.search( - r'^\[download\] Destination\: (.*)\s*$', - data, - ) - if match: - output_path = match.groups()[0] - - else: - - if self.streamlink_start_flag == False \ - and re.search( - r'^\[cli\]\[info\] Writing output to', - data, - ): - # Next line contains the output path - self.streamlink_start_flag = True - - elif self.streamlink_start_flag == True: - # This line contains the output path - output_path = data - self.streamlink_start_flag = False - - if output_path: - self.set_actual_output_path(output_path) - - # Download updates in 'streamlink' download mode - if self.dl_mode == 'streamlink': - - # !!! DEBUG This has not been tested, as the message is - # !!! currently not intercepted - # e.g. - # [download][output.mp4] Written 9.5 MB (36s @ 132.6 KB/s) - match = re.search( - r'^\[download\]\[[^\[\]]+\] Written (.*) \(\S+ \@ (.*)\)', - data, - ) - - if match: - self.segment_count += 1 - - # Pass a dictionary of values to - # self.download_worker_obj so the Progress List can be - # updated - self.download_data_callback( - match.groups()[1], # Bitrate - match.groups()[0], # Filesize - ) - - # Show output in the Output tab and/or terminal (if required) - self.show_msg(data) - - # STDERR (downloads using youtube-dl, with or without .m3u; ignoring - # any empty error messages) - elif data != '' and self.dl_mode != 'streamlink': - - mod_data = utils.stream_output_is_ignorable(data) - if mod_data is not None: - - # Treat this as if it were a STDOUT message - - # Download updates in 'default' and 'default_m3u' download - # modes - match = re.search( - r'^frame.*size\=\s*([\S]+).*bitrate\=\s*([\S]+)', - mod_data, - ) - if match: - self.segment_count += 1 - - self.check_time = (app_obj.livestream_dl_timeout * 60) \ - + time.time() - - # Pass a dictionary of values to self.download_worker_obj - # so the Progress List can be updated - self.download_data_callback( - match.groups()[1], # Bitrate - match.groups()[0], # Filesize - ) - - # Show output in the Output tab and/or terminal (if required) - self.show_msg(mod_data) - - # STDERR (downloads using youtube-dl/.m3u/FFmpeg, or using streamlink; - # ignoring any empty error messages) - elif data != '': - - # Check for recognised errors/warnings, and update the appropriate - # media data object (immediately, if possible, or later - # otherwise) - self.set_error(self.download_item_obj.media_data_obj, data) - - # Show output in the Output tab and/or terminal (if required) - self.show_error(data) - - # (An error fetching the .m3u manifest is fatal) - if self.m3u_waiting_flag: - self.show_error(_('Failed to download the .m3u manifest')) - self.set_return_code(self.ERROR) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def reset(self): - - """Called by self.do_download_m3u(). - - A modified version of self.close(). - - The calling code uses two sub-processes, one after the other. This - function is called when the first process is finished to reset - everything, ready for the second function. - """ - - if self.child_process: - - # Tell the PipeReader objects to shut down, thus joining their - # threads - self.stdout_reader.join() - self.stderr_reader.join() - - self.child_process = None - self.queue = queue.PriorityQueue() - self.stdout_reader = PipeReader(self.queue, 'stdout') - self.stderr_reader = PipeReader(self.queue, 'stderr') - - - def set_actual_output_path(self, output_path): - - """Called by self.do_download_m3u() and .read_child_process(). - - The downloader's output path is captured, meaning that the download has - started. - - Updates IVs and the Progress List. - - Args: - - output_path (str): Full path to the downloader's output file - - """ - - # Update IVs - directory, filename, ext = utils.extract_path_components(output_path) - - self.actual_output_path = output_path - self.actual_output_dir = directory - self.actual_output_filename = filename - self.actual_output_ext = ext - - # Pass a dictionary of values to self.download_worker_obj so the main - # window can be updated - self.download_data_callback() - - - def set_error(self, media_data_obj, msg): - - """Wrapper for media.Video.set_error(). - - Args: - - media_data_obj (media.Video): The media data object to update. Only - videos are updated by this function - - msg (str): The error message for this video - - """ - - if not self.first_error_flag: - - self.first_error_flag = True - - # The new error is the first error/warning generated during this - # operation; remove any errors/warnings from previous operations - media_data_obj.reset_error_warning() - - # Set the new error - media_data_obj.set_error(msg) - - - def set_return_code(self, code): - - """Can be called by anything. - - Based on YoutubeDLDownloader._set_returncode(). - - After the child process has terminated with an error of some kind, - sets a new value for self.return_code, but only if the new return code - is higher in the hierarchy of return codes than the current value. - - Args: - - code (int): A return code in the range 0-5 - - """ - - if code >= self.return_code: - self.return_code = code - - - def show_cmd(self, cmd): - - """Can be called by anything. - - Shows a system command in the Output tab and/or terminal window, if - required. - - Args: - - cmd (str): The system command to display - - """ - - # Import the main app (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Display the command in the Output tab, if allowed - if app_obj.ytdl_output_system_cmd_flag: - app_obj.main_win_obj.output_tab_write_system_cmd( - self.download_worker_obj.worker_id, - cmd, - ) - - # Display the message in the terminal, if allowed - if app_obj.ytdl_write_system_cmd_flag: - try: - print(cmd) - except: - print('Command echoed in STDOUT with unprintable characters') - - # Display the message in the downloader log, if allowed - if app_obj.ytdl_log_system_cmd_flag: - app_obj.write_downloader_log(cmd) - - - def show_msg(self, msg): - - """Can be called by anything. - - Shows a message in the Output tab and/or terminal window, if required. - - Args: - - msg (str): The message to display - - """ - - # Import the main app (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Display the message in the Output tab, if allowed - if app_obj.ytdl_output_stdout_flag: - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - msg, - ) - - # Display the message in the terminal, if allowed - if app_obj.ytdl_write_stdout_flag: - # Git #175, Japanese text may produce a codec error here, - # despite the .decode() call above - try: - print( - msg.encode(utils.get_encoding(), 'replace'), - ) - except: - print('Message echoed in STDOUT with unprintable characters') - - # Write the message to the downloader log, if allowed - if app_obj.ytdl_log_stdout_flag: - app_obj.write_downloader_log(msg) - - - def show_error(self, msg): - - """Can be called by anything. - - Shows an error message in the Output tab and/or terminal window, if - required. - - Args: - - msg (str): The message to display - - """ - - # Import the main app (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Display the message in the Output tab, if allowed - if app_obj.ytdl_output_stdout_flag: - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - msg, - ) - - # Display the message in the terminal, if allowed - if app_obj.ytdl_write_stderr_flag: - # Git #175, Japanese text may produce a codec error here, - # despite the .decode() call above - try: - print( - msg.encode(utils.get_encoding(), 'replace'), - ) - except: - print('Message echoed in STDERR with unprintable characters') - - # Write the message to the downloader log (if required) - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(msg) - - - def stop(self): - - """Called by DownloadWorker.close(). - - Terminates the child process and sets this object's return code to - self.STOPPED. - """ - - if self.is_child_process_alive(): - - if os.name == 'nt': - # os.killpg is not available on MS Windows (see - # https://bugs.python.org/issue5115 ) - self.child_process.kill() - - # When we kill the child process on MS Windows the return code - # gets set to 1, so we want to reset the return code back to - # 0 - self.child_process.returncode = 0 - - else: - os.killpg(self.child_process.pid, signal.SIGKILL) - - self.set_return_code(self.STOPPED) - - - def stop_soon(self): - - """Can be called by anything. Currently called by - mainwin.MainWin.on_progress_list_stop_soon(). - - StreamDownloader only downloads a single video, so we can ignore an - instruction to stop after that download has finished. - """ - - pass - - -class JSONFetcher(object): - - """Called by downloads.DownloadWorker.check_rss(). - - Python class to download JSON data for a video which is believed to be a - livestream, using youtube-dl. - - The video has been found in the channel's/playlist's RSS feed, but not by - youtube-dl, when the channel/playlist was last checked downloaded. - - If the data can be downloaded, we assume that the livestream is currently - broadcasting. If we get a 'This video is unavailable' error, we assume that - the livestream is waiting to start. - - This is the behaviour exhibited on YouTube. It might work on other - compatible websites, too, if the user has set manually set the URL for the - channel/playlist RSS feed. - - This class creates a system child process and uses the child process to - instruct youtube-dl to fetch the JSON data for the video. - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - If one of the two outcomes described above takes place, the media.Video - object's IVs are updated to mark it as a livestream. - - Args: - - download_manager_obj (downloads.DownloadManager): The download manager - object handling the entire download operation - - download_worker_obj (downloads.DownloadWorker): The parent download - worker object. The download manager uses multiple workers to - implement simultaneous downloads. The download manager checks for - free workers and, when it finds one, assigns it a - download.DownloadItem object. When the worker is assigned a - download item, it creates a new instance of this object for each - detected livestream, and waits for this object to complete its - task - - container_obj (media.Channel, media.Playlist): The channel/playlist - in which a livestream has been detected - - entry_dict (dict): A dictionary of values generated when reading the - RSS feed (provided by the Python feedparser module. The dictionary - represents available data for a single livestream video - - Warnings: - - The calling function is responsible for calling the close() method - when it's finished with this object, in order for this object to - properly close down. - - """ - - - # Standard class methods - - - def __init__(self, download_manager_obj, download_worker_obj, \ - container_obj, entry_dict): - - # IV list - class objects - # ----------------------- - # The downloads.DownloadManager object handling the entire download - # operation - self.download_manager_obj = download_manager_obj - # The parent downloads.DownloadWorker object - self.download_worker_obj = download_worker_obj - # The media.Channel or media.Playlist object in which a livestream has - # been detected - self.container_obj = container_obj - - # The child process created by self.create_child_process() - self.child_process = None - - # Read from the child process STDOUT (i.e. self.child_process.stdout) - # and STDERR (i.e. self.child_process.stderr) in an asynchronous way - # by polling this queue.PriorityQueue object - self.queue = queue.PriorityQueue() - self.stdout_reader = PipeReader(self.queue, 'stdout') - self.stderr_reader = PipeReader(self.queue, 'stderr') - - - # IV list - other - # --------------- - # A dictionary of values generated when reading the RSS feed (provided - # by the Python feedparser module. The dictionary represents - # available data for a single livestream video - self.entry_dict = entry_dict - # Important data is extracted from the entry (below), and added to - # these IVs, ready for use - self.video_name = None - self.video_source = None - self.video_descrip = None - self.video_thumb_source = None - self.video_upload_time = None - - # The time (in seconds) between iterations of the loop in - # self.do_fetch() - self.sleep_time = 0.1 - - - # Code - # ---- - # Initialise IVs from the RSS feed entry for the livestream video - # (saves a bit of time later) - if 'title' in entry_dict: - self.video_name = entry_dict['title'] - - if 'link' in entry_dict: - self.video_source = entry_dict['link'] - - if 'summary' in entry_dict: - self.video_descrip = entry_dict['summary'] - - if 'media_thumbnail' in entry_dict \ - and entry_dict['media_thumbnail'] \ - and 'url' in entry_dict['media_thumbnail'][0]: - self.video_thumb_source = entry_dict['media_thumbnail'][0]['url'] - - if 'published_parsed' in entry_dict: - - try: - # A time.struct_time object; convert to Unix time, to match - # media.Video.upload_time - dt_obj = datetime.datetime.fromtimestamp( - time.mktime(entry_dict['published_parsed']), - ) - - self.video_upload_time = int(dt_obj.timestamp()) - - except: - self.video_upload_time = None - - - # Public class methods - - - def do_fetch(self): - - """Called by downloads.DownloadWorker.check_rss(). - - Downloads JSON data for the livestream video whose URL is - self.video_source. - - If the data can be downloaded, we assume that the livestream is - currently broadcasting. If we get a 'This video is unavailable' error, - we assume that the livestream is waiting to start. - """ - - # Import the main app (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Convert a youtube-dl path beginning with ~ (not on MS Windows) - # (code copied from utils.generate_ytdl_system_cmd() ) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Generate the system command (but don't display it in the Output tab) - if app_obj.ytdl_path_custom_flag: - cmd_list = ['python3'] + [ytdl_path] + ['--dump-json'] \ - + [self.video_source] - else: - cmd_list = [ytdl_path] + ['--dump-json'] + [self.video_source] - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process' - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # Wait for the process to finish - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Process has finished. Read from STDOUT and STDERR - if self.read_child_process(): - - # Download the video's thumbnail, if possible - if self.video_thumb_source: - - # Get the thumbnail's extension... - remote_file, remote_ext = os.path.splitext( - self.video_thumb_source, - ) - - # ...and thus get the filename used by youtube-dl when storing - # the thumbnail locally (assuming that the video's name, and - # the filename when it is later downloaded, are the same) - local_thumb_path = os.path.abspath( - os.path.join( - self.container_obj.get_actual_dir(app_obj), - self.video_name + remote_ext, - ), - ) - - options_obj = self.download_worker_obj.options_manager_obj - if not options_obj.options_dict['sim_keep_thumbnail']: - local_thumb_path = utils.convert_path_to_temp( - app_obj, - local_thumb_path, - ) - - elif options_obj.options_dict['move_thumbnail']: - local_thumb_path = os.path.abspath( - os.path.join( - self.container_obj.get_actual_dir(app_obj), - app_obj.thumbs_sub_dir, - self.video_name + remote_ext, - ) - ) - - if local_thumb_path: - try: - request_obj = requests.get( - self.video_thumb_source, - timeout = app_obj.request_get_timeout, - ) - - with open(local_thumb_path, 'wb') as outfile: - outfile.write(request_obj.content) - - except: - pass - - # Convert .webp thumbnails to .jpg, if required - if local_thumb_path is not None \ - and not app_obj.ffmpeg_fail_flag \ - and app_obj.ffmpeg_convert_webp_flag \ - and not app_obj.ffmpeg_manager_obj.convert_webp( - local_thumb_path - ): - app_obj.set_ffmpeg_fail_flag(True) - GObject.timeout_add( - 0, - app_obj.system_error, - 314, - app_obj.ffmpeg_fail_msg, - ) - - - def close(self): - - """Called by downloads.DownloadWorker.check_rss(). - - Destructor function for this object. - """ - - # Tell the PipeReader objects to shut down, thus joining their threads - self.stdout_reader.join() - self.stderr_reader.join() - - - def create_child_process(self, cmd_list): - - """Called by self.do_fetch(). - - Based on YoutubeDLDownloader._create_process(). - - Executes the system command, creating a new child process which - executes youtube-dl. - - Args: - - cmd_list (list): Python list that contains the command to execute - - Return values: - - True on success, False on an error - - """ - - # Strip double quotes from arguments - # (Since we're sending the system command one argument at a time, we - # don't need to retain the double quotes around any single argument - # and, in fact, doing so would cause an error) - cmd_list = utils.strip_double_quotes(cmd_list) - - # Create the child process - info = preexec = None - - if os.name == 'nt': - # Hide the child process window that MS Windows helpfully creates - # for us - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - # Make this child process the process group leader, so that we can - # later kill the whole process group with os.killpg - preexec = os.setsid - - try: - self.child_process = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=preexec, - startupinfo=info, - ) - - return True - - except (ValueError, OSError) as error: - # (Errors are expected and frequent) - return False - - - def is_child_process_alive(self): - - """Called by self.do_fetch() and self.stop(). - - Based on YoutubeDLDownloader._proc_is_alive(). - - Called continuously during the self.do_fetch() loop to check whether - the child process has finished or not. - - Return values: - - True if the child process is alive, otherwise returns False. - - """ - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def read_child_process(self): - - """Called by self.do_fetch(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - For this JSONFetcher object, the order doesn't matter very much: we - are expecting data in either STDOUT or STDERR. - - Return values: - - True if either STDOUT or STDERR were read, None if both queues were - empty - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.download_manager_obj.app_obj.system_error, - 315, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # Convert bytes to string - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - if data[:1] == '{': - - # Broadcasting livestream detected; create a new media.Video - # object - GObject.timeout_add( - 0, - app_obj.create_livestream_from_download, - self.container_obj, - 2, # Livestream has started - self.video_name, - self.video_source, - self.video_descrip, - self.video_upload_time, - ) - - # STDERR (ignoring any empty error messages) - elif data != '': - - live_data_dict = utils.extract_livestream_data(data) - if live_data_dict: - - # Waiting livestream detected; create a new media.Video object - GObject.timeout_add( - 0, - app_obj.create_livestream_from_download, - self.container_obj, - 1, # Livestream waiting to start - self.video_name, - self.video_source, - self.video_descrip, - self.video_upload_time, - live_data_dict, - ) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def stop(self): - - """Called by DownloadWorker.close(). - - Terminates the child process. - """ - - if self.is_child_process_alive(): - - if os.name == 'nt': - # os.killpg is not available on MS Windows (see - # https://bugs.python.org/issue5115 ) - self.child_process.kill() - - # When we kill the child process on MS Windows the return code - # gets set to 1, so we want to reset the return code back to - # 0 - self.child_process.returncode = 0 - - else: - os.killpg(self.child_process.pid, signal.SIGKILL) - - -class StreamManager(threading.Thread): - - """Called by mainapp.TartubeApp.livestream_manager_start(). - - Python class to create a system child process, to check media.Video objects - already marked as livestreams, to see whether they have started or stopped - broadcasting. - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - super(StreamManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The downloads.MiniJSONFetcher object used to check each media.Video - # object marked as a livestream - self.mini_fetcher_obj = None - - - # IV list - other - # --------------- - # A local list of media.Video objects marked as livestreams (in case - # the mainapp.TartubeApp IV changes during the course of this - # operation) - # Dictionary in the form: - # key = media data object's unique .dbid - # value = the media data object itself - self.video_dict = {} - - # Flag set to False if self.stop_livestream_operation() is called - # The False value halts the loop in self.run() - self.running_flag = True - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Initiates the download. - """ - - # Generate a local list of media.Video objects marked as livestreams - # (in case the mainapp.TartubeApp IV changes during the course of - # this operation) - self.video_dict = self.app_obj.media_reg_live_dict.copy() - - for video_obj in self.video_dict.values(): - - if not self.running_flag: - break - - # For each media.Video in turn, try to fetch JSON data - # If the data is received, assume the livestream is live. If a - # 'This video is unavailable' error is received, the livestream - # is waiting to go live - self.mini_fetcher_obj = MiniJSONFetcher(self, video_obj) - - # Then execute the assigned job - self.mini_fetcher_obj.do_fetch() - - # Call the destructor function of the MiniJSONFetcher object - # (first checking it still exists, in case - # self.stop_livestream_operation() has been called) - if self.mini_fetcher_obj: - self.mini_fetcher_obj.close() - self.mini_fetcher_obj = None - - # Operation complete. If self.stop_livestream_operation() was called, - # then the mainapp.TartubeApp function has already been called - if self.running_flag: - self.running_flag = False - self.app_obj.livestream_manager_finished() - - - def stop_livestream_operation(self): - - """Can be called by anything. - - Based on downloads.DownloadManager.stop_downloads(). - - Stops the livestream operation. On the next iteration of self.run()'s - loop, the downloads.MiniJSONFetcher objects are cleaned up. - """ - - self.running_flag = False - - # Halt the MiniJSONFetcher; it doesn't matter if it was in the middle - # of doing something - if self.mini_fetcher_obj: - self.mini_fetcher_obj.close() - self.mini_fetcher_obj = None - - # Call the mainapp.TartubeApp function to update everything (it's not - # called from self.run(), in this situation) - self.app_obj.livestream_manager_finished() - - -class MiniJSONFetcher(object): - - """Called by downloads.StreamManager.run(). - - A modified version of downloads.JSONFetcher (the former is called by - downloads.DownloadWorker only; using a second Python class for the same - objective makes the code somewhat simpler). - - Python class to fetch JSON data for a livestream video, using youtube-dl. - - Creates a system child process and uses the child process to instruct - youtube-dl to fetch the JSON data for the video. - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - Args: - - livestream_manager_obj (downloads.StreamManager): The livestream - manager object handling the entire livestream operation - - video_obj (media.Video): The livestream video whose JSON data should be - fetched (the equivalent of right-clicking the video in the Video - Catalogue, and selecting 'Check this video') - - """ - - - # Standard class methods - - - def __init__(self, livestream_manager_obj, video_obj): - - # IV list - class objects - # ----------------------- - # The downloads.StreamManager object handling the entire livestream - # operation - self.livestream_manager_obj = livestream_manager_obj - # The media.Video object for which new JSON data must be fetched - # (the equivalent of right-clicking the video in the Video Catalogue, - # and selecting 'Check this video') - self.video_obj = video_obj - - # The child process created by self.create_child_process() - self.child_process = None - - # Read from the child process STDOUT (i.e. self.child_process.stdout) - # and STDERR (i.e. self.child_process.stderr) in an asynchronous way - # by polling this queue.PriorityQueue object - self.queue = queue.PriorityQueue() - self.stdout_reader = PipeReader(self.queue, 'stdout') - self.stderr_reader = PipeReader(self.queue, 'stderr') - - - # IV list - other - # --------------- - # The time (in seconds) between iterations of the loop in - # self.do_fetch() - self.sleep_time = 0.1 - - - # Public class methods - - - def do_fetch(self): - - """Called by downloads.StreamManager.run(). - - Downloads JSON data for the livestream video, self.video_obj. - - If the data can be downloaded, we assume that the livestream is - currently broadcasting. If we get a 'This video is unavailable' error, - we assume that the livestream is waiting to start. - """ - - # Import the main app (for convenience) - app_obj = self.livestream_manager_obj.app_obj - - # Convert a youtube-dl path beginning with ~ (not on MS Windows) - # (code copied from utils.generate_ytdl_system_cmd() ) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Generate the system command - if app_obj.ytdl_path_custom_flag: - cmd_list = ['python3'] + [ytdl_path] + ['--dump-json'] \ - + [self.video_obj.source] - else: - cmd_list = [ytdl_path] + ['--dump-json'] + [self.video_obj.source] - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - # Wait for the process to finish - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - - def close(self): - - """Called by downloads.StreamManager.run(). - - Destructor function for this object. - """ - - # Tell the PipeReader objects to shut down, thus joining their threads - self.stdout_reader.join() - self.stderr_reader.join() - - - def create_child_process(self, cmd_list): - - """Called by self.do_fetch(). - - Based on YoutubeDLDownloader._create_process(). - - Executes the system command, creating a new child process which - executes youtube-dl. - - Args: - - cmd_list (list): Python list that contains the command to execute - - Return values: - - True on success, False on an error - - """ - - # Strip double quotes from arguments - # (Since we're sending the system command one argument at a time, we - # don't need to retain the double quotes around any single argument - # and, in fact, doing so would cause an error) - cmd_list = utils.strip_double_quotes(cmd_list) - - # Create the child process - info = preexec = None - - if os.name == 'nt': - # Hide the child process window that MS Windows helpfully creates - # for us - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - # Make this child process the process group leader, so that we can - # later kill the whole process group with os.killpg - preexec = os.setsid - - try: - self.child_process = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=preexec, - startupinfo=info, - ) - - return True - - except (ValueError, OSError) as error: - # (Errors are expected and frequent) - return False - - - def is_child_process_alive(self): - - """Called by self.do_fetch() and self.stop(). - - Based on YoutubeDLDownloader._proc_is_alive(). - - Called continuously during the self.do_fetch() loop to check whether - the child process has finished or not. - - Return values: - - True if the child process is alive, otherwise returns False - - """ - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def parse_json(self, stdout): - - """Called by self.do_fetch(). - - Code copied from downloads.VideoDownloader.extract_stdout_data(). - - Converts the receivd JSON data into a dictionary, and returns the - dictionary. - - Args: - - stdout (str): A string of JSON data as it was received from - youtube-dl (and starting with the character { ) - - Return values: - - The JSON data, converted into a Python dictionary - - """ - - # (Try/except to check for invalid JSON) - try: - return json.loads(stdout) - - except: - GObject.timeout_add( - 0, - app_obj.system_error, - 316, - 'Invalid JSON data received from server', - ) - - return {} - - - def read_child_process(self): - - """Called by self.do_fetch(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Return values: - - True if either STDOUT or STDERR were read, None if both queues were - empty - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Import the main application (for convenience) - app_obj = self.livestream_manager_obj.app_obj - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.download_manager_obj.app_obj.system_error, - 317, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # Convert bytes to string - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - if data[:1] == '{': - - # Broadcasting livestream detected - json_dict = self.parse_json(data) - if self.video_obj.live_mode == 1: - - # Waiting livestream has gone live - GObject.timeout_add( - 0, - app_obj.mark_video_live, - self.video_obj, - 2, # Livestream is broadcasting - {}, # No livestream data - True, # Don't update Video Index yet - True, # Don't update Video Catalogue yet - ) - - elif self.video_obj.live_mode == 2 \ - and (not 'is_live' in json_dict or not json_dict['is_live']): - - # Broadcasting livestream has finished - GObject.timeout_add( - 0, - app_obj.mark_video_live, - self.video_obj, - 0, # Livestream has finished - {}, # Reset any livestream data - None, # Reset any l/s server messages - True, # Don't update Video Index yet - True, # Don't update Video Catalogue yet - ) - - # The video's name and description might change during the - # livestream; update them, if so - if 'title' in json_dict: - self.video_obj.set_nickname(json_dict['title']) - - if 'id' in json_dict: - self.video_obj.set_vid(json_dict['id']) - - if 'description' in json_dict: - self.video_obj.set_video_descrip( - app_obj, - json_dict['description'], - app_obj.main_win_obj.descrip_line_max_len, - ) - - # STDERR (ignoring any empty error messages) - elif data != '': - - # (v2.2.100: In approximately October 2020, YouTube started using a - # new error message for livestreams waiting to start) - if self.video_obj.live_mode == 1: - - live_data_dict = utils.extract_livestream_data(data) - if live_data_dict: - self.video_obj.set_live_data(live_data_dict) - - elif self.video_obj.live_mode == 2 \ - and re.search('This video is unavailable', data): - - # The livestream broadcast has been deleted by its owner (or is - # not available on the website, possibly temporarily) - app_obj.add_media_reg_live_vanished_dict(self.video_obj), - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def stop(self): - - """Called by DownloadWorker.close(). - - Terminates the child process. - """ - - if self.is_child_process_alive(): - - if os.name == 'nt': - # os.killpg is not available on MS Windows (see - # https://bugs.python.org/issue5115 ) - self.child_process.kill() - - # When we kill the child process on MS Windows the return code - # gets set to 1, so we want to reset the return code back to - # 0 - self.child_process.returncode = 0 - - else: - os.killpg(self.child_process.pid, signal.SIGKILL) - - -class CustomDLManager(object): - - """Called by mainapp.TartubeApp.create_custom_dl_manager(). - - Python class to store settings for a custom download. The user can create - as many instances of this object as they like, and can launch a custom - download using settings from any of them. - - Args: - - uid (int): Unique ID for this custom download manager (unique only to - this class of objects) - - name (str): Non-unique name forthis custom download manager - - """ - - - # Standard class methods - - - def __init__(self, uid, name): - - # IV list - other - # --------------- - # Unique ID for this custom download manager - self.uid = uid - # A non-unique name for this custom download manager - self.name = name - - # If True, during a custom download, download every video which is - # marked as not downloaded (often after clicking the 'Check all' - # button); don't download channels/playlists directly - self.dl_by_video_flag = False - # If True, during a custom download, perform a simulated download first - # (as happens by default in custom downloads launched from the - # Classic Mode tab). Ignored if self.dl_by_video_flag is False - self.dl_precede_flag = False - - # If True, during a custom download, only download the video if - # subtitles are available for it. Ignored if self.dl_precede_flag is - # False - self.dl_if_subs_flag = False - # If set, during the checking stage of a custom download, don't add - # (checked) videos to Tartube's database. Ignored if - # self.dl_if_subs_flag is False - self.ignore_if_no_subs_flag = False - # If set, during a custom download, only download the video if - # subtitles in any of these formats are available for it. Each item - # in the list is a value in formats.LANGUAGE_CODE_DICT (e.g. 'en', - # 'live_chat'). Ignored if self.dl_if_subs_flag is False - self.dl_if_subs_list = [] - - # If True, during a custom download, split a video into video clips - # using its timestamps. Ignored if self.dl_by_video_flag is False - # Note that IVs for splitting videos (e.g. - # mainapp.TartubeApp.split_video_name_mode) apply in this situation - # as well - self.split_flag = False - # If True, during a custom download, video slices identified by - # SponsorBlock are removed. Ignored if self.dl_by_video_flag is - # False, or if self.split_flag is True - self.slice_flag = False - # A dictionary specifying which categories of video slice should be - # removed. Keys are SponsorBlock categories; values are True to - # remove the slice, False to retain it - # NB A sorted list of keys from this dictionary appears in - # formats.SPONSORBLOCK_CATEGORY_LIST - self.slice_dict = { - 'sponsor': True, - 'selfpromo': False, - 'interaction': False, - 'intro': False, - 'outro': False, - 'preview': False, - 'music_offtopic': False, - } - # If True, during a custom download, a delay (in minutes) is applied - # between media data object downloads. When applied to a - # channel/playlist, the delay occurs after the whole channel/ - # playlist. When applied directly to videos, the delay occurs after - # each video - # NB The delay is applied during real downloads, but not during - # simulated downloads (operation types 'custom_sim' or 'classic_sim') - self.delay_flag = False - # The maximum delay to apply (in minutes, minimum value 0.2). Ignored - # if self.delay_flag is False - self.delay_max = 5 - # The minimum delay to apply (in minutes, minimum value 0, maximum - # value self.delay_max). If specified, the delay is a random length - # of time between this value and self.delay_max. Ignored if - # self.delay_flag is False - self.delay_min = 0 - - # During a custom download, any videos whose source URL is YouTube can - # be diverted to another website. This IV uses the values: - # 'default' - Use the original YouTube URL - # 'hooktube' - Divert to hooktube.com - # 'invidious' - Divert to invidio.us - # 'other' - user enters their own alternative front-end website - self.divert_mode = 'default' - # If self.divert_mode is 'other', the address of the YouTube - # alternative. The string directly replaces the 'youtube.com' part of - # a URL; so the string must be something like 'hooktube.com' not - # 'http://hooktube.com' or anything like that - # Ignored if it does not contain at least 3 characters. Ignored for any - # other value of self.divert_mode - self.divert_website = '' - - # If True, don't download broadcasting livestreams. Ignored if - # self.dl_by_video_flag is False - self.ignore_stream_flag = False - # If True, don't download finished livestreams. Ignored if - # self.dl_by_video_flag is False - self.ignore_old_stream_flag = False - # If True, only download broadcasting livestreams. Ignored if - # self.dl_by_video_flag is False. Mutually incompatible with - # self.ignore_stream_flag - self.dl_if_stream_flag = False - # If True, only download finished livestreams. Ignored if - # self.dl_by_video_flag is False. Mutually incompatible with - # self.ignore_old_stream_flag - self.dl_if_old_stream_flag = False - - - # Public class methods - - - def clone_settings(self, other_obj): - - """Called by mainapp.TartubeApp.clone_custom_dl_manager_from_window(). - - Clones custom download settings from the specified object into this - object, completely replacing this object's settings. - - Args: - - other_obj (downloads.CustomDLManager): The custom download manager - object (usually the current one), from which settings will be - cloned - - """ - - self.dl_by_video_flag = other_obj.dl_by_video_flag - self.dl_if_subs_flag = other_obj.dl_if_subs_flag - self.ignore_if_no_subs_flag = other_obj.ignore_if_no_subs_flag - self.dl_if_subs_list = other_obj.dl_if_subs_list - self.split_flag = other_obj.split_flag - self.slice_flag = other_obj.slice_flag - self.slice_dict = other_obj.slice_dict.copy() - self.divert_mode = other_obj.divert_mode - self.divert_website = other_obj.divert_website - self.delay_flag = other_obj.delay_flag - self.delay_min = other_obj.delay_min - - - def reset_settings(self): - - """Currently not called by anything (but might be needed in the - future). - - Resets settings to their default values. - """ - - self.dl_by_video_flag = False - self.dl_if_subs_flag = False - self.ignore_if_no_subs_flag = False - self.dl_if_subs_list = [] - self.split_flag = False - self.slice_flag = False - self.slice_dict = { - 'sponsor': True, - 'selfpromo': False, - 'interaction': False, - 'intro': False, - 'outro': False, - 'preview': False, - 'music_offtopic': False, - } - self.divert_mode = 'default' - self.divert_website = '' - self.delay_flag = False - self.delay_max = 5 - self.delay_min = 0 - - - def set_dl_precede_flag(self, flag): - - """Can be called by anything. Mostly called by - mainapp.TartubeApp.start() and .set_dl_precede_flag(). - - Updates the IV. - - Args: - - flag (bool): The new value of the IV - - """ - - if not flag: - self.dl_precede_flag = False - else: - self.dl_by_video_flag = True - self.dl_precede_flag = True - - -class PipeReader(threading.Thread): - - """Called by downloads.VideoDownloader.__init__(). - - Based on the PipeReader class in youtube-dl-gui. - - Python class used by downloads.VideoDownloader, downloads.ClipDownloader, - downloads.StreamDownloader, downloads.JSONFetcher, - downloads.MiniJSONFetcher, info.InfoManager and updates.UpdateManager, - to avoid deadlocks when reading from child process pipes STDOUT and STDERR. - - This class uses python threads and queues in order to read from child - process pipes in an asynchronous way. - - Args: - - queue (queue.PriorityQueue): Python queue to store the output of the - child process - - pipe_type (str): This object reads from either 'stdout' or 'stderr' - - Warnings: - - All the actions are based on 'str' types. The calling function must - convert the queued items back to 'unicode', if necessary - - """ - - - # Standard class methods - - - def __init__(self, queue, pipe_type): - - super(PipeReader, self).__init__() - - # IV list - other - # --------------- - # Python queue.PriorityQueue to store the output of the child process - self.queue = queue - # This object reads from either 'stdout' or 'stderr' - self.pipe_type = pipe_type - - # The time (in seconds) between iterations of the loop in self.run() - # Without some kind of delay, the GUI interface becomes sluggish. The - # length of the delay doesn't matter, so make it as short as - # reasonably possible - self.sleep_time = 0.001 - # Flag that is set to False by self.join(), which enables the loop in - # self.run() to terminate - self.running_flag = True - # Set by self.attach_fh(). The filehandle for the child process STDOUT - # or STDERR, e.g. downloads.VideoDownloader.child_process.stdout - self.fh = None - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Reads from STDOUT or STERR using the attached filed filehandle. - """ - - # Use this flag so that the loop can ignore FFmpeg error messsages - # (because the parent VideoDownloader object shouldn't use that as a - # serious error) - ignore_line = False - - while self.running_flag: - - if self.fh is not None: - - # Read the filehandle until the sentinel line (matching '') is - # found, marking the end of the file; see - # https://stackoverflow.com/questions/52446415/ - # line-in-iterfp-readline-rather-than-line-in-fp - for line in iter(self.fh.readline, str('')): - - if line == b'': - # End of file - break - - if str.encode('ffmpeg version') in line: - ignore_line = True - - if not ignore_line: - - # Add a tuple to the queue.PriorityQueue. The queue's - # entries are sorted by the first item of the tuple, - # so the queue is read in the correct order - self.queue.put_nowait( - [time.time(), self.pipe_type, line], - ) - - self.fh = None - ignore_line = False - - # This delay is required; see the comments in self.__init__() - time.sleep(self.sleep_time) - - - def attach_fh(self, fh): - - """Called by downloads.VideoDownloader.do_download() and comparable - functions. - - Sets the filehandle for the child process STDOUT or STDERR, e.g. - downloads.VideoDownloader.child_process.stdout - - Args: - - fh (filehandle): The open filehandle for STDOUT or STDERR - - """ - - self.fh = fh - - - def join(self, timeout=None): - - """Called by downloads.VideoDownloader.close(), which is the destructor - function for that object. - - Join the thread and update IVs. - - Args: - - timeout (-): No calling code sets a timeout - - """ - - self.running_flag = False - super(PipeReader, self).join(timeout) diff --git a/build/lib/tartube/ffmpeg_tartube.py b/build/lib/tartube/ffmpeg_tartube.py deleted file mode 100644 index 0209362a..00000000 --- a/build/lib/tartube/ffmpeg_tartube.py +++ /dev/null @@ -1,1261 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""FFmpeg manager classes.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import os -import re -import shutil -import subprocess - - -# Import our modules -import mainapp -import utils - - -# Classes - - -class FFmpegManager(object): - - """Called by mainapp.TartubeApp.__init__(). - - Python class to manage calls to FFmpeg that Tartube wants to make, - independently of youtube-dl. - - Most of the code in this file has been updated from youtube-dl itself. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - - super(FFmpegManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The main application - self.app_obj = app_obj - - - # Public class methods - - - def convert_webp(self, thumbnail_filename): - - """Called by mainapp.TartubeApp.update_video_when_file_found(), - downloads.VideoDownloader.confirm_sim_video() and - downloads.JSONFetcher.do_fetch(). - - Adapted from youtube-dl/youtube-dl/postprocessor/embedthumbnail.py. - - In June 2020, YouTube changed its image format from .jpg to .webp. - Unfortunately, the Gtk library doesn't support that format. - - Worse still, YouTube also began sending .webp thumbnails mislabelled as - .jpg. - - In response, in September 2020 youtube-dl implemented a fix for - embedded thumbnails, using FFmpeg to convert .webp to .jpg. That code - has been adapted here, so that YouTube thumbnails can be converted and - made visible in the main window again. - - Args: - - thumbnail_filename (str): Full path to the webp file to be - converted to jpg - - Return values: - - False if an attempted conversion fails, or True otherwise - (including when no conversion is attempted) - - """ - - # Sanity check - if not os.path.isfile(thumbnail_filename) \ - or self.app_obj.ffmpeg_fail_flag: - return True - - # Retain original thumbnails, if required - if self.app_obj.ffmpeg_convert_webp_flag \ - and self.app_obj.ffmpeg_retain_webp_flag: - retain_flag = True - else: - retain_flag = False - - # Correct extension for .webp files with the wrong extension - # (youtube-dl #25687, #25717) - _, thumbnail_ext = os.path.splitext(thumbnail_filename) - if thumbnail_ext: - - # Remove the initial full stop - thumbnail_ext = thumbnail_ext[1:].lower() - - if thumbnail_ext != 'webp' and self.is_webp(thumbnail_filename): - - # .webp mislabelled as .jpg - thumbnail_webp_filename = self.replace_extension( - thumbnail_filename, - 'webp', - ) - - if not retain_flag: - try: - os.rename(thumbnail_filename, thumbnail_webp_filename) - except: - return False - else: - try: - shutil.copyfile( - thumbnail_filename, - thumbnail_webp_filename, - ) - except: - return False - - thumbnail_filename = thumbnail_webp_filename - thumbnail_ext = 'webp' - - elif thumbnail_ext == 'webp' and \ - self.is_mislabelled_webp(thumbnail_filename): - - # .jpg mislabelled as .webp (Git #478) - thumbnail_jpg_filename = self.replace_extension( - thumbnail_filename, - 'jpg', - ) - - if not retain_flag: - try: - os.rename(thumbnail_filename, thumbnail_jpg_filename) - except: - return False - else: - try: - shutil.copyfile( - thumbnail_filename, - thumbnail_jpg_filename, - ) - except: - return False - - thumbnail_filename = thumbnail_jpg_filename - thumbnail_ext = 'jpg' - - # Convert unsupported thumbnail formats to JPEG - # (youtube-dl #25687, #25717) - if thumbnail_ext not in ['jpg', 'png']: - - # NB: % is supposed to be escaped with %% but this does not work - # for input files so working around with standard substitution - escaped_thumbnail_filename = thumbnail_filename.replace('%', '#') - escaped_thumbnail_jpg_filename = self.replace_extension( - escaped_thumbnail_filename, - 'jpg', - ) - - # Handle special characters - try: - os.rename(thumbnail_filename, escaped_thumbnail_filename) - except: - return False - - # Run FFmpeg to convert the thumbnail(s) - success_flag, msg = self.run_ffmpeg( - escaped_thumbnail_filename, - escaped_thumbnail_jpg_filename, - ['-bsf:v', 'mjpeg2jpeg'], - ) - - if not success_flag: - - # Conversion failed; most likely because FFmpeg is not - # installed - # Rename back to unescaped - try: - os.rename(escaped_thumbnail_filename, thumbnail_filename) - except: - pass - - return False - - else: - - # Conversion succeeded - # Rename the (converted file) to unescaped for further - # processing - thumbnail_jpg_filename = self.replace_extension( - thumbnail_filename, - 'jpg', - ) - - try: - os.rename( - escaped_thumbnail_jpg_filename, - thumbnail_jpg_filename - ) - except: - return False - - if not retain_flag: - - # The original .webp file is not retained - self.app_obj.remove_file(escaped_thumbnail_filename) - - # Procedure complete - return True - - - def _ffmpeg_filename_argument(self, path): - - """Called by self.run_ffmpeg_multiple_files(). - - Adapted from youtube-dl/youtube-dl/postprocessor/ffmpeg.py. - - Returns a filename in a format that won't confuse FFmpeg. - - Args: - - path (str): The full path to a file to be processed by FFmpeg - - Return values: - - The modified string - - """ - - # Always use 'file:' because the filename may contain ':' (ffmpeg - # interprets that as a protocol) or can start with '-' (-- is broken - # in ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for - # details) - # Also leave '-' intact in order not to break streaming to stdout - return 'file:' + path if path != '-' else path - - - def get_executable(self): - - """Called by self.run_ffmpeg_multiple_files(). - - Not adapted from youtube-dl. - - Returns the path to the FFmpeg executable, which the user may have - specified themselves. If not, assume ffmpeg is in the system path. - - Return values: - - The path to the executable - - """ - - if self.app_obj.ffmpeg_path: - return self.app_obj.ffmpeg_path - else: - return 'ffmpeg' - - - def is_webp(self, path): - - """Called by self.convert_webp() and - utils.find_thumbnail_webp_intact_or_broken(). - - Adapted from youtube-dl/youtube-dl/postprocessor/embedthumbnail.py. - - Tests whether a file is a .webp file (perhaps mislabelled as a .jpg - file). - - Args: - - path (str): The full path to a file to be processed by FFmpeg - - """ - - with open(path, 'rb') as fh: - data = fh.read(12) - - # Test .webp magic number - return data[0:4] == b'RIFF' and data[8:] == b'WEBP' - - - def is_mislabelled_webp(self, path): - - """Called by self.convert_webp() and - utils.find_thumbnail_webp_intact_or_broken(). - - Adapted from self.is_webp(). - - Tests whether a file is a .jpg file (perhaps mislabelled as a .webp - file), hoping to handle Git #478. - - Args: - - path (str): The full path to a file to be processed by FFmpeg - - """ - - with open(path, 'rb') as fh: - data = fh.read(3) - - return data[0:3] == b'\xff\xd8\xff' - - - def replace_extension(self, path, ext, expected_real_ext=None): - - """Called by self.convert_webp(). - - Adapted from youtube-dl/youtube-dl/utils.py. - - Given the full path to a file, replaces the extension, and returns the - modified path. - - Args: - - path (str): The full path to a file - - ext (str): The new file extension - - expected_real_ext (str): Not used by Tartube - - Return values: - - The modified path - - """ - - name, real_ext = os.path.splitext(path) - - return '{0}.{1}'.format( - name if not expected_real_ext \ - or real_ext[1:] == expected_real_ext \ - else path, - ext, - ) - - - def run_ffmpeg(self, input_path, out_path, opt_list, test_flag=False): - - """Can be called by anything (currently called only by - self.convert_webp() ). - - Adapted from youtube-dl/youtube-dl/postprocessor/ffmpeg.py. - - self.run_ffmpeg_multiple_files() expects a list of files. Pass on - this function's parameters in the expected format. - - Args: - - input_path (str): Full path to a file to be processed by FFmpeg - - out_path (str): Full path to FFmpeg's output file - - opt_list (list): List of FFmpeg command line options (may be an - empty list) - - test_flag (bool): If True, just returns the FFmpeg system command, - rather than executing it - - Return values: - - Returns a list of two items, in the form - (success_flag, optional_message) - - """ - - return self.run_ffmpeg_multiple_files( - [ input_path ], - out_path, - opt_list, - test_flag, - ) - - - def run_ffmpeg_multiple_files(self, input_path_list, out_path, opt_list, \ - test_flag=False): - - """Can be called by anything (currently called only by - self.run_ffmpeg() ). - - Adapted from youtube-dl/youtube-dl/postprocessor/ffmpeg.py. - - Prepares the FFmpeg system command, and then executes it. - - Args: - - input_path_list (list): List of full paths to files to be - processed by FFmpeg. At the moment, Tartube only processes one - file at a time - - out_path (str): Full path to FFmpeg's output file - - opt_list (list): List of FFmpeg command line options (may be an - empty list) - - test_flag (bool): If True, just returns the FFmpeg system command, - rather than executing it - - Return values: - - Returns a list of two items, in the form - (success_flag, optional_message) - - """ - - # Get the modification time for the oldest file - oldest_mtime = min(os.stat(path).st_mtime for path in input_path_list) - - # Prepare the system command - files_cmd_list = [] - for path in input_path_list: - files_cmd_list.extend(['-i', self._ffmpeg_filename_argument(path)]) - - cmd_list = [self.get_executable(), '-y'] - cmd_list += ['-loglevel', 'repeat+info'] - cmd_list += ( - files_cmd_list - + opt_list - + [self._ffmpeg_filename_argument(out_path)] - ) - - # Return the system command only, if required - if test_flag: - return [ True, cmd_list ] - - # Execute the system command in a subprocess - try: - p = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - ) - - except Exception as e: - return [ False, str(e) ] - - stdout, stderr = p.communicate() - if p.returncode != 0: - stderr = stderr.decode(utils.get_encoding(), 'replace') - return [ False, stderr.strip().splitlines()[-1] ] - - else: - return [ self.try_utime(out_path, oldest_mtime, oldest_mtime), '' ] - - - def run_ffmpeg_directly(self, video_obj, source_path, cmd_list): - - """Modified version of self.run_ffmpeg(), called by - process.ProcessManager.process_video(). - - Adapted from youtube-dl/youtube-dl/postprocessor/ffmpeg.py. - - Prepares the FFmpeg system command, and then executes it. - - Args: - - video_obj (media.Video): The video object to be processed - - source_path (str): The full path to the source file - - cmd_list (list): The FFmpeg system command to use, as a list - - Return values: - - Returns a list of two items, in the form - (success_flag, optional_message) - - """ - - # Get the file's modification time - mod_time = os.stat(source_path).st_mtime - - # Execute the system command in a subprocess - try: - p = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - ) - - except Exception as e: - return [ False, str(e) ] - - stdout, stderr = p.communicate() - if p.returncode != 0: - stderr = stderr.decode(utils.get_encoding(), 'replace') - return [ False, stderr.strip().splitlines()[-1] ] - - else: - return [ self.try_utime(source_path, mod_time, mod_time), '' ] - - - def try_utime(self, path, atime, mtime): - - """Called by self.run_ffmpeg_multiple_files(). - - Adapted from youtube-dl/youtube-dl/postprocessor/common.py. - - Return values: - - True on success, False on failure - - """ - - try: - os.utime(path, (atime, mtime)) - return True - - except Exception: - return False - - -class FFmpegOptionsManager(object): - - """This class handles options to passed to FFmpeg, when the user wants to - process video(s) directly (i.e. not via youtube-dl). - - Adapted from FFmpeg Command Line Wizard, by AndreKR - (https://github.com/AndreKR/ffmpeg-command-line-wizard). - - OPTIONS (NAME TAB) - - extra_cmd_string (str): A string of extra FFmpeg options, to be applied - to the system command (if an empty string, nothing is added) - - OPTIONS (FILE TAB) - - add_end_filename (str): A string to be added to the end of the - filename, converting the source file to an output file with a - different name (if an empty string, nothing is added) - - regex_match_filename (str): - regex_apply_subst (str): - Two strings used in a regex substituion. If the first regex matches - the filename, the second string is used in the substitution. This - converts the source file to an output file with a different name. - (If the first string is an empty string, the filename is not - changed) - - rename_both_flag (bool): If True, when a video is renamed (using any of - the options above), the thumbnail is also renamed (but not - vice-versa; renaming the thumbnail does not affect the video) - - change_file_ext (str): A new file extension. If not an empty string, - then FFmpeg converts the video/audio/image file from one format to - another. (If an empty string, the format is not changed. Ignored - if 'output_mode' is 'gif') - - delete_original_flag (bool): If True and the source/output files have - different names, then the source file is deleted - - OPTIONS (SETTINGS TAB) - - input_mode (str): 'video' to convert a video/audio file (the one - downloaded by the specified media.Video object), or 'thumb' to - convert that video's thumbnail (if it has been downloaded) - - output_mode (str): Always set to 'thumb' if 'input_mode' is set to - 'thumb'. Otherwise, the default value is 'h264'. The other possible - values are 'gif' (in which case the video format is changed to - .GIF), 'merge' (in which case the video is merged with au audio - file with the same file name), 'split' (in which case the video is - split into pieces according to timestamps) or 'slice' (In which - case slices are removed from a video); in all four cases, the - 'change_file_ext' option is ignored) - - audio_flag (bool): If True, and if 'input_mode' is 'video', the video's - audio is preserved when the file is converted. Ignored if - 'input_mode' is 'thumb' - - audio_bitrate (int): Value that's a multiple of 16 (minimum value is - 16) - - quality_mode (str): 'crf' for 'Manual rate factor', or 'abr' for - 'Determine from target bitrate (2-Pass)' - - rate_factor (int): Value in the range 0-51. Ignored if 'quality_mode' - is 'abr' - - dummy_file (str): A dummy file is created during the first pass. The - name of that file: 'output', 'dummy', '/dev/null/' for Linux, - 'NUL' for MS Windows. Ignored if 'quality_mode' is 'crf' - - patience_preset (str): Affects how long the file conversion takes, and - also the size of the output file. Values are those used by FFmpeg - itself: 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', - 'medium', 'slow', 'slower', 'veryslow' - - gpu_encoding (str): Optimisations for various GPUs. One of the values - 'libx264', 'libx265', 'h264_amf', 'hevc_amf', 'h264_nvenc', - 'hevc_nvenc' - - hw_accel (str): Hardware acceleration mode: 'none', 'auto', 'vdpau', - 'dxva2', 'vaapi', 'qsv' - - palette_mode (str): Ignored unless 'output_mode' is 'gif'. Values are - 'faster' or 'better' - - split_mode (str): Ignored unless 'output_mode' is 'split'. Values are - 'video' to use the video's timestamps to split the file, or - 'custom' to use a custom set of timestamps to split the file - - split_list (list): Ignored unless 'split_mode' is 'custom'. A list - of timestamps and clip titles used to split the video into clips. - A list in groups of three, in the form - [start_stamp, stop_stamp, clip_title] - If 'stop_stamp' is None, then the end of the video (or the next - 'start_stamp') is used. 'clip_title' is optional (None if not - specified) - - slice_mode (str): Ignored unless 'output_mode' is 'slice'. Values are - 'video' to remove slices from the video using the video's own - slice list, or 'custom' to use a custom set of slices - - slice_list (list): Ignored unless 'slice_mode' is 'custom'. A list of - video slice data used to remove slices from a video, in the form - described by media.Video.__init__() - - OPTIONS (OPTIMISATIONS TAB) - - seek_flag (bool): True to optimise for fast seeking (shorter keyframe - interval, about 10% larger file) - - tuning_film_flag (bool): True if the input video is a high-quality - movie - - tuning_animation_flag (bool): True if the input video is an animated - movie - - tuning_grain_flag (bool): True if the input video contains film grain - - tuning_still_image_flag (bool): True if the input video is an image - slideshow - - tuning_fast_decode_flag (bool): True to optimise for really weak CPU - playback devices - - profile_flag (bool): True to optimise for really old devices (requires - rate factor above 0) - - fast_start_flag (bool): True to move headers to the beginning of the - file (so it can play while still downloading) - - tuning_zero_latency_flag (bool): True for fast encoding and low - latency streaming - - limit_flag (bool): True to limit the bitrate, using the values - specified by the options 'limit_mbps' and 'limit_buffer' - - limit_mbps (int): Bitrate limit in Mbit/s. Value that's a multiple of - 0.2 (minimum value is 0). Ignored if 'limit_flag' is False - - limit_buffer (int): Assume a receiving buffer (in seconds), Value - that's a multiple of 0.2 (minimum value is 0). Ignored if - 'limit_flag' is False - - """ - - - # Standard class methods - - - def __init__(self, uid, name): - - # IV list - other - # --------------- - # Unique ID for this options manager - self.uid = uid - # A non-unique name for this options manager - self.name = name - - # Dictionary of FFmpeg options, set by a call to self.reset_options - self.options_dict = {} - - - # Code - # ---- - - # Initialise FFmpeg options - self.reset_options() - - - # Public class methods - - - def clone_options(self, other_options_manager_obj): - - """Called by mainapp.TartubeApp.clone_ffmpeg_options() and - .clone_ffmpeg_options_from_window(). - - Clones FFmpeg options from the specified object into this object, - completely replacing this object's FFmpeg options. - - Args: - - other_options_manager_obj (ffmpeg_tartube.FFmpegOptionsManager): - The FFmpeg options object (usually the current one), from which - options will be cloned - - """ - - # (All values are scalars; there are no lists/dictionaries to copy) - self.options_dict = other_options_manager_obj.options_dict.copy() - - - def reset_options(self): - - """Called by self.__init__(). - - Resets (or initialises) self.options_dict to its default state. - """ - - self.options_dict = { - # NAME TAB - 'extra_cmd_string': '', - # FILE TAB - 'add_end_filename': '', - 'regex_match_filename': '', - 'regex_apply_subst': '', - 'rename_both_flag': False, - 'change_file_ext': '', - 'delete_original_flag': False, - # SETTINGS TAB - # 'video', 'thumb' - 'input_mode': 'video', - # 'h264', 'gif', 'merge', 'split', 'thumb' - 'output_mode': 'h264', - # SETTINGS TAB ('output_mode' = 'h264') - 'audio_flag': True, - 'audio_bitrate': 128, - # 'cfg', 'abr' - 'quality_mode': 'crf', - 'rate_factor': 23, - # 'output', 'dummy', '/dev/null/', 'NUL' - 'dummy_file': 'output', - # 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', - # 'slow', 'slower', 'veryslow' - 'patience_preset': 'medium', - # 'libx264', 'libx265', 'h264_amf', 'hevc_amf', 'h264_nvenc', - # 'hevc_nvenc' - 'gpu_encoding': 'libx264', - # 'none', 'auto', 'vdpau', 'dxva2', 'vaapi', 'qsv' - 'hw_accel': 'none', - # SETTINGS TAB ('output_mode' = 'gif') - 'palette_mode': 'faster', # 'faster', 'better' - # SETTINGS TAB ('output_mode' = 'split') - 'split_mode': 'video', # 'video', 'custom' - 'split_list': [], # list in groups of 3 - # SETTINGS TAB ('output_mode' = 'slice') - 'slice_mode': 'video', # 'video', 'custom' - 'slice_list': [], # list of dictionaries - # OPTIMISATIONS TAB ('output_mode' = 'h264') - 'seek_flag': True, - 'tuning_film_flag': False, - 'tuning_animation_flag': False, - 'tuning_grain_flag': False, - 'tuning_still_image_flag': False, - 'tuning_fast_decode_flag': False, - 'profile_flag': False, - 'fast_start_flag': True, - 'tuning_zero_latency_flag': False, - 'limit_flag': False, - 'limit_mbps': 1, - 'limit_buffer': 2, - # NOT VISIBLE IN THE EDIT WINDOW (a constant value) - 'bitrate': 0, - } - - - def get_system_cmd(self, app_obj, video_obj=None, start_point=None, - stop_point=None, clip_title=None, clip_dir=None, edit_dict=[]): - - """Can be called by anything. - - Given the FFmpeg options specified by self.options_dict, generates the - FFmpeg system command, returning it as a list of options. - - N.B. The 'delete_original_flag' option is not applied until the end of - the process operation. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video or None): If specified, uses the video's - downloaded file as the source file. Not specified when called - from config.FFmpegOptionsEditWin, in which case a specimen - source file is used (so that a specimen system command can be - displayed in the edit window) - - start_point, stop_point, clip_title, clip_dir (str): - When splitting a video, the points at which to start/stop - (timestamps or values in seconds), the clip title, and the - destination directory for sections (if not the same as the - original file). Ignored if 'output_mode' is not 'split' or - 'slice'. If 'output_mode' is 'split' or 'slice', then these - arguments are not specified when called from - config.FFmpegOptionsEditWin, in which case specimen timestamps/ - titles are used - - edit_dict (dict): When called from the edit window, any changes - that have been made to the FFmpeg options, but which have not - yet been saved to this object. We take those changes into - account when compiling the system command. - - Return values: - - Returns a tuple of three items: - - - The full path to the source file - - The full path to the output file - - A (python) list of options comprising the complete system - commmand (including the FFmpeg binary and the source/output - files) - - """ - - opt_list = [] - tuning_list = [] - return_list = [] - - # When called from the edit window (config.FFmpegOptionsEditWin), any - # changes in the edit window may not have been applied to - # self.options_dict yet - # To produce an up-to-date system command, use a temporary copy of - # self.options_dict, to which the unapplied changes have been added - options_dict = self.options_dict.copy() - for key in edit_dict: - options_dict[key] = edit_dict[key] - - # (Shortcuts to values retrieved several times) - bitrate = options_dict['bitrate'] - input_mode = options_dict['input_mode'] - limit_buffer = options_dict['limit_buffer'] - limit_mbps = options_dict['limit_mbps'] - output_mode = options_dict['output_mode'] - rate_factor = options_dict['rate_factor'] - - # The 'extra_cmd_string' item must be processed, and split into - # a list of separate items, preserving everything inside quotes as a - # single item (just as we do for youtube-dl download options) - extra_cmd_string = options_dict['extra_cmd_string'] - if extra_cmd_string != '': - extra_cmd_list = utils.parse_options(extra_cmd_string) - else: - extra_cmd_list = [] - - # FFmpeg binary - binary = app_obj.ffmpeg_manager_obj.get_executable() - - # Set variables describing the full path to the source video/audio and/ - # or source thumbnail files - - # If no media.Video object was specified, then use specimen paths that - # can be displayed in the edit window's textview - if video_obj is None: - - source_video_path = 'source.ext' - source_audio_path = 'source.ext' - source_thumb_path = 'source.jpg' - - else: - - if video_obj.dummy_flag: - - # (Special case: 'dummy' video objects (those downloaded in the - # Classic Mode tab) use different IVs) - - # If a specified media.Video has an unknown path, then return - # an empty list; there is nothing for FFmpeg to convert - if video_obj.dummy_path is None: - return None - - # Check the video/audio file actually exists. If not, there is - # nothing for FFmpeg to convert - source_video_path = video_obj.dummy_path - if not os.path.exists(source_video_path) \ - and input_mode == 'video': - return None, None, [] - - else: - - # If a specified media.Video has an unknown filename, then - # return an empty list; there is nothing for FFmpeg to - # convert - if video_obj.file_name is None: - return None, None, [] - - # Check the video/audio file actually exists, and is marked as - # downloaded. If not, there is nothing for FFmpeg to convert - source_video_path = video_obj.get_actual_path(app_obj) - if ( - not os.path.exists(source_video_path) \ - or not video_obj.dl_flag - ) and input_mode == 'video': - return None, None, [] - - # Find the video's thumbnail - source_thumb_path = utils.find_thumbnail(app_obj, video_obj, True) - if source_thumb_path is None and input_mode == 'thumb': - # Return an empty list; there is nothing for FFmpeg to convert - return None, None, [] - - # If 'output_mode' is 'merge', then look for an audio file with the - # same name as the video file (but otherwise don't bother) - source_audio_path = None - if output_mode == 'merge': - - name, video_ext = os.path.splitext(source_video_path) - for audio_ext in formats.AUDIO_FORMAT_LIST: - - audio_path = os.path.abspath(os.path.join(name, audio_ext)) - if os.path.isfile(audio_path): - source_audio_path = audio_path - break - - if source_audio_path is None: - # Nothing merge - return None, None, [] - - # Break down the full path into its components, so that we can set the - # output file, after applying optional modifications - if input_mode == 'video': - output_dir, output_file = os.path.split(source_video_path) - else: - output_dir, output_file = os.path.split(source_thumb_path) - - output_name, output_ext = os.path.splitext(output_file) - - add_end_filename = options_dict['add_end_filename'] - if add_end_filename != '': - - # Remove trailing whitepsace - add_end_filename = re.sub( - r'\s+$', - '', - options_dict['add_end_filename'], - ) - - # Update the filename - output_name += add_end_filename - - regex_match_filename = options_dict['regex_match_filename'] - if regex_match_filename != '': - - output_name = re.sub( - regex_match_filename, - options_dict['regex_apply_subst'], - output_name, - ) - - change_file_ext = options_dict['change_file_ext'].lower() - if change_file_ext == '': - output_file = output_name + output_ext - else: - output_file = output_name + '.' + change_file_ext - - if video_obj is None: - output_path = output_file - else: - output_path = os.path.abspath( - os.path.join(output_dir, output_file), - ) - - # Special case: when called from config.FFmpegOptionsEditWin, then show - # a specimen system command resembling the one that will eventually - # be generated by FFmpegManager.run_ffmpeg_multiple_files() - if app_obj.simple_ffmpeg_options_flag \ - and video_obj is None \ - and output_mode != 'split' \ - and output_mode != 'slice': - - return_list.append(binary) - return_list.append('-y') - return_list.append('-loglevel') - return_list.append('repeat+info') - return_list.append('-i') - - if input_mode == 'video': - return_list.append(source_video_path) - else: - return_list.append(source_thumb_path) - - if extra_cmd_list: - return_list.extend(extra_cmd_list) - - return_list.extend( - [ - app_obj.ffmpeg_manager_obj._ffmpeg_filename_argument( - output_path, - ), - ], - ) - - return source_video_path, output_path, return_list - - # When the full GUI layout is visible, apply all FFmpeg options - if input_mode == 'video': - - opt_list.append('-i') - opt_list.append(source_video_path) - - else: - - opt_list.append('-i') - opt_list.append(source_thumb_path) - - # H.264 - if output_mode == 'h264': - - # In the original code, this was marked: - # Only necessary if the output filename does not end with .mp4 - opt_list.append('-c:v') - opt_list.append(options_dict['gpu_encoding']) - - opt_list.append('-preset') - opt_list.append(options_dict['patience_preset']) - - if options_dict['hw_accel'] != 'none': - opt_list.append('-hwaccel') - opt_list.append(options_dict['hw_accel']) - - if options_dict['tuning_film_flag']: - tuning_list.append('film') - if options_dict['tuning_animation_flag']: - tuning_list.append('animation') - if options_dict['tuning_grain_flag']: - tuning_list.append('grain') - if options_dict['tuning_still_image_flag']: - tuning_list.append('stillimage') - if options_dict['tuning_fast_decode_flag']: - tuning_list.append('fastdecode') - if options_dict['tuning_zero_latency_flag']: - tuning_list.append('zerolatency') - - if tuning_list: - opt_list.append('-tune') - opt_list.append(','.join(tuning_list)) - - if options_dict['fast_start_flag']: - opt_list.append('-movflags') - opt_list.append('faststart') - - if input_mode == 'video' and options_dict['audio_flag']: - opt_list.append('-c:a') - opt_list.append('aac') - opt_list.append('-b:a') - opt_list.append( - str(options_dict['audio_bitrate']) + 'k', - ) - - if options_dict['profile_flag'] and rate_factor != 0: - opt_list.append('-profile:v') - opt_list.append('baseline') - opt_list.append('-level') - opt_list.append('3.0') - - if options_dict['limit_flag']: - opt_list.append('-maxrate') - opt_list.append(str(limit_mbps) + 'M') - opt_list.append('-bufsize') - opt_list.append((str(limit_mbps) * str(limit_buffer)) + 'M') - - if options_dict['seek_flag']: - - # In the original code, this was marked: - # Inserts an I-frame every 15 frames - opt_list.append('-x264-params') - opt_list.append('keyint=15') - - # In the original code, this was marked: - # Preserves the frame timestamps of VFR videos - opt_list.append('-vsync') - opt_list.append('2') - opt_list.append('-enc_time_base') - opt_list.append('-1') - - if options_dict['quality_mode'] == 'crf': - - return_list.append(binary) - return_list.extend(opt_list) - return_list.append('-crf') - return_list.append(str(rate_factor)) - - if extra_cmd_list: - return_list.extend(extra_cmd_list) - - return_list.append(output_path) - - else: - dummy_file = options_dict['dummy_file'] - if dummy_file == 'output': - dummy_file = output_path - - return_list.append(binary) - return_list.append('-y') - return_list.extend(opt_list) - return_list.append('-b:v') - return_list.append(str(bitrate)) - return_list.append('-pass') - return_list.append('1') - return_list.append('-f') - return_list.append('mp4') - return_list.append(dummy_file) - - return_list.append('&&') - return_list.append(binary) - return_list.extend(opt_list) - return_list.append('-b:v') - return_list.append(str(bitrate)) - return_list.append('-pass') - return_list.append('2') - - if extra_cmd_list: - return_list.extend(extra_cmd_list) - - return_list.append(output_path) - - # GIF - elif output_mode == 'gif': - - if options_dict['palette_mode'] == 'faster': - - return_list.append(binary) - return_list.extend(opt_list) - - if extra_cmd_list: - return_list.extend(extra_cmd_list) - - return_list.append(output_name + '.gif') - - else: - - return_list.append(binary) - return_list.extend(opt_list) - return_list.append('-vf') - return_list.append('palettegen') - return_list.append('palette.png') - - return_list.append('&&') - return_list.append(binary) - return_list.extend(opt_list) - return_list.append('-i') - return_list.append('palette.png') - return_list.append('-filter_complex') - return_list.append('"[0:v][1:v] paletteuse"') - - if extra_cmd_list: - return_list.extend(extra_cmd_list) - - return_list.append(output_name + '.gif') - - # Merge video/audio - elif output_mode == 'merge': - - return_list.append(binary) - return_list.extend(opt_list) - - return_list.append('-i') - return_list.append(source_audio_path) - return_list.append('-c:v') - return_list.append('copy') - return_list.append('-c:a') - return_list.append('copy') - - return_list.append(output_path) - - # Split video by timestamps, or times in seconds - elif output_mode == 'split' or output_mode == 'slice': - - return_list.append(binary) - return_list.extend(opt_list) - - if output_mode == 'split': - - return_list.append('-ss') - if start_point is None: - # (A specimen timestamp) - return_list.append('0:00') - else: - return_list.append(str(start_point)) - - # (If no timestamp is specified, the end of the video is used) - if stop_point is not None: - return_list.append('-to') - return_list.append(str(stop_point)) - - else: - - return_list.append('-ss') - if start_point is None: - # (A specimen time, in seconds) - return_list.append('0') - else: - return_list.append(str(start_point)) - - # (If no timestamp is specified, the end of the video is used) - if stop_point is not None: - return_list.append('-to') - return_list.append(str(stop_point)) - - if clip_title is None or clip_title == "": - # (When called from config.FFmpegOptionsEditWin) - clip_title = app_obj.split_video_generic_title - - if clip_dir is None: - - output_path = os.path.abspath( - os.path.join(output_dir, clip_title + output_ext), - ) - - else: - - output_path = os.path.abspath( - os.path.join(clip_dir, clip_title + output_ext), - ) - - return_list.append(output_path) - - # Video thumbnails - else: - - return_list.append(binary) - return_list.extend(opt_list) - return_list.append(output_path) - - # All done - if output_mode == 'thumb': - return source_thumb_path, output_path, return_list - else: - return source_video_path, output_path, return_list diff --git a/build/lib/tartube/files.py b/build/lib/tartube/files.py deleted file mode 100644 index 4cc21c90..00000000 --- a/build/lib/tartube/files.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""File manager classes.""" - - -# Import Gtk modules -# ... - - -# Import other modules -from gi.repository import GdkPixbuf -import json -import os -import threading - - -# Import our modules -import utils - - -# Classes - - -class FileManager(threading.Thread): - - """Called by mainapp.TartubeApp.__init__(). - - Python class to manage loading of thumbnail, icon and JSON files safely - (i.e. without causing a Gtk crash). - """ - - - # Standard class methods - - - def __init__(self): - - super(FileManager, self).__init__() - - - # Public class methods - - - def load_json(self, full_path): - - """Can be called by anything. - - Given the full path to a JSON file, loads the file into a Python - dictionary and returns the dictionary. - - Args: - - full_path (str): The full path to the JSON file - - Return values: - - The JSON data, converted to a Python dictionary (an empty - dictionary if the file is missing or can't be loaded) - - """ - - empty_dict = {} - if not os.path.isfile(full_path): - return empty_dict - - # RFC 7159: JSON should be represented by UTF-8 - with open( - full_path, - 'r', - encoding='UTF-8', - errors='ignore', - ) as json_file: - - try: - json_dict = json.load(json_file) - return json_dict - - except: - return empty_dict - - - def load_text(self, full_path): - - """Can be called by anything. - - Given the full path to a text file, loads it. - - Args: - - full_path (str): The full path to the text file - - Return values: - - The contents of the text file as a string, or or None if the file - is missing or can't be loaded - - """ - - if not os.path.isfile(full_path): - return None - - with open( - full_path, - 'r', - encoding='UTF-8', - errors='ignore', - ) as text_file: - - try: - text = text_file.read() - return text - - except: - return None - - - def load_to_pixbuf(self, full_path, width=None, height=None): - - """Can be called by anything. - - Given the full path to an icon file, loads the icon into a pibxuf, and - returns the pixbuf. - - Args: - - full_path (str): The full path to the icon file - - width, height (int or None): If both are specified, the icon is - scaled to that size - - Return values: - - A GdkPixbuf (as a tuple), or None if the file is missing or can't - be loaded - - """ - - if not os.path.isfile(full_path): - return None - - try: - # (Returns a tuple) - pixbuf = GdkPixbuf.Pixbuf.new_from_file(full_path) - except: - return None - - if width is not None and height is not None: - pixbuf = pixbuf.scale_simple( - width, - height, - GdkPixbuf.InterpType.BILINEAR, - ) - - return pixbuf diff --git a/build/lib/tartube/formats.py b/build/lib/tartube/formats.py deleted file mode 100644 index 09dd4450..00000000 --- a/build/lib/tartube/formats.py +++ /dev/null @@ -1,1257 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Constant variables used in various parts of the code.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import datetime -import re - - -# Import our modules -# Use same gettext translations -from mainapp import _ - - -# Supported locales: _ -locale_setup_list = [ - 'en_GB', 'English', - 'en_US', 'English (American)', - 'es', 'Español', - 'fr', 'Français', - 'ko_KR', '한국어', - 'nl_NL', 'Nederlands', - 'ru', 'русский', - 'tr', 'Türkçe', - 'vi', 'Tiếng Việt', - 'zh_CN', '中文简体', -] - -LOCALE_DEFAULT = locale_setup_list[0] -LOCALE_LIST = [] -LOCALE_DICT = {} - -while locale_setup_list: - key = locale_setup_list.pop(0) - value = locale_setup_list.pop(0) - - LOCALE_LIST.append(key) - LOCALE_DICT[key] = value - -# Some icons are different at Christmas and on national holidays -today = datetime.date.today() -day = today.strftime('%d') -month = today.strftime('%m') - -xmas_flag = False -eesti_flag = False -anglo_flag = False -if (int(month) == 12 and int(day) >= 24) \ -or (int(month) == 1 and int(day) <= 5): - xmas_flag = True -elif (int(month) == 2 and int(day) == 24) \ -or (int(month) == 8 and int(day) == 20): - eesti_flag = True -elif (int(month) == 4 and int(day) == 23) \ -or (int(month) == 11 and int(day) == 14): - anglo_flag = True - -language_setup_list = [ - # ISO 639-1 Language Codes (with one extra key-value pair to handle live - # chat) - # English is top of the list, because it's the default setting in - # options.OptionsManager - # NB These values must not contain square brackets [...] - _('English'), 'en', - 'YouTube live chat', 'live_chat', - 'Abkhazian', 'ab', - 'Afar', 'aa', - 'Afrikaans', 'af', - 'Akan', 'ak', - 'Albanian', 'sq', - 'Amharic', 'am', - 'Arabic', 'ar', - 'Aragonese', 'an', - 'Armenian', 'hy', - 'Assamese', 'as', - 'Avaric', 'av', - 'Avestan', 'ae', - 'Aymara', 'ay', - 'Azerbaijani', 'az', - 'Bambara', 'bm', - 'Bashkir', 'ba', - 'Basque', 'eu', - 'Belarusian', 'be', - 'Bengali (Bangla)', 'bn', - 'Bihari', 'bh', - 'Bislama', 'bi', - 'Bosnian', 'bs', - 'Breton', 'br', - 'Bulgarian', 'bg', - 'Burmese', 'my', - 'Catalan', 'ca', - 'Chamorro', 'ch', - 'Chechen', 'ce', - 'Chichewa, Chewa, Nyanja', 'ny', - 'Chinese', 'zh', - 'Chinese (Simplified)', 'zh-Hans', - 'Chinese (Traditional)', 'zh-Hant', - 'Chuvash', 'cv', - 'Cornish', 'kw', - 'Corsican', 'co', - 'Cree', 'cr', - 'Croatian', 'hr', - 'Czech', 'cs', - 'Danish', 'da', - 'Divehi, Dhivehi, Maldivian', 'dv', - _('Dutch'), 'nl', - 'Dzongkha', 'dz', - 'Esperanto', 'eo', - 'Estonian', 'et', - 'Ewe', 'ee', - 'Faroese', 'fo', - 'Fijian', 'fj', - 'Finnish', 'fi', - _('French'), 'fr', - 'Fula, Fulah, Pulaar, Pular', 'ff', - 'Galician', 'gl', - 'Gaelic (Scottish)', 'gd', - 'Gaelic (Manx)', 'gv', - 'Georgian', 'ka', - 'German', 'de', - 'Greek', 'el', - 'Greenlandic, Kalaallisut', 'kl', - 'Guarani', 'gn', - 'Gujarati', 'gu', - 'Haitian Creole', 'ht', - 'Hausa', 'ha', - 'Hebrew', 'he', - 'Herero', 'hz', - 'Hindi', 'hi', - 'Hiri Motu', 'ho', - 'Hungarian', 'hu', - 'Icelandic', 'is', - 'Ido', 'io', - 'Igbo', 'ig', - 'Indonesian', 'id', - 'Interlingua', 'ia', - 'Interlingue', 'ie', - 'Inuktitut', 'iu', - 'Inupiak', 'ik', - 'Irish', 'ga', - 'Italian', 'it', - 'Japanese', 'ja', - 'Javanese', 'jv', - 'Kannada', 'kn', - 'Kanuri', 'kr', - 'Kashmiri', 'ks', - 'Kazakh', 'kk', - 'Khmer', 'km', - 'Kikuyu', 'ki', - 'Kinyarwanda (Rwanda)', 'rw', - 'Kirundi', 'rn', - 'Klingon', 'tlh', # Actually ISO 639-2 - 'Kyrgyz', 'ky', - 'Komi', 'kv', - 'Kongo', 'kg', - _('Korean'), 'ko', - 'Kurdish', 'ku', - 'Kwanyama', 'kj', - 'Lao', 'lo', - 'Latin', 'la', - 'Latvian (Lettish)', 'lv', - 'Limburgish ( Limburger)', 'li', - 'Lingala', 'ln', - 'Lithuanian', 'lt', - 'Luga-Katanga', 'lu', - 'Luganda, Ganda', 'lg', - 'Luxembourgish', 'lb', - 'Macedonian', 'mk', - 'Malagasy', 'mg', - 'Malay', 'ms', - 'Malayalam', 'ml', - 'Maltese', 'mt', - 'Maori', 'mi', - 'Marathi', 'mr', - 'Marshallese', 'mh', - 'Moldavian', 'mo', - 'Mongolian', 'mn', - 'Nauru', 'na', - 'Navajo', 'nv', - 'Ndonga', 'ng', - 'Northern Ndebele', 'nd', - 'Nepali', 'ne', - 'Norwegian', 'no', - 'Norwegian bokmål', 'nb', - 'Norwegian nynorsk', 'nn', - 'Occitan', 'oc', - 'Ojibwe', 'oj', - 'Old Church Slavonic, Old Bulgarian', 'cu', - 'Oriya', 'or', - 'Oromo (Afaan Oromo)', 'om', - 'Ossetian', 'os', - 'Pāli', 'pi', - 'Pashto, Pushto', 'ps', - 'Persian (Farsi)', 'fa', - 'Polish', 'pl', - 'Portuguese', 'pt', - 'Punjabi (Eastern)', 'pa', - 'Quechua', 'qu', - 'Romansh', 'rm', - 'Romanian', 'ro', - _('Russian'), 'ru', - 'Sami', 'se', - 'Samoan', 'sm', - 'Sango', 'sg', - 'Sanskrit', 'sa', - 'Serbian', 'sr', - 'Serbo-Croatian', 'sh', - 'Sesotho', 'st', - 'Setswana', 'tn', - 'Shona', 'sn', - 'Sichuan Yi, Nuoso', 'ii', - 'Sindhi', 'sd', - 'Sinhalese', 'si', - 'Swati, Siswati', 'ss', - 'Slovak', 'sk', - 'Slovenian', 'sl', - 'Somali', 'so', - 'Southern Ndebele', 'nr', - _('Spanish'), 'es', - 'Sundanese', 'su', - 'Swahili (Kiswahili)', 'sw', - 'Swedish', 'sv', - 'Tagalog', 'tl', - 'Tahitian', 'ty', - 'Tajik', 'tg', - 'Tamil', 'ta', - 'Tatar', 'tt', - 'Telugu', 'te', - 'Thai', 'th', - 'Tibetan', 'bo', - 'Tigrinya', 'ti', - 'Tonga', 'to', - 'Tsonga', 'ts', - _('Turkish'), 'tr', - 'Turkmen', 'tk', - 'Twi', 'tw', - 'Uyghur', 'ug', - 'Ukrainian', 'uk', - 'Urdu', 'ur', - 'Uzbek', 'uz', - 'Venda', 've', - _('Vietnamese'), 'vi', - 'Volapük', 'vo', - 'Wallon', 'wa', - 'Welsh', 'cy', - 'Wolof', 'wo', - 'Western Frisian', 'fy', - 'Xhosa', 'xh', - 'Yiddish', 'yi', - 'Yoruba', 'yo', - 'Zhuang, Chuang', 'za', - 'Zulu', 'zu', -] - -LANGUAGE_CODE_LIST = [] -LANGUAGE_CODE_DICT = {} - -while language_setup_list: - key = language_setup_list.pop(0) - value = language_setup_list.pop(0) - - LANGUAGE_CODE_LIST.append(key) - LANGUAGE_CODE_DICT[key] = value - -# 'Enhanced' websites. As of v2.3.597, this data is only used to extract RSS -# feeds, but that functionality could be extended in the future -# The 'convert' templates work like this: any four-character sequence beginning -# and ending with a space character is replaced: -# ' vi ' - replaced with video ID -# ' vn ' - replaced with video name -# ' ci ' - replaced with channel ID -# ' cn ' - replaced with channel name -# ' pi ' - replaced with playlist ID -# ' pn ' - replaced with playlist name -# The IDs and/or names are those extracted from a full video/channel/playlist -# URL (or provided by a video's metadata file) -# In each mini-dictionary, the keys 'name', 'pretty_name' must be set. The -# 'detect_list' item must not be empty; all other values can be empty lists, -# if not applicable -enhanced_setup_list = [ - { - # Key in the dictionary below - 'name': 'youtube', - # Name displayed in the Video Catalogue - 'pretty_name': 'YouTube', - # Regexes to recognise the website (no groups used) - 'detect_list': [ - '^https?:\/\/(www\.)?youtube\.com\/', - ], - # Regexes to extract a video ID/name. The second group is used (so that - # the optional www can be the first group) - 'extract_vid_list': [ - '^https?:\/\/(www\.)?youtube\.com\/watch\?v=([^\/]+)', - ], - 'extract_vname_list': [], - # Regexes to extract a channel/playlist ID/name. The second group is - # used - 'extract_cid_list': [ - '^https?:\/\/(www\.)?youtube\.com\/channel\/([^\/]+)', - ], - 'extract_cname_list': [ - '^https?:\/\/(www\.)?youtube\.com\/user\/([^\/]+)\/videos\/?', - '^https?:\/\/(www\.)?youtube\.com\/c\/([^\/]+)\/videos\/?', - ], - # Regexes to extract a playlist ID/name. The second group is used - 'extract_pid_list': [ - '^https?:\/\/(www\.)?youtube\.com\/channel\?list=([^\/]+)', - '^https?:\/\/(www\.)?youtube\.com\/playlist\?list=([^\/]+)', - ], - 'extract_pname_list': [], - # Templates to convert video ID/name to URL - 'convert_video_list': [ - 'https://www.youtube.com/watch?v= vi ', - ], - # Templates to convert channel ID/name to URL - 'convert_channel_list': [ - 'https://www.youtube.com/c/ cn /videos', - 'https://www.youtube.com/user/ cn /videos', - 'https://www.youtube.com/channel/ ci ', - ], - # Templates to convert playlist ID/name to URL - 'convert_playlist_list': [ -# 'https://www.youtube.com/channel?list= pi ', - 'https://www.youtube.com/playlist?list= pi ', - ], - # Templates to convert channel ID/name to RSS feed - 'rss_channel_list': [ - 'https://www.youtube.com/feeds/videos.xml?channel_id= ci ', - ], - # Templates to convert playlist ID/name to RSS feed - 'rss_playlist_list': [ - 'https://www.youtube.com/feeds/videos.xml?playlist_id= pi ', - ], - }, - { - 'name': 'odysee', - 'pretty_name': 'Odysee', - 'detect_list': [ - '^https?:\/\/(www\.)?odysee\.com\/', - ], - 'extract_vid_list': [], - 'extract_vname_list': [ - '^https?:\/\/(www\.)?odysee\.com\/\@[^\/]+\/([^\:]+)\:', - ], - 'extract_cid_list': [], - 'extract_cname_list': [ - '^https?:\/\/(www\.)?odysee\.com\/\@([^\:]+)\:', - ], - 'extract_pid_list': [], - 'extract_pname_list': [], - 'convert_video_list': [], - 'convert_channel_list': [], - 'convert_playlist_list': [], - 'rss_channel_list': [ - 'https://lbryfeed.melroy.org/channel/odysee/ cn ', - ], - 'rss_playlist_list': [], - }, - { - 'name': 'bitchute', - 'pretty_name': 'BitChute', - 'detect_list': [ - '^https?:\/\/(www\.)?bitchute\.com\/', - ], - 'extract_vid_list': [ - '^https?:\/\/(www\.)?bitchute\.com\/video\/([^\/]+)', - ], - 'extract_vname_list': [], - 'extract_cid_list': [ - '^https?:\/\/(www\.)?bitchute\.com\/channel\/([^\/]+)', - ], - 'extract_cname_list': [], - 'extract_pid_list': [], - 'extract_pname_list': [], - 'convert_video_list': [ - 'https://www.bitchute.com/video/ vi ', - ], - 'convert_channel_list': [ - 'https://www.bitchute.com/video/ ci ', - ], - 'convert_playlist_list': [], - 'rss_channel_list': [ - 'https://www.bitchute.com/feeds/rss/channel/ cn ', - ], - 'rss_playlist_list': [], - }, - { - 'name': 'twitch', - 'pretty_name': 'Twitch', - 'detect_list': [ - '^https?:\/\/(www\.)?twitch\.tv\/', - ], - 'extract_vid_list': [ - '^https?:\/\/(www\.)?twitch\.tv\/videos\/([^\/]+)', - ], - 'extract_vname_list': [], - 'extract_cid_list': [], - 'extract_cname_list': [ - '^https?:\/\/(www\.)?twitch\.tv\/([^\/]+)', - ], - 'extract_pid_list': [], - 'extract_pname_list': [], - 'convert_video_list': [ - 'https://www.twitch.tv/video/ vi ', - ], - 'convert_channel_list': [ - 'https://www.twitch.tv/ cn ', - ], - 'convert_playlist_list': [], - 'rss_channel_list': [ - 'https://twitchrss.appspot.com/vod/ cn ', - ], - 'rss_playlist_list': [], - }, -] - -ENHANCED_SITE_LIST = [] -ENHANCED_SITE_DICT = {} - -for mini_dict in enhanced_setup_list: - ENHANCED_SITE_LIST.append(mini_dict['name']) - ENHANCED_SITE_DICT[mini_dict['name']] = mini_dict - -# Standard list and dictionaries -time_metric_setup_list = [ - 'seconds', _('seconds'), 1, - 'minutes', _('minutes'), 60, - 'hours', _('hours'), int(60 * 60), - 'days', _('days'), int(60 * 60 * 24), - 'weeks', _('weeks'), int(60 * 60 * 24 * 7), - 'years', _('years'), int(60 * 60 * 24 * 365), -] - -TIME_METRIC_LIST = [] -TIME_METRIC_DICT = {} -TIME_METRIC_TRANS_DICT = {} - -while time_metric_setup_list: - key = time_metric_setup_list.pop(0) - trans_key = time_metric_setup_list.pop(0) - value = time_metric_setup_list.pop(0) - - TIME_METRIC_LIST.append(key) - TIME_METRIC_DICT[key] = value - TIME_METRIC_TRANS_DICT[key] = trans_key - -specified_days_setup_list = [ - 'every_day', _('Every day'), - 'weekdays', _('Weekdays'), - 'weekends', _('Weekends'), - 'monday', _('Monday'), - 'tuesday', _('Tuesday'), - 'wednesday', _('Wednesday'), - 'thursday', _('Thursday'), - 'friday', _('Friday'), - 'saturday', _('Saturday'), - 'sunday', _('Sunday'), -] - -SPECIFIED_DAYS_LIST = [] -SPECIFIED_DAYS_DICT = {} - -while specified_days_setup_list: - key = specified_days_setup_list.pop(0) - value = specified_days_setup_list.pop(0) - - SPECIFIED_DAYS_LIST.append(key) - SPECIFIED_DAYS_DICT[key] = value - -KILO_SIZE = 1024.0 -filesize_metric_setup_list = [ - 'B', 1, - 'KiB', int(KILO_SIZE ** 1), - 'MiB', int(KILO_SIZE ** 2), - 'GiB', int(KILO_SIZE ** 3), - 'TiB', int(KILO_SIZE ** 4), - 'PiB', int(KILO_SIZE ** 5), - 'EiB', int(KILO_SIZE ** 6), - 'ZiB', int(KILO_SIZE ** 7), - 'YiB', int(KILO_SIZE ** 8), -] - -FILESIZE_METRIC_LIST = [] -FILESIZE_METRIC_DICT = {} - -while filesize_metric_setup_list: - key = filesize_metric_setup_list.pop(0) - value = filesize_metric_setup_list.pop(0) - - FILESIZE_METRIC_LIST.append(key) - FILESIZE_METRIC_DICT[key] = value - -file_output_setup_list = [ - 0, 'Custom', - None, # (The same as option 2 by default) - 1, 'ID', - '%(id)s.%(ext)s', - 2, 'Title', - '%(title)s.%(ext)s', - 3, 'Title + ID', - '%(title)s-%(id)s.%(ext)s', - 4, 'Title + Quality', - '%(title)s-%(height)sp.%(ext)s', - 5, 'Title + ID + Quality', - '%(title)s-%(id)s-%(height)sp.%(ext)s', - 6, 'Autonumber + Title', - '%(playlist_index)s-%(title)s.%(ext)s', - 7, 'Autonumber + Title + ID', - '%(playlist_index)s-%(title)s-%(id)s.%(ext)s', - 8, 'Autonumber + Title + Quality', - '%(playlist_index)s-%(title)s-%(height)sp.%(ext)s', - 9, 'Autonumber + Title + ID + Quality', - '%(playlist_index)s-%(title)s-%(id)s-%(height)sp.%(ext)s', -] - -FILE_OUTPUT_NAME_DICT = {} -FILE_OUTPUT_CONVERT_DICT = {} - -while file_output_setup_list: - key = file_output_setup_list.pop(0) - value = file_output_setup_list.pop(0) - value2 = file_output_setup_list.pop(0) - - FILE_OUTPUT_NAME_DICT[key] = value - FILE_OUTPUT_CONVERT_DICT[key] = value2 - -YTDLP_OUTPUT_TYPE_LIST = [ - 'subtitle', - 'thumbnail', - 'description', - 'annotation', - 'infojson', - 'pl_thumbnail', - 'pl_description', - 'pl_infojson', - 'chapter', -] - -SPONSORBLOCK_CATEGORY_LIST = [ - 'sponsor', - 'selfpromo', - 'interaction', - 'intro', - 'outro', - 'preview', - 'music_offtopic', -] -SPONSORBLOCK_ACTION_LIST = [ - 'skip', -] - -video_option_setup_list = [ - # List of YouTube extractor (format) codes, based on the original list in - # youtube-dl-gui, and supplemented by this list: - # - # https://gist.github.com/sidneys/7095afe4da4ae58694d128b1034e01e2 - # - # Unfortunately, as of late September 2019, that list was already out of - # date - # Unfortunately, the list is YouTube-specific, and will not necessarily - # work on other websites - # - # I'm not sure about the meaning of some extractor codes; in those cases, - # I add the code itself to distinguish it from similar codes (e.g. - # compare codes 18 and 396) - # - # Dummy extractor codes - progressive scan resolutions - '144p', 'Any format [144p]', False, - '240p', 'Any format [240p]', False, - '360p', 'Any format [360p]', False, - '480p', 'Any format [480p]', False, - '720p', 'Any format [720p]', False, - '720p60', 'Any format [720p 60fps]', False, - '1080p', 'Any format [1080p]', False, - '1080p60', 'Any format [1080p 60fps]', False, - '1440p', 'Any format [1440p]', False, - '1440p60', 'Any format [1440p 60fps]', False, - '2160p', 'Any format [2160p]', False, - '2160p60', 'Any format [2160p 60fps]', False, - '4320p', 'Any format [4320p]', False, - '4320p60', 'Any format [4320p 60fps]', False, - # Dummy extractor codes - other - '3gp', '3gp', False, - 'flv', 'flv', False, - 'm4a', 'm4a', True, - 'mp4', 'mp4', False, - 'webm', 'webm', False, - # Real extractor codes - '17', '3gp [144p] <17>', False, - '36', '3gp [240p] <36>', False, - '5', 'flv [240p] <5>', False, - '6', 'flv [270p] <6>', False, - '34', 'flv [360p] <34>', False, - '35', 'flv [480p] <35>', False, - # v1.3.037 - not sure whether the HLS format codes should be added here, or - # not. 'hls' has not been added as a dummy extractor code because - # youtube-dl doesn't support that - '151', 'hls [72p] <151>', False, - '132', 'hls [240p] <132>', False, - '92', 'hls [240p] (3D) <92>', False, - '93', 'hls [360p] (3D) <93>', False, - '94', 'hls [480p] (3D) <94>', False, - '95', 'hls [720p] (3D) <95>', False, - '96', 'hls [1080p] <96>', False, - '139', 'm4a 48k (DASH Audio) <139>', True, - '140', 'm4a 128k (DASH Audio) <140>', True, - '256', 'm4a 192k (DASH Audio) <256>', True, - '141', 'm4a 256k (DASH Audio) <141>', True, - '258', 'm4a 384k (DASH Audio) <258>', True, - '18', 'mp4 [360p] <18>', False, - '22', 'mp4 [720p] <22>', False, - '37', 'mp4 [1080p] <37>', False, - '38', 'mp4 [4K] <38>', False, - '160', 'mp4 [144p] (DASH Video) <160>', False, - '133', 'mp4 [240p] (DASH Video) <133>', False, - '134', 'mp4 [360p] (DASH Video) <134>', False, - '135', 'mp4 [480p] (DASH Video) <135>', False, - '136', 'mp4 [720p] (DASH Video) <136>', False, - '298', 'mp4 [720p 60fps] (DASH Video) <298>', False, - '137', 'mp4 [1080p] (DASH Video) <137>', False, - '299', 'mp4 [1080p 60fps] (DASH Video) <299>', False, - '264', 'mp4 [1440p] (DASH Video) <264>', False, - '138', 'mp4 [2160p] (DASH Video) <138>', False, - '266', 'mp4 [2160p 60fps] (DASH Video) <266>', False, - '82', 'mp4 [360p] (3D) <82>', False, - '83', 'mp4 [480p] (3D) <83>', False, - '84', 'mp4 [720p] (3D) <84>', False, - '85', 'mp4 [1080p] (3D) <85>', False, - '394', 'mp4 [144p] <394>', False, - '395', 'mp4 [240p] <395>', False, - '396', 'mp4 [360p] <396>', False, - '397', 'mp4 [480p] <397>', False, - '398', 'mp4 [720p] <398>', False, - '399', 'mp4 [1080p] <399>', False, - '400', 'mp4 [1440p] <400>', False, - '401', 'mp4 [2160p] <401>', False, - '402', 'mp4 [2880p] <402>', False, - '571', 'mp4 [8k] <571>', False, - '43', 'webm [360p] <43>', False, - '44', 'webm [480p] <44>', False, - '45', 'webm [720p] <45>', False, - '46', 'webm [1080p] <46>', False, - '242', 'webm [240p] (DASH Video) <242>', False, - '243', 'webm [360p] (DASH Video) <243>', False, - '244', 'webm [480p] (DASH Video) <244>', False, - '247', 'webm [720p] (DASH Video) <247>', False, - '302', 'webm [720p 60fps] (DASH Video) <302>', False, - '248', 'webm [1080p] (DASH Video) <248>', False, - '303', 'webm [1080p 60fps] (DASH Video) <303>', False, - '271', 'webm [1440p] (DASH Video) <271>', False, - '308', 'webm [1440p 60fps] (DASH Video) <300>', False, - '313', 'webm [2160p] (DASH Video) <313>', False, - '315', 'webm [2160p 60fps] (DASH Video) <315>', False, - '272', 'webm [4320p] (DASH Video) <272>', False, - '100', 'webm [360p] (3D) <100>', False, - '101', 'webm [480p] (3D) <101>', False, - '102', 'webm [720p] (3D) <102>', False, - '330', 'webm [144p 60fps] (HDR) <330>', False, - '331', 'webm [240p 60fps] (HDR) <331>', False, - '332', 'webm [360p 60fps] (HDR) <332>', False, - '333', 'webm [480p 60fps] (HDR) <333>', False, - '334', 'webm [720p 60fps] (HDR) <334>', False, - '335', 'webm [1080p 60fps] (HDR) <335>', False, - '336', 'webm [1440p 60fps] (HDR) <336>', False, - '337', 'webm [2160p 60fps] (HDR) <337>', False, - '600', 'webm (36k Audio) <600>', True, - '249', 'webm (52k Audio) <249>', True, - '250', 'webm (64k Audio) <250>', True, - '251', 'webm (116k Audio) <251>', True, - '219', 'webm [144p] <219>', False, - '278', 'webm [144p] <278>', False, - '167', 'webm [360p] <167>', False, - '168', 'webm [480p] <168>', False, - '218', 'webm [480p] <218>', False, - '245', 'webm [480p] <245>', False, - '246', 'webm [480p] <246>', False, - '169', 'webm [1080p] <169>', False, - '171', 'webm 48k (DASH Audio) <171>', True, - '172', 'webm 256k (DASH Audio) <172>', True, -] - -VIDEO_OPTION_LIST = [] -VIDEO_OPTION_DICT = {} -VIDEO_OPTION_TYPE_DICT = {} - -while video_option_setup_list: - value = video_option_setup_list.pop(0) - key = video_option_setup_list.pop(0) - audio_only_flag = video_option_setup_list.pop(0) - - VIDEO_OPTION_LIST.append(key) - VIDEO_OPTION_DICT[key] = value - VIDEO_OPTION_TYPE_DICT[value] = audio_only_flag - -video_resolution_setup_list = [ - '144p', '144', - '240p', '240', - '360p', '360', - '480p', '480', - '720p', '720', - '720p60', '720', - '1080p', '1080', - '1080p60', '1080', - '1440p', '1440', - '1440p60', '1440', - '2160p', '2160', - '2160p60', '2160', - '4320p', '4320', - '4320p60', '4320', -] - -VIDEO_RESOLUTION_LIST = [] -VIDEO_RESOLUTION_DICT = {} -VIDEO_RESOLUTION_DEFAULT = '720p' - -while video_resolution_setup_list: - key = video_resolution_setup_list.pop(0) - value = video_resolution_setup_list.pop(0) - - VIDEO_RESOLUTION_LIST.append(key) - VIDEO_RESOLUTION_DICT[key] = value - -VIDEO_FPS_DICT = { - # Contains a subset of VIDEO_RESOLUTION_DICT. Only required to distinguish - # 30fps from 60fps formats - '720p60': '60', - '1080p60': '60', - '1440p60': '60', - '2160p60': '60', - '4320p60': '60', -} - -video_format_setup_list = ['mp4', 'flv', 'ogg', 'webm', 'mkv', 'avi', '3gp'] - -VIDEO_FORMAT_LIST = [] -VIDEO_FORMAT_DICT = {} - -while video_format_setup_list: - key = value = video_format_setup_list.pop(0) - - VIDEO_FORMAT_LIST.append(key) - VIDEO_FORMAT_DICT[key] = value - -audio_setup_list = ['mp3', 'wav', 'aac', 'm4a', 'vorbis', 'opus', 'flac'] - -AUDIO_FORMAT_LIST = [] -AUDIO_FORMAT_DICT = {} - -while audio_setup_list: - key = value = audio_setup_list.pop(0) - - AUDIO_FORMAT_LIST.append(key) - AUDIO_FORMAT_DICT[key] = value - -# (Used for detecting video thumbnails. Unfortunately Gtk can't display .webp -# files yet) -IMAGE_FORMAT_LIST = ['.jpg', '.png', '.gif'] -# (The same list including .webp, for any code that needs it) -IMAGE_FORMAT_EXT_LIST = ['.jpg', '.png', '.gif', '.webp'] - -FILE_SIZE_UNIT_LIST = [ - ['Bytes', ''], - ['Kilobytes', 'k'], - ['Megabytes', 'm'], - ['Gigabytes', 'g'], - ['Terabytes', 't'], - ['Petabytes', 'p'], - ['Exabytes', 'e'], - ['Zetta', 'z'], - ['Yotta', 'y'], -] - -DIALOGUE_ICON_DICT = { - 'newbie_classic_icon': 'newbie_classic_icon.png', - 'newbie_icon': 'newbie_icon_64.png', - 'ready_icon': 'ready_icon_64.png', - 'setup_classic_icon': 'setup_classic_icon.png', - 'system_icon': 'system_icon_64.png', - 'yt_icon': 'yt_icon_32.png', - 'yt_remind_icon_en': 'yt_remind_icon_en.png', - 'yt_remind_icon_es': 'yt_remind_icon_es.png', - 'yt_remind_icon_fr': 'yt_remind_icon_fr.png', - 'yt_remind_icon_kr': 'yt_remind_icon_kr.png', - 'yt_remind_icon_nl': 'yt_remind_icon_nl.png', - 'yt_remind_icon_ru': 'yt_remind_icon_ru.png', - 'yt_remind_icon_vi': 'yt_remind_icon_vi.png', -} -if xmas_flag: - DIALOGUE_ICON_DICT['system_icon'] = 'system_icon_xmas_64.png' -elif eesti_flag: - DIALOGUE_ICON_DICT['system_icon'] = 'system_icon_eesti_64.png' -elif anglo_flag: - DIALOGUE_ICON_DICT['system_icon'] = 'system_icon_anglo_64.png' - -if xmas_flag: - STATUS_ICON_DICT = { - 'default_icon': 'status_default_icon_xmas_64.png', - 'check_icon': 'status_check_icon_xmas_64.png', - 'check_live_icon': 'status_check_live_icon_xmas_64.png', - 'download_icon': 'status_download_icon_xmas_64.png', - 'download_live_icon': 'status_download_live_icon_xmas_64.png', - 'update_icon': 'status_update_icon_xmas_64.png', - 'refresh_icon': 'status_refresh_icon_xmas_64.png', - 'info_icon': 'status_info_icon_xmas_64.png', - 'tidy_icon': 'status_tidy_icon_xmas_64.png', - 'livestream_icon': 'status_livestream_icon_xmas_64.png', - 'process_icon': 'status_process_icon_xmas_64.png', - } -elif eesti_flag: - STATUS_ICON_DICT = { - 'default_icon': 'status_default_icon_eesti_64.png', - 'check_icon': 'status_check_icon_eesti_64.png', - 'check_live_icon': 'status_check_live_icon_eesti_64.png', - 'download_icon': 'status_download_icon_eesti_64.png', - 'download_live_icon': 'status_download_live_icon_eesti_64.png', - 'update_icon': 'status_update_icon_eesti_64.png', - 'refresh_icon': 'status_refresh_icon_eesti_64.png', - 'info_icon': 'status_info_icon_eesti_64.png', - 'tidy_icon': 'status_tidy_icon_eesti_64.png', - 'livestream_icon': 'status_livestream_icon_eesti_64.png', - 'process_icon': 'status_process_icon_eesti_64.png', - } -elif anglo_flag: - STATUS_ICON_DICT = { - 'default_icon': 'status_default_icon_anglo_64.png', - 'check_icon': 'status_check_icon_anglo_64.png', - 'check_live_icon': 'status_check_live_icon_anglo_64.png', - 'download_icon': 'status_download_icon_anglo_64.png', - 'download_live_icon': 'status_download_live_icon_anglo_64.png', - 'update_icon': 'status_update_icon_anglo_64.png', - 'refresh_icon': 'status_refresh_icon_anglo_64.png', - 'info_icon': 'status_info_icon_anglo_64.png', - 'tidy_icon': 'status_tidy_icon_anglo_64.png', - 'livestream_icon': 'status_livestream_icon_anglo_64.png', - 'process_icon': 'status_process_icon_anglo_64.png', - } -else: - STATUS_ICON_DICT = { - 'default_icon': 'status_default_icon_64.png', - 'check_icon': 'status_check_icon_64.png', - 'check_live_icon': 'status_check_live_icon_64.png', - 'download_icon': 'status_download_icon_64.png', - 'download_live_icon': 'status_download_live_icon_64.png', - 'update_icon': 'status_update_icon_64.png', - 'refresh_icon': 'status_refresh_icon_64.png', - 'info_icon': 'status_info_icon_64.png', - 'tidy_icon': 'status_tidy_icon_64.png', - 'livestream_icon': 'status_livestream_icon_64.png', - 'process_icon': 'status_process_icon_64.png', - } - -TOOLBAR_ICON_DICT = { - 'tool_channel_large': 'channel_large.png', - 'tool_channel_small': 'channel_small.png', - 'tool_check_large': 'check_large.png', - 'tool_check_small': 'check_small.png', - 'tool_download_large': 'download_large.png', - 'tool_download_small': 'download_small.png', - 'tool_folder_large': 'folder_large.png', - 'tool_folder_small': 'folder_small.png', - 'tool_hide_large': 'hide_large.png', - 'tool_hide_small': 'hide_small.png', - 'tool_options_large': 'options_large.png', - 'tool_options_small': 'options_small.png', - 'tool_playlist_large': 'playlist_large.png', - 'tool_playlist_small': 'playlist_small.png', - 'tool_preferences_large': 'preferences_large.png', - 'tool_preferences_small': 'preferences_small.png', - 'tool_quit_large': 'quit_large.png', - 'tool_quit_small': 'quit_small.png', - 'tool_stop_large': 'stop_large.png', - 'tool_stop_small': 'stop_small.png', - 'tool_switch_large': 'switch_large.png', - 'tool_switch_small': 'switch_small.png', - 'tool_video_large': 'video_large.png', - 'tool_video_small': 'video_small.png', -} - -LARGE_ICON_DICT = { - 'attention_large': 'attention.png', - 'channel_large': 'channel.png', - 'copy_large': 'copy.png', - 'cursor_large': 'cursor.png', - 'error_large': 'error.png', - 'folder_large': 'folder_yellow.png', - 'folder_fixed_large': 'folder_green.png', - 'folder_no_parent_large': 'folder_black.png', - 'folder_private_large': 'folder_red.png', - 'folder_temp_large': 'folder_blue.png', - 'hand_left_large': 'hand_left.png', - 'hand_right_large': 'hand_right.png', - 'learn_left_large': 'learn_left.png', - 'learn_right_large': 'learn_right.png', - 'limits_off_large': 'limits_off.png', - 'limits_on_large': 'limits_on.png', - 'playlist_large': 'playlist.png', - 'question_large': 'question.png', - 'video_large': 'video.png', - 'warning_large': 'warning.png', -} - -LARGE_ICON_COMPOSITE_LIST = [ - 'channel_large', - 'folder_large', - 'folder_fixed_large', - 'folder_no_parent_large', - 'folder_private_large', - 'folder_temp_large', - 'playlist_large', - 'video_large', -] - -SMALL_ICON_DICT = { - 'video_small': 'video.png', - 'channel_small': 'channel.png', - 'playlist_small': 'playlist.png', - 'folder_small': 'folder.png', - - 'archived_small': 'archived.png', - 'arrow_up_small': 'arrow_up.png', - 'arrow_down_small': 'arrow_down.png', - 'attention_small': 'attention.png', - 'check_small': 'check.png', - 'comment_small': 'comment.png', - 'debut_now_small': 'debut_now.png', - 'debut_wait_small': 'debut_wait.png', - 'delete_small': 'delete.png', - 'dl_options_small': 'dl_options.png', - 'download_small': 'download.png', - 'error_small': 'error.png', - 'external_small': 'external.png', - 'favourite_small': 'favourite.png', - 'folder_black_small': 'folder_black.png', - 'folder_blue_small': 'folder_blue.png', - 'folder_green_small': 'folder_green.png', - 'folder_red_small': 'folder_red.png', - 'keyboard_small': 'keyboard.png', - 'likes_small': 'likes.png', - 'have_file_small': 'have_file.png', - 'live_now_small': 'live_now.png', - 'live_old_small': 'live_old.png', - 'live_old_no_file_small': 'live_old_no_file.png', - 'live_wait_small': 'live_wait.png', - 'no_file_small': 'no_file.png', - 'slice_small': 'slice.png', - 'split_file_small': 'split_file.png', - 'stamp_small': 'stamp.png', - 'subs_small': 'subs.png', - 'system_error_small': 'system_error.png', - 'system_warning_small': 'system_warning.png', - 'unavailable_small': 'unavailable.png', - 'uploader_small': 'uploader.png', - 'warning_small': 'warning.png', -} - -THUMB_ICON_DICT = { - 'thumb_none_tiny': 'thumb_none_tiny.png', - 'thumb_none_small': 'thumb_none_small.png', - 'thumb_none_medium': 'thumb_none_medium.png', - 'thumb_none_large': 'thumb_none_large.png', - 'thumb_none_enormous': 'thumb_none_enormous.png', - - 'thumb_left_tiny': 'thumb_left_tiny.png', - 'thumb_left_small': 'thumb_left_small.png', - 'thumb_left_medium': 'thumb_left_medium.png', - 'thumb_left_large': 'thumb_left_large.png', - 'thumb_left_enormous': 'thumb_left_enormous.png', - - 'thumb_right_tiny': 'thumb_right_tiny.png', - 'thumb_right_small': 'thumb_right_small.png', - 'thumb_right_medium': 'thumb_right_medium.png', - 'thumb_right_large': 'thumb_right_large.png', - 'thumb_right_enormous': 'thumb_right_enormous.png', - - 'thumb_both_tiny': 'thumb_both_tiny.png', - 'thumb_both_small': 'thumb_both_small.png', - 'thumb_both_medium': 'thumb_both_medium.png', - 'thumb_both_large': 'thumb_both_large.png', - 'thumb_both_enormous': 'thumb_both_enormous.png', - - 'thumb_default_tiny': 'thumb_default_tiny.png', - 'thumb_default_small': 'thumb_default_small.png', - 'thumb_default_medium': 'thumb_default_medium.png', - 'thumb_default_large': 'thumb_default_large.png', - 'thumb_default_enormous': 'thumb_default_enormous.png', - - 'thumb_block_tiny': 'thumb_block_tiny.png', - 'thumb_block_small': 'thumb_block_small.png', - 'thumb_block_medium': 'thumb_block_medium.png', - 'thumb_block_large': 'thumb_block_large.png', - 'thumb_block_enormous': 'thumb_block_enormous.png', -} - -EXTERNAL_ICON_DICT = { - 'ytdl_gui': 'youtube-dl-gui.png', -} - -# (Replaces system stock icons, if not available) -STOCK_ICON_DICT = { - 'stock_add': 'add_small.png', - 'stock_cancel': 'cancel_small.png', - 'stock_cut': 'cut_small.png', - 'stock_delete': 'delete_small.png', - 'stock_execute': 'ffmpeg_small.png', - 'stock_file': 'file_small.png', - 'stock_find': 'find_small.png', - 'stock_go_back': 'go_back_small.png', - 'stock_go_down': 'go_down_small.png', - 'stock_go_forward': 'go_forward_small.png', - 'stock_go_up': 'go_up_small.png', - 'stock_goto_first': 'goto_first_small.png', - 'stock_goto_last': 'goto_last_small.png', - 'stock_hide_filter': 'hide_filter_small.png', - 'stock_index': 'index_small.png', - 'stock_media_play': 'media_play_small.png', - 'stock_media_stop': 'media_stop_small.png', - 'stock_open': 'open_small.png', - 'stock_properties': 'properties_small.png', - 'stock_properties_large': 'properties_large.png', - 'stock_redo': 'resort_small.png', # Used for a sorting button - 'stock_refresh': 'refresh_small.png', - 'stock_show_filter': 'show_filter_small.png', - 'stock_sort_ascending': 'sort_ascending_small.png', - 'stock_sort_descending': 'sort_descending_small.png', -} - -if xmas_flag: - WIN_ICON_LIST = [ - 'system_icon_xmas_16.png', - 'system_icon_xmas_24.png', - 'system_icon_xmas_32.png', - 'system_icon_xmas_48.png', - 'system_icon_xmas_64.png', - 'system_icon_xmas_128.png', - 'system_icon_xmas_256.png', - 'system_icon_xmas_512.png', - ] -elif eesti_flag: - WIN_ICON_LIST = [ - 'system_icon_eesti_16.png', - 'system_icon_eesti_24.png', - 'system_icon_eesti_32.png', - 'system_icon_eesti_48.png', - 'system_icon_eesti_64.png', - 'system_icon_eesti_128.png', - 'system_icon_eesti_256.png', - 'system_icon_eesti_512.png', - ] -elif anglo_flag: - WIN_ICON_LIST = [ - 'system_icon_anglo_16.png', - 'system_icon_anglo_24.png', - 'system_icon_anglo_32.png', - 'system_icon_anglo_48.png', - 'system_icon_anglo_64.png', - 'system_icon_anglo_128.png', - 'system_icon_anglo_256.png', - 'system_icon_anglo_512.png', - ] -else: - WIN_ICON_LIST = [ - 'system_icon_16.png', - 'system_icon_24.png', - 'system_icon_32.png', - 'system_icon_48.png', - 'system_icon_64.png', - 'system_icon_128.png', - 'system_icon_256.png', - 'system_icon_512.png', - ] - -CONFIG_WIN_ICON_LIST = [ - 'config_icon_16.png', - 'config_icon_24.png', - 'config_icon_32.png', - 'config_icon_48.png', - 'config_icon_64.png', - 'config_icon_128.png', - 'config_icon_256.png', - 'config_icon_512.png', -] - -def do_translate(config_flag=False): - - """Function called for the first time below, setting various values. - - If mainapp.TartubeApp.load_config() changes the locale to something else, - called for a second time to update those values. - - Args: - - config_flag (bool): False for the initial call, True for the second - call from mainapp.TartubeApp.load_config() - - """ - - global FOLDER_ALL_VIDEOS, FOLDER_BOOKMARKS, FOLDER_FAVOURITE_VIDEOS, \ - FOLDER_LIVESTREAMS, FOLDER_MISSING_VIDEOS, FOLDER_NEW_VIDEOS, \ - FOLDER_RECENT_VIDEOS, FOLDER_WAITING_VIDEOS, FOLDER_TEMPORARY_VIDEOS, \ - FOLDER_UNSORTED_VIDEOS, FOLDER_VIDEO_CLIPS - - global YTDL_UPDATE_DICT - - global MAIN_STAGE_QUEUED, MAIN_STAGE_NOT_STARTED, MAIN_STAGE_ACTIVE, \ - MAIN_STAGE_PAUSED, MAIN_STAGE_COMPLETED, MAIN_STAGE_ERROR, \ - MAIN_STAGE_STALLED, ACTIVE_STAGE_PRE_PROCESS, ACTIVE_STAGE_DOWNLOAD, \ - ACTIVE_STAGE_CONCATENATE, ACTIVE_STAGE_POST_PROCESS, \ - ACTIVE_STAGE_CAPTURE, ACTIVE_STAGE_MERGE, ACTIVE_STAGE_CHECKING, \ - COMPLETED_STAGE_FINISHED, COMPLETED_STAGE_WARNING, \ - COMPLETED_STAGE_ALREADY, ERROR_STAGE_ERROR, ERROR_STAGE_STOPPED, \ - ERROR_STAGE_ABORT - - global TIME_METRIC_TRANS_DICT - - global FILE_OUTPUT_NAME_DICT, FILE_OUTPUT_CONVERT_DICT - - global VIDEO_OPTION_LIST, VIDEO_OPTION_DICT - - # System folder names - FOLDER_ALL_VIDEOS = _('All Videos') - FOLDER_BOOKMARKS = _('Bookmarks') - FOLDER_FAVOURITE_VIDEOS = _('Favourite Videos') - FOLDER_LIVESTREAMS = _('Livestreams') - FOLDER_MISSING_VIDEOS = _('Missing Videos') - FOLDER_NEW_VIDEOS = _('New Videos') - FOLDER_RECENT_VIDEOS = _('Recent Videos') - FOLDER_WAITING_VIDEOS = _('Waiting Videos') - FOLDER_TEMPORARY_VIDEOS = _('Temporary Videos') - FOLDER_UNSORTED_VIDEOS = _('Unsorted Videos') - FOLDER_VIDEO_CLIPS = _('Video Clips') - - # youtube-dl update shell commands - YTDL_UPDATE_DICT = { - 'ytdl_update_default_path': - _('Update using default youtube-dl path'), - 'ytdl_update_local_path': - _('Update using local youtube-dl path'), - 'ytdl_update_custom_path': - _('Update using custom youtube-dl path'), - 'ytdl_update_pip': - _('Update using pip'), - 'ytdl_update_pip_no_dependencies': - _('Update using pip (use --no-dependencies option)'), - 'ytdl_update_pip_omit_user': - _('Update using pip (omit --user option)'), - 'ytdl_update_pip3': - _('Update using pip3'), - 'ytdl_update_pip3_no_dependencies': - _('Update using pip3 (use --no-dependencies option)'), - 'ytdl_update_pip3_omit_user': - _('Update using pip3 (omit --user option)'), - 'ytdl_update_pip3_recommend': - _('Update using pip3 (recommended)'), - 'ytdl_update_pypi_path': - _('Update using PyPI youtube-dl path'), - 'ytdl_update_win_32': - _('Windows 32-bit update (recommended)'), - 'ytdl_update_win_32_no_dependencies': - _('Windows 32-bit update (use --no-dependencies option)'), - 'ytdl_update_win_64': - _('Windows 64-bit update (recommended)'), - 'ytdl_update_win_64_no_dependencies': - _('Windows 64-bit update (use --no-dependencies option)'), - 'ytdl_update_disabled': - _('youtube-dl updates are disabled'), - } - - # Download operation stages - MAIN_STAGE_QUEUED = _('Queued') - MAIN_STAGE_NOT_STARTED = _('Not started') - MAIN_STAGE_ACTIVE = _('Active') - MAIN_STAGE_PAUSED = _('Paused') # (not actually used) - MAIN_STAGE_COMPLETED = _('Completed') # (not actually used) - MAIN_STAGE_ERROR = _('Error') - MAIN_STAGE_STALLED = _('Stalled') - # Sub-stages of the 'Active' stage - ACTIVE_STAGE_PRE_PROCESS = _('Pre-processing') - ACTIVE_STAGE_DOWNLOAD = _('Downloading') - ACTIVE_STAGE_CONCATENATE = _('Concatenating') - ACTIVE_STAGE_POST_PROCESS = _('Post-processing') - ACTIVE_STAGE_CHECKING = _('Checking') - # Sub-stages of the 'Completed' stage - COMPLETED_STAGE_FINISHED = _('Finished') - COMPLETED_STAGE_WARNING = _('Warning') - COMPLETED_STAGE_ALREADY = _('Already downloaded') - # Sub-stages of the 'Error' stage - ERROR_STAGE_ERROR = _('Error') # (not actually used) - ERROR_STAGE_STOPPED = _('Stopped') - ERROR_STAGE_ABORT = _('Filesize abort') - - if config_flag: - - for key in TIME_METRIC_TRANS_DICT: - TIME_METRIC_TRANS_DICT[key] = _(key) - - # File output templates use a combination of English words, each of - # which must be translated - ignore_me = _( - 'TRANSLATOR\'S NOTE: ID refers to a video\'s unique ID on the' \ - + ' website, e.g. on YouTube "CS9OO0S5w2k"', - ) - - new_name_dict = {} - for key in FILE_OUTPUT_NAME_DICT.keys(): - - mod_value \ - = re.sub('Custom', _('Custom'), FILE_OUTPUT_NAME_DICT[key]) - mod_value = re.sub('ID', _('ID'), mod_value) - mod_value = re.sub('Title', _('Title'), mod_value) - mod_value = re.sub('Quality', _('Quality'), mod_value) - mod_value = re.sub('Autonumber', _('Autonumber'), mod_value) - - new_name_dict[key] = mod_value - - FILE_OUTPUT_NAME_DICT = new_name_dict - - # Video/audio formats. A number of them contain 'Any format', which - # must be translated - new_list = [] - new_dict = {} - for item in VIDEO_OPTION_LIST: - - mod_item = re.sub('Any format', _('Any format'), item) - new_list.append(mod_item) - new_dict[mod_item] = VIDEO_OPTION_DICT[item] - - VIDEO_OPTION_LIST = new_list - VIDEO_OPTION_DICT = new_dict - - # End of this function - return - - -# Call the function for the first time -do_translate() diff --git a/build/lib/tartube/info.py b/build/lib/tartube/info.py deleted file mode 100644 index 89aaf6da..00000000 --- a/build/lib/tartube/info.py +++ /dev/null @@ -1,628 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Info operation classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import GObject - - -# Import other modules -import os -import queue -import re -import requests -import signal -import subprocess -import threading -import time - - -# Import our modules -import __main__ -import downloads -import utils -# Use same gettext translations -from mainapp import _ - - -# Classes - - -class InfoManager(threading.Thread): - - """Called by mainapp.TartubeApp.info_manager_start(). - - Python class to create a system child process, to do one of three jobs: - - 1. Fetch a list of available formats for a video, directly from youtube-dl - - 2. Fetch a list of available subtitles for a video, directly from - youtube-dl - - 3. Test youtube-dl with specified download options; everything is - downloaded into a temporary directory - - 4. Check the Tartube website, and inform the user if a new release is - available - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - info_type (str): The type of information to fetch: 'formats' for a list - of video formats, 'subs' for a list of subtitles, or 'test_ytdl' - to test youtube-dl with specified options, 'version' to check for a - new release of Tartube - - media_data_obj (media.Video): For 'formats' and 'subs', the media.Video - object for which formats/subtitles should be fetched. For - 'test_ytdl', set to None - - url_string (str): For 'test_ytdl', the video URL to download (can be - None or an empty string, if no download is required, for example - 'youtube-dl --version'. For 'formats' and 'subs', set to None - - options_string (str): For 'test_ytdl', a string containing one or more - youtube-dl download options. The string, generated by a - Gtk.TextView, typically contains newline and/or multiple whitespace - characters; the info.InfoManager code deals with that. Can be None - or an empty string, if no download options are required. For - 'formats' and 'subs', set to None - - """ - - - # Standard class methods - - - def __init__(self, app_obj, info_type, media_data_obj, url_string, - options_string): - - super(InfoManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The video for which information will be fetched (None if - # self.info_type is 'test_ytdl') - self.video_obj = media_data_obj - - # The child process created by self.run() - self.child_process = None - - # Read from the child process STDOUT (i.e. self.child_process.stdout) - # and STDERR (i.e. self.child_process.stderr) in an asynchronous way - # by polling this queue.PriorityQueue object - self.queue = queue.PriorityQueue() - self.stdout_reader = downloads.PipeReader(self.queue, 'stdout') - self.stderr_reader = downloads.PipeReader(self.queue, 'stderr') - - - # IV list - other - # --------------- - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.1 - - # The type of information to fetch: 'formats' for a list of video - # formats, 'subs' for a list of subtitles, 'test_ytdl' to test - # youtube-dl with specified options, or 'version' to check for a new - # release of Tartube - self.info_type = info_type - # For 'test_ytdl', the video URL to download (can be None or an empty - # string, if no download is required, for example - # 'youtube-dl --version'. For 'formats' and 'subs', set to None - self.url_string = url_string - # For 'test_ytdl', a string containing one or more youtube-dl download - # options. The string, generated by a Gtk.TextView, typically - # contains newline and/or multiple whitespace characters; the - # info.InfoManager code deals with that. Can be None or an empty - # string, if no download options are required. For 'formats' and - # 'subs', set to None - self.options_string = options_string - # For 'version', the version numbers (e.g. 1.2.003) retrieved from the - # main website (representing a stable release), and from github - # (representing a development release) - self.stable_version = None - self.dev_version = None - - # Flag set to True if the info operation succeeds, False if it fails - self.success_flag = False - - # The list of formats/subtitles extracted from STDOUT - self.output_list = [] - - # (For debugging purposes, store any STDOUT/STDERR messages received; - # otherwise we would just set a flag if a STDERR message was - # received) - self.stdout_list = [] - self.stderr_list = [] - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Creates a child process to run the youtube-dl system command. - - Reads from the child process STDOUT and STDERR, and calls the main - application with the result of the process (success or failure). - """ - - # Checking for a new release of Tartube doesn't involve any system - # commands or child processes, so it is handled by a separate - # function - if self.info_type == 'version': - - return self.run_check_version() - - # Show information about the info operation in the Output tab - if self.info_type == 'test_ytdl': - - msg = _( - 'Starting info operation, testing downloader with specified' \ - + ' options', - ) - - else: - - if self.info_type == 'formats': - - msg = _( - 'Starting info operation, fetching list of video/audio'\ - + ' formats for \'{0}\'', - ).format(self.video_obj.name) - - else: - - msg = _( - 'Starting info operation, fetching list of subtitles'\ - + ' for \'{0}\'', - ).format(self.video_obj.name) - - self.app_obj.main_win_obj.output_tab_write_stdout(1, msg) - - # Convert a path beginning with ~ (not on MS Windows) - ytdl_path = self.app_obj.check_downloader(self.app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Prepare the system command... - if self.info_type == 'formats': - - cmd_list = [ - ytdl_path, - '--list-formats', - self.video_obj.source, - ] - - elif self.info_type == 'subs': - - cmd_list = [ - ytdl_path, - '--list-subs', - self.video_obj.source, - ] - - else: - - if self.app_obj.ytdl_path_custom_flag: - cmd_list = ['python3'] + [ytdl_path] - else: - cmd_list = [ytdl_path] - - if self.options_string is not None \ - and self.options_string != '': - - # Parse the string into a list. It was obtained from a - # Gtk.TextView, so it can contain newline and/or multiple - # whitepsace characters. Whitespace characters within - # double quotes "..." must be preserved - option_list = utils.parse_options(self.options_string) - for item in option_list: - cmd_list.append(item) - - if self.url_string is not None \ - and self.url_string != '': - - cmd_list.append('-o') - cmd_list.append( - os.path.join( - self.app_obj.temp_test_dir, - '%(title)s.%(ext)s', - ), - ) - - cmd_list.append(self.url_string) - - # ...display it in the Output tab (if required) - space = ' ' - self.app_obj.main_win_obj.output_tab_write_system_cmd( - 1, - space.join(cmd_list), - ) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_child_process(): - pass - - # (Generate our own error messages for debugging purposes, in certain - # situations) - if self.child_process is None: - - msg = _('System process did not start') - self.stderr_list.append(msg) - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - msg, - ) - - elif self.child_process.returncode > 0: - - msg = _('Child process exited with non-zero code: {}').format( - self.child_process.returncode, - ) - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - msg, - ) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.info_manager_finished() - if not self.stderr_list: - self.success_flag = True - - # Show a confirmation in the the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Info operation finished'), - ) - - # Let the timer run for a few more seconds to prevent Gtk errors - GObject.timeout_add( - 0, - self.app_obj.info_manager_halt_timer, - ) - - - def run_check_version(self): - - """Called by self.run(). - - Checking for a new release of Tartube doesn't involve any system - commands or child processes, so it is handled separately by this - function. - - There is a stable release at Sourceforge, and a development release at - Github. Fetch the VERSION file from each, and store the stable/ - development versions, so that mainapp.TartubeApp.info_manager_finished - can display them. - """ - - # Show information about the info operation in the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Starting info operation, checking for new releases of Tartube'), - ) - - # Check the stable version, http://tartube.sourceforge.io/VERSION - stable_path = __main__.__website__ + '/VERSION' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Checking stable release...'), - ) - - self.app_obj.main_win_obj.output_tab_write_system_cmd(1, stable_path) - - try: - request_obj = requests.get( - stable_path, - timeout = self.app_obj.request_get_timeout, - ) - - response = utils.strip_whitespace(request_obj.text) - if not re.search(r'^\d+\.\d+\.\d+\s*$', response): - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Ignoring invalid version'), - ) - - else: - - self.stable_version = response - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Retrieved version:') + ' ' + str(response), - ) - - except: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Connection failed'), - ) - - # Check the development version, - # http://raw.githubusercontent.com/axcore/tartube/master/VERSION - dev_path = __main__.__website_dev__ + '/VERSION' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Checking development release...'), - ) - - self.app_obj.main_win_obj.output_tab_write_system_cmd(1, dev_path) - - try: - request_obj = requests.get( - dev_path, - timeout = self.app_obj.request_get_timeout, - ) - - response = utils.strip_whitespace(request_obj.text) - if not re.search(r'^\d+\.\d+\.\d+\s*$', response): - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Ignoring invalid version'), - ) - - else: - - self.dev_version = response - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Retrieved version:') + ' ' + str(response), - ) - - except: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Connection failed'), - ) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.info_manager_finished() - self.success_flag = True - - # Show a confirmation in the the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Info operation finished'), - ) - - # Let the timer run for a few more seconds to prevent Gtk errors - GObject.timeout_add( - 0, - self.app_obj.info_manager_halt_timer, - ) - - - def create_child_process(self, cmd_list): - - """Called by self.run(). - - Based on code from downloads.VideoDownloader.create_child_process(). - - Executes the system command, creating a new child process which - executes youtube-dl. - - Args: - - cmd_list (list): Python list that contains the command to execute - - """ - - # Strip double quotes from arguments - # (Since we're sending the system command one argument at a time, we - # don't need to retain the double quotes around any single argument - # and, in fact, doing so would cause an error) - cmd_list = utils.strip_double_quotes(cmd_list) - - # Create the child process - info = preexec = None - - if os.name == 'nt': - # Hide the child process window that MS Windows helpfully creates - # for us - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - # Make this child process the process group leader, so that we can - # later kill the whole process group with os.killpg - preexec = os.setsid - - try: - self.child_process = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=preexec, - startupinfo=info, - ) - - except (ValueError, OSError) as error: - # (The code in self.run() will spot that the child process did not - # start) - self.stderr_list.append(_('Child process did not start')) - - - def is_child_process_alive(self): - - """Called by self.run() and .stop_info_operation(). - - Based on code from downloads.VideoDownloader.is_child_process_alive(). - - Called continuously during the self.run() loop to check whether the - child process has finished or not. - - Return values: - - True if the child process is alive, otherwise returns False. - - """ - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def read_child_process(self): - - """Called by self.run(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Return values: - - True if either STDOUT or STDERR were read, None if both queues were - empty - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.app_obj.system_error, - 601, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # On MS Windows we use cp1252, so that Tartube can communicate with the - # Windows console - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - self.output_list.append(data) - self.stdout_list.append(data) - - # Show command line output in the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - data, - ) - - # STDERR (ignoring any empty error messages) - elif data != '': - - # While testing youtube-dl, don't treat anything as an error - if self.info_type == 'test_ytdl': - self.stdout_list.append(data) - - # When fetching subtitles from a video that has none, don't treat - # youtube-dl WARNING: messages as something that makes the info - # operation fail - elif self.info_type == 'subs': - - if not re.search(r'^WARNING\:', data): - self.stderr_list.append(data) - - # When fetching formats, recognise all warnings as errors - else: - self.stderr_list.append(data) - - # Show command line output in the Output tab - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - data, - ) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def stop_info_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Based on code from downloads.VideoDownloader.stop(). - - Terminates the child process. - """ - - if self.is_child_process_alive(): - - if os.name == 'nt': - # os.killpg is not available on MS Windows (see - # https://bugs.python.org/issue5115 ) - self.child_process.kill() - - # When we kill the child process on MS Windows the return code - # gets set to 1, so we want to reset the return code back to - # 0 - self.child_process.returncode = 0 - - else: - os.killpg(self.child_process.pid, signal.SIGKILL) diff --git a/build/lib/tartube/mainapp.py b/build/lib/tartube/mainapp.py deleted file mode 100644 index 11c97db0..00000000 --- a/build/lib/tartube/mainapp.py +++ /dev/null @@ -1,28121 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Main application class.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, GdkPixbuf - - -# Import Python standard modules -from gi.repository import Gio -import datetime -import json -import locale -import math -import os -import pickle -import platform -import re -import shutil -import sys -import threading -import time - -import gettext -_ = gettext.gettext - - -# Import other Python modules -try: - import feedparser - HAVE_FEEDPARSER_FLAG = True -except: - HAVE_FEEDPARSER_FLAG = False - -try: - import matplotlib - HAVE_MATPLOTLIB_FLAG = True -except: - HAVE_MATPLOTLIB_FLAG = False - -try: - import moviepy.editor - HAVE_MOVIEPY_FLAG = True -except: - HAVE_MOVIEPY_FLAG = False - -try: - import playsound - HAVE_PLAYSOUND_FLAG = True -except: - HAVE_PLAYSOUND_FLAG = False - -if os.name != 'nt': - try: - from xdg_tartube import XDG_CONFIG_HOME - HAVE_XDG_FLAG = True - except: - HAVE_XDG_FLAG = False -else: - HAVE_XDG_FLAG = False - -if platform.system() != 'Windows' and platform.system != 'Darwin': - try: - gi.require_version('Notify', '0.7') - from gi.repository import Notify - HAVE_NOTIFY_FLAG = True - except: - HAVE_NOTIFY_FLAG = False -else: - HAVE_NOTIFY_FLAG = False - - -# Import our modules -import __main__ -import classes -import config -import dialogue -import downloads -import ffmpeg_tartube -import files -import formats -import info -import mainwin -import media -import options -import process -import refresh -#import testing -import tidy -import updates -import utils -import wizwin - - -# Classes - - -class TartubeApp(Gtk.Application): - - """Main python class for the Tartube application.""" - - - # Standard class methods - - - def __init__(self, *args, **kwargs): - - # Register the application - if not __main__.__multiple_instance_flag__: - - # Restrict Tartube to a single instance - super(TartubeApp, self).__init__( - *args, - application_id=__main__.__app_id__, - flags=Gio.ApplicationFlags.FLAGS_NONE, - **kwargs, - ) - - else: - - # Permit multiple instances of Tartube - super(TartubeApp, self).__init__( - *args, - application_id=None, - flags=Gio.ApplicationFlags.FLAGS_NONE, - **kwargs, - ) - - - # Debugging flags - # --------------- - # After installation, don't show wizard or dialogue windows, prompting - # the user to select various settings; just use default values - self.debug_no_dialogue_flag = False - # When loading a config/database file, if a lockfile is present, load - # the config/database file anyway (i.e., ignore lockfiles) - self.debug_ignore_lockfile_flag = False - # In the main window's menu, show a menu item for adding a set of - # media data objects for testing - self.debug_test_media_menu_flag = False - # In the main window's menu, show a menu item for executing some - # arbitrary test code (by calling testing.run_test_code()) - self.debug_test_code_menu_flag = False - # Open the main window in the top-left corner of the desktop - self.debug_open_top_left_flag = False - # Automatically open the system preferences window on startup - self.debug_open_pref_win_flag = False - # Automatically open the general download options window on startup - self.debug_open_options_win_flag = False - # Hide all the system folders (this is not reversible by setting the - # flag back to False) - self.debug_hide_folders_flag = False - # Disable showing mainwin.NewbieDialogue altogether - self.debug_disable_newbie_flag = False - # Write the Gtk version to the terminal on startup - self.debug_write_gtk_flag = False - - - # Instance variable (IV) list - class objects - # ------------------------------------------- - # The main window object, set as soon as it's created - self.main_win_obj = None - # The system tray icon (a mainwin.StatusIcon object, inheriting from - # Gtk.StatusIcon) - self.status_icon_obj = None - # - # At the moment, there are seven operations - the download, update, - # refresh, info, tidy, livestream and process operations - # Only one operation can be in progress at a time. When an operation is - # in progress, many functions (such as opening configuration windows) - # are not possible - # - # A download operation is handled by a downloads.DownloadManager - # object. It downloads files from a server (for example, it downloads - # videos from YouTube) - # Although its not possible to run more than one download - # operation at a time, a single download operation can handle - # multiple simultaneous downloads - # The current downloads.DownloadManager object, if a download operation - # is in progress (or None, if not) - self.download_manager_obj = None - # An update operation (to update youtube-dl) is handled by an - # updates.UpdateManager object. It updates youtube-dl to the latest - # version - # The current updates.UpdateManager object, if an upload operation is - # in progress (or None, if not) - self.update_manager_obj = None - # A refresh operation compares the media registry with the contents of - # Tartube's data directories, adding new videos to the media registry - # and marking missing videos as not downloaded, as appropriate - # The current refresh.RefreshManager object, if a refresh operation is - # in progress (or None, if not) - self.refresh_manager_obj = None - # An info operation fetches information about a particular video; - # currently, its available formats and available subtitles - # It can also perform a youtube-dl test using specified download - # options; any downloaded files are stored in a temporary directory - # It can also check the Tartube website, to tell the user if a new - # release is available - # The current info.InfoManager object, if an info operation is in - # progress (or None, if not) - self.info_manager_obj = None - # A tidy operation can check that videos still exist and aren't - # corrupted, or can remove all videos, or all thumbnails, and so on - # The current tidy.TidyManager object, if a tidy operation is in - # progress (or None, if not) - self.tidy_manager_obj = None - # A livestream operation is handled by a downloads.StreamManager - # object. It checks media.Video objects marked as livestreams, to - # see whether have started or stopped broadcasting - # Livestreams operations can take place when a download operation is - # already running (but not when any other kind of operation is - # running) - # If a download operation is started when a livestream operation is - # running, the livestream operation is cancelled immediately - self.livestream_manager_obj = None - # A process operation is handled by a process.ProcessManager object. It - # sends a list of media.Video objects to FFmpeg for processing - # The current process.ProcessManager object, if a process operation is - # in progress (or None, if not) - self.process_manager_obj = None - # - # When an operation is in progress, the manager object is stored here - # (so code can quickly check if an operation is in progress, or not) - # Livestream operations run silently in the background, and no - # functionality is disabled. Therefore, this IV remains set to None - # when the livestream operation is running - self.current_manager_obj = None - # - # The file manager, files.FileManager, for loading thumbnail, icon - # and JSON files safely (i.e. without causing a Gtk crash) - self.file_manager_obj = files.FileManager() - # The FFmpeg manager, for when Tartube needs to call FFmpeg directly. - # Most of the code has been adapted from youtube-dl - self.ffmpeg_manager_obj = ffmpeg_tartube.FFmpegManager(self) - # The message dialogue manager, dialogue.DialogueManager, for showing - # message dialogue windows safely (i.e. without causing a Gtk crash) - self.dialogue_manager_obj = None - - - # Instance variable (IV) list - other - # ----------------------------------- - # Flag set to True when startup is complete. Set to True by the last - # line of code in self.start_continue() - self.startup_complete_flag = False - - # N.B. Setting will not work if the current working directory is the - # one containing this file. If running Tartube from source code, run - # it from the directory containing setup.py! - # System locale (e.g. en_GB) - self.system_locale = locale.getdefaultlocale()[0] - # Override locale. If None, use the system locale - self.override_locale = None - # The current locale (self.override_locale if set; self.current_locale - # otherwise) - self.current_locale = self.system_locale - - # Default regex for handling video timestamps (a string in the form - # 'mm:ss' or 'h:mm:ss'. Leading zeroes are optional for all - # components, and the 'h' component can contain any number of digits) - self.timestamp_regex = r'((\d+)\:)?([0-5]?[0-9]):([0-5]?[0-9])' - - # Default window sizes (in pixels) - self.main_win_width = 1000 - self.main_win_height = 700 - self.config_win_width = 650 - self.config_win_height = 450 - # Default slider position. This value applies to the sliders in the - # Videos, Progress and Classic Mode tabs - self.paned_default_size = 250 - # Default size (in pixels) of space between various widgets - self.default_spacing_size = 5 - # Default thumbnail sizes, assuming an original size of 1280x720. All - # sizes are available in the Video Catalogue, when videos are - # displayed as a grid; otherwise only the 'tiny' size is used - self.thumb_size_dict = { - 'tiny' : [128, 72], - 'small' : [256, 144], - 'medium' : [384, 216], - 'large' : [512, 288], - 'enormous' : [640, 360], - } - # Ordered list of thumbnail sizes and translations, for use in various - # comboboxes - self.thumb_size_list = [ - _('Tiny'), 'tiny', - _('Small'), 'small', - _('Medium'), 'medium', - _('Large'), 'large', - _('Enormous'), 'enormous', - ] - # A custom thumbnail size; one of the keys in self.thumb_size_dict. - # Used when the Video Catalogue is displaying videos in a grid. (When - # displaying them as a list, the 'small' size is always used) - self.thumb_size_custom = 'small' - - # Custom window sizes - # Flag set to True if Tartube should remember the main window size - # when saving the config file, and then use that size when - # re-starting tartube - self.main_win_save_size_flag = False - # Flag set to True if Tartube should remember the positions of the - # sliders in the Video, Progress and Classic Mode tabs. If False, - # the sliders have default positions. Ignored if - # self.main_win_save_size_flag is not True - self.main_win_save_slider_flag = False - # The size of the main window, when the config file was last saved... - self.main_win_save_width = self.main_win_width - self.main_win_save_height = self.main_win_height - # ...and the position of the sliders in the Videos, Progress and - # Classic Mode tabs, when the config file was last saved - self.main_win_videos_slider_posn = self.paned_default_size - self.main_win_progress_slider_posn = self.paned_default_size - self.main_win_classic_slider_posn = self.paned_default_size + 75 - # Because of Gtk issues, resetting main window sliders to their default - # positions has to be done twice. Flag set to True if - # self.script_fast_timer_callback() should reset the position of - # sliders again - self.main_win_slider_reset_flag = False - - # The current Gtk version - self.gtk_version_major = Gtk.get_major_version() - self.gtk_version_minor = Gtk.get_minor_version() - self.gtk_version_micro = Gtk.get_micro_version() - - # Standard timeout (in seconds) for calls to the Python requests module - # when downloading a URL - self.request_get_timeout = 30 - - # IVs used to place a lock on the loaded database file, so that - # competing instances of Tartube don't try to use it at the same time - # Time to wait (in seconds) to save the config file, if a lockfile - # exists for it - self.config_lock_time = 5 - # The path to the database lockfile created by this instance of - # Tartube (None if no lockfile has been created) - self.db_lock_file_path = None - # Flag set to True while self.load_db() is being executed, and set - # back to False when it stops - # If the code crashes because of a python error, it won't be obvious - # to the user that the load has failed. Instead, before starting an - # operation, this flag is checked and, if True, the operation does - # not start and load/save is disabled as normal - # N.B. This is a failsafe; in most cases, an interrupted database load - # leaves widgets such as the 'Check all' and 'Download all' buttons - # (in the Videos tab) desensitised anyway - self.db_loading_flag = False - - # At all times (after initial setup), two GObject timers run - a fast - # one and a slow one - # The slow timer's ID - self.script_slow_timer_id = None - # The slow timer interval time (in milliseconds) - self.script_slow_timer_time = 30000 - # A timer that calls self.script_slow_timer_callback() once, before - # the first call from self.script_slow_timer_id, and just a few - # seconds after Tartube starts - # (Any scheduled downloads which are due to start when Tartube starts, - # actually start a few seconds later, for aesthetic reasons) - self.script_once_timer_id = None - # The once-only timer interval (in milliseconds) - self.script_once_timer_time = 3000 - # The fast timer's ID - self.script_fast_timer_id = None - # The fast timer interval time (in milliseconds) - self.script_fast_timer_time = 250 - - # Flag set to True if the main toolbar should not be drawn when the - # main window is opened - self.toolbar_hide_flag = False - # Flag set to True if the main toolbar should be compressed (by - # removing the labels); ideal if the toolbar's contents won't fit in - # the standard-sized window (as it almost certainly won't on MS - # Windows) - if os.name != 'nt': - self.toolbar_squeeze_flag = False - else: - self.toolbar_squeeze_flag = True - # Flag set to True if the 'Show' button on the main toolbar (which - # hides most system folders) is selected - self.toolbar_system_hide_flag = False - # Flag set to True if tooltips should be visible in the Video Index, - # Video Catalogue, Progress List, Results List and Classic Progress - # List - self.show_tooltips_flag = True - # Flag set to True if tooltips should include errors/warnings in the - # Progress List, Results List and Classic Progress List (only). - # Ignored if self.show_tooltips_flag is False - self.show_tooltips_extra_flag = True - # Flag set to True if stock icons in the Videos/Classic Mode tabs - # should be replaced by a custom set of icons (in case the stock - # icons are not visible, for some reason) - self.show_custom_icons_flag = True - # Flag set to True if a marker should be visible on each row in the - # Video Index, False if not - self.show_marker_in_index_flag = True - # Flag set to True if small icons should be used in the Video Index, - # False if large icons should be used - self.show_small_icons_in_index_flag = False - # Flag set to True if the Video Index treeview should auto-expand/ - # auto-collapse when an item is clicked, to show/hide its children - # (only folders have children visible in the Video Index, though) - self.auto_expand_video_index_flag = False - # Flag set to True if the treeview should be fully expanded when an - # item is clicked; False if only the next level should be expanded - # (ignored if self.auto_expand_video_index_flag is False) - self.full_expand_video_index_flag = False - # Flag set to True if the 'Download all' and 'Custom download all' - # buttons in the main window toolbar and in the Videos tab should be - # disabled (in case the user is sure they only want to do simulated - # downloads) - # Does not apply to the download buttons in the Classic Mode tab - self.disable_dl_all_flag = False - # Flag set to True if a 'Custom download all' button should be visible - # in the Videos tab - self.show_custom_dl_button_flag = False - # Flag set to True if free disk space should be visible in the Videos - # tab during a download operation - self.show_free_space_flag = True - # Flag set to True if we should use 'Today' and 'Yesterday' in the - # Video Index, rather than a date - self.show_pretty_dates_flag = True - - # In the Video Catalogue, flags set to True if we should filter videos - # by name, description and/or comments - self.catalogue_filter_name_flag = True - self.catalogue_filter_descrip_flag = False - self.catalogue_filter_comment_flag = False - # In the Video Catalogue, flag set to True if a frame should be drawn - # around each video, False if not - self.catalogue_draw_frame_flag = True - # In the Video Catalogue, flag set to True if status icons should be - # drawn, False if not - self.catalogue_draw_icons_flag = True - # In the Video Catalogue, flag set to True if downloaded videos should - # be drawn, False if not. Note that when a filter is applied, any - # matching videos are always visible, regardless of the value of this - # IV - self.catalogue_draw_downloaded_flag = True - # In the Video Catalogue, flag set to True if undownloaded videos - # should be drawn, False if not. Note that when a filter is applied, - # any matching videos are always visible, regardless of the value of - # this IV - self.catalogue_draw_undownloaded_flag = True - # In the Video Catalogue, flag set to True if blocked videos should be - # drawn, False if not. Note that when a filter is applied, any - # matching videos are always visible, regardless of the value of this - # IV - self.catalogue_draw_blocked_flag = False - # In the Video Catalogue, flag set to True if channel/playlist names - # should be clickable (in grid mode only) - self.catalogue_clickable_container_flag = True - # In the Video Catalogue, flag set to True if the video .nickname - # should be displayed, or False if the video .name should be - # displayed - self.catalogue_show_nickname_flag = True - - # Flags specifying what data should be transferred to an external - # application, if videos are dragged there from the Video Catalogue - # (and also from the Results List and Classic Progress List) - # All or any of the flags may be set. If none are set, or if only - # self.drag_video_separator_flag is set, no data is transferred - # Flag set to True if the transferred text should start with a - # separator (line with -----) - self.drag_video_separator_flag = False - # Flag set to True if the full file path should be transferred - self.drag_video_path_flag = True - # Flag set to True if the video's source URL should be transferred - self.drag_video_source_flag = False - # Flag set to True if the video's name should be transferred - self.drag_video_name_flag = False - # Flag set to True if any errors/warnings produced the last time this - # video was checked/downloaded should be transferred - self.drag_video_msg_flag = False - # Flag set to True if the full file path to the thumbnail file should - # be transferred - self.drag_thumb_path_flag = False - - # Flags specifying what data should be transferred to an external - # application, if messages in the Errors/Warnings tab are dragged - # there - # All or any of the flags may be set. If none are set, or if only - # self.drag_error_separator_flag is set, no data is transferred - # Flag set to True if the transferred text should start with a - # separator (line with -----) - self.drag_error_separator_flag = False - # Flag set to True if the full file path should be transferred - self.drag_error_path_flag = True - # Flag set to True if the video/channel/playlist source URL should be - # transferred - self.drag_error_source_flag = False - # Flag set to True if the video/channel/playlist name should be - # transferred - self.drag_error_name_flag = False - # Flag set to True if any error/warning itself should be transferred - self.drag_error_msg_flag = True - - # Flag set to True if an icon should be displayed in the system tray - self.show_status_icon_flag = True - # Flag set to True if Tartube should open in the system tray. Ignored - # if self.show_status_icon_flag is False - self.open_in_tray_flag = False - # Flag set to True if the main window should close to the tray, rather - # than halting the application altogether. Ignored if - # self.show_status_icon_flag is False - self.close_to_tray_flag = False - # Flag set to True if Tartube should remember the position of the main - # window, when it is closed to the tray. (Does not work at all on - # Wayland, and does not apply when Tartube shuts down and restarts) - self.restore_posn_from_tray_flag = False - - # Flag set to True if rows in the Progress List should be hidden once - # the download operation has finished with the corresponding media - # data object (so the user can see the media data objects currently - # being downloaded more easily) - self.progress_list_hide_flag = False - # Flag set to True if new rows should be added to the Results List - # at the top, False if they should be added at the bottom - self.results_list_reverse_flag = False - # Flag set to True if the width of certain columns in the Progress - # List, Results List and Classic Progress List should be remembered - # for the next session - self.progress_list_remember_width_flag = False - # Remembered sizes of those columns, or None to use the default size - self.progress_list_width_source = None - self.progress_list_width_incoming = None - self.results_list_width_video = None - self.classic_progress_list_width_source = None - self.classic_progress_list_width_incoming = None - - # Flag set to True if system error messages should be shown in the - # Errors/Warnings tab - self.system_error_show_flag = True - # Flag set to True if system warning messages should be shown in the - # Errors/Warnings tab - self.system_warning_show_flag = True - # Flag set to True if operation error messages should be shown in the - # Errors/Warnings tab - self.operation_error_show_flag = True - # Flag set to True if operation warning messages should be shown in the - # Errors/Warnings tab - self.operation_warning_show_flag = True - - # Flag set to True if the date (as well as the time) should be shown in - # the Errors/Warnings tab - self.system_msg_show_date_flag = True - # Flag set to True if the channel/playlist/folder name should be shown - # in the Errors/Warnings tab - self.system_msg_show_container_flag = True - # Flag set to True if the video name should be shown in the Errors/ - # Warnings tab - self.system_msg_show_video_flag = True - # Flag set to True if the multi-line messages should be shown in the - # Errors/ Warnings tab - self.system_msg_show_multi_line_flag = True - # Flag set to True if the total number of system error/warning messages - # visible (not including hidden messages) in the tab label is not - # reset until the 'Clear the list' button is explicitly clicked - # (normally, the total numbers are reset when the user switches to a - # different tab) - self.system_msg_keep_totals_flag = False - - # For quick lookup, the directory in which the 'tartube' executable - # file is found, and its parent directory - self.script_dir = sys.path[0] - self.script_parent_dir = os.path.abspath( - os.path.join(self.script_dir, os.pardir), - ) - - # Tartube's data directory (platform-dependent), i.e. 'tartube-data' - # Note that, using the MSWin installer, Cygwin gives file paths with - # both / and \ separators. Throughout the code, we use - # os.path.abspath to circumvent this problem - self.default_data_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - ), - ) - self.data_dir = self.default_data_dir - # A list of data directories used recently by the user. The list - # includes the current value of self.data_dir, and can be - # customised by the user (to forget directories no longer needed) - # Multiple instances of Tartube can use the same config file, but - # they cannot use the same database file at the same time - # When Tartube starts, if the database file in the directory - # self.data_dir is locked, Tartube will try other directories in this - # list, in order, until finding one that isn't locked - self.data_dir_alt_list = [ self.data_dir ] - # self.data_dir records the path to the database file that was in - # memory, when the config file was last saved. Flag set to False to - # use this path (meaning that, on startup, the same database file is - # loaded), or True if the first path in self.data_dir_alt_list is - # loaded instead - self.data_dir_use_first_flag = True - # On startup (but not when switching databases), if the database file - # in self.data_dir is locked, when this flag is True Tartube will try - # other directories in self.data_dir_alt_list (as described above). - # If False, only self.data_dir is tried - self.data_dir_use_list_flag = True - # When switching to a new database file, the data directory (containing - # the file) is added to the list, if the flag it True - self.data_dir_add_from_list_flag = True - - # The data directory is structured like this: - # /tartube-data - # tartube.db [the Tartube database file] - # /.backups - # tartube_BU.db [any number of database file backups] - # /.temp [temporary directory, deleted on startup] - # /pewdiepie [example of a custom media.Channel] - # /Temporary Videos [standard media.Folder] - # /Unsorted Videos [standard media.Folder] - # /Video Clips [standard media.Folder] - # Before v1.3.099, the data directory was structured like this: - # /tartube-data - # tartube.db - # tartube_BU.db - # /.temp - # /downloads - # /pewdiepie - # /Temporary Videos - # /Unsorted Videos - # Tartube can read from both stcuctures although, when creating a new - # data directory, only the new structure is created - # - # The sub-directory into which videos are downloaded (new and old - # style) - self.downloads_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - ), - ) - self.alt_downloads_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - 'downloads', - ), - ) - # A hidden directory, used for storing backups of the Tartube database - # file - self.backup_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - '.backups', - ), - ) - - # A temporary directory, deleted when Tartube starts and stops - self.temp_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - '.temp', - ), - ) - # Inside the temporary directory, a downloads folder, replicating the - # layout of self.downloads_dir, and used for storing description, - # JSON and thumbnail files which the user doesn't want to store in - # self.downloads_dir - self.temp_dl_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - '.temp', - 'downloads', - ), - ) - # Inside the temporary directory, a test folder into which an info - # operation can allow youtube-dl to download files - self.temp_test_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - '.temp', - 'ytdl-test', - ), - ) - - # When the user tries to switch databases (in a call to - # self.switch_db() ), we make backup copies of those IVs. If the - # switch fails, then their values can be restored, and the user can - # continue using the old database as normal - self.backup_data_dir = None - self.backup_downloads_dir = None - self.backup_alt_downloads_dir = None - self.backup_backup_dir = None - self.backup_temp_dir = None - self.backup_temp_dl_dir = None - self.backup_temp_test_dir = None - self.backup_data_dir_alt_list = None - - # The user can opt to move thumbnails to a '.thumbs' sub-directory, and - # other metadata files to a '.data' sub-directory (by setting the - # download option 'move_description', etc) - # The names of those sub-directories - self.thumbs_sub_dir = '.thumbs' - self.metadata_sub_dir = '.data' - - # By default, Tartube passes a path to a cookie jar to youtube-dl, to - # prevent it writing one to ../tartube/tartube. The name of the - # default file, which is stored in the data directory - # If the options manager specifies a different path, then that path is - # passed to youtube-dl instead - self.cookie_file_name = 'cookies.txt' - - # The directory in which sound files are found, set in the call to - # self.find_sound_effects() - self.sound_dir = None - # List of sound files found in the ../sounds directory (e.g. - # 'beep.mp3') - self.sound_list = [] - # The user's preferred sound effect (for livestream alarms) - self.sound_custom = 'bell.mp3' - - # Name of the Tartube config file - self.config_file_name = 'settings.json' - # The config file can be stored at one of two locations, depending on - # whether XDG is available, or not - self.config_file_dir = os.path.abspath(self.script_parent_dir) - self.config_file_path = os.path.abspath( - os.path.join(self.script_parent_dir, self.config_file_name), - ) - - if not HAVE_XDG_FLAG: - self.config_file_xdg_dir = None - self.config_file_xdg_path = None - else: - self.config_file_xdg_dir = os.path.abspath( - os.path.join( - XDG_CONFIG_HOME, - __main__.__packagename__, - ), - ) - - self.config_file_xdg_path = os.path.abspath( - os.path.join( - XDG_CONFIG_HOME, - __main__.__packagename__, - self.config_file_name, - ), - ) - - # Name of the Tartube database file (storing media data). The database - # file is always found somewhere in self.data_dir - self.db_file_name = __main__.__packagename__ + '.db' - # Names of the database export files (for JSON, CSV and plain text) - self.export_json_file_name \ - = __main__.__packagename__ + '_db_export.json' - self.export_csv_file_name \ - = __main__.__packagename__ + '_db_export.csv' - self.export_text_file_name \ - = __main__.__packagename__ + '_db_export.txt' - # The separator to use for CSV exports/imports. This will be escaped in - # regexes, so only values such as '|' and ',' should be used - self.export_csv_separator = '|' - # How Tartube should make backups of its database file: - # 'default' - make a backup file during a save procedure, but delete - # it when the save procedure is complete - # 'single' - make a backup file during a save procedure, replacing - # any existing backup file, and don't delete it when the save - # procedure is complete - # 'daily' - make a backup file once per day, the first time a save - # procedure is performed in that day. The file is labelled with - # the date, so backup files from previous days are not - # overwritten - # 'always' - always make a backup file, labelled with the date and - # time, so that no backup file is ever overwritten - self.db_backup_mode = 'always' - # If loading/saving of a config or database file fails, this flag is - # set to True, which disables all loading/saving for the rest of the - # session - # Exception: as of v2.3.555, a failure to save the database file no - # longer sets this flag to True - self.disable_load_save_flag = False - # ...but it does set this flag to True, preventing scheduled downloads - # from starting until the database has been successfully saved (or - # loaded) - self.disable_scheduled_dl_flag = False - # Optional error message generated when self.disable_load_save_flag - # was set to True - self.disable_load_save_msg = None - # If loading a database file (only) fails because of a lock file, this - # flag is set to True, so the user is prompted to remove the possibly - # stale lock file. If the user declines, the error message stored in - # self.disable_load_save_msg is then displayed - self.disable_load_save_lock_flag = False - # Users have reported that the Tartube database file was corrupted. On - # inspection, it was almost completely empty, presumably because - # self.save_db() had been called before .load_db() - # As the corruption was catastrophic, make sure that can never happen - # again with this flag, set to False until the code has either - # loaded a database file, or wants to call .save_db to create one - self.allow_db_save_flag = False - - # Flag set to True if the Classic Mode tab should be the visible one, - # when Tartube first starts (for the benefit of users who only want - # Classic Mode downloads) - self.show_classic_tab_on_startup_flag = False - # Flag set to True if custom downloads are enabled in the Classic Mode - # tab - self.classic_custom_dl_flag = False - # Users can add more destination directories to the combobox in the - # Classic Mode tab. Tartube remembers those directories, up to the - # maximum number specified below - self.classic_dir_list = [ os.path.expanduser('~') ] - # The maximum size of the list. When a new directory is added by the - # user, it's moved to the top of the list. If the list is now too - # big, the last item is removed - self.classic_dir_max = 8 - # The most recently-selected destination directory. On startup, if this - # directory still exists in self.classic_dir_list, it is moved to the - # top (and so it appears as the first item in the combobox). This IV - # is then reset - self.classic_dir_previous = None - # The selected format. If 'Default' is selected in the Classic Mode - # tab's combo, set to None - self.classic_format_selection = None - # Flag set to False, if videos should be downloaded in that format, or - # True if they should be converted to the format using FFmpeg/AVConv - self.classic_format_convert_flag = True - # The selected resolution. If 'Resolution' is selected in the Classic - # Mode tab's combo, set to None - self.classic_resolution_selection = None - # Flag set to True, if the URL should be treated as a broadcasting - # livestream - self.classic_livestream_flag = False - # Flag set to True, if SponsorBlock should be used with the URL - self.classic_sblock_flag = False - # Flag set to True, if pending URLs (still visible in the top half of - # the Classic Mode tab, or not yet downloaded in the bottom half) - # should be saved when Tartube shuts down, and restored (to the top - # half) when Tartube restarts - self.classic_pending_flag = False - # List of pending URLs. Set just before the config file is saved, and - # used just after it is loaded - self.classic_pending_list = [] - # In the Classic Mode tab, when the user clicks the 'Add URLs' button, - # flag set to True if a duplicate URL (one which has already been - # added to the Classic Progress List), should be deleted from the - # textview at the top, rather than being retained - self.classic_duplicate_remove_flag = False - - # The youtube-dl binary to use (platform-dependent) - 'youtube-dl' or - # 'youtube-dl.exe', depending on the platform. The default value is - # set by self.start() - self.ytdl_bin = None - # The default path to the youtube-dl binary. The value is set by - # self.start(). On MSWin, it is 'youtube-dl.exe'. On Linux, it is - # '/usr/bin/youtube-dl' - self.ytdl_path_default = None - # The path to the youtube-dl binary, after installation using PyPI. - # Not used on MS Windows. The initial ~ character must be substituted - # for os.path.expanduser('~'), before use - self.ytdl_path_pypi = '~/.local/bin/youtube-dl' - # The actual path to use in the shell command during a download or - # update operation. Initially given the same value as - # self.ytdl_path_default. After configurations, its value might be - # '/usr/bin/youtube-dl', '~/.local/bin/youtube-dl', just 'youtube-dl' - # or a custom path specified by the user - self.ytdl_path = None - # When the user has selected a custom path, this flag is set to True - # (even when that path is '/usr/bin/youtube-dl' or one of the other - # values listed above) - self.ytdl_path_custom_flag = False - # The shell command to use during an update operation depends on how - # youtube-dl was installed - # Depending on the operating system, Tartube provides some of these - # methods (listed here with the description visible to the user): - # - # 'ytdl_update_default_path' - # Update using default youtube-dl path - # 'ytdl_update_local_path' - # Update using local youtube-dl path - # 'ytdl_update_custom_path' - # Update using the path sepcified by self.ytdl_path - # 'ytdl_update_pip' - # Update using pip - # 'ytdl_update_pip_no_dependencies' - # Update using pip (use --no-dependencies option) - # 'ytdl_update_pip_omit_user' - # Update using pip (omit --user option) - # 'ytdl_update_pip3' - # Update using pip3 - # 'ytdl_update_pip3_no_dependencies' - # Update using pip3 (use --no-dependencies option) - # 'ytdl_update_pip3_omit_user' - # Update using pip3 (omit --user option) - # 'ytdl_update_pip3_recommend' - # Update using pip3 (recommended) - # 'ytdl_update_pypi_path' - # Update using PyPI youtube-dl path - # 'ytdl_update_win_32', - # Windows 32-bit update (recommended) - # 'ytdl_update_win_32_no_dependencies', - # Windows 32-bit update (use --no-dependencies option) - # 'ytdl_update_win_64', - # Windows 64-bit update (recommended) - # 'ytdl_update_win_64_no_dependencies', - # Windows 64-bit update (use --no-dependencies option) - # 'ytdl_update_disabled' - # youtube-dl updates are disabled - # A dictionary containing some possibilities, populated by self.start() - # Dictionary in the form - # key: method name (one of those listed above) - # value: list of words to use in the shell command - self.ytdl_update_dict = {} - # A list of keys from self.ytdl_update_dict in a standard order (so the - # combobox in config.SystemPrefWin is in a standard order) - self.ytdl_update_list = [] - # The user's choice of shell command; one of the keys in - # self.ytdl_update_dict, set by self.setup_paths() - self.ytdl_update_current = None - - # Flag set to True if the Output tab should be revealed automatically - # during an update operation, and during some info operations - self.auto_switch_output_flag = True - # Maximum size of textviews in the Output tab - self.output_size_default = 1000 - # (Absolute minimum and maximum values) - self.output_size_max = 10000 - self.output_size_min = 1 - # Flag set to True when the limit is actually applied, False when not - self.output_size_apply_flag = True - - # Flag set to True if an update operation has succeeded at least once - # (the first time, we try to auto-detect youtube-dl's location) - self.ytdl_update_once_flag = False - # If specified the name of a youtube-dl fork to use, instead of the - # original youtube-dl. When specified, all system commands replace - # youtube-dl with this value - # If not specified, the value should be None (not an empty string). - # Ignored when self.ytdl_path_custom_flag is True - # (Tartube assumes that the fork is largely compatible with the - # original) - self.ytdl_fork = None - # Descriptions of various forks, used in the preference window and also - # in the wizard window - self.ytdl_fork_descrip_dict = { - 'yt-dlp': \ - 'A popular fork of the original youtube-dl, created in 2020' \ - + ' by pukkandan. Recommended for all users.', - 'youtube-dl': \ - 'This is the original downloader, created by Ricardo Garcia' \ - + ' Gonzalez in 2006. NOT recommended for most users.', - 'custom': \ - 'Tartube may be compatible with other versions of youtube-dl.', - } - # v2.3.182: Currently, yt-dlp can't be installed on MS Windows under - # MSYS2, because the pycryptodome dependency can't be installed - # Flag set to True if yt-dlp (only), when installed via pip, should be - # installed without dependencies - if os.name == 'nt': - self.ytdl_fork_no_dependency_flag = True - else: - self.ytdl_fork_no_dependency_flag = False - - # Flag set to True if youtube-dl system commands should be displayed in - # the Output tab - self.ytdl_output_system_cmd_flag = True - # Flag set to True if youtube-dl's STDOUT should be displayed in the - # Output tab - self.ytdl_output_stdout_flag = True - # Flag set to True if we should ignore JSON output when displaying text - # in the Output tab (ignored if self.ytdl_output_stdout_flag is - # False) - self.ytdl_output_ignore_json_flag = True - # Flag set to True if we should ignore download progress (as a - # percentage) when displaying text in the Output tab (ignored if - # self.ytdl_output_stdout_flag is False) - self.ytdl_output_ignore_progress_flag = True - # Flag set to True if youtube-dl's STDERR should be displayed in the - # Output tab - self.ytdl_output_stderr_flag = True - # Flag set to True if pages in the Output tab should be emptied at the - # start of each operation - self.ytdl_output_start_empty_flag = True - # Flag set to True if a summary page should be visible in the Output - # tab. Changes to this flag are applied when Tartube restarts - self.ytdl_output_show_summary_flag = False - - # Flag set to True if youtube-dl system commands should be written to - # the terminal window - self.ytdl_write_system_cmd_flag = False - # Flag set to True if youtube-dl's STDOUT should be written to the - # terminal window - self.ytdl_write_stdout_flag = False - # Flag set to True if we should ignore JSON output when writing to the - # terminal window (ignored if self.ytdl_write_stdout_flag is False) - self.ytdl_write_ignore_json_flag = True - # Flag set to True if we should ignore download progress (as a - # percentage) when writing to the terminal window (ignored if - # self.ytdl_write_stdout_flag is False) - self.ytdl_write_ignore_progress_flag = True - # Flag set to True if youtube-dl's STDERR should be written to the - # terminal window - self.ytdl_write_stderr_flag = False - - # Name of an optional downloader log, written in the downloads folder - if os.name != 'nt': - self.ytdl_log_name = 'ytdl-log.log' - else: - self.ytdl_log_name = 'ytdl-log.txt' - # Flag set to True if youtube-dl system commands should be written to - # the downloader log - self.ytdl_log_system_cmd_flag = False - # Flag set to True if youtube-dl's STDOUT should be written to the - # downloader log - self.ytdl_log_stdout_flag = False - # Flag set to True if we should ignore JSON output when writing to the - # downloader log (ignored if self.ytdl_log_stdout_flag is False) - self.ytdl_log_ignore_json_flag = True - # Flag set to True if we should ignore download progress (as a - # percentage) when writing to the downloader log (ignored if - # self.ytdl_log_stdout_flag is False) - self.ytdl_log_ignore_progress_flag = True - # Flag set to True if youtube-dl's STDERR should be written to the - # downloader log - self.ytdl_log_stderr_flag = False - - # Flag set to True if youtube-dl should show verbose output (using the - # --verbose option). The setting applies to the Output tab, the - # terminal window and/or the downloader log - self.ytdl_write_verbose_flag = False - - # Flag set to True if, during a refresh operation, videos should be - # displayed in the Output tab. Set to False if only channels, - # playlists and folders should be displayed there - self.refresh_output_videos_flag = True - # Flag set to True if, during a refresh operation, non-matching videos - # should be displayed in the Output tab. Set to False if only - # matching videos should be displayed there. Ignore if - # self.refresh_output_videos_flag is False - self.refresh_output_verbose_flag = False - # The moviepy module hangs indefinitely, if it is used to open a - # corrupted video file - # (see https://github.com/Zulko/moviepy/issues/639) - # To counter this, self.update_video_from_filesystem() moves the - # procedure into a thread, and applies a timeout to that thread - # The timeout (in seconds) to apply. Must be an integer, 0 or above. - # If 0, the moviepy procedure is allowed to hang indefinitely - self.refresh_moviepy_timeout = 10 - - # Paths to the post-processor binaries. If neither is set, we assume - # that FFmpeg and AVConv are in the user's path. If one is set to any - # value besides None, it is passed to youtube-dl. If both are set, - # then one of them is passed to youtube-dl: AVConv if the download - # option 'prefer_avconv' applies, FFmpeg if not - # None of these values are used on MS Windows - # Default path to the FFmpeg binary - self.default_ffmpeg_path = '/usr/bin/ffmpeg' - # Path to the FFmpeg binary - self.ffmpeg_path = None - # Default path to the AVConv binary - self.default_avconv_path = '/usr/local/bin/avconv' - # Path to the AVConv binary - self.avconv_path = None - # Flag set to True when a call to FFmpegManager.convert_webp() fails, - # indicating that FFmpeg is not installed on the user's system - # When True, the code will not attempt to convert any more .webp - # thumbnails to .jpg (until the user restarts Tartube) - self.ffmpeg_fail_flag = False - # The system error message to display when failure occurs (used - # several times, so defined here) - self.ffmpeg_fail_msg = _( - 'Failed to convert a thumbnail from .webp to .jpg. No more' \ - + ' conversions will be attempted until you install FFmpeg on' \ - + ' your system, or (if FFmpeg is already installed) you set the' \ - + ' correct FFmpeg path. To attempt more conversions, restart' \ - + ' Tartube. To stop these messages, disable thumbnail' \ - + ' conversions', - ) - # Flag set to True if Tartube should attempt to convert .webp - # thumbnails (from YouTube), which can't be displayed in the main - # window, into .jpg thumbnails, which can be displayed - # Ignored if self.ffmpeg_fail_flag is True - self.ffmpeg_convert_webp_flag = True - # Flag set to True if the original .webp thumbnails should be retained. - # If False, they are deleted (assuming a successful conversion) - # Ignored if self.ffmpeg_fail_flag is True or - # self.ffmpeg_convert_webp_flag is False - self.ffmpeg_retain_webp_flag = True - - # Mode for downloading broadcasting livestreams: - # 'default' - use the current downloader alone. This normally works - # with yt-dlp, probably not with youtube-dl - # 'default_m3u' - use the current downloader to fetch the .m3u - # manifest, then FFmpeg to download the livestream. Requires - # FFmpeg - # 'streamlink' - use streamlink (if installed on the user's system) - self.livestream_dl_mode = 'default_m3u' - # Settings for downloading broadcasting livestreams - # Timeout (in minutes) during livestream downloads (minimum value 1, - # fractional numbers are allowed) - self.livestream_dl_timeout = 3 - # If True, resume an earlier download. If False, replace the earlier - # download. When using streamlink, the earlier download is always - # replaced - self.livestream_replace_flag = True - # When a download is stopped (for example, when the user clicks the - # main window's 'Stop' button), mark the download as finished when - # this flag is True - self.livestream_stop_is_final_flag = True - # If True, check the media.Video object before downloading the - # livestream (which ensures the thumbnail and other metadata files - # are downloaded) - self.livestream_force_check_flag = True - - # Path to the streamlink binary. If not set, we assume that streamlink - # is in the user's path - self.streamlink_path = None - - # During a download operation, a GObject timer runs, so that the - # Progress tab and Output tab can be updated at regular intervals - # There is also a delay between the instant at which youtube-dl - # reports a video file has been downloaded, and the instant at which - # it appears in the filesystem. The timer checks for newly-existing - # files at regular intervals, too - # The timer's ID (None when no timer is running) - self.dl_timer_id = None - # The timer interval time (in milliseconds) - self.dl_timer_time = 500 - # At the end of the download operation, the timer continues running for - # a few seconds, to give new files a chance to appear in the - # filesystem. The maximum time to wait (in seconds) - self.dl_timer_final_time = 5 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.dl_timer_check_time = None - - # During a download operation, we periodically check whether the device - # containing self.data_dir is running out of space - # The check interval time (in seconds) - self.dl_timer_disk_space_time = 60 - # The time (matchs time.time()) at which the next check takes place - self.dl_timer_disk_space_check_time = None - - # Flag set to True if Tartube should warn if the system is running out - # of disk space (on the drive containing self.data_dir), False if - # not. The warning is issued at the start of a download operation - self.disk_space_warn_flag = True - # The amount of free disk space (in Gb) below which the warning is - # issued. If 0, no warning is issued. Ignored if - # self.disk_space_warn_flag is False - self.disk_space_warn_limit = 1 - # Flag set to True if Tartube should refuse to start a download - # operation, and halt an existing download operation, if the system - # is running out of disk space (on the drive containing - # self.data_dir), False if not - self.disk_space_stop_flag = True - # The amount of free disk space (in Gb) below which the refusal/halt - # is enacted. If 0, a download operation will continue downloading - # files until the device actually runs out of space. Ignored if - # self.disk_space_stop_flag is False - self.disk_space_stop_limit = 0.5 - # The IVs above can be set to any number (0 or above), but the - # Gtk.SpinButtons in the system preferences window increment/ - # decrement the value by this many Gb at a time - self.disk_space_increment = 0.1 - # An absolute minimum of disk space, below which a download operation - # will not start, or will halt, regardless of the values of the IVs - # above (in Gb) - self.disk_space_abs_limit = 0.05 - - # Default invidio.us mirror to use (the original site closed in - # September 2020); this value never changes - self.default_invidious_mirror = 'yewtu.be' - # Custom mirror to use (can be set by the user) - self.custom_invidious_mirror = self.default_invidious_mirror - - # Default SponsorBlock API mirror to use (this value never changes) - self.default_sblock_mirror = 'sponsor.ajay.app/api' - # Custom mirror to use (can be set by the user) - self.custom_sblock_mirror = self.default_sblock_mirror - - # Custom download operation settings - # A downloads.CustomDLManager object specifies settings to use during a - # custom download - # Each CustomDLManager has a unique .uid IV (unique only to this class - # of objects), and a non-unique name (in case the user wants to - # import settings, which might have a duplicate name) - # The number of downloads.CustomDLManager objects ever created - # (including any that have been deleted), used to generate the unique - # .uid - self.custom_dl_reg_count = 0 - # A dictionary containing all downloads.CustomDLManager objects (but - # not those which have been deleted) - # Dictionary in the form - # key = object's unique .uid - # value = the custom download manager object itself - self.custom_dl_reg_dict = {} - # The General Custom Download Manager, used as the default manager - self.general_custom_dl_obj = None - # The downloads.CustomDLManager object used in the Classic Mode tab. If - # None, then self.general_custom_dl_obj is used - self.classic_custom_dl_obj = None - - # List of proxies. If set, a download operation cycles between them. - # Does not apply to streamlink downloads - self.dl_proxy_list = [] - # At the start of a download operation, the contents of - # self.dl_proxy_list are copied into this list. Whenever a proxy is - # required, the first item in the list is used, and moved to the - # bottom of the list - self.dl_proxy_cycle_list = [] - - # During an update operation, a separate GObject timer runs, so that - # the Output tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.update_timer_id = None - # The timer interval time (in milliseconds) - self.update_timer_time = 500 - # At the end of the update operation, the timer continues running for - # a few seconds, to prevent various Gtk errors (and occasionally - # crashes) for systems with Gtk < 3.24. The maximum time to wait (in - # seconds) - self.update_timer_final_time = 3 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.update_timer_check_time = None - - # During a refresh operation, a separate GObject timer runs, so that - # the Output tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.refresh_timer_id = None - # The timer interval time (in milliseconds) - self.refresh_timer_time = 500 - # At the end of the refresh operation, the timer continues running for - # a few seconds, to prevent various Gtk errors (and occasionally - # crashes) for systems with Gtk < 3.24. The maximum time to wait (in - # seconds) - self.refresh_timer_final_time = 2 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.refresh_timer_check_time = None - - # During an info operation, a separate GObject timer runs, so that - # the Output tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.info_timer_id = None - # The timer interval time (in milliseconds) - self.info_timer_time = 500 - # At the end of the info operation, the timer continues running for - # a few seconds, to prevent various Gtk errors (and occasionally - # crashes) for systems with Gtk < 3.24. The maximum time to wait (in - # seconds) - # (Shorter wait time than other operations, because this type of - # operation finishes quickly) - self.info_timer_final_time = 2 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.info_timer_check_time = None - - # During a tidy operation, a separate GObject timer runs, so that - # the Output tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.tidy_timer_id = None - # The timer interval time (in milliseconds) - self.tidy_timer_time = 500 - # At the end of the tidy operation, the timer continues running for - # a few seconds, to prevent various Gtk errors (and occasionally - # crashes) for systems with Gtk < 3.24. The maximum time to wait (in - # seconds) - # (Shorter wait time than other operations, because this type of - # operation might finish quickly) - self.tidy_timer_final_time = 2 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.tidy_timer_check_time = None - - # During a process operation, a separate GObject timer runs, so that - # the Output tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.process_timer_id = None - # The timer interval time (in milliseconds) - self.process_timer_time = 500 - # At the end of most operations, the timer continues running for a few - # seconds, to prevent various Gtk errors. There are no such issues - # with a process operation, so the wait time is only 1 - self.process_timer_final_time = 1 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.process_timer_check_time = None - - # During any operation (except livestream operations), a flag set to - # True if the operation was halted by the user, rather than being - # allowed to complete naturally - self.operation_halted_flag = False - # During a download operation, a flag set to True if Tartube must shut - # down when the operation is finished - self.halt_after_operation_flag = False - # During a download operation, a flag set to True if no dialogue - # window must be shown at the end of that operation (but not - # necessarily any future download operations) - self.no_dialogue_this_time_flag = False - - # For a channel/playlist containing hundreds (or more!) videos, a - # download operation will take a very long time, even though we might - # only want to check for new videos - # Flag set to True if the download operation should give up checking a - # channel or playlist when its starts receiving details of videos - # about which it already knows (from a previous download operation) - # This works well if the website sends video in order, youngest first - # (as YouTube does), but won't work at all otherwise - self.operation_limit_flag = False - # During simulated video downloads (e.g. after clicking the 'Check all' - # button), stop checking the channel/playlist after receiving details - # for this many videos, when a media.Video object exists for them - # and the object's .file_name and .name IVs are set - # Must be an positive integer or 0. If 0, no limit applies. Ignored if - # self.operation_limit_flag is False - self.operation_check_limit = 3 - # During actual video downloads (e.g. after clicking the 'Download all' - # button), stop downloading the channel/playlist after receiving - # this many 'video already downloaded' messages, when a media.Video - # objects exists for them and the object's .dl_flag is set - # Must be an positive integer or 0. If 0, no limit applies. Ignored if - # self.operation_limit_flag is False - self.operation_download_limit = 3 - - # Flag set to True if the newbie dialogue should appear after a failed - # download operation, explaining what to do - self.show_newbie_dialogue_flag = True - # Flag set to True if a dialogue should appear when the user opens the - # MSYS2 terminal from the main menu (on MS Windows only; ignored on - # other systems) - self.show_msys2_dialogue_flag = True - # Flag set to True if a dialogue should appear when deleting video(s) - # from within the Video Catalogue's popup menu - self.show_delete_video_dialogue_flag = True - # Default setting for this dialogue: True to delete files, False to - # just remove the video(s) from the database - self.delete_video_files_flag = False - # Flag set to True if a dialogue should appear when deleting a channel, - # playlist or folder from withint the Video Index's popup menu - self.show_delete_container_dialogue_flag = True - # Default setting for this dialogue: True to delete files, False to - # just remove the container and its videos from the database - self.delete_container_files_flag = False - - # Media data classes are those specified in media.py. Those class - # objects are media.Video (for individual videos), media.Channel, - # media.Playlist and media.Folder (reprenting a sub-directory inside - # Tartube's data directory) - # Some media data objects have a list of children which are themselves - # media data objects. In that way, the user can organise their videos - # in convenient folders - # media.Folder objects can have any media data objects as their - # children (including other media.Folder objects). media.Channel and - # media.Playlist objects can have media.Video objects as their - # children. media.Video objects don't have any children - # - # Every media data object has a unique .dbid (which is an integer) - # Every media data object has a non-unique .name (which is a string) - # - # media.Video objects may use any value as their .name, subject to the - # following restrictions: - # 1. The name must not be longer than the filesystem's maximum file - # length - # 2. On MS Windows, some filenames are illegal (see - # self.illegal_name_mswin_list below) - # - # media.Channel, media.Playlist and media.Folder objects may use any - # value as their .name, subject to the following restrictions: - # 1. The name must not be longer than self.container_name_max_len - # (set below) - # 2. On MS Windows, some filenames are illegal (see - # self.illegal_name_mswin_list below) - # 3. The name "downloads" (all lower-case) is not allowed. (This is - # to prevent problems caused by Tartube's old database directoy - # structure, which had a sub-directory called "downloads". A - # couple of other names are not allowed (see - # self.illegal_name_regex_list below) - # 4. A media.Folder may not contain more than one child channel/ - # playlist/folder with the same name. Likewise, the top-level - # container list may not contain channels/playlist/folders with - # the same name. Otherwise, duplicate containers are allowed - # (for example, two channels both called "Test", one inside a - # folder called "Folder 1", the other inside a folder called - # "Folder 2" - # - # The number of media data objects ever created (including any that - # have been deleted), used to give new media data objects their .dbid - self.media_reg_count = 0 - # A dictionary containing all media data objects (but not those which - # have been deleted) - # Dictionary in the form - # key = media data object's unique .dbid - # value = the media data object itself - self.media_reg_dict = {} - # A subset of the media data registry, containing only media.Channel, - # media.Playlist and media.Folder objects - # Dictionary in the form - # key = media data object's unique .dbid - # value = the media data object itself - self.container_reg_dict = {} - # For backwards compatibility (versions before v2.4.117), a temporary - # copy of the old container dictionary in its old format - # It is populated when the database is loaded, then the data is - # converted to the new format and moved to - # self.container_reg_dict during the call to self.update_db() - # Dictionary in the form - # key = media data object's .name - # value = media data object's unique .dbid - self.old_container_reg_dict = {} - # media.Channel, media.Playlist and media.Folder objects can have an - # external directory set (i.e. videos are downloaded outside of - # Tartube's data directory) - # When the database is loaded, we check the semaphore file in each - # external directory. Any from which we can't write or read - # (indicating that the location is not available on the user's - # filesystem) are added to this dictionary. Entries can be removed - # from the dictionary when the channel/playlist/folder's external - # directory is modified (or reset), or otherwise when a new database - # is loaded - # Channels/playlists/folders added to this dictionary cannot be - # checked/downloaded/custom downloaded - # A subset of the media data registry, containing only media.Channel, - # media.Playlist and media.Folder objects - # Dictionary in the form - # key = media data object's unique .dbid - # value = the media data object itself - self.container_unavailable_dict = {} - # An ordered list of media.Channel, media.Playlist and media.Folder - # objects which have no parents (in the order they're displayed) - # This list, combined with each media data object's child list, is - # used to construct a family tree. A typical family tree looks - # something like this: - # Folder - # Channel - # Video - # Video - # Channel - # Video - # Video - # Folder - # Folder - # Playlist - # Video - # Video - # Folder - # Playlist - # Video - # Video - # Folder - # Video - # Video - # A list of .dbid IVs for all top-level media.Channel, media.Playlist - # and media.Folder objects - self.container_top_level_list = [] - # The maximum depth of the media registry. The diagram above shows - # channels on the 2nd level and playlists on the third level. - # Container objects cannot be added beyond the following level - self.container_max_level = 8 - # The maximum length of channel, playlist and folder names (does not - # apply to video names) - self.container_name_max_len = 64 - # Standard name for a media.Video object, when the actual name of the - # video is not yet known - self.default_video_name = '(video with no name)' - # Forbidden names for channels, playlists and folders. This is to - # prevent the user overwriting directories in self.data_dir, that - # Tartube uses for its own purposes, and to prevent the user fooling - # Tartube into thinking that the old file structure is being used - # Every item in this list is a regex; a name for a channel, playlist - # or folder must not match any item in the list. (media.Video - # objects can still have any name) - self.illegal_name_regex_list = [ - r'^\.', - r'^downloads$', - __main__.__packagename__, - ] - # Extended list of forbidden names for channels, playlists and folders - # (on MS Windows). Each item is still illegal if followed by a file - # extension, e.g. 'LPT1.txt' - self.illegal_name_mswin_list = [ - 'CON', 'PRN', 'AUX', 'NUL', - 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', - 'COM9', - 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', - 'LPT9', - ] - # Temporary dictionary of channel/playlist names extracted from the - # child video's metadata. The user can use this dictionary to - # update channel/playlists names (for example, if they have been - # given a generic name like 'channel_1') - # The dictionary is reset and then updated during a download operation - # (real or simulated). A key-value pair for a channel/playlist is - # added if: - # - At least one video was checked or downloaded, and its - # metadata was extracted - # - The channel/playlist name, according to the metadata, is - # different from the name of the media.Channel/media.Playlist - # object - # Note that once a key-value pair is added for a channel/playlist, it - # is not updated again during a download operation (so if several - # child videos supply different channel/playlist names, only the - # first one is used) - # Dictionary in the form - # key = media data object's .dbid - # value = the channel/playlist name extracted from the child video - # metadata - self.media_reset_container_dict = {} - - # A subset of self.media_reg_dict, containing only media.Videos which - # are marked as livestreams (and which must therefore be checked by - # livestream operations) - self.media_reg_live_dict = {} - # A subset of self.media_reg_live_dict, containing only media.Videos - # which are waiting live streams. When the livestream goes live, a - # desktop notification is shown for them - self.media_reg_auto_notify_dict = {} - # A subset of self.media_reg_live_dict, containing only media.Videos - # which are waiting live streams. When the livestream goes live, an - # alarm is sounded for them - self.media_reg_auto_alarm_dict = {} - # A subset of self.media_reg_live_dict, containing only media.Videos - # which are waiting live streams. When the livestream goes live, the - # video is opened in the system's web browser - self.media_reg_auto_open_dict = {} - # A subset of self.media_reg_live_dict, containing only media.Videos - # which should be downloaded, as soon as they start (as soon as this - # is processed, the entry is removed from the dictionary) - self.media_reg_auto_dl_start_dict = {} - # A subset of self.media_reg_live_dict, containing only media.Videos - # which should be downloaded, as soon as they stop (as soon as this - # is processed, the entry is removed from the dictionary) - self.media_reg_auto_dl_stop_dict = {} - - # Subsets of self.media_reg_dict, containing only media.Videos whose - # .live_mode has recently changed. These IVs are not stored in - # Tartube's database - # Entries are added temporarily until self.download_manager_finished() - # or self.livestream_manager_finished() have a chance to act on them - # (for example, by playing an alarm for for a livestream that has - # just gone live) - # Dictionary of videos which have just started broadcasting - # (.live_mode was 0/1, now 2) - self.media_reg_live_started_dict = {} - # Dictionary of videos which have stopped broadcasting (.live_mode was - # 2, now 0) - self.media_reg_live_stopped_dict = {} - # Dictionary of livestreams which were broadcasting (.live_mode was - # 2), but for which downloads.StreamManager cannot obtain JSON data - self.media_reg_live_vanished_dict = {} - - # Some media data objects are fixed (i.e. are created when Tartube - # first starts, and cannot be deleted by the user). Shortcuts to - # those objects - # Private folder containing all videos (users cannot add anything to a - # private folder, because it's used by Tartube for special purposes) - self.fixed_all_folder = None - # Private folder containing only bookmarked videos - self.fixed_bookmark_folder = None - # Private folder containing only favourite videos - self.fixed_fav_folder = None - # Private folder containing only videos marked as (waiting or - # broadcasting) livestreams - self.fixed_live_folder = None - # Private folder containing only videos that have been removed from - # a channel/playlist (by the creator); only updated when - # self.track_missing_videos_flag is enabled - self.fixed_missing_folder = None - # Private folder containing only new videos - self.fixed_new_folder = None - # Private folder containing only videos from the most recent download - # operation. The folder is emptied at the start of every new - # download operation - self.fixed_recent_folder = None - # Private folder containing only playlist videos (when the user - # watches one, online or locally, the video is removed from the - # playlist) - self.fixed_waiting_folder = None - # Public folder that's used as the second one in the 'Add video' - # dialogue window, in which the user can store any individual videos - # that are automatically deleted when Tartube shuts down - self.fixed_temp_folder = None - # Public folder that's used as the first one in the 'Add video' - # dialogue window, in which the user can store any individual videos - self.fixed_misc_folder = None - # Public folder that's used for storing video clips (optionally) - self.fixed_clips_folder = None - - # The locale for which the fixed folders are named. When the database - # file is loaded, if this value no longer matches - # self.current_locale, then the folder names are all updated for the - # new locale - self.fixed_folder_locale = self.current_locale - # For the Recent Videos folder (self.fixed_recent_folder), the time - # (in days) after which videos should be removed. If 0, videos are - # removed at the start of every download operation - self.fixed_recent_folder_days = 0 - - # A list of media.Video objects the user wants to watch, as soon as - # they have been downloaded. Videos are added by a call to - # self.download_watch_videos(), and removed by a call to - # self.announce_video_download(), etc - self.watch_after_dl_list = [] - - # The edit/preference windows can draw graphs. The format of the graphs - # are specified by five standard comboboxes (specified in - # config.GenericConfigWin.add_combos_for_graphs() ), and those - # specifications are remembered between windows - # The data type ('receive' for download times, 'upload' for upload - # times, 'size' for file size, 'duration' for video duration) - self.graph_data_type = 'receive' - # The type of graph to plot ('graph' for a line plot graph, or 'chart' - # for a bar chart) - self.graph_plot_type = 'graph' - # The period of time used as the span of the x-axis (in seconds, e.g. - # 31536000 is the equivalent of a year) - self.graph_time_period_secs = 60*60*24*365 - # The time unit to use (in seconds, e.g. 604800 is the equivalent of a - # week). We count the number of videos for the time unit and use it - # as a single point on the x-axis - self.graph_time_unit_secs = 60*60*24*30 - # The colour to use ('red', 'green', 'blue', 'black', white') - self.graph_ink_colour = 'blue' - - # Scheduled downloads - # The user can create as many scheduled downloads as they want (this is - # a change from earlier versions, in which only one of each type of - # scheduled download could be created) - # Each scheduled download is represented by a media.Scheduled object. - # The objects are stored in the database file - # A list of media.Scheduled objects. When deciding whether to start a - # scheduled download, the objects are checked in this order - self.scheduled_list = [] - - # Flag set to True if a reduced set of preferences should be shown in - # the system preferences window (for inexperienced users) - self.simple_prefs_flag = True - # Flag set to True if a reduced set of options should be shown in the - # download options edit window (for inexperienced users) - self.simple_options_flag = True - # Flag set to True if the edit window should use a GUI layout adapted - # from FFmpeg command line wizard by AndreKR. If False, a minimal - # layout is used, and the user must specify most of the FFmpeg - # command line options manually - self.simple_ffmpeg_options_flag = True - - # Profiles - # Items in the Video Index can be marked (using their checkboxes). When - # at least one item is marked, the 'Check all' and 'Download all' - # buttons become 'Check marked items' and 'Download marked items' - # A profile is a list of .dbids for marked items, so the user can - # switch between them - # Dictionary in the form - # key = unique name for the profile - # value = list of .dbid values for media.Channel, media.Playlist and - # media.Folder items - self.profile_dict = {} - # The profile which was most recently created, or to which the user - # most recently switched. Reset when that profile is deleted - self.last_profile = None - # Flag set to True if Tartube should automatically switch to that - # profile, when the database is loaded - self.auto_switch_profile_flag = False - # Maximum number of profiles (a constant value) - self.profile_max = 16 - - # Download Options Managers - # During a download operation, youtube-dl is supplied with a set of - # download options. Those options are specified by an - # options.OptionsManager object - # Each media data object may have an options.OptionsManager object - # applied to it directly. If not, it uses the options.OptionsManager - # object of its parent (or of its parent's parent, and so on) - # If this chain of family relationships doesn't provide an - # options.OptionsManager object, then a default object, known as the - # General Options Manager, is used - # Every options.OptionsManager object has a unique .uid IV, and a non- - # unique name (because, for example, a video might have the same - # name as a channel; it's up to the user to avoid duplicate names) - # The number of options.OptionsManager objects ever created (including - # any that have been deleted), used to generate the unique .uid - self.options_reg_count = 0 - # A dictionary containing all options.OptionsManager objects (but not - # those which have been deleted) - # Dictionary in the form - # key = object's unique .uid - # value = the options manager object itself - self.options_reg_dict = {} - # The general (default) options.OptionsManager object described above - self.general_options_obj = None - # The options.OptionsManager object used in the Classic Mode tab. If - # None, then self.general_options_obj is used - self.classic_options_obj = None - # An ordered list of options.OptionsManager .uids, used in the - # Drag and Drop tab. The list does not have to contain every (or - # even any) items, but there must be no duplicates - # Maximum size is 16 (any more items are ignored) - self.classic_dropzone_list = [] - # Flag set to True if the General Options Manager - # (self.general_options_obj) should be cloned whenever the user - # applies a new options manager to a media data object (e.g. by - # right-clicking a channel in the Video Index, and selecting - # Downloads > Apply options manager) - self.auto_clone_options_flag = True - # Flag set to True if options applied to a media.Video object should be - # deleted, once the video has been downloaded - self.auto_delete_options_flag = True - - # FFmpeg options manager - # We can pass a list of video(s) directly to the process operation, - # using a custom set of FFmpeg command line options - # Each individual set is stored in an - # ffmpeg_tartube.FFmpegOptionsManager object - # Each options manager has a unique .uid IV (unique only to this class - # of objects; the number range is not shared with - # options.OptionsManager objects), and a non-unique name (in case the - # user wants to import a set of options, which might have a duplicate - # name) - # The number of ffmpeg_tartube.FFmpegOptionsManager objects ever - # created (including any that have been deleted), used to generate - # the unique .uid - self.ffmpeg_reg_count = 0 - # A dictionary containing all ffmpeg_tartube.FFmpegOptionsManager - # objects (but not those which have been deleted) - # Dictionary in the form - # key = object's unique .uid - # value = the FFmpeg options manager object itself - self.ffmpeg_reg_dict = {} - # Of all the objects, only one is in use at any time. The current - # FFmpeg options manager - self.ffmpeg_options_obj = None - - # Flag set to True if checking/downloading livestreams should be - # blocked by yt-dlp (does not work with other downloaders) - self.block_livestreams_flag = False - # Flag set to True if Tartube should try to detect livestreams (on - # compatible websites only) - # This feature is only tested on YouTube. It might work on other - # websites, if the user has set the RSS feed for each channel/ - # playlist individually - # If enabled, the download operation checks a channel/playlist RSS for - # videos that weren't picked up by ytdl, and marks them as - # livestreams. If JSON data can't be downloaded from it, assume it's - # an upcoming livestream; otherwise assume the livestream is live - self.enable_livestreams_flag = True - # If enabled, Tartube will assume that the website lists videos in - # order of announcement time, and will stop checking the RSS feed - # when it finds videos which are at least this old (in days). If set - # to zero, Tartube stops checking the RSS feed when it finds the - # first non-livestream video - self.livestream_max_days = 7 - # Flag set to True if livestream videos in the Video Catalogue should - # be drawn with a coloured background, False if not - self.livestream_use_colour_flag = True - # Flag set to True if background colours should be the same for debut - # and livestream videos, False if four separate background colours - # should be used (ignored if self.livestream_use_colour_flag os - # False) - self.livestream_simple_colour_flag = False - # Flag set to True if a desktop notification should be shown when a - # waiting livestream goes live (the setting can then be enabled/ - # disabled for each video individually in the Video Catalogue) - self.livestream_auto_notify_flag = False - # Flag set to True if a Tartube should play an alarm when a waiting - # livestream goes live (the setting can then be enabled/disabled for - # each video individually in the Video Catalogue) - self.livestream_auto_alarm_flag = False - # Flag set to True if a video should be opened in the system's web - # browser when it goes live (the setting can then be enabled/ - # disabled for each video individually in the Video Catalogue) - self.livestream_auto_open_flag = False - # Flag set to True if a video should be downloaded as soon as the - # livestream starts (media.Video.live_mode was 0/1, set to 2; the - # setting can then be enabled/disabled for each video individually in - # the Video Catalogue) - # The start of the download may be delayed if a download operation is - # already in progress - self.livestream_auto_dl_start_flag = False - # Flag set to True if a video should be downloaded as soon as the - # livestream stops (media.Video.live_mode was 2, set to 0; the - # setting can then be enabled/disabled for each video individually in - # the Video Catalogue) - # The start of the download may be delayed if a download operation is - # already in progress - # If both this flag and self.livestream_auto_dl_start_flag are set to - # True, then youtube-dl is instructed to overwrite the earlier file - # (NB As of April 2020, this is still not possible; as a temporary - # measure, the earlier file is renamed instead) - self.livestream_auto_dl_stop_flag = False - # The livestream operation can run periodically and checks the - # status of videos marked as livestreams - # Flag set to True if the livestream task should run periodically - self.scheduled_livestream_flag = True - # The time (in minutes) between scheduled livestream operations, if - # enabled (cannot be fractional, minimum value 1) - self.scheduled_livestream_wait_mins = 3 - # The time (system time, in seconds) at which the last livestream - # operation started - self.scheduled_livestream_last_time = 0 - # Flag set to True if livestream operations should be performed at - # least every minute, when any livestream is due to start - self.scheduled_livestream_extra_flag = True - - # Flag set to True if a download operation should auto-stop after a - # certain period of time (applies to both real and simulated - # downloads) - self.autostop_time_flag = False - # Auto-stop after this amount of time (minimum value 1)... - self.autostop_time_value = 1 - # ...in this many units (any of the values in - # formats.TIME_METRIC_LIST) - self.autostop_time_unit = 'hours' - # Flag set to True if a download operation should auto-stop after a - # certain number of videos (applies to both real and simulated - # downloads) - self.autostop_videos_flag = False - # Auto-stop after this many videos (minimum value 1) - self.autostop_videos_value = 100 - # Flag set to True if a download operation should auto-stop after - # downloading videos of a certain combined size (applies to real - # downloads only; the specified size is approximate, because it - # relies on the video size reported by youtube-dl, and doesn't take - # account of thumbnails, JSON data, and so on) - self.autostop_size_flag = False - # Auto-stop after this amount of diskspace (minimum value 1)... - self.autostop_size_value = 1 - # ...in this many units (any of the values in - # formats.FILESIZE_METRIC_LIST) - self.autostop_size_unit = 'GiB' - - # Flag set to True if an update operation should be automatically - # started before the beginning of every download operation - self.operation_auto_update_flag = False - # When that flag is True, the following IVs are set by the initial - # call to self.download_manager_start(), reminding - # self.update_manager_finished() to start a download operation, and - # supplying it with the arguments from the original call to - # self.download_manager_start() - self.operation_waiting_flag = False - self.operation_waiting_type = None - self.operation_waiting_list = [] - self.operation_waiting_obj = None - # Flag set to True if files should be saved at the end of every - # operation - self.operation_save_flag = True - # Flag set to True if, during download operations using simulated - # downloads, videos whose parent is a media.Folder (i.e. videos not - # in channels/playlists) should not be added to the downlist list, - # unless (1) the location of the video file is not set and no - # thumbnail has been downloaded, or (2) the video is passed directly - # to the download operation (for example, by right-clicking a video - # and selecting 'Check Video' in the popup menu). If False, those - # videos are always added to the download list - # (This does not affect real downloads, in which such videos are never - # added to the download list) - self.operation_sim_shortcut_flag = True - - # Flag set to True if, during download operations (of all kinds), if a - # job stalls, it should be restarted - self.operation_auto_restart_flag = False - # When youtube-dl reports network problems, how many minutes should - # Tartube wait before restarting the job (minimum value is 1, - # ignore if self.operation_auto_restart_flag is not set) - self.operation_auto_restart_time = 2 - # The maximum number of times to restart a stalled job. If 0, no - # maximum applies. Ignored ignored if - # self.operation_sim_shortcut_flag is not set) - self.operation_auto_restart_max = 5 - - # How to notify the user at the end of each download/update/refresh - # operation: 'dialogue' to use a dialogue window, 'desktop' to use a - # desktop notification, or 'default' to do neither - # NB Desktop notifications don't work on MS Windows - self.operation_dialogue_mode = 'dialogue' - # What to do when the user creates a media.Video object whose URL - # represents a channel or playlist - # 'channel' to create a new media.Channel object, and place all the - # downloaded videos inside it (the original media.Video object is - # destroyed) - # 'playlist' to create a new media.Playlist object, and place all the - # downloaded videos inside it (the original media.Video object is - # destroyed) - # 'multi' to create a new media.Video object for each downloaded video, - # placed in the same folder as the original media.Video object (the - # original is destroyed) - # 'disable' to download nothing from the URL - # There are some restrictions. If the original media.Video object is - # contained in a folder whose .restrict_mode is not 'open', and if - # the mode is 'channel' or 'playlist', then the new channel/playlist - # is not created in that folder. If the original media.Video object - # is contained in a channel or playlist, all modes to default to - # 'disable' - # For downloads launched from the Classic Mode tab, none of this - # applies. Tartube downloads all videos associated with the URLs, - # and doesn't much care if the URLs represent channels and playlists - # or not - self.operation_convert_mode = 'channel' - # Flag set to True if self.update_video_from_filesystem() should get - # the video duration, if not already known, using the moviepy.editor - # module (an optional dependency) - self.use_module_moviepy_flag = True - - # Flag set to True if dialogue windows for adding videos, channels and - # playlists should copy the contents of the system clipboard - self.dialogue_copy_clipboard_flag = True - # Flag set to True if dialogue windows for adding channels and - # playlists should continually re-open, whenever the use clicks the - # OK button (so multiple channels etc can be added quickly) - self.dialogue_keep_open_flag = False - # Flag set to True is, when adding a YouTube channel and the URL - # doesn't end with .../videos, the user should be prompted to add it - self.dialogue_yt_remind_flag = True - - # On Virtualbox MSWin installations, dialogue windows can freeze - # Tartube, forcing a restart. Flag set to True if message dialogue - # windows (only) should be disabled, their messages being displayed - # in the terminal instead - self.dialogue_disable_msg_flag = False - - # Standard name for the youtube-dl archive file (cannot be changed by - # the user) - self.ytdl_archive_name = 'ytdl-archive.txt' - # Flag set to True if, when downloading videos, youtube-dl should be - # passed the download option '--download-archive', creating an - # archive file - # If the file exists, youtube-dl won't re-download a video a user has - # deleted - # Ignored for system folders like 'Unsorted Videos' - self.allow_ytdl_archive_flag = True - # The location of the archive file: 'default' to place it in the same - # directory as the video, 'top' to place it in self.data_dir, or - # 'custom' - self.allow_ytdl_archive_mode = 'default' - # When 'custom', the full path to the directory in which the archive - # file is stored (if no path set, we behave as if - # self.allow_ytdl_archive_mode was 'default') - self.allow_ytdl_archive_path = None - # Flag set to True if an archive file should be created when - # downloading from the Classic Mode tab (this is marked 'not - # recommended' in the edit window) - self.classic_ytdl_archive_flag = False - - # Flag set to True if, when checking videos/channels/playlists, we - # should apply a timeout (in case youtube-dl gets stuck downloading - # the JSON data) - self.apply_json_timeout_flag = True - # The length of the timeouts to apply, in minutes, when not fetching - # comments (self.check_comment_fetch_flag and - # self.dl_comment_fetch_flag are both False) - self.json_timeout_no_comments_time = 2 - # The length of the timeouts to apply, in minutes, when fetching - # comments (self.check_comment_fetch_flag and/or - # self.dl_comment_fetch_flag are True) - self.json_timeout_with_comments_time = 5 - # Flag set to True if, when checking/downloading channels/playlists, - # we should look out for previously-downloaded videos (that the - # creator has since removed from their channel/playlist), and add - # them to the system 'Missing videos' folder - self.track_missing_videos_flag = False - # Flag set to True if a time limit should be placed on missing videos. - # Ignored if self.track_missing_videos_flag is False - self.track_missing_time_flag = False - # The time limit (in days) to apply. If videos will only be marked as - # missing if uploaded within this many days. If set to 0, no videos - # are marked as missing. Ignored if self.track_missing_videos_flag or - # self.track_missing_time_flag is False - self.track_missing_time_days = 14 - # Flag set to True if, during a real (not simulated) download, - # youtube-dl error/warning messages without a video ID (which is, at - # the time of writing, most of them) should be assigned to the most - # probable media.Video object; False if anonymous messages should be - # assigned to the parent channel/playlist/folder instead - # (Assigning anonymous messages to videos is not an exact science, but - # should work well enough for most users) - self.auto_assign_errors_warnings_flag = True - # Flag set to True if, during a download operation, videos marked as - # being censored, age-restricted or otherwise unavailable for - # download should be added to the database - self.add_blocked_videos_flag = True - # Flag set to True if Tartube should retrieve the playlist ID from each - # checked/downloaded video's metadata, and store it in the parent - # channel/playlist - # (For 'enhanced' websites specified by formats.ENHANCED_SITE_DICT, - # the user can use the collected IDs to get a list of playlists - # associated with a channel) - self.store_playlist_id_flag = True - - # Flag set to True if a list of timestamps should be extracted from a - # video's .info.json file, when it is received - self.video_timestamps_extract_json_flag = False - # Flag set to True if a list of timestamps should be extracted from a - # video's description file, when it is received - self.video_timestamps_extract_descrip_flag = False - # Flag set to True if the previous set of timestamps should be - # replaced, when a video is checked/downloaded - self.video_timestamps_replace_flag = False - # Flag set to True if, just before trying to split a video based on its - # timestamps, downloads.ClipDownloader and process.ProcessManager - # should try to re-extract the timestamps from the metadata or - # description files (if none have been extracted so far) - self.video_timestamps_re_extract_flag = False - # Timestamp download mode - 'downloader' to use yt-dlp's - # --download-sections option, 'ffmpeg' to download using FFmpeg - self.video_timestamps_dl_mode = 'ffmpeg' - # When splitting videos, the format for the name of the video clips: - # num Number - # clip Clip Title - # num_clip Number + Clip Title - # clip_num Clip Title + Number - # orig Original Title - # orig_num Original Title + Number - # orig_clip Original Title + Clip Title - # orig_num_clip Original Title + Number + Clip Title - # orig_clip_num Original Title + Clip Title + Number - self.split_video_name_mode = 'orig_num_clip' - # Flag set to True if the video clips should be moved to the 'Video - # Clips' system folder, False if they should be saved in the same - # place as the original video - self.split_video_clips_dir_flag = True - # Flag set to True if the video clips should be moved into a - # sub-directory, with the same name as the original video. The - # subdirectory is either within the 'Video Clips' system folder, or - # within the original video's parent folder, depending on the value - # of self.split_video_clips_dir_flag - self.split_video_subdir_flag = False - # Flag set to True if the video clips should be added to Tartube's - # database, if possible (for example, a media.Folder cannot be - # created within a media.Channel. When self.split_video_subdir_flag - # is True and it's not possible to create a media.Folder, then a new - # sub-directory is created in the filesystem anyway, and the video - # clips are moved there) - self.split_video_add_db_flag = True - # Flag set to True if the each video clip should be given its own - # thumbnail, a copy of the original video's thumbnail - self.split_video_copy_thumb_flag = True - # Clip titles used if the user/video description does not specify one - # Generic title (cannot be changed by the user) - self.split_video_generic_title = 'Video' - # Custom title (can be changed by the user. If an empty string, the - # generic title is used) - self.split_video_custom_title = 'Video' - # Flag set to True to force keyframes at cuts (slower, but results in - # few artefacts around cuts) - self.split_video_force_keyframe_flag = True - # Flag set to True if the destination directory should be opened on - # the desktop, after splitting files - self.split_video_auto_open_flag = False - # Flag set to True if the original video should be deleted after - # splitting files. Does not apply to a video in a media.Channel or - # media.Playlist - self.split_video_auto_delete_flag = False - - # Temporary timestamp buffer - # Entries are added by PrepareClipDialogue, and removed by code in - # downloads.py or process.py as soon as the operation starts. (As a - # safeguard, the buffer is completely emptied at the end of a - # download/process operation) - # Dictionary in the form - # temp_stamp_buffer_dict[dbid] = list - # ...where 'dbid' is the .dbid of a media.Video, and 'list' is in the - # form: - # [download_type, stamp_list, stamp_list...] - # ...where 'stamp_list' uses the same group-of-3 format as - # media.Video.stamp_list, and 'download_type' is one of the following - # values: - # 'chapters': Use yt-dlp's --split-chapters option. When set to - # this value, any 'stamp_list' values are ignored - # 'downloader': Use yt-dlp's '--download-sections' options - # 'ffmpeg': Use FFmpeg - # 'create': Create clips from a video that's already been - # downloaded, using FFmpeg - self.temp_stamp_buffer_dict = {} - - # Flag set to True if downloads.VideoDownloader should contact the - # SponsorBlock server, when checking/downloading videos - self.sblock_fetch_flag = False - # Flag set to True if we should obfuscate the video's ID, when - # contacting the server (recommended for privacy) - self.sblock_obfuscate_flag = True - # Flag set to True if the previous set of video slices should be - # replaced, when a video is checked/downloaded - self.sblock_replace_flag = False - # Flag set to True if, just before trying to remove slices from a - # video, downloads.ClipDownloader and process.ProcessManager should - # contact SponsorBlock again to update the video slice list (if it is - # currently empty) - self.sblock_re_extract_flag = False - # Flag set to True to force keyframes at cuts (slower, but results in - # few artefacts around cuts) - self.slice_video_force_keyframe_flag = True - # Flag set to True if timestamps/slices should be removed from a video, - # after it is sliced (since they are then incorrect) - self.slice_video_cleanup_flag = True - - # Temporary video slice buffer - # Entries are added by PrepareSliceDialogue, and removed by code in - # downloads.py or process.py as soon as the operation starts. (As a - # safeguard, the buffer is completely emptied at the end of a - # download/process operation) - # Dictionary in the form - # temp_slice_buffer_dict[dbid] = list - # ...where 'dbid' is the .dbid of a media.Video, and 'list' is in the - # form: - # [download_type, slice_dict, slice_dict...] - # ...where 'slice_dict' uses the same form as items in - # media.Video.slice_list, and 'download_type' is one of the following - # values: - # 'default': Use FFmpeg to download clips, the concatenate them - # together into a new video file - # 'create': Remove slices from a video that's already been - # downloaded, again using FFmpeg - self.temp_slice_buffer_dict = {} - - # Temporary output template override buffer - # Set by a call from - # mainwin.MainWin.on_video_catalogue_output_override() - # During the subsequent download of the media.Video, youtube-dl's - # output template is overriden with the name stored in this buffer - # Dictionary in the form - # temp_output_override_dict[dbid] = name - # ...where 'dbid' is the .dbid of a media.Video, and 'name' is any - # acceptable filename for the operating system - self.temp_output_override_dict = {} - - # Flag set to True if download operations with yt-dlp should add the - # '--write-comments' option, downloading comments to the .info.json - # file - # Flag used in simulated downloads (checking videos) - self.check_comment_fetch_flag = False - # Flag used in real downloads - self.dl_comment_fetch_flag = False - # Flag set to True if the comments should also be stored in the Tartube - # database, in each media.Vidoe object - self.comment_store_flag = False - # Flag set to False if the 'timestamp' field should be visible in the - # config.VideoEditWin; True if the 'time' field should be visible - # (as of v2.3.318, all comments in a YouTube video share the same - # timestamp) - self.comment_show_text_time_flag = True - # Flag set to False if a flat list of comments should be visible in the - # config.VideoEditWin; True if a formatted list should be visible - self.comment_show_formatted_flag = True - - # Flag set to True if 'Child process exited with non-zero code' - # messages, generated by Tartube, should be ignored (in the - # Errors/Warnings tab) - self.ignore_child_process_exit_flag = True - # Flag set to True if 'unable to download video data: HTTP Error 404' - # and 'Unable to extract video data' messages from youtube-dl should - # be ignored (in the Errors/Warnings tab) - self.ignore_http_404_error_flag = False - # Flag set to True if 'Did not get any data blocks' messages from - # youtube-dl should be ignored (in the Errors/Warnings tab) - self.ignore_data_block_error_flag = False - # Flag set to True if 'Requested formats are incompatible for merge and - # will be merged into mkv' messages from youtube-dl should be ignored - # (in the Errors/Warnings tab) - self.ignore_merge_warning_flag = False - # Flag set to True if 'No video formats found; please report this - # issue on...' messages from youtube-dl should be ignored (in the - # Errors/Warnings tab) - self.ignore_missing_format_error_flag = False - # Flag set to True if 'There are no annotations to write' messages - # should be ignored (in the Errors/Warnings tab) - self.ignore_no_annotations_flag = True - # Flag set to True if 'video doesn't have subtitles' errors should be - # ignored (in the Errors/Warnings tab) - self.ignore_no_subtitles_flag = True - # Flag set to True if 'A channel/user page was given' warnings should - # be ignored (in the Errors/Warnings tab) - self.ignore_page_given_flag = False - # Flag set to True if 'There's no playlist description to write' - # warnings should be ignored (in the Errors/Warnings tab) - self.ignore_no_descrip_flag = False - # Flag set to True if 'Unable to download video thumbnail [N]: HTTP - # Error 404: Not Found' warnings should be ignored (in the Errors/ - # Warnings tab) - self.ignore_thumb_404_flag = True - - # Flag set to True if YouTube copyright messages should be ignored (in - # the Errors/Warnings tab) - self.ignore_yt_copyright_flag = False - # Flag set to True if YouTube age-restriction messages should be - # ignored (in the Errors/Warnings tab) - self.ignore_yt_age_restrict_flag = False - # Flag set to True if 'The uploader has not made this video available' - # messages should be ignored (in the Errors/Warnings tab) - self.ignore_yt_uploader_deleted_flag = False - # Flag set to True if 'This video requires payment to watch' errors - # should be ignored (in the Errors/Warnings tab) - self.ignore_yt_payment_flag = False - - # Websites other than YouTube typically use different error messages - # A custom list of strings or regexes, which are matched against error - # messages. Any matching error messages are not displayed in the - # Errors/Warnings tab. The user can add - self.ignore_custom_msg_list = [] - # Flag set to True if the contents of the list are regexes, False if - # they are ordinary strings - self.ignore_custom_regex_flag = False - - # During a download operation, the number of simultaneous downloads - # allowed. (An instruction to youtube-dl to download video(s) from a - # single URL is called a download job) - # NB Because Tartube just passes a set of instructions to youtube-dl - # and then waits for the results, an increase in this number is - # applied to a download operation immediately, but a decrease is not - # applied until one of the download jobs has finished - self.num_worker_default = 2 - # (Absolute minimum and maximum values) - self.num_worker_max = 32 - self.num_worker_min = 1 - # Flag set to True when the limit is actually applied, False when not - self.num_worker_apply_flag = True - # Flag set to True if the maximum simultaneous downloads limit should - # be bypassed, if a broadcasting livestream is to be downloaded - # (For example, the maximum is two, but three livestreams are - # broadcasting; in that case, we allow the creation of an extra - # downloads.DownloadWorker to handle it. That worker is only used - # for broadcasting livestreams, and otherwise stands idle) - self.num_worker_bypass_flag = True - - # During a download operation, the bandwith limit (in KiB/s) - # NB Because Tartube just passes a set of instructions to youtube-dl, - # any change in this value is not applied until one of the download - # jobs has finished - self.bandwidth_default = 500 - # (Absolute minimum and maximum values) - self.bandwidth_max = 10000 - self.bandwidth_min = 1 - # Flag set to True when the limit is currently applied, False when not - self.bandwidth_apply_flag = False - - # During a download operation, the maximum video resolution to - # download. Must be one of the keys in formats.VIDEO_RESOLUTION_DICT - # (e.g. '720p') - self.video_res_default = '720p' - # Flag set to True when this maximum video resolution is applied. When - # applied, it overrides the download option 'video_format_list' (see - # the comments in options.OptionsManager) - self.video_res_apply_flag = False - - # Alternative performance limits (applied at certain times of the - # day/week) - # Note that these limits, if applied, apply to the whole video/ - # channel/playlist, at the moment the video/channel/playlist download - # starts. Tartube cannot magically change youtube-dl's internal - # settings once the download has started - self.alt_num_worker = 4 - self.alt_num_worker_apply_flag = False - self.alt_bandwidth = 1000 - self.alt_bandwidth_apply_flag = False - # Two 24 hour clock times, marking the start and stop of the period - # during which alternative limits are applied. If the stop time is - # earlier than the start time, then it is applied the next day - self.alt_start_time = '21:00' - self.alt_stop_time = '07:00' - # A string describing the days on which the limit is applied: - # 'every_day', 'weekdays', 'weekends', or 'monday', 'tuesday' etc. - # The strings are translated by formats.SPECIFIED_DAYS_DICT - self.alt_day_string = 'every_day' - - # The method of matching downloaded videos against existing - # media.Video objects: - # 'exact_match' - The video name must match exactly - # 'match_first' - The first n characters of the video name must - # match exactly - # 'ignore_last' - All characters before the last n characters of - # the video name must match exactly - self.match_method = 'exact_match' - # Default values for self.match_first_chars and .match_ignore_chars - self.match_default_chars = 10 - # For 'match_first', the number of characters (n) to use. Set to the - # default value when self.match_method is not 'match_first'; range - # 1-999 - self.match_first_chars = self.match_default_chars - # For 'ignore_last', the number of characters (n) to ignore. Set to the - # default value of when self.match_method is not 'ignore_last'; range - # 1-999 - self.match_ignore_chars = self.match_default_chars - # For all values of self.match_method, check against both - # media.Video.name and media.Video.nickname - # (If custom file templates have been applied, media.Video.nickname - # will preserve the video's original name, which is probably the one - # the user expects to match) - self.match_nickname_flag = True - - # Automatic video deletion/removal. Applies only to downloaded videos - # (not to checked videos) - # Flag set to True if videos (and all their associated files) should be - # deleted after a certain time - # (Note that if both self.auto_delete_flag and self.auto_remove_flag - # are True, and using the same time, then deletion not removal - # occurs) - self.auto_delete_flag = False - # Videos are automatically deleted after this many days (must be an - # integer, minimum value 0; ignored if self.auto_delete_flag is - # False) - self.auto_delete_days = 30 - # Flag set to True if videos should be removed from the Tartube - # database after a certain time, but with no files deleted - self.auto_remove_flag = False - # Videos are automatically removed after this many days (must be an - # integer, minimum value 0; ignored if self.auto_remove_flag is - # False) - self.auto_remove_days = 30 - # Flag set to True if videos should be automatically deleted/removed, - # but only if they have been watched (media.Video.dl_flag is True, - # media.Video.new_flag is False; ignored if - # self.auto_delete_flag and/or self.auto_remove_flag are False) - self.auto_delete_watched_flag = False - # Flag set to True if the deletion/removal should take place after - # every download operation. If False, it takes place when the - # database is loaded - self.auto_delete_asap_flag = False - - # Temporary folder emptying (applies to all media.Folder objects whose - # .temp_flag is True) - # Temporary folders are always emptied when Tartube starts. Flag set to - # True if they should be emptied when Tartube shuts down, as well - self.delete_on_shutdown_flag = False - # Flag set to True if temporary folders should be opened (on the - # desktop) when Tartube shuts down, so the user can more conveniently - # copy things out of it (but only if videos actually exist in the - # folder(s). Ignored if self.delete_on_shutdown_flag is True - self.open_temp_on_desktop_flag = False - - # How much information to show in the Video Index. False to show - # minimal video stats, True to show full video stats - self.complex_index_flag = False - # The Video Catalogue has several display modes. The current mode: - # 'simple_hide_parent' - No thumbnail, show description - # 'simple_show_parent' - No thumbnail, show parent - # 'complex_hide_parent' - Thumbnail, show description - # 'complex_hide_parent_ext' - Thumbnail, description & extra labels - # 'complex_show_parent' - Thumbnail, show parent - # 'complex_show_parent_ext' - Thumbnail, parent & extra labels - # 'grid_show_parent' - Grid mode with thumbnail and parent - # 'grid_show_parent_ext' - Grid mode with thumb, parent & extra - # labels - self.catalogue_mode = 'grid_show_parent' - # The current Video Catalogue mode type: 'simple', 'complex' or 'grid' - self.catalogue_mode_type = 'grid' - # Ordered list of Video Catalogue modes, used for switching between - # them (and for setting up Tartube's main menu) - ignore_me = _( - 'TRANSLATOR\'S NOTE: Videos in the Videos tab can be displayed' \ - + ' in one of several formats' - ) - self.catalogue_mode_list = [ - [ - 'simple_hide_parent', - 'simple', - _('_Basic list'), - ], - [ - 'simple_show_parent', - 'simple', - _('Basic list with _container names'), - ], - [ - 'complex_hide_parent', - 'complex', - _('_Thumbnails'), - ], - [ - 'complex_hide_parent_ext', - 'complex', - _('Thumbnails and _extra labels'), - ], - [ - 'complex_show_parent', - 'complex', - _('T_humbnails and container names'), - ], - [ - 'complex_show_parent_ext', - 'complex', - _('Th_umbnails, container names and extra labels'), - ], - [ - 'grid_show_parent', - 'grid', - _('_Grid'), - ], - [ - 'grid_show_parent_ext', - 'grid', - _('G_rid with extra labels'), - ], - ] - # The Video Catalogue splits its video list into pages (as Gtk - # struggles with a list of hundreds, or thousands, of videos) - # The number of videos per page, or 0 to always use a single page - self.catalogue_page_size = 50 - # Flag set to True if the Video Catalogue toolbar should show an extra - # row, containing video filter options - self.catalogue_show_filter_flag = False - # Video catalogue sorting mode: 'default' to sort by upload time, - # 'alpha' to sort alphabetically, 'receive' to sort by download - # time, 'dbid' to sort by the video's database ID (.dbid) - # When set to 'default', the sorting algorithm actually sorts by - # livestream status, then by playlist index, then by upload time, - # then by receive time, then by name, then by .dbid - # The other values are more strict, sorting only by specified method, - # then (only if necessary) by name or by .dbid - # Note that YouTube and others only provide an upload date, which - # Tartube converts to a time (and is not, therefore, accurate) - self.catalogue_sort_mode = 'default' - # Flag set to True to enable reverse sort (using the method specified - # by self.catalogue_sort_mode) - self.catalogue_reverse_sort_flag = False - # Flag set to True if the 'Regex' button is toggled on, meaning that - # when the searching the catalogue, we match videos using a regex, - # rather than a simple string - self.catalogue_use_regex_flag = False - - # Two flags used for bulk-editing URLS of media data objects (in the - # config.SystemPrefWin window) - # Flag set to True if the user should be prompted for confirmation - # every time an individual URL is changed (in the window) - self.url_change_confirm_flag = True - # Flag set to True if search/replace operations on multiple URLs - # should use a regex, False if the pattern is an ordinary substring - self.url_change_regex_flag = True - - # Default and customisable colours used as backgrounds in the Video - # Catalogue to highlight livestream/debut videos. (The Video - # Catalogue uses three different formats, and not every format uses - # every colour) - # Colours are stored as lists in the form [R, G, B, A], matching the - # arguments for a Gdk.RGBA object - # Dictionary of default background colours - self.default_bg_table = { - # Not selected - 'live_wait': [1, 0, 0, 0.1], # Red - 'live_now': [0, 1, 0, 0.2], # Green - 'debut_wait': [1, 1, 0, 0.2], # Yellow - 'debut_now': [0, 1, 1, 0.2], # Cyan - # Selected - 'select': [0, 0, 1, 0.1], # Blue - 'select_wait': [1, 0, 1, 0.1], # Purple - 'select_live': [1, 0, 1, 0.1], # Purple - # Drag and drop tab - 'drag_drop_notify': [1, 0, 1, 0.1], # Purple - 'drag_drop_odd': [1, 1, 0, 0.1], # Orange - 'drag_drop_even': [1, 1, 0, 0.05], # Pale orange - } - # Dictionary of customisable colours - self.custom_bg_table = self.default_bg_table.copy() - - # Dictionary of youtube-dl download options that are filtered out, when - # splitting video clips (in a call to - # utils.generate_ffmpeg_split_system_cmd(), etc) - # The keys are youtube-dl download options; the corresponding values - # are False for a boolean option, or True for an option that takes - # an argument - self.split_ignore_option_dict = { - # DOWNLOAD OPTIONS - # native_hls - '--hls-prefer-native': False, - # external_downloader - '--external-downloader': True, - # external_arg_string - '--external-downloader-args': True, - # FILESYSTEM OPTIONS - # (builds --output and --paths) - '-o': True, - '--output': True, - '-p': True, - '--paths': True, - # write_description - '--write-description': False, - # write_info - '--write-info-json': False, - # write_annotations - '--write-annotations': False, - # THUMBNAIL IMAGES - # write_thumbnail - '--write-thumbnail': False, - # VIDEO FORMAT OPTIONS - # all_formats - '--all-formats': False, - # prefer_free_formats - '--prefer-free-formats': False, - # yt_skip_dash - '--youtube-skip-dash-manifest': False, - # SUBTITLE OPTIONS - # write_subs - '--write-sub': False, - # write_auto_subs - '--write-auto-sub': False, - # write_all_subs - '--all-subs': False, - # subs_format - '--sub-format': True, - # subs_lang - '--sub-lang': True, - # POST-PROCESSING OPTIONS - # embed_subs - '--embed-subs': False, - # prefer_avconv - '--prefer-avconv': False, - # prefer_ffmpeg - '--prefer-ffmpeg': False, - # (Added directly in options.OptionsManager) - '--newline': False, - # YT-DLP OPTIONS - '--extractor-args': True, - '--split-chapters': False, - } - - # Flag set to True if download options unique to yt-dlp should be - # filtered out, when self.ytdl_fork is not set to 'yt-dlp' - self.ytdlp_filter_options_flag = True - # Dictionary of yt-dlp options to filter out, in that case - # The keys are youtube-dl download options; the corresponding values - # are False for a boolean option, or True for an option that takes - # an argument - self.ytdlp_exclusive_options_dict = { - # not passed to yt-dlp directly - '--paths': True, - '-P': True, # Alias of --paths - '--extractor-args': True, - # Options - 'live_from_start': False, - 'wait_for_video_min': 0, - # Video Selection Options - '--playlist-items': True, - '-I': True, # Alias of --playlist-items - '--break-on-existing': False, - '--break-on-reject': False, - '--skip-playlist-after-errors': True, - # Download Options - '--concurrent-fragments': True, - '-N': True, # Alias of --concurrent-fragments - '--throttled-rate': True, - # Filesystem Options - '--windows-filenames': False, - '--trim-filenames': True, - '--no-overwrites': False, - '--force-overwrites': False, - '--write-playlist-metafiles': False, - '--no-clean-infojson': False, - '--no-cookies': False, - '--cookies-from-browser': '', - '--no-cookies-from-browser': True, - # Internet Shortcut Options - '--write-link': False, - '--write-url-link': False, - '--write-webloc-link': False, - '--write-desktop-link': False, - # Verbosity and Simulation Options - '--ignore-no-formats-error': False, - '--force-write-archive': False, - # Workaround Options - '--sleep-requests': True, - '--sleep-subtitles': True, - # Video Format Options - '--video-multistreams': False, - '--audio-multistreams': False, - '--check-formats': False, - '--allow-unplayable-formats': False, - # Post-Processing Options - '--remux-video': True, - '--embed-metadata': False, - '--convert-thumbnails': True, - '--split-chapters': False, - # Extractor Options - '--extractor-retries': True, - '--no_allow_dynamic_mpd': False, - '--hls-split-discontinuity': False, - } - - - def do_startup(self): - - """Gio.Application standard function.""" - - GObject.threads_init() - Gtk.Application.do_startup(self) - - # Menu actions - # ------------ - - # 'File' column - change_db_menu_action = Gio.SimpleAction.new('change_db_menu', None) - change_db_menu_action.connect('activate', self.on_menu_change_db) - self.add_action(change_db_menu_action) - - check_db_menu_action = Gio.SimpleAction.new('check_db_menu', None) - check_db_menu_action.connect('activate', self.on_menu_check_db) - self.add_action(check_db_menu_action) - - save_db_menu_action = Gio.SimpleAction.new('save_db_menu', None) - save_db_menu_action.connect('activate', self.on_menu_save_db) - self.add_action(save_db_menu_action) - - save_all_menu_action = Gio.SimpleAction.new('save_all_menu', None) - save_all_menu_action.connect('activate', self.on_menu_save_all) - self.add_action(save_all_menu_action) - - close_tray_menu_action = Gio.SimpleAction.new('close_tray_menu', None) - close_tray_menu_action.connect('activate', self.on_menu_close_tray) - self.add_action(close_tray_menu_action) - - quit_menu_action = Gio.SimpleAction.new('quit_menu', None) - quit_menu_action.connect('activate', self.on_menu_quit) - self.add_action(quit_menu_action) - - # 'Edit' column - system_prefs_action = Gio.SimpleAction.new('system_prefs_menu', None) - system_prefs_action.connect( - 'activate', - self.on_menu_system_preferences, - ) - self.add_action(system_prefs_action) - - gen_options_action = Gio.SimpleAction.new('gen_options_menu', None) - gen_options_action.connect('activate', self.on_menu_general_options) - self.add_action(gen_options_action) - - # 'System' column - if os.name == 'nt': - - open_msys2_action = Gio.SimpleAction.new('open_msys2_menu', None) - open_msys2_action.connect('activate', self.on_menu_open_msys2) - self.add_action(open_msys2_action) - - show_install_action = Gio.SimpleAction.new( - 'show_install_menu', - None, - ) - show_install_action.connect('activate', self.on_menu_show_install) - self.add_action(show_install_action) - - show_script_action = Gio.SimpleAction.new( - 'show_script_menu', - None, - ) - show_script_action.connect('activate', self.on_menu_show_script) - self.add_action(show_script_action) - - change_theme_action = Gio.SimpleAction.new( - 'change_theme_menu', - None, - ) - change_theme_action.connect('activate', self.on_menu_change_theme) - self.add_action(change_theme_action) - - # 'Media' column - add_video_menu_action = Gio.SimpleAction.new('add_video_menu', None) - add_video_menu_action.connect('activate', self.on_menu_add_video) - self.add_action(add_video_menu_action) - - add_channel_menu_action = Gio.SimpleAction.new( - 'add_channel_menu', - None, - ) - add_channel_menu_action.connect('activate', self.on_menu_add_channel) - self.add_action(add_channel_menu_action) - - add_playlist_menu_action = Gio.SimpleAction.new( - 'add_playlist_menu', - None, - ) - add_playlist_menu_action.connect( - 'activate', - self.on_menu_add_playlist, - ) - self.add_action(add_playlist_menu_action) - - add_folder_menu_action = Gio.SimpleAction.new('add_folder_menu', None) - add_folder_menu_action.connect('activate', self.on_menu_add_folder) - self.add_action(add_folder_menu_action) - - add_bulk_menu_action = Gio.SimpleAction.new('add_bulk_menu', None) - add_bulk_menu_action.connect('activate', self.on_menu_add_bulk) - self.add_action(add_bulk_menu_action) - - reset_container_menu_action = Gio.SimpleAction.new( - 'reset_container_menu', - None, - ) - reset_container_menu_action.connect( - 'activate', - self.on_menu_reset_container, - ) - self.add_action(reset_container_menu_action) - - export_db_menu_action = Gio.SimpleAction.new('export_db_menu', None) - export_db_menu_action.connect('activate', self.on_menu_export_db) - self.add_action(export_db_menu_action) - - import_db_menu_action = Gio.SimpleAction.new('import_db_menu', None) - import_db_menu_action.connect('activate', self.on_menu_import_db) - self.add_action(import_db_menu_action) - - import_yt_menu_action = Gio.SimpleAction.new('import_yt_menu', None) - import_yt_menu_action.connect('activate', self.on_menu_import_yt) - self.add_action(import_yt_menu_action) - - hide_system_menu_action = Gio.SimpleAction.new( - 'hide_system_menu', - None, - ) - hide_system_menu_action.connect('activate', self.on_menu_hide_system) - self.add_action(hide_system_menu_action) - - show_hidden_menu_action = Gio.SimpleAction.new( - 'show_hidden_menu', - None, - ) - show_hidden_menu_action.connect('activate', self.on_menu_show_hidden) - self.add_action(show_hidden_menu_action) - - auto_switch_menu_action = Gio.SimpleAction.new( - 'auto_switch_menu', - None, - ) - auto_switch_menu_action.connect( - 'activate', - self.on_menu_auto_switch, - ) - self.add_action(auto_switch_menu_action) - - create_profile_menu_action = Gio.SimpleAction.new( - 'create_profile_menu', - None, - ) - create_profile_menu_action.connect( - 'activate', - self.on_menu_create_profile, - ) - self.add_action(create_profile_menu_action) - - mark_all_menu_action = Gio.SimpleAction.new( - 'mark_all_menu', - None, - ) - mark_all_menu_action.connect('activate', self.on_menu_mark_all) - self.add_action(mark_all_menu_action) - - unmark_all_menu_action = Gio.SimpleAction.new( - 'unmark_all_menu', - None, - ) - unmark_all_menu_action.connect('activate', self.on_menu_unmark_all) - self.add_action(unmark_all_menu_action) - - if self.debug_test_media_menu_flag: - test_menu_action = Gio.SimpleAction.new('test_menu', None) - test_menu_action.connect('activate', self.on_menu_test) - self.add_action(test_menu_action) - - if self.debug_test_code_menu_flag: - test_code_menu_action = Gio.SimpleAction.new( - 'test_code_menu', - None, - ) - test_code_menu_action.connect('activate', self.on_menu_test_code) - self.add_action(test_code_menu_action) - - # 'Operations' column - check_all_menu_action = Gio.SimpleAction.new('check_all_menu', None) - check_all_menu_action.connect( - 'activate', - self.on_menu_check_all, - ) - self.add_action(check_all_menu_action) - - download_all_menu_action = Gio.SimpleAction.new( - 'download_all_menu', - None, - ) - download_all_menu_action.connect( - 'activate', - self.on_menu_download_all, - ) - self.add_action(download_all_menu_action) - - custom_dl_all_menu_action = Gio.SimpleAction.new( - 'custom_dl_all_menu', - None, - ) - custom_dl_all_menu_action.connect( - 'activate', - self.on_menu_custom_dl_all, - ) - self.add_action(custom_dl_all_menu_action) - - refresh_db_menu_action = Gio.SimpleAction.new('refresh_db_menu', None) - refresh_db_menu_action.connect('activate', self.on_menu_refresh_db) - self.add_action(refresh_db_menu_action) - - ytdl_menu_action = Gio.SimpleAction.new('update_ytdl_menu', None) - ytdl_menu_action.connect('activate', self.on_menu_update_ytdl) - self.add_action(ytdl_menu_action) - - ytdl_test_menu_action = Gio.SimpleAction.new('test_ytdl_menu', None) - ytdl_test_menu_action.connect('activate', self.on_menu_test_ytdl) - self.add_action(ytdl_test_menu_action) - - if os.name == 'nt': - - ffmpeg_menu_action = Gio.SimpleAction.new( - 'install_ffmpeg_menu', - None, - ) - ffmpeg_menu_action.connect( - 'activate', - self.on_menu_install_ffmpeg, - ) - self.add_action(ffmpeg_menu_action) - - matplotlib_menu_action = Gio.SimpleAction.new( - 'install_matplotlib_menu', - None, - ) - matplotlib_menu_action.connect( - 'activate', - self.on_menu_install_matplotlib, - ) - self.add_action(matplotlib_menu_action) - - streamlink_menu_action = Gio.SimpleAction.new( - 'install_streamlink_menu', - None, - ) - streamlink_menu_action.connect( - 'activate', - self.on_menu_install_streamlink, - ) - self.add_action(streamlink_menu_action) - - tidy_up_menu_action = Gio.SimpleAction.new('tidy_up_menu', None) - tidy_up_menu_action.connect('activate', self.on_menu_tidy_up) - self.add_action(tidy_up_menu_action) - - stop_operation_menu_action = Gio.SimpleAction.new( - 'stop_operation_menu', - None, - ) - stop_operation_menu_action.connect( - 'activate', - self.on_button_stop_operation, - ) - self.add_action(stop_operation_menu_action) - - stop_soon_menu_action = Gio.SimpleAction.new('stop_soon_menu', None) - stop_soon_menu_action.connect('activate', self.on_menu_stop_soon) - self.add_action(stop_soon_menu_action) - - # 'Livestreams' column - live_prefs_menu_action = Gio.SimpleAction.new( - 'live_prefs_menu', - None, - ) - live_prefs_menu_action.connect( - 'activate', - self.on_menu_live_preferences, - ) - self.add_action(live_prefs_menu_action) - - update_live_menu_action = Gio.SimpleAction.new( - 'update_live_menu', - None, - ) - update_live_menu_action.connect('activate', self.on_menu_update_live) - self.add_action(update_live_menu_action) - - cancel_live_menu_action = Gio.SimpleAction.new( - 'cancel_live_menu', - None, - ) - cancel_live_menu_action.connect('activate', self.on_menu_cancel_live) - self.add_action(cancel_live_menu_action) - - # 'Help' column - about_menu_action = Gio.SimpleAction.new('about_menu', None) - about_menu_action.connect('activate', self.on_menu_about) - self.add_action(about_menu_action) - - tutorial_menu_action = Gio.SimpleAction.new('tutorial_menu', None) - tutorial_menu_action.connect('activate', self.on_menu_tutorial) - self.add_action(tutorial_menu_action) - - check_version_menu_action = Gio.SimpleAction.new( - 'check_version_menu', - None, - ) - check_version_menu_action.connect( - 'activate', - self.on_menu_check_version, - ) - self.add_action(check_version_menu_action) - - go_website_menu_action = Gio.SimpleAction.new('go_website_menu', None) - go_website_menu_action.connect('activate', self.on_menu_go_website) - self.add_action(go_website_menu_action) - - send_feedback_menu_action = Gio.SimpleAction.new( - 'send_feedback_menu', - None, - ) - send_feedback_menu_action.connect( - 'activate', - self.on_menu_send_feedback, - ) - self.add_action(send_feedback_menu_action) - - # Main toolbar actions - # -------------------- - - add_video_toolbutton_action = Gio.SimpleAction.new( - 'add_video_toolbutton', - None, - ) - add_video_toolbutton_action.connect( - 'activate', - self.on_menu_add_video, - ) - self.add_action(add_video_toolbutton_action) - - add_channel_toolbutton_action = Gio.SimpleAction.new( - 'add_channel_toolbutton', - None, - ) - add_channel_toolbutton_action.connect( - 'activate', - self.on_menu_add_channel, - ) - self.add_action(add_channel_toolbutton_action) - - add_playlist_toolbutton_action = Gio.SimpleAction.new( - 'add_playlist_toolbutton', - None, - ) - add_playlist_toolbutton_action.connect( - 'activate', - self.on_menu_add_playlist, - ) - self.add_action(add_playlist_toolbutton_action) - - add_folder_toolbutton_action = Gio.SimpleAction.new( - 'add_folder_toolbutton', - None, - ) - add_folder_toolbutton_action.connect( - 'activate', - self.on_menu_add_folder, - ) - self.add_action(add_folder_toolbutton_action) - - check_all_toolbutton_action = Gio.SimpleAction.new( - 'check_all_toolbutton', - None, - ) - check_all_toolbutton_action.connect( - 'activate', - self.on_menu_check_all, - ) - self.add_action(check_all_toolbutton_action) - - download_all_toolbutton_action = Gio.SimpleAction.new( - 'download_all_toolbutton', - None, - ) - download_all_toolbutton_action.connect( - 'activate', - self.on_menu_download_all, - ) - self.add_action(download_all_toolbutton_action) - - stop_operation_button_action = Gio.SimpleAction.new( - 'stop_operation_toolbutton', - None, - ) - stop_operation_button_action.connect( - 'activate', - self.on_button_stop_operation, - ) - self.add_action(stop_operation_button_action) - - system_prefs_button_action = Gio.SimpleAction.new( - 'system_prefs_toolbutton', - None, - ) - system_prefs_button_action.connect( - 'activate', - self.on_menu_system_preferences, - ) - self.add_action(system_prefs_button_action) - - gen_options_button_action = Gio.SimpleAction.new( - 'gen_options_toolbutton', - None, - ) - gen_options_button_action.connect( - 'activate', - self.on_menu_general_options, - ) - self.add_action(gen_options_button_action) - - switch_view_button_action = Gio.SimpleAction.new( - 'switch_view_toolbutton', - None, - ) - switch_view_button_action.connect( - 'activate', - self.on_button_switch_view, - ) - self.add_action(switch_view_button_action) - - hide_system_button_action = Gio.SimpleAction.new( - 'hide_system_toolbutton', - None, - ) - hide_system_button_action.connect( - 'activate', - self.on_button_hide_system, - ) - self.add_action(hide_system_button_action) - - quit_button_action = Gio.SimpleAction.new('quit_toolbutton', None) - quit_button_action.connect('activate', self.on_menu_quit) - self.add_action(quit_button_action) - - # Video catalogue toolbar actions - # ------------------------------- - - first_page_toolbutton_action = Gio.SimpleAction.new( - 'first_page_toolbutton', - None, - ) - first_page_toolbutton_action.connect( - 'activate', - self.on_button_first_page, - ) - self.add_action(first_page_toolbutton_action) - - previous_page_toolbutton_action = Gio.SimpleAction.new( - 'previous_page_toolbutton', - None, - ) - previous_page_toolbutton_action.connect( - 'activate', - self.on_button_previous_page, - ) - self.add_action(previous_page_toolbutton_action) - - next_page_toolbutton_action = Gio.SimpleAction.new( - 'next_page_toolbutton', - None, - ) - next_page_toolbutton_action.connect( - 'activate', - self.on_button_next_page, - ) - self.add_action(next_page_toolbutton_action) - - last_page_toolbutton_action = Gio.SimpleAction.new( - 'last_page_toolbutton', - None, - ) - last_page_toolbutton_action.connect( - 'activate', - self.on_button_last_page, - ) - self.add_action(last_page_toolbutton_action) - - scroll_up_toolbutton_action = Gio.SimpleAction.new( - 'scroll_up_toolbutton', - None, - ) - scroll_up_toolbutton_action.connect( - 'activate', - self.on_button_scroll_up, - ) - self.add_action(scroll_up_toolbutton_action) - - scroll_down_toolbutton_action = Gio.SimpleAction.new( - 'scroll_down_toolbutton', - None, - ) - scroll_down_toolbutton_action.connect( - 'activate', - self.on_button_scroll_down, - ) - self.add_action(scroll_down_toolbutton_action) - - show_filter_toolbutton_action = Gio.SimpleAction.new( - 'show_filter_toolbutton', - None, - ) - show_filter_toolbutton_action.connect( - 'activate', - self.on_button_show_filter, - ) - self.add_action(show_filter_toolbutton_action) - - # (Second/third rows) - - reverse_sort_toolbutton_action = Gio.SimpleAction.new( - 'reverse_sort_toolbutton', - None, - ) - reverse_sort_toolbutton_action.connect( - 'activate', - self.on_button_reverse_sort_catalogue, - ) - self.add_action(reverse_sort_toolbutton_action) - - resort_toolbutton_action = Gio.SimpleAction.new( - 'resort_toolbutton', - None, - ) - resort_toolbutton_action.connect( - 'activate', - self.on_button_resort_catalogue, - ) - self.add_action(resort_toolbutton_action) - - use_regex_togglebutton_action = Gio.SimpleAction.new( - 'use_regex_togglebutton', - None, - ) - use_regex_togglebutton_action.connect( - 'activate', - self.on_button_use_regex, - ) - self.add_action(use_regex_togglebutton_action) - - apply_filter_button_action = Gio.SimpleAction.new( - 'apply_filter_toolbutton', - None, - ) - apply_filter_button_action.connect( - 'activate', - self.on_button_apply_filter, - ) - self.add_action(apply_filter_button_action) - - cancel_filter_button_action = Gio.SimpleAction.new( - 'cancel_filter_toolbutton', - None, - ) - cancel_filter_button_action.connect( - 'activate', - self.on_button_cancel_filter, - ) - self.add_action(cancel_filter_button_action) - - find_date_toolbutton_action = Gio.SimpleAction.new( - 'find_date_toolbutton', - None, - ) - find_date_toolbutton_action.connect( - 'activate', - self.on_button_find_date, - ) - self.add_action(find_date_toolbutton_action) - - cancel_date_toolbutton_action = Gio.SimpleAction.new( - 'cancel_date_toolbutton', - None, - ) - cancel_date_toolbutton_action.connect( - 'activate', - self.on_button_cancel_date, - ) - self.add_action(cancel_date_toolbutton_action) - - # Videos tab actions - # ------------------ - - # Buttons - - check_all_button_action = Gio.SimpleAction.new( - 'check_all_button', - None, - ) - check_all_button_action.connect('activate', self.on_button_check_all) - self.add_action(check_all_button_action) - - download_all_button_action = Gio.SimpleAction.new( - 'download_all_button', - None, - ) - download_all_button_action.connect( - 'activate', - self.on_button_download_all, - ) - self.add_action(download_all_button_action) - - custom_dl_all_button_action = Gio.SimpleAction.new( - 'custom_dl_all_button', - None, - ) - custom_dl_all_button_action.connect( - 'activate', - self.on_button_custom_dl_all, - ) - self.add_action(custom_dl_all_button_action) - - # Classic Mode tab actions - # ------------------------ - - # Buttons - - classic_menu_button_action = Gio.SimpleAction.new( - 'classic_menu_button', - None, - ) - classic_menu_button_action.connect( - 'activate', - self.on_button_classic_menu, - ) - self.add_action(classic_menu_button_action) - - classic_dest_dir_button_action = Gio.SimpleAction.new( - 'classic_dest_dir_button', - None, - ) - classic_dest_dir_button_action.connect( - 'activate', - self.on_button_classic_dest_dir, - ) - self.add_action(classic_dest_dir_button_action) - - classic_dest_dir_open_action = Gio.SimpleAction.new( - 'classic_dest_dir_open_button', - None, - ) - classic_dest_dir_open_action.connect( - 'activate', - self.on_button_classic_dest_dir_open, - ) - self.add_action(classic_dest_dir_open_action) - - classic_add_clips_button_action = Gio.SimpleAction.new( - 'classic_add_clips_button', - None, - ) - classic_add_clips_button_action.connect( - 'activate', - self.on_button_classic_add_clips, - ) - self.add_action(classic_add_clips_button_action) - - classic_add_urls_button_action = Gio.SimpleAction.new( - 'classic_add_urls_button', - None, - ) - classic_add_urls_button_action.connect( - 'activate', - self.on_button_classic_add_urls, - ) - self.add_action(classic_add_urls_button_action) - - classic_play_button_action = Gio.SimpleAction.new( - 'classic_play_button', - None, - ) - classic_play_button_action.connect( - 'activate', - self.on_button_classic_play, - ) - self.add_action(classic_play_button_action) - - classic_open_button_action = Gio.SimpleAction.new( - 'classic_open_button', - None, - ) - classic_open_button_action.connect( - 'activate', - self.on_button_classic_open, - ) - self.add_action(classic_open_button_action) - - classic_redownload_button_action = Gio.SimpleAction.new( - 'classic_redownload_button', - None, - ) - classic_redownload_button_action.connect( - 'activate', - self.on_button_classic_redownload, - ) - self.add_action(classic_redownload_button_action) - - classic_stop_button_action = Gio.SimpleAction.new( - 'classic_stop_button', - None, - ) - classic_stop_button_action.connect( - 'activate', - self.on_button_classic_stop, - ) - self.add_action(classic_stop_button_action) - - classic_archive_button_action = Gio.SimpleAction.new( - 'classic_archive_button', - None, - ) - classic_archive_button_action.connect( - 'activate', - self.on_button_classic_archive, - ) - self.add_action(classic_archive_button_action) - - classic_clips_button_action = Gio.SimpleAction.new( - 'classic_clips_button', - None, - ) - classic_clips_button_action.connect( - 'activate', - self.on_button_classic_clips, - ) - self.add_action(classic_clips_button_action) - - classic_ffmpeg_button_action = Gio.SimpleAction.new( - 'classic_ffmpeg_button', - None, - ) - classic_ffmpeg_button_action.connect( - 'activate', - self.on_button_classic_ffmpeg, - ) - self.add_action(classic_ffmpeg_button_action) - - classic_move_up_button_action = Gio.SimpleAction.new( - 'classic_move_up_button', - None, - ) - classic_move_up_button_action.connect( - 'activate', - self.on_button_classic_move_up, - ) - self.add_action(classic_move_up_button_action) - - classic_move_down_button_action = Gio.SimpleAction.new( - 'classic_move_down_button', - None, - ) - classic_move_down_button_action.connect( - 'activate', - self.on_button_classic_move_down, - ) - self.add_action(classic_move_down_button_action) - - classic_remove_button_action = Gio.SimpleAction.new( - 'classic_remove_button', - None, - ) - classic_remove_button_action.connect( - 'activate', - self.on_button_classic_remove, - ) - self.add_action(classic_remove_button_action) - - classic_clear_dl_button_action = Gio.SimpleAction.new( - 'classic_clear_dl_button', - None, - ) - classic_clear_dl_button_action.connect( - 'activate', - self.on_button_classic_clear_dl, - ) - self.add_action(classic_clear_dl_button_action) - - classic_clear_button_action = Gio.SimpleAction.new( - 'classic_clear_button', - None, - ) - classic_clear_button_action.connect( - 'activate', - self.on_button_classic_clear, - ) - self.add_action(classic_clear_button_action) - - classic_download_button_action = Gio.SimpleAction.new( - 'classic_download_button', - None, - ) - classic_download_button_action.connect( - 'activate', - self.on_button_classic_download, - ) - self.add_action(classic_download_button_action) - - # Drag and Drop tab actions - # ------------------------- - - # Buttons - - drag_drop_add_button_action = Gio.SimpleAction.new( - 'drag_drop_add_button', - None, - ) - drag_drop_add_button_action.connect( - 'activate', - self.on_button_drag_drop_add, - ) - self.add_action(drag_drop_add_button_action) - - # Errors/Warnings tab actions - # ---------------------------- - - # Buttons - - apply_error_filter_button_action = Gio.SimpleAction.new( - 'apply_error_filter_toolbutton', - None, - ) - apply_error_filter_button_action.connect( - 'activate', - self.on_button_apply_error_filter, - ) - self.add_action(apply_error_filter_button_action) - - cancel_error_filter_button_action = Gio.SimpleAction.new( - 'cancel_error_filter_toolbutton', - None, - ) - cancel_error_filter_button_action.connect( - 'activate', - self.on_button_cancel_error_filter, - ) - self.add_action(cancel_error_filter_button_action) - - - def do_activate(self): - - """Gio.Application standard function.""" - - # If the flag is set, restrict Tartube to a single instance - if not __main__.__multiple_instance_flag__ and self.main_win_obj: - - self.main_win_obj.present() - - # Otherwise permit multiple instances - else: - - self.start() - - # If debugging flags are set... - if self.main_win_obj: - - # ...open the system preferences window - if self.debug_open_pref_win_flag: - config.SystemPrefWin(self) - - # ...open the general download options window - if self.debug_open_options_win_flag: - config.OptionsEditWin(self, self.general_options_obj) - - # ...write the Gtk version to the terminal - if self.debug_write_gtk_flag: - print( - 'Tartube running on Gtk v' \ - + str(self.gtk_version_major) + '.' \ - + str(self.gtk_version_minor) + '.' \ - + str(self.gtk_version_micro) - ) - - - def do_shutdown(self): - - """Gio.Application standard function. - - Clean shutdowns (for example, from the main window's toolbar) are - handled by self.stop(). - - N.B. When called by mainwin.MainWin.on_delete_event(), the config/ - database files have already been saved. - """ - - # Stop the GObject timers immediately - if self.script_slow_timer_id: - GObject.source_remove(self.script_slow_timer_id) - if self.script_fast_timer_id: - GObject.source_remove(self.script_fast_timer_id) - if self.dl_timer_id: - GObject.source_remove(self.dl_timer_id) - if self.update_timer_id: - GObject.source_remove(self.update_timer_id) - if self.refresh_timer_id: - GObject.source_remove(self.refresh_timer_id) - if self.info_timer_id: - GObject.source_remove(self.info_timer_id) - if self.tidy_timer_id: - GObject.source_remove(self.tidy_timer_id) - if self.process_timer_id: - GObject.source_remove(self.process_timer_id) - - # Don't prompt the user before halting an operation, as we would do in - # calls to self.stop() - if self.download_manager_obj: - self.download_manager_obj.stop_download_operation() - elif self.update_manager_obj: - self.update_manager_obj.stop_update_operation() - elif self.refresh_manager_obj: - self.refresh_manager_obj.stop_refresh_operation() - elif self.info_manager_obj: - self.info_manager_obj.stop_info_operation() - elif self.tidy_manager_obj: - self.tidy_manager_obj.stop_tidy_operation() - elif self.process_manager_obj: - self.process_manager_obj.stop_process_operation() - - # If there is a lock on the database file, release it - self.remove_db_lock_file() - - # Stop immediately - Gtk.Application.do_shutdown(self) - # After an update operation, only this method might work - os._exit(0) - # Still here? Do a brute-force exit - exit() - - - # Public class methods - - - def start(self): - - """Called by self.do_activate(). - - Performs general initialisation. - """ - - # Part 1 - Give mainapp.TartubeApp IVs their initial values - # --------------------------------------------------------- - - # Set youtube-dl path IVs - self.setup_paths() - - # Compile a list of available sound effects - self.find_sound_effects() - - # Part 2 - create the main window - # ------------------------------- - - # (The window is not set up, nor made visible, until the config file - # has been loaded/saved/created) - self.main_win_obj = mainwin.MainWin(self) - - # Part 3 - setup some managers - # ---------------------------- - - # Start the dialogue manager (thread-safe code for Gtk message dialogue - # windows) - self.dialogue_manager_obj = dialogue.DialogueManager( - self, - self.main_win_obj, - ) - - # Set the General Options Manager - self.general_options_obj = self.create_download_options('general') - self.general_options_obj.set_general_options() - # Apply a different set of download options to the Classic Mode tab, by - # default - self.classic_options_obj = self.create_download_options('classic') - self.classic_options_obj.set_classic_mode_options() - # Create a third set of download options for use in the Drag and Drop - # tab - mp3_options_obj = self.create_download_options('mp3') - mp3_options_obj.set_mp3_options() - - # Add these download options managers to the Drag and Drop Grid - self.classic_dropzone_list = [ - self.general_options_obj.uid, - self.classic_options_obj.uid, - mp3_options_obj.uid, - ] - - # Set the current FFmpeg Options Manager - self.ffmpeg_options_obj = self.create_ffmpeg_options('default') - - # Set the General Custom Download Manager - self.general_custom_dl_obj = self.create_custom_dl_manager('general') - # Use a different manager in the Classic Mode tab, by default - self.classic_custom_dl_obj = self.create_custom_dl_manager('classic') - self.classic_custom_dl_obj.set_dl_precede_flag(True) - - # Part 4 - Load the config file - # ----------------------------- - - # Make sure the directory containing the config file exists - # v2.0.003 (amended v2.1.034) The user can force Tartube to use the - # config file in the script's directory (rather than the one in the - # location described by xdg) by placing a 'settings.json' file there. - # If that file is created when Tartube is already running, it can be - # an empty file (because Tartube overwrites it). Otherwise, it should - # be a copy of a legitimate config file - if not os.path.isfile(self.config_file_path): - - config_dir = None - if ( - self.config_file_xdg_dir is not None - and not os.path.isdir(self.config_file_xdg_dir) - ): - config_dir = self.config_file_xdg_dir - - elif ( - self.config_file_xdg_dir is None - and not os.path.isdir(self.config_file_dir) - ): - config_dir = self.config_file_dir - - if config_dir is not None \ - and not self.make_directory(config_dir): - - dialogue_win \ - = self.dialogue_manager_obj.show_simple_msg_dialogue( - _( - 'Tartube can\'t create the folder in which its' \ - + ' configuration file is saved', - ), - 'error', - 'ok', - ) - dialogue_win.connect('destroy', self.stop) - - return - - # If the config file exists, load it. If not, create it - new_config_flag = False - if ( - self.config_file_xdg_path is not None \ - and os.path.isfile(self.config_file_xdg_path) - ) or os.path.isfile(self.config_file_path): - new_config_flag = self.load_config() - - else: - - # The system locale is applied in the call to self.load_config(). - # Since we aren't calling that now, we must apply the locale - # directly - if self.current_locale != formats.LOCALE_DEFAULT: - self.apply_locale() - - # Now respond to the missing config file - if self.debug_no_dialogue_flag: - self.save_config() - new_config_flag = True - - elif not self.disable_load_save_flag: - - # New Tartube installation - new_config_flag = True - - if new_config_flag and not self.debug_no_dialogue_flag: - - # Open the wizard window, so the user can set the data directory, - # specify which fork of youtube-dl to use, and (depending on the - # system) download and install youtube-dl and/or FFmpeg - self.open_wiz_win() - - elif self.disable_load_save_flag: - - # Load/save has been disabled. Show the error message in a dialogue - # window, then shut down - msg = _('Tartube failed to start because:') \ - + '\n\n' \ - + utils.tidy_up_long_string( - self.disable_load_save_msg, - self.main_win_obj.long_string_max_len, - ) + '\n\n' \ - + utils.tidy_up_long_string( - _( - 'If you don\'t know how to resolve this error, please' \ - + ' contact the authors', - ), - self.main_win_obj.long_string_max_len, - ) - - dialogue_win = self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'error', - 'ok', - ) - dialogue_win.connect('destroy', self.stop) - - else: - - # The config file has been loaded (or created), so continue with - # general initialisation immediately - self.start_continue(new_config_flag) - - - def start_continue(self, new_config_flag): - - """Called by self.start() or .open_wiz_win_continue(). - - The config file has been loaded (or created), so continue with general - initialisation immediately. - - Args: - - new_config_flag (bool): True for a new Tartube installation, False - otherwise - - """ - - # Part 5 - Finish setting up the main window - # ------------------------------------------ - - # Resize the main window to match the previous size, if required (but - # don't bother if the previous size is the same as the standard one) - if self.main_win_save_size_flag \ - and ( - self.main_win_save_width != self.main_win_width - or self.main_win_save_height != self.main_win_height - ): - self.main_win_obj.resize_self( - self.main_win_save_width, - self.main_win_save_height, - ) - - # Create the main window - self.main_win_obj.setup_win() - - # Set up widgets in the Video Catalogue toolbar - self.main_win_obj.update_catalogue_filter_widgets() - self.main_win_obj.update_catalogue_sort_widgets() - self.main_win_obj.update_catalogue_reverse_sort_widgets() - self.main_win_obj.update_catalogue_thumb_widgets() - # Add the right number of pages to the Output tab - self.main_win_obj.output_tab_setup_pages() - # If the flag it set, switch to the Classic Mode tab - if not __main__.__pkg_no_download_flag__ \ - and self.show_classic_tab_on_startup_flag: - self.main_win_obj.notebook.set_current_page( - self.main_win_obj.notebook_tab_dict['classic'], - ) - - # Most main widgets are desensitised, until the database file has been - # loaded - self.main_win_obj.sensitise_widgets_if_database(False) - # Disable tooltips, if necessary - if not self.show_tooltips_flag: - self.main_win_obj.disable_tooltips() - # Disable the 'Download all' button and related widgets, if necessary - if self.disable_dl_all_flag: - self.main_win_obj.disable_dl_all_buttons() - - # If the debugging flag is set, move the window to the top-left corner - # of the desktop - if self.debug_open_top_left_flag: - self.main_win_obj.move(0, 0) - - # Prepare to add an icon to the system tray. It becomes actually - # visible only when settings specify that - # Also, the main window must remain invisible, if settings specify that - # Tartube should open in the system tray - self.status_icon_obj = mainwin.StatusIcon(self) - if self.show_status_icon_flag: - self.status_icon_obj.show_icon() - if self.open_in_tray_flag: - self.main_win_obj.force_invisible() - else: - self.main_win_obj.show_all() - else: - self.main_win_obj.show_all() - - # Part 6 - Select a database file - # ------------------------------- - - if not new_config_flag \ - and not self.debug_no_dialogue_flag: - - # Multiple instances of Tartube can share the same config file, but - # not the same database file - # If the database file specified by the config file we've just - # loaded is locked (meaning it's in use by another instance), we - # might be able to use an alternative data directory - if self.data_dir_use_list_flag: - self.choose_alt_db() - - # Check that the data directory specified by self.data_dir actually - # exists. If not, the most common reason is that the user has - # forgotten to mount an external drive - # If the directory doesn't exist, prompt the user for further - # instructions. However, for a new installation (or at least, for a - # new config file), go ahead and try to create the directory without - # prompting, but only once - first_attempt_flag = new_config_flag - make_dir_fail_flag = False - - while not os.path.isdir(self.data_dir): - - # Ask the user what to do next. The False argument tells the - # dialogue window that it's a missing directory - if not first_attempt_flag: - - dialogue_win = mainwin.MountDriveDialogue( - self.main_win_obj, - make_dir_fail_flag, - ) - dialogue_win.run() - - # If the data directory now exists, or can be created in - # principle by the code just below (because the user wants to - # use the default location), then available_flag will be True - available_flag = dialogue_win.available_flag - dialogue_win.destroy() - - if not available_flag: - - # The user opted to shut down Tartube. Destroying the main - # window calls self.do_shutdown() - return self.main_win_obj.destroy() - - # On subsequent loops, always show the dialogue window - first_attempt_flag = False - - # Try creating the specified directory, if it doesn't exist. If - # this fails, the loop is repeated - make_dir_fail_flag = False - if not os.path.isdir(self.data_dir) \ - and not self.make_directory(self.data_dir): - - make_dir_fail_flag = True - if self.debug_no_dialogue_flag: - - # (If we can't prompt the user, then shut down rather than - # trying again) - return self.main_win_obj.destroy() - - # Part 7 - Create sub-directories - # ------------------------------- - - # Create directories within the main directory directory. On failure, - # show system errors - - # Create the directory for database file backups - if not os.path.isdir(self.backup_dir): - self.make_directory(self.backup_dir) - - # Create the temporary data directories (or empty them, if they already - # exist) - if os.path.isdir(self.temp_dir): - self.remove_directory(self.temp_dir) - - else: - self.make_directory(self.temp_dir) - - if not os.path.isdir(self.temp_dl_dir): - self.make_directory(self.temp_dl_dir) - - if not os.path.isdir(self.temp_test_dir): - self.make_directory(self.temp_test_dir) - - # Part 8 - Load the database file - # ------------------------------- - - # If the database file exists, load it. If not, create it - db_path = os.path.abspath( - os.path.join(self.data_dir, self.db_file_name), - ) - - if os.path.isfile(db_path): - - self.load_db() - - else: - - # New database. First create fixed media data objects (media.Folder - # objects) that can't be removed by the user (though they can be - # hidden) - self.create_fixed_folders() - - # Populate the Video Index - self.main_win_obj.video_index_populate() - - # Create the database file - self.allow_db_save_flag = True - self.save_db() - - # Part 9 - Warn user about failed loads - # ------------------------------------- - - # After a stale lockfile, when the user clicked 'No', just shut down - if self.disable_load_save_lock_flag: - - return self.main_win_obj.destroy() - - # If file load/save has been disabled for any other reason, we can now - # show a dialogue window - elif self.disable_load_save_flag: - - # (If self.show_classic_tab_on_startup_flag is set, then the - # Classic Mode tab is visible. This looks weird, so quickly - # switch back to the Videos tab) - self.main_win_obj.notebook.set_current_page(0) - - if self.disable_load_save_msg is None: - - self.file_error_dialogue( - _( - 'Because of an error, file load/save has been disabled', - ), - ) - - else: - - self.file_error_dialogue( - self.disable_load_save_msg + '\n\n' \ - + _( - 'Because of the error, file load/save has been disabled', - ) - ) - - # Part 10 - Start system timers - # ----------------------------- - - if not self.disable_load_save_flag: - - # Start the script's GObject slow timer - self.script_slow_timer_id = GObject.timeout_add( - self.script_slow_timer_time, - self.script_slow_timer_callback, - ) - - # Start the once-only timer, calling the same function as the slow - # timer - self.script_once_timer_id = GObject.timeout_add( - self.script_once_timer_time, - self.script_slow_timer_callback, - ) - - # Start the script's GObject fast timer - self.script_fast_timer_id = GObject.timeout_add( - self.script_fast_timer_time, - self.script_fast_timer_callback, - ) - - # Part 11 - Restore pending URLs to the Classic Mode tab's textview - # ----------------------------------------------------------------- - - if self.classic_pending_list: - - self.main_win_obj.classic_mode_tab_restore_urls( - self.classic_pending_list, - ) - - self.classic_pending_list = [] - - # Part 12 - Any scheduled download operations should start soon - # ------------------------------------------------------------- - - if not self.disable_load_save_flag: - - # If scheduled download operation(s) are scheduled to occur on or - # some time after startup, then prepare them to start - for scheduled_obj in self.scheduled_list: - - if scheduled_obj.start_mode == 'start': - # (For aesthetic reasons, the scheduled download does not - # start immediately, but a few seconds from now) - scheduled_obj.set_only_time(time.time()) - - elif scheduled_obj.start_mode == 'start_after': - - wait_time = scheduled_obj.wait_value \ - * formats.TIME_METRIC_DICT[scheduled_obj.wait_unit] - - scheduled_obj.set_only_time(time.time() + wait_time) - - # Part 13 - Startup complete - # ------------------------------------- - - # (This flag is necessary, so that Tartube can open in the system tray, - # if settings require that) - self.startup_complete_flag = False - - # Part 14 - Any debug stuff can go here - # ------------------------------------- - - pass - - - def stop(self, dialogue_win=None): - - """Called by self.on_menu_quit() and - mainwin.MainWin.on_quit_menu_item(). - - Before terminating the Tartube app, gets confirmation from the user (if - an operation is in progress). - - If no operation is in progress, calls self.stop_continue() to terminate - the app now. Otherwise, self.stop_continue() is only called when the - clicks the dialogue window's 'Yes' button. - - Args: - - dialogue_win (dialogue.MessageDialogue): Ignored if specified - - """ - - # If a (silent) livestream operation is in progress, we can stop it - # immediately - if self.livestream_manager_obj: - - self.livestream_manager_obj.stop_livestream_operation() - self.stop_continue() - - # If an operation is in progress, get confirmation before stopping - elif self.current_manager_obj: - - ignore_me = _( - 'TRANSLATOR\'S NOTE: In Tartube, there is a small set of' \ - + '\'operations\', each with a unique name. Most operations' \ - + ' use a separate Python file. Two or more operations never' \ - + ' happen simultaneously. You can choose any name you like' \ - + ' for each type of operation; for example, you don\'t need' \ - + ' to translate the Process Operation literally' - ) - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download operations can download videos' \ - + ' or fetch a list of downloadable videos (usually called' \ - + ' \'checking\' the videos)' - ) - ignore_me = _( - 'TRANSLATOR\'S NOTE: Update operations update youtube-dl,' \ - + ' FFmpeg, etc' - ) - ignore_me = _( - 'TRANSLATOR\'S NOTE: Refresh operations examine the files' \ - + ' in Tartube\'s data folder, and update the database' \ - + ' accordingly' - ) - ignore_me = _( - 'TRANSLATOR\'S NOTE: Info operations fetch a list of' \ - + ' available formats/subtitles for a video' - ) - ignore_me = _( - 'TRANSLATOR\'S NOTE: Tidy operations remove or convert' \ - + ' files in Tartube\'s data folder' - ) - ignore_me = _( - 'TRANSLATOR\'S NOTE: Process operations convert videos' \ - + ' using FFmpeg or AVConv' - ) - - if self.download_manager_obj: - string = _('There is a download operation in progress.') - elif self.update_manager_obj: - string = _('There is an update operation in progress.') - elif self.refresh_manager_obj: - string = _('There is a refresh operation in progress.') - elif self.info_manager_obj: - string = _('There is an info operation in progress.') - elif self.tidy_manager_obj: - string = _('There is a tidy operation in progress.') - elif self.process_manager_obj: - string = _('There is a process operation in progress.') - - # If the user clicks 'yes', call self.stop_continue() to complete - # the shutdown - self.dialogue_manager_obj.show_msg_dialogue( - string + ' ' + _('Are you sure you want to quit Tartube?'), - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'stop_continue', - } - ) - - # No confirmation required, so call self.stop_continue() now - else: - self.stop_continue() - - - def stop_continue(self): - - """Called by self.stop() or self.download_manager_finished(). - - Terminates the Tartube app. Forced shutdowns (for example, by clicking - the X in the top corner of the window) are handled by - self.do_shutdown(). - """ - - # (No need to check the livestream operation here - it was stopped in - # the call to self.stop() ) - if self.download_manager_obj: - self.download_manager_obj.stop_download_operation() - - elif self.update_manager_obj: - self.update_manager_obj.stop_update_operation() - - elif self.refresh_manager_obj: - self.refresh_manager_obj.stop_refresh_operation() - - elif self.info_manager_obj: - self.info_manager_obj.stop_info_operation() - - elif self.tidy_manager_obj: - self.tidy_manager_obj.stop_tidy_operation() - - elif self.process_manager_obj: - self.process_manager_obj.stop_process_operation() - - # Stop the GObject timers immediately. So this action is not repeated - # in the standard call to self.do_shutdown, reset the IVs - if self.script_slow_timer_id: - GObject.source_remove(self.script_slow_timer_id) - self.script_slow_timer_id = None - - if self.script_fast_timer_id: - GObject.source_remove(self.script_fast_timer_id) - self.script_fast_timer_id = None - - if self.dl_timer_id: - GObject.source_remove(self.dl_timer_id) - self.dl_timer_id = None - - if self.update_timer_id: - GObject.source_remove(self.update_timer_id) - self.update_timer_id = None - - if self.refresh_timer_id: - GObject.source_remove(self.refresh_timer_id) - self.refresh_timer_id = None - - if self.info_timer_id: - GObject.source_remove(self.info_timer_id) - self.info_timer_id = None - - if self.tidy_timer_id: - GObject.source_remove(self.tidy_timer_id) - self.tidy_timer_id = None - - if self.process_timer_id: - GObject.source_remove(self.process_timer_id) - self.process_timer_id = None - - # Empty any temporary folders from the database (if allowed; those - # temporary folders are always deleted when Tartube starts) - # Otherwise, open the temporary folders on the desktop, if allowd - if self.delete_on_shutdown_flag: - self.delete_temp_folders() - elif self.open_temp_on_desktop_flag: - self.open_temp_folders() - - # Delete Tartube's temporary folder from the filesystem - if os.path.isdir(self.temp_dir): - self.remove_directory(self.temp_dir) - - # Save the config and database files for the final time, and release - # the database lockfile - self.save_config() - self.save_db() - self.remove_db_lock_file() - - # I'm outta here! - self.quit() - - - def system_error(self, error_code, msg): - - """Can be called by anything. - - Wrapper function for mainwin.MainWin.errors_list_add_system_msg(). - - Args: - - error_code (int): An error code in the range 100-999 - - msg (str): A system error message to display in the main window's - Errors List. - - Notes: - - Error codes for this function and for self.system_warning are - currently assigned thus: - - 100-199: mainapp.py (in use: 101-197) - 200-299: mainwin.py (in use: 201-275) - 300-399: downloads.py (in use: 301-318) - 400-499: config.py (in use: 401-406) - 500-599: utils.py (in use: 501-503) - 600-699: info.py (in use: 601) - 700-799: updates.py (in use: 701-704) - 901: Uncaught exceptions, handled by a call to - self.system_exception() - (in use: 901) - - """ - - if self.main_win_obj and self.system_error_show_flag: - GObject.timeout_add( - 0, - self.main_win_obj.errors_list_add_system_msg, - 'error', - error_code, - msg, - ) - - else: - # Emergency fallback: display in the terminal window - print('SYSTEM ERROR ' + str(error_code) + ': ' + msg) - - - def system_warning(self, error_code, msg): - - """Can be called by anything. - - Wrapper function for mainwin.MainWin.errors_list_add_system_msg(). - - Args: - - error_code (int): An error code in the range 100-999. This function - and self.system_error() share the same error codes - - msg (str): A system error message to display in the main window's - Errors List. - - """ - - if self.main_win_obj and self.system_warning_show_flag: - GObject.timeout_add( - 0, - self.main_win_obj.errors_list_add_system_msg, - 'warning', - error_code, - msg, - ) - - else: - # Emergency fallback: display in the terminal window - print('SYSTEM WARNING ' + str(error_code) + ': ' + msg) - - - def system_exception(self, except_type, value, details): - - """Called after an uncaught exception is handled. - - Displays a message in the Errors/Warnings tab, so ordinary users can - actually see it and (hopefully) report it. - - Args: - except_type (type): Exception type - value (TypeError): Exception value - details (str): Exception traceback message - - """ - - self.system_error( - 901, - "Uncaught exception:\n" \ - f"Type: {except_type}\n" \ - f"Value: {value}\n" \ - f"Traceback: {details}", - ) - - - def write_downloader_log(self, msg): - - """Can be called by anything. - - Writes a line of text to the downloader log (a file in Tartube's - data directory). - - Args: - - msg (str): The text to write - - """ - - path = os.path.abspath(os.path.join(self.data_dir, self.ytdl_log_name)) - with open(path, 'a') as outfile: - outfile.write(msg + '\n') - - - # (Config/database files load/save) - - - def load_config(self): - - """Called by self.start() (only). - - Loads the Tartube config file. If loading fails, disables all file - loading/saving. - - Return values: - - True if this appears to be a new Tartube installation, False - otherwise (regardless of whether loading the config file - succeeds, or not) - - """ - - # Define global variables for this function - global _ - - # The config file can be stored at one of two locations, depending on - # whether xdg is available, or not - # v2.0.003 (amended v2.1.034) The user can force Tartube to use the - # config file in the script's directory (rather than the one in the - # location described by xdg) by placing a 'settings.json' file there. - # If that file is created when Tartube is already running, it can be - # an empty file (because Tartube overwrites it). Otherwise, it should - # be a copy of a legitimate config file - config_file_path = self.get_config_path() - - # Sanity check - if self.current_manager_obj \ - or not os.path.isfile(config_file_path) \ - or self.disable_load_save_flag: - - self.disable_load_save( - _( - 'Failed to load the Tartube config file (failed sanity check)', - ), - ) - - # Return False to mark this as not a new installation - return False - - # In case a competing instance of Tartube is saving the same config - # file, check for the lockfile and, if it exists, wait a reasonable - # time for it to be released - if not self.debug_ignore_lockfile_flag: - - lock_path = config_file_path + '.lock' - if os.path.isfile(lock_path): - - check_time = time.time() + self.config_lock_time - while time.time() < check_time and os.path.isfile(lock_path): - time.sleep(0.1) - - if os.path.isfile(lock_path): - - self.disable_load_save( - _( - 'Failed to load the Tartube config file (file is' \ - + ' locked)', - ), - ) - - # Return False to mark this as not a new installation - return False - - # Try to load the config file - try: - with open(config_file_path) as infile: - json_dict = json.load(infile) - - except: - - # If we're loading a config file from the script's own directory, - # then treat it as if it were a blank file, waiting to be - # overwritten by the next call to self.save_config (as described - # above) - if config_file_path == self.config_file_path: - - # A blank file probably means it's a new Tartube installation. - # Return True to mark that, so the calling code can open the - # setup wizard window - return True - - else: - - self.disable_load_save( - _( - 'Failed to load the Tartube config file (JSON load' \ - + ' failure)', - ), - ) - - # Return False to mark this as not a new installation - return False - - # Do some basic checks on the loaded data - if not json_dict \ - or not 'script_name' in json_dict \ - or not 'script_version' in json_dict \ - or not 'save_date' in json_dict \ - or not 'save_time' in json_dict \ - or json_dict['script_name'] != __main__.__packagename__: - - self.disable_load_save( - _( - 'Failed to load the Tartube config file (file is invalid)', - ), - ) - - # Return False to mark this as not a new installation - return False - - # Convert a version, e.g. 1.234.567, into a simple number, e.g. - # 1234567, that can be compared with other versions - version = self.convert_version(json_dict['script_version']) - # Now check that the config file wasn't written by a more recent - # version of Tartube (which this older version might not be able to - # read) - if version is None \ - or version > self.convert_version(__main__.__version__): - - self.disable_load_save( - _( - 'Failed to load the Tartube config file (file cannot be read' \ - + ' by this version)', - ), - ) - - # Return False to mark this as not a new installation - return False - - # Since v1.0.008, config files have identified their file type - if version >= 1000008 \ - and ( - not 'file_type' in json_dict or json_dict['file_type'] != 'config' - ): - self.disable_load_save( - _( - 'Failed to load the Tartube config file (missing file type)', - ), - ) - - # Return False to mark this as not a new installation - return False - - # Load and apply the locale - - # Removed v2.4.103 -# if version >= 2000081: -# self.custom_locale = json_dict['custom_locale'] - if version >= 2004170 and 'override_locale' in json_dict: - self.override_locale = json_dict['override_locale'] - if self.override_locale is None: - self.current_locale = self.system_locale - else: - self.current_locale = self.override_locale - - if self.current_locale != formats.LOCALE_DEFAULT: - self.apply_locale() - - # Set IVs to their new values - - if version >= 2002075 and 'thumb_size_custom' in json_dict: - self.thumb_size_custom = json_dict['thumb_size_custom'] - - if version >= 1004040 and 'main_win_save_size_flag' in json_dict: - self.main_win_save_size_flag = json_dict['main_win_save_size_flag'] - if version >= 2003018 and 'main_win_save_slider_flag' in json_dict: - self.main_win_save_slider_flag \ - = json_dict['main_win_save_slider_flag'] - if version >= 1004040 and 'main_win_save_width' in json_dict: - self.main_win_save_width = json_dict['main_win_save_width'] - self.main_win_save_height = json_dict['main_win_save_height'] - - if version >= 2003018 and 'main_win_videos_slider_posn' in json_dict: - self.main_win_videos_slider_posn \ - = json_dict['main_win_videos_slider_posn'] - self.main_win_progress_slider_posn \ - = json_dict['main_win_progress_slider_posn'] - self.main_win_classic_slider_posn \ - = json_dict['main_win_classic_slider_posn'] - elif version >= 1004040 and 'main_win_save_posn' in json_dict: - # Renamed in v2.3.018 - self.main_win_videos_slider_posn = json_dict['main_win_save_posn'] - - # Removed v2.3.434 -# if version >= 1003122: -# self.gtk_emulate_broken_flag \ -# = json_dict['gtk_emulate_broken_flag'] - - if version >= 2001024 and 'toolbar_hide_flag' in json_dict: - self.toolbar_hide_flag = json_dict['toolbar_hide_flag'] - if version >= 5024 and 'toolbar_squeeze_flag' in json_dict: - self.toolbar_squeeze_flag = json_dict['toolbar_squeeze_flag'] - # (Moved to database file) -# if version >= 2002109: -# self.toolbar_system_hide_flag \ -# = json_dict['toolbar_system_hide_flag'] - if version >= 1001064 and 'show_tooltips_flag' in json_dict: - self.show_tooltips_flag = json_dict['show_tooltips_flag'] - if version >= 2003047 and 'show_tooltips_extra_flag' in json_dict: - self.show_tooltips_extra_flag \ - = json_dict['show_tooltips_extra_flag'] - if version >= 2001036 and 'show_custom_icons_flag' in json_dict: - self.show_custom_icons_flag \ - = json_dict['show_custom_icons_flag'] - if version >= 2003541 and 'show_marker_in_index_flag' in json_dict: - self.show_marker_in_index_flag \ - = json_dict['show_marker_in_index_flag'] - if version >= 2001036 \ - and 'show_small_icons_in_index_flag' in json_dict: - self.show_small_icons_in_index_flag \ - = json_dict['show_small_icons_in_index_flag'] - elif version >= 1001075 and 'show_small_icons_in_index' in json_dict: - self.show_small_icons_in_index_flag \ - = json_dict['show_small_icons_in_index'] - if version >= 1001077 and 'auto_expand_video_index_flag' in json_dict: - self.auto_expand_video_index_flag \ - = json_dict['auto_expand_video_index_flag'] - if version >= 2000014 and 'full_expand_video_index_flag' in json_dict: - self.full_expand_video_index_flag \ - = json_dict['full_expand_video_index_flag'] - if version >= 1001064 and 'disable_dl_all_flag' in json_dict: - self.disable_dl_all_flag = json_dict['disable_dl_all_flag'] - if version >= 2003192 and 'show_custom_dl_button_flag' in json_dict: - self.show_custom_dl_button_flag \ - = json_dict['show_custom_dl_button_flag'] - if version >= 2003560 and 'show_free_space_flag' in json_dict: - self.show_free_space_flag = json_dict['show_free_space_flag'] - if version >= 1004011 and 'show_pretty_dates_flag' in json_dict: - self.show_pretty_dates_flag = json_dict['show_pretty_dates_flag'] - - if version >= 2003397 and 'catalogue_filter_name_flag' in json_dict: - self.catalogue_filter_name_flag \ - = json_dict['catalogue_filter_name_flag'] - self.catalogue_filter_descrip_flag \ - = json_dict['catalogue_filter_descrip_flag'] - self.catalogue_filter_comment_flag \ - = json_dict['catalogue_filter_comment_flag'] - if version >= 2002085 and 'catalogue_draw_frame_flag' in json_dict: - self.catalogue_draw_frame_flag \ - = json_dict['catalogue_draw_frame_flag'] - self.catalogue_draw_icons_flag \ - = json_dict['catalogue_draw_icons_flag'] - if version >= 2003612 \ - and 'catalogue_draw_downloaded_flag' in json_dict: - self.catalogue_draw_downloaded_flag \ - = json_dict['catalogue_draw_downloaded_flag'] - self.catalogue_draw_undownloaded_flag \ - = json_dict['catalogue_draw_undownloaded_flag'] - if version >= 2003481 and 'catalogue_draw_blocked_flag' in json_dict: - self.catalogue_draw_blocked_flag \ - = json_dict['catalogue_draw_blocked_flag'] - if version >= 2003232 \ - and 'catalogue_clickable_container_flag' in json_dict: - self.catalogue_clickable_container_flag \ - = json_dict['catalogue_clickable_container_flag'] - if version >= 2004025 and 'catalogue_show_nickname_flag' in json_dict: - self.catalogue_show_nickname_flag \ - = json_dict['catalogue_show_nickname_flag'] - - if version >= 2004265 and 'drag_video_separator_flag' in json_dict: - self.drag_video_separator_flag \ - = json_dict['drag_video_separator_flag'] - if version >= 2002028 and 'drag_video_path_flag' in json_dict: - self.drag_video_path_flag = json_dict['drag_video_path_flag'] - self.drag_video_source_flag = json_dict['drag_video_source_flag'] - self.drag_video_name_flag = json_dict['drag_video_name_flag'] - if version >= 2004265 and 'drag_video_msg_flag' in json_dict: - self.drag_video_msg_flag = json_dict['drag_video_msg_flag'] - if version >= 2002162 and 'drag_thumb_path_flag' in json_dict: - self.drag_thumb_path_flag = json_dict['drag_thumb_path_flag'] - - if version >= 2004265 and 'drag_error_separator_flag' in json_dict: - self.drag_error_separator_flag \ - = json_dict['drag_error_separator_flag'] - self.drag_error_path_flag = json_dict['drag_error_path_flag'] - self.drag_error_source_flag = json_dict['drag_error_source_flag'] - self.drag_error_name_flag = json_dict['drag_error_name_flag'] - self.drag_error_msg_flag = json_dict['drag_error_msg_flag'] - - if version >= 1003024 and 'show_status_icon_flag' in json_dict: - self.show_status_icon_flag = json_dict['show_status_icon_flag'] - if version >= 2003504 and 'open_in_tray_flag' in json_dict: - self.open_in_tray_flag = json_dict['open_in_tray_flag'] - if version >= 1003024 and 'close_to_tray_flag' in json_dict: - self.close_to_tray_flag = json_dict['close_to_tray_flag'] - if version >= 2003125 and 'restore_posn_from_tray_flag' in json_dict: - self.restore_posn_from_tray_flag \ - = json_dict['restore_posn_from_tray_flag'] - - if version >= 1003129 and 'progress_list_hide_flag' in json_dict: - self.progress_list_hide_flag = json_dict['progress_list_hide_flag'] - if version >= 1000029 and 'results_list_reverse_flag' in json_dict: - self.results_list_reverse_flag \ - = json_dict['results_list_reverse_flag'] - if version >= 2004244 \ - and 'progress_list_remember_width_flag' in json_dict: - self.progress_list_remember_width_flag \ - = json_dict['progress_list_remember_width_flag'] - self.progress_list_width_source \ - = json_dict['progress_list_width_source'] - self.progress_list_width_incoming \ - = json_dict['progress_list_width_incoming'] - self.results_list_width_video \ - = json_dict['results_list_width_video'] - self.classic_progress_list_width_source \ - = json_dict['classic_progress_list_width_source'] - self.classic_progress_list_width_incoming \ - = json_dict['classic_progress_list_width_incoming'] - - if version >= 1003069 and 'system_error_show_flag' in json_dict: - self.system_error_show_flag = json_dict['system_error_show_flag'] - if version >= 6006 and 'system_warning_show_flag' in json_dict: - self.system_warning_show_flag \ - = json_dict['system_warning_show_flag'] - if version >= 1003079 and 'operation_error_show_flag' in json_dict: - self.operation_error_show_flag \ - = json_dict['operation_error_show_flag'] - self.operation_warning_show_flag \ - = json_dict['operation_warning_show_flag'] - if version >= 2003116 and 'system_msg_show_date_flag' in json_dict: - self.system_msg_show_date_flag \ - = json_dict['system_msg_show_date_flag'] - if version >= 2003513 \ - and 'system_msg_show_container_flag' in json_dict: - self.system_msg_show_container_flag \ - = json_dict['system_msg_show_container_flag'] - self.system_msg_show_video_flag \ - = json_dict['system_msg_show_video_flag'] - self.system_msg_show_multi_line_flag \ - = json_dict['system_msg_show_multi_line_flag'] - if version >= 1000007 and 'system_msg_keep_totals_flag' in json_dict: - self.system_msg_keep_totals_flag \ - = json_dict['system_msg_keep_totals_flag'] - - self.data_dir = json_dict['data_dir'] - - if version >= 1004069 and 'data_dir_alt_list' in json_dict: - self.data_dir_alt_list = json_dict['data_dir_alt_list'] - self.data_dir_use_first_flag = json_dict['data_dir_use_first_flag'] - self.data_dir_use_list_flag = json_dict['data_dir_use_list_flag'] - self.data_dir_add_from_list_flag \ - = json_dict['data_dir_add_from_list_flag'] - else: - self.data_dir_alt_list = [ self.data_dir ] - - if version >= 2000069 and 'sound_custom' in json_dict: - self.sound_custom = json_dict['sound_custom'] - - if version >= 2003214 and 'export_csv_separator' in json_dict: - self.export_csv_separator = json_dict['export_csv_separator'] - if version >= 3014: - self.db_backup_mode = json_dict['db_backup_mode'] - - if version >= 2000029 \ - and 'show_classic_tab_on_startup_flag' in json_dict: - self.show_classic_tab_on_startup_flag \ - = json_dict['show_classic_tab_on_startup_flag'] - if version >= 2002164 and 'classic_custom_dl_flag' in json_dict: - self.classic_custom_dl_flag = json_dict['classic_custom_dl_flag'] - if version >= 2000029 and 'classic_dir_list' in json_dict: - self.classic_dir_list = json_dict['classic_dir_list'] - self.classic_dir_previous = json_dict['classic_dir_previous'] - if version >= 2003190 and 'classic_format_selection' in json_dict: - # (Before v2.3.369, this values was stored with leading zeroes) - self.classic_format_selection \ - = utils.strip_whitespace(json_dict['classic_format_selection']) - self.classic_format_convert_flag \ - = json_dict['classic_format_convert_flag'] - if version >= 2003473 and 'classic_resolution_selection' in json_dict: - self.classic_resolution_selection \ - = json_dict['classic_resolution_selection'] - if version >= 2003586 and 'classic_livestream_flag' in json_dict: - self.classic_livestream_flag = json_dict['classic_livestream_flag'] - if version >= 2004332 and 'classic_sblock_flag' in json_dict: - self.classic_sblock_flag = json_dict['classic_sblock_flag'] - if version >= 2002129 and 'classic_pending_flag' in json_dict: - self.classic_pending_flag = json_dict['classic_pending_flag'] - self.classic_pending_list = json_dict['classic_pending_list'] - if version >= 2003046 and 'classic_duplicate_remove_flag' in json_dict: - self.classic_duplicate_remove_flag \ - = json_dict['classic_duplicate_remove_flag'] - - # (In various versions between v0.5.027 and v2.0.097, the youtube - # update IVs were overhauled several times) - self.load_config_ytdl_update(version, json_dict) - - if version >= 2001086 and 'auto_switch_output_flag' in json_dict: - self.auto_switch_output_flag = json_dict['auto_switch_output_flag'] - if version >= 2002043 and 'output_size_default' in json_dict: - self.output_size_default = json_dict['output_size_default'] - self.output_size_apply_flag = json_dict['output_size_apply_flag'] - - if version >= 2001117 and 'ytdl_update_once_flag' in json_dict: - self.ytdl_update_once_flag = json_dict['ytdl_update_once_flag'] - else: - # Don't auto-detect youtube-dl if this installation is not the - # first one (as the user won't be expecting that) - self.ytdl_update_once_flag = True - - if version >= 2003182 and 'ytdl_fork_no_dependency_flag' in json_dict: - self.ytdl_fork_no_dependency_flag \ - = json_dict['ytdl_fork_no_dependency_flag'] - - if version >= 1003074 and 'ytdl_output_system_cmd_flag' in json_dict: - self.ytdl_output_system_cmd_flag \ - = json_dict['ytdl_output_system_cmd_flag'] - if version >= 1002030 and 'ytdl_output_stdout_flag' in json_dict: - self.ytdl_output_stdout_flag = json_dict['ytdl_output_stdout_flag'] - self.ytdl_output_ignore_json_flag \ - = json_dict['ytdl_output_ignore_json_flag'] - self.ytdl_output_ignore_progress_flag \ - = json_dict['ytdl_output_ignore_progress_flag'] - self.ytdl_output_stderr_flag = json_dict['ytdl_output_stderr_flag'] - self.ytdl_output_start_empty_flag \ - = json_dict['ytdl_output_start_empty_flag'] - if version >= 1003064 and 'ytdl_output_show_summary_flag' in json_dict: - self.ytdl_output_show_summary_flag \ - = json_dict['ytdl_output_show_summary_flag'] - - if version >= 1003074 and 'ytdl_write_system_cmd_flag' in json_dict: - self.ytdl_write_system_cmd_flag \ - = json_dict['ytdl_write_system_cmd_flag'] - self.ytdl_write_stdout_flag = json_dict['ytdl_write_stdout_flag'] - if version >= 5004 and 'ytdl_write_ignore_json_flag' in json_dict: - self.ytdl_write_ignore_json_flag \ - = json_dict['ytdl_write_ignore_json_flag'] - if version >= 1002030 \ - and 'ytdl_write_ignore_progress_flag' in json_dict: - self.ytdl_write_ignore_progress_flag \ - = json_dict['ytdl_write_ignore_progress_flag'] - self.ytdl_write_stderr_flag = json_dict['ytdl_write_stderr_flag'] - - if version >= 2004266 and 'ytdl_log_system_cmd_flag' in json_dict: - self.ytdl_log_system_cmd_flag \ - = json_dict['ytdl_log_system_cmd_flag'] - self.ytdl_log_stdout_flag = json_dict['ytdl_log_stdout_flag'] - self.ytdl_log_ignore_json_flag \ - = json_dict['ytdl_log_ignore_json_flag'] - self.ytdl_log_ignore_progress_flag \ - = json_dict['ytdl_log_ignore_progress_flag'] - self.ytdl_log_stderr_flag = json_dict['ytdl_log_stderr_flag'] - - self.ytdl_write_verbose_flag = json_dict['ytdl_write_verbose_flag'] - # Removed v2.3.565 -# if version >= 2002179: -# self.ytsc_write_verbose_flag = json_dict['ytsc_write_verbose_flag'] - - if version >= 1002024 and 'refresh_output_videos_flag' in json_dict: - self.refresh_output_videos_flag \ - = json_dict['refresh_output_videos_flag'] - if version >= 1002027 and 'refresh_output_verbose_flag' in json_dict: - self.refresh_output_verbose_flag \ - = json_dict['refresh_output_verbose_flag'] - if version >= 1003012 and 'refresh_moviepy_timeout' in json_dict: - self.refresh_moviepy_timeout = json_dict['refresh_moviepy_timeout'] - - if version >= 1002030 and 'disk_space_warn_flag' in json_dict: - self.disk_space_warn_flag = json_dict['disk_space_warn_flag'] - self.disk_space_warn_limit = json_dict['disk_space_warn_limit'] - self.disk_space_stop_flag = json_dict['disk_space_stop_flag'] - self.disk_space_stop_limit = json_dict['disk_space_stop_limit'] - if version < 2003556: - # (In this version, values changed from MB to GB) - self.disk_space_warn_limit \ - = round((self.disk_space_warn_limit / 1000), 3) - self.disk_space_stop_limit \ - = round((self.disk_space_stop_limit / 1000), 3) - - if version >= 2001094 and 'custom_invidious_mirror' in json_dict: - self.custom_invidious_mirror = json_dict['custom_invidious_mirror'] - if version >= 2003236 and 'custom_sblock_mirror' in json_dict: - self.custom_sblock_mirror = json_dict['custom_sblock_mirror'] - - # (Moved to database file) -# if version >= 1004024: -# self.custom_dl_by_video_flag \ -# = json_dict['custom_dl_by_video_flag'] -# if version >= 2003155: -# self.custom_dl_split_flag = json_dict['custom_dl_split_flag'] -# if version >= 2003240: -# self.custom_dl_slice_flag = json_dict['custom_dl_slice_flag'] -# self.custom_dl_slice_dict = json_dict['custom_dl_slice_dict'] - # (Moved to database file) -# if version >= 1004052: -# self.custom_dl_divert_mode = json_dict['custom_dl_divert_mode'] -# elif version >= 1004024: -# if json_dict['custom_dl_divert_hooktube_flag']: -# self.custom_dl_divert_mode = 'hooktube' -# if version >= 2001047: -# self.custom_dl_divert_website \ -# = json_dict['custom_dl_divert_website'] -# if version >= 1004024: -# self.custom_dl_delay_flag = json_dict['custom_dl_delay_flag'] -# self.custom_dl_delay_max = json_dict['custom_dl_delay_max'] -# self.custom_dl_delay_min = json_dict['custom_dl_delay_min'] - - if version >= 2003029 and 'dl_proxy_list' in json_dict: - self.dl_proxy_list = json_dict['dl_proxy_list'] - - if version >= 1001054 and 'ffmpeg_path' in json_dict: - self.ffmpeg_path = json_dict['ffmpeg_path'] - if version >= 2001095 and 'avconv_path' in json_dict: - self.avconv_path = json_dict['avconv_path'] - else: - # (Before this version, .ffmpeg_path was used for the avconv binary - # too) - if self.ffmpeg_path is not None \ - and re.search(r'avconv', self.ffmpeg_path): - self.avconv_path = self.ffmpeg_path - self.ffmpeg_path = None - if version >= 2001098 and 'ffmpeg_convert_webp_flag' in json_dict: - self.ffmpeg_convert_webp_flag \ - = json_dict['ffmpeg_convert_webp_flag'] - if version >= 2004148 and 'ffmpeg_retain_webp_flag' in json_dict: - self.ffmpeg_retain_webp_flag \ - = json_dict['ffmpeg_retain_webp_flag'] - - if version >= 2003566 and 'livestream_dl_mode' in json_dict: - self.livestream_dl_mode = json_dict['livestream_dl_mode'] - if version >= 2003619 and 'livestream_dl_timeout' in json_dict: - self.livestream_dl_timeout = json_dict['livestream_dl_timeout'] - if version >= 2003582 and 'livestream_replace_flag' in json_dict: - self.livestream_replace_flag = json_dict['livestream_replace_flag'] - self.livestream_stop_is_final_flag \ - = json_dict['livestream_stop_is_final_flag'] - self.livestream_force_check_flag \ - = json_dict['livestream_force_check_flag'] - - # Removed v2.3.565 -# if version >= 2002178: -# self.ytsc_path = json_dict['ytsc_path'] -# if version >= 2002181: -# self.ytsc_path = json_dict['ytsc_path'] -# self.ytsc_priority_flag = json_dict['ytsc_priority_flag'] -# self.ytsc_wait_time = json_dict['ytsc_wait_time'] -# self.ytsc_restart_max = json_dict['ytsc_restart_max'] - - if version >= 2003570 and 'streamlink_path' in json_dict: - self.streamlink_path = json_dict['streamlink_path'] - - # Removed v2.2.156 -# if version >= 2001104: -# self.ffmpeg_add_string = json_dict['ffmpeg_add_string'] -# self.ffmpeg_regex_string = json_dict['ffmpeg_regex_string'] -# self.ffmpeg_substitute_string \ -# = json_dict['ffmpeg_substitute_string'] -# self.ffmpeg_ext_string = json_dict['ffmpeg_ext_string'] -# self.ffmpeg_option_string = json_dict['ffmpeg_option_string'] -# self.ffmpeg_delete_flag = json_dict['ffmpeg_delete_flag'] -# self.ffmpeg_keep_flag = json_dict['ffmpeg_keep_flag'] - - if version >= 2003067 and 'graph_data_type' in json_dict: - self.graph_data_type = json_dict['graph_data_type'] - self.graph_plot_type = json_dict['graph_plot_type'] - self.graph_time_period_secs = json_dict['graph_time_period_secs'] - self.graph_time_unit_secs = json_dict['graph_time_unit_secs'] - self.graph_ink_colour = json_dict['graph_ink_colour'] - - if version >= 2004263 and 'simple_prefs_flag' in json_dict: - self.simple_prefs_flag = json_dict['simple_prefs_flag'] - else: - self.simple_prefs_flag = self.simple_options_flag - if version >= 1002013 and 'simple_options_flag' in json_dict: - self.simple_options_flag = json_dict['simple_options_flag'] - - if version >= 3029 and 'operation_limit_flag' in json_dict: - self.operation_limit_flag = json_dict['operation_limit_flag'] - self.operation_check_limit = json_dict['operation_check_limit'] - self.operation_download_limit \ - = json_dict['operation_download_limit'] - - if version >= 2003114 and 'show_newbie_dialogue_flag' in json_dict: - self.show_newbie_dialogue_flag \ - = json_dict['show_newbie_dialogue_flag'] - if version >= 2003376 and 'show_msys2_dialogue_flag' in json_dict: - self.show_msys2_dialogue_flag \ - = json_dict['show_msys2_dialogue_flag'] - if version >= 2004062 \ - and 'show_delete_video_dialogue_flag' in json_dict: - self.show_delete_video_dialogue_flag \ - = json_dict['show_delete_video_dialogue_flag'] - self.delete_video_files_flag \ - = json_dict['delete_video_files_flag'] - self.show_delete_container_dialogue_flag \ - = json_dict['show_delete_container_dialogue_flag'] - self.delete_container_files_flag \ - = json_dict['delete_container_files_flag'] - - if version >= 2004013 and 'auto_switch_profile_flag' in json_dict: - self.auto_switch_profile_flag \ - = json_dict['auto_switch_profile_flag'] - - if version >= 1003032 and 'auto_clone_options_flag' in json_dict: - self.auto_clone_options_flag = json_dict['auto_clone_options_flag'] - if version >= 2002116 and 'auto_delete_options_flag' in json_dict: - self.auto_delete_options_flag \ - = json_dict['auto_delete_options_flag'] - - # Removed v2.2.015 -# if version >= 1001067: -# self.scheduled_dl_mode = json_dict['scheduled_dl_mode'] -# self.scheduled_check_mode = json_dict['scheduled_check_mode'] -# -# # Renamed in v2.1.056 -# if 'scheduled_dl_wait_value' in json_dict: -# self.scheduled_dl_wait_value \ -# = json_dict['scheduled_dl_wait_value'] -# self.scheduled_dl_wait_unit \ -# = json_dict['scheduled_dl_wait_unit'] -# self.scheduled_check_wait_value \ -# = json_dict['scheduled_check_wait_value'] -# self.scheduled_check_wait_unit \ -# = json_dict['scheduled_check_wait_unit'] -# else: -# self.scheduled_dl_wait_value \ -# = json_dict['scheduled_dl_wait_hours'] -# self.scheduled_dl_wait_unit = 'hours' -# self.scheduled_check_wait_value \ -# = json_dict['scheduled_check_wait_hours'] -# self.scheduled_check_wait_unit = 'hours' -# -# self.scheduled_dl_last_time \ -# = json_dict['scheduled_dl_last_time'] -# self.scheduled_check_last_time \ -# = json_dict['scheduled_check_last_time'] -# -# # Renamed in v1.3.120 -# if 'scheduled_stop_flag' in json_dict: -# self.scheduled_shutdown_flag \ -# = json_dict['scheduled_stop_flag'] -# else: -# self.scheduled_shutdown_flag \ -# = json_dict['scheduled_shutdown_flag'] -# -# if version >= 2001110: -# self.scheduled_custom_mode = json_dict['scheduled_custom_mode'] -# self.scheduled_custom_wait_value \ -# = json_dict['scheduled_custom_wait_value'] -# self.scheduled_custom_wait_unit \ -# = json_dict['scheduled_custom_wait_unit'] -# self.scheduled_custom_last_time \ -# = json_dict['scheduled_custom_last_time'] - - # Import scheduled downloads created before v2.2.015, and convert them - # to the new media.Scheduled objects - if version < 2002015: - self.load_config_import_scheduled(version, json_dict) - - if version >= 2004085 and 'block_livestreams_flag' in json_dict: - self.block_livestreams_flag = json_dict['block_livestreams_flag'] - if version >= 2000037 and 'enable_livestreams_flag' in json_dict: - self.enable_livestreams_flag = json_dict['enable_livestreams_flag'] - if version >= 2000047 and 'livestream_max_days' in json_dict: - self.livestream_max_days = json_dict['livestream_max_days'] - self.livestream_use_colour_flag \ - = json_dict['livestream_use_colour_flag'] - if version >= 2002204 and 'livestream_simple_colour_flag' in json_dict: - self.livestream_simple_colour_flag \ - = json_dict['livestream_simple_colour_flag'] - if version >= 2000052 and 'livestream_auto_notify_flag' in json_dict: - self.livestream_auto_notify_flag \ - = json_dict['livestream_auto_notify_flag'] - if version >= 2000068 and 'livestream_auto_alarm_flag' in json_dict: - self.livestream_auto_alarm_flag \ - = json_dict['livestream_auto_alarm_flag'] - if version >= 2000052 and 'livestream_auto_open_flag' in json_dict: - self.livestream_auto_open_flag \ - = json_dict['livestream_auto_open_flag'] - if version >= 2000054 and 'livestream_auto_dl_start_flag' in json_dict: - self.livestream_auto_dl_start_flag \ - = json_dict['livestream_auto_dl_start_flag'] - self.livestream_auto_dl_stop_flag \ - = json_dict['livestream_auto_dl_stop_flag'] - if version >= 2000037 and 'scheduled_livestream_flag' in json_dict: - self.scheduled_livestream_flag \ - = json_dict['scheduled_livestream_flag'] - self.scheduled_livestream_wait_mins \ - = json_dict['scheduled_livestream_wait_mins'] - self.scheduled_livestream_last_time \ - = json_dict['scheduled_livestream_last_time'] - if version >= 2002108 \ - and 'scheduled_livestream_extra_flag' in json_dict: - self.scheduled_livestream_extra_flag \ - = json_dict['scheduled_livestream_extra_flag'] - - if version >= 1003112 and 'autostop_time_flag' in json_dict: - self.autostop_time_flag = json_dict['autostop_time_flag'] - self.autostop_time_value = json_dict['autostop_time_value'] - self.autostop_time_unit = json_dict['autostop_time_unit'] - self.autostop_videos_flag = json_dict['autostop_videos_flag'] - self.autostop_videos_value = json_dict['autostop_videos_value'] - self.autostop_size_flag = json_dict['autostop_size_flag'] - self.autostop_size_value = json_dict['autostop_size_value'] - self.autostop_size_unit = json_dict['autostop_size_unit'] - - self.operation_auto_update_flag \ - = json_dict['operation_auto_update_flag'] - self.operation_save_flag = json_dict['operation_save_flag'] - if version >= 1004003 and 'operation_sim_shortcut_flag' in json_dict: - self.operation_sim_shortcut_flag \ - = json_dict['operation_sim_shortcut_flag'] - - if version >= 2002112 and 'operation_auto_restart_flag' in json_dict: - self.operation_auto_restart_flag \ - = json_dict['operation_auto_restart_flag'] - self.operation_auto_restart_time \ - = json_dict['operation_auto_restart_time'] - # Removed v2.3.461 -# if version >= 2003012: -# self.operation_auto_restart_network_flag \ -# = json_dict['operation_auto_restart_network_flag'] - if version >= 2002169 and 'operation_auto_restart_max' in json_dict: - self.operation_auto_restart_max \ - = json_dict['operation_auto_restart_max'] - - # Removed v1.3.028 -# self.operation_dialogue_flag = json_dict['operation_dialogue_flag'] - if version >= 1003028 and 'operation_dialogue_mode' in json_dict: - self.operation_dialogue_mode = json_dict['operation_dialogue_mode'] - if version >= 1003060 and 'operation_convert_mode' in json_dict: - self.operation_convert_mode = json_dict['operation_convert_mode'] - - self.use_module_moviepy_flag = json_dict['use_module_moviepy_flag'] - # Removed v0.5.003 -# self.use_module_validators_flag \ -# = json_dict['use_module_validators_flag'] - - if version >= 1000006 and 'dialogue_copy_clipboard_flag' in json_dict: - self.dialogue_copy_clipboard_flag \ - = json_dict['dialogue_copy_clipboard_flag'] - self.dialogue_keep_open_flag \ - = json_dict['dialogue_keep_open_flag'] - # Removed v1.3.022 -# self.dialogue_keep_container_flag \ -# = json_dict['dialogue_keep_container_flag'] - if version >= 2003130 and 'dialogue_yt_remind_flag' in json_dict: - self.dialogue_yt_remind_flag = json_dict['dialogue_yt_remind_flag'] - - if version >= 2003371 and 'dialogue_disable_msg_flag' in json_dict: - self.dialogue_disable_msg_flag \ - = json_dict['dialogue_disable_msg_flag'] - - if version >= 1003018 and 'allow_ytdl_archive_flag' in json_dict: - self.allow_ytdl_archive_flag \ - = json_dict['allow_ytdl_archive_flag'] - if version >= 2003401 and 'allow_ytdl_archive_mode' in json_dict: - self.allow_ytdl_archive_mode \ - = json_dict['allow_ytdl_archive_mode'] - self.allow_ytdl_archive_path \ - = json_dict['allow_ytdl_archive_path'] - if version >= 2001022 and 'classic_ytdl_archive_flag' in json_dict: - self.classic_ytdl_archive_flag \ - = json_dict['classic_ytdl_archive_flag'] - - if version >= 5004 and 'apply_json_timeout_flag' in json_dict: - self.apply_json_timeout_flag \ - = json_dict['apply_json_timeout_flag'] - if version >= 2003551 and 'json_timeout_no_comments_time' in json_dict: - self.json_timeout_no_comments_time \ - = json_dict['json_timeout_no_comments_time'] - self.json_timeout_with_comments_time \ - = json_dict['json_timeout_with_comments_time'] - if version >= 2001060 and 'track_missing_videos_flag' in json_dict: - self.track_missing_videos_flag \ - = json_dict['track_missing_videos_flag'] - self.track_missing_time_flag \ - = json_dict['track_missing_time_flag'] - self.track_missing_time_days \ - = json_dict['track_missing_time_days'] - if version >= 2003464 and 'add_blocked_videos_flag' in json_dict: - self.add_blocked_videos_flag \ - = json_dict['add_blocked_videos_flag'] - if version >= 2003382 and 'store_playlist_id_flag' in json_dict: - self.store_playlist_id_flag \ - = json_dict['store_playlist_id_flag'] - - if version >= 2003181 \ - and 'video_timestamps_extract_json_flag' in json_dict: - self.video_timestamps_extract_json_flag \ - = json_dict['video_timestamps_extract_json_flag'] - self.video_timestamps_extract_descrip_flag \ - = json_dict['video_timestamps_extract_descrip_flag'] - self.video_timestamps_replace_flag \ - = json_dict['video_timestamps_replace_flag'] - self.video_timestamps_re_extract_flag \ - = json_dict['video_timestamps_re_extract_flag'] - if version >= 2004292 \ - and 'video_timestamps_dl_mode' in json_dict: - self.video_timestamps_dl_mode \ - = json_dict['video_timestamps_dl_mode'] - if version >= 2003181 \ - and 'video_timestamps_extract_json_flag' in json_dict: - self.split_video_name_mode = json_dict['split_video_name_mode'] - self.split_video_clips_dir_flag \ - = json_dict['split_video_clips_dir_flag'] - self.split_video_subdir_flag = json_dict['split_video_subdir_flag'] - self.split_video_add_db_flag = json_dict['split_video_add_db_flag'] - self.split_video_copy_thumb_flag \ - = json_dict['split_video_copy_thumb_flag'] - self.split_video_custom_title \ - = json_dict['split_video_custom_title'] - if version >= 2004328 \ - and 'split_video_force_keyframe_flag' in json_dict: - self.split_video_force_keyframe_flag \ - = json_dict['split_video_force_keyframe_flag'] - if version >= 2003181 \ - and 'video_timestamps_extract_json_flag' in json_dict: - self.split_video_auto_open_flag \ - = json_dict['split_video_auto_open_flag'] - self.split_video_auto_delete_flag \ - = json_dict['split_video_auto_delete_flag'] - - if version >= 2003236 and 'sblock_fetch_flag' in json_dict: - self.sblock_fetch_flag = json_dict['sblock_fetch_flag'] - self.sblock_obfuscate_flag = json_dict['sblock_obfuscate_flag'] - if version >= 2003257 and 'sblock_replace_flag' in json_dict: - self.sblock_replace_flag = json_dict['sblock_replace_flag'] - self.sblock_re_extract_flag = json_dict['sblock_re_extract_flag'] - if version >= 2004329 \ - and 'slice_video_force_keyframe_flag' in json_dict: - self.slice_video_force_keyframe_flag \ - = json_dict['slice_video_force_keyframe_flag'] - if version >= 2003250 and 'slice_video_cleanup_flag' in json_dict: - self.slice_video_cleanup_flag \ - = json_dict['slice_video_cleanup_flag'] - - if version > 2003316 \ - and version < 2003552 \ - and 'comment_fetch_flag' in json_dict: - self.check_comment_fetch_flag = json_dict['comment_fetch_flag'] - self.dl_comment_fetch_flag = json_dict['comment_fetch_flag'] - elif version >= 2003552 and 'check_comment_fetch_flag' in json_dict: - self.check_comment_fetch_flag \ - = json_dict['check_comment_fetch_flag'] - self.dl_comment_fetch_flag = json_dict['dl_comment_fetch_flag'] - if version >= 2003316 and 'comment_store_flag' in json_dict: - self.comment_store_flag = json_dict['comment_store_flag'] - if version >= 2003318 and 'comment_show_text_time_flag' in json_dict: - self.comment_show_text_time_flag \ - = json_dict['comment_show_text_time_flag'] - if version >= 2003319 and 'comment_show_formatted_flag' in json_dict: - self.comment_show_formatted_flag \ - = json_dict['comment_show_formatted_flag'] - - if version >= 5004 and 'ignore_child_process_exit_flag' in json_dict: - self.ignore_child_process_exit_flag \ - = json_dict['ignore_child_process_exit_flag'] - if version >= 1003088 and 'ignore_http_404_error_flag' in json_dict: - self.ignore_http_404_error_flag \ - = json_dict['ignore_http_404_error_flag'] - self.ignore_data_block_error_flag \ - = json_dict['ignore_data_block_error_flag'] - if version >= 1027 and 'ignore_merge_warning_flag' in json_dict: - self.ignore_merge_warning_flag \ - = json_dict['ignore_merge_warning_flag'] - if version >= 1003088 \ - and 'ignore_missing_format_error_flag' in json_dict: - self.ignore_missing_format_error_flag \ - = json_dict['ignore_missing_format_error_flag'] - if version >= 1001077 and 'ignore_no_annotations_flag' in json_dict: - self.ignore_no_annotations_flag \ - = json_dict['ignore_no_annotations_flag'] - if version >= 1002004 and 'ignore_no_subtitles_flag' in json_dict: - self.ignore_no_subtitles_flag \ - = json_dict['ignore_no_subtitles_flag'] - if version >= 2003340 and 'ignore_page_given_flag' in json_dict: - self.ignore_page_given_flag = json_dict['ignore_page_given_flag'] - self.ignore_no_descrip_flag = json_dict['ignore_no_descrip_flag'] - if version >= 2003403 and 'ignore_thumb_404_flag' in json_dict: - self.ignore_thumb_404_flag = json_dict['ignore_thumb_404_flag'] - - if version >= 5004 and 'ignore_yt_copyright_flag' in json_dict: - self.ignore_yt_copyright_flag \ - = json_dict['ignore_yt_copyright_flag'] - if version >= 1003084 and 'ignore_yt_age_restrict_flag' in json_dict: - self.ignore_yt_age_restrict_flag \ - = json_dict['ignore_yt_age_restrict_flag'] - if version >= 1003088 \ - and 'ignore_yt_uploader_deleted_flag' in json_dict: - self.ignore_yt_uploader_deleted_flag \ - = json_dict['ignore_yt_uploader_deleted_flag'] - if version >= 2002025 and 'ignore_yt_payment_flag' in json_dict: - self.ignore_yt_payment_flag \ - = json_dict['ignore_yt_payment_flag'] - - if version >= 1003090 and 'ignore_custom_msg_list' in json_dict: - self.ignore_custom_msg_list \ - = json_dict['ignore_custom_msg_list'] - self.ignore_custom_regex_flag \ - = json_dict['ignore_custom_regex_flag'] - - self.num_worker_default = json_dict['num_worker_default'] - self.num_worker_apply_flag = json_dict['num_worker_apply_flag'] - if version >= 2002184 and 'num_worker_bypass_flag' in json_dict: - self.num_worker_bypass_flag = json_dict['num_worker_bypass_flag'] - - self.bandwidth_default = json_dict['bandwidth_default'] - self.bandwidth_apply_flag = json_dict['bandwidth_apply_flag'] - - if version >= 1002011 and 'video_res_default' in json_dict: - self.video_res_default = json_dict['video_res_default'] - self.video_res_apply_flag = json_dict['video_res_apply_flag'] - - if version >= 2003117 and 'alt_num_worker' in json_dict: - self.alt_num_worker = json_dict['alt_num_worker'] - self.alt_num_worker_apply_flag \ - = json_dict['alt_num_worker_apply_flag'] - self.alt_bandwidth = json_dict['alt_bandwidth'] - self.alt_bandwidth_apply_flag \ - = json_dict['alt_bandwidth_apply_flag'] - self.alt_start_time = json_dict['alt_start_time'] - self.alt_stop_time = json_dict['alt_stop_time'] - self.alt_day_string = json_dict['alt_day_string'] - - self.match_method = json_dict['match_method'] - self.match_first_chars = json_dict['match_first_chars'] - self.match_ignore_chars = json_dict['match_ignore_chars'] - if version >= 2004169 and 'match_nickname_flag' in json_dict: - self.match_nickname_flag = json_dict['match_nickname_flag'] - - if version >= 1001029 and 'auto_delete_flag' in json_dict: - self.auto_delete_flag = json_dict['auto_delete_flag'] - self.auto_delete_days = json_dict['auto_delete_days'] - if version >= 2003609 and 'auto_remove_flag' in json_dict: - self.auto_remove_flag = json_dict['auto_remove_flag'] - self.auto_remove_days = json_dict['auto_remove_days'] - if version >= 1001029 and 'auto_delete_watched_flag' in json_dict: - self.auto_delete_watched_flag \ - = json_dict['auto_delete_watched_flag'] - if version >= 2003610 and 'auto_delete_asap_flag' in json_dict: - self.auto_delete_asap_flag = json_dict['auto_delete_asap_flag'] - - if version >= 1002041 and 'delete_on_shutdown_flag' in json_dict: - self.delete_on_shutdown_flag = json_dict['delete_on_shutdown_flag'] - if version >= 1004027 and 'open_temp_on_desktop_flag' in json_dict: - self.open_temp_on_desktop_flag \ - = json_dict['open_temp_on_desktop_flag'] - - self.complex_index_flag = json_dict['complex_index_flag'] - if version >= 3019 and 'catalogue_mode' in json_dict: - self.catalogue_mode = json_dict['catalogue_mode'] - if version >= 2002069 and 'catalogue_mode_type' in json_dict: - self.catalogue_mode_type = json_dict['catalogue_mode_type'] - if version >= 3023 and 'catalogue_page_size' in json_dict: - self.catalogue_page_size = json_dict['catalogue_page_size'] - if version >= 1004005 and 'catalogue_show_filter_flag' in json_dict: - self.catalogue_show_filter_flag \ - = json_dict['catalogue_show_filter_flag'] - # Removed v2.4.194; now stored in the database file -# if version >= 1004005 and version < 2002159: -# catalogue_alpha_sort_flag = json_dict['catalogue_alpha_sort_flag'] -# if not catalogue_alpha_sort_flag: -# self.catalogue_sort_mode = 'default' -# else: -# self.catalogue_sort_mode = 'alpha' -# elif version >= 2002159: -# self.catalogue_sort_mode = json_dict['catalogue_sort_mode'] - if version >= 1004005: - if 'catologue_use_regex_flag' in json_dict: - self.catalogue_use_regex_flag \ - = json_dict['catologue_use_regex_flag'] - # (Typo corrected in v2.4.250) - elif 'catalogue_use_regex_flag' in json_dict: - self.catalogue_use_regex_flag \ - = json_dict['catalogue_use_regex_flag'] - - if version >= 2003129 and 'url_change_confirm_flag' in json_dict: - self.url_change_confirm_flag = json_dict['url_change_confirm_flag'] - self.url_change_regex_flag = json_dict['url_change_regex_flag'] - - if version >= 2003195 and 'custom_bg_table' in json_dict: - self.custom_bg_table = json_dict['custom_bg_table'] - if version < 2003537: - # (New key-value pairs added) - for key in [ - 'drag_drop_notify', 'drag_drop_odd', 'drag_drop_even', - ]: - self.custom_bg_table[key] = self.default_bg_table[key] - - if version >= 2003230 and 'ytdlp_filter_options_flag' in json_dict: - self.ytdlp_filter_options_flag \ - = json_dict['ytdlp_filter_options_flag'] - - # Having loaded the config file, set various file paths... - if self.data_dir_use_first_flag and self.data_dir_alt_list: - self.data_dir = self.data_dir_alt_list[0] - - self.update_data_dirs() - - # Set custom background colours for the Video Catalogue - for key in self.custom_bg_table: - self.main_win_obj.setup_bg_colour(key) - - # If the most-recently selected directory, self.classic_dir_previous, - # still exists in self.classic_dir_list, move it to the top, so it's - # the first item displayed in the combo - if self.classic_dir_previous is not None \ - and self.classic_dir_previous in self.classic_dir_list: - - self.classic_dir_list.remove(self.classic_dir_previous) - self.classic_dir_list.insert(0, self.classic_dir_previous) - - # In either case, we don't need to remember the previous session's - # destination directory any more - self.classic_dir_previous = None - - # Return False to mark this as not a new installation - return False - - - def load_config_ytdl_update(self, version, json_dict): - - """"Called by self.load_config(). - - The IVs handling youtube-dl updates have been overhauled several - times. - - To keep the layout of self.load_config() reasonable, this function is - called to import the IVs from the loaded config file, and update them - as appropriate. - - Args: - - version (int): The config file's Tartube version, converted to a - simple integer in a call to self.convert_version() - - json_dict: The data loaded from the config file - - """ - - # (In version v0.5.027, the value of these IVs were overhauled. If - # loading from an earlier config file, replace those values with the - # new default values) - if version >= 5027 and 'ytdl_bin' in json_dict: - self.ytdl_bin = json_dict['ytdl_bin'] - self.ytdl_path_default = json_dict['ytdl_path_default'] - self.ytdl_path = json_dict['ytdl_path'] - self.ytdl_update_dict = json_dict['ytdl_update_dict'] - self.ytdl_update_list = json_dict['ytdl_update_list'] - self.ytdl_update_current = json_dict['ytdl_update_current'] - - # (In version v1.3.903, these IVs were modified a little, but not - # on MS Windows) - if os.name != 'nt' and version <= 1003090: - self.ytdl_update_dict['Update using pip3 (recommended)'] \ - = ['pip3', 'install', '--upgrade', '--user', 'youtube-dl'] - self.ytdl_update_dict['Update using pip3 (omit --user option)'] \ - = ['pip3', 'install', '--upgrade', 'youtube-dl'] - self.ytdl_update_dict['Update using pip'] \ - = ['pip', 'install', '--upgrade', '--user', 'youtube-dl'] - self.ytdl_update_dict['Update using pip (omit --user option)'] \ - = ['pip', 'install', '--upgrade', 'youtube-dl'] - self.ytdl_update_list = [ - 'Update using pip3 (recommended)', - 'Update using pip3 (omit --user option)', - 'Update using pip', - 'Update using pip (omit --user option)', - 'Update using default youtube-dl path', - 'Update using local youtube-dl path', - ] - - # (In version v1.5.012, these IVs were modified a little, but not on - # MS Windows) - if os.name != 'nt' and version <= 1005012: - self.ytdl_update_dict['Update using PyPI youtube-dl path'] \ - = [self.ytdl_path_pypi, '-U'] - self.ytdl_update_list.append('Update using PyPI youtube-dl path') - - - # (In version v2.0.086, these IVs were completely overhauled on all - # operating systems) - if version < 2000096: - - update_dict = { - 'Update using default youtube-dl path': - 'ytdl_update_default_path', - 'Update using local youtube-dl path': - 'ytdl_update_local_path', - 'Update using pip': - 'ytdl_update_pip', - 'Update using pip (omit --user option)': - 'ytdl_update_pip_omit_user', - 'Update using pip3': - 'ytdl_update_pip3', - 'Update using pip3 (omit --user option)': - 'ytdl_update_pip3_omit_user', - 'Update using pip3 (recommended)': - 'ytdl_update_pip3_recommend', - 'Update using PyPI youtube-dl path': - 'ytdl_update_pypi_path', - 'Windows 32-bit update (recommended)': - 'ytdl_update_win_32', - 'Windows 64-bit update (recommended)': - 'ytdl_update_win_64', - 'youtube-dl updates are disabled': - 'ytdl_update_disabled', - } - - ytdl_update_dict = {} - for key in self.ytdl_update_dict: - ytdl_update_dict[update_dict[key]] = self.ytdl_update_dict[key] - - self.ytdl_update_dict = ytdl_update_dict - - ytdl_update_list = [] - for item in self.ytdl_update_list: - ytdl_update_list.append(update_dict[item]) - - self.ytdl_update_list = ytdl_update_list - - self.ytdl_update_current = update_dict[self.ytdl_update_current] - - # (In version v2.0.109, the directory location used by tartube_mswin.sh - # was changed) - if version < 2000109 and os.name == 'nt': - - if 'PROGRAMFILES(X86)' in os.environ: - recommended = 'ytdl_update_win_64' - else: - recommended = 'ytdl_update_win_32' - - recommended_list = self.ytdl_update_dict[recommended] - mod_list = [] - - for item in recommended_list: - mod_list.append(re.sub(r'^..\\', '', item)) - - self.ytdl_update_dict[recommended] = mod_list - - # (In version v2.1.083, added support for youtube-dl forks) - if version >= 2001083 and 'ytdl_fork' in json_dict: - - self.ytdl_fork = json_dict['ytdl_fork'] - - # (In version v2.3.082, these IVs were modified a little on all - # systems) - if version < 2003082: - - self.ytdl_update_dict['ytdl_update_custom_path'] \ - = ['python3', self.ytdl_path, '-U'] - - self.ytdl_update_list.insert( - (len(self.ytdl_update_list) - 1), - 'ytdl_update_custom_path', - ) - - elif 'ytdl_path_custom_flag' in json_dict: - - self.ytdl_path_custom_flag = json_dict['ytdl_path_custom_flag'] - - # (In version v2.3.183, self.ytdl_update_dict was modified again) - if version < 2003183: - - self.ytdl_update_dict['ytdl_update_pip_no_dependencies'] = [ - 'pip', 'install', '--upgrade', '--no-dependencies', - 'youtube-dl', - ] - - self.ytdl_update_dict['ytdl_update_pip3_no_dependencies'] = [ - 'pip3', 'install', '--upgrade', '--no-dependencies', - 'youtube-dl', - ] - - ytdl_update_list = [] - for item in self.ytdl_update_list: - - if item == 'ytdl_update_pip': - ytdl_update_list.append('ytdl_update_pip_no_dependencies') - elif item == 'ytdl_update_pip3': - ytdl_update_list.append('ytdl_update_pip3_no_dependencies') - - ytdl_update_list.append(item) - - self.ytdl_update_list = ytdl_update_list - - # (In version v2.3.277, self.ytdl_update_dict was modified yet again) - if version < 2003277: - - if os.name == 'nt' and 'PROGRAMFILES(X86)' in os.environ: - - self.ytdl_update_dict['ytdl_update_win_64_no_dependencies'] = [ - '..\\..\\..\\mingw64\\bin\python3.exe', - '..\\..\\..\\mingw64\\bin\pip3-script.py', - 'install', - '--upgrade', - '--no-dependencies', - 'youtube-dl', - ] - - elif os.name == 'nt' and not 'PROGRAMFILES(X86)' in os.environ: - - self.ytdl_update_dict['ytdl_update_win_64_no_dependencies'] = [ - '..\\..\\..\\mingw32\\bin\python3.exe', - '..\\..\\..\\mingw32\\bin\pip3-script.py', - 'install', - '-upgrade', - '--no-dependencies', - 'youtube-dl', - ] - - ytdl_update_list = [] - for item in self.ytdl_update_list: - - ytdl_update_list.append(item) - if item == 'ytdl_update_win_64': - ytdl_update_list.append( - 'ytdl_update_win_64_no_dependencies', - ) - - elif item == 'ytdl_update_win_32': - ytdl_update_list.append( - 'ytdl_update_win_32_no_dependencies', - ) - - self.ytdl_update_list = ytdl_update_list - - - def load_config_import_scheduled(self, version, json_dict): - - """"Called by self.load_config(). - - Since v2.2.015, scheduled downloads have been handled by - media.Scheduled objects. stored in the database file. Before that, they - were handled by a set of IVs stored in the config file. - - This function is called when reading a config file for earlier - versions. It extracts the values of the old scheduled download IVs, - and then converts them to media.Scheduled objects (so any scheduled - downloads will happen as normal, without the user needing to do - anything). - - Args: - - version (int): The config file's Tartube version, converted to a - simple integer in a call to self.convert_version() - - json_dict: The data loaded from the config file - - """ - - # Set up variables whose values are the default values of the old IVs - scheduled_check_mode = 'disabled' - scheduled_check_wait_value = 2 - scheduled_check_wait_unit = 'hours' - scheduled_check_last_time = 0 - - scheduled_dl_mode = 'disabled' - scheduled_dl_wait_value = 2 - scheduled_dl_wait_unit = 'hours' - scheduled_dl_last_time = 0 - - scheduled_custom_mode = 'disabled' - scheduled_custom_wait_value = 2 - scheduled_custom_wait_unit = 'hours' - scheduled_custom_last_time = 0 - - scheduled_shutdown_flag = False - - # Now update those values from the config file - if version >= 1001067 and 'scheduled_dl_mode' in json_dict: - scheduled_dl_mode = json_dict['scheduled_dl_mode'] - scheduled_check_mode = json_dict['scheduled_check_mode'] - - # Renamed in v2.1.056 - if 'scheduled_dl_wait_value' in json_dict: - scheduled_dl_wait_value = json_dict['scheduled_dl_wait_value'] - scheduled_dl_wait_unit = json_dict['scheduled_dl_wait_unit'] - scheduled_check_wait_value \ - = json_dict['scheduled_check_wait_value'] - scheduled_check_wait_unit \ - = json_dict['scheduled_check_wait_unit'] - elif 'scheduled_dl_wait_hours' in json_dict: - scheduled_dl_wait_value = json_dict['scheduled_dl_wait_hours'] - scheduled_dl_wait_unit = 'hours' - scheduled_check_wait_value \ - = json_dict['scheduled_check_wait_hours'] - scheduled_check_wait_unit = 'hours' - - scheduled_dl_last_time = json_dict['scheduled_dl_last_time'] - scheduled_check_last_time = json_dict['scheduled_check_last_time'] - - # Renamed in v1.3.120 - if 'scheduled_stop_flag' in json_dict: - scheduled_shutdown_flag = json_dict['scheduled_stop_flag'] - elif 'scheduled_shutdown_flag' in json_dict: - scheduled_shutdown_flag = json_dict['scheduled_shutdown_flag'] - - if version >= 2001110 and 'scheduled_custom_mode' in json_dict: - scheduled_custom_mode = json_dict['scheduled_custom_mode'] - scheduled_custom_wait_value \ - = json_dict['scheduled_custom_wait_value'] - scheduled_custom_wait_unit \ - = json_dict['scheduled_custom_wait_unit'] - scheduled_custom_last_time \ - = json_dict['scheduled_custom_last_time'] - - # v2.3.467, changes to the values of some media.Scheduled IVs - if scheduled_check_mode == 'none': - scheduled_check_mode = 'disabled' - elif scheduled_check_mode == 'scheduled': - scheduled_check_mode = 'repeat' - - if scheduled_dl_mode == 'none': - scheduled_dl_mode = 'disabled' - elif scheduled_dl_mode == 'scheduled': - scheduled_dl_mode = 'repeat' - - if scheduled_custom_mode == 'none': - scheduled_custom_mode = 'disabled' - elif scheduled_custom_mode == 'scheduled': - scheduled_custom_mode = 'repeat' - - # Finally create new media.Scheduled objects - if scheduled_check_mode != 'disabled': - - new_obj = media.Scheduled( - 'default_check', - 'sim', - scheduled_check_mode, - ) - - new_obj.wait_value = scheduled_check_wait_value - new_obj.wait_unit = scheduled_check_wait_unit - new_obj.last_time = scheduled_check_last_time - new_obj.shutdown_flag = scheduled_shutdown_flag - - self.scheduled_list.append(new_obj) - - if scheduled_dl_mode != 'disabled': - - new_obj = media.Scheduled( - 'default_download', - 'real', - scheduled_dl_mode, - ) - - new_obj.wait_value = scheduled_dl_wait_value - new_obj.wait_unit = scheduled_dl_wait_unit - new_obj.last_time = scheduled_dl_last_time - new_obj.shutdown_flag = scheduled_shutdown_flag - - self.scheduled_list.append(new_obj) - - if scheduled_custom_mode != 'disabled': - - new_obj = media.Scheduled( - 'default_custom', - 'custom_real', - scheduled_custom_mode, - ) - - new_obj.wait_value = scheduled_custom_wait_value - new_obj.wait_unit = scheduled_custom_wait_unit - new_obj.last_time = scheduled_custom_last_time - new_obj.shutdown_flag = scheduled_shutdown_flag - - self.scheduled_list.append(new_obj) - - - def save_config(self): - - """Called by self.start(), .stop_continue(), switch_db(), - .download_manager_finished(), .update_manager_finished(), - .refresh_manager_finished(), .info_manager_finished(), - .tidy_manager_finished(), .on_menu_save_all(), - - Saves the Tartube config file. If saving fails, disables all file - loading/saving. - """ - - # The config file can be stored at one of two locations, depending on - # whether xdg is available, or not - # v2.0.003 (amended v2.1.034) The user can force Tartube to use the - # config file in the script's directory (rather than the one in the - # location described by xdg) by placing a 'settings.json' file there. - # If that file is created when Tartube is already running, it can be - # an empty file (because Tartube overwrites it). Otherwise, it should - # be a copy of a legitimate config file - config_file_path = self.get_config_path() - - # Sanity check - if self.current_manager_obj or self.disable_load_save_flag: - - # When called from self.start(), no main window object exists - # yet, and so Tartube will be shut down with this error message - # When called from anything else, throughout this function the - # response is different - if not self.main_win_obj: - self.disable_load_save( - _( - 'Failed to save the Tartube config file (failed sanity' \ - + ' check)', - ), - ) - - return - - # Prepare values - local = utils.get_local_time() - - # Remember the size of the main window, and the positions of sliders in - # the Videos, Progress and Classic Mode tabs, if required - # The minimum saveable size for the main window is half the standard - # size. There is no minimum saveable size for the paneds (but the - # sliders cannot be reduce to nothing anyway) - if self.main_win_obj and self.main_win_save_size_flag: - (width, height) = self.main_win_obj.get_size() - - if width >= int(self.main_win_width / 2): - self.main_win_save_width = width - else: - self.main_win_save_width = self.main_win_width - - if height >= int(self.main_win_height / 2): - self.main_win_save_height = height - else: - self.main_win_save_height = self.main_win_height - - if self.main_win_save_slider_flag: - - self.main_win_videos_slider_posn \ - = self.main_win_obj.videos_paned.get_position() - - self.main_win_progress_slider_posn \ - = self.main_win_obj.progress_paned.get_position() - - self.main_win_classic_slider_posn \ - = self.main_win_obj.classic_paned.get_position() - - # Remember the size of various columns in the Progress List, Results - # List and Classic Progress List, if required - if self.main_win_obj and self.progress_list_remember_width_flag: - - self.progress_list_width_source, \ - self.progress_list_width_incoming \ - = self.main_win_obj.progress_list_get_column_widths() - - self.results_list_width_video \ - = self.main_win_obj.results_list_get_column_widths() - - self.classic_progress_list_width_source, \ - self.classic_progress_list_width_incoming \ - = self.main_win_obj.classic_mode_tab_get_column_widths() - - # If the user wants to recover undownloaded URLs from the Classic Mode - # tab when Tartube restarts, then compile that list of URLs now - if self.classic_pending_flag: - - self.classic_pending_list \ - = self.main_win_obj.classic_mode_tab_extract_pending_urls() - - else: - - self.classic_pending_list = [] - - # Prepare a dictionary of data to save as a JSON file - json_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(local.strftime('%d %b %Y')), - 'save_time': str(local.strftime('%H:%M:%S')), - 'file_type': 'config', - # Data - 'override_locale': self.override_locale, - - 'thumb_size_custom': self.thumb_size_custom, - - 'main_win_save_size_flag': self.main_win_save_size_flag, - 'main_win_save_slider_flag': self.main_win_save_slider_flag, - 'main_win_save_width': self.main_win_save_width, - 'main_win_save_height': self.main_win_save_height, - 'main_win_videos_slider_posn': self.main_win_videos_slider_posn, - 'main_win_progress_slider_posn': \ - self.main_win_progress_slider_posn, - 'main_win_classic_slider_posn': self.main_win_classic_slider_posn, - - 'toolbar_hide_flag': self.toolbar_hide_flag, - 'toolbar_squeeze_flag': self.toolbar_squeeze_flag, - 'show_tooltips_flag': self.show_tooltips_flag, - 'show_tooltips_extra_flag': self.show_tooltips_extra_flag, - 'show_custom_icons_flag': self.show_custom_icons_flag, - 'show_marker_in_index_flag': self.show_marker_in_index_flag, - 'show_small_icons_in_index_flag': \ - self.show_small_icons_in_index_flag, - 'auto_expand_video_index_flag': self.auto_expand_video_index_flag, - 'full_expand_video_index_flag': self.full_expand_video_index_flag, - 'disable_dl_all_flag': self.disable_dl_all_flag, - 'show_custom_dl_button_flag': self.show_custom_dl_button_flag, - 'show_free_space_flag': self.show_free_space_flag, - 'show_pretty_dates_flag': self.show_pretty_dates_flag, - - 'catalogue_filter_name_flag': self.catalogue_filter_name_flag, - 'catalogue_filter_descrip_flag': \ - self.catalogue_filter_descrip_flag, - 'catalogue_filter_comment_flag': \ - self.catalogue_filter_comment_flag, - 'catalogue_draw_frame_flag': self.catalogue_draw_frame_flag, - 'catalogue_draw_icons_flag': self.catalogue_draw_icons_flag, - 'catalogue_draw_downloaded_flag': \ - self.catalogue_draw_downloaded_flag, - 'catalogue_draw_undownloaded_flag': \ - self.catalogue_draw_undownloaded_flag, - 'catalogue_draw_blocked_flag': self.catalogue_draw_blocked_flag, - 'catalogue_clickable_container_flag': \ - self.catalogue_clickable_container_flag, - 'catalogue_show_nickname_flag': self.catalogue_show_nickname_flag, - - 'drag_video_separator_flag': self.drag_video_separator_flag, - 'drag_video_path_flag': self.drag_video_path_flag, - 'drag_video_source_flag': self.drag_video_source_flag, - 'drag_video_name_flag': self.drag_video_name_flag, - 'drag_video_msg_flag': self.drag_video_msg_flag, - 'drag_thumb_path_flag': self.drag_thumb_path_flag, - - 'drag_error_separator_flag': self.drag_error_separator_flag, - 'drag_error_path_flag': self.drag_error_path_flag, - 'drag_error_source_flag': self.drag_error_source_flag, - 'drag_error_name_flag': self.drag_error_name_flag, - 'drag_error_msg_flag': self.drag_error_msg_flag, - - 'show_status_icon_flag': self.show_status_icon_flag, - 'open_in_tray_flag': self.open_in_tray_flag, - 'close_to_tray_flag': self.close_to_tray_flag, - 'restore_posn_from_tray_flag': self.restore_posn_from_tray_flag, - - 'progress_list_hide_flag': self.progress_list_hide_flag, - 'results_list_reverse_flag': self.results_list_reverse_flag, - 'progress_list_remember_width_flag': \ - self.progress_list_remember_width_flag, - 'progress_list_width_source': self.progress_list_width_source, - 'progress_list_width_incoming': self.progress_list_width_incoming, - 'results_list_width_video': self.results_list_width_video, - 'classic_progress_list_width_source': \ - self.classic_progress_list_width_source, - 'classic_progress_list_width_incoming': \ - self.classic_progress_list_width_incoming, - - 'system_error_show_flag': self.system_error_show_flag, - 'system_warning_show_flag': self.system_warning_show_flag, - 'operation_error_show_flag': self.operation_error_show_flag, - 'operation_warning_show_flag': self.operation_warning_show_flag, - 'system_msg_show_date_flag': self.system_msg_show_date_flag, - 'system_msg_show_container_flag': \ - self.system_msg_show_container_flag, - 'system_msg_show_video_flag': self.system_msg_show_video_flag, - 'system_msg_show_multi_line_flag': \ - self.system_msg_show_multi_line_flag, - 'system_msg_keep_totals_flag': self.system_msg_keep_totals_flag, - - 'data_dir': self.data_dir, - 'data_dir_alt_list': self.data_dir_alt_list, - 'data_dir_use_first_flag': self.data_dir_use_first_flag, - 'data_dir_use_list_flag': self.data_dir_use_list_flag, - 'data_dir_add_from_list_flag': self.data_dir_add_from_list_flag, - - 'sound_custom': self.sound_custom, - - 'export_csv_separator': self.export_csv_separator, - 'db_backup_mode': self.db_backup_mode, - - 'show_classic_tab_on_startup_flag': \ - self.show_classic_tab_on_startup_flag, - 'classic_custom_dl_flag': self.classic_custom_dl_flag, - 'classic_dir_list': self.classic_dir_list, - 'classic_dir_previous': self.classic_dir_previous, - 'classic_format_selection': self.classic_format_selection, - 'classic_format_convert_flag': self.classic_format_convert_flag, - 'classic_resolution_selection': self.classic_resolution_selection, - 'classic_livestream_flag': self.classic_livestream_flag, - 'classic_sblock_flag': self.classic_sblock_flag, - 'classic_pending_flag': self.classic_pending_flag, - 'classic_pending_list': self.classic_pending_list, - 'classic_duplicate_remove_flag': \ - self.classic_duplicate_remove_flag, - - 'ytdl_bin': self.ytdl_bin, - 'ytdl_path_default': self.ytdl_path_default, - 'ytdl_path': self.ytdl_path, - 'ytdl_path_custom_flag': self.ytdl_path_custom_flag, - 'ytdl_update_dict': self.ytdl_update_dict, - 'ytdl_update_list': self.ytdl_update_list, - 'ytdl_update_current': self.ytdl_update_current, - - 'auto_switch_output_flag': self.auto_switch_output_flag, - 'output_size_default': self.output_size_default, - 'output_size_apply_flag': self.output_size_apply_flag, - - 'ytdl_update_once_flag': self.ytdl_update_once_flag, - 'ytdl_fork': self.ytdl_fork, - 'ytdl_fork_no_dependency_flag': self.ytdl_fork_no_dependency_flag, - - 'ytdl_output_system_cmd_flag': self.ytdl_output_system_cmd_flag, - 'ytdl_output_stdout_flag': self.ytdl_output_stdout_flag, - 'ytdl_output_ignore_json_flag': self.ytdl_output_ignore_json_flag, - 'ytdl_output_ignore_progress_flag': \ - self.ytdl_output_ignore_progress_flag, - 'ytdl_output_stderr_flag': self.ytdl_output_stderr_flag, - 'ytdl_output_start_empty_flag': self.ytdl_output_start_empty_flag, - 'ytdl_output_show_summary_flag': \ - self.ytdl_output_show_summary_flag, - - 'ytdl_write_system_cmd_flag': self.ytdl_write_system_cmd_flag, - 'ytdl_write_stdout_flag': self.ytdl_write_stdout_flag, - 'ytdl_write_ignore_json_flag': self.ytdl_write_ignore_json_flag, - 'ytdl_write_ignore_progress_flag': \ - self.ytdl_write_ignore_progress_flag, - 'ytdl_write_stderr_flag': self.ytdl_write_stderr_flag, - - 'ytdl_log_system_cmd_flag': self.ytdl_log_system_cmd_flag, - 'ytdl_log_stdout_flag': self.ytdl_log_stdout_flag, - 'ytdl_log_ignore_json_flag': self.ytdl_log_ignore_json_flag, - 'ytdl_log_ignore_progress_flag': \ - self.ytdl_log_ignore_progress_flag, - 'ytdl_log_stderr_flag': self.ytdl_log_stderr_flag, - - 'ytdl_write_verbose_flag': self.ytdl_write_verbose_flag, - - 'refresh_output_videos_flag': self.refresh_output_videos_flag, - 'refresh_output_verbose_flag': self.refresh_output_verbose_flag, - 'refresh_moviepy_timeout': self.refresh_moviepy_timeout, - - 'disk_space_warn_flag': self.disk_space_warn_flag, - 'disk_space_warn_limit': self.disk_space_warn_limit, - 'disk_space_stop_flag': self.disk_space_stop_flag, - 'disk_space_stop_limit': self.disk_space_stop_limit, - - 'custom_invidious_mirror': self.custom_invidious_mirror, - 'custom_sblock_mirror': self.custom_sblock_mirror, - - 'dl_proxy_list': self.dl_proxy_list, - - 'ffmpeg_path': self.ffmpeg_path, - 'avconv_path': self.avconv_path, - 'ffmpeg_convert_webp_flag': self.ffmpeg_convert_webp_flag, - 'ffmpeg_retain_webp_flag': self.ffmpeg_retain_webp_flag, - - 'livestream_dl_mode': self.livestream_dl_mode, - 'livestream_dl_timeout': self.livestream_dl_timeout, - 'livestream_replace_flag': self.livestream_replace_flag, - 'livestream_stop_is_final_flag': \ - self.livestream_stop_is_final_flag, - 'livestream_force_check_flag': self.livestream_force_check_flag, - - 'streamlink_path': self.streamlink_path, - - 'graph_data_type': self.graph_data_type, - 'graph_plot_type': self.graph_plot_type, - 'graph_time_period_secs': self.graph_time_period_secs, - 'graph_time_unit_secs': self.graph_time_unit_secs, - 'graph_ink_colour': self.graph_ink_colour, - - 'simple_prefs_flag': self.simple_prefs_flag, - 'simple_options_flag': self.simple_options_flag, - - 'operation_limit_flag': self.operation_limit_flag, - 'operation_check_limit': self.operation_check_limit, - 'operation_download_limit': self.operation_download_limit, - - 'show_newbie_dialogue_flag': self.show_newbie_dialogue_flag, - 'show_msys2_dialogue_flag': self.show_msys2_dialogue_flag, - 'show_delete_video_dialogue_flag': \ - self.show_delete_video_dialogue_flag, - 'delete_video_files_flag': self.delete_video_files_flag, - 'show_delete_container_dialogue_flag': \ - self.show_delete_container_dialogue_flag, - 'delete_container_files_flag': self.delete_container_files_flag, - - 'auto_switch_profile_flag': self.auto_switch_profile_flag, - - 'auto_clone_options_flag': self.auto_clone_options_flag, - 'auto_delete_options_flag': self.auto_delete_options_flag, - - 'block_livestreams_flag': self.block_livestreams_flag, - 'enable_livestreams_flag': self.enable_livestreams_flag, - 'livestream_max_days': self.livestream_max_days, - 'livestream_use_colour_flag': self.livestream_use_colour_flag, - 'livestream_simple_colour_flag': \ - self.livestream_simple_colour_flag, - 'livestream_auto_notify_flag': self.livestream_auto_notify_flag, - 'livestream_auto_alarm_flag': self.livestream_auto_alarm_flag, - 'livestream_auto_open_flag': self.livestream_auto_open_flag, - 'livestream_auto_dl_start_flag': \ - self.livestream_auto_dl_start_flag, - 'livestream_auto_dl_stop_flag': self.livestream_auto_dl_stop_flag, - 'scheduled_livestream_flag': self.scheduled_livestream_flag, - 'scheduled_livestream_wait_mins': \ - self.scheduled_livestream_wait_mins, - 'scheduled_livestream_last_time': \ - self.scheduled_livestream_last_time, - 'scheduled_livestream_extra_flag': \ - self.scheduled_livestream_extra_flag, - - 'autostop_time_flag': self.autostop_time_flag, - 'autostop_time_value': self.autostop_time_value, - 'autostop_time_unit': self.autostop_time_unit, - 'autostop_videos_flag': self.autostop_videos_flag, - 'autostop_videos_value': self.autostop_videos_value, - 'autostop_size_flag': self.autostop_size_flag, - 'autostop_size_value': self.autostop_size_value, - 'autostop_size_unit': self.autostop_size_unit, - - 'operation_auto_update_flag': self.operation_auto_update_flag, - 'operation_save_flag': self.operation_save_flag, - 'operation_sim_shortcut_flag': self.operation_sim_shortcut_flag, - - 'operation_auto_restart_flag': self.operation_auto_restart_flag, - 'operation_auto_restart_time': self.operation_auto_restart_time, - 'operation_auto_restart_max': self.operation_auto_restart_max, - - 'operation_dialogue_mode': self.operation_dialogue_mode, - 'operation_convert_mode': self.operation_convert_mode, - 'use_module_moviepy_flag': self.use_module_moviepy_flag, - - 'dialogue_copy_clipboard_flag': self.dialogue_copy_clipboard_flag, - 'dialogue_keep_open_flag': self.dialogue_keep_open_flag, - 'dialogue_yt_remind_flag': self.dialogue_yt_remind_flag, - - 'dialogue_disable_msg_flag': self.dialogue_disable_msg_flag, - - 'allow_ytdl_archive_flag': self.allow_ytdl_archive_flag, - 'allow_ytdl_archive_mode': self.allow_ytdl_archive_mode, - 'allow_ytdl_archive_path': self.allow_ytdl_archive_path, - 'classic_ytdl_archive_flag': \ - self.classic_ytdl_archive_flag, - - 'apply_json_timeout_flag': self.apply_json_timeout_flag, - 'json_timeout_no_comments_time': \ - self.json_timeout_no_comments_time, - 'json_timeout_with_comments_time': \ - self.json_timeout_with_comments_time, - 'track_missing_videos_flag': self.track_missing_videos_flag, - 'track_missing_time_flag': self.track_missing_time_flag, - 'track_missing_time_days': self.track_missing_time_days, - 'add_blocked_videos_flag': self.add_blocked_videos_flag, - 'store_playlist_id_flag': self.store_playlist_id_flag, - - 'video_timestamps_extract_json_flag': \ - self.video_timestamps_extract_json_flag, - 'video_timestamps_extract_descrip_flag': \ - self.video_timestamps_extract_descrip_flag, - 'video_timestamps_replace_flag': \ - self.video_timestamps_replace_flag, - 'video_timestamps_re_extract_flag': \ - self.video_timestamps_re_extract_flag, - 'video_timestamps_dl_mode': \ - self.video_timestamps_dl_mode, - 'split_video_name_mode': self.split_video_name_mode, - 'split_video_clips_dir_flag': self.split_video_clips_dir_flag, - 'split_video_subdir_flag': self.split_video_subdir_flag, - 'split_video_add_db_flag': self.split_video_add_db_flag, - 'split_video_copy_thumb_flag': self.split_video_copy_thumb_flag, - 'split_video_custom_title': self.split_video_custom_title, - 'split_video_force_keyframe_flag': \ - self.split_video_force_keyframe_flag, - 'split_video_auto_open_flag': self.split_video_auto_open_flag, - 'split_video_auto_delete_flag': self.split_video_auto_delete_flag, - - 'sblock_fetch_flag': self.sblock_fetch_flag, - 'sblock_obfuscate_flag': self.sblock_obfuscate_flag, - 'sblock_replace_flag': self.sblock_replace_flag, - 'sblock_re_extract_flag': self.sblock_re_extract_flag, - 'slice_video_force_keyframe_flag': \ - self.slice_video_force_keyframe_flag, - 'slice_video_cleanup_flag': self.slice_video_cleanup_flag, - - 'check_comment_fetch_flag': self.check_comment_fetch_flag, - 'dl_comment_fetch_flag': self.dl_comment_fetch_flag, - 'comment_store_flag': self.comment_store_flag, - 'comment_show_text_time_flag': self.comment_show_text_time_flag, - 'comment_show_formatted_flag': self.comment_show_formatted_flag, - - 'ignore_child_process_exit_flag': \ - self.ignore_child_process_exit_flag, - 'ignore_http_404_error_flag': self.ignore_http_404_error_flag, - 'ignore_data_block_error_flag': self.ignore_data_block_error_flag, - 'ignore_merge_warning_flag': self.ignore_merge_warning_flag, - 'ignore_missing_format_error_flag': \ - self.ignore_missing_format_error_flag, - 'ignore_no_annotations_flag': self.ignore_no_annotations_flag, - 'ignore_no_subtitles_flag': self.ignore_no_subtitles_flag, - 'ignore_page_given_flag': self.ignore_page_given_flag, - 'ignore_no_descrip_flag': self.ignore_no_descrip_flag, - 'ignore_thumb_404_flag': self.ignore_thumb_404_flag, - - 'ignore_yt_copyright_flag': self.ignore_yt_copyright_flag, - 'ignore_yt_age_restrict_flag': self.ignore_yt_age_restrict_flag, - 'ignore_yt_uploader_deleted_flag': \ - self.ignore_yt_uploader_deleted_flag, - 'ignore_yt_payment_flag': self.ignore_yt_payment_flag, - - 'ignore_custom_msg_list': self.ignore_custom_msg_list, - 'ignore_custom_regex_flag': self.ignore_custom_regex_flag, - - 'num_worker_default': self.num_worker_default, - 'num_worker_apply_flag': self.num_worker_apply_flag, - 'num_worker_bypass_flag': self.num_worker_bypass_flag, - - 'bandwidth_default': self.bandwidth_default, - 'bandwidth_apply_flag': self.bandwidth_apply_flag, - - 'video_res_default': self.video_res_default, - 'video_res_apply_flag': self.video_res_apply_flag, - - 'alt_num_worker': self.alt_num_worker, - 'alt_num_worker_apply_flag': self.alt_num_worker_apply_flag, - 'alt_bandwidth': self.alt_bandwidth, - 'alt_bandwidth_apply_flag': self.alt_bandwidth_apply_flag, - 'alt_start_time': self.alt_start_time, - 'alt_stop_time': self.alt_stop_time, - 'alt_day_string': self.alt_day_string, - - 'match_method': self.match_method, - 'match_first_chars': self.match_first_chars, - 'match_ignore_chars': self.match_ignore_chars, - 'match_nickname_flag': self.match_nickname_flag, - - 'auto_delete_flag': self.auto_delete_flag, - 'auto_delete_days': self.auto_delete_days, - 'auto_remove_flag': self.auto_remove_flag, - 'auto_remove_days': self.auto_remove_days, - 'auto_delete_watched_flag': self.auto_delete_watched_flag, - 'auto_delete_asap_flag': self.auto_delete_asap_flag, - - 'delete_on_shutdown_flag': self.delete_on_shutdown_flag, - 'open_temp_on_desktop_flag': self.open_temp_on_desktop_flag, - - 'complex_index_flag': self.complex_index_flag, - 'catalogue_mode': self.catalogue_mode, - 'catalogue_mode_type': self.catalogue_mode_type, - 'catalogue_page_size': self.catalogue_page_size, - 'catalogue_show_filter_flag': self.catalogue_show_filter_flag, - 'catalogue_use_regex_flag': self.catalogue_use_regex_flag, - - 'url_change_confirm_flag': self.url_change_confirm_flag, - 'url_change_regex_flag': self.url_change_regex_flag, - - 'custom_bg_table': self.custom_bg_table, - - 'ytdlp_filter_options_flag': self.ytdlp_filter_options_flag, - } - - # In case a competing instance of Tartube is saving the same config - # file, check for the lockfile and, if it exists, wait a reasonable - # time for it to be released - if not self.debug_ignore_lockfile_flag: - - lock_path = config_file_path + '.lock' - if os.path.isfile(lock_path): - - check_time = time.time() + self.config_lock_time - while time.time() < check_time and os.path.isfile(lock_path): - time.sleep(0.1) - - if os.path.isfile(lock_path): - - msg = _( - 'Failed to save the Tartube config file (file is' \ - + ' locked)', - ) + '\n\n' + _('File load/save has been disabled') - - if not self.main_win_obj: - self.disable_load_save(msg) - else: - self.disable_load_save() - self.file_error_dialogue(msg) - - return - - # Place our own lock on the config file - if not self.debug_ignore_lockfile_flag: - - try: - fh = open(lock_path, 'a').close() - - except: - - msg = _( - 'Failed to save the Tartube config file (file already' \ - + ' in use)' - ) - - if not self.main_win_obj: - self.disable_load_save(msg) - else: - self.disable_load_save() - self.file_error_dialogue(msg) - - return - - # Try to save the config file - try: - with open(config_file_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - - except: - self.remove_file(lock_path) - self.disable_load_save(_('Failed to save the Tartube config file')) - - return - - # Procedure successful; remove the lock - if not self.debug_ignore_lockfile_flag: - self.remove_file(lock_path) - - - def get_config_path(self): - - """Can be called by anything (for example, called by self.load_config() - and .save_config() ). - - Gets the expected full path to the config file (but does not test - whether it exists, or not). - - Return values: - - Returns a full path to the config file. - - """ - - # The config file can be stored at one of two locations, depending on - # whether xdg is available, or not - # v2.0.003 (amended v2.1.034) The user can force Tartube to use the - # config file in the script's directory (rather than the one in the - # location described by xdg) by placing a 'settings.json' file there. - # If that file is created when Tartube is already running, it can be - # an empty file (because Tartube overwrites it). Otherwise, it should - # be a copy of a legitimate config file - if self.config_file_xdg_path is None \ - or ( - os.path.isfile(self.config_file_path) \ - and not __main__.__pkg_strict_install_flag__ - ): - return self.config_file_path - else: - return self.config_file_xdg_path - - - def load_db(self, switch_flag=False): - - """Called by self.start() and .switch_db(). - - Loads the Tartube database file. If loading fails, disables all file - loading/saving. - - N.B. Due to serialisation issues in the python 'pickle' module, Tartube - databases from before v1.1.008 cannot be loaded. - - Args: - - switch_flag (bool): True when called by self.switch_db(), False - otherwise - - Return values: - - True on success, False on failure - - """ - - # Sanity check - path = os.path.abspath(os.path.join(self.data_dir, self.db_file_name)) - if self.current_manager_obj \ - or not os.path.isfile(path) \ - or self.db_loading_flag \ - or self.disable_load_save_flag: - return False - - # This flag is used to detect an interrupted database load due to a - # Python error, which would mean this function never returns - # It is set back to False a few times here, and also by any call to - # self.disable_load_save() - self.db_loading_flag = True - - # If a lockfile already exists, then another competing instance of - # Tartube is already using this database file - if not self.debug_ignore_lockfile_flag: - - lock_path = path + '.lock' - if os.path.isfile(lock_path): - - dialogue_win = mainwin.RemoveLockFileDialogue( - self.main_win_obj, - switch_flag, - ) - - dialogue_win.run() - remove_flag = dialogue_win.remove_flag - dialogue_win.destroy() - - if remove_flag: - - # The user thinks it's safe to ignore the stale lockfile - self.remove_stale_lock_file() - - elif switch_flag: - - # Let the calling code show a dialogue window - self.db_loading_flag = False - return False - - else: - - # Failed to load database on startup, and therefore - # Tartube will shut down - # (The True argument signals that the user should be - # prompted to artificially remove the lockfile) - self.disable_load_save( - _('Failed to load the Tartube database file'), - True, - ) - - return False - - # Place our own lock on the database file - try: - fh = open(lock_path, 'a').close() - self.db_lock_file_path = lock_path - - except: - - # (Failure may mean that the directory is unwriteable) - self.disable_load_save( - _('Failed to load the Tartube database file'), - ) - - return False - - # Reset main window tabs now so the user can't manipulate their widgets - # during the load - # (Don't reset the Errors/Warnings tab, as failed attempts to load a - # database generate messages there) - if self.main_win_obj: - self.main_win_obj.video_index_reset_marker() - self.main_win_obj.video_index_reset() - self.main_win_obj.video_catalogue_reset() - self.main_win_obj.progress_list_reset() - self.main_win_obj.results_list_reset() - self.main_win_obj.drag_drop_grid_empty() - # If opening Tartube in the system tray, we can't call .show_all() - if self.startup_complete_flag or not self.open_in_tray_flag: - self.main_win_obj.show_all() - - # Most main widgets are desensitised, until the database file has been - # loaded - self.main_win_obj.sensitise_widgets_if_database(False) - - # Try to load the database file - try: - fh = open(path, 'rb') - load_dict = pickle.load(fh) - fh.close() - - except Exception as e: - self.remove_db_lock_file() - self.disable_load_save( - _('Failed to load the Tartube database file') \ - + ': \n\n' + str(e), - ) - - return False - - # Do some basic checks on the loaded data - if not load_dict \ - or not 'script_name' in load_dict \ - or not 'script_version' in load_dict \ - or not 'save_date' in load_dict \ - or not 'save_time' in load_dict \ - or load_dict['script_name'] != __main__.__packagename__: - - self.remove_db_lock_file() - self.file_error_dialogue( - _('The Tartube database file is invalid'), - ) - - self.db_loading_flag = False - return False - - # Convert a version, e.g. 1.234.567, into a simple number, e.g. - # 1234567, that can be compared with other versions - version = self.convert_version(load_dict['script_version']) - # Now check that the database file wasn't written by a more recent - # version of Tartube (which this older version might not be able to - # read) - if version is None \ - or version > self.convert_version(__main__.__version__): - - self.remove_db_lock_file() - self.disable_load_save( - _('Database file can\'t be read by this version of Tartube'), - ) - - return False - - # Before v1.3.099, self.data_dir and self.downloads_dir had different - # values - # If a /downloads directory exists, then the data directory is using - # the old structure - if os.path.isdir(self.alt_downloads_dir): - - # Use the old location of self.downloads_dir - self.downloads_dir = self.alt_downloads_dir - # Move any database backup files to their new location - self.move_backup_files() - - else: - - # Use the new location - self.downloads_dir = self.data_dir - - # Set IVs to their new values - if version >= 2003259: # v2.3.258 - self.custom_dl_reg_count = load_dict['custom_dl_reg_count'] - self.custom_dl_reg_dict = load_dict['custom_dl_reg_dict'] - self.general_custom_dl_obj = load_dict['general_custom_dl_obj'] - self.classic_custom_dl_obj = load_dict['classic_custom_dl_obj'] - self.media_reg_count = load_dict['media_reg_count'] - self.media_reg_dict = load_dict['media_reg_dict'] - if version >= 2004132: # v2.4.132 - self.container_reg_dict = load_dict['container_reg_dict'] - self.old_container_reg_dict = {} - self.container_top_level_list \ - = load_dict['container_top_level_list'] - else: - self.container_reg_dict = {} - self.old_container_reg_dict = load_dict['media_name_dict'] - self.container_top_level_list = load_dict['media_top_level_list'] - if version >= 2000048: # v2.0.048 - self.media_reg_live_dict = load_dict['media_reg_live_dict'] - if version >= 2000052: # v2.0.052 - self.media_reg_auto_notify_dict \ - = load_dict['media_reg_auto_notify_dict'] - if version >= 2000068: # v2.0.068 - self.media_reg_auto_alarm_dict \ - = load_dict['media_reg_auto_alarm_dict'] - if version >= 2000052: # v2.0.052 - self.media_reg_auto_open_dict \ - = load_dict['media_reg_auto_open_dict'] - if version >= 2000054: # v2.0.054 - self.media_reg_auto_dl_start_dict \ - = load_dict['media_reg_auto_dl_start_dict'] - self.media_reg_auto_dl_stop_dict \ - = load_dict['media_reg_auto_dl_stop_dict'] - self.fixed_all_folder = load_dict['fixed_all_folder'] - self.fixed_fav_folder = load_dict['fixed_fav_folder'] - self.fixed_new_folder = load_dict['fixed_new_folder'] - self.fixed_temp_folder = load_dict['fixed_temp_folder'] - self.fixed_misc_folder = load_dict['fixed_misc_folder'] - if version >= 1004028: # v1.4.028 - self.fixed_bookmark_folder = load_dict['fixed_bookmark_folder'] - self.fixed_waiting_folder = load_dict['fixed_waiting_folder'] - if version >= 2000042: # v2.0.042 - self.fixed_live_folder = load_dict['fixed_live_folder'] - if version >= 2001060: # v2.1.060 - self.fixed_missing_folder = load_dict['fixed_missing_folder'] - if version >= 2003071: # v2.3.071 - self.fixed_recent_folder = load_dict['fixed_recent_folder'] - if version >= 2000098: # v2.0.098 - self.fixed_folder_locale = load_dict['fixed_folder_locale'] - if version >= 2003122: # v2.3.122 - self.fixed_recent_folder_days \ - = load_dict['fixed_recent_folder_days'] - if version >= 2002015: # v2.2.015 - self.scheduled_list = load_dict['scheduled_list'] - if version >= 2004013: # v2.4.013 - self.profile_dict = load_dict['profile_dict'] - self.last_profile = load_dict['last_profile'] - if version >= 2002034: # v2.2.034 - self.options_reg_count = load_dict['options_reg_count'] - self.options_reg_dict = load_dict['options_reg_dict'] - self.general_options_obj = load_dict['general_options_obj'] - # Removed v2.2.124 -# if version >= 2002051: # v2.1.051 -# self.classic_options_list = load_dict['classic_options_list'] - if version >= 2001007: # v2.1.007 - self.classic_options_obj = load_dict['classic_options_obj'] - if version >= 2003487: # v2.3.487 - self.classic_dropzone_list = load_dict['classic_dropzone_list'] - if version >= 2002149: # v2.2.149 - self.ffmpeg_reg_count = load_dict['ffmpeg_reg_count'] - self.ffmpeg_reg_dict = load_dict['ffmpeg_reg_dict'] - self.ffmpeg_options_obj = load_dict['ffmpeg_options_obj'] - # Renamed v2.4.365 - if 'ffmpeg_simple_options_flag' in load_dict: - self.simple_ffmpeg_options_flag \ - = load_dict['ffmpeg_simple_options_flag'] - elif 'simple_ffmpeg_options_flag' in load_dict: - self.simple_ffmpeg_options_flag \ - = load_dict['simple_ffmpeg_options_flag'] - if version >= 2002219: # v2.2.219 - self.toolbar_system_hide_flag \ - = load_dict['toolbar_system_hide_flag'] - if version >= 2003149: # v2.3.149 - self.fixed_clips_folder = load_dict['fixed_clips_folder'] - if version >= 2004194: # v2.4.194 - self.catalogue_sort_mode = load_dict['catalogue_sort_mode'] - self.catalogue_reverse_sort_flag \ - = load_dict['catalogue_reverse_sort_flag'] - - # Update the loaded data for this version of Tartube - self.update_db(version) - - # If the old directory structure is being used, the user might try to - # manually copy the contents of the /downloads directory into the - # directory above - # To prevent problems when that happens, preemptively rename any media - # data object called 'downloads' - # N.B. As of v2.4.132, 'downloads' is an illegal container name, so we - # now perform the check regardless of whether the old directory - # structure was detected above - for container_obj in self.get_container_list('downloads'): - - # Generate a new name; the function returns None on failure - new_name = utils.find_available_name(self, 'downloads') - if new_name is not None: - self.rename_container_silently(container_obj, new_name) - - # If the locale has changed since the loaded database file was last - # saved, update the names of fixed folders - if self.fixed_folder_locale != self.current_locale: - - self.rename_fixed_folders() - self.fixed_folder_locale = self.current_locale - - # Empty any temporary folders - self.delete_temp_folders() - - # Auto-delete and auto-remove old downloaded videos - self.auto_delete_old_videos() - self.auto_remove_old_videos() - - # Test any channels/playlists/folders which have external directories - # set. If we can't read/write to the external directory, then mark - # the channels/playlists/folders as unavailable - self.check_external() - - # If the debugging flag is set, hide all fixed folders - if self.debug_hide_folders_flag: - self.fixed_all_folder.set_hidden_flag(True) - self.fixed_bookmark_folder.set_hidden_flag(True) - self.fixed_fav_folder.set_hidden_flag(True) - self.fixed_live_folder.set_hidden_flag(True) - self.fixed_missing_folder.set_hidden_flag(True) - self.fixed_new_folder.set_hidden_flag(True) - self.fixed_recent_folder.set_hidden_flag(True) - self.fixed_waiting_folder.set_hidden_flag(True) - self.fixed_temp_folder.set_hidden_flag(True) - self.fixed_misc_folder.set_hidden_flag(True) - self.fixed_clips_folder.set_hidden_flag(True) - - # Now that a database file has been loaded, most main window widgets - # can be sensitised... - self.main_win_obj.sensitise_widgets_if_database(True) - # ...and saving the database file is now allowed - self.allow_db_save_flag = True - - if self.main_win_obj: - - # (Dis)activate the main window's menu/toolbar items for showing/ - # hiding system folders, as required - if ( - not self.main_win_obj.hide_system_menu_item.get_active() \ - and self.toolbar_system_hide_flag - ): - self.main_win_obj.hide_system_menu_item.set_active(True) - elif ( - self.main_win_obj.hide_system_menu_item.get_active() \ - and not self.toolbar_system_hide_flag - ): - self.main_win_obj.hide_system_menu_item.set_active(False) - - # Update other main menu items - self.main_win_obj.update_menu() - - # Update the Video Catalogue toolbar - self.main_win_obj.update_catalogue_sort_widgets() - self.main_win_obj.update_catalogue_reverse_sort_widgets() - - # Repopulate the Video Index, showing the new data - self.main_win_obj.video_index_catalogue_reset() - # Automatically mark channels/playlists/folders for download, if - # required - if self.auto_switch_profile_flag and self.last_profile is not None: - self.main_win_obj.switch_profile(self.last_profile) - - # Repopulate the Drag and Drop tab - self.main_win_obj.drag_drop_grid_reset() - - # Load succeeded - self.db_loading_flag = False - # Permit scheduled downloads again, if they were disabled in an earlier - # unsuccessful call to self.save_db() - self.disable_scheduled_dl_flag = False - - return True - - - def update_db(self, version): - - """Called by self.load_db(). - - When the Tartube database created by a previous version of Tartube is - loaded, update IVs as required. - - Args: - - version (int): The version of Tartube that created the database, - already converted to a simple integer by self.convert_version() - - """ - - # (Other system folders, having been added later, are not required by - # this list) - fixed_folder_list = [ - self.fixed_all_folder, - self.fixed_fav_folder, - self.fixed_new_folder, - ] - - options_obj_list = [self.general_options_obj] - if self.classic_options_obj: - options_obj_list.append(self.classic_options_obj) - for options_obj in self.options_reg_dict.values(): - if options_obj != self.general_options_obj \ - and ( - self.classic_options_obj is None \ - or options_obj != self.general_options_obj - ): - options_obj_list.append(options_obj) - - options_media_list = [] - for media_data_obj in self.media_reg_dict.values(): - if media_data_obj.options_obj is not None \ - and not media_data_obj.options_obj in options_obj_list: - options_media_list.append(media_data_obj) - - if version < 3012: # v0.3.012 - - # This version fixed some problems, in which the deletion of media - # data objects was not handled correctly - # Repair the media data registry, as required - for folder_obj in fixed_folder_list: - - # Check that videos in 'All Videos', 'New Videos' and - # 'Favourite Videos' still exist in the media data registry - copy_list = folder_obj.child_list.copy() - for child_obj in copy_list: - if isinstance(child_obj, media.Video) \ - and not child_obj.parent_obj.dbid in self.media_reg_dict: - folder_obj.del_child(child_obj) - - # Video counts in 'All Videos', 'New Videos' and 'Favourite - # Videos' might be wrong - vid_count = new_count = fav_count = dl_count = 0 - - for child_obj in folder_obj.child_list: - if isinstance(child_obj, media.Video): - vid_count += 1 - - if child_obj.new_flag: - new_count += 1 - - if child_obj.fav_flag: - fav_count += 1 - - if child_obj.dl_flag: - dl_count += 1 - - folder_obj.reset_counts( - vid_count, - 0, - dl_count, - fav_count, - 0, - 0, - new_count, - 0, - ) - - if version < 4003: # v0.4.002 - - # This version fixes video format options, which were stored - # incorrectly in options.OptionsManager - key_list = [ - 'video_format', - 'second_video_format', - 'third_video_format', - ] - - for options_obj in options_obj_list: - for key in key_list: - - val = options_obj.options_dict[key] - if val != '0': - - if val in formats.VIDEO_OPTION_DICT: - # Invert the key-value pair used before v0.4.002 - options_obj.options_dict[key] \ - = formats.VIDEO_OPTION_DICT[val] - - else: - # Completely invalid format description, so - # just reset it - options_obj.options_dict[key] = '0' - -# if version < 4004: # v0.4.004 -# -# # This version fixes a bug in which moving a channel, playlist or -# # folder to a new location in the media data registry's tree -# # failed to update all the videos that moved with it -# # To be safe, update every video in the registry -# for media_data_obj in self.media_reg_dict.values(): -# if isinstance(media_data_obj, media.Video): -# media_data_obj.reset_file_dir() - - if version < 4015: # v0.4.015 - - # This version fixes issues with sorting videos. Channels, - # playlists and folders in a loaded database might not be sorted - # correctly, so just sort them all using the new algorithms - # (Other system folders, having been added later, are not required - # by this list) - container_list = [ - self.fixed_all_folder, - self.fixed_new_folder, - self.fixed_fav_folder, - self.fixed_misc_folder, - self.fixed_temp_folder, - ] - - for dbid in self.old_container_reg_dict.values(): - container_list.append(self.media_reg_dict[dbid]) - - for container_obj in container_list: - container_obj.sort_children(self) - - if version < 4022: # v0.4.022 - - # This version fixes a rare issue in which media.Video.index was - # set to a string, rather than int, value - # Update all existing videos - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.index is not None: - media_data_obj.index = int(media_data_obj.index) - - if version < 6003: # v0.6.003 - - # This version fixes an issue in which deleting an individual video - # and then re-adding the same video, downloading it then deleting - # it a second time, messes up the parent container's count IVs - # Nothing for it but to recalculate them all, just in case - for dbid in self.old_container_reg_dict.values(): - container_obj = self.media_reg_dict[dbid] - - vid_count = new_count = fav_count = dl_count = 0 - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - vid_count += 1 - - if child_obj.new_flag: - new_count += 1 - - if child_obj.fav_flag: - fav_count += 1 - - if child_obj.dl_flag: - dl_count += 1 - - container_obj.reset_counts( - vid_count, - 0, - dl_count, - fav_count, - 0, - 0, - new_count, - 0, - ) - - if version < 1000013: # v1.0.013 - - # This version adds nicknames to channels, playlists and folders - for dbid in self.old_container_reg_dict.values(): - container_obj = self.media_reg_dict[dbid] - container_obj.nickname = container_obj.name - - if version < 1000031: # v1.0.031 - - # This version adds nicknames to videos. If the database is large, - # warn the user before continuing - if self.media_reg_dict.len() > 1000: - - dialogue_win \ - = self.dialogue_manager_obj.show_simple_msg_dialogue( - _('Tartube is applying an essential database update') \ - + '\n\n' \ - + _('This might take a few minutes, so please be patient'), - 'info', - 'ok', - self.main_win_obj, - ) - - dialogue_win.set_modal(True) - - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - - media_data_obj.nickname = media_data_obj.name - - # If the video's JSON data has been saved, we can use that - # to set the nickname - json_path = media_data_obj.get_actual_path_by_ext( - self, - '.info.json', - ) - - if os.path.isfile(json_path): - json_dict = self.file_manager_obj.load_json(json_path) - if 'title' in json_dict: - media_data_obj.nickname = json_dict['title'] - - if version < 1001031: # v1.1.031 - - # This version adds the ability to disable checking/downloading for - # media data objects - for dbid in self.old_container_reg_dict.values(): - media_data_obj = self.media_reg_dict[dbid] - media_data_obj.dl_disable_flag = False - - if version < 1001032: # v1.1.032 - - # This version adds video archiving. Archived videos cannot be - # auto-deleted - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.archive_flag = False - - if version < 1001037: # v1.1.037 - - # This version adds alternative destination directories for a - # channel's/playlist's/folder's videos, thumbnails (etc) - for dbid in self.old_container_reg_dict.values(): - media_data_obj = self.media_reg_dict[dbid] - media_data_obj.master_dbid = media_data_obj.dbid - media_data_obj.slave_dbid_list = [] - - if version < 1001045: # v1.1.045 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['use_fixed_folder'] = None - - if version < 1001060: # v1.1.060 - - # This version adds new options to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['abort_on_error'] = False - - options_obj.options_dict['socket_timeout'] = '' - options_obj.options_dict['source_address'] = '' - options_obj.options_dict['force_ipv4'] = False - options_obj.options_dict['force_ipv6'] = False - - options_obj.options_dict['geo_verification_proxy'] = '' - options_obj.options_dict['geo_bypass'] = False - options_obj.options_dict['no_geo_bypass'] = False - options_obj.options_dict['geo_bypass_country'] = '' - options_obj.options_dict['geo_bypass_ip_block'] = '' - - options_obj.options_dict['match_title_list'] = [] - options_obj.options_dict['reject_title_list'] = [] - - options_obj.options_dict['date'] = '' - options_obj.options_dict['date_before'] = '' - options_obj.options_dict['date_after'] = '' - options_obj.options_dict['min_views'] = 0 - options_obj.options_dict['max_views'] = 0 - options_obj.options_dict['match_filter'] = '' - options_obj.options_dict['age_limit'] = '' - options_obj.options_dict['include_ads'] = False - - options_obj.options_dict['playlist_reverse'] = False - options_obj.options_dict['playlist_random'] = False - options_obj.options_dict['prefer_ffmpeg'] = False - options_obj.options_dict['external_downloader'] = '' - options_obj.options_dict['external_arg_string'] = '' - - options_obj.options_dict['force_encoding'] = '' - options_obj.options_dict['no_check_certificate'] = False - options_obj.options_dict['prefer_insecure'] = False - - options_obj.options_dict['all_formats'] = False - options_obj.options_dict['prefer_free_formats'] = False - options_obj.options_dict['yt_skip_dash'] = False - options_obj.options_dict['merge_output_format'] = '' - - options_obj.options_dict['subs_format'] = '' - - options_obj.options_dict['two_factor'] = '' - options_obj.options_dict['net_rc'] = False - - options_obj.options_dict['recode_video'] = '' - options_obj.options_dict['pp_args'] = '' - options_obj.options_dict['fixup_policy'] = '' - options_obj.options_dict['prefer_avconv'] = False - options_obj.options_dict['prefer_ffmpeg'] = False - - options_obj.options_dict['write_annotations'] = True - options_obj.options_dict['keep_annotations'] = False - options_obj.options_dict['sim_keep_annotations'] = False - - # (Also rename one option) - if 'to_audio' in options_obj.options_dict: - options_obj.options_dict['extract_audio'] \ - = options_obj.options_dict['to_audio'] - options_obj.options_dict.pop('to_audio') - else: - options_obj.options_dict['extract_audio'] = False - -# if version < 1003004: # v1.3.004 -# -# # The way that directories are stored in media.VideoObj.file_dir -# # has changed. Reset those values for all video objects -# for media_data_obj in self.media_reg_dict.values(): -# if isinstance(media_data_obj, media.Video): -# -# media_data_obj.reset_file_dir() - - if version < 1003009: # v1.3.009 - - # In earlier versions, - # refresh.RefreshManager.refresh_from_default_destination() set a - # video's .name, but not its .nickname - # The .refresh_from_default_destination() is already fixed, but we - # need to check every video in the database, and set its - # .nickname if not set - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - if ( - media_data_obj.nickname is None \ - or media_data_obj.nickname == self.default_video_name - ) and media_data_obj.name is not None \ - and media_data_obj.name != self.default_video_name: - media_data_obj.nickname = media_data_obj.name - - if version < 1003017: # v1.3.017 - - for options_obj in options_obj_list: - - # In earlier versions, the 'prefer_ffmpeg' and - # 'hls_prefer_ffmpeg' download options had been confused - options_obj.options_dict['hls_prefer_ffmpeg'] = False - - # In earlier versions, MS Windows users could set the - # 'prefer_ffmpeg' and 'prefer_avconv' options, even though - # the MS Windows installer does not provide AVConv. Reset - # both values - options_obj.options_dict['prefer_ffmpeg'] = False - options_obj.options_dict['prefer_avconv'] = False - - # In earlier versions, the download options 'video_format', - # 'second_video_format' and/or 'third_video_format' could - # incorrectly be set to a sound format like 'mp3'. This is - # not the way youtube-dl-gui was supposed to implement its - # formats; remove them, if the user has specified them - if 'third_video_format' in options_obj.options_dict: - - if not options_obj.options_dict['third_video_format'] \ - in formats.VIDEO_OPTION_DICT: - options_obj.options_dict['third_video_format'] = '0' - - if not options_obj.options_dict['second_video_format'] \ - in formats.VIDEO_OPTION_DICT: - options_obj.options_dict['second_video_format'] = '0' - if options_obj.options_dict['third_video_format'] \ - != '0': - options_obj.options_dict['second_video_format'] \ - = options_obj.options_dict['third_video_format'] - options_obj.options_dict['third_video_format'] \ - = '0' - - if not options_obj.options_dict['video_format'] \ - in formats.VIDEO_OPTION_DICT: - options_obj.options_dict['video_format'] = '0' - if options_obj.options_dict['second_video_format'] \ - != '0': - options_obj.options_dict['video_format'] \ - = options_obj.options_dict['second_video_format'] - options_obj.options_dict['second_video_format'] \ - = options_obj.options_dict['third_video_format'] - - if version <= 1003099: # v1.3.099 - - # In this version, some container names have become illegal. - # Replace any illegal names with legal ones - for old_name in self.old_container_reg_dict.keys(): - if not self.check_container_name_is_legal(old_name): - - dbid = self.old_container_reg_dict[old_name] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - media_data_obj, - utils.find_available_name(self, 'downloads', 2, -1), - ) - - if version < 1003106: # v1.3.106 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - if options_obj.options_dict['subs_lang'] == '': - options_obj.options_dict['subs_lang_list'] = [] - else: - options_obj.options_dict['subs_lang_list'] \ - = [ options_obj.options_dict['subs_lang'] ] - - if version < 1003110: # v1.3.110 - - # Before this version, the 'output_template' in - # options.OptionManager was completely broken, containing both - # the filepath to this file, and an '%(uploader)s string that - # broke the structure of Tartube's data directory - # Reset the value if it seems to contain either - for options_obj in options_obj_list: - output_template = options_obj.options_dict['output_template'] - if re.search(sys.path[0], output_template) \ - or re.search(r'\%\(uploader\)s', output_template): - options_obj.options_dict['output_template'] \ - = '%(title)s.%(ext)s' - - if version < 1003111: # v1.3.111 - - # In this version, formats.py.FILE_OUTPUT_NAME_DICT and - # .FILE_OUTPUT_CONVERT_DICT, so that the custom format's index - # is 0 (was 3) - for options_obj in options_obj_list: - output_format = options_obj.options_dict['output_format'] - if output_format == 3: - options_obj.options_dict['output_format'] = 0 - elif output_format < 3: - options_obj.options_dict['output_format'] \ - = output_format + 1 - - if version < 1004028: # v1.4.028 - - # This version adds two new fixed folders. If there are existing - # folders with the same name, they must be renamed - old_list \ - = [formats.FOLDER_BOOKMARKS, formats.FOLDER_WAITING_VIDEOS] - for old_name in old_list: - - if old_name in self.old_container_reg_dict: - - dbid = self.old_container_reg_dict[old_name] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - media_data_obj, - utils.find_available_name(self, 'downloads', 2, -1), - ) - - # Now create the new fixed folders - self.fixed_bookmark_folder = self.add_folder( - formats.FOLDER_BOOKMARKS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - self.fixed_waiting_folder = self.add_folder( - formats.FOLDER_WAITING_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - if version < 1004037: # v1.4.037 - - # Having added new fixed folders, add corresponding new IVs for - # each media.Video object - for dbid in self.old_container_reg_dict.values(): - container_obj = self.media_reg_dict[dbid] - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - child_obj.bookmark_flag = False - child_obj.waiting_flag = False - - if version < 1004037: # v1.4.037 - - # This version adds new IVs to channels, playlists and folders - for dbid in self.old_container_reg_dict.values(): - container_obj = self.media_reg_dict[dbid] - - container_obj.bookmark_count = 0 - container_obj.waiting_count = 0 - -# # Some of the count IVs were not working 100%, so we'll just -# # recalculate them all -# container_obj.recalculate_counts() - - if version < 1004043: # v1.4.043 - - # This version removes an IV from media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - del media_data_obj.file_dir - - if version < 2000012: # v2.0.012 - - # This version does not add the ytdl-archive.txt file to system - # folders ('Unsorted Videos' and 'Temporary Videos'), but - # continues to add it to channels, playlists and non-system - # folders - # Remove the archive file from system folders, if present - - # 'Temporary Videos' - temp_path = os.path.abspath( - os.path.join( - self.fixed_temp_folder.get_default_dir(self), - self.ytdl_archive_name, - ), - ) - - if os.path.isfile(temp_path): - self.remove_file(temp_path) - - # 'Unsorted Videos' - unsorted_path = os.path.abspath( - os.path.join( - self.fixed_misc_folder.get_default_dir(self), - self.ytdl_archive_name, - ), - ) - - if os.path.isfile(unsorted_path): - self.remove_file(unsorted_path) - - if version < 2000025: # v2.0.025 - - # This version adds the Classic Mode tab, and new IVs used by it. - # Most of them are only created when needed - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.dummy_flag = False - - if version < 2000035: # v2.0.035 - - # This version adds IVs for livestream detection on compatible - # websites - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.live_mode = 0 - elif not isinstance(media_data_obj, media.Folder): - media_data_obj.rss = None - - if version < 2000042: # v2.0.042 - - # This version adds new IVs to channels, playlists and folders - for dbid in self.old_container_reg_dict.values(): - container_obj = self.media_reg_dict[dbid] - - container_obj.live_count = 0 - - # This version also creates a new fixed folder. If there are - # existing folders with the same name, they must be renamed - if formats.FOLDER_LIVESTREAMS in self.old_container_reg_dict: - - dbid = self.old_container_reg_dict[formats.FOLDER_LIVESTREAMS] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - media_data_obj, - utils.find_available_name(self, 'downloads', 2, -1), - ) - - # Now create the new fixed folder - self.fixed_live_folder = self.add_folder( - formats.FOLDER_LIVESTREAMS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - if version < 2000105: # v2.0.105 - - # This version adds new options to options.OptionsManager, and - # deletes some existing ones - for options_obj in options_obj_list: - - options_obj.options_dict['video_format_list'] = [] - - if options_obj.options_dict['all_formats']: - options_obj.options_dict['video_format_mode'] = 'all' - options_obj.options_dict['all_formats'] = False - else: - options_obj.options_dict['video_format_mode'] = 'single' - - if 'second_video_format' in options_obj.options_dict: - options_obj.options_dict.pop('second_video_format') - if 'third_video_format' in options_obj.options_dict: - options_obj.options_dict.pop('third_video_format') - - if version < 2001010: # v2.1.010 - - # This version adds a new IV to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.was_live_flag = False - -# if version < 2001012: # v2.1.012 -# -# # v2.1.005 Addresses problems in which a media.Video might still -# # exist inside the 'New videos' folder (etc), but not anywhere -# # else in the database -# # Still not sure what the cause was, but assuming that it was some -# # ancient issue, long since fixed, force a silent call to the -# # check/fix functions -# self.check_integrity_db(True) - - if version < 2001037: # v2.1.037 - - # This version adds a new IV to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.orig_parent = None - - if version < 2001041: # v2.1.041 - - # This version fixes a problem in options.OptionsManager; when - # options were applied to a channel/playlist/folder, the cloned - # dictionary of options contained lists that were not copied - # properly; hence changing one list changed all of them - for options_obj in options_obj_list: - - for key in [ - 'match_title_list', 'reject_title_list', - 'video_format_list', 'subs_lang_list', - ]: - options_obj.options_dict[key] \ - = options_obj.options_dict[key].copy() - - if version < 2001060: # v2.1.060 - - # This version adds a new fixed folder. If there is an existing - # folder with the same name, it must be renamed - if formats.FOLDER_MISSING_VIDEOS in self.old_container_reg_dict: - - dbid \ - = self.old_container_reg_dict[formats.FOLDER_MISSING_VIDEOS] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - media_data_obj, - utils.find_available_name(self, 'downloads', 2, -1), - ) - - # Now create the new fixed folder - self.fixed_missing_folder = self.add_folder( - formats.FOLDER_MISSING_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - # Having added new the fixed folder, add a corresponding new IV for - # each media.Video object - for dbid in self.old_container_reg_dict.values(): - - container_obj = self.media_reg_dict[dbid] - container_obj.missing_count = 0 - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - child_obj.missing_flag = False - - if version < 2001089: # v2.1.089 - - # This version adds new options to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['move_description'] = False - options_obj.options_dict['move_info'] = False - options_obj.options_dict['move_annotations'] = False - options_obj.options_dict['move_thumbnail'] = False - - if version < 2002033: # v2.2.033 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['min_sleep_interval'] = 0 - options_obj.options_dict['max_sleep_interval'] = 0 - - if version < 2002034: # v2.2.034 - - # This version adds a registry for options.OptionsManager objects, - # and gives each object new IVs. Update all IVs - self.options_reg_dict = {} - - if self.general_options_obj: - self.options_reg_count += 1 - self.general_options_obj.uid = self.options_reg_count - self.general_options_obj.name = 'general' - self.general_options_obj.dbid = None - self.options_reg_dict[self.general_options_obj.uid] \ - = self.general_options_obj - - if self.classic_options_obj: - self.options_reg_count += 1 - self.classic_options_obj.uid = self.options_reg_count - self.classic_options_obj.name = 'classic' - self.classic_options_obj.dbid = None - self.options_reg_dict[self.classic_options_obj.uid] \ - = self.classic_options_obj - - for media_data_obj in options_media_list: - - options_obj = media_data_obj.options_obj - self.options_reg_count +=1 - options_obj.uid = self.options_reg_count - options_obj.name = media_data_obj.name - options_obj.dbid = media_data_obj.dbid - self.options_reg_dict[options_obj.uid] = options_obj - - if version < 2002049: # v2.2.049 - - # This version adds an IV to media.Scheduled objects - for scheduled_obj in self.scheduled_list: - scheduled_obj.ignore_limits_flag = False - -# if version < 2002051: # v2.2.051 -# -# # This version adds a new IV, initially containing any -# # options.OptionsManager objects not attached to a media data -# # object -# self.classic_options_list = [] -# if self.classic_options_obj: -# self.classic_options_list.append(self.classic_options_obj) -# -# for options_obj in self.options_reg_dict.values(): -# -# if options_obj.dbid is None \ -# and options_obj != self.general_options_obj \ -# and ( -# self.classic_options_obj is None \ -# or options_obj != self.classic_options_obj -# ): -# self.classic_options_list.append(options_obj) - - if version < 2002101: # v2.2.101 - - # This version adds a new IV to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.live_msg = '' - - if version < 2002107: # v2.2.107 - - # This version adds new IVs to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.live_debut_flag = False - media_data_obj.live_time = 0 - -# if version < 2002115: # v2.2.115 -# -# # The update code for v1.4.037 and v2.1.012 crashes with a python -# # error, when loading a v1.4 database -# # Can fix both problems by doing a silent database integrity check -# self.check_integrity_db(True) - - if version < 2002125: # v2.2.125 - - # Prior to this version, changes to the options.OptionsManager - # name in its edit window were saved to the .options_dict IV by - # mistake. Fix this error - for options_obj in options_obj_list: - if 'name' in options_obj.options_dict: - del options_obj.options_dict['name'] - -# if version < 2002160: # v2.2.160 -# -# # This version adds a new IV to media.Channel, media.Playlist and -# # media.Folder objects -# for media_data_obj in self.media_reg_dict.values(): -# if not isinstance(media_data_obj, media.Video): -# media_data_obj.last_sort_mode = 'default' - - if version < 2002175: # v2.2.175 - - # In recent versions of Tartube, the value of media.Video.live_mode - # could have been set to a dictionary, rather than a valid - # integer. Fix that problem - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video) \ - and type(media_data_obj.live_mode) is dict: - media_data_obj.live_mode = 0 - - if version < 2002188: # v2.2.188 - - # media.Video IVs that only existed for 'dummy' videos are added to - # all videos in this version - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - if not hasattr(media_data_obj, 'dummy_dir'): - media_data_obj.dummy_dir = None - media_data_obj.dummy_path = None - media_data_obj.dummy_format = None - - if version < 2002191: # v2.2.191 - - # Before this version, drag-and-drop into the main window could - # create a media.Video object whose .source should have been a - # URL, but was instead a URI to a file path, in the form - # 'file://PATH' - # Check every video to remove the invalid sources - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.source is not None \ - and re.search(r'^file\:\/\/', media_data_obj.source): - media_data_obj.source = None - - if version < 2003006: # v2.3.006 - - # Fix an error, in which self.reset_db did not reset all IVs that - # are saved in the Tartube database file - # Nothing we can do to reverse time, but we can make sure that - # options.OptionsManager.dbid points to the a valid DBID - for options_obj in self.options_reg_dict.values(): - if options_obj.dbid is not None \ - and not options_obj.dbid in self.options_reg_dict: - options_obj.dbid = None - - if version < 2003026: # v2.3.026 - - # Fix an error, in which a media data object's .options_obj IV is - # not updated, when the options object is reset (i.e. replaced - # with a new one) - for options_obj in self.options_reg_dict.values(): - if options_obj.dbid is not None: - - if not options_obj.dbid in self.media_reg_dict: - # Git #228, don't have an explanation for this error - # yet (may have been fixed in v2.3.026-027) - options_obj.reset_dbid() - - else: - media_data_obj = self.media_reg_dict[options_obj.dbid] - media_data_obj.set_options_obj(options_obj) - - if version < 2003049: # v2.3.049 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['cookies_path'] = '' - - if version < 2003071: # v2.3.071 - - # This version adds a new fixed folder. If there is an existing - # folder with the same name, it must be renamed - if formats.FOLDER_RECENT_VIDEOS in self.old_container_reg_dict: - - dbid \ - = self.old_container_reg_dict[formats.FOLDER_RECENT_VIDEOS] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - media_data_obj, - utils.find_available_name(self, 'downloads', 2, -1), - ) - - # Now create the new fixed folder - self.fixed_recent_folder = self.add_folder( - formats.FOLDER_RECENT_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - if version < 2003107: # v2.3.107 - - # This version adds new options to - # ffmpeg_tartube.FFmpegOptionsManager - for options_obj in self.ffmpeg_reg_dict.values(): - options_obj.options_dict['gpu_encoding'] = 'libx264' - options_obj.options_dict['hw_accel'] = 'none' - - if version < 2003108: # v2.3.108 - - # Apply fix to youtube-dl update IVs, caused by an issue in - # self.auto_detect_paths(), now fixed (Git #256) - if os.name != 'nt' and __main__.__pkg_strict_install_flag__: - - self.ytdl_update_dict = { - 'ytdl_update_disabled': [], - } - self.ytdl_update_list = [ - 'ytdl_update_disabled', - ] - self.ytdl_update_current = 'ytdl_update_disabled' - - if version < 2003119: # v2.3.119 - - # This version adds IVs to media.Scheduled objects - for scheduled_obj in self.scheduled_list: - - scheduled_obj.scheduled_num_worker = 2 - scheduled_obj.scheduled_num_worker_apply_flag = False - scheduled_obj.scheduled_bandwidth = 500 - scheduled_obj.scheduled_bandwidth_apply_flag = False - - if version < 2003126: # v2.3.126 - - # This version adds new options to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['direct_cmd_flag'] = False - options_obj.options_dict['direct_url_flag'] = False - - if version < 2003136: # v2.3.136 - - # This version adds a list of timestamps extracted from the video - # description - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.stamp_list = [] - - if version < 2003140: # v2.3.140 - - # This version adds new options to - # ffmpeg_tartube.FFmpegOptionsManager - for options_obj in self.ffmpeg_reg_dict.values(): - options_obj.options_dict['split_mode'] = 'video' - options_obj.options_dict['split_list'] = [] - - if version < 2003146: # v2.3.146 - - # This version adds a new IV to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.split_flag = False - - if version < 2003149: # v2.3.149 - - # This version adds a new fixed folder. If there is an existing - # folder with the same name, it must be renamed - if formats.FOLDER_VIDEO_CLIPS in self.old_container_reg_dict: - - dbid = self.old_container_reg_dict[formats.FOLDER_VIDEO_CLIPS] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - media_data_obj, - utils.find_available_name(self, 'downloads', 2, -1), - ) - - # Now create the new fixed folder - self.fixed_clips_folder = self.add_folder( - formats.FOLDER_VIDEO_CLIPS, - None, # No parent folder - False, # Allow downloads - 'partial', # Can contain videos and folders - True, # Fixed (folder cannot be removed) - False, # Public - False, # Not temporary - ) - - if version < 2003205: # v2.3.205 - - # Git #307. In v2.3.149, media.Folder.restrict_flag was changed to - # media.Folder.restrict_mode, but this function was not updated - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Folder) \ - and hasattr(media_data_obj, 'restrict_flag'): - - if media_data_obj.restrict_flag: - media_data_obj.restrict_mode = 'full' - else: - media_data_obj.restrict_mode = 'open' - - del media_data_obj.restrict_flag - - if version < 2003216: # v2.3.216 - - # This version adds new IVs to media.Channel, media.Playlist and - # media.Folder objects - for media_data_obj in self.media_reg_dict.values(): - if not isinstance(media_data_obj, media.Video): - media_data_obj.external_dir = None - - if version < 2003224: # v2.3.224 - - # This version adds new IVs to media.Channel, media.Playlist and - # media.Folder objects - for media_data_obj in self.media_reg_dict.values(): - if not isinstance(media_data_obj, media.Video): - media_data_obj.dl_no_db_flag = False - - # Other flags in this group are now mutually exclusive; - # check that the older flags aren't both set to True - if media_data_obj.dl_disable_flag \ - and media_data_obj.dl_sim_flag: - media_data_obj.dl_sim_flag = False - - if version < 2003225: # v2.3.225 - - # In this version, the behaviour of .dl_disable_flag for - # media.Channel, media.Playlist and media.Folder objects changes: - # it no longer applies to any descendants - # In order to avoid any nasty surprises, update the IV for all - # descendants of any channel/playlist/folder whose - # .dl_disable_flag is True - check_list = self.container_top_level_list.copy() - - while check_list: - - dbid = check_list.pop() - media_data_obj = self.media_reg_dict[dbid] - - if not isinstance(media_data_obj, media.Video) \ - and media_data_obj.dl_disable_flag: - - for child_obj in media_data_obj.child_list: - if not isinstance(media_data_obj, media.Video): - - child_obj.dl_disable_flag = True - # By adding the child to check_list, we ensure that - # its grandchildren are checked as well - check_list.append(child_obj.dbid) - - if version < 2003227: # v2.3.227 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['downloader_config'] = False - - if version < 2003228: # v2.3.228 - - # This version adds new options to options.OptionsManager, and - # replaces an existing option - for options_obj in options_obj_list: - options_obj.options_dict['output_format_list'] = [] - options_obj.options_dict['output_path_list'] = [] - # Removed v2.4.059 -# options_obj.options_dict['save_path_list'] = [] - if 'save_path' in options_obj.options_dict: - del options_obj.options_dict['save_path'] - - if version < 2003229: # v2.3.229 - - # This version adds new options to options.OptionsManager - for options_obj in options_obj_list: - - # (All downloaders) - options_obj.options_dict['ap_mso'] = '' - options_obj.options_dict['ap_username'] = '' - options_obj.options_dict['ap_password'] = '' - - # (yt-dlp only) - options_obj.options_dict['extractor_args_list'] = [] - - options_obj.options_dict['break_on_existing'] = False - options_obj.options_dict['break_on_reject'] = False - options_obj.options_dict['skip_playlist_after_errors'] = 0 - - options_obj.options_dict['concurrent_fragments'] = 1 - options_obj.options_dict['throttled_rate'] = 0 - - options_obj.options_dict['windows_filenames'] = False - options_obj.options_dict['trim_filenames'] = 0 - options_obj.options_dict['force_overwrites'] = False - options_obj.options_dict['write_playlist_metafiles'] = False - options_obj.options_dict['no_clean_info_json'] = False - options_obj.options_dict['write_comments'] = False - - options_obj.options_dict['write_link'] = False - options_obj.options_dict['write_url_link'] = False - options_obj.options_dict['write_webloc_link'] = False - options_obj.options_dict['write_desktop_link'] = False - - options_obj.options_dict['ignore_no_formats_error'] = False - options_obj.options_dict['force_write_archive'] = False - - options_obj.options_dict['sleep_requests'] = 0 - options_obj.options_dict['sleep_subtitles'] = 0 - - options_obj.options_dict['video_multistreams'] = False - options_obj.options_dict['audio_multistreams'] = False - options_obj.options_dict['check_formats'] = False - options_obj.options_dict['allow_unplayable_formats'] = False - - options_obj.options_dict['remux_video'] = '' - options_obj.options_dict['embed_metadata'] = False - options_obj.options_dict['convert_thumbnails'] = '' - options_obj.options_dict['split_chapters'] = False - - options_obj.options_dict['extractor_retries'] = '3' - options_obj.options_dict['no_allow_dynamic_mpd'] = False - options_obj.options_dict['hls_split_discontinuity'] = False - - if version < 2003237: # v2.3.237 - - # This version adds new IVs to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.vid = None - media_data_obj.slice_list = [] - - if version < 2003251: # v2.3.251 - - # This version adds new options to - # ffmpeg_tartube.FFmpegOptionsManager - for options_obj in self.ffmpeg_reg_dict.values(): - options_obj.options_dict['slice_mode'] = 'video' - options_obj.options_dict['slice_list'] = [] - - if version < 2003291: # v2.3.291 - - # This version adds a new IV to downloads.CustomDLManager objects - for custom_dl_obj in self.custom_dl_reg_dict.values(): - - if self.classic_custom_dl_obj is not None \ - and self.classic_custom_dl_obj == custom_dl_obj: - custom_dl_obj.dl_by_video_flag = True - custom_dl_obj.dl_precede_flag = True - else: - custom_dl_obj.dl_precede_flag = False - - if version < 2003295: # v2.3.295 - - # This version adds a new IV to media.Scheduled objects - for scheduled_obj in self.scheduled_list: - if scheduled_obj.dl_mode == 'custom': - scheduled_obj.dl_mode = 'custom_real' - scheduled_obj.custom_dl_uid \ - = self.general_custom_dl_obj.uid - else: - scheduled_obj.custom_dl_uid = None - - if version < 2003304: # v2.3.304 - - # This version fixes an incorrect value for an IV in media.Folder - # objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.restrict_mode == 'free': - media_data_obj.restrict_mode = 'open' - - if version < 2003314: # v2.3.314 - - # This version adds a new IV to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.comment_list = [] - - if version < 2003316: # v2.3.316 - - # This version removes an option to options.OptionsManager - for options_obj in options_obj_list: - if 'write_comments' in options_obj.options_dict: - del options_obj.options_dict['write_comments'] - - if version < 2003375: # v2.3.375 - - # This version adds new options to options.OptionsManager - for options_obj in options_obj_list: - - # (yt-dlp only) - options_obj.options_dict['no_cookies'] = False - options_obj.options_dict['cookies_from_browser'] = '' - options_obj.options_dict['no_cookies_from_browser'] = True - - if version < 2003382: # v2.3.382 - - # This version adds a new IV to media.Channel and media.Playlist - # objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Channel) \ - or isinstance(media_data_obj, media.Playlist): - media_data_obj.playlist_id_dict = {} - - if version < 2003409: # v2.3.409 - - # This version adds a new IV to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.subs_list = [] - - if version < 2003412: # v2.3.412 - - # This version adds new IVs to downloads.CustomDLManager objects - for custom_dl_obj in self.custom_dl_reg_dict.values(): - - custom_dl_obj.dl_if_subs_flag = False - custom_dl_obj.ignore_if_no_subs_flag = False - custom_dl_obj.dl_if_subs_list = [] - - if version < 2003464: # v2.3.464 - - # This version adds a new IV to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.block_flag = False - - if version < 2003470: # v2.3.470 - - # This version modifies IVs in media.Scheduled objects - for scheduled_obj in self.scheduled_list: - scheduled_obj.timetable_list = [] - scheduled_obj.timetable_window = 300 - if scheduled_obj.start_mode == 'scheduled': - scheduled_obj.start_mode = 'repeat' - if scheduled_obj.start_mode == 'none': - scheduled_obj.start_mode = 'disabled' - - if version < 2003487: # v2.3.487 - - # This version provides options.OptionsManager objects with a - # short description - for options_obj in options_obj_list: - - if options_obj == self.general_options_obj: - options_obj.descrip = _( - 'General (default) download options', - ) - - elif self.classic_options_obj \ - and options_obj == self.classic_options_obj: - options_obj.descrip = _( - 'Download options for the Classic Mode tab', - ) - - else: - options_obj.descrip = options_obj.name - - # This version also adds options.OptionsManager objects to an - # ordered list for use in the Drag and Drop tab - self.classic_dropzone_list = [self.general_options_obj.uid] - - if self.classic_options_obj: - self.classic_dropzone_list.append(self.classic_options_obj.uid) - - # If the user has already created an options manager called 'mp3', - # use it; otherwise create a new one (as self.start() does) - match_flag = False - for options_obj in options_obj_list: - if options_obj.name == 'mp3': - match_flag = True - self.classic_dropzone_list.append(options_obj.uid) - break - - if not match_flag: - - mp3_options_obj = self.create_download_options('mp3') - mp3_options_obj.set_mp3_options() - self.classic_dropzone_list.append(mp3_options_obj.uid) - - if version < 2003510: # v2.3.510 - - # This version adds new IVs to downloads.CustomDLManager objects - for custom_dl_obj in self.custom_dl_reg_dict.values(): - - custom_dl_obj.ignore_stream_flag = False - custom_dl_obj.ignore_old_stream_flag = False - custom_dl_obj.dl_if_stream_flag = False - custom_dl_obj.dl_if_old_stream_flag = False - - if version < 2003536: # v2.3.536 - - # This version adds a new IV to media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.dummy_dl_flag = False - - if version < 2003553: # v2.3.553 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['no_overwrites'] = False - - if version < 2003554: # v2.3.554 - - # Fix for Git #399, remove invalid options.OptionsManager objects - # from the dropzone list - dropzone_list = [] - for uid in self.classic_dropzone_list: - if uid in self.options_reg_dict: - dropzone_list.append(uid) - - self.classic_dropzone_list = dropzone_list - - if version < 2003595: # v2.3.595 - - # This version adds a new IV to media.Channel and media.Playlist - # objects - for dbid in self.old_container_reg_dict.values(): - media_data_obj = self.media_reg_dict[dbid] - if not isinstance(media_data_obj, media.Folder): - media_data_obj.enhanced \ - = utils.is_enhanced(media_data_obj.source) - - if version < 2003607: # v2.3.607 - - # Git #405: sticky plaster to repair this broken database - - # options.OptionsManager objects have been applied to media data - # objects, but are not in the registry - for media_data_obj in self.media_reg_dict.values(): - - if media_data_obj.options_obj \ - and not media_data_obj.options_obj.uid \ - in self.options_reg_dict: - self.options_reg_dict[media_data_obj.options_obj.uid] \ - = media_data_obj.options_obj - - # (options.OptionsManager.dbid does not match its own - # media data object, so fix that as well) - media_data_obj.options_obj.dbid = media_data_obj.dbid - - # Because of that error, options.OptionsManager have not been - # updated by this function - test_options_obj = options.OptionsManager(-1, 'test') - for real_options_obj in self.options_reg_dict.values(): - - if not hasattr(real_options_obj, 'descrip'): - real_options_obj.descrip = real_options_obj.name - - for option in test_options_obj.options_dict: - if not option in real_options_obj.options_dict: - - if isinstance( - test_options_obj.options_dict[option], - list, - ) or isinstance( - test_options_obj.options_dict[option], - dict, - ): - real_options_obj.options_dict[option] \ - = test_options_obj.options_dict[option].copy() - else: - real_options_obj.options_dict[option] \ - = test_options_obj.options_dict[option] - - if version < 2004056: # v2.4.056 - - # This version adds new options to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['live_from_start'] = False - options_obj.options_dict['wait_for_video_min'] = 0 - - if version < 2004059: # v2.4.059 - - # This version removes an obsolete download option from - # options.OptionsManager - for options_obj in options_obj_list: - if 'save_path_list' in options_obj.options_dict: - del options_obj.options_dict['save_path_list'] - - if version < 2004074: # v2.4.074 - - # Perform repairs on self.classic_dropzone_list, which was - # corrupted when any of the options.OptionsManager objects were - # reset - mod_list = [] - for uid in self.classic_dropzone_list: - if uid in self.options_reg_dict: - mod_list.append(uid) - - self.classic_dropzone_list = mod_list - - if version < 2004084: # v2.4.084 - - # This version adds new options to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['playlist_items'] = '' - - if version < 2004132: # 2.4.132 - - # This version changes the format of media.Scheduled objects' - # media lists - for scheduled_obj in self.scheduled_list: - - media_list = [] - for name in scheduled_obj.media_list: - media_list.append(self.old_container_reg_dict[name]) - - scheduled_obj.media_list = media_list - - # This version changes the format of the old container registry, - # temporarily stored in self.old_container_reg_dict - for dbid in self.old_container_reg_dict.values(): - - # (One can never be certain...) - if dbid in self.media_reg_dict: - self.container_reg_dict[dbid] = self.media_reg_dict[dbid] - - self.old_container_reg_dict = [] - - # This version modifies the value stored in the 'use_fixed_folder' - # download option, replacing a literal folder name with one of - # three permanent values - for options_obj in options_obj_list: - - folder_name = options_obj.options_dict['use_fixed_folder'] - if folder_name == formats.FOLDER_TEMPORARY_VIDEOS \ - or folder_name == self.fixed_temp_folder.name: - options_obj.options_dict['use_fixed_folder'] = 'temp' - elif folder_name == formats.FOLDER_UNSORTED_VIDEOS \ - or folder_name == self.fixed_misc_folder.name: - options_obj.options_dict['use_fixed_folder'] = 'misc' - elif folder_name == formats.FOLDER_VIDEO_CLIPS \ - or folder_name == self.fixed_clips_folder.name: - options_obj.options_dict['use_fixed_folder'] = 'clips' - else: - # Failsafe - options_obj.options_dict['use_fixed_folder'] = None - - if version < 2004139: # v2.4.139 - - # This version converts a scalar value to a list value in - # options.OptionsManager - for options_obj in options_obj_list: - - if hasattr(options_obj, 'dbid'): - - if options_obj.dbid is None: - options_obj.dbid_list = [] - else: - options_obj.dbid_list = [options_obj.dbid] - - del options_obj.dbid - - if version < 2004188: # v2.4.188 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['abort_on_unavailable_fragment'] \ - = False - - if version < 2004195: # v2.4.195 - - # This version fixes the dropzone list (in the Drag and Drop tab) - # which was not reset correctly, when creating a new Tartube - # database - new_list = [] - for uid in self.classic_dropzone_list: - if uid in self.options_reg_dict: - new_list.append(uid) - - self.classic_dropzone_list = new_list - - if version < 2004196: # v2.4.196 - - # This version adds a new IV to all media data objects - for media_data_obj in self.media_reg_dict.values(): - media_data_obj.natname = media_data_obj.get_natural_name( - media_data_obj.nickname, - ) - - if version < 2004213: # v2.4.213 - - # This version removes an IV from media.Channel, media.Playlist and - # media.Folder objects - for media_data_obj in self.container_reg_dict.values(): - if hasattr(media_data_obj, 'last_sort_mode'): - del media_data_obj.last_sort_mode - - if version < 2004339: # v2.4.339 - - # This version adds a new IV to all media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.dummy_sblock_flag = False - - # --- Do this last, or the call to .check_integrity_db() fails ------- - # -------------------------------------------------------------------- - - if version < 2002115: # v2.2.115 - - # The update code for v1.4.037 and v2.1.012 crashes with a python - # error, when loading a v1.4 database - # Can fix both problems by doing a silent database integrity check - self.check_integrity_db(True) - - elif self.check_broken_objs(): - - # v2.3.356: Git #356 reports failure of self.update_db() to update - # the database correctly, in a way that's hard to analyse - # Solve all future issues of this kind by routinely checking - # that media data objects have the IVs they're supposed to have - self.fix_broken_objs() - - - def save_db(self): - - """Called by self.start(), .stop_continue(), .switch_db(), - .fix_integrity_db(), .download_manager_finished(), - .update_manager_finished(), .refresh_manager_finished(), - .info_manager_finished(), .tidy_manager_finished(), - .move_container_to_top_continue(), .move_container_continue(), - .rename_container(), .on_menu_save_all() and .on_menu_save_db(). - - Saves the Tartube database file. - - Since v2.3.555 (Git #400), file loading/saving is no longer disabled if - saving the database fails (so the user can correct a problem like a - full hard drive, before trying again). - - Return values: - - True on success, False on failure - - """ - - # Sanity check - if self.current_manager_obj \ - or self.disable_load_save_flag \ - or not self.allow_db_save_flag: - return False - - # Prepare values - local = utils.get_local_time() - path = os.path.abspath(os.path.join(self.data_dir, self.db_file_name)) - bu_path = os.path.abspath( - os.path.join( - self.backup_dir, - __main__.__packagename__ + '_BU.db', - ), - ) - temp_bu_path = os.path.abspath( - os.path.join( - self.backup_dir, - __main__.__packagename__ + '_TEMP_BU.db', - ), - ) - - # Prepare a dictionary of data to save, using Python pickle - save_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(local.strftime('%d %b %Y')), - 'save_time': str(local.strftime('%H:%M:%S')), - # Custom downloads - 'custom_dl_reg_count': self.custom_dl_reg_count, - 'custom_dl_reg_dict': self.custom_dl_reg_dict, - 'general_custom_dl_obj': self.general_custom_dl_obj, - 'classic_custom_dl_obj': self.classic_custom_dl_obj, - # Media data objects - 'media_reg_count': self.media_reg_count, - 'media_reg_dict': self.media_reg_dict, - 'container_reg_dict': self.container_reg_dict, - 'container_top_level_list': self.container_top_level_list, - 'media_reg_live_dict': self.media_reg_live_dict, - 'media_reg_auto_notify_dict': self.media_reg_auto_notify_dict, - 'media_reg_auto_alarm_dict': self.media_reg_auto_alarm_dict, - 'media_reg_auto_open_dict': self.media_reg_auto_open_dict, - 'media_reg_auto_dl_start_dict': self.media_reg_auto_dl_start_dict, - 'media_reg_auto_dl_stop_dict': self.media_reg_auto_dl_stop_dict, - 'fixed_all_folder': self.fixed_all_folder, - 'fixed_bookmark_folder': self.fixed_bookmark_folder, - 'fixed_fav_folder': self.fixed_fav_folder, - 'fixed_live_folder': self.fixed_live_folder, - 'fixed_missing_folder': self.fixed_missing_folder, - 'fixed_new_folder': self.fixed_new_folder, - 'fixed_recent_folder': self.fixed_recent_folder, - 'fixed_waiting_folder': self.fixed_waiting_folder, - 'fixed_temp_folder': self.fixed_temp_folder, - 'fixed_misc_folder': self.fixed_misc_folder, - 'fixed_clips_folder': self.fixed_clips_folder, - 'fixed_folder_locale': self.fixed_folder_locale, - 'fixed_recent_folder_days': self.fixed_recent_folder_days, - # Scheduled downloads - 'scheduled_list': self.scheduled_list, - # Profiles - 'profile_dict': self.profile_dict, - 'last_profile': self.last_profile, - # Download options - 'options_reg_count' : self.options_reg_count, - 'options_reg_dict' : self.options_reg_dict, - 'general_options_obj' : self.general_options_obj, - 'classic_options_obj' : self.classic_options_obj, - 'classic_dropzone_list': self.classic_dropzone_list, - # FFmpeg options - 'ffmpeg_reg_count' : self.ffmpeg_reg_count, - 'ffmpeg_reg_dict' : self.ffmpeg_reg_dict, - 'ffmpeg_options_obj' : self.ffmpeg_options_obj, - 'simple_ffmpeg_options_flag' : self.simple_ffmpeg_options_flag, - # Main window toolbar - 'toolbar_system_hide_flag' : self.toolbar_system_hide_flag, - # Video Catalogue toolbar - 'catalogue_sort_mode' : self.catalogue_sort_mode, - 'catalogue_reverse_sort_flag': self.catalogue_reverse_sort_flag, - } - - # Back up any existing file - if os.path.isfile(path): - try: - shutil.copyfile(path, temp_bu_path) - - except: -# self.disable_load_save() - self.disable_scheduled_dl() - self.file_error_dialogue( - _('Failed to save the Tartube database file') \ - + '\n\n' \ - + _( - '(Could not make a backup copy of the existing file)' - ) \ - + '\n\n' \ - + _('File load/save has been disabled'), - ) - - return False - - # If there is no lock already in place (for example, because this is a - # new database file), then create a lockfile - if not self.debug_ignore_lockfile_flag: - - if self.db_lock_file_path is None: - - lock_path = path + '.lock' - if os.path.isfile(lock_path): - - self.system_error( - 101, - 'Database file \'' + path + '\' already exists,' \ - + ' and is locked', - ) - - return False - - else: - - # Place our own lock on the database file - try: - fh = open(lock_path, 'a').close() - self.db_lock_file_path = lock_path - - except: -# self.disable_load_save( -# _( -# 'Failed to save the Tartube database file (file' \ -# + ' already in use)', -# ), -# ) - self.disable_scheduled_dl() - self.file_error_dialogue( - _( - 'Failed to save the Tartube database file (file' \ - + ' already in use)', - ), - ) - - return False - - # Try to save the database file - try: - fh = open(path, 'wb') - pickle.dump(save_dict, fh) - fh.close() - - except: - -# self.disable_load_save() - self.disable_scheduled_dl() - - if os.path.isfile(temp_bu_path): - self.file_error_dialogue( - _('Failed to save the Tartube database file') \ - + '\n\n' \ - + _('A backup of the previous file can be found at:') \ - + '\n\n ' + temp_bu_path + '\n\n' \ - + _('File load/save has been disabled'), - ) - - else: - self.file_error_dialogue( - _('Failed to save the Tartube database file') \ - + '\n\n' + _('File load/save has been disabled'), - ) - - return False - - # In the event that there was no database file to backup, then the - # following code isn't necessary - if os.path.isfile(temp_bu_path): - - # Make the backup file permanent, or not, depending on settings - if self.db_backup_mode == 'default': - self.remove_file(temp_bu_path) - - elif self.db_backup_mode == 'single': - - utils.rename_file(self, temp_bu_path, bu_path) - - elif self.db_backup_mode == 'daily': - - daily_bu_path = os.path.abspath( - os.path.join( - self.backup_dir, - __main__.__packagename__ + '_BU_' \ - + str(local.strftime('%Y_%m_%d')) + '.db', - ), - ) - - # Only make a new backup file once per day - if not os.path.isfile(daily_bu_path): - utils.rename_file(self, temp_bu_path, daily_bu_path) - else: - self.remove_file(temp_bu_path) - - elif self.db_backup_mode == 'always': - - always_bu_path = os.path.abspath( - os.path.join( - self.backup_dir, - __main__.__packagename__ + '_BU_' \ - + str(local.strftime('%Y_%m_%d_%H_%M_%S')) + '.db', - ), - ) - - utils.rename_file(self, temp_bu_path, always_bu_path) - - # Saving a database file, in order to create a new file, is much like - # loading one: main window widgets can now be sensitised - self.main_win_obj.sensitise_widgets_if_database(True) - - # Save succeeded. Permit scheduled downloads again, if they were - # disabled in an earlier unsuccessful call to this function - self.disable_scheduled_dl_flag = False - - return True - - - def switch_db(self, data_list): - - """Called by config.SystemPrefWin.try_switch_db(). - - When the user selects a new location for a data directory, first save - our existing database. - - Then load the database at the new location, if exists, or create a new - database there, if not. - - Args: - - data_list (list): A list containing two items: the full file path - to the location of the new data directory, and the system - preferences window (config.SystemPrefWin) that the user has - open - - Return values: - - True on success, False on failure - - """ - - # Extract values from the argument list - path = data_list.pop(0) - pref_win_obj = data_list.pop(0) - - # Sanity check - if self.current_manager_obj or self.disable_load_save_flag: - return False - - # If the old path is the same as the new one, we don't need to do - # anything - if path == self.data_dir: - return False - - # Save the existing database, and release its lockfile - if not self.save_db(): - return False - else: - self.remove_db_lock_file() - - # If the new database file is not loaded for any reason, then we can - # restore the values of various IVs. (As far as the user is - # concerned, nothing has happened) - self.backup_data_variables_before_switch() - - # Update IVs for the new location of the data directory - self.data_dir = path - self.update_data_dirs() - - if self.data_dir_add_from_list_flag \ - and not self.data_dir in self.data_dir_alt_list: - self.data_dir_alt_list.append(self.data_dir) - - # Before v1.3.099, self.data_dir and self.downloads_dir were different - # If a /downloads directory exists, then the data directory is using - # the old structure - if os.path.isdir(self.alt_downloads_dir): - - # Use the old location of self.downloads_dir - self.downloads_dir = self.alt_downloads_dir - - else: - - # Use the new location - self.downloads_dir = self.data_dir - - # If the data directory, and/or any of its sub-directories don't exist, - # then try to create them - if not os.path.isdir(self.data_dir) \ - and not self.make_directory(self.data_dir): - return False - - if not os.path.isdir(self.backup_dir) \ - and not self.make_directory(self.backup_dir): - return False - - # If the database file itself doesn't exist, create it. Otherwise, try - # to load it - db_path = os.path.abspath( - os.path.join(self.data_dir, self.db_file_name), - ) - if not os.path.isfile(db_path): - - # Reset main window widgets - # (Don't reset the Erors/Warnings tab, as failed attempts to load a - # database generate messages there) - self.main_win_obj.video_index_reset() - self.main_win_obj.video_catalogue_reset() - self.main_win_obj.progress_list_reset() - self.main_win_obj.results_list_reset() - self.main_win_obj.drag_drop_grid_empty() - - # Reset database IVs - self.reset_db() - - # Create a new database file - self.save_db() - - # Save the config file, to preserve the new location of the data - # directory - self.save_config() - - # Repopulate the Video Index, showing the new data - self.main_win_obj.video_index_populate() - # Reset the Drag and Drop tab - self.main_win_obj.drag_drop_grid_reset() - - # If the system preferences window is open, reset it to show the - # new data directory - if pref_win_obj and pref_win_obj.is_visible(): - - pref_win_obj.reset_window() - pref_win_obj.select_switch_db_tab() - - self.dialogue_manager_obj.show_msg_dialogue( - _('Database file created'), - 'info', - 'ok', - pref_win_obj, - ) - - else: - - # (Parent window is the main window) - self.dialogue_manager_obj.show_msg_dialogue( - _('Database file created'), - 'info', - 'ok', - ) - - # Update temporary directories for both the old and new - # database locations - self.update_temporary_dirs_after_switch() - - # Reset the backup values for various IVs that we no longer need - self.clear_data_variables_after_switch() - - return True - - elif not self.load_db(True): - - # Failed to load the database file. Restore the values for various - # IVs - self.restore_data_variables_after_switch() - - return False - - else: - - # Successfully loaded the database file. Update temporary - # directories for both the old and new database locations - self.update_temporary_dirs_after_switch() - - # Reset the backup values for various IVs that we no longer need - self.clear_data_variables_after_switch() - - # Save the config file, to preserve the new location of the data - # directory - self.save_config() - return True - - - def backup_data_variables_before_switch(self): - - """Called by self.switch_db(). - - Before loading the replacement database, make a backup copy of several - IVs. If the load fails, then those values can be restored (in a call to - self.restore_data_variables_after_switch() ), and the user can continue - using the previous database, as before. - """ - - self.backup_data_dir = self.data_dir - self.backup_downloads_dir = self.downloads_dir - self.backup_alt_downloads_dir = self.alt_downloads_dir - self.backup_backup_dir = self.backup_dir - self.backup_temp_dir = self.temp_dir - self.backup_temp_dl_dir = self.temp_dl_dir - self.backup_temp_test_dir = self.temp_test_dir - self.backup_data_dir_alt_list = self.data_dir_alt_list.copy() - - - def clear_data_variables_after_switch(self): - - """Called by self.switch_db(). - - After succesfully loading a replacement database, reset the backup - copies of several IVs we made, in case the load failed. - """ - - self.backup_data_dir = None - self.backup_downloads_dir = None - self.backup_alt_downloads_dir = None - self.backup_backup_dir = None - self.backup_temp_dir = None - self.backup_temp_dl_dir = None - self.backup_temp_test_dir = None - self.backup_data_dir_alt_list = None - - - def restore_data_variables_after_switch(self): - - """Called by self.switch_db(). - - After failing to load a replacement database, restore the original - values of several IVs, so the user can continue using the previous - database, as before. - """ - - self.data_dir = self.backup_data_dir - self.downloads_dir = self.backup_downloads_dir - self.alt_downloads_dir = self.backup_alt_downloads_dir - self.dir = self.backup_dir - self.temp_dir = self.backup_temp_dir - self.temp_dl_dir = self.backup_temp_dl_dir - self.temp_test_dir = self.backup_temp_test_dir - self.data_dir_alt_list = self.backup_data_dir_alt_list.copy() - - - def update_temporary_dirs_after_switch(self): - - """Called by self.switch_db(). - - After succesfully loading a replacement database, remove temporary - directories, both for the old and new database files. - """ - - # For the old database, delete Tartube's temporary folder from the - # filesystem - if os.path.isdir(self.backup_temp_dir): - self.remove_directory(self.backup_temp_dir) - - # For the new database, the temporary data directory should be emptied, - # if it already exists) - if os.path.isdir(self.temp_dir) \ - and not self.remove_directory(self.temp_dir): - - if not self.make_directory(self.temp_dir): - return False - else: - self.remove_directory(self.temp_dir) - - if not os.path.isdir(self.temp_dir): - self.make_directory(self.temp_dir) - - if not os.path.isdir(self.temp_dl_dir): - self.make_directory(self.temp_dl_dir) - - if not os.path.isdir(self.temp_test_dir): - self.make_directory(self.temp_test_dir) - - - def choose_alt_db(self): - - """Called by self.start() (only), shortly after loading (or creating) - the config file. - - Multiple instances of Tartube can share the same config file, but not - the same database file. - - If the database file specified by the config file we've just loaded - is locked (meaning it's in use by another instance), we might be - able to use one of the alternative data directories specified by the - user. - """ - - db_file_path = os.path.abspath( - os.path.join(self.data_dir, self.db_file_name), - ) - - lock_file_path = db_file_path + '.lock' - - if os.path.exists(self.data_dir) \ - and os.path.isfile(db_file_path) \ - and os.path.isfile(lock_file_path) \ - and not self.debug_ignore_lockfile_flag: - - msg = 'Tartube database \'{0}\' can\'t be loaded - another' \ - + ' instance of Tartube may be using it. If not, you can' \ - + ' fix this problem by deleting the lockfile \'{1}\'' - - self.system_warning( - 102, - msg.format(self.data_dir, lock_file_path), - ) - - for alt_data_dir in self.data_dir_alt_list: - - if alt_data_dir == self.data_dir: - # Already tried this one - continue - - alt_db_file_path = os.path.abspath( - os.path.join(alt_data_dir, self.db_file_name), - ) - - alt_lock_file_path = alt_db_file_path + '.lock' - - if os.path.exists(alt_data_dir) \ - and os.path.isfile(alt_db_file_path) \ - and ( - not os.path.isfile(alt_lock_file_path) \ - or self.debug_ignore_lockfile_flag - ): - # Try loading this database instead - self.data_dir = alt_data_dir - self.update_data_dirs() - - return - - else: - - msg = 'Tartube database \'{0}\' can\'t be loaded' \ - + ' - another instance of Tartube may be using it.' \ - + ' If not, you can fix this problem by deleting' \ - + ' the lockfile \'{1}\'' - - self.system_warning( - 103, - msg.format(alt_data_dir, alt_lock_file_path), - ) - - - def forget_db(self, data_list): - - """Called by config.SystemPrefWin.on_data_dir_forget_button_clicked(). - - When the user selects a data directory to be forgotten (i.e. removed - from self.data_dir_alt_list), perform that action. - - Args: - - data_list (list): A list containing two items: the full file path - to the location of the selected data directory, and the system - preferences window (config.SystemPrefWin) that the user has - open - - Return values: - - True on success, False on failure - - """ - - # Extract values from the argument list - path = data_list.pop(0) - pref_win_obj = data_list.pop(0) - - # Sanity check. It shouldn't be possible to select the current data - # directory, but we'll check anyway - if self.current_manager_obj \ - or self.disable_load_save_flag \ - or path == self.data_dir: - return False - - # Update the IV - if path in self.data_dir_alt_list: - self.data_dir_alt_list.remove(path) - - # If the system preferences window is open, reset it to show the new - # contents of the IV - if pref_win_obj and pref_win_obj.is_visible(): - pref_win_obj.reset_window() - pref_win_obj.select_switch_db_tab() - - # Procedure complete - return True - - - def forget_all_db(self, pref_win_obj=None): - - """Called by - config.SystemPrefWin.on_data_dir_forget_all_button_clicked(). - - When the user wants to forget all data directories except the current - one, perform that action. - - Args: - - pref_win_obj (config.SystemPrefWin): The system preferences window - that the user has open, if any - - Return values: - - True on success, False on failure - - """ - - # Sanity check - if self.current_manager_obj or self.disable_load_save_flag: - return False - - # Update the IV - self.data_dir_alt_list = [ self.data_dir ] - - # If the system preferences window is open, reset it to show the new - # contents of the IV - if pref_win_obj and pref_win_obj.is_visible(): - pref_win_obj.reset_window() - pref_win_obj.select_switch_db_tab() - - # Procedure complete - return True - - - def reset_db(self): - - """Called by self.switch_db(). - - Resets media registry IVs, so that a new Tartube database file can be - created. - """ - - # Reset IVs to their default states - self.custom_dl_reg_count = 0 - self.custom_dl_reg_dict = {} - self.general_custom_dl_obj = self.create_custom_dl_manager('general') - self.classic_custom_dl_obj = self.create_custom_dl_manager('classic') - self.media_reg_count = 0 - self.media_reg_dict = {} - self.container_reg_dict = {} - self.old_container_reg_dict = [] - self.container_top_level_list = [] - self.media_reg_live_dict = {} - self.media_reg_auto_notify_dict = {} - self.media_reg_auto_alarm_dict = {} - self.media_reg_auto_open_dict = {} - self.media_reg_auto_dl_start_dict = {} - self.media_reg_auto_dl_stop_dict = {} - self.fixed_all_folder = None - self.fixed_bookmark_folder = None - self.fixed_fav_folder = None - self.fixed_live_folder = None - self.fixed_missing_folder = None - self.fixed_new_folder = None - self.fixed_recent_folder = None - self.fixed_waiting_folder = None - self.fixed_temp_folder = None - self.fixed_misc_folder = None - self.fixed_clips_folder = None - self.fixed_folder_locale = self.current_locale - self.scheduled_list = [] - self.options_reg_count = 0 - self.options_reg_dict = {} - self.general_options_obj = self.create_download_options('general') - self.classic_options_obj = self.create_download_options('classic') - self.classic_dropzone_list = [ - self.general_custom_dl_obj.uid, - self.classic_custom_dl_obj.uid, - ] - self.ffmpeg_reg_count = 0 - self.ffmpeg_reg_dict = {} - self.ffmpeg_options_obj = self.create_ffmpeg_options('default') - self.simple_ffmpeg_options_flag = True - self.toolbar_system_hide_flag = False - - # Create new fixed folders (which sets the values of - # self.fixed_all_folder, etc) - self.create_fixed_folders() - - - def check_integrity_db(self, no_prompt_flag=False, parent_win_obj=None): - - """Called by config.SystemPrefWin.on_data_check_button_clicked() and - also by self.update_db(). - - In case the Tartube database contains inconsistencies of any kind (for - example, an earlier failure in mainwin.DeleteContainerDialogue left - some channel/playlist/folder objects in a half-deleted state), check - the database for inconsistencies. - - If inconsistencies are found, prompt the user for permission to - repair them. The repair process only updates Tartube data; it doesn't - modify any other files or folders in the user's filesystem. - - Args: - - no_prompt_flag (bool): If True, don't prompt the user to repair - errors; just go ahead and repair them - - parent_win_obj (config.SystemPrefWin): Specified when called from - the preferences window, in which case that window is presented - (moved to the forefront) when the confirmation window is - closed, instead of the main window - - """ - - # Basic checks - if self.disable_load_save_flag: - - self.system_error( - 104, - 'Cannot check/fix database after load/save has been disabled', - ) - - return - - if self.current_manager_obj: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Tartube\'s database can\'t be checked while an operation is' \ - + ' in progress', - ), - 'error', - 'ok', - ) - - return - - # Check the database, looking for media.Video, media.Channel, - # media.Playlist and media.Folder objects (or their .dbids) that, - # due to some problem or other, appear in one IV but not another - # If inconsistencies are found, add them to this dictionary, and - # then apply the fixes once we've finished checking everything - error_reg_dict = {} - # (Two additional dictionaries for recording any errors in the - # .master_dbid and .slave_dbid_list IVs, which are fixed separately) - error_master_dict = {} - error_slave_dict = {} - # (The number of channel/playlist/folders whose flag counts are wrong) - flag_error_count = 0 - # (Two further dictionaries for recording inconsistencies in - # options.OptionsManager objects) - error_options_dict = {} - error_options_media_dict = {} - error_options_media_reverse_dict = {} - # (The number of media data objects which appear to have missing IVs, - # because of some failure or other in self.update_db() ) - broken_obj_count = 0 - - # Check that entries in self.container_reg_dict appear in - # self.media_reg_dict - for dbid in self.container_reg_dict.keys(): - if not dbid in self.media_reg_dict \ - or self.container_reg_dict[dbid] != self.media_reg_dict[dbid]: - error_reg_dict[dbid] = None - - # Check that entries in self.container_top_level_list appear in - # self.media_reg_dict - for dbid in self.container_top_level_list: - if not dbid in self.media_reg_dict: - error_reg_dict[dbid] = None - - # Check that entries in self.media_reg_live_dict (and its subsets) - # appear in self.media_reg_dict - for dbid in self.media_reg_live_dict.keys(): - if not dbid in self.media_reg_dict: - error_reg_dict[dbid] = None - - for dbid in self.media_reg_auto_notify_dict.keys(): - if not dbid in self.media_reg_dict: - error_reg_dict[dbid] = None - - for dbid in self.media_reg_auto_alarm_dict.keys(): - if not dbid in self.media_reg_dict: - error_reg_dict[dbid] = None - - for dbid in self.media_reg_auto_open_dict.keys(): - if not dbid in self.media_reg_dict: - error_reg_dict[dbid] = None - - for dbid in self.media_reg_auto_dl_start_dict.keys(): - if not dbid in self.media_reg_dict: - error_reg_dict[dbid] = None - - for dbid in self.media_reg_auto_dl_stop_dict.keys(): - if not dbid in self.media_reg_dict: - error_reg_dict[dbid] = None - - # self.media_reg_dict contains, in theory, every video/channel/ - # playlist/folder object - # Walk the tree whose top level is self.container_top_level_list to get - # a list of all containers - toplevel_container_obj_list = [] - for dbid in self.container_top_level_list: - if not dbid in error_reg_dict: - toplevel_container_obj_list.append(self.media_reg_dict[dbid]) - - full_container_obj_list = [] - for container_obj in toplevel_container_obj_list: - - full_container_obj_list.extend( - container_obj.compile_all_containers( [] ), - ) - - # v2.1.029 In some older databases, a fixed folder called 'downloads_2' - # was created, containing a small number of videos. I'm still not - # sure under which circumstances that folder was created; in any - # case, such a folder should be deleted - for container_obj in full_container_obj_list: - if isinstance(container_obj, media.Folder) \ - and container_obj.fixed_flag \ - and not self.check_fixed_folder(container_obj): - error_reg_dict[container_obj.dbid] = container_obj - - # Make a copy of self.media_reg_dict... - check_reg_dict = self.media_reg_dict.copy() - # ...then compare the list of containers (and their child videos), - # looking for any which don't appear in self.media_reg_dict - for container_obj in full_container_obj_list: - - if container_obj.dbid in self.media_reg_dict: - - # Container OK - if container_obj.dbid in check_reg_dict: - del check_reg_dict[container_obj.dbid] - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - - if not child_obj.dbid in self.media_reg_dict \ - or child_obj != self.media_reg_dict[child_obj.dbid]: - # Child video not OK - error_reg_dict[child_obj.dbid] = child_obj - else: - # Child video OK - if child_obj.dbid in check_reg_dict: - del check_reg_dict[child_obj.dbid] - - else: - # Container not OK - error_reg_dict[container_obj.dbid] = container_obj - - # Anything left in check_reg_dict shouldn't be there - for dbid in check_reg_dict: - error_reg_dict[dbid] = check_reg_dict[dbid] - - # Check every media data object's parent - for media_data_obj in self.media_reg_dict.values(): - if media_data_obj.parent_obj is not None \ - and ( - not media_data_obj.parent_obj.dbid in self.media_reg_dict \ - or isinstance(media_data_obj.parent_obj, media.Video) \ - or not media_data_obj in media_data_obj.parent_obj.child_list - ): - error_reg_dict[media_data_obj.dbid] = media_data_obj - - # Check every media data object's children (but don't check private - # folders, as their children are also stored in a different - # channel/playlist/folder) - for media_data_obj in self.media_reg_dict.values(): - - if not isinstance(media_data_obj, media.Video) \ - and ( - not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.priv_flag - ): - for child_obj in media_data_obj.child_list: - if child_obj.parent_obj is None \ - or child_obj.parent_obj != media_data_obj: - error_reg_dict[child_obj.dbid] = child_obj - - # Check alternative download destinations for each channel/playlist/ - # folder - for media_data_obj in self.media_reg_dict.values(): - if not isinstance(media_data_obj, media.Video): - - # (Check the destination still exists in the media data - # registry) - if media_data_obj.master_dbid is not None \ - and not media_data_obj.master_dbid in self.media_reg_dict: - - error_master_dict[media_data_obj.dbid] = media_data_obj - - for slave_dbid in media_data_obj.slave_dbid_list: - if not slave_dbid in self.media_reg_dict: - error_slave_dict[media_data_obj.dbid] = media_data_obj - - # Initial check complete. Any media data object in error_reg_dict - # must have its children added too (we can't remove an object from - # the database, and not its children) - for dbid in error_reg_dict.keys(): - - media_data_obj = error_reg_dict[dbid] - if media_data_obj is not None \ - and not isinstance(media_data_obj, media.Video): - - descendant_list = media_data_obj.compile_all_containers( [] ) - for descendant_obj in descendant_list: - - error_reg_dict[descendant_obj.dbid] = descendant_obj - for child_obj in descendant_obj.child_list: - if isinstance(child_obj, media.Video): - error_reg_dict[child_obj.dbid] = child_obj - - # Check that container counts are correct - for dbid in self.container_reg_dict.keys(): - - # (Don't bother checking broken media data objects, since all - # counts for all channels/playlists/folders will be recalculated - # anyway) - if dbid not in error_reg_dict: - - media_data_obj = self.media_reg_dict[dbid] - if media_data_obj.test_counts(): - flag_error_count += 1 - - # Failsafe check: it shouldn't be possible for system folders to be - # in error_reg_dict, but check anyway, and discard them if found - mod_error_reg_dict = {} - for dbid in error_reg_dict.keys(): - - media_data_obj = error_reg_dict[dbid] - - # (The corresponding media.Video, media.Channel, media.Playlist or - # media.Folder may be known, or not) - if media_data_obj is None \ - or not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.fixed_flag \ - or not self.check_fixed_folder(media_data_obj): - mod_error_reg_dict[dbid] = media_data_obj - - # Final check on options.OptionsManager objects - for options_obj in self.options_reg_dict.values(): - - for dbid in options_obj.dbid_list: - - if not dbid in self.media_reg_dict: - error_options_dict[options_obj.uid] = options_obj - - else: - media_data_obj = self.media_reg_dict[dbid] - if media_data_obj.options_obj is None \ - or media_data_obj.options_obj != options_obj: - error_options_dict[options_obj.uid] = options_obj - error_options_media_dict[media_data_obj.dbid] \ - = media_data_obj - - for media_data_obj in self.media_reg_dict.values(): - - if media_data_obj.options_obj \ - and not media_data_obj.options_obj.uid in self.options_reg_dict: - error_options_media_reverse_dict[media_data_obj.dbid] \ - = media_data_obj.options_obj - - # .update_db() updates objects from earlier releases of Tartube with - # new IVs. Git #356 reports a database that has become broken due to - # missing IVs that .update_db() was not able to add - # Check one example of each type of object, looking for missing IVs - broken_obj_count = self.check_broken_objs() - - # Check complete - if not mod_error_reg_dict \ - and not error_master_dict \ - and not error_slave_dict \ - and not flag_error_count \ - and not error_options_dict \ - and not error_options_media_dict \ - and not error_options_media_reverse_dict \ - and not broken_obj_count: - - if not no_prompt_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - _('Database check complete, no inconsistencies found'), - 'info', - 'ok', - parent_win_obj, - ) - - return - - elif no_prompt_flag: - - # Don't prompt the user to repair errors; just go ahead and repair - # them - self.fix_integrity_db( - [ - mod_error_reg_dict, - error_master_dict, - error_slave_dict, - error_options_dict, - error_options_media_dict, - error_options_media_reverse_dict, - broken_obj_count, - no_prompt_flag, - parent_win_obj, - ], - ) - - else: - - total = len(error_reg_dict) + len(error_master_dict) \ - + len(error_slave_dict) + flag_error_count \ - + len(error_options_dict) + len(error_options_media_dict) \ - + len(error_options_media_reverse_dict) - - # Prompt the user before deleting stuff - self.dialogue_manager_obj.show_simple_msg_dialogue( - _('Database check complete, problems found:') \ - + ' ' + str(total) + '\n\n' \ - + _( - 'Do you want to repair these problems? (The database will be' \ - + ' fixed, but no files will be deleted)', - ), - 'question', - 'yes-no', - parent_win_obj, - # Arguments passed directly to .fix_integrity_db() - { - 'yes': 'fix_integrity_db', - 'data': [ - mod_error_reg_dict, - error_master_dict, - error_slave_dict, - error_options_dict, - error_options_media_dict, - error_options_media_reverse_dict, - broken_obj_count, - no_prompt_flag, - parent_win_obj, - ], - }, - ) - - - def fix_integrity_db(self, data_list): - - """Called by self.check_integrity_db() (only). - - After the user has given permission to fix inconsistencies in the - Tartube database, perform the repairs, and save files. - - The repair process only updates Tartube IVs; it doesn't delete any - files or folders in the filesystem. - - Args: - - data_list (list): A list containing several dictionaries and some - flags. List in the form: - - error_reg_dict[dbid] = media_data_obj - error_reg_dict[dbid] = None - - (A general dictionary of errors to fix. All references to - the media data objects in this dictionary are removed from - all IVs) - - error_master_dict[dbid] = media_data_obj - - (A dictionary of errors in a channel/playlist/folder's - .master_dbid IV, which are fixed separately) - - error_slave_dict[dbid] = media_data_obj - - (A dictionary of errors in a channel/playlist/folder's - .slave_dbid_list IV, which are fixed separately) - - error_options_dict[uid] = options_obj - error_options_media_dict[dbid] = media_data_obj - error_options_media_reverse_dict[dbid] = options_obj - - (Dictionaries of inconsistencies between media data objects - and options.OptionsManager objects. These - errors are fixed separately) - - broken_obj_count (list): The number of media data objects which - appear to have missing IVs, because of some failure or - other in self.update_db() - - no_prompt_flag (bool): If True, don't show a dialogue window at - the end of the procedure - - parent_win_obj (config.SystemPrefWin): Specified when called - from the preferences window, in which case that window is - presented (moved to the forefront) when the confirmation - window is closed, instead of the main window - - """ - - # Extract the arguments - error_reg_dict = data_list.pop(0) - error_master_dict = data_list.pop(0) - error_slave_dict = data_list.pop(0) - error_options_dict = data_list.pop(0) - error_options_media_dict = data_list.pop(0) - error_options_media_reverse_dict = data_list.pop(0) - broken_obj_count = data_list.pop(0) - no_prompt_flag = data_list.pop(0) - parent_win_obj = data_list.pop(0) - - # Update mainapp.TartubeApp IVs - for dbid in error_reg_dict.keys(): - - if dbid in self.media_reg_dict: - del self.media_reg_dict[dbid] - - if dbid in self.container_reg_dict: - del self.container_reg_dict[dbid] - - if dbid in self.container_top_level_list: - self.container_top_level_list.remove(dbid) - - if dbid in self.media_reg_live_dict: - del self.media_reg_live_dict[dbid] - - if dbid in self.media_reg_auto_notify_dict: - del self.media_reg_auto_notify_dict[dbid] - - if dbid in self.media_reg_auto_alarm_dict: - del self.media_reg_auto_alarm_dict[dbid] - - if dbid in self.media_reg_auto_open_dict: - del self.media_reg_auto_open_dict[dbid] - - if dbid in self.media_reg_auto_dl_start_dict: - del self.media_reg_auto_dl_start_dict[dbid] - - if dbid in self.media_reg_auto_dl_stop_dict: - del self.media_reg_auto_dl_stop_dict[dbid] - - # Check each media data object's child list, and remove anything that - # should be removed - for media_data_obj in self.media_reg_dict.values(): - if not isinstance(media_data_obj, media.Video): - - remove_list = [] - for child_obj in media_data_obj.child_list: - - if child_obj.dbid in error_reg_dict: - remove_list.append(child_obj) - - for child_obj in remove_list: - media_data_obj.child_list.remove(child_obj) - - # Recalculate counts for all channels/playlists/folders - for dbid in self.container_reg_dict.keys(): - media_data_obj = self.media_reg_dict[dbid] - media_data_obj.recalculate_counts() - - # Deal with alternative download destinations - for media_data_obj in error_master_dict.values(): - - if not media_data_obj.master_dbid in self.media_reg_dict: - media_data_obj.set_master_dbid(self, media_data_obj.dbid) - - for media_data_obj in error_slave_dict.values(): - - del_list = [] - for slave_dbid in media_data_obj.slave_dbid_list: - if not slave_dbid in self.media_reg_dict: - del_list.append(slave_dbid) - - for slave_dbid in del_list: - media_data_obj.del_slave_dbid(slave_dbid) - - # Deal with inconsistencies between the media data registry and - # options.OptionsManager objects - for options_obj in error_options_dict.values(): - options_obj.reset_dbid() - - for media_data_obj in error_options_media_dict.values(): - media_data_obj.reset_options_obj() - - if error_options_media_reverse_dict: - - # (In the case of options.OptionsManager objects missing from their - # registry, they may not have been updated in self.update_db(). - # Check for that as well) - test_options_obj = options.OptionsManager(-1, 'test') - - for dbid in error_options_media_reverse_dict.keys(): - media_data_obj = self.media_reg_dict[dbid] - options_obj = error_options_media_reverse_dict[dbid] - - self.options_reg_dict[options_obj.uid] = options_obj - media_data_obj.options_obj.add_dbid(dbid) - - if not hasattr(options_obj, 'descrip'): - options_obj.descrip = options_obj.name - - for option in test_options_obj.options_dict: - if not option in options_obj.options_dict: - - if isinstance( - test_options_obj.options_dict[option], - list, - ) or isinstance( - test_options_obj.options_dict[option], - dict, - ): - options_obj.options_dict[option] \ - = test_options_obj.options_dict[option].copy() - else: - options_obj.options_dict[option] \ - = test_options_obj.options_dict[option] - - # Deal with broken objects due to failures in .update_db() - if broken_obj_count: - self.fix_broken_objs() - - # Save the database file (unless load/save has been disabled very - # recently) - if not self.disable_load_save_flag: - self.save_db() - - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - # Show confirmation (if allowed) - if not no_prompt_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - _('Database inconsistencies repaired'), - 'info', - 'ok', - parent_win_obj, - ) - - - def check_broken_objs(self): - - """Called by self.update_db() and .check_integrity_db(). - - Git #356 reports failure of self.update_db() to update the database - correctly, in a way that's hard to analyse. - - Check that all media data objects have the IVs they're supposed to - have, and return the number of problems found. - - Return values: - - A number in the range 0-4 - - """ - - count = 0 - - # Get a specimen of each type of media data object - video_obj, channel_obj, playlist_obj, folder_obj \ - = self.compile_specimen_obj_list() - - # Check IVs in a specimen media.Video objec - if video_obj: - - iv_dict = video_obj.compile_updated_ivs() - for key in iv_dict.keys(): - - if not (hasattr(video_obj, key)): - count += 1 - break - - # Check IVs in a specimen media.Channel objec - if channel_obj: - - iv_dict = channel_obj.compile_updated_ivs() - for key in iv_dict.keys(): - - if not (hasattr(channel_obj, key)): - count += 1 - break - - # Check IVs in a specimen media.Playlist objec - if playlist_obj: - - iv_dict = playlist_obj.compile_updated_ivs() - for key in iv_dict.keys(): - - if not (hasattr(playlist_obj, key)): - count += 1 - break - - # Check IVs in a specimen media.Folder objec - if folder_obj: - - iv_dict = folder_obj.compile_updated_ivs() - for key in iv_dict.keys(): - - if not (hasattr(folder_obj, key)): - count += 1 - break - - # Return the number of problems found - return count - - - def fix_broken_objs(self): - - """Called by self.update_db() and .check_integrity_db(), immediately - after a call to self.check_broken_objs(). - - Some or all media data objects have missing IVs, as a result of a - failure somewhere in self.update_db(). Update all media data objects to - add any missing IVs with default values. - """ - - for media_data_obj in self.media_reg_dict.values(): - - update_dict = media_data_obj.compile_updated_ivs() - for key in update_dict.keys(): - - if not hasattr(media_data_obj, key): - setattr(media_data_obj, key, update_dict[key]) - - - def compile_specimen_obj_list(self): - - """Called by self.check_broken_objs(). - - Returns a list of specimen media data objects: one media.Video, - media.Channel, media.Playlist and media.Folder object. - - Return values: - - A list of objects, in the order (video, channel, playlist, folder). - If those objects haven't been created yet, the list contains - one or more None values instead - - """ - - # Find a specimen media.Video object - video_obj = None - for media_data_obj in self.media_reg_dict.values(): - - if isinstance(media_data_obj, media.Video): - - video_obj = media_data_obj - break - - # Find specimen media.Channel, media.Playlist and media.Folder objects - count = 0 - channel_obj = None - playlist_obj = None - folder_obj = None - - for dbid in self.container_reg_dict.keys(): - - media_data_obj = self.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Channel) and not channel_obj: - - channel_obj = media_data_obj - count += 1 - if count >= 3: - break - - elif isinstance(media_data_obj, media.Playlist) \ - and not playlist_obj: - - playlist_obj = media_data_obj - count += 1 - if count >= 3: - break - - elif isinstance(media_data_obj, media.Folder) \ - and not folder_obj: - - folder_obj = media_data_obj - count += 1 - if count >= 3: - break - - return video_obj, channel_obj, playlist_obj, folder_obj - - - def update_data_dirs(self): - - """Called by self.load_config() or by any other function. - - After changing the value of self.data_dir (perhaps via a call to - self.set_data_dir() ), any code can call this function to update the - variables that are derived from it. - """ - - self.downloads_dir = self.data_dir - self.alt_downloads_dir = os.path.abspath( - os.path.join(self.data_dir, 'downloads'), - ) - self.backup_dir = os.path.abspath( - os.path.join(self.data_dir, '.backups'), - ) - self.temp_dir = os.path.abspath(os.path.join(self.data_dir, '.temp')) - self.temp_dl_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp', 'downloads'), - ) - self.temp_test_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp', 'ytdl-test'), - ) - - - def setup_paths(self): - - """Called by self.start(). - - Sets the default values of various IVs handling the path of the - installed youtube-dl. - - On MS Windows, these are fixed. On other operating systems, we try to - auto-detect youtube-dl's location, if possible. - """ - - # Set youtube-dl path IVs - if os.name == 'nt': - - if 'PROGRAMFILES(X86)' in os.environ: - # 64-bit MS Windows - recommended = 'ytdl_update_win_64' - alt_recommended = 'ytdl_update_win_64_no_dependencies' - python_path = '..\\..\\..\\mingw64\\bin\python3.exe' - pip_path = '..\\..\\..\\mingw64\\bin\pip3-script.py' - else: - # 32-bit MS Windows - recommended = 'ytdl_update_win_32' - alt_recommended = 'ytdl_update_win_32_no_dependencies' - python_path = '..\\..\\..\\mingw32\\bin\python3.exe' - pip_path = '..\\..\\..\\mingw32\\bin\pip3-script.py' - - self.ytdl_bin = 'youtube-dl' - self.ytdl_path_default = 'youtube-dl' - self.ytdl_path = 'youtube-dl' - self.ytdl_update_dict = { - recommended: [ - python_path, - pip_path, - 'install', - '--upgrade', - 'youtube-dl', - ], - alt_recommended: [ - python_path, - pip_path, - 'install', - '--upgrade', - '--no-dependencies', - 'youtube-dl', - ], - 'ytdl_update_pip3': [ - 'pip3', 'install', '--upgrade', 'youtube-dl', - ], - 'ytdl_update_pip3_no_dependencies': [ - 'pip3', 'install', '--upgrade', '--no-dependencies', - 'youtube-dl', - ], - 'ytdl_update_pip': [ - 'pip', 'install', '--upgrade', 'youtube-dl', - ], - 'ytdl_update_pip_no_dependencies': [ - 'pip', 'install', '--upgrade', '--no-dependencies', - 'youtube-dl', - ], - 'ytdl_update_default_path': [ - self.ytdl_path_default, '-U', - ], - 'ytdl_update_local_path': [ - self.ytdl_bin, '-U', - ], - 'ytdl_update_custom_path': [ - 'python3', self.ytdl_path, '-U', - ], - } - self.ytdl_update_list = [ - recommended, - alt_recommended, - 'ytdl_update_pip3', - 'ytdl_update_pip3_no_dependencies', - 'ytdl_update_pip', - 'ytdl_update_pip_no_dependencies', - 'ytdl_update_default_path', - 'ytdl_update_local_path', - 'ytdl_update_custom_path', - ] - self.ytdl_update_current = recommended - - else: - - self.ytdl_bin = 'youtube-dl' - self.ytdl_path_default = os.path.abspath( - os.path.join(os.sep, 'usr', 'bin', self.ytdl_bin), - ) - - # Set up the shell commands for updating youtube-dl - if __main__.__pkg_strict_install_flag__: - - self.ytdl_update_dict = { - 'ytdl_update_disabled': [], - } - self.ytdl_update_list = [ - 'ytdl_update_disabled', - ] - self.ytdl_update_current = 'ytdl_update_disabled' - - else: - - self.ytdl_update_dict = { - 'ytdl_update_pip3_recommend': [ - 'pip3', 'install', '--upgrade', '--user', 'youtube-dl', - ], - 'ytdl_update_pip3_omit_user': [ - 'pip3', 'install', '--upgrade', 'youtube-dl', - ], - 'ytdl_update_pip3_no_dependencies': [ - 'pip3', 'install', '--upgrade', '--no-dependencies', - 'youtube-dl', - ], - 'ytdl_update_pip': [ - 'pip', 'install', '--upgrade', '--user', 'youtube-dl', - ], - 'ytdl_update_pip_omit_user': [ - 'pip', 'install', '--upgrade', 'youtube-dl', - ], - 'ytdl_update_pip_no_dependencies': [ - 'pip', 'install', '--upgrade', '--no-dependencies', - 'youtube-dl', - ], - 'ytdl_update_default_path': [ - self.ytdl_path_default, '-U', - ], - 'ytdl_update_local_path': [ - self.ytdl_bin, '-U', - ], - 'ytdl_update_custom_path': [ - 'python3', self.ytdl_path, '-U', - ], - 'ytdl_update_pypi_path': [ - self.ytdl_path_pypi, '-U', - ], - } - self.ytdl_update_list = [ - 'ytdl_update_pip3_recommend', - 'ytdl_update_pip3_omit_user', - 'ytdl_update_pip3_no_dependencies', - 'ytdl_update_pip', - 'ytdl_update_pip_omit_user', - 'ytdl_update_pip_no_dependencies', - 'ytdl_update_default_path', - 'ytdl_update_local_path', - 'ytdl_update_custom_path', - 'ytdl_update_pypi_path', - ] - - # Auto-detect the location of youtube-dl, and set the perferred - # shell command - self.auto_detect_paths() - - - def auto_detect_paths(self): - - """Can be called by anything (initially called by self.setup_paths() ). - - Tries to auto-detect the location of yt-dlp or youtube-dl, and updates - IVs accordingly. - """ - - # Auto-detection does not apply to MS Windows, for which paths are - # fixed - if os.name == 'nt': - return - - # Check for yt-dlp first (as of 2022, it is virtually abandoned) - pypi_path = re.sub( - r'^\~', os.path.expanduser('~'), - re.sub('youtube-dl', 'yt-dlp', self.ytdl_path_pypi), - ) - default_path = re.sub('youtube-dl', 'yt-dlp', self.ytdl_path_default) - bin_path = re.sub('youtube-dl', 'yt-dlp', self.ytdl_bin) - - path = None - if os.path.isfile(pypi_path): - path = pypi_path - elif os.path.isfile(default_path): - path = default_path - elif os.path.isfile(bin_path): - path = bin_path - - if path is not None: - - self.ytdl_path = path - - if not __main__.__pkg_strict_install_flag__: - - if self.ytdl_path == self.ytdl_path_pypi: - self.ytdl_update_current = 'ytdl_update_pip3_recommend' - elif self.ytdl_path == self.ytdl_path_default: - self.ytdl_update_current = 'ytdl_update_default_path' - else: - self.ytdl_update_current = 'ytdl_update_local_path' - - return - - # Otherwise, assume youtube-dl - pypi_path = re.sub( - r'^\~', os.path.expanduser('~'), - self.ytdl_path_pypi, - ) - - if os.path.isfile(pypi_path) \ - or __main__.__pkg_install_flag__: - self.ytdl_path = self.ytdl_path_pypi - elif os.path.isfile(self.ytdl_path_default): - self.ytdl_path = self.ytdl_path_default - else: - self.ytdl_path = self.ytdl_bin - - if not __main__.__pkg_strict_install_flag__: - - if self.ytdl_path == self.ytdl_path_pypi: - self.ytdl_update_current = 'ytdl_update_pip3_recommend' - elif self.ytdl_path == self.ytdl_path_default: - self.ytdl_update_current = 'ytdl_update_default_path' - else: - self.ytdl_update_current = 'ytdl_update_local_path' - - - def auto_delete_old_videos(self, update_flag=False): - - """Called by self.load_db and .download_manager_finished(). - - Auto-delete any old downloaded videos (if auto-deletion is enabled). - - The video is removed from the database, and all files associated with - the video are deleted from the filesystem. - - Args: - - update_flag (bool): If True, the Video Catalogue is updated; - otherwise it is not - - """ - - if not self.auto_delete_flag: - return - - # Calculate the system time before which any downloaded videos can be - # deleted - time_limit = int(time.time()) - (self.auto_delete_days * 24 * 60 * 60) - - # Import a list of media data objects (as self.media_reg_dict will be - # modified during this procedure) - media_list = list(self.media_reg_dict.values()) - - # Auto-delete any videos as required - for media_data_obj in media_list: - - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.dl_flag \ - and not media_data_obj.archive_flag \ - and media_data_obj.receive_time < time_limit \ - and ( - not self.auto_delete_watched_flag \ - or not media_data_obj.new_flag - ): - # Ddelete this video - self.delete_video( - media_data_obj, - True, - not update_flag, - not update_flag, - ) - - - def auto_remove_old_videos(self, update_flag=False): - - """Called by self.load_db and .download_manager_finished(). - - Auto-remove any old downloaded videos (if auto-removal is enabled). - - The video is removed from the database, but no files associated with - the video are deleted from the filesystem. - - Args: - - update_flag (bool): If True, the Video Catalogue is updated; - otherwise it is not - - """ - - if not self.auto_remove_flag: - return - - # Calculate the system time before which any downloaded videos can be - # removed - time_limit = int(time.time()) - (self.auto_remove_days * 24 * 60 * 60) - - # Import a list of media data objects (as self.media_reg_dict will be - # modified during this procedure) - media_list = list(self.media_reg_dict.values()) - - # Auto-remove any videos as required - for media_data_obj in media_list: - - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.dl_flag \ - and not media_data_obj.archive_flag \ - and media_data_obj.receive_time < time_limit \ - and ( - not self.auto_delete_watched_flag \ - or not media_data_obj.new_flag - ): - # Remove this video - self.delete_video( - media_data_obj, - False, - not update_flag, - not update_flag, - ) - - - def check_external(self): - - """Called by self.load_db() (only). - - Test any channels/playlists/folders which have external directories - set. If we can't read/write to the external directory, then mark them - as unavailable. - - An unavailable channel/playlist/folder can't be checked/downloaded/ - custom downloaded. - """ - - # After loading a new database, clear any existing unavailable - # containers - self.container_unavailable_dict = {} - - # (If multiple channels/playlists/folders use a shared external - # directory, we don't need to test the directory more than once) - check_dict = {} - - # Now check every container which has an external directory set - for dbid in self.container_reg_dict.keys(): - - media_data_obj = self.media_reg_dict[dbid] - if media_data_obj.external_dir is not None: - - if media_data_obj.external_dir in check_dict \ - or not self.check_external_dir(media_data_obj.external_dir): - - self.container_unavailable_dict[dbid] = media_data_obj - check_dict[media_data_obj.external_dir] = None - - - def check_external_dir(self, dir_path): - - """Called by self.check_external (only). - - The specified directory is the external directory for a channel, - playlist or folder. - - If it contains a semaphore file, check that it can be read and written. - - If it doesn't contain a semaphore file, try to create one there. - - Args: - - dir_path (str): Full path to the external directory to check - - Return values: - - True if the external directory is readable, False if it should be - marked as unavailable - - """ - - # Make the semaphore file, if it doesn't already exist - # (This function returns the file path on success, or if the file - # alread exists. It returns None if the directory doesn't exist or if - # the semaphore file can't be created) - file_path = self.make_semaphore_file(dir_path) - if file_path is None: - return False - - # Check reading the file - try: - fh = open(file_path, 'r') - fh.read() - fh.close() - - except: - return False - - # Try writing the file - try: - fh = open(file_path, 'w') - fh.write('') - fh.close() - - except: - return False - - # All good - return True - - - def convert_version(self, version): - - """Can be called by anything, but mostly called by self.load_config() - and load_db(). - - Converts a Tartube version number, a string in the form '1.234.567', - into a simple integer in the form 1234567. - - The calling function can then compare the version number for this - installation of Tartube with the version number that created the file. - - Args: - - version (str): A string in the form '1.234.567' - - Return values: - - The simple integer, or None if the 'version' argument was invalid - - """ - - num_list = version.split('.') - if len(num_list) != 3: - return None - else: - return (int(num_list[0]) * 1000000) + (int(num_list[1]) * 1000) \ - + int(num_list[2]) - - - def find_sound_effects(self): - - """Called by self.start(). - - Set the directory in which sound files are stored. - - When installed via PyPI, the files are moved to ../tartube/sounds. - - When installed via a Debian/RPM package, the files are moved to - /usr/share/tartube/sounds. - - Compiles a list of paths to sound effects found in the /sounds - directory, and updates the IVs. - """ - - sound_dir_list = [] - sound_dir_list.append( - os.path.abspath( - os.path.join(self.script_parent_dir, 'sounds'), - ), - ) - - sound_dir_list.append( - os.path.abspath( - os.path.join( - os.path.dirname(os.path.realpath(__file__)), - 'sounds', - ), - ), - ) - - sound_dir_list.append( - os.path.join( - '/', 'usr', 'share', __main__.__packagename__, 'sounds', - ) - ) - - for sound_dir_path in sound_dir_list: - if os.path.isdir(sound_dir_path): - self.sound_dir = sound_dir_path - - # Get a list of available sound files, and sort alphabetically - for (dirpath, dir_list, file_list) in os.walk(self.sound_dir): - for filename in file_list: - if filename != 'COPYING': - self.sound_list.append(filename) - - self.sound_list.sort() - - return - - - def create_fixed_folders(self): - - """Called by self.start() and .reset_db(). - - Creates the fixed (system) media.Folder objects that can't be - destroyed by the user. - """ - - self.fixed_all_folder = self.add_folder( - formats.FOLDER_ALL_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - self.fixed_bookmark_folder = self.add_folder( - formats.FOLDER_BOOKMARKS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - self.fixed_fav_folder = self.add_folder( - formats.FOLDER_FAVOURITE_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - self.fixed_fav_folder.set_fav_flag(True) - - self.fixed_live_folder = self.add_folder( - formats.FOLDER_LIVESTREAMS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - self.fixed_missing_folder = self.add_folder( - formats.FOLDER_MISSING_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - self.fixed_new_folder = self.add_folder( - formats.FOLDER_NEW_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - self.fixed_recent_folder = self.add_folder( - formats.FOLDER_RECENT_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - self.fixed_waiting_folder = self.add_folder( - formats.FOLDER_WAITING_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - True, # Private - False, # Not temporary - ) - - self.fixed_temp_folder = self.add_folder( - formats.FOLDER_TEMPORARY_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'open', # Can contain any media data object - True, # Fixed (folder cannot be removed) - False, # Public - True, # Temporary - ) - - self.fixed_misc_folder = self.add_folder( - formats.FOLDER_UNSORTED_VIDEOS, - None, # No parent folder - False, # Allow downloads - 'full', # Can only contain videos - True, # Fixed (folder cannot be removed) - False, # Public - False, # Not temporary - ) - - self.fixed_clips_folder = self.add_folder( - formats.FOLDER_VIDEO_CLIPS, - None, # No parent folder - False, # Allow downloads - 'partial', # Can contain videos and folders - True, # Fixed (folder cannot be removed) - False, # Public - False, # Not temporary - ) - - - def rename_fixed_folders(self): - - """Called by self.load_db() (only). - - If the locale used when saving the database file has changed then, - having loaded the file, we can rename all the fixed folders to match - the new locale. - - This function must only be called for that reason; fixed folders cannot - otherwise be renamed. - """ - - self.rename_fixed_folder( - self.fixed_all_folder, - formats.FOLDER_ALL_VIDEOS, - ) - - self.rename_fixed_folder( - self.fixed_bookmark_folder, - formats.FOLDER_BOOKMARKS, - ) - - self.rename_fixed_folder( - self.fixed_fav_folder, - formats.FOLDER_FAVOURITE_VIDEOS, - ) - - self.rename_fixed_folder( - self.fixed_live_folder, - formats.FOLDER_LIVESTREAMS, - ) - - self.rename_fixed_folder( - self.fixed_missing_folder, - formats.FOLDER_MISSING_VIDEOS, - ) - - self.rename_fixed_folder( - self.fixed_new_folder, - formats.FOLDER_NEW_VIDEOS, - ) - - self.rename_fixed_folder( - self.fixed_recent_folder, - formats.FOLDER_RECENT_VIDEOS, - ) - - self.rename_fixed_folder( - self.fixed_waiting_folder, - formats.FOLDER_WAITING_VIDEOS, - ) - - self.rename_fixed_folder( - self.fixed_temp_folder, - formats.FOLDER_TEMPORARY_VIDEOS, - ) - - self.rename_fixed_folder( - self.fixed_misc_folder, - formats.FOLDER_UNSORTED_VIDEOS, - ) - - self.rename_fixed_folder( - self.fixed_clips_folder, - formats.FOLDER_VIDEO_CLIPS, - ) - - - def rename_fixed_folder(self, media_data_obj, new_name): - - """Called by self.rename_fixed_folders() (only). - - Renames the specified media.Folder object to match the new locale. - - Args: - - media_data_obj (media.Folder): The folder to rename - - new_name (str): The folder's new name, matching (for example) - formats.FOLDER_ALL_VIDEOS, formats.FOLDER_BOOKMARKS, etc - - """ - - # If there is (by chance) a folder with the same name, it must be - # renamed - for other_obj in self.get_container_list(new_name): - - # Sanity check: don't rename another fixed folder - if isinstance(other_obj, media.Folder) and other_obj.fixed_flag: - return - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - other_obj, - utils.find_available_name(self, other_obj.name, 2, -1), - ) - - # Now rename the specified folder - self.rename_container_silently(media_data_obj, new_name) - - - def check_fixed_folder(self, media_data_obj): - - """Called by self.check_fixed_folder() or .delete_container(). - - Checks whether a specified media data object is one of the standard - fixed folders (i.e., a system folder that can't be deleted). - - Args: - - media_data_obj (media.Folder): The media data object to test - - Return values: - - True if it's one of the recognised fixed folders, False otherwise - - """ - - if media_data_obj is not None \ - and isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag \ - and ( - media_data_obj is self.fixed_all_folder \ - or media_data_obj is self.fixed_bookmark_folder \ - or media_data_obj is self.fixed_fav_folder \ - or media_data_obj is self.fixed_live_folder \ - or media_data_obj is self.fixed_missing_folder \ - or media_data_obj is self.fixed_new_folder \ - or media_data_obj is self.fixed_recent_folder \ - or media_data_obj is self.fixed_waiting_folder \ - or media_data_obj is self.fixed_temp_folder \ - or media_data_obj is self.fixed_misc_folder \ - or media_data_obj is self.fixed_clips_folder - ): - return True - else: - return False - - - def delete_temp_folders(self): - - """Called by self.stop_continue() and self.load_db(). - - Deletes the contents of any folders marked temporary, such as the - 'Temporary Videos' folder. (The folders themselves are not deleted). - """ - - # (Must compile a list of top-level container objects first, or Python - # will complain about the dictionary changing size) - obj_list = [] - for dbid in self.container_reg_dict.keys(): - obj_list.append(self.media_reg_dict[dbid]) - - for media_data_obj in obj_list: - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.temp_flag: - - # Delete all child objects - for child_obj in list(media_data_obj.child_list.copy()): - if isinstance(child_obj, media.Video): - self.delete_video(child_obj) - else: - self.delete_container(child_obj) - - # Remove files from the filesystem, leaving an empty directory - dir_path = media_data_obj.get_default_dir(self) - if os.path.isdir(dir_path): - self.remove_directory(dir_path) - - self.make_directory(dir_path) - - - def open_temp_folders(self): - - """Called by self.stop_continue(). - - Checks all folders marked temporary. Any of them that contain videos - are opened on the desktop (so it's more convenient for the user to copy - things out of them, before they are deleted.) - """ - - for dbid in self.container_reg_dict.keys(): - media_data_obj = self.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.temp_flag \ - and media_data_obj.child_list: - - utils.open_file(self, media_data_obj.get_default_dir(self)) - - - def disable_load_save(self, error_msg=None, lock_flag=False): - - """Called by self.load_config(), .save_config(), load_db() and - .save_db(). - - After an error, disables loading/saving, and desensitises many widgets - in the main window. - - Args: - - error_msg (str or None): An optional error message that can be - retrieved later, if required - - lock_flag (bool): True when the error was caused by being unable to - load a database file because of a lockfile; in which the user - is prompted if they want to remove it, or not - - """ - - - # This flag is used to detect an interrupted database load due to a - # Python error, which would mean that the call to self.load_db() - # never returns - # It is set back to False whenever that function returns, which is - # often directly after a call to this function - self.db_loading_flag = False - - # Ignore subsequent calls to this function; only the initial error - # is of interest - if not self.disable_load_save_flag: - - self.disable_load_save_flag = True - self.allow_db_save_flag = False - self.disable_load_save_msg = error_msg - self.disable_load_save_lock_flag = lock_flag - - # (Check both that the window exists, and that some widgets have - # been drawn in it, before trying to desensitise those widgets) - if self.main_win_obj is not None \ - and self.main_win_obj.grid is not None: - self.main_win_obj.sensitise_widgets_if_database(False) - - - def disable_scheduled_dl(self): - - """Called by self.save_db() only. - - After a failure to save a database file, file load/save is not - disabled altogether, but scheduled downloads are. - - Set the flag to do that, and also show a message in the Errors/ - Warnings tab. - """ - - self.disable_scheduled_dl_flag = True - self.system_error( - 105, - 'After failing to save the database file, scheduled downloads' \ - + ' have been disabled', - ) - - - def remove_db_lock_file(self): - - """Called by self.do_shutdown(), .stop_continue(), .load_db() and - .switch_db(). - - Removes the lockfile protecting the Tartube database file, and updates - IVs. - """ - - if self.db_lock_file_path is not None: - - if os.path.isfile(self.db_lock_file_path): - self.remove_file(self.db_lock_file_path) - - self.db_lock_file_path = None - - - def remove_stale_lock_file(self): - - """Called by self.start() (only), after a call to - mainwin.RemoveLockFileDialogue. - - The user has confirmed that the lockfile protecting a Tartube database - file is stale, and can be removed; so remove it. - """ - - lock_path = os.path.abspath( - os.path.join(self.data_dir, self.db_file_name + '.lock'), - ) - - if os.path.exists(lock_path): - self.remove_file(lock_path) - - - def file_error_dialogue(self, msg): - - """Called by self.start(), .save_config(), load_db() and .save_db(). - - After a failure to load/save a file, display a dialogue window if the - main window is open, or write to the terminal if not. - - Args: - - msg (str): The message to display - - """ - - if self.main_win_obj and self.dialogue_manager_obj: - self.dialogue_manager_obj.show_msg_dialogue(msg, 'error', 'ok') - - else: - # Main window not open yet, so remove any newline characters - # (which look weird when printed to the terminal) - msg = re.sub( - r'\n', - ' ', - msg, - ) - - print('FILE ERROR: ' + msg) - - - def make_directory(self, dir_path): - - """Can be called by anything. - - The call to os.makedirs() might fail with a 'Permission denied' error, - meaning that the specified directory is unwriteable. - - Convenience function to intercept the error, and display a system error - in response. - - Args: - - dir_path (str): The full path to the directory to be created with a - call to os.makedirs() - - Return values: - - True if the directory was created, False if not - - """ - - try: - os.makedirs(dir_path) - return True - - except: - - # Show a system error - self.system_error( - 106, - 'Failed to create directory \'' + dir_path + '\'', - ) - - return False - - - def remove_directory(self, dir_path): - - """Can be called by anything. - - The call to shutil.rmtree() might fail. - - Convenience function to intercept the error, and display a system error - in response. - - Args: - - dir_path (str): The full path to the directory to be removed with a - call to shutil.rmtree() - - Return values: - - True if the directory was removed, False if not - - """ - - try: - shutil.rmtree(dir_path) - return True - - except: - - # Show a system error - self.system_error( - 107, - 'Failed to remove directory \'' + dir_path + '\'', - ) - - return False - - - def remove_file(self, file_path): - - """Can be called by anything. - - The call to os.remove() might fail. - - Convenience function to intercept the error, and display a system error - in response. - - Args: - - file_path (str): The full path to the file to be removed with a - call to os.remove() - - Return values: - - True if the file was removed, False if not - - """ - - try: - os.remove(file_path) - return True - - except: - - # Show a system error - self.system_error( - 108, - 'Failed to remove file \'' + file_path + '\'', - ) - - return False - - - def move_file_or_directory(self, old_path, new_path): - - """Can be called by anything. - - The call to shutil.move() might fail. - - Convenience function to intercept the error, and display a system error - in response. - - Args: - - old_path (str): The current full path of the file or directory - - new_path (str): The new full path of the file or directory - - Return values: - - True if the file/directory was moved, False if not - - """ - - try: - shutil.move(old_path, new_path) - return True - - except: - - # Show a system error - self.system_error( - 109, - 'Failed to move file/directory \'' + old_path + '\' to \'' \ - + new_path + '\'', - ) - - return False - - - def make_semaphore_file(self, dir_path): - - """Can be called by anything. - - Currently called to create a semaphore file in an external directory - (i.e. one outside of Tartube's data directory). The semaphore file is - used to test that the directory is readable/writeable, before using it - to store videos. - - The specified 'dir_path' must already exist. - - Args: - - dir_path (str): The full path to the directory in which the - semaphore file should be created - - Return values: - - Full path to the semaphore file if it was created or already - exists, None if the specified directory doesn't exist, or if - the semaphore file doesn't exist and can't be created - - """ - - # e.g. .tartube.sem - file_path = os.path.abspath( - os.path.join(dir_path, '.' + __main__.__packagename__ + '.sem'), - ) - - if not os.path.isdir(dir_path): - return None - - elif os.path.isfile(file_path): - return file_path - - try: - fh = open(file_path, 'w') - fh.close() - return file_path - - except: - - # Show a system error - self.system_error( - 110, - 'Failed to write files to directory \'' + dir_path + '\'', - ) - - return None - - - def move_backup_files(self): - - """Called by self.load_db(). - - Before v1.3.099, Tartube's data directory used a different structure, - with the database backup files stored in self.data_dir itself. - - After v1.3.099, they are stored in self.backup_dir. - - The calling function has detected that the old file structure is being - used. As a convenience to the user, move all the backup files to their - new location. - """ - - try: - file_list = os.listdir(path = self.data_dir) - except: - return - - for filename in file_list: - - if re.search(r'^tartube_BU_.*\.db$', filename): - - old_path = os.path.abspath( - os.path.join(self.data_dir, filename), - ) - - new_path = os.path.abspath( - os.path.join(self.backup_dir, filename), - ) - - # (Don't handle unwisely-named directories...) - if os.path.isfile(old_path): - utils.rename_file(self, old_path, new_path) - - - def open_wiz_win(self): - - """Called by self.start() when no configuration file exists (meaning - this is probably a new Tartube installation). - - Called by self.load_config() if the config file exists in the Tartube - directory, but is unreadable (meaning that the user has created a - blank config file there, in order to force the new Tartube installation - to use that directory). - - Open the wizard window, so the user can set the data directory, specify - which fork of youtube-dl to use, and (depending on the system) download - and install youtube-dl and/or FFmpeg - """ - - # Open the wizard window. When it closes, self.open_wiz_win_continue() - # is called - wizwin.SetupWizWin(self) - - - def open_wiz_win_continue(self, wiz_win_obj): - - """Called by wizwin.SetupWizWin.apply_changes(). - - For a new installation, the user has specified various settings. Create - the config file, then continue the setup process. - - Args: - - wiz_win_obj (wizwin.SetupWizWin): The calling wizard window - - """ - - # Once again, auto-detect the location of youtube-dl (or its fork), - # now that the user has chosen one - self.auto_detect_paths() - # Setup wizard window was completed. Create a new config file - self.save_config() - # Resume general initialisation. The True flag means that a new config - # file was created - self.start_continue(True) - # Open the tutorial wizard window, if required - if wiz_win_obj.open_tutorial_flag: - - # (code in mainwin.MainWin only permits one wizard window at a - # time) - wiz_win_obj.destroy() - wizwin.TutorialWizWin(self) - - - def prompt_user_for_data_dir(self): - - """Called by mainwin.MountDriveDialogue.do_select_dir(). - - When the user wants to specify a non-default location for Tartube's - data directory, prompt the user to select/create a directory. - - Return values: - - True if the user selects a location, False if they do not - - """ - - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Please select Tartube\'s data folder'), - self.main_win_obj, - 'folder', - ) - - # Get the user's response - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - - self.data_dir = dialogue_win.get_filename() - self.data_dir_alt_list = [ self.data_dir ] - - self.update_data_dirs() - - dialogue_win.destroy() - if response == Gtk.ResponseType.OK: - - # Location selected; the remaining code in self.start() will - # create the data directory, if necessary - return True - - else: - - # Location not selected. Tartube will now shut down - return False - - - def check_downloader(self, arg, wiz_win_obj=None): - - """Called by several functions as they prepare a system command to - execute. - - The specified value is one of the arguments in the system command, - containing the text 'youtube-dl'. - - If self.ytdl_fork is specified, substitutes the fork for the original, - and returns the modified value. - - If the specified value doesn't actually contain 'youtube-dl', or if the - user has specified a custom path to the youtube-dl executable, then - 'arg' is returned unmodified. - - Args: - - arg (str): An argument in a system command, which should contain - 'youtube-dl' - - wiz_win_obj (wizwin.SetupWizWin or None): If called from the setup - wizard window, uses its IV, rathern than ours - - Return values: - - The modified (or original) value - - """ - - if not wiz_win_obj: - - if self.ytdl_path_custom_flag: - - if re.search('youtube-dl', arg) \ - and self.ytdl_path is not None: - return self.ytdl_path - else: - # Failsafe - return arg - - elif self.ytdl_fork is not None: - return re.sub('youtube-dl', self.ytdl_fork, arg) - - else: - return arg - - else: - - if wiz_win_obj.ytdl_fork is not None: - return re.sub('youtube-dl', wiz_win_obj.ytdl_fork, arg) - else: - return arg - - - def get_downloader(self, wiz_win_obj=None): - - """Can be called by anything. - - If a youtube-dl fork (self.ytdl_fork) has been specified, returns it. - - Otherwise returns the string 'youtube-dl' (self.ytdl_bin). - - Args: - - wiz_win_obj (wizwin.SetupWizWin or None): If called from the setup - wizard window, uses its IV, rathern than ours - - Return values: - - The string described above - - """ - - if not wiz_win_obj: - - if self.ytdl_fork is not None: - return self.ytdl_fork - else: - return self.ytdl_bin - - else: - - if wiz_win_obj.ytdl_fork is not None: - return wiz_win_obj.ytdl_fork - else: - return self.ytdl_bin - - - def retrieve_videos_from_db(self, data_list, dummy_flag=False): - - """Can be called by anything. - - Given a list of data, which can contain a mix of full paths to a video/ - audio file and/or URLs, searches the media data registry for a - matching media.Video objects. - - Optionally checks the list of 'dummy' media.Video objects maintained - by mainwin.MainWin, too. - - Returns a list of matching media.Video objects (without duplicates). - - Args: - - data_list (list): A list of full paths and/or URLs - - Return values: - - A list of matching media.Video objects - - """ - - return_list = [] - - # Search the media data registry - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - - source = media_data_obj.source - if media_data_obj.file_name is not None: - path = media_data_obj.get_actual_path(self) - else: - path = None - - for data in data_list: - - if data is not None \ - and data != '' \ - and ( - (source is not None and source == data) - or (path is not None and path == data) - ): - return_list.append(media_data_obj) - break - - if dummy_flag: - - # Search the list of 'dummy' media.Video objects created by the - # Classic Mode tab - for video_obj in self.main_win_obj.classic_media_dict.values(): - - source = video_obj.source - path = video_obj.get_actual_path(self) - - for data in data_list: - - if data is not None \ - and data != '' \ - and ( - (source is not None and source == data) - or (path is not None and path == data) - ) and not video_obj in return_list: - return_list.append(video_obj) - break - - return return_list - - - def get_proxy(self): - - """Called by options.OptionsParser.build_proxy(). - - self.dl_proxy_cycle_list() specifies a list of proxies which can be - passed to youtube-dl as the --proxy option. - - So that the user can cycle through the list of proxies, return the - item at the top of the list (if any), having moved it to the bottom of - the list. - - Return values; - - The URL to a proxy, or None - - """ - - if not self.dl_proxy_list: - return None - - else: - proxy = self.dl_proxy_list.pop(0) - self.dl_proxy_list.append(proxy) - - return proxy - - - def apply_locale(self): - - """Called by self.start(). - - Calls the python gettext module to apply the current system locale. - """ - - LOCALE = None - error_msg = None - - # The current working directory should be the same one as - # self.script_parent_dir, so this relative path should work - try: - LOCALE = gettext.translation( - 'base', - localedir = 'locale', - languages = [self.current_locale], - ) - - except Exception as e: - error_msg = str(e) - - # If it dosen't work, then try an absolute path - if LOCALE is None: - - try: - locale_path = os.path.abspath( - os.path.join( - self.script_parent_dir, - 'locale', - ), - ) - - LOCALE = gettext.translation( - 'base', - localedir = locale_path, - languages = [self.current_locale], - ) - - except Exception as e: - - # Use the first error message, as it's probably more useful - if error_msg is None: - error_msg = str(e) - - if LOCALE is None: - - self.current_locale = formats.LOCALE_DEFAULT - - return - - try: - LOCALE.install() - - # (Apply to this file) - _ = LOCALE.gettext - # (Apply to other files) - config._ = _ - downloads._ = _ - formats._ = _ - info._ = _ - mainwin._ = _ - media._ = _ - process._ = _ - refresh._ = _ - tidy._ = _ - updates._ = _ - utils._ = _ - wizwin._ = _ - # (Update download operation stages, e.g. - # formats.MAIN_STAGE_QUEUED - formats.do_translate(True) - - except Exception as e: - self.system_error( - 197, - 'Cannot use locale \'' + str(self.current_locale) + '\': ' \ - + str(e) - ) - - self.current_locale = formats.LOCALE_DEFAULT - - - # (Operations) - - - def download_manager_start(self, operation_type, \ - automatic_flag=False, media_data_list=[], custom_dl_obj=None): - - """Can be called by anything. - - Creates a new downloads.DownloadManager object to handle the download - operation. When the operation is complete, - self.download_manager_finished() is called. - - Args: - - operation_type (str): 'sim' if channels/playlists should just be - checked for new videos, without downloading anything. 'real' - if videos should be downloaded (or not) depending on each media - data object's .dl_sim_flag IV - - 'custom_real' is like 'real', but with additional options - applied (specified by a downloads.CustomDLManager object). - A 'custom_real' operation is sometimes preceded by a - 'custom_sim' operation (which is the same as a 'sim' operation, - except that it is always followed by a 'custom_real' - operation) - - For downloads launched from the Classic Mode tab, - 'classic_real' for an ordinary download, or 'classic_custom' - for a custom download. A 'classic_custom' operation is always - preceded by a 'classic_sim' operation (which is the same as a - 'sim' operation, except that it is always followed by a - 'classic_custom' operation) - - automatic_flag (bool): True when called by - self.script_slow_timer_callback(). When set, dialogue windows - are not displayed (as they ordinarly would be). - - media_data_list (list): List of media.Video, media.Channel, - media.Playlist and/or media.Folder objects. Can also be a list - of (exclusively) media.Scheduled objects. If not an empty list, - only the specified media data objects (and their children) are - checked/downloaded. If an empty list, all media data objects - are checked/downloaded. If operation_type is 'classic_real', - 'classic_sim' or 'classic_custom', then the media_data_list can - contain a list of dummy media.Video objects created by an - earlier call to this function; if the list is empty, all - dummy media.Video objects in mainwin.MainWin.classic_media_dict - are downloaded - - custom_dl_obj (downloads.CustomDLManager or None): The custom - download manager that applies to this download operation. Only - specified when 'operation_type' is 'custom_sim', 'custom_real', - 'classic_sim' or 'classic_real' - - For 'custom_real' and 'classic_real', not specified if - self.temp_stamp_buffer_dict or self.temp_slice_buffer_dict have - been populated (because those values take priority) - - For 'custom_real', not specified if media_data_list contains - media.Scheduled objects - - """ - - # Check for interrupted database loads - if self.db_loading_flag: - - self.disable_load_save() - self.system_error( - 111, - 'Cannot start the download operation because Tartube failed' \ - + ' to finish loading a database (which seems to be broken)', - ) - - return - - # Code to deal with calls from self.script_slow_timer_callback() - if media_data_list: - - first_obj = media_data_list[0] - if isinstance(first_obj, media.Scheduled): - - # If the first item in the list is a media.Scheduled object, - # then they all should be - for scheduled_obj in media_data_list: - if not isinstance(scheduled_obj, media.Scheduled): - self.system_error( - 112, - 'Unexpected item in scheduled download list', - ) - - return - - # If a media.Scheduled object which wants to start a - # 'custom_real' download exists in 'media_data_list', it - # should be the only one there - if first_obj.dl_mode == 'custom_real' \ - and first_obj.custom_dl_uid is not None \ - and len(media_data_list) > 1: - if not isinstance(scheduled_obj, media.Scheduled): - self.system_error( - 113, - 'Unexpected item in scheduled download list', - ) - - return - - # Set the time at which this scheduled download began - for scheduled_obj in media_data_list: - scheduled_obj.set_last_time(int(time.time())) - - # Convert the 'operation_type' for this download operation from - # 'custom_real' to 'custom_sim' or 'real', as required - if first_obj.dl_mode == 'custom_real': - - if first_obj.custom_dl_uid is None \ - or not first_obj.custom_dl_uid in self.custom_dl_reg_dict: - # Fallback - operation_type = 'real' - - elif not custom_dl_obj: - - custom_dl_obj \ - = self.custom_dl_reg_dict[first_obj.custom_dl_uid] - if custom_dl_obj.dl_by_video_flag \ - and custom_dl_obj.dl_precede_flag: - operation_type = 'custom_sim' - - # If a livestream operation is running, tell it to stop immediately - if self.livestream_manager_obj: - self.livestream_manager_obj.stop_livestream_operation() - - # If using a no-download package, then videos can't be downloaded - if __main__.__pkg_no_download_flag__ \ - and operation_type != 'sim' \ - and operation_type != 'classic_sim': - - self.system_error( - 114, - 'This Tartube package cannot be used to download videos', - ) - - return - - # If a livestream operation was running, this IV should already have - # been reset - elif self.current_manager_obj: - - # Operation already in progress - if not automatic_flag: - self.system_error( - 115, - 'An operation is already in progress', - ) - - return - - elif self.main_win_obj.config_win_list: - - # Download operation is not allowed when a configuration window is - # open - if not automatic_flag: - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'A download operation cannot start if one or more' \ - + ' configuration windows are still open', - ), - 'error', - 'ok', - ) - - return - - # If the device containing self.data_dir is running low on space, - # warn the user before proceeding - disk_space = utils.disk_get_free_space(self.data_dir) - total_space = utils.disk_get_total_space(self.data_dir) - - if ( - self.disk_space_stop_flag \ - and self.disk_space_stop_limit != 0 \ - and disk_space <= self.disk_space_stop_limit - ) or disk_space < self.disk_space_abs_limit: - - # Refuse to proceed with the operation - if not automatic_flag: - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'You only have {0} / {1} Gb remaining on your device', - ).format(str(disk_space), str(total_space)), - 'error', - 'ok', - None, # Parent window is main window - ) - - return - - elif self.disk_space_warn_flag \ - and self.disk_space_warn_limit != 0 \ - and disk_space <= self.disk_space_warn_limit: - - if automatic_flag: - - # Don't perform a scheduled download operation if disk space is - # below the limit at which a warning would normally be issued - return - - else: - - # Warn the user that their free disk space is running low, and - # get confirmation before starting the download operation - self.dialogue_manager_obj.show_simple_msg_dialogue( - _( - 'You only have {0} / {1} Gb remaining on your device', - ).format(str(disk_space), str(total_space)) \ - + '\n\n' \ - + _('Are you sure you want to continue?'), - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .download_manager_continue() - { - 'yes': 'download_manager_continue', - 'data': [ - operation_type, - automatic_flag, - media_data_list, - custom_dl_obj, - ], - }, - ) - - else: - - # Start the download operation immediately - self.download_manager_continue([ - operation_type, - automatic_flag, - media_data_list, - custom_dl_obj, - ]) - - - def download_manager_continue(self, arg_list): - - """Called by self.download_manager_start() and - .update_manager_finished(). - - Having obtained confirmation from the user (if required), start the - download operation. - - Args: - - arg_list (list): List of arguments originally supplied to - self.download_manager_start(). A list in the form - - [ operation_type, automatic_flag, media_data_list, - custom_dl_obj ] - - """ - - # Extract arguments from arg_list - operation_type = arg_list.pop(0) - automatic_flag = arg_list.pop(0) - media_data_list = arg_list.pop(0) - custom_dl_obj = arg_list.pop(0) - - # When not called by the Classic Mode tab: - # - # The media data registry consists of a collection of media data - # objects (media.Video, media.Channel, media.Playlist and - # media.Folder) - # If a list of media data objects was specified by the calling - # function, those media data object and all of their descendants are - # are assigned a downloads.DownloadItem object. If that list instead - # contains media.Scheduled objects, then those objects specify the - # media data objects to download - # Otherwise, all media data objects are assigned a - # downloads.DownloadItem object - # Those downloads.DownloadItem objects are collectively stored in a - # downloads.DownloadList object - # - # When called by the Classic Mode tab: - # - # The user has added one or more URLs to the tab's download list and, - # in response, Tartube has created a number of dummy media.Video - # objects (which have not been added to the media data registry). - # Each dummy object corresponds to a single URL (which might - # represent a video, channel or playlist) - # If a list of dummy media.Video objects was specified by the calling - # function, they are downloaded. Otherwise all dummy media.Video - # objects are downloaded - download_list_obj = downloads.DownloadList( - self, - operation_type, - media_data_list, - custom_dl_obj, - ) - - if not download_list_obj.download_item_list: - - if not automatic_flag: - - if operation_type == 'classic_real' \ - or operation_type == 'classic_sim' \ - or operation_type == 'classic_custom': - - msg = _( - '1. Copy URLs into the box at the top' \ - + '\n2. Select a destination and a format' \ - + '\n3. Click \'Add URLs\'' \ - + '\n4. Click \'Download all\'', - ) - - elif operation_type == 'sim': - - msg = _('There is nothing to check!') - - else: - - msg = _('There is nothing to download!') - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'error', - 'ok', - ) - - return - - # If the flag is set, do an update operation before starting the - # download operation - if self.operation_auto_update_flag and not self.operation_waiting_flag: - - self.update_manager_start('ytdl') - # These IVs tells self.update_manager_finished to start a download - # operation - self.operation_waiting_flag = True - self.operation_waiting_type = operation_type - self.operation_waiting_list = media_data_list - self.operation_waiting_obj = custom_dl_obj - return - - # Set a list of proxies. When one is required, a call to - # self.get_proxy() returns the item at the top of the list, and moves - # it to the bottom of the list - self.dl_proxy_cycle_list = self.dl_proxy_list.copy() - - # Remove videos from the 'Recent Videos' folder, so it can be re-filled - # by any videos checked/downloaded by this operation - # (Don't do so when the download operation was launched from the - # Classic Mode tab, or when a 'custom_sim' operation has finished and - # we're about to start a 'custom_real' operation) - if operation_type != 'classic_real' \ - and operation_type != 'classic_sim' \ - and operation_type != 'classic_custom' \ - and ( - operation_type != 'custom_real' \ - or not custom_dl_obj \ - or not custom_dl_obj.dl_by_video_flag \ - or not custom_dl_obj.dl_precede_flag - ): - remove_time = int(time.time()) \ - - (self.fixed_recent_folder_days * 24 * 60 * 60) - - child_list = self.fixed_recent_folder.child_list.copy() - for child_obj in child_list: - - if not self.fixed_recent_folder_days \ - or child_obj.receive_time < remove_time: - self.fixed_recent_folder.del_child(child_obj) - - # Update the Video Index (and the Video Catalogue, if appropriate) - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_icon, - self.fixed_recent_folder, - ) - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_recent_folder, - ) - - if self.main_win_obj.video_index_current_dbid \ - == self.fixed_recent_folder.dbid: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - ) - - # Reset the dictionary of channel/playlist names extracted from video - # metadata, so it can be refilled - self.media_reset_container_dict = {} - - # Don't show a dialogue window at the end of a scheduled download - if automatic_flag: - self.no_dialogue_this_time_flag = True - - # During a download operation, show a progress bar in the Videos tab - # (except when launched from the Classic Mode tab, in which case we - # just desensitise the existing buttons) - if operation_type == 'sim' or operation_type == 'custom_sim': - self.main_win_obj.show_progress_bar('check') - elif operation_type == 'real' or operation_type == 'custom_real': - self.main_win_obj.show_progress_bar('download') - else: - self.main_win_obj.sensitise_progress_bar(False) - - # Reset the Progress List - self.main_win_obj.progress_list_reset() - # Reset the Results List - self.main_win_obj.results_list_reset() - # Reset the Output tab - self.main_win_obj.output_tab_reset_pages() - - if operation_type == 'sim' \ - or operation_type == 'real' \ - or operation_type == 'custom_sim' \ - or operation_type == 'custom_real': - - # Initialise the Progress List with one row for each media data - # object in the downloads.DownloadList object - # (The Classic Progress List, if in use, has already been - # initialised) - self.main_win_obj.progress_list_init(download_list_obj) - - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(False) - # Make the widget changes visible - self.main_win_obj.show_all() - - # During a download operation, a GObject timer runs, so that at regular - # intervals we can update the Progress tab/Classic Progress tab, and - # the Output tab - # There is also a delay between the instant at which youtube-dl reports - # a video file has been downloaded, and the instant at which it - # appears in the filesystem. The timer checks for newly-existing - # files at regular intervals, too - # Create the timer - self.dl_timer_id = GObject.timeout_add( - self.dl_timer_time, - self.dl_timer_callback, - ) - - # Initiate the download operation. Any code can check whether a - # download, update or refresh operation is in progress, or not, by - # checking this IV - self.current_manager_obj = downloads.DownloadManager( - self, - operation_type, - download_list_obj, - custom_dl_obj, - ) - self.download_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def download_manager_halt_timer(self): - - """Called by downloads.DownloadManager.run() when that function has - finished. - - During a download operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if self.dl_timer_id: - self.dl_timer_check_time \ - = int(time.time()) + self.dl_timer_final_time - - - def download_manager_finished(self): - - """Called by self.dl_timer_callback() and - downloads.DownloadManager.run(). - - The download operation has finished, so update IVs and main window - widgets. - """ - - # This function behaves differently, if the download operation was - # launched from the Classic Mode tab - operation_type = self.download_manager_obj.operation_type - if operation_type == 'clasic_sim' \ - or operation_type == 'classic_real' \ - or operation_type == 'classic_custom': - classic_mode_flag = True - else: - classic_mode_flag = False - - # If the operation was stopped manually, don't proceed from a - # 'custom_sim' to a 'custom_real' operation, or from a - # 'classic_sim' to a 'classic_real' operation - manual_stop_flag = self.download_manager_obj.manual_stop_flag - - # For 'custom_sim' operations, get the original list of media data - # objects that was passed to self.download_manager_start() - orig_media_data_list \ - = self.download_manager_obj.download_list_obj.orig_media_data_list - # For Classic Mode tab custom downloads, get the list of videos which - # were extrascted, along with their metadata - classic_extract_list = self.download_manager_obj.classic_extract_list - - # Get the number of videos downloaded (real and simulated), as well as - # the number of individual video clips downloaded and the number of - # video slices removed - dl_count = self.download_manager_obj.total_dl_count - sim_count = self.download_manager_obj.total_sim_count - clip_count = self.download_manager_obj.total_clip_count - slice_count = self.download_manager_obj.total_slice_count - other_count = self.download_manager_obj.other_video_count - size_count = self.download_manager_obj.total_size_count - - # For the 'custom_sim'/'classic_sim' operation, we need to use the same - # custom download manager - custom_dl_obj = self.download_manager_obj.custom_dl_obj - - # Get the time taken by the download operation, so we can convert it - # into a nice string below (e.g. '05:15') - # For refresh operations, refresh.RefreshManager.stop_time() might not - # have been set at this point (for some reason), so we need to check - # for the equivalent problem - if self.download_manager_obj.stop_time is not None: - time_num = int( - self.download_manager_obj.stop_time \ - - self.download_manager_obj.start_time - ) - else: - time_num = int(time.time() - self.download_manager_obj.start_time) - - # Any code can check whether an operation is in progress, or not, by - # checking this IV - self.current_manager_obj = None - self.download_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.dl_timer_id) - self.dl_timer_id = None - self.dl_timer_check_time = None - # (All videos marked to be launched in the system's default media - # player should have been launched already, but just to be safe, - # empty this list) - self.watch_after_dl_list = [] - - # Downloaded videos can be deleted/removed, if required. The True flag - # updates the Video Catalogue - if self.auto_delete_asap_flag: - self.auto_delete_old_videos(True) - self.auto_remove_old_videos(True) - - # After a download operation, save files, if allowed (but don't bother - # when launched from the Classic Mode tab) - if not classic_mode_flag and self.operation_save_flag: - self.save_config() - self.save_db() - - # After a download operation, update the status icon in the system tray - self.status_icon_obj.update_icon() - - if not classic_mode_flag: - - # Remove the progress bar in the Videos tab - self.main_win_obj.hide_progress_bar() - - # If remaining lines in the Progress List should be hidden, hide - # them - if self.progress_list_hide_flag: - self.main_win_obj.progress_list_check_hide_rows(True) - - else: - - # No progress bar exists; just reset the text on the existing - # buttons, and then resensitise them - self.main_win_obj.update_free_space_msg() - self.main_win_obj.sensitise_progress_bar(True) - - # Reset the update label in the Progress tab, and clear instantaneous - # download speeds used to calculate the rolling average - self.main_win_obj.progress_list_reset_rolling_average() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(True) - # Make the widget changes visible (not necessary if the main window has - # been closed to the system tray) - if self.main_win_obj.is_visible(): - self.main_win_obj.show_all() - - # If Tartube is due to shut down, then shut it down - show_newbie_dialogue_flag = False - - if self.halt_after_operation_flag: - self.stop_continue() - - # Show a dialogue window or desktop notification, if required - elif not self.no_dialogue_this_time_flag \ - and operation_type != 'custom_sim' \ - and operation_type != 'classic_sim': - - # If videos were expected to be checked/downloaded, but nothing - # happened, show a newbie dialogue explaining what to do next - if self.show_newbie_dialogue_flag \ - and dl_count == 0 \ - and sim_count == 0 \ - and clip_count == 0 \ - and slice_count == 0 \ - and other_count == 0: - - show_newbie_dialogue_flag = True - - else: - - if not self.operation_halted_flag: - msg = _('Download operation complete') - else: - msg = _('Download operation halted') - - if dl_count or sim_count or other_count: - - msg += '\n\n' + _('Videos downloaded:') + ' ' \ - + str(dl_count) + '\n' + _('Videos checked:') \ - + ' ' + str(sim_count) - - if clip_count or slice_count: - msg += '\n' - - if clip_count: - msg += '\n' + _('Clips downloaded:') + ' ' \ - + str(clip_count) - if slice_count: - msg += '\n' + _('Video slices removed:') + ' ' \ - + str(slice_count) - - if size_count and (dl_count or other_count): - - msg += '\n\n' + _('Total download:') + ' ' + str( - utils.convert_bytes_to_string(size_count) - ) - - if time_num >= 10: - msg += '\n\n' + _('Time taken:') + ' ' \ - + utils.convert_seconds_to_string(time_num, True) - - if self.operation_dialogue_mode == 'dialogue': - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - ) - - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # In any case, reset those IVs - self.halt_after_operation_flag = False - self.no_dialogue_this_time_flag = False - # Also reset temporary clip/slice/override buffers - self.temp_stamp_buffer_dict = {} - self.temp_slice_buffer_dict = {} - self.temp_output_override_dict = {} - # Also reset operation IVs - self.operation_halted_flag = False - - # A 'classic_sim' operation is followed by a 'classic_real' operation, - # but only if some videos were extracted during the 'classic_sim' - # operation - # A 'custom_sim' operation is followed by a 'custom_real' operation in - # all cases - # Exception: If the first of the two operations was stopped manually, - # don't start the second one - # Launch the second operation, if the first has just finished - if operation_type == 'classic_sim' \ - and classic_extract_list \ - and not manual_stop_flag: - - # The list is in groups of two, in the form - # [ parent_obj, json_dict ] - # ...where 'parent_obj' is a 'dummy' media.Video object - # representing a video, channel or playlist, from which the - # metedata for a single video, 'json_dict', has been extracted - # Create new dummy media.Video objects, one for each extracted - # video - dummy_list = [] - while classic_extract_list: - - parent_obj = classic_extract_list.pop(0) - json_dict = classic_extract_list.pop(0) - - dummy_video_obj \ - = self.main_win_obj.classic_mode_tab_create_dummy_video( - json_dict['webpage_url'], - parent_obj.dummy_dir, - parent_obj.dummy_format, - ) - - dummy_list.append(dummy_video_obj) - - # Update the dummy video's filename/file extension, if - # available, so that clip titles can be set correctly (when - # required) - # Git #177 reports that this value might be 'None', so check - # for that - if '_filename' in json_dict \ - and json_dict['_filename'] is not None: - dummy_video_obj.set_file_from_path(json_dict['_filename']) - - # Update the dummy video object's metadata and/or description, - # so that its timestamp list can be set - if 'chapters' in json_dict: - - dummy_video_obj.extract_timestamps_from_chapters( - self, - json_dict['chapters'], - ) - - elif 'description' in json_dict: - - dummy_video_obj.set_video_descrip( - self, - json_dict['description'], - self.main_win_obj.descrip_line_max_len, - ) - - # In the Classic Progress List, remove the row for parent_obj - # (if it still exists) - if parent_obj.dbid in self.main_win_obj.classic_media_dict: - self.main_win_obj.classic_mode_tab_remove_rows( - [ parent_obj.dbid ], - ) - - # Start the second download operation - self.download_manager_start( - 'classic_custom', - False, # Not called from a timer - dummy_list, - custom_dl_obj, - ) - - elif operation_type == 'custom_sim' and not manual_stop_flag: - - # Start the second download operation - self.download_manager_start( - 'custom_real', - False, # Not called from a timer - orig_media_data_list, - custom_dl_obj, - ) - - # Otherwise, show the newbie dialogue, if required - elif show_newbie_dialogue_flag \ - and not self.debug_disable_newbie_flag \ - and not manual_stop_flag: - - dialogue_win = mainwin.NewbieDialogue( - self.main_win_obj, - classic_mode_flag, - ) - dialogue_win.run() - - # Retrieve user choices from the dialogue window... - newbie_update_flag = dialogue_win.update_flag - newbie_config_flag = dialogue_win.config_flag - newbie_change_flag = dialogue_win.change_flag - newbie_website_flag = dialogue_win.website_flag - newbie_issues_flag = dialogue_win.issues_flag - - self.show_newbie_dialogue_flag = dialogue_win.show_flag - - dialogue_win.destroy() - - if newbie_update_flag: - self.update_manager_start('ytdl') - elif newbie_config_flag: - config.SystemPrefWin(self, 'paths') - elif newbie_change_flag: - config.SystemPrefWin(self, 'forks') - elif newbie_website_flag: - utils.open_file(self, __main__.__website__) - elif newbie_issues_flag: - utils.open_file(self, __main__.__website_bugs__) - - - def update_manager_start(self, update_type): - - """Can be called by anything. - - Initiates an update operation to do one of two jobs: - - 1. Install youtube-dl (or a fork of it), or update it to its most - recent version. - - 2. Install FFmpeg, matplotlib or streamlink (on MS Windows only) - - Creates a new updates.UpdateManager object to handle the update - operation. When the operation is complete, - self.update_manager_finished() is called. - - Args: - - update_type (str): 'ffmpeg' to install FFmpeg, 'matplotlib' to - install matplotlib, 'streamlink' to install streamlinkg, or - 'ytdl' to install/update youtube-dl (or a fork of it) - - """ - - # Check for interrupted database loads - if self.db_loading_flag: - - self.disable_load_save() - self.system_error( - 116, - 'Cannot start the update operation because Tartube failed' \ - + ' to finish loading a database (which seems to be broken)', - ) - - return - - # If a livestream operation is running, tell it to stop immediately - if self.livestream_manager_obj: - self.livestream_manager_obj.stop_livestream_operation() - - # If a livestream operation was running, this IV should now be reset - if self.current_manager_obj: - - # Operation already in progress - return self.system_error( - 117, - 'Operation already in progress', - ) - - elif self.main_win_obj.config_win_list: - # Update operation is not allowed when a configuration window is - # open - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'An update operation cannot start if one or more' \ - + ' configuration windows are still open', - ), - 'error', - 'ok', - ) - - return - - elif __main__.__pkg_strict_install_flag__: - # Update operation is disabled in the Debian/RPM package. It should - # not be possible to call this function, but we'll show an error - # message anyway - return self.system_error( - 118, - 'Update operations are disabled in this version of Tartube', - ) - - elif update_type == 'ffmpeg' and os.name != 'nt': - # The update operation can only install FFmpeg on the MS Windows - # installation of Tartube. It should not be possible to call this - # function, but we'll show an error message anyway - return self.system_error( - 119, - 'Update operation cannot install FFmpeg on your operating' \ - + ' system', - ) - - elif update_type == 'matplotlib' and os.name != 'nt': - # The same applies to matplotlib - return self.system_error( - 120, - 'Update operation cannot install matplotlib on your' \ - + ' operating system', - ) - - elif update_type == 'streamlink' and os.name != 'nt': - # The same applies to streamlink - return self.system_error( - 121, - 'Update operation cannot install streamlink on your' \ - + ' operating system', - ) - - # During an update operation, certain widgets are modified and/or - # desensitised - self.main_win_obj.sensitise_check_dl_buttons(False, update_type) - self.main_win_obj.output_tab_reset_pages() - if self.auto_switch_output_flag: - self.main_win_obj.output_tab_show_first_page() - - # During an update operation, a GObject timer runs, so that the Output - # tab can be updated at regular intervals - # Create the timer - self.update_timer_id = GObject.timeout_add( - self.update_timer_time, - self.update_timer_callback, - ) - - # Initiate the update operation. Any code can check whether a - # download, update or refresh operation is in progress, or not, by - # checking this IV - self.current_manager_obj = updates.UpdateManager(self, update_type) - self.update_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def update_manager_start_from_wizwin(self, wiz_win_obj, update_type): - - """Called by the setup wizard window, before the (real) main window has - been created. - - Initiates an update operation to do one of two jobs: - - 1. Install youtube-dl (or a fork of it), or update it to its most - recent version. - - 2. Install FFmpeg (on MS Windows only; at the moment, the wizard - window does not try to install matplotlib or streamlink) - - Creates a new updates.UpdateManager object to handle the update - operation. When the operation is complete, - self.update_manager_finished() is called. - - Args: - - wiz_win_obj (wizwin.SetupWizWin): The calling window - - update_type (str): 'ffmpeg' to install FFmpeg, or 'ytdl' to - install/update youtube-dl - - Return values: - - True if we attempted to start the update operation, False if not - - """ - - if self.current_manager_obj \ - or __main__.__pkg_strict_install_flag__ \ - or update_type == 'ffmpeg' and os.name != 'nt': - return False - - # During an update operation, a GObject timer runs, so that the Output - # tab can be updated at regular intervals - # Create the timer - self.update_timer_id = GObject.timeout_add( - self.update_timer_time, - self.update_timer_callback, - ) - - # Initiate the update operation. Any code can check whether a - # download, update or refresh operation is in progress, or not, by - # checking this IV - self.current_manager_obj = updates.UpdateManager( - self, - update_type, - wiz_win_obj, - ) - - self.update_manager_obj = self.current_manager_obj - - return True - - - def update_manager_halt_timer(self): - - """Called by updates.UpdateManager.install_ffmpeg(), - .install_matplotlib(), .install_streamlink or .install_ytdl() when - those functions have finished. - - During an update operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if self.update_timer_id: - self.update_timer_check_time \ - = int(time.time()) + self.update_timer_final_time - - - def update_manager_finished(self): - - """Called by self.update_timer_callback(). - - The update operation has finished, so update IVs and main window - widgets. - """ - - global HAVE_MATPLOTLIB_FLAG - - # Import IVs from updates.UpdateManager, before it is destroyed - update_type = self.update_manager_obj.update_type - wiz_win_obj = self.update_manager_obj.wiz_win_obj - success_flag = self.update_manager_obj.success_flag - ytdl_version = self.update_manager_obj.ytdl_version - - # Any code can check whether an operation is in progress, or not, by - # checking this IV - self.current_manager_obj = None - self.update_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.update_timer_id) - self.update_timer_id = None - self.update_timer_check_time = None - - # If this is the first successful update operation, auto-detect - # youtube-dl's actual location (but not on MS Windows, for which the - # location is set in stone) - if success_flag and not self.ytdl_update_once_flag: - - self.ytdl_update_once_flag = True - # (When called from the setup wizard window, this is not done until - # later) - if os.name != 'nt' and not wiz_win_obj: - self.auto_detect_paths() - - # If matplotlib is successfully installed, update the setting - if update_type == 'matplotlib' and success_flag: - HAVE_MATPLOTLIB_FLAG = True - - # After an update operation, save files, if allowed - if not wiz_win_obj: - - if self.operation_save_flag: - self.save_config() - self.save_db() - - # During an update operation, certain widgets are modified and/or - # desensitised; restore them to their original state - self.main_win_obj.sensitise_check_dl_buttons(True) - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - # Then show a dialogue window/desktop notification, if allowed (and if - # a download operation is not waiting to start) - if update_type == 'ffmpeg' \ - or update_type == 'matplotlib' \ - or update_type == 'streamlink': - - if not success_flag: - msg = _('Installation failed') - else: - msg = _('Installation complete') - - else: - if not success_flag: - msg = _('Update operation failed') - elif self.operation_halted_flag: - msg = _('Update operation halted') - else: - msg = _('Update operation complete') \ - + '\n\n' + self.get_downloader(wiz_win_obj) + ' ' \ - + _('version:') + ' ' - if ytdl_version is not None: - msg += ytdl_version - else: - msg += _('(unknown)') - - if wiz_win_obj: - - if update_type == 'ytdl': - wiz_win_obj.downloader_fetch_finished(msg) - else: - wiz_win_obj.ffmpeg_fetch_finished(msg) - - elif self.operation_dialogue_mode != 'default' \ - and not self.operation_waiting_flag: - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - ) - - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - - # If a download operation is waiting to start, start it - if self.operation_waiting_flag: - self.download_manager_continue( - [ - self.operation_waiting_type, - False, - self.operation_waiting_list, - self.operation_waiting_obj, - ], - ) - - # Reset those IVs, ready for any future download operations - self.operation_waiting_flag = False - self.operation_waiting_type = None - self.operation_waiting_list = [] - self.operation_waiting_obj = None - - - def refresh_manager_start(self, media_data_obj=None): - - """Can be called by anything. - - Initiates a refresh operation to compare Tartube's data directory with - the media registry, updating the registry as appropriate. - - Creates a new refresh.RefreshManager object to handle the refresh - operation. When the operation is complete, - self.refresh_manager_finished() is called. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder or - None): If specified, only this channel/playlist/folder is - refreshed. If not specified, the entire media registry is - refreshed - - """ - - # Check for interrupted database loads - if self.db_loading_flag: - - self.disable_load_save() - self.system_error( - 122, - 'Cannot start the refresh operation because Tartube failed' \ - + ' to finish loading a database (which seems to be broken)', - ) - - return - - # If a livestream operation is running, tell it to stop immediately - if self.livestream_manager_obj: - self.livestream_manager_obj.stop_livestream_operation() - - # If a livestream operation was running, this IV should now be reset - if self.current_manager_obj: - - # Operation already in progress - return self.system_error( - 123, - 'Operation already in progress', - ) - - elif media_data_obj is not None \ - and isinstance(media_data_obj, media.Video): - - return self.system_error( - 124, - 'Refresh operation cannot be applied to an individual video', - ) - - elif self.main_win_obj.config_win_list: - - # Refresh operation is not allowed when a configuration window is - # open - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'A refresh operation cannot start if one or more' \ - + ' configuration windows are still open', - ), - 'error', - 'ok', - ) - - return - - # The user might not be aware of what a refresh operation is, or the - # effect it might have on Tartube's database - # Warn them, and give them the opportunity to back out - msg = _( - 'During a refresh operation, Tartube analyses its data folder,' \ - + ' looking for videos that haven\'t yet been added to its' \ - + ' database', - ) + '\n\n' + _( - 'You only need to perform a refresh operation if you have' \ - + ' manually copied videos into Tartube\'s data folder', - ) + '\n\n' - - if not media_data_obj: - - msg += _( - 'Before starting a refresh operation, you should click the' \ - + ' \'Check all\' button in the main window', - ) - - elif isinstance(media_data_obj, media.Channel): - - msg += _( - 'Before starting a refresh operation, you should right-click' \ - + ' the channel and select \'Check channel\'', - ) - - elif isinstance(media_data_obj, media.Playlist): - - msg += _( - 'Before starting a refresh operation, you should right-click' \ - + ' the playlist and select \'Check playlist\'', - ) - - else: - - msg += _( - 'Before starting a refresh operation, you should right-click' \ - + ' the folder and select \'Check folder\'', - ) - - msg += '\n\n' + _( - 'Are you sure you want to proceed with the refresh operation?', - ) - - - self.dialogue_manager_obj.show_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .move_container_to_top_continue() - { - 'yes': 'refresh_manager_continue', - 'data': media_data_obj, - }, - ) - - - def refresh_manager_continue(self, media_data_obj=None): - - """Called by self.refresh_manager_start(). - - Having obtained confirmation from the user, start the refresh - operation. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder or - None): If specified, only this channel/playlist/folder is - refreshed. If not specified, the entire media registry is - refreshed - - """ - - # During a refresh operation, show a progress bar in the Videos tab - self.main_win_obj.show_progress_bar('refresh') - # Reset the Output tab - self.main_win_obj.output_tab_reset_pages() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(False, True) - # Make the widget changes visible - self.main_win_obj.show_all() - - # During a refresh operation, a GObject timer runs, so that the Output - # tab can be updated at regular intervals - # Create the timer - self.refresh_timer_id = GObject.timeout_add( - self.refresh_timer_time, - self.refresh_timer_callback, - ) - - # Initiate the refresh operation. Any code can check whether a - # download, update or refresh operation is in progress, or not, by - # checking this IV - self.current_manager_obj = refresh.RefreshManager(self, media_data_obj) - self.refresh_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def refresh_manager_halt_timer(self): - - """Called by refresh.RefreshManager.run() when that function has - finished. - - During a refresh operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if self.refresh_timer_id: - self.refresh_timer_check_time \ - = int(time.time()) + self.refresh_timer_final_time - - - def refresh_manager_finished(self): - - """Called by self.refresh_timer_callback(). - - The refresh operation has finished, so update IVs and main window - widgets. - """ - - # Get the time taken by the refresh operation, so we can convert it - # into a nice string below (e.g. '05:15') - # For some reason, RefreshManager.stop_time() might not be set, so we - # need to check for that - if self.refresh_manager_obj.stop_time is not None: - time_num = int( - self.refresh_manager_obj.stop_time \ - - self.refresh_manager_obj.start_time - ) - else: - time_num = int(time.time() - self.refresh_manager_obj.start_time) - - # Any code can check whether a download/update/refresh/info/tidy - # operation is in progress, or not, by checking this IV - self.current_manager_obj = None - self.refresh_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.refresh_timer_id) - self.refresh_timer_id = None - self.refresh_timer_check_time = None - - # After a refresh operation, save files, if allowed - if self.operation_save_flag: - self.save_config() - self.save_db() - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - # Remove the progress bar in the Videos tab - self.main_win_obj.hide_progress_bar() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(True) - # Make the widget changes visible (not necessary if the main window has - # been closed to the system tray) - if self.main_win_obj.is_visible(): - self.main_win_obj.show_all() - - # Then show a dialogue window/desktop notification, if allowed - if self.operation_dialogue_mode != 'default': - - if not self.operation_halted_flag: - msg = _('Refresh operation complete') - else: - msg = _('Refresh operation halted') - - if time_num >= 10: - msg += '\n\n' + _('Time taken:') + ' ' \ - + utils.convert_seconds_to_string(time_num, True) - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - - - def info_manager_start(self, info_type, media_data_obj=None, - url_string=None, options_string=None): - - """Can be called by anything. - - Initiates an info operation to do one of three jobs: - - 1. Fetch a list of available formats for a video, directly from - youtube-dl - - 2. Fetch a list of available subtitles for a video, directly from - youtube-dl - - 3. Test youtube-dl with specified download options; everything is - downloaded into a temporary directory - - 4. Check the Tartube website, and inform the user if a new release is - available - - Creates a new info.InfoManager object to handle the info operation. - When the operation is complete, self.info_manager_finished() is - called. - - Args: - - info_type (str): 'formats' to fetch a list of formats, 'subs' to - fetch a list of subtitles, or 'test_ytdl' to test youtube-dl - with specified options, 'version' to check for a new release of - Tartube - - media_data_obj (media.Video): For 'formats' and 'subs', the - media.Video object for which formats/subtitles should be - fetched. For 'test_ytdl', set to None - - url_string (str): For 'test_ytdl', the video URL to download (can - be None or an empty string, if no download is required, for - example 'youtube-dl --version'. For 'formats' and 'subs', - set to None - - options_string (str): For 'test_ytdl', a string containing one or - more youtube-dl download options. The string, generated by a - Gtk.TextView, typically contains newline and/or multiple - whitespace characters; the info.InfoManager code deals with - that. Can be None or an empty string, if no download options - are required. For 'formats' and 'subs', set to None - - """ - - # Check for interrupted database loads - if self.db_loading_flag: - - self.disable_load_save() - self.system_error( - 125, - 'Cannot start the info operation because Tartube failed to' \ - + ' finish loading a database (which seems to be broken)', - ) - - return - - # If a livestream operation is running, tell it to stop immediately - if self.livestream_manager_obj: - self.livestream_manager_obj.stop_livestream_operation() - - # If a livestream operation was running, this IV should now be reset - if self.current_manager_obj: - - # Operation already in progress - return self.system_error( - 126, - 'Operation already in progress', - ) - - elif info_type != 'formats' \ - and info_type != 'subs' \ - and info_type != 'test_ytdl' \ - and info_type != 'version': - # Unrecognised argument - return self.system_error( - 127, - 'Invalid info operation argument', - ) - - elif media_data_obj is not None \ - and ( - not isinstance(media_data_obj, media.Video) - or not media_data_obj.source - ): - # Unusable media data object - return self.system_error( - 128, - 'Wrong media data object type or missing source', - ) - - elif self.main_win_obj.config_win_list: - - # Info operation is not allowed when a configuration window is open - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'An info operation cannot start if one or more' \ - + ' configuration windows are still open', - ), - 'error', - 'ok', - ) - - return - - # During an info operation, certain widgets are modified and/or - # desensitised - self.main_win_obj.output_tab_reset_pages() - self.main_win_obj.sensitise_check_dl_buttons(False, info_type) - - # During an info operation, a GObject timer runs, so that the Output - # tab can be updated at regular intervals - # Create the timer - self.info_timer_id = GObject.timeout_add( - self.info_timer_time, - self.info_timer_callback, - ) - - # If testing youtube-dl, empty the temporary directory into which - # anything is downloaded - if info_type == 'test_ytdl': - - if os.path.isdir(self.temp_test_dir) \ - and self.remove_directory(self.temp_test_dir): - self.make_directory(self.temp_test_dir) - - # Initiate the info operation. Any code can check whether an operation - # is in progress, or not, by checking this IV - self.current_manager_obj = info.InfoManager( - self, - info_type, - media_data_obj, - url_string, - options_string, - ) - - self.info_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def info_manager_halt_timer(self): - - """Called by info.InfoManager.run() when that function has finished. - - During an info operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if self.info_timer_id: - self.info_timer_check_time \ - = int(time.time()) + self.info_timer_final_time - - - def info_manager_finished(self): - - """Called by self.info_timer_callback(). - - The info operation has finished, so update IVs and main window widgets. - """ - - # Import IVs from info.InfoManager, before it is destroyed - video_obj = self.info_manager_obj.video_obj - info_type = self.info_manager_obj.info_type - success_flag = self.info_manager_obj.success_flag - output_list = self.info_manager_obj.output_list.copy() - url_string = self.info_manager_obj.url_string - stable_version = self.info_manager_obj.stable_version - dev_version = self.info_manager_obj.dev_version - - # Any code can check whether an operation is in progress, or not, by - # checking this IV - self.current_manager_obj = None - self.info_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.info_timer_id) - self.info_timer_id = None - self.info_timer_check_time = None - - # After an info operation, save files, if allowed - if self.operation_save_flag: - self.save_config() - self.save_db() - - # During an info operation, certain widgets are modified and/or - # desensitised; restore them to their original state - self.main_win_obj.sensitise_check_dl_buttons(True) - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - # When testing youtube-dl, and a source URL was specified by the user, - # open the temporary directory so the user can see what (if - # anything) was downloaded - if url_string is not None and url_string != '': - utils.open_file(self, self.temp_test_dir) - - # Show a confirmation, if allowed - if self.operation_dialogue_mode == 'dialogue' \ - and (info_type == 'formats' or info_type == 'subs') \ - and success_flag: - - # Show a custom dialogue window, that enables the user to modify or - # apply download options directly - dialogue_win = mainwin.FormatsSubsDialogue( - self.main_win_obj, - video_obj, - info_type, - ) - - response = dialogue_win.run() - dialogue_win.destroy() - - elif self.operation_dialogue_mode != 'default': - - # Then show a message dialogue window/desktop notification, if - # allowed - if info_type != 'version' or not success_flag: - - if not success_flag: - msg = _('Operation failed') - else: - msg = _('Operation complete') - - msg += '\n\n' + _('Click the Output tab to see the results') - - else: - - msg = '' - - if stable_version is not None: - - mod_stable_version = self.convert_version(stable_version) - mod_current_version \ - = self.convert_version(__main__.__version__) - - if mod_stable_version > mod_current_version: - msg += _('A new release is available!') + '\n\n' - else: - msg += _('Your installation is up to date!') + '\n\n' - - msg += _('Installed version:') + ' v' \ - + str(__main__.__version__) + '\n\n' - - if stable_version is not None: - - msg += _('Stable release:') + ' v' + str(stable_version) \ - + '\n\n' - - else: - - msg += _('Stable release: not found') + '\n\n' - - if dev_version is not None: - msg += _('Development release:') + ' v' + str(dev_version) - else: - msg += _('Development release: not found') - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - ) - - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - - - def tidy_manager_start(self, choices_dict): - - """Can be called by anything. - - Initiates a tidy operation to tidy up the directories used by each of - one or more media.Channel, media.Playlist and media.Folder objects. - The tidy-up process consists of one or more of the following jobs: - - 1. Check video files are not corrupted (and optionally delete any - that are) - - 2. Check that video files which should exist, actually do (and - vice-versa) - - 3. Delete video files, audio files, description files, metadata (JSON) - files, annotation files, thumbnail files and/or youtube-dl - archive files - - Creates a new tidy.TidyManager object to handle the tidy operation. - When the operation is complete, self.tidy_manager_finished() is - called. - - Args: - - choices_dict (dict): A dictionary specifying the choices made by - the user in mainwin.TidyDialogue. The dictionary is in the - following format: - - media_data_obj: A media.Channel, media.Playlist or media.Folder - object, or None if all channels/playlists/folders are to be - tidied up. If specified, the channel/playlist/folder and - all of its descendants are checked - - corrupt_flag: True if video files should be checked for - corruption - - del_corrupt_flag: True if corrupted video files should be - deleted - - exist_flag: True if video files that should exist should be - checked, in case they don't (and vice-versa) - - del_video_flag: True if downloaded video files should be - deleted - - del_others_flag: True if all video/audio files with the same - name should be deleted (as artefacts of post-processing - with FFmpeg or AVConv) - - remove_no_url_flag: True if any media.Video objects whose URL - is not set should be removed from the database (no files - are deleted) - - remove_duplicate_flag: True if any media.Video objects, which - are not marked as downloaded and which share a URL with - another media.Video object with the same parent and which - is marked as downloaded, should be removed from the - database (no files are deleted) - - del_archive_flag: True if all youtube-dl archive files should - be deleted - - move_thumb_flag: True if all thumbnail files should be moved - into a subdirectory - - del_thumb_flag: True if all thumbnail files should be deleted - - del_webp_flag: True if all .webp thumbnail files should be - deleted - - convert_webp_flag: True if all .webp thumbnail files should be - converted to .jpg - - move_data_flag: True if description, metadata (JSON) and - annotation files should be moved into a subdirectory - - del_descrip_flag: True if all description files should be - deleted - - del_json_flag: True if all metadata (JSON) files should be - deleted - - del_xml_flag: True if all annotation files should be deleted - - convert_ext_flag: True if .unknown_video file extensions should - be converted to .mp4 (experimental; see Git #472) - - """ - - # Check for interrupted database loads - if self.db_loading_flag: - - self.disable_load_save() - self.system_error( - 129, - 'Cannot start the tidy operation because Tartube failed' \ - + ' to finish loading a database (which seems to be broken)', - ) - - return - - # If a livestream operation is running, tell it to stop immediately - if self.livestream_manager_obj: - self.livestream_manager_obj.stop_livestream_operation() - - # If a livestream operation was running, this IV should now be reset - if self.current_manager_obj: - - # Operation already in progress - return self.system_error( - 130, - 'Operation already in progress', - ) - - elif self.main_win_obj.config_win_list: - - # Tidy operation is not allowed when a configuration window is open - if not automatic_flag: - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'A tidy operation cannot start if one or more' \ - + ' configuration windows are still open', - ), - 'error', - 'ok', - ) - - return - - # During a tidy operation, show a progress bar in the Videos tab - self.main_win_obj.show_progress_bar('tidy') - # Reset the Output tab - self.main_win_obj.output_tab_reset_pages() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(False, True) - # Make the widget changes visible - self.main_win_obj.show_all() - - # During a tidy operation, a GObject timer runs, so that the Output tab - # can be updated at regular intervals - # Create the timer - self.tidy_timer_id = GObject.timeout_add( - self.tidy_timer_time, - self.tidy_timer_callback, - ) - - # Initiate the tidy operation. Any code can check whether an operation - # is in progress, or not by checking this IV - self.current_manager_obj = tidy.TidyManager(self, choices_dict) - self.tidy_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def tidy_manager_halt_timer(self): - - """Called by tidy.TidyManager.run() when that function has finished. - - During a tidy operation, a GObject timer was running. Let it continue - running for a few seconds more. - """ - - if self.tidy_timer_id: - self.tidy_timer_check_time \ - = int(time.time()) + self.tidy_timer_final_time - - - def tidy_manager_finished(self): - - """Called by self.tidy_timer_callback(). - - The tidy operation has finished, so update IVs and main window widgets. - """ - - # Get the time taken by the tidy operation, so we can convert it into a - # nice string below (e.g. '05:15') - # For some reason, TidyManager.stop_time() might not be set, so we need - # to check for that - if self.tidy_manager_obj.stop_time is not None: - time_num = int( - self.tidy_manager_obj.stop_time \ - - self.tidy_manager_obj.start_time - ) - else: - time_num = int(time.time() - self.tidy_manager_obj.start_time) - - # Any code can check whether an operation is in progress, or not, by - # checking this IV - self.current_manager_obj = None - self.tidy_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.tidy_timer_id) - self.tidy_timer_id = None - self.tidy_timer_check_time = None - - # After a tidy operation, save files, if allowed - if self.operation_save_flag: - self.save_config() - self.save_db() - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - # Remove the progress bar in the Videos tab - self.main_win_obj.hide_progress_bar() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(True) - # Make the widget changes visible (not necessary if the main window has - # been closed to the system tray) - if self.main_win_obj.is_visible(): - self.main_win_obj.show_all() - - # The Video Catalogue must be redrawn - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - ) - - # Show a dialogue window/desktop notification, if allowed - if self.operation_dialogue_mode != 'default': - - if not self.operation_halted_flag: - msg = _('Tidy operation complete') - else: - msg = _('Tidy operation halted') - - if time_num >= 10: - msg += '\n\n' + _('Time taken:') + ' ' \ - + utils.convert_seconds_to_string(time_num, True) - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - - - def livestream_manager_start(self): - - """Can be called by anything. - - Initiates a livestream operation to check the status of all media.Video - objects marked as livestreams (everything in self.media_reg_live_dict). - - This is one by telling youtube-dl to fetch the video's JSON data. - - If a waiting livestream has started, the data is received (otherwise an - error is received). - - If a current livestream has finished, the JSON data will say so. - - Creates a new downloads.StreamManager object to handle the livestream - operation. When the operation is complete, - self.livestream_manager_finished() is called. - """ - - # N.B. Check commented out at the moment, as I think it's unnecessary -# # Check for interrupted database loads -# if self.db_loading_flag: -# -# self.disable_load_save() -# self.system_error( -# 131, -# 'Cannot start the livestream operation because Tartube' \ -# + ' failed to finish loading a database (which seems to be' \ -# + ' broken)', -# ) -# -# return - - # Operation already in progress, or a configuration window is open, or - # there are no livestreams to check - # A livestream operation is allowed to start when a download operation - # is already running (but not when any other operation is running) - if (self.current_manager_obj and not self.download_manager_obj) \ - or self.livestream_manager_obj \ - or self.main_win_obj.config_win_list \ - or not self.media_reg_live_dict: - - # Don't show a dialogue window as we would for other operations, as - # the livestream operation occurs silently - return - - # For the benefit of future scheduled livestream operations, set the - # time at which this operation began - self.scheduled_livestream_last_time = int(time.time()) - - # Initiate the livestream operation. Any code can check whether an - # operation is in progress, or not, by checking this IV - # (NB Since livestream operations run silently in the background and - # since no functionality is disabled during a livestream operation, - # self.current_manager_obj remains set to None) - self.livestream_manager_obj = downloads.StreamManager(self) - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def livestream_manager_finished(self): - - """Called by downloads.StreamManager.run(). - - The livestream operation has finished, so update IVs and main window - widgets. - """ - - # Any code can check whether livestream operation is in progress, or - # not, by checking this IV - self.livestream_manager_obj = None - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - # Any videos marked as vanished (i.e. missing from the data collected - # by downloads.MiniJSONFetcher) can be removed from the media - # registry immediately - # (Note that if a download operation is running, this function won't - # do everything that it would normally do) - if not self.download_manager_obj: - for video_obj in self.media_reg_live_vanished_dict.values(): - - # The True argument tells the function to delete files - # associated with the video (the thumbnail, in this case) - self.delete_video(video_obj, True) - - # Any videos whose livestream status has changed must be redrawn in - # the Video catalogue - # (This function is called from the downloads.StreamManager object, so - # to prevent a crash, its calls must be wrapped in a timer) - if self.main_win_obj.video_index_current_dbid \ - == self.fixed_live_folder.dbid: - - # Livestreams folder visible; just redraw it - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_redraw_all, - self.fixed_live_folder.dbid, - ) - - else: - - # (Compile a new dictionary, to eliminate duplicates) - temp_dict = {} - for dbid in self.media_reg_live_started_dict: - temp_dict[dbid] = self.media_reg_live_started_dict[dbid] - for dbid in self.media_reg_live_stopped_dict: - temp_dict[dbid] = self.media_reg_live_stopped_dict[dbid] - for this_obj in self.media_reg_live_dict.values(): - temp_dict[this_obj.dbid] = this_obj - - for video_obj in temp_dict.values(): - - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Notify the user and/or open videos in the system's web browser, if - # a waiting livestream has just gone live (and if allowed to do so) - sound_flag = False - for video_obj in self.media_reg_live_started_dict.values(): - - if video_obj.dbid in self.media_reg_dict: - - # Use the video's thumbnail as the notification icon, if - # available (or None, if not, in which case a generic icon is - # automatically used) - if video_obj.dbid in self.media_reg_auto_notify_dict: - self.main_win_obj.notify_desktop( - _('Livestream has started'), - video_obj.name, - utils.find_thumbnail(self, video_obj), - video_obj.source, - ) - - # Play only one sound effect per livestream operation - if video_obj.dbid in self.media_reg_auto_alarm_dict: - sound_flag = True - - if video_obj.dbid in self.media_reg_auto_open_dict \ - and video_obj.source: - utils.open_file(self, video_obj.source) - - if sound_flag: - self.play_sound() - - # If the livestream has just started or just stopped, download it (if - # required to do so) - # First compile a dictionary to eliminate duplicate videos - dl_dict = {} - for video_obj in self.media_reg_live_started_dict.values(): - if video_obj.dbid in self.media_reg_auto_dl_start_dict: - dl_dict[video_obj.dbid] = video_obj - - for video_obj in self.media_reg_live_stopped_dict.values(): - if video_obj.dbid in self.media_reg_auto_dl_stop_dict: - dl_dict[video_obj.dbid] = video_obj - - # If the livestream was downloaded when it was still - # broadcasting, then a new download must overwrite the - # original file - # As of April 2023, the youtube-dl --yes-overwrites option has - # been removed (but yt-dlp provides --force-overwrites) - # To keep things consistent for all forks, we will rename the - # original file (in case the download fails) - self.prepare_overwrite_video(video_obj) - - # Reset the temporary IVs, whose values we don't need any more - self.media_reg_live_started_dict = {} - self.media_reg_live_stopped_dict = {} - self.media_reg_live_vanished_dict = {} - - # Then download the marked videos - if dl_dict: - - if not self.download_manager_obj: - - # Start a new download operation - self.download_manager_start( - 'real', - False, - list(dl_dict.values()), - ) - - else: - - # Download operation already in progress - for video_obj in dl_dict.values(): - - download_item_obj \ - = self.download_manager_obj.download_list_obj.create_item( - video_obj, - None, # media.Scheduled object - 'real', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.main_win_obj.progress_list_add_row( - download_item_obj.item_id, - video_obj, - ) - - # Update the main window's progress bar - self.download_manager_obj.nudge_progress_bar() - - - def process_manager_start(self, options_obj, video_list): - - """Can be called by anything. Currently only called by - config.FFmpegOptionsEditWin.apply_changes(). - - Initiates a process operation to process video(s) with FFmpeg, using - the options specified by a ffmpeg_tartube.FFmpegOptionsManager object. - - Creates a new proces.ProcessManager object to handle the process - operation. When the operation is complete, - self.process_manager_finished() is called. - - Args: - - options_obj (ffmpeg_tartube.FFmpegOptionsManager): The object - specifying FFmpeg options to apply when processing the video(s) - - video_list (list): A listof media.Video objects (should contain at - least one video) - - """ - - # Check for interrupted database loads - if self.db_loading_flag: - - self.disable_load_save() - self.system_error( - 132, - 'Cannot start the process operation because Tartube failed' \ - + ' to finish loading a database (which seems to be broken)', - ) - - return - - if self.current_manager_obj: - - # Operation already in progress - return self.system_error( - 133, - 'Operation already in progress', - ) - - elif not video_list: - - return self.system_error( - 134, - 'Process operation requires at least one video', - ) - - # (Process operations don't modify the media data registry, therefore - # they are allowed to run when a configuration window is open) - - # (Process operations should not cause Gtk stability issues) - - # During a process operation, show a progress bar in the Videos tab - self.main_win_obj.show_progress_bar('process') - # Reset the Output tab - self.main_win_obj.output_tab_reset_pages() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(False, True) - # Make the widget changes visible - self.main_win_obj.show_all() - - # During a process operation, a GObject timer runs, so that the Output - # tab can be updated at regular intervals - # Create the timer - self.process_timer_id = GObject.timeout_add( - self.process_timer_time, - self.process_timer_callback, - ) - - # Initiate the process operation. Any code can check whether an - # operation is in progress, or not, by checking this IV - self.current_manager_obj = process.ProcessManager( - self, - options_obj, - video_list, - ) - - self.process_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def process_manager_halt_timer(self): - - """Called by process.ProcessManager.run() when that function has - finished. - - During a process operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if self.process_timer_id: - self.process_timer_check_time \ - = int(time.time()) + self.process_timer_final_time - - - def process_manager_finished(self): - - """Called by self.process_timer_callback(). - - The process operation has finished, so update IVs and main window - widgets. - """ - - # Get the number of successes and failures - success_count = self.process_manager_obj.success_count - fail_count = self.process_manager_obj.fail_count - new_video_list = self.process_manager_obj.new_video_list - - # Get the time taken by the process operation, so we can convert it - # into a nice string below (e.g. '05:15') - # For some reason, ProcessManager.stop_time() might not be set, so we - # need to check for that - if self.process_manager_obj.stop_time is not None: - time_num = int( - self.process_manager_obj.stop_time \ - - self.process_manager_obj.start_time - ) - else: - time_num = int(time.time() - self.process_manager_obj.start_time) - - # Any code can check whether an operation is in progress, or not, by - # checking this IV - self.current_manager_obj = None - self.process_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.process_timer_id) - self.process_timer_id = None - self.process_timer_check_time = None - - # Set the video length and file size for any new media.Video objects - # that have been created (by now, they should all exist on the - # filesystem and be detectable there) - for new_video_obj in new_video_list: - - new_video_path = new_video_obj.get_actual_path(self) - if new_video_path and os.path.isfile(new_video_path): - - self.update_video_from_filesystem( - new_video_obj, - new_video_path, - ) - - # After a process operation, save files, if allowed - if self.operation_save_flag: - self.save_config() - self.save_db() - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - # Remove the progress bar in the Videos tab - self.main_win_obj.hide_progress_bar() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(True) - # Make the widget changes visible (not necessary if the main window has - # been closed to the system tray) - if self.main_win_obj.is_visible(): - self.main_win_obj.show_all() - - # Then show a dialogue window/desktop notification, if allowed - if self.operation_dialogue_mode != 'default': - - if not self.operation_halted_flag: - msg = _('Process operation complete') - else: - msg = _('Process operation halted') - - if success_count or fail_count: - - msg += '\n\n' + _('Files processed:') + ' ' \ - + str(success_count) + '\n' + _('Errors:') + ' ' \ - + str(fail_count) - - if time_num >= 10: - msg += '\n\n' + _('Time taken:') + ' ' \ - + utils.convert_seconds_to_string(time_num, True) - - if self.operation_dialogue_mode == 'dialogue': - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - ) - - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - # Also reset temporary clip/slice/override buffers - self.temp_stamp_buffer_dict = {} - self.temp_slice_buffer_dict = {} - self.temp_output_override_dict = {} - - - # (Download operation support functions) - - - def create_video_from_download(self, download_item_obj, dir_path, \ - filename, extension, no_sort_flag=False): - - """Called downloads.VideoDownloader.confirm_new_video(), - .confirm_old_video() and .confirm_sim_video(). - - When an individual video has been downloaded, this function is called - to create a new media.Video object. - - Args: - - download_item_obj (downloads.DownloadItem): The object used to - track the download status of a media data object (media.Video, - media.Channel or media.Playlist) - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - no_sort_flag (bool): True when called by - downloads.VideoDownloader.confirm_sim_video(), because the - video's parent containers (including the 'All Videos' folder) - should delay sorting their lists of child objects until that - calling function is ready. False when called by anything else - - Return values: - - video_obj (media.Video) - The video object created - - """ - - # The downloads.DownloadItem handles a download for a video, a channel - # or a playlist - media_data_obj = download_item_obj.media_data_obj - - if isinstance(media_data_obj, media.Video): - - # The downloads.DownloadItem object is handling a single video - video_obj = media_data_obj - - else: - - # The downloads.DownloadItem object is handling a channel or - # playlist - # Does a media.Video object already exist? - video_obj = None - for child_obj in media_data_obj.child_list: - - child_file_dir = None - if child_obj.file_name is not None: - child_file_dir = media_data_obj.get_actual_dir(self) - - if isinstance(child_obj, media.Video) \ - and child_file_dir \ - and child_file_dir == dir_path \ - and child_obj.file_name \ - and child_obj.file_name == filename: - video_obj = child_obj - - if video_obj is None: - - # Create a new media data object for the video - options_manager_obj = download_item_obj.options_manager_obj - fixed_folder_obj \ - = options_manager_obj.options_dict['use_fixed_folder'] - - if fixed_folder_obj: - # Download options specify that the parent is the fixed - # 'Temporary Videos', 'Unsorted Videos' or 'Video Clips' - # folder - video_obj = self.add_video( - fixed_folder_obj, - None, - False, - no_sort_flag, - ) - - else: - video_obj = self.add_video( - media_data_obj, - None, - False, - no_sort_flag, - ) - - # Update the video name/nickname, if it is not set - if video_obj.name == self.default_video_name: - video_obj.set_name(filename) - video_obj.set_nickname(filename) - - # Update the filepath. Even if it is already known, the extension may - # have changed (for example, after checking a video, then downloading - # it) - if video_obj: - video_obj.set_file(filename, extension) - - # If the video is marked as a livestream, then the livestream has - # finished - if video_obj.live_mode: - - self.mark_video_live( - video_obj, - 0, # Not a livestream - {}, # No livestream data - True, # Don't update Video Index yet - True, # Don't update Video Catalogue yet - no_sort_flag, - ) - - # If the video is in a channel or a playlist, assume that youtube-dl is - # supplying a list of videos in the order of upload, newest first - - # in which case, now is a good time to set the video's .receive_time - # IV - # (If not, the IV is set by media.Video.set_dl_flag when the video is - # actually downloaded) - if isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist): - video_obj.set_receive_time() - - return video_obj - - - def convert_video_from_download(self, container_obj, options_manager_obj, - dir_path, filename, extension, no_sort_flag=False): - - """Called downloads.VideoDownloader.confirm_new_video() and - .confirm_sim_video(). - - A modified version of self.create_video_from_download, called when - youtube-dl is about to download a channel or playlist into a - media.Video object. - - Args: - - container_obj (media.Folder): The folder into which a replacement - media.Video object is to be created - - options_manager_obj (options.OptionsManager): The download options - for this media data object - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - no_sort_flag (bool): True when called by - downloads.VideoDownloader.confirm_sim_video(), because the - video's parent containers (including the 'All Videos' folder) - should delay sorting their lists of child objects until that - calling function is ready. False when called by anything else - - Return values: - - video_obj (media.Video) - The video object created - - """ - - # Does the container object already contain this video? - video_obj = None - for child_obj in container_obj.child_list: - - child_file_dir = None - if child_obj.file_dir is not None: - child_file_dir = container_obj.get_actual_dir(self) - - if isinstance(child_obj, media.Video) \ - and child_file_dir \ - and child_file_dir == dir_path \ - and child_obj.file_name \ - and child_obj.file_name == filename: - video_obj = child_obj - - if video_obj is None: - - # Create a new media data object for the video - options_manager_obj = download_item_obj.options_manager_obj - fixed_folder_obj \ - = options_manager_obj.options_dict['use_fixed_folder'] - - if fixed_folder_obj: - # Download options specify that the parent is the fixed - # 'Temporary Videos', 'Unsorted Videos' or 'Video Clips' - # folder - video_obj = self.add_video( - fixed_folder_obj, - None, - False, - no_sort_flag, - ) - - else: - video_obj = self.add_video( - container_obj, - None, - False, - no_sort_flag, - ) - - # Since we have them to hand, set the video's file path IVs - # immediately - video_obj.set_file(filename, extension) - - return video_obj - - - def announce_video_download(self, download_item_obj, video_obj, \ - mini_options_dict): - - """Called by downloads.VideoDownloader.confirm_new_video(), - .confirm_old_video() and .confirm_sim_video(). - - Updates the main window. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object describing the URL from which youtube-dl should download - video(s). - - video_obj (media.Video): The video object for the downloaded video - - mini_options_dict (dict): A dictionary containing a subset of - download options from the the options.OptionsManager object - used to download the video. It contains zero, some or all of - the following download options: - - keep_description keep_info keep_annotations keep_thumbnail - move_description move_info move_annotations move_thumbnail - - """ - - # Add the video to the 'Recent Videos' folder, if it's not already - # there. (The code has to go here and not, say, in - # self.create_video_from_download(), because the latter is not - # always called) - if video_obj and not video_obj in self.fixed_recent_folder.child_list: - - self.fixed_recent_folder.add_child(self, video_obj) - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_recent_folder, - ) - - # If the video's parent media data object (a channel, playlist or - # folder) is selected in the Video Index, update the Video Catalogue - # for the downloaded video - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update the Results List - self.main_win_obj.results_list_add_row( - download_item_obj, - video_obj, - mini_options_dict, - ) - - - def announce_video_clone(self, video_obj): - - """Called by downloads.VideoDownloader.confirm_old_video(). - - This is a modified version of self.update_video_when_file_found(), - called when a channel/playlist/folder is using an alternative - download destination for its videos (in which case, - self.update_video_when_file_found() can't be called). - - Args: - - video_obj (media.Video): The video which already exists on the - user's filesystem (in the alternative download destination) - - """ - - video_path = video_obj.get_actual_path(self) - - # Only set the .name IV if the video is currently unnamed - if video_obj.name == self.default_video_name: - video_obj.set_name(video_obj.file_name) - # (The video's title, stored in the .nickname IV, will be updated - # from the JSON data in a moment) - video_obj.set_nickname(video_obj.file_name) - - # Set the file size - video_obj.set_file_size(os.path.getsize(video_path)) - - # If the JSON file was downloaded, we can extract video statistics from - # it - self.update_video_from_json(video_obj) - - # For any of those statistics that haven't been set (because the JSON - # file was missing or didn't contain the right statistics), set them - # directly - self.update_video_from_filesystem(video_obj, video_path) - - # Mark the video as (fully) downloaded (and update everything else) - self.mark_video_downloaded(video_obj, True) - - - def create_livestream_from_download(self, container_obj, live_mode, - video_name, video_source, video_descrip, video_upload_time, - live_data_dict={}): - - """Called by downloads.JSONFetcher.do_fetch(). - - A modified form of self.create_video_from_download(), called at the end - of a download operation when the RSS feed for a channel or playlist is - checked, and contains an unfamiliar video (indicating that it's a - livestream). - - Creates a new media.Video object for the livestream. - - Args: - - containe_obj (media.Channel or media.Playlist): The channel or - playlist in which a livestream has been detected - - live_mode (int): Matches media.Video.live_mode: 1 for a waiting - livestream, 2 for a livestream that has started - - video_name, video_source, video_descrip (str): Information about - the detected livestream, grabbed from the RSS feed itself - - video_upload_time (int): The video's upload time (in Unix time, to - match media.Video.upload_time) - - live_data_dict (dict): Dictionary of additional data obtained from - the YouTube STDERR message (empty for a livestream already - broadcasting, or if the message couldn't be interpreted). - Dictionary in the form - - live_msg (str): Text that can be displayed in the Video - Catalogue - live_time (int): Approximate time (matching time.time()) at - which the livestream is due to start - live_debut_flag (bool): True for a YouTube 'premiere' video, - False for an ordinary livestream - - """ - - # Fetch the options.OptionsManager object that applies to the container - options_manager_obj = utils.get_options_manager(self, container_obj) - - # Create a new media data object for the video - fixed_folder_obj = options_manager_obj.options_dict['use_fixed_folder'] - if fixed_folder_obj: - # Download options specify that the parent is the fixed 'Temporary - # Videos', 'Unsorted Videos' or 'Video Clips' folder - container_obj = fixed_folder_obj - - video_obj = self.add_video( - container_obj, - video_source, - False, # Not a simulated download - True, # Let the calling function sort the container - ) - - # Update its IVs - video_obj.set_receive_time() - video_obj.set_name(video_name) - video_obj.set_nickname(video_name) - video_obj.set_video_descrip( - self, - video_descrip, - self.main_win_obj.descrip_line_max_len, - ) - video_obj.set_upload_time(video_upload_time) - - # Give it a fake filename/extension, so that the Video Catalogue can - # find the thumbnail - # (If a youtube-dl output template is applied, the file that might be - # downloaded later will have a modified name and/or extension) - video_obj.set_file(video_name, '.mp4') - - # Mark it as a livestream - self.mark_video_live(video_obj, live_mode, live_data_dict) - - # We can now sort the parent containers - video_obj.parent_obj.sort_children(self) - self.fixed_all_folder.sort_children(self) - self.fixed_recent_folder.sort_children(self) - self.fixed_live_folder.sort_children(self) - - - def update_video_when_file_found(self, video_obj, video_path, temp_dict, \ - mkv_flag=False): - - """Called by mainwin.MainWin.results_list_update_row(). - - When youtube-dl reports it is finished, there is a short delay before - the final downloaded video(s) actually exist in the filesystem. - - Once the calling function has confirmed the file exists, it calls this - function to update the media.Video object's IVs. - - Args: - - video_obj (media.Video): The video object to update - - video_path (str): The full filepath to the video file that has been - confirmed to exist - - temp_dict (dict): Dictionary of values used to update the video - object, in the form: - - 'video_obj': not required by this function, as we already have - it - 'row_num': not required by this function - 'keep_description', 'keep_info', 'keep_annotations', - 'keep_thumbnail', 'move_description', 'move_info', - 'move_annotations', 'move_thumbnail': flags from the - options.OptionsManager object used for to download the - video ('keep_description', etc, are not not added to the - dictionary at all for simulated downloads) - - mkv_flag (bool): If the warning 'Requested formats are incompatible - for merge and will be merged into mkv' has been seen, the - calling function has found an .mkv file rather than the .mp4 - file it was expecting, and has set this flag to True - - """ - - # Only set the .name IV if the video is currently unnamed - # (N.B. The output template override, if applicable, is handled below) - if video_obj.name == self.default_video_name: - video_obj.set_name(video_obj.file_name) - # (The video's title, stored in the .nickname IV, will be updated - # from the JSON data in a momemnt) - video_obj.set_nickname(video_obj.file_name) - - # If it's an .mkv file because of a failed merge, update the IV - if mkv_flag: - video_obj.set_mkv() - - # Set the file size - video_obj.set_file_size(os.path.getsize(video_path)) - - # If the JSON file was downloaded, we can extract video statistics from - # it - self.update_video_from_json(video_obj) - - # That function call updates the video's .nickname. If an output - # template override applies to this video, we can update IVs now, - # completely replacing the nickname generated already - if video_obj.dbid in self.temp_output_override_dict: - name = self.temp_output_override_dict[video_obj.dbid] - video_obj.set_nickname(name) - # (The temporary buffer, once used, must be emptied immediately) - del self.temp_output_override_dict[video_obj.dbid] - - # For any of those statistics that haven't been set (because the JSON - # file was missing or didn't contain the right statistics), set them - # directly - self.update_video_from_filesystem(video_obj, video_path) - - # If FFmpeg is installed, convert .webp thumbnail files to .jpg - thumb_path = utils.find_thumbnail_webp_intact_or_broken( - self, - video_obj, - ) - if thumb_path is not None \ - and not self.ffmpeg_fail_flag \ - and self.ffmpeg_convert_webp_flag \ - and not self.ffmpeg_manager_obj.convert_webp(thumb_path): - - self.ffmpeg_fail_flag = True - self.system_error(135, self.ffmpeg_fail_msg) - - # Discard the description, JSON, annotations and thumbnail files, if - # required to do so. The files are moved to Tartube's temporary - # directory, to be deleted at or before the next startup - # If the files aren't discarded, move them into the sub-directories - # '.thumbs' or '.data', if required - - # Description file - if 'keep_description' in temp_dict \ - and not temp_dict['keep_description']: - - old_path = video_obj.check_actual_path_by_ext(self, '.description') - if old_path is not None: - - utils.convert_path_to_temp( - self, - old_path, - True, # Move the file - ) - - elif 'move_description' in temp_dict \ - and temp_dict['move_description']: - - utils.move_metadata_to_subdir(self, video_obj, '.description') - - # JSON file - if 'keep_info' in temp_dict and not temp_dict['keep_info']: - - old_path = video_obj.check_actual_path_by_ext(self, '.info.json') - if old_path is not None: - - utils.convert_path_to_temp( - self, - old_path, - True, # Move the file - ) - - elif 'move_info' in temp_dict and temp_dict['move_info']: - - utils.move_metadata_to_subdir(self, video_obj, '.info.json') - - # Annotations file - if 'keep_annotations' in temp_dict \ - and not temp_dict['keep_annotations']: - - old_path = video_obj.check_actual_path_by_ext( - self, - '.annotations.xml', - ) - - if old_path is not None: - - utils.convert_path_to_temp( - self, - old_path, - True, # Move the file - ) - - elif 'move_annotations' in temp_dict \ - and temp_dict['move_annotations']: - - utils.move_metadata_to_subdir(self, video_obj, '.annotations.xml') - - # Thumbnail - if 'keep_thumbnail' in temp_dict and not temp_dict['keep_thumbnail']: - - old_path = utils.find_thumbnail(self, video_obj) - - if old_path is not None: - utils.convert_path_to_temp( - self, - old_path, - True, # Move the file - ) - - elif 'move_thumbnail' in temp_dict and temp_dict['move_thumbnail']: - - utils.move_thumbnail_to_subdir(self, video_obj) - - # Mark the video as (fully) downloaded (and update everything else) - self.mark_video_downloaded(video_obj, True) - - # Register the video's size with the download manager, so that disk - # space limits can be applied, if required - if self.download_manager_obj and video_obj.dl_flag: - self.download_manager_obj.register_video_size(video_obj.file_size) - - # If required, launch this video in the system's default media player - if video_obj in self.watch_after_dl_list: - - self.watch_after_dl_list.remove(video_obj) - self.watch_video_in_player(video_obj) - self.mark_video_new(video_obj, False) - if video_obj.waiting_flag: - self.mark_video_waiting(video_obj, False) - - - def update_video_from_json(self, video_obj, mode='default'): - - """Called by self.update_video_when_file_found(), - .announce_video_clone(), - refresh.RefreshManager.refresh_from_default_destination() and - process.ProcessManager.run() and several other functions. - - If a video's JSON file exists, extract video statistics from it, and - use them to update the video object. - - Args: - - video_obj (media.Video): The video object to update - - mode (str): 'default' to update everything, 'chapters' to update - only video timestamps, 'comments' to update only comments - - """ - - json_path = video_obj.check_actual_path_by_ext(self, '.info.json') - if json_path is not None: - - json_dict = self.file_manager_obj.load_json(json_path) - - if mode == 'default': - - if 'title' in json_dict: - video_obj.set_nickname(json_dict['title']) - - if 'id' in json_dict: - video_obj.set_vid(json_dict['id']) - - # (Git #322, 'upload_date' might be None) - if 'upload_date' in json_dict \ - and json_dict['upload_date'] is not None: - - try: - # date_string in form YYYYMMDD - date_string = json_dict['upload_date'] - dt_obj = datetime.datetime.strptime( - date_string, - '%Y%m%d', - ) - video_obj.set_upload_time(dt_obj.timestamp()) - - except: - video_obj.set_upload_time() - - if 'duration' in json_dict: - video_obj.set_duration(json_dict['duration']) - - if 'webpage_url' in json_dict: - # !!! DEBUG: yt-dlp Git #119: filter out the extraneous - # characters at the end of the URL, if present -# video_obj.set_source(json_dict['webpage_url']) - video_obj.set_source( - re.sub( - r'\&has_verified\=.*\&bpctr\=.*', - '', - json_dict['webpage_url'], - ) - ) - - if 'description' in json_dict: - video_obj.set_video_descrip( - self, - json_dict['description'], - self.main_win_obj.descrip_line_max_len, - ) - - if self.store_playlist_id_flag \ - and 'playlist_id' in json_dict \ - and not isinstance(video_obj.parent_obj, media.Folder): - - if 'playlist_title' in json_dict: - video_obj.parent_obj.set_playlist_id( - json_dict['playlist_id'], - json_dict['playlist_title'], - ) - else: - video_obj.parent_obj.set_playlist_id( - json_dict['playlist_id'], - None, - ) - - if 'subtitles' in json_dict and json_dict['subtitles']: - video_obj.extract_subs_list(json_dict['subtitles']) - else: - video_obj.reset_subs_list() - - self.extract_parent_name_from_metadata(video_obj, json_dict) - - if isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist): - # 'Enhanced' websites only: set the channel/playlist RSS - # feed, if not already set - video_obj.parent_obj.update_rss_from_json(json_dict) - - # If downloading from a channel/playlist, remember the - # video's index. (The server supplies an index even for a - # channel, and the user might want to convert a channel - # to a playlist) - if 'playlist_index' in json_dict: - video_obj.set_index(json_dict['playlist_index']) - - if ( - (mode == 'default' and self.comment_store_flag) \ - or mode == 'comments' - ) and 'comments' in json_dict: - video_obj.set_comments(json_dict['comments']) - - if ( - (mode == 'default' and self.video_timestamps_extract_json_flag) - or mode == 'chapters' - ) and 'chapters' in json_dict \ - and ( - not video_obj.stamp_list \ - or self.video_timestamps_replace_flag - ): - video_obj.extract_timestamps_from_chapters( - self, - json_dict['chapters'], - ) - - if mode == 'default': - - if 'is_live' in json_dict \ - and json_dict['is_live'] \ - and not video_obj.live_mode \ - and not video_obj.was_live_flag: - self.mark_video_live( - video_obj, - 2, - {}, - True, # Don't update Video Index - True, # Don't update Video Catalogue - True, # Don't sort the parent container - ) - - elif 'was_live' in json_dict \ - and json_dict['was_live']: - if video_obj.live_mode: - self.mark_video_live( - video_obj, - 0, - {}, - True, # Don't update Video Index - True, # Don't update Video Catalogue - True, # Don't sort the parent container - ) - elif not video_obj.was_live_flag: - video_obj.set_was_live_flag(True) - - - def update_video_from_filesystem(self, video_obj, video_path, - override_flag=False): - - """Called by self.update_video_when_file_found(), - .announce_video_clone() and - refresh.RefreshManager.refresh_from_default_destination(). - - Also called by config.VideoEditWin.on_file_button_clicked(). - - If a video's JSON file does not exist, or did not contain the - statistics we were looking for, we can set some of them directly from - the filesystem. - - Args: - - video_obj (media.Video): The video object to update - - video_path (str): The full path to the video's file - - override_flag (bool): If True, the video's existing statistics are - overwritten, if already set. If False, the video's statistics - are only set if not already defined - - """ - - if override_flag or video_obj.upload_time is None: - video_obj.set_upload_time(os.path.getmtime(video_path)) - - if (override_flag or video_obj.duration is None) \ - and HAVE_MOVIEPY_FLAG \ - and self.use_module_moviepy_flag: - - # When the video file is corrupted, moviepy freezes indefinitely - # Instead, let's try placing the procedure inside a thread (unless - # the user has specified a timeout of zero; in which case, don't - # use a thread and let moviepy freeze indefinitely) - if not self.refresh_moviepy_timeout: - - clip = moviepy.editor.VideoFileClip(video_path) - video_obj.set_duration(clip.duration) - - else: - - this_thread = threading.Thread( - target=self.set_duration_from_moviepy, - args=(video_obj, video_path,), - ) - - this_thread.daemon = True - this_thread.start() - this_thread.join(self.refresh_moviepy_timeout) - if this_thread.is_alive(): - self.system_error( - 136, - '\'' + video_obj.parent_obj.name \ - + '\': moviepy module failed to fetch duration' \ - + ' of video \'' + video_obj.name + '\'', - ) - - if override_flag or video_obj.descrip is None: - video_obj.read_video_descrip( - self, - self.main_win_obj.descrip_line_max_len, - ) - - if override_flag or video_obj.file_size is None: - try: - video_obj.set_file_size(os.path.getsize(video_path)) - except: - self.system_error( - 137, - '\'' + video_obj.parent_obj.name \ - + '\': failed to set file size of video \'' \ - + video_obj.name + '\'', - ) - - - def set_duration_from_moviepy(self, video_obj, video_path): - - """Called by self.update_video_from_filesystem(). - - When we call moviepy.editor.VideoFileClip() on a corrupted video file, - moviepy freezes indefinitely. - - This function is called inside a thread, so a timeout of (by default) - ten seconds can be applied. - - Args: - - video_obj (media.Video): The video object being updated - - video_path (str): The path to the video file itself - - """ - - try: - clip = moviepy.editor.VideoFileClip(video_path) - video_obj.set_duration(clip.duration) - except: - self.system_error( - 138, - '\'' + video_obj.parent_obj.name + '\': moviepy module' \ - + ' failed to fetch duration of video \'' \ - + video_obj.name + '\'', - ) - - - def prepare_overwrite_video(self, video_obj): - - """Called by self.livestream_manager_finished() and - mainwin.MainWin.on_click_watch_player_label(). - - If the specified video is a livestream that was downloaded when it was - still broadcasting, then a new download must overwrite the original - file. - - As of April 2023, the youtube-dl --yes-overwrites option has been - removed (but yt-dlp provides --force-overwrites). To keep things - consistent for all forks, we will rename the original file (in case the - download fails). - - Args: - - video_obj (media.Video): The video which this function assumes is - (or was) a livestream - - """ - - path = os.path.abspath( - os.path.join( - video_obj.parent_obj.get_actual_dir(self), - video_obj.file_name + video_obj.file_ext, - ), - ) - - bu_path = path + '_BU' - - if os.path.isfile(path): - - utils.rename_file(self, path, bu_path) - - - def extract_parent_name_from_metadata(self, video_obj, json_dict): - - """Called by self.update_video_from_json() and - downloads.VideoDownloader.confirm_sim_video(). - - self.media_reset_container_dict contains a collection of channels/ - playlists whose names in Tartube's database don't match the names - extracted from a video's metadata. - - Add a new key-value pair to the list, if required. - - Args: - - video_obj (media.Video): The video which has just been checked/ - downloaded - - json_dict (dict): JSON metadata for that video - - """ - - # For a typical YouTube video, both fields will exist - # For a typical YouTube playlist, the .channel field will be something - # like 'REAL_NAME - videos' - if 'channel' in json_dict: - channel_name = json_dict['channel'] - else: - channel_name = None - - if 'playlist_title' in json_dict: - playlist_name = json_dict['playlist_title'] - else: - playlist_name = None - - # The IV only keeps track of channels/playlists - # The IV only keeps track of the first channel/playlist name - # extracted from a child video - # THe IV only keeps track of channels/playlists whose names are not the - # same as those used in Tartube's database - if not isinstance(video_obj.parent_obj, media.Folder) \ - and not video_obj.parent_obj.dbid in self.media_reset_container_dict: - - if isinstance(video_obj.parent_obj, media.Channel) \ - and channel_name is not None \ - and channel_name != '': - self.media_reset_container_dict[video_obj.parent_obj.dbid] \ - = channel_name - - elif isinstance(video_obj.parent_obj, media.Playlist) \ - and playlist_name is not None \ - and playlist_name != '': - self.media_reset_container_dict[video_obj.parent_obj.dbid] \ - = playlist_name - - - # (Add media data objects) - - - def add_video(self, parent_obj, source=None, dl_sim_flag=False, - no_sort_flag=False): - - """Can be called by anything. - - Creates a new media.Video object, and updates IVs. - - Args: - - parent_obj (media.Channel, media.Playlist or media.Folder): The - media data object for which the new media.Video object is the - child (all videos have a parent) - - source (str): The video's source URL, if known - - dl_sim_flag (bool): If True, the video object's .dl_sim_flag IV is - set to True, which forces simulated downloads - - no_sort_flag (bool): True when - self.create_video_from_download() is called by - downloads.VideoDownloader.confirm_sim_video(), because the - video's parent containers (including the 'All Videos' folder) - should delay sorting their lists of child objects until that - calling function is ready. False when called by anything else - - Return values: - - The new media.Video object - - """ - - # Videos can't be placed inside other videos - if parent_obj and isinstance(parent_obj, media.Video): - return self.system_error( - 139, - 'Videos cannot be placed inside other videos', - ) - - # Videos can't be added directly to a private folder - elif parent_obj and isinstance(parent_obj, media.Folder) \ - and parent_obj.priv_flag: - return self.system_error( - 140, - 'Videos cannot be placed inside a private folder', - ) - - # Create a new media.Video object - video_obj = media.Video( - self, - self.media_reg_count, - self.default_video_name, - parent_obj, - None, # Use default download options - no_sort_flag, - ) - - if source is not None: - video_obj.set_source(source) - - if dl_sim_flag: - video_obj.set_dl_sim_flag(True) - - # Update IVs - self.media_reg_count += 1 - self.media_reg_dict[video_obj.dbid] = video_obj - - # The private 'All Videos' folder also has this video as a child object - self.fixed_all_folder.add_child(self, video_obj, no_sort_flag) - - # Update the row in the Video Index for both the parent and private - # folder - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - video_obj.parent_obj, - ) - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_all_folder, - ) - - # If the video's parent is the one visible in the Video Catalogue (or - # if 'Unsorted Videos' or 'Temporary Videos', etc, is the one visible - # in the Video Catalogue), the new video itself won't be visible - # there yet - # Make sure the video is visible, if appropriate - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - return video_obj - - - def add_channel(self, name, parent_obj=None, source=None, \ - dl_sim_flag=None): - - """Can be called by anything. - - Creates a new media.Channel object, and updates IVs. - - Args: - - name (str): The channel name - - parent_obj (media.Folder): The media data object for which the new - media.Channel object is a child (if any) - - source (str): The channel's source URL, if known - - dl_sim_flag (bool): True if we should simulate downloads for videos - in this channel, False if we should actually download them - (when allowed) - - Return values: - - The new media.Channel object - - """ - - # Channels can only be placed inside an unrestricted media.Folder - # object (if they have a parent at all) - if parent_obj \ - and ( - not isinstance(parent_obj, media.Folder) \ - or parent_obj.restrict_mode != 'open' - ): - return self.system_error( - 141, - 'Channels cannot be added to a restricted folder', - ) - - # There is a limit to the number of levels allowed in the media - # registry - if parent_obj and parent_obj.get_depth() >= self.container_max_level: - return self.system_error( - 142, - 'Channel exceeds maximum depth of media registry', - ) - - # Some names are not allowed at all - if name is None \ - or re.search(r'^\s*$', name) \ - or not self.check_container_name_is_legal(name): - return self.system_error( - 143, - 'Illegal channel name', - ) - - # Create a new media.Channel object - channel_obj = media.Channel( - self, - self.media_reg_count, - name, - parent_obj, - None, # Use default download options - ) - - if source is not None: - channel_obj.set_source(source) - - if dl_sim_flag is not None: - channel_obj.set_dl_sim_flag(dl_sim_flag) - - # Update IVs - self.media_reg_count += 1 - self.media_reg_dict[channel_obj.dbid] = channel_obj - self.container_reg_dict[channel_obj.dbid] = channel_obj - if not parent_obj: - self.container_top_level_list.append(channel_obj.dbid) - - # Create the directory used by this channel (if it doesn't already - # exist) - dir_path = channel_obj.get_default_dir(self) - if not os.path.exists(dir_path): - self.make_directory(dir_path) - - return channel_obj - - - def add_playlist(self, name, parent_obj=None, source=None, \ - dl_sim_flag=None): - - """Can be called by anything. - - Creates a new media.Playlist object, and updates IVs. - - Args: - - name (str): The playlist name - - parent_obj (media.Folder): The media data object for which the new - media.Playlist object is a child (if any) - - source (str): The playlist's source URL, if known - - dl_sim_flag (bool): True if we should simulate downloads for videos - in this playlist, False if we should actually download them - (when allowed) - - Return values: - - The new media.Playlist object - - """ - - # Playlists can only be place inside an unrestricted media.Folder - # object (if they have a parent at all) - if parent_obj \ - and ( - not isinstance(parent_obj, media.Folder) \ - or parent_obj.restrict_mode != 'open' - ): - return self.system_error( - 144, - 'Playlists cannot be added to a restricted folder', - ) - - # There is a limit to the number of levels allowed in the media - # registry - if parent_obj and parent_obj.get_depth() >= self.container_max_level: - return self.system_error( - 145, - 'Playlist exceeds maximum depth of media registry', - ) - - # Some names are not allowed at all - if name is None \ - or re.search(r'^\s*$', name) \ - or not self.check_container_name_is_legal(name): - return self.system_error( - 146, - 'Illegal playlist name', - ) - - # Create a new media.Playlist object - playlist_obj = media.Playlist( - self, - self.media_reg_count, - name, - parent_obj, - None, # Use default download options - ) - - if source is not None: - playlist_obj.set_source(source) - - if dl_sim_flag is not None: - playlist_obj.set_dl_sim_flag(dl_sim_flag) - - # Update IVs - self.media_reg_count += 1 - self.media_reg_dict[playlist_obj.dbid] = playlist_obj - self.container_reg_dict[playlist_obj.dbid] = playlist_obj - if not parent_obj: - self.container_top_level_list.append(playlist_obj.dbid) - - # Create the directory used by this playlist (if it doesn't already - # exist) - dir_path = playlist_obj.get_default_dir(self) - if not os.path.exists(dir_path): - self.make_directory(dir_path) - - # Procedure complete - return playlist_obj - - - def add_folder(self, name, parent_obj=None, dl_sim_flag=False, - restrict_mode='open', fixed_flag=False, priv_flag=False, temp_flag=False): - - """Can be called by anything. - - Creates a new media.Folder object, and updates IVs. - - Args: - - name (str): The folder name - - parent_obj (media.Folder): The media data object for which the new - media.Channel object is a child (if any) - - dl_sim_flag (bool): If True, the folders .dl_sim_flag IV is set to - True, which forces simulated downloads for any videos, - channels or playlists contained in the folder - - restrict_mode (str): 'full' if this folder can contain videos, but - not channels/playlists/folders, 'partial' if this folder can - contain videos and folders, but not channels and playlists, - 'open' if this folder can contain any combination of videos, - channels, playlists and folders - - fixed_flag, priv_flag, temp_flag (bool): Flags sent to the object's - .__init__() function - - Return values: - - The new media.Folder object - - """ - - # Folders can only be placed inside an unrestricted media.Folder object - # (if they have a parent at all) - if parent_obj \ - and ( - not isinstance(parent_obj, media.Folder) \ - or parent_obj.restrict_mode == 'full' - ): - return self.system_error( - 147, - 'Folders cannot be added to another restricted folder', - ) - - # There is a limit to the number of levels allowed in the media - # registry - if parent_obj and parent_obj.get_depth() >= self.container_max_level: - return self.system_error( - 148, - 'Folder exceeds maximum depth of media registry', - ) - - # Some names are not allowed at all - if name is None \ - or re.search(r'^\s*$', name) \ - or not self.check_container_name_is_legal(name): - return self.system_error( - 149, - 'Illegal folder name', - ) - - folder_obj = media.Folder( - self, - self.media_reg_count, - name, - parent_obj, - None, # Use default download options - restrict_mode, - fixed_flag, - priv_flag, - temp_flag, - ) - - if dl_sim_flag: - folder_obj.set_dl_sim_flag(True) - - # Update IVs - self.media_reg_count += 1 - self.media_reg_dict[folder_obj.dbid] = folder_obj - self.container_reg_dict[folder_obj.dbid] = folder_obj - if not parent_obj: - self.container_top_level_list.append(folder_obj.dbid) - - # Create the directory used by this folder (if it doesn't already - # exist) - # Obviously don't do that for private folders - dir_path = folder_obj.get_default_dir(self) - if not folder_obj.priv_flag and not os.path.exists(dir_path): - self.make_directory(dir_path) - - # Procedure complete - return folder_obj - - - # (Move media data objects) - - - def move_container_to_top(self, media_data_obj): - - """Called by mainwin.MainWin.on_video_index_move_to_top(). - - Before moving a channel, playlist or folder, get confirmation from the - user. - - After getting confirmation, call self.move_container_to_top_continue() - to move the channel, playlist or folder to the top level (in other - words, removes its parent folder). - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - moving media data object - - """ - - # Do some basic checks - if media_data_obj is None or isinstance(media_data_obj, media.Video) \ - or self.current_manager_obj or not media_data_obj.parent_obj: - return self.system_error( - 150, - 'Move container to top request failed sanity check', - ) - - # Check that the top-level list doesn't already contain a channel/ - # playlist/folder with the same name - if self.find_duplicate_name_in_container(None, media_data_obj.name): - - # (The same error message appears in self.move_container() ) - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Cannot move the channel/playlist/folder because a' \ - + ' container with the same name already exists', - ), - 'error', - 'ok', - ) - - return - - # Check that the target directory doesn't already exist (unlikely, but - # possible if the user has been copying files manually) - target_path = os.path.abspath( - os.path.join( - self.downloads_dir, - media_data_obj.name, - ), - ) - - if os.path.isdir(target_path) or os.path.isfile(target_path): - - # (The same error message appears in self.move_container() ) - self.dialogue_manager_obj.show_simple_msg_dialogue( - _('Cannot move anything to:') + '\n\n' + target_path + '\n\n' \ - + _( - 'because a file or folder with the same name already' \ - + ' exists (although Tartube\'s database doesn\'t know' \ - + ' anything about it)', - ) + '\n\n' + _( - 'You probably created that file/folder accidentally,' \ - + ' in which case you should delete it manually before' \ - + ' trying again', - ), - 'error', - 'ok', - ) - - return - - # Prompt the user for confirmation. If the user clicks 'yes', call - # self.move_container_to_top_continue() to complete the move - media_type = media_data_obj.get_type() - if media_type == 'channel': - msg = _('Are you sure you want to move this channel:') - elif media_type == 'playlist': - msg = _('Are you sure you want to move this playlist:') - else: - msg = _('Are you sure you want to move this folder:') - - msg += '\n\n ' + media_data_obj.name + '\n\n' - - msg += _( - 'This procedure will move all downloaded files to the top' \ - + ' level of Tartube\'s data folder', - ) - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .move_container_to_top_continue() - { - 'yes': 'move_container_to_top_continue', - 'data': media_data_obj, - }, - ) - - - def move_container_to_top_continue(self, media_data_obj): - - """Called by self.move_container_to_top(). - - Moves a channel, playlist or folder to the top level (in other words, - removes its parent folder). - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - moving media data object - - """ - - # Move the sub-directories to their new location - if not self.move_file_or_directory( - media_data_obj.get_default_dir(self), - self.downloads_dir, - ): - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Could not move \'{0}\' (filesystem error)' - ).format(media_data_obj.name), - 'error', - 'ok', - ) - - return - - # Update IVs - media_data_obj.parent_obj.del_child(media_data_obj) - media_data_obj.set_parent_obj(None) - self.container_top_level_list.append(media_data_obj.dbid) - - # Save the database (because, if the user terminates Tartube and then - # restarts it, then tries to perform a download operation, a load of - # Python error messages will be generated, complaining that - # directories don't exist) - self.save_db() - - # Redraw the whole Video Index, which makes sure the moved container - # has an expanding arrow button - self.main_win_obj.video_index_reset() - self.main_win_obj.video_index_populate() - - # Select the moving object, which redraws the Video Catalogue - self.main_win_obj.video_index_select_row(media_data_obj) - - - def move_container(self, source_obj, dest_obj): - - """Called by mainwin.MainWin.on_video_index_drag_data_received(). - - Before moving a channel, playlist or folder, get confirmation from the - user. - - After getting confirmation, call self.move_container_continue() to move - the channel, playlist or folder into another folder. - - Args: - - source_obj (media.Channel, media.Playlist, media.Folder): The - moving media data object - - dest_obj (media.Folder): The destination folder - - """ - - # Do some basic checks - if source_obj is None or isinstance(source_obj, media.Video) \ - or dest_obj is None or isinstance(dest_obj, media.Video): - return self.system_error( - 151, - 'Move container request failed sanity check', - ) - - elif source_obj == dest_obj or source_obj.parent_obj == dest_obj: - # No need for a system error message if the user drags a folder - # onto itself, or onto its own parent; just do nothing - return - - # Ignore Video Index drag-and-drop during an operation - elif self.current_manager_obj: - return - - elif not isinstance(dest_obj, media.Folder): - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Channels, playlists and folders can only be dragged into' \ - + ' a folder', - ), - 'error', - 'ok', - ) - - return - - elif isinstance(source_obj, media.Folder) and source_obj.fixed_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'The fixed folder \'{0}\' cannot be moved (but it can still' \ - + ' be hidden)', - ).format(dest_obj.name), - 'error', - 'ok', - ) - - return - - elif dest_obj.restrict_mode == 'full': - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'The folder \'{0}\' can only contain videos', - ).format(dest_obj.name), - 'error', - 'ok', - ) - - return - - elif dest_obj.restrict_mode == 'partial': - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'The folder \'{0}\' can only contain other folders and videos', - ).format(dest_obj.name), - 'error', - 'ok', - ) - - return - - # Check that the parent folder (or top-level list) doesn't already - # contain a channel/playlist/folder with the same name - if self.find_duplicate_name_in_container(dest_obj, source_obj.name): - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Cannot move the channel/playlist/folder because a' \ - + ' container with the same name already exists', - ), - 'error', - 'ok', - ) - - return - - # Check that the target directory doesn't already exist (unlikely, but - # possible if the user has been copying files manually) - target_path = os.path.abspath( - os.path.join( - dest_obj.get_default_dir(self), - source_obj.name, - ), - ) - - if os.path.isdir(target_path) or os.path.isfile(target_path): - - self.dialogue_manager_obj.show_simple_msg_dialogue( - _('Cannot move anything to:') + '\n\n' + target_path + '\n\n' \ - + _( - 'because a file or folder with the same name already exists' \ - + ' (although Tartube\'s database doesn\'t know anything' \ - + ' about it)', - ) + '\n\n' \ - + _( - 'You probably created that file/folder accidentally, in' \ - + ' which case, you should delete it manually before trying' \ - + ' again', - ), - 'error', - 'ok', - ) - - return - - # Prompt the user for confirmation - source_type = source_obj.get_type() - if source_type == 'channel': - msg = _('Are you sure you want to move this channel:') - elif source_type == 'playlist': - msg = _('Are you sure you want to move this playlist:') - else: - msg = _('Are you sure you want to move this folder:') - - msg += '\n\n ' + source_obj.name + '\n\n' + _('into this folder:') \ - + '\n\n ' + dest_obj.name + '\n\n' - - msg += _( - 'This procedure will move all downloaded files to the new' \ - + ' location', - ) - - if dest_obj.temp_flag: - msg += '\n\n' + _( - 'WARNING: The destination folder is marked as temporary, so' \ - + ' everything inside it will be DELETED when Tartube' \ - + ' restarts!', - ) - - # If the user clicks 'yes', call self.move_container_continue() to - # complete the move - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .move_container_continue() - { - 'yes': 'move_container_continue', - 'data': [source_obj, dest_obj], - }, - ) - - - def move_container_continue(self, media_list): - - """Called by self.move_container(). - - Moves a channel, playlist or folder into another folder. - - Args: - - media_list (list): List in the form (destination, source), where - both are media.Folder objects - - """ - - source_obj = media_list[0] - dest_obj = media_list[1] - - # Move the sub-directories to their new location - if not self.move_file_or_directory( - source_obj.get_default_dir(self), - dest_obj.get_default_dir(self), - ): - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Could not move \'{0}\' (filesystem error)' - ).format(media_data_obj.name), - 'error', - 'ok', - ) - - return - - # Update both media data objects' IVs - if source_obj.parent_obj: - source_obj.parent_obj.del_child(source_obj) - - dest_obj.add_child(self, source_obj) - source_obj.set_parent_obj(dest_obj) - - if source_obj.dbid in self.container_top_level_list: - index = self.container_top_level_list.index(source_obj.dbid) - del self.container_top_level_list[index] - - # Save the database (because, if the user terminates Tartube and then - # restarts it, then tries to perform a download operation, a load of - # Python error messages will be generated, complaining that - # directories don't exist) - self.save_db() - - # Redraw the whole Video Index, which makes sure the moved container - # has an expanding arrow button - self.main_win_obj.video_index_reset() - self.main_win_obj.video_index_populate() - - # Select the moving object, which redraws the Video Catalogue - self.main_win_obj.video_index_select_row(source_obj) - - - def move_videos(self, dest_obj, video_list): - - """Called by mainwin.MainWin.on_video_index_drag_data_received(). - - Moves one or more videos to a new parent container. - - Args: - - dest_obj (media.Channel, media.Playlist, media.Folder): The - destination container - - video_list (list): List of media.Video objects to move into the - destination container - - """ - - if isinstance(dest_obj, media.Video) or not video_list: - return self.system_error( - 152, - 'Move videos request failed sanity check', - ) - - for video_obj in video_list: - if not isinstance(video_obj, media.Video): - return self.system_error( - 153, - 'Move videos request failed sanity check', - ) - - # Videos cannot be dragged into most fixed folders - if isinstance(dest_obj, media.Folder) \ - and dest_obj.priv_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - _('Videos cannot be dragged into this folder'), - 'error', - 'ok', - ) - - return - - # Prompt the user for confirmation - if len(video_list) == 1: - msg = _( - 'Are you sure you want to move the video to \'{0}\'?', - ).format(dest_obj.name) - - else: - - msg = _( - 'Are you sure you want to move \'{0}\' videos to \'{1}\'?', - ).format(len(video_list), dest_obj.name) - - if isinstance(dest_obj, media.Folder) \ - and dest_obj.temp_flag: - msg += '\n\n' + _( - 'WARNING: The destination folder is marked as temporary, so' \ - + ' everything inside it will be DELETED when Tartube' \ - + ' restarts!', - ) - - # If the user clicks 'yes', call self.move_videos_continue() to - # complete the move - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .move_videos_continue() - { - 'yes': 'move_videos_continue', - 'data': [dest_obj, video_list], - }, - ) - - - def move_videos_continue(self, media_list): - - """Called by self.move_videos(). - - Moves a list of videos to a new channel, playlist or folder. - - Args: - - media_list (list): List in the form (destination, video_list), - where the 'destination' is a media.Channel, media.Playlist or - media.Folder object, and 'video_list' is a list of media.Video - objects - - """ - - dest_obj = media_list[0] - video_list = media_list[1] - - # Get the destination directory. If the channel/playlist/folder is - # set up to use another channel/playlist/folder's directory, - # then ignore that: use its default directory - # Exception: do use an external directory, if it is set - if dest_obj.external_dir: - dest_dir = dest_obj.external_dir - else: - dest_dir = dest_obj.get_default_dir(self) - - # Move videos and their associated files, one at a time - success_count = 0 - fail_count = 0 - already_count = 0 - for video_obj in video_list: - - if video_obj.file_name is None: - # No video file to move - fail_count += 1 - continue - - if video_obj.parent_obj == dest_obj: - # Video is already here - already_count += 1 - continue - - # Move the video file (if it has been downloaded) - if video_obj.dl_flag: - old_path = video_obj.get_actual_path(self) - new_path = os.path.abspath( - os.path.join( - dest_dir, - video_obj.file_name + video_obj.file_ext, - ), - ) - - if not os.path.isfile(old_path) \ - or os.path.isfile(new_path): - # Don't move a non-existent file, or overwrite an existing - # file - fail_count += 1 - continue - - if not self.move_file_or_directory(old_path, new_path): - fail_count += 1 - continue - - # Move the metadata files - for file_ext in ('.description', '.info.json', '.annotations.xml'): - - old_path = video_obj.check_actual_path_by_ext(self, file_ext) - if old_path is not None: - - new_path = os.path.abspath( - os.path.join( - dest_dir, - video_obj.file_name + file_ext, - ), - ) - - if os.path.isfile(old_path) \ - and not os.path.isfile(new_path): - self.move_file_or_directory(old_path, new_path) - - # Move the thumbnail - old_path = utils.find_thumbnail(self, video_obj) - if old_path is not None: - - ignore, thumb_file = os.path.split(old_path) - new_path = os.path.abspath( - os.path.join(dest_dir, thumb_file), - ) - - if os.path.isfile(old_path) and not os.path.isfile(new_path): - self.move_file_or_directory(old_path, new_path) - - # Update IVs in the old and new parent containers - video_obj.parent_obj.del_child(video_obj) - # (The True argument instructs the container to delay sorting its - # children) - dest_obj.add_child(self, video_obj, True) - # Update the video - video_obj.set_parent_obj(dest_obj) - - success_count += 1 - - # All done. Tell the destination to sort its children - dest_obj.sort_children(self) - - # Redraw the Video Index (the videos may have come from multiple - # locations, so it's simpler just to redraw the whole thing) - if success_count: - self.main_win_obj.video_index_reset() - self.main_win_obj.video_index_populate() - # Open the destination, which redraws the Video Catalogue - self.main_win_obj.video_index_select_row(dest_obj) - - # Show confirmation dialogue - msg = _('Videos moved') + ': ' + str(success_count) \ - + '\n' + _('Videos not moved') + ': ' \ - + str(fail_count + already_count) - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - ) - - - # (Convert channels to playlists, and vice-versa) - - - def convert_remote_container(self, old_obj): - - """Called by mainwin.MainWin.on_video_index_convert_container(). - - Converts a media.Channel object into a media.Playlist object, or vice- - versa. - - Usually called after the user has copy-pasted a list of URLs into the - mainwin.AddVideoDialogue window, some of which actually represent - channels or playlists, not individual videos. During the next - download operation, new channels or playlists can be automatically - created (depending on the value of self.operation_convert_mode - - The user can then convert a channel to a playlist, and back again, as - required. - - Args: - - old_obj (media.Channel, media.Playlist): The media data object to - convert - - """ - - if ( - not isinstance(old_obj, media.Channel) \ - and not isinstance(old_obj, media.Playlist) - ) or self.current_manager_obj: - return self.system_error( - 154, - 'Convert container request failed sanity check', - ) - - # If old_obj is a media.Channel, create a playlist. If old_obj is - # a media.Playlist, create a channel - if isinstance(old_obj, media.Channel): - - new_obj = self.add_playlist( - old_obj.name, - old_obj.parent_obj, - old_obj.source, - old_obj.dl_sim_flag, - ) - - elif isinstance(old_obj, media.Playlist): - - new_obj = self.add_channel( - old_obj.name, - old_obj.parent_obj, - old_obj.source, - old_obj.dl_sim_flag, - ) - - # Move any children from the old object to the new one - for child_obj in old_obj.child_list: - - # The True argument means to delay sorting the child list - new_obj.add_child(self, child_obj, True) - child_obj.set_parent_obj(new_obj) - - # Deal with alternative download destinations - if old_obj.master_dbid: - new_obj.set_master_dbid(self, old_obj.master_dbid) - master_obj = self.media_reg_dict[old_obj.master_dbid] - master_obj.del_slave_dbid(old_obj.dbid) - - for slave_dbid in old_obj.slave_dbid_list: - slave_obj = self.media_reg_dict[slave_dbid] - slave_obj.set_master_dbid(self, new_obj.dbid) - - # Copy remaining properties from the old object to the new one - new_obj.clone_properties(old_obj) - - # Remove the old object from the media data registry - # (self.container_reg_dict should already be updated) - del self.media_reg_dict[old_obj.dbid] - if old_obj.dbid in self.container_top_level_list: - self.container_top_level_list.remove(old_obj.dbid) - - # Remove the old object from the Video Index... - self.main_win_obj.video_index_delete_row(old_obj) - # ...and add the new one, selecting it at the same time - self.main_win_obj.video_index_add_row(new_obj) - - - # (Delete media data objects) - - - def delete_video(self, video_obj, delete_files_flag=False, - no_update_index_flag=False, no_update_catalogue_flag=False): - - """Can be called by anything. - - Deletes a video object from the media registry. - - Args: - - video_obj (media.Video): The media.Video object to delete - - delete_files_flag (bool): True when called by - mainwin.MainWin.on_video_catalogue_delete_video, in which case - the video and its associated files are deleted from the - filesystem - - no_update_index_flag (bool): True when called by - self.delete_old_videos() or self.delete_container(), in which - case the Video Index is not updated - - no_update_catalogue_flag (bool): True when called by - self.delete_old_videos(), in which case the Video Catalogue is - not updated - - """ - - if not isinstance(video_obj, media.Video): - return self.system_error( - 155, - 'Delete video request failed sanity check', - ) - - # Remove the options.OptionsManager object attached to this video (if - # any). The True argument tells the function not to update the Video - # Index or Video Catalogue - if video_obj.options_obj: - self.remove_download_options(video_obj, True) - - # Remove the video from its parent object - video_obj.parent_obj.del_child(video_obj) - - # Remove the corresponding entry in each private folder's child list - update_list = [video_obj.parent_obj] - if self.fixed_all_folder.del_child(video_obj): - update_list.append(self.fixed_all_folder) - - if self.fixed_bookmark_folder.del_child(video_obj): - update_list.append(self.fixed_bookmark_folder) - - if self.fixed_fav_folder.del_child(video_obj): - update_list.append(self.fixed_fav_folder) - - if self.fixed_live_folder.del_child(video_obj): - update_list.append(self.fixed_live_folder) - - if self.fixed_missing_folder.del_child(video_obj): - update_list.append(self.fixed_missing_folder) - - if self.fixed_new_folder.del_child(video_obj): - update_list.append(self.fixed_new_folder) - - if self.fixed_recent_folder.del_child(video_obj): - update_list.append(self.fixed_recent_folder) - - if self.fixed_waiting_folder.del_child(video_obj): - update_list.append(self.fixed_waiting_folder) - - # Remove the video from our IVs - # v1.2.017 When deleting folders containing thousands of videos, I - # noticed that a small number of video DBIDs didn't exist in the - # media data registry. Not sure what the cause is, but the following - # lines prevent a python error - if video_obj.dbid in self.media_reg_dict: - del self.media_reg_dict[video_obj.dbid] - - if video_obj.dbid in self.media_reg_live_dict: - del self.media_reg_live_dict[video_obj.dbid] - - if video_obj.dbid in self.media_reg_auto_notify_dict: - del self.media_reg_auto_notify_dict[video_obj.dbid] - - if video_obj.dbid in self.media_reg_auto_alarm_dict: - del self.media_reg_auto_alarm_dict[video_obj.dbid] - - if video_obj.dbid in self.media_reg_auto_open_dict: - del self.media_reg_auto_open_dict[video_obj.dbid] - - if video_obj.dbid in self.media_reg_auto_dl_start_dict: - del self.media_reg_auto_dl_start_dict[video_obj.dbid] - - if video_obj.dbid in self.media_reg_auto_dl_stop_dict: - del self.media_reg_auto_dl_stop_dict[video_obj.dbid] - - # Delete files from the filesystem, if required - # If the parent container has an alternative download destination set, - # the files are in the corresponding directory. We don't delete the - # files because another channel/playlist/folder might be using them - if delete_files_flag \ - and video_obj.file_name \ - and video_obj.parent_obj.dbid == video_obj.parent_obj.master_dbid: - self.delete_video_files(video_obj) - - # Remove the video from the list of watchable videos - if video_obj in self.watch_after_dl_list: - self.watch_after_dl_list.remove(video_obj) - - # Remove the video from the catalogue, if present - if not no_update_catalogue_flag: - self.main_win_obj.video_catalogue_delete_video(video_obj) - - # Update rows in the Video Index, first checking that the parent - # container object is currently drawn there (which it might not be, - # if emptying temporary folders on startup) - if not no_update_index_flag: - for container_obj in update_list: - - if container_obj.dbid \ - in self.main_win_obj.video_index_row_dict: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - container_obj, - ) - - # Update a row in the Results List, if the video is visible there - self.main_win_obj.results_list_update_row_on_delete(video_obj.dbid) - - - def delete_video_files(self, video_obj): - - """Called by self.delete_video(), .on_button_classic_redownload() and - mainwin.MainWin.on_video_catalogue_re_download(). - - Deletes the files associated with a media.Video object, including not - just the original video/audio file, but its metadata files too. - - Args: - - video_obj (media.Video): The media.Video object whose files should - be deleted - - """ - - # Sanity check - if video_obj.file_name is None and not video_obj.dummy_flag: - return - - # There might be thousands of files in the directory, so using - # os.walk() or something like that might be too expensive - # Also, post-processing might create various artefacts, all of which - # must be deleted - ext_list = [ - 'description', - 'info.json', - 'annotations.xml', - ] - ext_list.extend(formats.VIDEO_FORMAT_LIST) - ext_list.extend(formats.AUDIO_FORMAT_LIST) - - for ext in ext_list: - - if video_obj.dummy_flag: - - if video_obj.dummy_path is None: - - # Nothing to delete - continue - - else: - - dummy_file, dummy_ext \ - = os.path.splitext(video_obj.dummy_path) - main_path = dummy_file + '.' + ext - if os.path.isfile(main_path): - self.remove_file(main_path) - - else: - - main_path = video_obj.get_default_path_by_ext(self, ext) - if os.path.isfile(main_path): - self.remove_file(main_path) - - else: - - subdir_path \ - = video_obj.get_default_path_in_subdirectory_by_ext( - self, - ext, - ) - - if os.path.isfile(subdir_path): - self.remove_file(subdir_path) - - # (Thumbnails might be in one of two locations, so are handled - # separately) - thumb_path = utils.find_thumbnail(self, video_obj) - if thumb_path and os.path.isfile(thumb_path): - self.remove_file(thumb_path) - - - def delete_container(self, media_data_obj, empty_flag=False): - - """Can be called by anything. - - Before deleting a channel, playlist or folder object from the media - data registry, get confirmation from the user. - - The process is split across three functions. - - This functions obtains confirmation from the user. If deleting files, - a second confirmation is required, and self.delete_container_continue() - is called in response. - - In either case, self.delete_container_complete() is then called to - update the media data registry. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): - The container media data object - - empty_flag (bool): If True, the container media data object is to - be emptied, rather than being deleted - - """ - - # Check this isn't a video or a fixed folder (which cannot be removed) - # v2.1.029 In some older databases, a fixed folder called 'downloads_2' - # was created, containing a small number of videos. I'm still not - # sure under which circumstances that folder was created; in any - # case, such a folder needs to be deleteable - if isinstance(media_data_obj, media.Video) \ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.fixed_flag - and self.check_fixed_folder(media_data_obj) - ): - return self.system_error( - 156, - 'Delete container request failed sanity check', - ) - - # Prompt the user for confirmation, even if the container object has no - # children - # (Even though there are no children, we can't guarantee that the - # sub-directories in Tartube's data directory are empty) - # Exceptions: don't prompt for confirmation if media_data_obj is - # somewhere inside a temporary folder, or if the user has disabled - # these dialogue windows) - confirm_flag = self.show_delete_container_dialogue_flag - delete_file_flag = False - parent_obj = media_data_obj.parent_obj - - while parent_obj is not None: - if isinstance(parent_obj, media.Folder) and parent_obj.temp_flag: - # The media data object is somewhere inside a temporary folder; - # no need to prompt for confirmation - confirm_flag = False - - parent_obj = parent_obj.parent_obj - - if confirm_flag: - - # Prompt the user for confirmation - dialogue_win = mainwin.DeleteContainerDialogue( - self.main_win_obj, - media_data_obj, - empty_flag, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - if dialogue_win.button2.get_active(): - delete_file_flag = True - else: - delete_file_flag = False - - if dialogue_win.button3.get_active(): - show_win_flag = True - else: - show_win_flag = False - - # ...before destroying it - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - # Update IVs - self.delete_container_files_flag = delete_file_flag - self.show_delete_container_dialogue_flag = show_win_flag - - # Get a second confirmation - if delete_file_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Are you SURE you want to delete files? This procedure' \ - ' cannot be reversed!', - ), - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .delete_container_continue() - { - 'yes': 'delete_container_continue', - 'data': [media_data_obj, empty_flag, delete_file_flag], - } - ) - - else: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Are you SURE you want to remove these items from your' \ - + ' database? This procedure cannot be reversed!', - ), - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .delete_container_continue() - { - 'yes': 'delete_container_continue', - 'data': [media_data_obj, empty_flag, delete_file_flag], - } - ) - - - def delete_container_continue(self, data_list): - - """Called by self.delete_container(). - - After getting a confirmation from the user, continue with the - deletion process. - - Args: - - data_list (list): A list of three items. The first is the container - media data object; the second is a flag set to True if the - container should be emptied, rather than being deleted; the - third is True if files should be deleted from the user's - filesystem - - """ - - # Unpack the arguments - media_data_obj = data_list[0] - empty_flag = data_list[1] - delete_file_flag = data_list[2] - - if delete_file_flag: - - container_dir = media_data_obj.get_default_dir(self) - if os.path.isdir(container_dir): - self.remove_directory(container_dir) - - # If emptying the container rather than deleting it, just create a - # replacement (empty) directory on the filesystem - if empty_flag: - self.make_directory(container_dir) - - # Now call self.delete_container_complete() to handle the media data - # registry - self.delete_container_complete(media_data_obj, empty_flag) - - - def delete_container_complete(self, media_data_obj, empty_flag, - recursive_flag=False): - - """Called by self.delete_container_continue(). Subsequently called by - this function recursively. - - Deletes a channel, playlist or folder object from the media data - registry. - - This function calls itself recursively to delete all of the container - object's descendants. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): - The container media data object - - empty_flag (bool): If True, the container media data object is to - be emptied, rather than being deleted - - recursive_flag (bool): Set to False on the initial call to this - function from some other part of the code, and True when this - function calls itself recursively - - """ - - # Confirmation has been obtained, and any files have been deleted (if - # required), so now deal with window widgets and with the media data - # registry - - # Update any profiles that depend on this container - delete_list = [] - for profile_name in self.profile_dict.keys(): - - dbid_list = self.profile_dict[profile_name] - if media_data_obj.dbid in dbid_list: - dbid_list.remove(media_data_obj.dbid) - - # (Profiles cannot be empty) - if not dbid_list: - delete_list.append(profile_name) - - for profile_name in delete_list: - self.delete_profile(profile_name) - - # Remove the marker (if any) on the Video Index row - if media_data_obj.dbid in self.main_win_obj.video_index_marker_dict: - self.main_win_obj.video_index_reset_marker(media_data_obj.dbid) - - # Remove the options.OptionsManager object attached to this media data - # object (if any). The True argument tells the function not to update - # the Video Index or Video Catalogue - if media_data_obj.options_obj: - self.remove_download_options(media_data_obj, True) - - # Recursively remove all of the container object's children. The code - # doesn't work as intended, unless we make a copy of the list of - # child objects first - copy_list = media_data_obj.child_list.copy() - for child_obj in copy_list: - if isinstance(child_obj, media.Video): - self.delete_video(child_obj, False, True, True) - else: - self.delete_container_complete(child_obj, False, True) - - if not empty_flag or recursive_flag: - - # Remove the container object from its own parent object (if it has - # one) - if media_data_obj.parent_obj: - media_data_obj.parent_obj.del_child(media_data_obj) - - # Reset alternative download destinations - media_data_obj.set_master_dbid(self, media_data_obj.dbid) - - # Remove the media data object from our IVs - del self.media_reg_dict[media_data_obj.dbid] - del self.container_reg_dict[media_data_obj.dbid] - if media_data_obj.dbid in self.container_unavailable_dict: - del self.container_unavailable_dict[media_data_obj.dbid] - if media_data_obj.dbid in self.container_top_level_list: - index = self.container_top_level_list.index( - media_data_obj.dbid - ) - del self.container_top_level_list[index] - - # If this container is the alternative download destination for any - # other container(s), then update the other container(s) - for other_dbid in media_data_obj.slave_dbid_list: - other_obj = self.media_reg_dict[other_dbid] - # (No reason why this check should fail, but let's play safe) - if other_obj.master_dbid == media_data_obj.dbid: - other_obj.reset_master_dbid() - - # During the initial call to this function, delete the container - # object from the Video Index (which automatically resets the Video - # Catalogue) - # (If deleting the contents of temporary folders while loading a - # Tartube database, the Video Index may not yet have been drawn, so - # we have to check for that) - if not recursive_flag and not empty_flag \ - and media_data_obj.dbid in self.main_win_obj.video_index_row_dict: - - self.main_win_obj.video_index_delete_row(media_data_obj) - - # Also redraw the private folders in the Video Index, to show the - # correct number of downloaded/new videos, etc - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_all_folder, - ) - - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_bookmark_folder, - ) - - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_fav_folder, - ) - - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_live_folder, - ) - - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_missing_folder, - ) - - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_new_folder, - ) - - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_recent_folder, - ) - - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - self.fixed_waiting_folder, - ) - - elif not recursive_flag and empty_flag: - - # When emptying the container, the quickest way to update the Video - # Index is just to redraw it from scratch - self.main_win_obj.video_index_catalogue_reset() - - - # (Check media data objects) - - - def get_container_list(self, name): - - """Can be called by anything. - - Returns a list of all media.Channel, media.Playlist and media.Folder - objects (ignoring media.Videos) with a matching name. - - Args: - - name (str): The name to check - - Return values: - - A list of channel, playlist and/or folder objects (may be empty) - - """ - - return_list = [] - - for container_obj in self.container_reg_dict.values(): - if container_obj.name == name: - return_list.append(container_obj) - - return return_list - - - def get_first_container(self, name): - - """Can be called by anything. - - Compiles a list of all media.Channel, media.Playlist and media.Folder - objects (ignoring all media.Videos) with a matching name. - - Then walks the list, looking for the object with the lowest .dbid - (representing the one that was created first), and returns that object. - - However, if any of Tartube's system folders have a matching name, then - that folder is returned (regardless of when it was created). - - Args: - - name (str): The name to check - - Return values: - - The oldest media.Channel, media.Playlist or media.Folder found, or - 'None' if no objects with a matching name are found - - """ - - earliest_obj = None - earliest_dbid = 0 - - for container_obj in self.get_container_list(name): - - if isinstance(container_obj, media.Folder) \ - and container_obj.fixed_flag: - return container_obj - - if earliest_dbid == 0 or container_obj.dbid < earliest_dbid: - earliest_obj = container_obj - earliest_dbid = container_obj.dbid - - return earliest_obj - - - def find_duplicate_name_in_container(self, parent_obj, name): - - """Can be called by anything. - - 'parent_obj' is either a media.Folder, or None. - - If 'parent_obj' is specified, we look for a child media.Channel, - media.Playlist or media.Folder object matching the specified name. - media.Video objects are ignored. - - If 'parent_obj' is not specified, we look in the top-level list for a - channel/playlist/folder matching the specified name. - - Args: - - parent_obj (media.Folder or None): The parent folder to check - - name (str): The name of a child channel/playlist/folder to find - - Return values: - - Returns the matching media.Channel, media.Playlist or media.Folder - object, or None if no matching object is found - - """ - - if parent_obj is None: - - for dbid in self.container_top_level_list: - media_data_obj = self.media_reg_dict[dbid] - if not isinstance(media_data_obj, media.Video) \ - and media_data_obj.name == name: - return media_data_obj - - else: - - for media_data_obj in parent_obj.child_list: - if not isinstance(media_data_obj, media.Video) \ - and media_data_obj.name == name: - return media_data_obj - - # No matching object - return None - - - def is_container(self, name): - - """Can be called by anything. - - An alternative to self.get_container_list(), when we only want a yes/no - answer to the question "is there any media.Channel, media.Playlist or - media.Folder object with the same name, anywhere in the database?" - - Args: - - name (str): The name to check - - Return values: - - True if at least one matching object is found, False if no matching - objects are found - - """ - - for container_obj in self.container_reg_dict.values(): - if container_obj.name == name: - return True - - return False - - - def get_fixed_folder(self, fixed_type): - - """Can be called by anything. - - Returns one of Tartube's system media.Folder objects, identified by a - short string (whose value matches the identifiers used in an - options.OptionsManager object). - - Args: - - fixed_type (str): One of the strings 'all', 'bookmark', 'fav', - 'live', 'missing', 'new', 'recent', 'waiting', 'temp', - 'misc' or 'clips' - - Return values: - - The system media.Folder object, or None if fixed_type is - unrecognised - - - """ - - # These values are used by options.OptionsManager - if fixed_type == 'temp': - return self.fixed_temp_folder - elif fixed_type == 'misc': - return self.fixed_misc_folder - elif fixed_type == 'clips': - return self.fixed_clips_folder - - # These values are currently not used by anything, but might be used - # in the future (in which case, this function could be made much - # more efficient) - elif fixed_type == 'all': - return self.fixed_all_folder - elif fixed_type == 'bookmark': - return self.fixed_bookmark_folder - elif fixed_type == 'fav': - return self.fixed_fav_folder - elif fixed_type == 'live': - return self.fixed_live_folder - elif fixed_type == 'missing': - return self.fixed_missing_folder - elif fixed_type == 'new': - return self.fixed_new_folder - elif fixed_type == 'recent': - return self.fixed_recent_folder - elif fixed_type == 'waiting': - return self.fixed_waiting_folder - - else: - return None - - - # (Change media data object settings, updating all related things) - - - def prepare_mark_video(self, data_list): - - """Called by self.mark_container_favourite(), - .mark_container_missing(), .mark_container_new() and - mainwin.MainWin.on_video_index_mark_bookmark(), etc. - - The procecure to mark a container's video as bookmarked or not - bookmarked (etc) can take a very long time, especially if there are - thousands of videos. - - This function takes some shortcuts to reduce the time to a few - seconds. - - Args: - - data_list (list): List in the form - - (action_type, action_flag, container_obj, video_list) - - ...where 'action_type' is one of the strings 'bookmark', - 'favourite', 'missing', 'new' or 'waiting', 'action_flag' is - True (e,g. to bookmark a video) or False (e.g. to unbookmark a - video), 'container_obj' is a media.Channel, media.Playlist or - media.Folder object, and 'video_list' is a list of media.Video - objects to update (only specified when 'action_type' is - 'favourite', 'missing' or 'new') - - """ - - action_type = data_list.pop(0) - action_flag = data_list.pop(0) - container_obj = data_list.pop(0) - if action_type == 'favourite' or action_type == 'missing' \ - or action_type == 'new': - video_list = data_list.pop(0) - else: - video_list = container_obj.child_list - - # Take some shortcuts - for child_obj in video_list: - - if isinstance(child_obj, media.Video): - - if action_type == 'bookmark': - self.mark_video_bookmark( - child_obj, - action_flag, # Mark video bookmarked - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - elif action_type == 'favourite': - - self.mark_video_favourite( - child_obj, - action_flag, # Mark video favourite (or not) - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - elif action_type == 'missing': - - self.mark_video_missing( - child_obj, - action_flag, # Mark video missing (or not) - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - elif action_type == 'new': - - self.mark_video_new( - child_obj, - action_flag, # Mark video favourite (or not) - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - elif action_type == 'waiting': - - self.mark_video_waiting( - child_obj, - action_flag, # Mark video waiting (or not) - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - # Now we can sort the system folder's child list... - if action_type == 'bookmark': - self.fixed_bookmark_folder.sort_children(self) - elif action_type == 'favourite': - self.fixed_fav_folder.sort_children(self) - elif action_type == 'missing': - self.fixed_missing_folder.sort_children(self) - elif action_type == 'new': - self.fixed_new_folder.sort_children(self) - elif action_type == 'waiting': - self.fixed_waiting_folder.sort_children(self) - - # ...and then can redraw the Video Index and Video Catalogue, - # re-selecting the current selection, if any - self.main_win_obj.video_index_catalogue_reset(True) - - - def mark_video_bookmark(self, video_obj, bookmark_flag, \ - no_update_index_flag=False, no_update_catalogue_flag=False, \ - no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as bookmarked or not bookmarked. - - The video object's .bookmark_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - bookmark_flag (bool): True to mark the video as bookmarked, False - to mark it as not bookmarked - - no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'Bookmarks' folder), because the - calling function wants to do that itself - - no_update_catalogue_flag (bool): True if rows in the Video - Catalogue should not be updated, because the calling function - wants to redraw the whole catalogue itself - - no_sort_flag (bool): True if the parent container's .child_list - should not be sorted, because the calling function wants to do - that itself - - """ - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_bookmark_folder] - if not no_update_index_flag: - update_list.append(video_obj.parent_obj) - update_list.append(self.fixed_all_folder) - update_list.append(self.fixed_recent_folder) - if video_obj.fav_flag: - update_list.append(self.fixed_fav_folder) - if video_obj.live_mode: - update_list.append(self.fixed_live_folder) - if video_obj.missing_flag: - update_list.append(self.fixed_missing_folder) - if video_obj.new_flag: - update_list.append(self.fixed_new_folder) - if video_obj.waiting_flag: - update_list.append(self.fixed_waiting_folder) - - # Mark the video as bookmarked or not bookmarked - if not isinstance(video_obj, media.Video): - return self.system_error( - 157, - 'Mark video as bookmarked request failed sanity check', - ) - - elif not bookmark_flag: - - # Mark video as not bookmarked - if not video_obj.bookmark_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_bookmark_flag(False) - # Update the parent object - video_obj.parent_obj.dec_bookmark_count() - - # Remove this video from the private 'Bookmarks' folder (the - # folder's count IVs are automatically updated) - self.fixed_bookmark_folder.del_child(video_obj) - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'Bookmarks' folder is visible) - if not no_update_catalogue_flag: - - if self.main_win_obj.video_index_current_dbid is not None \ - and self.main_win_obj.video_index_current_dbid \ - == self.fixed_bookmark_folder.dbid: - self.main_win_obj.video_catalogue_delete_video( - video_obj, - ) - - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.dec_bookmark_count() - self.fixed_bookmark_folder.dec_bookmark_count() - if video_obj.fav_flag: - self.fixed_fav_folder.dec_bookmark_count() - if video_obj.live_mode: - self.fixed_live_folder.dec_bookmark_count() - if video_obj.missing_flag: - self.fixed_missing_folder.dec_bookmark_count() - if video_obj.new_flag: - self.fixed_new_folder.dec_bookmark_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.dec_bookmark_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_bookmark_count() - - else: - - # Mark video as bookmarked - if video_obj.bookmark_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_bookmark_flag(True) - # Update the parent object - video_obj.parent_obj.inc_bookmark_count() - - # Add this video to the private 'Bookmarks' folder - self.fixed_bookmark_folder.add_child( - self, - video_obj, - no_sort_flag, - ) - - self.fixed_bookmark_folder.inc_bookmark_count() - if video_obj.dl_flag: - self.fixed_bookmark_folder.inc_dl_count() - if video_obj.fav_flag: - self.fixed_bookmark_folder.inc_fav_count() - if video_obj.live_mode: - self.fixed_bookmark_folder.inc_live_count() - if video_obj.missing_flag: - self.fixed_bookmark_folder.inc_missing_count() - if video_obj.new_flag: - self.fixed_bookmark_folder.inc_new_count() - if video_obj.waiting_flag: - self.fixed_bookmark_folder.inc_waiting_count() - - # Update the Video Catalogue, if that folder is the visible one - if not no_update_catalogue_flag: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.inc_bookmark_count() - if video_obj.fav_flag: - self.fixed_fav_folder.inc_bookmark_count() - if video_obj.live_mode: - self.fixed_live_folder.inc_bookmark_count() - if video_obj.missing_flag: - self.fixed_missing_folder.inc_bookmark_count() - if video_obj.new_flag: - self.fixed_new_folder.inc_bookmark_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.inc_bookmark_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_bookmark_count() - - # Update rows in the Video Index - for container_obj in update_list: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - container_obj, - ) - - - def mark_video_downloaded(self, video_obj, dl_flag, not_new_flag=False): - - """Can be called by anything. - - Marks a video object as downloaded (i.e. the video file exists on the - user's filesystem) or not downloaded. - - The video object's .dl_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark. - - dl_flag (bool): True to mark the video as downloaded, False to mark - it as not downloaded. - - not_new_flag (bool): Set to True when called by - downloads.confirm_old_video(). The video is downloaded, but not - new - - """ - - # (List of Video Index rows to update, at the end of this function) - update_list = [video_obj.parent_obj, self.fixed_all_folder] - - # Mark the video as downloaded or not downloaded - if not isinstance(video_obj, media.Video): - return self.system_error( - 158, - 'Mark video as downloaded request failed sanity check', - ) - - elif not dl_flag: - - # Mark video as not downloaded - if not video_obj.dl_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_dl_flag(False) - # (A video that is not downloaded cannot be marked archived) - video_obj.set_archive_flag(False) - # Update the parent container object - video_obj.parent_obj.dec_dl_count() - # Update private folders - self.fixed_all_folder.dec_dl_count() - self.fixed_new_folder.dec_dl_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_dl_count() - update_list.append(self.fixed_bookmark_folder) - if video_obj.fav_flag: - self.fixed_fav_folder.dec_dl_count() - update_list.append(self.fixed_fav_folder) - if video_obj.live_mode: - self.fixed_live_folder.dec_dl_count() - update_list.append(self.fixed_live_folder) - if video_obj.missing_flag: - self.fixed_missing_folder.dec_dl_count() - update_list.append(self.fixed_missing_folder) - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.dec_dl_count() - update_list.append(self.fixed_recent_folder) - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_dl_count() - update_list.append(self.fixed_waiting_folder) - - # Also mark the video as not new (if required)... - if not not_new_flag: - self.mark_video_new(video_obj, False, True) - # ...and not missing (in all circumstances) - self.mark_video_missing(video_obj, False, True) - - else: - - # Mark video as downloaded - if video_obj.dl_flag: - - # Already marked - return - - else: - - # If any ancestor channels, playlists or folders are marked as - # favourite, the video must be marked favourite as well - if video_obj.ancestor_is_favourite(): - self.mark_video_favourite(video_obj, True, True) - - # Update the video object's IVs - video_obj.set_dl_flag(True) - # Update the parent container object - video_obj.parent_obj.inc_dl_count() - # Update private folders - self.fixed_all_folder.inc_dl_count() - self.fixed_new_folder.inc_dl_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_dl_count() - update_list.append(self.fixed_bookmark_folder) - if video_obj.fav_flag: - self.fixed_fav_folder.inc_dl_count() - update_list.append(self.fixed_fav_folder) - if video_obj.live_mode: - self.fixed_live_folder.inc_dl_count() - update_list.append(self.fixed_live_folder) - if video_obj.missing_flag: - self.fixed_missing_folder.inc_dl_count() - update_list.append(self.fixed_missing_folder) - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.inc_dl_count() - update_list.append(self.fixed_recent_folder) - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_dl_count() - update_list.append(self.fixed_waiting_folder) - - # Also mark the video as new - if not not_new_flag: - self.mark_video_new(video_obj, True, True) - - # If a download options manager (options.OptionsManager) has - # been applied to this video, remove it (if required) - if video_obj.options_obj \ - and len(video_obj.options_obj.dbid_list) > 1 \ - and self.auto_delete_options_flag: - self.remove_download_options(video_obj) - - # Update rows in the Video Index - for container_obj in update_list: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - container_obj, - ) - - - def mark_video_favourite(self, video_obj, fav_flag, \ - no_update_index_flag=False, no_update_catalogue_flag=False, - no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as favourite or not favourite. - - The video object's .fav_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - fav_flag (bool): True to mark the video as favourite, False to mark - it as not favourite - - no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'Favourite Videos' folder), - because the calling function wants to do that itself - - no_update_catalogue_flag (bool): True if rows in the Video - Catalogue should not be updated, because the calling function - wants to redraw the whole catalogue itself - - no_sort_flag (bool): True if the parent container's .child_list - should not be sorted, because the calling function wants to do - that itself - - """ - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_fav_folder] - if not no_update_index_flag: - update_list.append(video_obj.parent_obj) - update_list.append(self.fixed_all_folder) - update_list.append(self.fixed_recent_folder) - if video_obj.bookmark_flag: - update_list.append(self.fixed_bookmark_folder) - if video_obj.live_mode: - update_list.append(self.fixed_live_folder) - if video_obj.missing_flag: - update_list.append(self.fixed_missing_folder) - if video_obj.new_flag: - update_list.append(self.fixed_new_folder) - if video_obj.waiting_flag: - update_list.append(self.fixed_waiting_folder) - - # Mark the video as favourite or not favourite - if not isinstance(video_obj, media.Video): - return self.system_error( - 159, - 'Mark video as favourite request failed sanity check', - ) - - elif not fav_flag: - - # Mark video as not favourite - if not video_obj.fav_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_fav_flag(False) - # Update the parent object - video_obj.parent_obj.dec_fav_count() - - # Remove this video from the private 'Favourite Videos' folder - # (the folder's count IVs are automatically updated) - self.fixed_fav_folder.del_child(video_obj) - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'Favourite Videos' folder is - # visible) - if not no_update_catalogue_flag: - - if self.main_win_obj.video_index_current_dbid is not None \ - and self.main_win_obj.video_index_current_dbid \ - == self.fixed_fav_folder.dbid: - self.main_win_obj.video_catalogue_delete_video( - video_obj, - ) - - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.dec_fav_count() - self.fixed_fav_folder.dec_fav_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_fav_count() - if video_obj.live_mode: - self.fixed_live_folder.dec_fav_count() - if video_obj.missing_flag: - self.fixed_missing_folder.dec_fav_count() - if video_obj.new_flag: - self.fixed_new_folder.dec_fav_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.dec_fav_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_fav_count() - - else: - - # Mark video as favourite - if video_obj.fav_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_fav_flag(True) - # Update the parent object - video_obj.parent_obj.inc_fav_count() - - # Add this video to the private 'Favourite Videos' folder - self.fixed_fav_folder.add_child(self, video_obj, no_sort_flag) - self.fixed_fav_folder.inc_fav_count() - if video_obj.bookmark_flag: - self.fixed_fav_folder.inc_bookmark_count() - if video_obj.dl_flag: - self.fixed_fav_folder.inc_dl_count() - if video_obj.live_mode: - self.fixed_fav_folder.inc_live_count() - if video_obj.missing_flag: - self.fixed_fav_folder.inc_missing_count() - if video_obj.new_flag: - self.fixed_fav_folder.inc_new_count() - if video_obj.waiting_flag: - self.fixed_fav_folder.inc_waiting_count() - - # Update the Video Catalogue, if that folder is the visible one - if not no_update_catalogue_flag: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.inc_fav_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_fav_count() - if video_obj.live_mode: - self.fixed_live_folder.inc_fav_count() - if video_obj.missing_flag: - self.fixed_missing_folder.inc_fav_count() - if video_obj.new_flag: - self.fixed_new_folder.inc_fav_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.inc_fav_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_fav_count() - - # Update rows in the Video Index - for container_obj in update_list: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - container_obj, - ) - - - def mark_video_live(self, video_obj, live_mode, live_data_dict={}, \ - no_update_index_flag=False, no_update_catalogue_flag=False, \ - no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as a livestream. - - The video object's .live_mode IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - live_mode (int): 0 if the video is not a livestream (or if it was a - livestream which has now finished, and behaves like a normal - uploaded video), 1 if the livestream has not started, 2 if the - livestream is currently being broadcast - - live_data_dict (dict): Dictionary of additional data obtained from - the YouTube STDERR message (empty for a livestream already - broadcasting, or if the message couldn't be interpreted). - Dictionary in the form - - live_msg (str): Text that can be displayed in the Video - Catalogue - live_time (int): Approximate time (matching time.time()) at - which the livestream is due to start - live_debut_flag (bool): True for a YouTube 'premiere' video, - False for an ordinary livestream - - no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'Livestreams' folder), because - the calling function wants to do that itself - - no_update_catalogue_flag (bool): True if rows in the Video - Catalogue should not be updated, because the calling function - wants to redraw the whole catalogue itself - - no_sort_flag (bool): True if the parent container's .child_list - should not be sorted, because the calling function wants to do - that itself - - """ - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_live_folder] - if not no_update_index_flag: - update_list.append(video_obj.parent_obj) - update_list.append(self.fixed_all_folder) - update_list.append(self.fixed_recent_folder) - if video_obj.bookmark_flag: - update_list.append(self.fixed_bookmark_folder) - if video_obj.fav_flag: - update_list.append(self.fixed_fav_folder) - if video_obj.missing_flag: - update_list.append(self.fixed_missing_folder) - if video_obj.new_flag: - update_list.append(self.fixed_new_folder) - if video_obj.waiting_flag: - update_list.append(self.fixed_waiting_folder) - - # Mark the video as a livestream or not a livestream - if not isinstance(video_obj, media.Video): - return self.system_error( - 160, - 'Mark video as livestream request failed sanity check', - ) - - # Update IVs required by self.livestream_manager_finished() - if (video_obj.live_mode == 0 or video_obj.live_mode == 1) \ - and live_mode == 2: - self.media_reg_live_started_dict[video_obj.dbid] = video_obj - elif video_obj.live_mode == 2 and live_mode == 0: - self.media_reg_live_stopped_dict[video_obj.dbid] = video_obj - - # Update other IVs - if live_mode == 0: - - # Mark video as not a livestream - if video_obj.live_mode == 0: - - # Already marked - return - - else: - - # Update the main registries - if video_obj.dbid in self.media_reg_live_dict: - del self.media_reg_live_dict[video_obj.dbid] - if video_obj.dbid in self.media_reg_auto_alarm_dict: - del self.media_reg_auto_alarm_dict[video_obj.dbid] - if video_obj.dbid in self.media_reg_auto_open_dict: - del self.media_reg_auto_open_dict[video_obj.dbid] - if video_obj.dbid in self.media_reg_auto_dl_start_dict: - del self.media_reg_auto_dl_start_dict[video_obj.dbid] - if video_obj.dbid in self.media_reg_auto_dl_stop_dict: - del self.media_reg_auto_dl_stop_dict[video_obj.dbid] - - # Update the video object's IVs - video_obj.set_live_mode(live_mode) - video_obj.set_was_live_flag(True) - video_obj.set_live_data(live_data_dict) - # Update the parent object - video_obj.parent_obj.dec_live_count() - - # Remove this video from the private 'Livestreams' folder - # (the folder's count IVs are automatically updated) - self.fixed_live_folder.del_child(video_obj) - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'Livestreams' folder is visible) - if not no_update_catalogue_flag: - - if self.main_win_obj.video_index_current_dbid is not None \ - and self.main_win_obj.video_index_current_dbid \ - == self.fixed_live_folder.dbid: - self.main_win_obj.video_catalogue_delete_video( - video_obj, - ) - - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.dec_live_count() - self.fixed_live_folder.dec_live_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_live_count() - if video_obj.fav_flag: - self.fixed_fav_folder.dec_live_count() - if video_obj.missing_flag: - self.fixed_missing_folder.dec_live_count() - if video_obj.new_flag: - self.fixed_new_folder.dec_live_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.dec_waiting_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_waiting_count() - - else: - - # Mark video as a livestream - if video_obj.live_mode == live_mode: - - # Already marked as either a 'waiting' or a 'live' livestream - return - - elif video_obj.was_live_flag: - - # A livestream video which has been marked as a normal video - # can never be marked as a livestream video again - # (This prevents any problems in reading the RSS feeds from - # continually marking an old video as a livestream again) - return - - else: - - if video_obj.live_mode == 0: - # Video was not a livestream, but now is - convert_flag = False - else: - # Video was a 'waiting' livestream, and is now 'live' (or - # vice-versa) - convert_flag = True - - # Update the main registry - self.media_reg_live_dict[video_obj.dbid] = video_obj - if self.livestream_auto_notify_flag: - self.media_reg_auto_notify_dict[video_obj.dbid] = video_obj - if HAVE_PLAYSOUND_FLAG \ - and self.livestream_auto_alarm_flag: - self.media_reg_auto_alarm_dict[video_obj.dbid] = video_obj - if self.livestream_auto_open_flag: - self.media_reg_auto_open_dict[video_obj.dbid] = video_obj - if self.livestream_auto_dl_start_flag: - self.media_reg_auto_dl_start_dict[video_obj.dbid] \ - = video_obj - if self.livestream_auto_dl_stop_flag: - self.media_reg_auto_dl_stop_dict[video_obj.dbid] \ - = video_obj - - # Update the video object's IVs - video_obj.set_live_mode(live_mode) - video_obj.set_live_data(live_data_dict) - - # Update the parent object - if not convert_flag: - video_obj.parent_obj.inc_waiting_count() - - # Add this video to the private 'Livestreams' folder - if not convert_flag: - self.fixed_live_folder.add_child( - self, - video_obj, - no_sort_flag, - ) - - self.fixed_live_folder.inc_live_count() - if video_obj.bookmark_flag: - self.fixed_live_folder.inc_bookmark_count() - if video_obj.dl_flag: - self.fixed_live_folder.inc_dl_count() - if video_obj.fav_flag: - self.fixed_live_folder.inc_fav_count() - if video_obj.missing_flag: - self.fixed_live_folder.inc_missing_count() - if video_obj.new_flag: - self.fixed_live_folder.inc_new_count() - if video_obj.waiting_flag: - self.fixed_live_folder.inc_waiting_count() - - # Update the Video Catalogue, if that folder is the visible one - if not no_update_catalogue_flag: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - if not convert_flag: - self.fixed_all_folder.inc_live_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_live_count() - if video_obj.fav_flag: - self.fixed_fav_folder.inc_live_count() - if video_obj.missing_flag: - self.fixed_missing_folder.inc_live_count() - if video_obj.new_flag: - self.fixed_new_folder.inc_live_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.inc_live_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_live_count() - - # Update rows in the Video Index - for container_obj in update_list: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - container_obj, - ) - - # Changing a video's live mode almost always changes its position in - # the parent container's child list, so perform a resort - video_obj.parent_obj.sort_children(self) - - - def mark_video_missing(self, video_obj, missing_flag, \ - no_update_index_flag=False, no_update_catalogue_flag=False, \ - no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as missing or not missing. (A video is missing if - it has been downloaded from a channel/playlist by the user, but has - since been removed from that channel/playlist by its creator). - - The video object's .missing_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - missing_flag (bool): True to mark the video as missing, False to - mark it as not missing - - no_update_index_flag (bool): True if the Video Index should not be - updated, because the calling function wants to do that itself - - no_update_catalogue_flag (bool): True if rows in the Video - Catalogue should not be updated, because the calling function - wants to redraw the whole catalogue itself - - no_sort_flag (bool): True if the parent container's .child_list - should not be sorted, because the calling function wants to do - that itself - - """ - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_missing_folder] - if not no_update_index_flag: - update_list.append(video_obj.parent_obj) - update_list.append(self.fixed_all_folder) - update_list.append(self.fixed_recent_folder) - if video_obj.bookmark_flag: - update_list.append(self.fixed_bookmark_folder) - if video_obj.fav_flag: - update_list.append(self.fixed_fav_folder) - if video_obj.live_mode: - update_list.append(self.fixed_live_folder) - if video_obj.new_flag: - update_list.append(self.fixed_new_folder) - if video_obj.waiting_flag: - update_list.append(self.fixed_waiting_folder) - - # Mark the video as missing or not missing - if not isinstance(video_obj, media.Video): - return self.system_error( - 161, - 'Mark video as missing request failed sanity check', - ) - - elif not missing_flag: - - # Mark video as not missing - if not video_obj.missing_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_missing_flag(False) - # Update the parent object - video_obj.parent_obj.dec_missing_count() - - # Remove this video from the private 'Missing Videos' folder - # (the folder's count IVs are automatically updated) - self.fixed_missing_folder.del_child(video_obj) - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'Missing Videos' folder is - # visible) - if not no_update_catalogue_flag: - - if self.main_win_obj.video_index_current_dbid is not None \ - and self.main_win_obj.video_index_current_dbid \ - == self.fixed_missing_folder.dbid: - self.main_win_obj.video_catalogue_delete_video( - video_obj, - ) - - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.dec_missing_count() - self.fixed_missing_folder.dec_missing_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_missing_count() - if video_obj.fav_flag: - self.fixed_fav_folder.dec_missing_count() - if video_obj.live_mode: - self.fixed_live_folder.dec_missing_count() - if video_obj.missing_flag: - self.fixed_missing_folder.dec_missing_count() - if video_obj.new_flag: - self.fixed_new_folder.dec_missing_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.dec_missing_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_missing_count() - - else: - - # Mark video as missing (but not if the video is not marked as - # downloaded) - if video_obj.missing_flag or not video_obj.dl_flag: - - # Already marked, or not elligible - return - - else: - - # Update the video object's IVs - video_obj.set_missing_flag(True) - # Update the parent object - video_obj.parent_obj.inc_missing_count() - - # Add this video to the private 'Missing Videos' folder - self.fixed_missing_folder.add_child( - self, - video_obj, - no_sort_flag, - ) - - self.fixed_missing_folder.inc_missing_count() - if video_obj.bookmark_flag: - self.fixed_missing_folder.inc_bookmark_count() - if video_obj.dl_flag: - self.fixed_missing_folder.inc_dl_count() - if video_obj.fav_flag: - self.fixed_missing_folder.inc_fav_count() - if video_obj.live_mode: - self.fixed_missing_folder.inc_live_count() - if video_obj.missing_flag: - self.fixed_missing_folder.inc_missing_count() - if video_obj.new_flag: - self.fixed_missing_folder.inc_new_count() - if video_obj.waiting_flag: - self.fixed_missing_folder.inc_waiting_count() - - # Update the Video Catalogue, if that folder is the visible one - if not no_update_catalogue_flag: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.inc_missing_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_missing_count() - if video_obj.fav_flag: - self.fixed_fav_folder.inc_missing_count() - if video_obj.live_mode: - self.fixed_live_folder.inc_missing_count() - if video_obj.missing_flag: - self.fixed_missing_folder.inc_missing_count() - if video_obj.new_flag: - self.fixed_new_folder.inc_missing_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.inc_missing_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_missing_count() - - # Update rows in the Video Index - for container_obj in update_list: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - container_obj, - ) - - - def mark_video_new(self, video_obj, new_flag, no_update_index_flag=False, - no_update_catalogue_flag=False, no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as new (i.e. unwatched by the user), or as not - new (already watched by the user). - - The video object's .new_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - new_flag (bool): True to mark the video as new, False to mark it as - not new - - no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'New Videos' folder), because - the calling function wants to do that itself - - no_update_catalogue_flag (bool): True if rows in the Video - Catalogue should not be updated, because the calling function - wants to redraw the whole catalogue itself - - no_sort_flag (bool): True if the parent container's .child_list - should not be sorted, because the calling function wants to do - that itself - - """ - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_new_folder] - if not no_update_index_flag: - update_list.append(video_obj.parent_obj) - update_list.append(self.fixed_all_folder) - update_list.append(self.fixed_recent_folder) - if video_obj.bookmark_flag: - update_list.append(self.fixed_bookmark_folder) - if video_obj.fav_flag: - update_list.append(self.fixed_fav_folder) - if video_obj.live_mode: - update_list.append(self.fixed_live_folder) - if video_obj.missing_flag: - update_list.append(self.fixed_missing_folder) - if video_obj.waiting_flag: - update_list.append(self.fixed_waiting_folder) - - # Mark the video as new or not new - if not isinstance(video_obj, media.Video): - return self.system_error( - 162, - 'Mark video as new request failed sanity check', - ) - - elif not new_flag: - - # Mark video as not new - if not video_obj.new_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_new_flag(False) - # Update the parent object - video_obj.parent_obj.dec_new_count() - - # Remove this video from the private 'New Videos' folder - # (the folder's count IVs are automatically updated) - self.fixed_new_folder.del_child(video_obj) - self.fixed_new_folder.dec_new_count() - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'New Videos' folder is visible) - if not no_update_catalogue_flag: - - if self.main_win_obj.video_index_current_dbid is not None \ - and self.main_win_obj.video_index_current_dbid \ - == self.fixed_new_folder.dbid: - self.main_win_obj.video_catalogue_delete_video( - video_obj, - ) - - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.dec_new_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_new_count() - if video_obj.fav_flag: - self.fixed_fav_folder.dec_new_count() - if video_obj.live_mode: - self.fixed_live_folder.dec_new_count() - if video_obj.missing_flag: - self.fixed_missing_folder.dec_new_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.dec_new_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_new_count() - - else: - - # Mark video as new - if video_obj.new_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_new_flag(True) - # Update the parent object - video_obj.parent_obj.inc_new_count() - - # Add this video to the private 'New Videos' folder - self.fixed_new_folder.add_child(self, video_obj, no_sort_flag) - self.fixed_new_folder.inc_new_count() - if video_obj.bookmark_flag: - self.fixed_new_folder.inc_bookmark_count() - if video_obj.fav_flag: - self.fixed_new_folder.inc_fav_count() - if video_obj.live_mode: - self.fixed_new_folder.inc_live_count() - if video_obj.missing_flag: - self.fixed_new_folder.inc_missing_count() - if video_obj.waiting_flag: - self.fixed_new_folder.inc_waiting_count() - # Update the Video Catalogue, if that folder is the visible one - if not no_update_catalogue_flag: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.inc_new_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_new_count() - if video_obj.fav_flag: - self.fixed_fav_folder.inc_new_count() - if video_obj.live_mode: - self.fixed_live_folder.inc_new_count() - if video_obj.missing_flag: - self.fixed_missing_folder.inc_new_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.inc_new_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_new_count() - - # Update rows in the Video Index - for container_obj in update_list: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - container_obj, - ) - - - def mark_video_waiting(self, video_obj, waiting_flag, \ - no_update_index_flag=False, no_update_catalogue_flag=False, \ - no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as in the waiting list or not in the waiting list. - - The video object's .waiting_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - waiting_flag (bool): True to mark the video as in the waiting list, - False to mark it as not in the waiting list - - no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'Waiting Videos' folder), - because the calling function wants to do that itself - - no_update_catalogue_flag (bool): True if rows in the Video - Catalogue should not be updated, because the calling function - wants to redraw the whole catalogue itself - - no_sort_flag (bool): True if the parent container's .child_list - should not be sorted, because the calling function wants to do - that itself - - """ - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_waiting_folder] - if not no_update_index_flag: - update_list.append(video_obj.parent_obj) - update_list.append(self.fixed_all_folder) - update_list.append(self.fixed_recent_folder) - if video_obj.bookmark_flag: - update_list.append(self.fixed_bookmark_folder) - if video_obj.fav_flag: - update_list.append(self.fixed_fav_folder) - if video_obj.live_mode: - update_list.append(self.fixed_live_folder) - if video_obj.missing_flag: - update_list.append(self.fixed_missing_folder) - if video_obj.new_flag: - update_list.append(self.fixed_new_folder) - - # Mark the video as in the waiting list or not in the waiting list - if not isinstance(video_obj, media.Video): - return self.system_error( - 163, - 'Mark video as in waiting list request failed sanity check', - ) - - elif not waiting_flag: - - # Mark video as not in the waiting list - if not video_obj.waiting_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_waiting_flag(False) - # Update the parent object - video_obj.parent_obj.dec_waiting_count() - - # Remove this video from the private 'Waiting Videos' folder - # (the folder's count IVs are automatically updated) - self.fixed_waiting_folder.del_child(video_obj) - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'Waiting Videos' folder is - # visible) - if not no_update_catalogue_flag: - - if self.main_win_obj.video_index_current_dbid is not None \ - and self.main_win_obj.video_index_current_dbid \ - == self.fixed_waiting_folder.dbid: - self.main_win_obj.video_catalogue_delete_video( - video_obj, - ) - - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.dec_waiting_count() - self.fixed_waiting_folder.dec_waiting_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_waiting_count() - if video_obj.fav_flag: - self.fixed_fav_folder.dec_waiting_count() - if video_obj.live_mode: - self.fixed_live_folder.dec_waiting_count() - if video_obj.missing_flag: - self.fixed_missing_folder.dec_waiting_count() - if video_obj.new_flag: - self.fixed_new_folder.dec_waiting_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.dec_waiting_count() - - else: - - # Mark video as in the waiting list - if video_obj.waiting_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_waiting_flag(True) - # Update the parent object - video_obj.parent_obj.inc_waiting_count() - - # Add this video to the private 'Waiting Videos' folder - self.fixed_waiting_folder.add_child( - self, - video_obj, - no_sort_flag, - ) - - self.fixed_waiting_folder.inc_waiting_count() - if video_obj.bookmark_flag: - self.fixed_waiting_folder.inc_bookmark_count() - if video_obj.dl_flag: - self.fixed_waiting_folder.inc_dl_count() - if video_obj.fav_flag: - self.fixed_waiting_folder.inc_fav_count() - if video_obj.live_mode: - self.fixed_waiting_folder.inc_live_count() - if video_obj.missing_flag: - self.fixed_waiting_folder.inc_missing_count() - if video_obj.new_flag: - self.fixed_waiting_folder.inc_new_count() - - # Update the Video Catalogue, if that folder is the visible one - if not no_update_catalogue_flag: - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Update other private folders - self.fixed_all_folder.inc_waiting_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_waiting_count() - if video_obj.fav_flag: - self.fixed_fav_folder.inc_waiting_count() - if video_obj.live_mode: - self.fixed_live_folder.inc_waiting_count() - if video_obj.missing_flag: - self.fixed_missing_folder.inc_waiting_count() - if video_obj.new_flag: - self.fixed_new_folder.inc_waiting_count() - if video_obj in self.fixed_recent_folder.child_list: - self.fixed_recent_folder.inc_waiting_count() - - # Update rows in the Video Index - for container_obj in update_list: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - container_obj, - ) - - - def mark_folder_hidden(self, folder_obj, hidden_flag): - - """Called by callbacks in self.on_menu_show_hidden(), - .on_menu_hide_system() and - mainwin.MainWin.on_video_index_hide_folder(). - - Marks a folder as hidden (not visible in the Video Index) or not - hidden (visible in the Video Index, although the user might be - required to expand the tree to see it). - - Args: - - folder_obj (media.Folder): The folder object to mark - - hidden_flag (bool): True to mark the folder as hidden, False to - mark it as not hidden - - """ - - if not isinstance(folder_obj, media.Folder): - return self.system_error( - 164, - 'Mark folder as hidden request failed sanity check', - ) - - if not hidden_flag: - - # Mark folder as not hidden - if not folder_obj.hidden_flag: - - # Already marked - return - - else: - - # Update the folder object's IVs - folder_obj.set_hidden_flag(False) - # Update the Video Index - self.main_win_obj.video_index_add_row(folder_obj) - - else: - - # Mark video as hidden - if folder_obj.hidden_flag: - - # Already marked - return - - else: - - # Update the folder object's IVs - folder_obj.set_hidden_flag(True) - # Update the Video Index - self.main_win_obj.video_index_delete_row(folder_obj) - - - def mark_container_archived(self, media_data_obj, archive_flag, - only_child_videos_flag): - - """Called by mainwin.MainWin.on_video_index_mark_archived() and - .on_video_index_mark_not_archived(). - - Marks any descendant videos as archived. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The container object to update - - archive_flag (bool): True to mark as archived, False to mark as not - archived - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if the container object and all - its descendants should be marked - - """ - - if isinstance(media_data_obj, media.Video): - return self.system_error( - 165, - 'Mark container as archived request failed sanity check', - ) - - # Special arrangements for private folders - if media_data_obj == self.fixed_all_folder: - - # Check every video - for other_obj in self.media_reg_dict.values(): - - if isinstance(other_obj, media.Video) and other_obj.dl_flag: - other_obj.set_archive_flag(archive_flag) - - elif not archive_flag and media_data_obj == self.fixed_bookmark_folder: - - # Check videos in this folder - for other_obj in self.fixed_bookmark_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.bookmark_flag: - other_obj.set_archive_flag(archive_flag) - - elif not archive_flag and media_data_obj == self.fixed_fav_folder: - - # Check videos in this folder - for other_obj in self.fixed_fav_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.fav_flag: - other_obj.set_archive_flag(archive_flag) - - elif media_data_obj == self.fixed_live_folder: - - # Check videos in this folder - for other_obj in self.fixed_live_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.live_mode: - other_obj.set_archive_flag(archive_flag) - - elif media_data_obj == self.fixed_missing_folder: - - # Check videos in this folder - for other_obj in self.fixed_missing_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.missing_flag: - other_obj.set_archive_flag(archive_flag) - - elif media_data_obj == self.fixed_new_folder: - - # Check videos in this folder - for other_obj in self.fixed_new_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.new_flag: - other_obj.set_archive_flag(archive_flag) - - elif media_data_obj == self.fixed_recent_folder: - - # Check videos in this folder - for other_obj in self.fixed_recent_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag: - other_obj.set_archive_flag(archive_flag) - - elif media_data_obj == self.fixed_waiting_folder: - - # Check videos in this folder - for other_obj in self.fixed_waiting_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.waiting_flag: - other_obj.set_archive_flag(archive_flag) - - elif only_child_videos_flag: - - # Check videos in this channel/playlist/folder - for other_obj in media_data_obj.child_list: - - if isinstance(other_obj, media.Video): - other_obj.set_archive_flag(archive_flag) - - else: - - # Check videos in this channel/playlist/folder, and in any - # descendant channels/playlists/folders - for other_obj in media_data_obj.compile_all_videos( [] ): - - if isinstance(other_obj, media.Video) and other_obj.dl_flag: - other_obj.set_archive_flag(archive_flag) - - # In all cases, update the row on the Video Index - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_icon, - media_data_obj, - ) - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - media_data_obj, - ) - # If this container is the one visible in the Video Catalogue, redraw - # the Video Catalogue - if self.main_win_obj.video_index_current_dbid == media_data_obj.dbid: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - ) - - - def mark_container_favourite(self, media_data_obj, fav_flag, - only_child_videos_flag): - - """Called by mainwin.MainWin.on_video_index_mark_favourite() and - .on_video_index_mark_not_favourite(). - - Marks this channel, playlist or folder as favourite (or not favourite). - Also marks any descendant videos as (not) favourite (but not descendent - channels, playlists or folders). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The container object to update - - fav_flag (bool): True to mark as favourite, False to mark as not - favourite - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if the container object and all - its descendants should be marked - - """ - - if isinstance(media_data_obj, media.Video): - return self.system_error( - 166, - 'Mark container as favourite request failed sanity check', - ) - - # Special arrangements for private folders. Mark the videos as - # favourite, but don't modify their parent channels, playlists and - # folders - # (For the private 'Favourite Videos' folder, don't need to do anything - # if 'fav_flag' is True, because the popup menu item is desensitised) - video_list = [] - - if media_data_obj == self.fixed_all_folder: - - # Check every video - for other_obj in self.media_reg_dict.values(): - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - elif media_data_obj == self.fixed_bookmark_folder: - - # Check videos in this folder - for other_obj in self.fixed_bookmark_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.bookmark_flag: - video_list.append(other_obj) - - elif not fav_flag and media_data_obj == self.fixed_fav_folder: - - # Check videos in this folder - for other_obj in self.fixed_fav_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.fav_flag: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_live_folder: - - # Check videos in this folder - for other_obj in self.fixed_live_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.live_mode: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_missing_folder: - - # Check videos in this folder - for other_obj in self.fixed_missing_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.missing_flag: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_new_folder: - - # Check videos in this folder - for other_obj in self.fixed_new_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.new_flag: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_recent_folder: - - # Check videos in this folder - for other_obj in self.fixed_recent_folder.child_list: - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - elif media_data_obj == self.fixed_waiting_folder: - - # Check videos in this folder - for other_obj in self.fixed_waiting_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.waiting_flag: - video_list.append(other_obj) - - elif only_child_videos_flag: - - # Check only videos that are children of the specified media data - # object - for other_obj in media_data_obj.child_list: - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - else: - - # Check only video objects that are descendants of the specified - # media data object - for other_obj in media_data_obj.compile_all_videos( [] ): - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - else: - # For channels, playlists and folders, we can set the IV - # directly - other_obj.set_fav_flag(fav_flag) - - # The channel, playlist or folder itself is also marked as - # favourite (obviously, we don't do that for private folders) - media_data_obj.set_fav_flag(fav_flag) - - # Take action, depending on how many videos there are - count = len(video_list) - - if not count: - - # Just update the row on the Video Index - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_icon, - media_data_obj, - ) - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - media_data_obj, - ) - - elif count < self.main_win_obj.mark_video_lower_limit: - - # The procedure should be quick - for child_obj in video_list: - self.mark_video_favourite(child_obj, fav_flag) - - elif count < self.main_win_obj.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.prepare_mark_video( - ['favourite', fav_flag, media_data_obj, video_list], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - media_type = media_data_obj.get_type() - if media_type == 'channel': - msg = _( - 'The channel contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - elif media_type == 'playlist': - msg = _( - 'The playlist contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - else: - msg = _( - 'The folder contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - msg += '\n\n' + _('Are you sure you want to continue?') - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': \ - ['favourite', fav_flag, media_data_obj, video_list], - }, - ) - - - def mark_container_missing(self, media_data_obj, missing_flag): - - """Called by mainwin.MainWin.on_video_index_mark_missing() and - .on_video_index_mark_not_missing(). - - Marks this channel or playlist as missing (or not missing). Note that - this function can't be called for folders (except for the fixed - 'Missing Videos' folder). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The container object to update - - missing_flag (bool): True to mark as missing, False to mark as not - missing - - """ - - if isinstance(media_data_obj, media.Video) \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj != self.fixed_missing_folder - ): - return self.system_error( - 167, - 'Mark container as missing request failed sanity check', - ) - - # Special arrangements for the 'Missing Videos' folder. Mark the - # videos as missing, but don't modify their parent channels, - # playlists and folders - video_list = [] - - if media_data_obj == self.fixed_missing_folder: - - # Check videos in this folder - for other_obj in self.fixed_missing_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.missing_flag: - video_list.append(other_obj) - - else: - - # Check only videos that are children of the specified media data - # object - for other_obj in media_data_obj.child_list: - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - # Take action, depending on how many videos there are - count = len(video_list) - - if not count: - - # Just update the row on the Video Index - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_icon, - media_data_obj, - ) - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - media_data_obj, - ) - - elif count < self.main_win_obj.mark_video_lower_limit: - - # The procedure should be quick - for child_obj in video_list: - self.mark_video_missing(child_obj, missing_flag) - - elif count < self.main_win_obj.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.prepare_mark_video( - ['missing', missing_flag, media_data_obj, video_list], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - media_type = media_data_obj.get_type() - if media_type == 'channel': - msg = _( - 'The channel contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - elif media_type == 'playlist': - msg = _( - 'The playlist contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - else: - msg = _( - 'The folder contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - msg += '\n\n' + _('Are you sure you want to continue?') - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': \ - ['missing', missing_flag, media_data_obj, video_list], - }, - ) - - - def mark_container_new(self, media_data_obj, new_flag, - only_child_videos_flag): - - """Called by mainwin.MainWin.on_video_index_mark_new() and - .on_video_index_mark_not_new(). - - Marks videos in this channel, playlist or folder as new (or not new). - Also marks any descendant videos as (not) new (but not descendent - channels, playlists or folders). - - Unlike self.mark_container_favourite, the container itself is not - marked as new. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The container object to update - - new_flag (bool): True to mark as new, False to mark as not - new - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if the container object and all - its descendants should be marked - - """ - - if isinstance(media_data_obj, media.Video): - return self.system_error( - 168, - 'Mark container as new request failed sanity check', - ) - - # Special arrangements for private folders - # (For the private 'Favourite Videos' folder, don't need to do anything - # if 'new_flag' is True, because the popup menu item is desensitised) - video_list = [] - - if media_data_obj == self.fixed_all_folder: - - # Check every video - for other_obj in self.media_reg_dict.values(): - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - elif media_data_obj == self.fixed_bookmark_folder: - - # Check videos in this folder - for other_obj in self.fixed_bookmark_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.bookmark_flag: - video_list.append(other_obj) - - elif not new_flag and media_data_obj == self.fixed_fav_folder: - - # Check videos in this folder - for other_obj in self.fixed_fav_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.fav_flag: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_live_folder: - - # Check videos in this folder - for other_obj in self.fixed_live_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.live_mode: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_missing_folder: - - # Check videos in this folder - for other_obj in self.fixed_missing_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.missing_flag: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_recent_folder: - - # Check videos in this folder - for other_obj in self.fixed_recent_folder.child_list: - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - elif media_data_obj == self.fixed_waiting_folder: - - # Check videos in this folder - for other_obj in self.fixed_waiting_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.waiting_flag: - video_list.append(other_obj) - - elif only_child_videos_flag: - - # Check only videos that are children of the specified media data - # object - for other_obj in media_data_obj.child_list: - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - else: - - # Check only video objects that are descendants of the specified - # media data object - for other_obj in media_data_obj.compile_all_videos( [] ): - - # (Only downloaded videos can be marked as new) - if not new_flag or other_obj.dl_flag: - video_list.append(other_obj) - - # Take action, depending on how many videos there are - count = len(video_list) - - if not count: - - # Just update the row on the Video Index - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_icon, - media_data_obj, - ) - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_text, - media_data_obj, - ) - - elif count < self.main_win_obj.mark_video_lower_limit: - - # The procedure should be quick - for child_obj in video_list: - self.mark_video_new(child_obj, new_flag) - - elif count < self.main_win_obj.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.prepare_mark_video( - ['new', new_flag, media_data_obj, video_list], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - media_type = media_data_obj.get_type() - if media_type == 'channel': - msg = _( - 'The channel contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - elif media_type == 'playlist': - msg = _( - 'The playlist contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - else: - msg = _( - 'The folder contains {0} item(s), so this action may' \ - + ' take a while', - ).format(str(count)) - - msg += '\n\n' + _('Are you sure you want to continue?') - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': ['new', new_flag, media_data_obj, video_list], - }, - ) - - - def rename_container(self, media_data_obj): - - """Called by mainwin.MainWin.on_video_index_rename_location(). - - Renames a channel, playlist or folder. Also renames the corresponding - directory in Tartube's data directory. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - media data object to be renamed - - """ - - # Do some basic checks - if media_data_obj is None \ - or isinstance(media_data_obj, media.Video) \ - or self.current_manager_obj \ - or self.main_win_obj.config_win_list \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag - ): - return self.system_error( - 169, - 'Rename container request failed sanity check', - ) - - # Prompt the user for a new name - dialogue_win = mainwin.RenameContainerDialogue( - self.main_win_obj, - media_data_obj, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - new_name = dialogue_win.entry.get_text() - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and new_name != '' \ - and new_name != media_data_obj.name: - - # Check that the name is legal - if new_name is None or re.search(r'^\s*$', new_name): - - self.dialogue_manager_obj.show_msg_dialogue( - _('You did not give the folder a new name'), - 'error', - 'ok', - ) - - return - - - elif not self.check_container_name_is_legal(new_name): - - self.dialogue_manager_obj.show_msg_dialogue( - _('The name \'{0}\' is not allowed').format(new_name), - 'error', - 'ok', - ) - - return - - # Remove leading/trailing whitespace from the name; make sure the - # name is not excessively long; reject illegal names - new_name = utils.tidy_up_container_name( - self, - new_name, - self.container_name_max_len, - ) - if new_name == '': - - self.dialogue_manager_obj.show_msg_dialogue( - _('That name is not permitted on your system'), - 'error', - 'ok', - ) - - return - - # If there is no parent folder, there must be no containers in the - # top-level list with the same name - # If there is a parent folder, it must not contain a container with - # the same name - duplicate_obj = self.find_duplicate_name_in_container( - media_data_obj.parent_obj, - new_name, - ) - if duplicate_obj: - self.reject_container_name( - new_name, - media_data_obj.parent_obj, - duplicate_obj, - ) - - return - - # The new name is acceptable. Attempt to rename the sub-directory - # itself - old_dir = media_data_obj.get_default_dir(self) - new_dir = media_data_obj.get_default_dir(self, new_name) - if not self.move_file_or_directory(old_dir, new_dir): - - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to rename \'{0}\'').format(media_data_obj.name), - 'error', - 'ok', - ) - - return - - # Filesystem updated, so now update the media data object itself. - # This call also updates the object's .nickname IV - old_name = media_data_obj.name - media_data_obj.set_name(new_name) - - # Reset the Video Index and the Video Catalogue (this prevents a - # lot of problems) - self.main_win_obj.video_index_catalogue_reset() - - # Save the database file (since the filesystem itself has changed) - self.save_db() - - - def rename_container_silently(self, media_data_obj, new_name): - - """Called by self.load_db() and .rename_fixed_folder(). - - A modified form of self.rename_container(). No dialogue windows are - used, no widgets are updated or desensitised, and the Tartube database - file is not saved. - - This function does the usual checks that the specified name is legal, - but it does not check for duplicate container names (in the same - parent folder). It's up to the calling code to check this function's - return value and respond appropriately. - - Renames a channel, playlist or folder. Also renames the corresponding - directory in Tartube's data directory. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - media data object to be renamed - - new_name (str): The object's new name - - Return values: - - True on success, False on failure - - """ - - # Nothing in the Tartube code should be capable of calling this - # function with an illegal name, but we'll still check - if new_name is None \ - or re.search(r'^\s*$', new_name) \ - or not self.check_container_name_is_legal(new_name): - - self.system_error( - 170, - 'Illegal container name', - ) - - return False - - new_name = utils.tidy_up_container_name( - self, - new_name, - self.container_name_max_len, - ) - if new_name == '': - - self.system_error( - 170, - 'Illegal container name', - ) - - return False - - # Attempt to rename the sub-directory itself - # (Private folders don't have a sub-directory to rename, so check for - # that) - if not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.priv_flag: - old_dir = media_data_obj.get_default_dir(self) - new_dir = media_data_obj.get_default_dir(self, new_name) - if not self.move_file_or_directory(old_dir, new_dir): - return False - - # Filesystem updated, so now update the media data object itself. This - # call also updates the object's .nickname IV - old_name = media_data_obj.name - media_data_obj.set_name(new_name) - - return True - - - def check_container_name_is_legal(self, name): - - """Can be called by anything. - - Checks that the name of a channel, playlist or folder is legal, i.e. - that it doesn't match one of the regexes in - self.illegal_name_regex_list. - - Does not check whether an existing container is already using the name; - that's the responsibility of the calling code. - - Note that code in utils.tidy_up_container_name() should also be called - to check for illegal MS Windows filenames, to strip whitespace, and so - on. - - Args: - - name (str): A proposed name for a media.Channel, media.Playlist or - media.Folder object - - Return values: - - True if the name is legal, False if it is illegal - - """ - - for regex in self.illegal_name_regex_list: - if re.search(regex, name, re.IGNORECASE): - # Illegal name - return False - - # Legal name - return True - - - def update_container_url(self, data_list): - - """Called by config.SystemPrefWin.on_container_url_edited(). - - When the user has confirmed a change to a channel/playlist's source - URL, implement that change, and update the window's treeview. - - Args: - - data_list (list): A list containing four items: the treeview model, - an iter pointing to a cell in the model, the media data object - and the updated URL - - """ - - # Extract values from the argument list - model = data_list.pop(0) - tree_iter = data_list.pop(0) - media_data_obj = data_list.pop(0) - url = data_list.pop(0) - - # Update the media data object - media_data_obj.set_source(url) - model[tree_iter][3] = url - - - def update_container_url_multiple(self, data_list): - - """Called by config.SystemPrefWin.on_container_url_edited(). - - Modified version of self.update_container_url, used when performing a - substitution on the source URL of multiple channels/playlist. - - Args: - - data_list (list): A list containing six items: the parent - preference window, the treeview model, a list of liststore - paths, a corresponding list of media data objects to update, - the pattern and the substitution text - - """ - - # Extract values from the argument list - pref_win = data_list.pop(0) - model = data_list.pop(0) - mod_path_list = data_list.pop(0) - media_list = data_list.pop(0) - pattern = data_list.pop(0) - subst = data_list.pop(0) - - # Search and replace the source URL for each media data object - success_count = 0 - fail_count = 0 - for path in mod_path_list: - - media_data_obj = media_list.pop(0) - - if not self.url_change_regex_flag: - mod_url = media_data_obj.source - mod_url.replace(pattern, subst) - else: - mod_url = re.sub(pattern, subst, media_data_obj.source) - - if not utils.check_url(mod_url): - fail_count += 1 - else: - # (Update the contents of the treeview cell immediately) - media_data_obj.set_source(mod_url) - tree_iter = model.get_iter(path) - model[tree_iter][3] = mod_url - success_count += 1 - - # Confirm the result - msg = _('Search/replace complete') + '\n\n' \ - + _('Updated URLs: {0}').format(success_count) + '\n' \ - + _('Errors: {0}').format(fail_count) - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - pref_win, # The parent window is the preference window - ) - - - def update_container_name(self, data_list): - - """Called by config.SystemPrefWin.on_container_name_edited(). - - When the user has confirmed a change to a channel/playlist's name, - implement that change, and update the window's treeview. - - Args: - - data_list (list): A list containing four items: the treeview model, - an iter pointing to a cell in the model, the media data object - and the updated name - - """ - - # Extract values from the argument list - model = data_list.pop(0) - tree_iter = data_list.pop(0) - media_data_obj = data_list.pop(0) - name = data_list.pop(0) - - # Update the media data object and Video Index - if self.rename_container_silently(media_data_obj, name): - model[tree_iter][2] = name - self.main_win_obj.video_index_reset() - self.main_win_obj.video_index_populate() - - - # (Sorting functions) - - - def video_compare(self, obj1, obj2): - - """Standard media.Video sorting function. - - The function occurs here, rather than in utils.py, so that it's - possible to retrieve self.catalogue_sort_mode and - self.catalogue_reverse_sort_flag when called by functools.cmp_to_key(). - - Args: - - obj1, obj2 (media.Video): Two media.Video objects, one of which - must be sorted before the other - - Return values: - - -1 if obj1 comes before obj2, 1 if obj2 comes before obj1 (the code - does not return 0) - - """ - - if self.catalogue_reverse_sort_flag: - obj1, obj2 = obj2, obj1 - - if self.catalogue_sort_mode == 'default': - - # Sort videos by livestream mode (if applicable), then by playlist - # index (if set), then by upload time, and then by receive - # (download) time - # Only if necessary do we sort by name or by .dbid (later in this - # function) - # The video's index is not relevant unless sorting a playlist (and - # not relevant in private folders, e.g. 'All Videos') - if obj1.live_mode > obj2.live_mode: - return -1 - elif obj1.live_mode < obj2.live_mode: - return 1 - elif obj1.live_time < obj2.live_time: - return -1 - elif obj1.live_time > obj2.live_time: - return 1 - - if isinstance(obj1.parent_obj, media.Playlist) \ - and obj1.parent_obj == obj2.parent_obj: - - if obj1.index is None and obj2.index is not None: - return 1 - elif obj2.index is None and obj1.index is not None: - return -1 - elif obj1.index is not None and obj2.index is not None: - if obj1.index < obj2.index: - return -1 - elif obj2.index < obj1.index: - return 1 - - if obj1.upload_time is not None and obj2.upload_time is not None: - - if obj1.upload_time > obj2.upload_time: - return -1 - elif obj1.upload_time < obj2.upload_time: - return 1 - elif obj1.receive_time is not None \ - and obj2.receive_time is not None: - - # In private folders, the most recently received video goes - # to the top of the list - if isinstance(obj1, media.Folder) \ - and obj1.parent_obj == obj2.parent_obj \ - and obj1.parent_obj.priv_flag: - if obj1.receive_time > obj2.receive_time: - return -1 - elif obj1.receive_time < obj2.receive_time: - return 1 - # ...but for everything else, the sorting algorithm is the - # same as for media.GenericRemoteContainer.do_sort(), in - # which we assume the website is sending us videos, - # newest first - else: - if obj1.receive_time < obj2.receive_time: - return -1 - elif obj1.receive_time > obj2.receive_time: - return 1 - - elif self.catalogue_sort_mode == 'receive' \ - and obj1.receive_time is not None \ - and obj2.receive_time is not None: - if obj1.receive_time < obj2.receive_time: - return -1 - elif obj1.receive_time > obj2.receive_time: - return 1 - - elif self.catalogue_sort_mode == 'dbid': - if obj1.dbid < obj2.dbid: - return -1 - else: - return 1 - - # Fallback sorting method (including when self.catalogue_sort_mode is - # set to 'alpha'): - # Sort alphabetically, then by .dbid - if obj1.natname < obj2.natname: - return -1 - elif obj1.natname > obj2.natname: - return 1 - elif obj1.dbid < obj2.dbid: - return -1 - else: - return 1 - - - def folder_child_compare(self, obj1, obj2): - - """Standard folder sorting function, called by - media.Folder.sort_children(). - - The function occurs here, rather than in utils.py, so that it's - possible to retrieve self.catalogue_sort_mode and - self.catalogue_reverse_sort_flag when called by functools.cmp_to_key(). - - Standard sorting function for the children of a container, which might - be any combination of media.Video, media.Channel, media.Playlist and - media.Folder objects. - - Args: - - obj1, obj2 (media.Video): Two media data objects, one of which - must be sorted before the other - - """ - - if self.catalogue_reverse_sort_flag: - obj1, obj2 = obj2, obj1 - - if str(obj1.__class__) == str(obj2.__class__) \ - or ( - isinstance(obj1, media.GenericRemoteContainer) \ - and isinstance(obj2, media.GenericRemoteContainer) - ): - if isinstance(obj1, media.Video): - - # If both objects are media.Video objects, use the standard - # video sorting function - return self.video_compare(obj1, obj2) - - else: - - # If the objects are of different class, then we can sort by class - if isinstance(obj1, media.Folder): - return -1 - elif isinstance(obj2, media.Folder): - return 1 - elif isinstance(obj1, media.Channel) \ - or isinstance(obj1, media.Playlist): - return -1 - elif isinstance(obj2, media.Channel) \ - or isinstance(obj2, media.Playlist): - return 1 - - # As a last resort, sort by name, and then by .dbid - if obj1.natname < obj2.natname: - return -1 - elif obj1.natname > obj2.natname: - return 1 - elif obj1.dbid < obj2.dbid: - return -1 - else: - return 1 - - - # (Export/import data to/from the Tartube database) - - - def export_from_db(self, media_list): - - """Called by self.on_menu_export_db() and - mainwin.MainWin.on_video_index_export(). - - Exports a summary of the Tartube database to an export file - either a - structured JSON file, or a CSV file, or a plain text file, at the - user's option. - - The export file typically contains a list of videos, channels, - playlists and folders, but not any downloaded files (videos, - thumbnails, etc). - - The export file is not the same as a Tartube database file (usually - tartube.db) and cannot be loaded as a database file. However, the - export file can be imported into an existing database. - - Args: - - media_list (list): A list of media data objects. If specified, only - those objects (and any media data objects they contain) are - included in the export. If an empty list is passed, the whole - database is included. - - """ - - # If the specified list is empty, a summary of the whole database is - # exported - if not media_list: - whole_flag = True - else: - whole_flag = False - - # Prompt the user for which kinds of media data object should be - # included in the export, and which type of file (JSON or plain text) - # should be created - dialogue_win = mainwin.ExportDialogue(self.main_win_obj, whole_flag) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - include_video_flag = dialogue_win.checkbutton.get_active() - include_channel_flag = dialogue_win.checkbutton2.get_active() - include_playlist_flag = dialogue_win.checkbutton3.get_active() - preserve_folder_flag = dialogue_win.checkbutton4.get_active() - json_flag = dialogue_win.radiobutton.get_active() - csv_flag = dialogue_win.radiobutton2.get_active() - plain_text_flag = dialogue_win.radiobutton3.get_active() - separator = dialogue_win.separator - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - # Prompt the user for the file path to use - if json_flag: - suggestion = self.export_json_file_name - elif csv_flag: - suggestion = self.export_csv_file_name - else: - suggestion = self.export_text_file_name - - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Select where to save the database export'), - self.main_win_obj, - 'save', - suggestion, - ) - - response = dialogue_win.run() - if response != Gtk.ResponseType.OK: - dialogue_win.destroy() - return - - file_path = dialogue_win.get_filename() - dialogue_win.destroy() - if not file_path: - return - - # Compile a dictionary of data to export, representing the contents of - # the database (in whole or in part) - # Throughout the export/import code, dictionaries in this form are - # called 'db_dict' - # Depending on the user's choices, the dictionary preserves the folder - # structure of the database (or not) - # - # Key-value pairs in the dictionary are in the form - # - # dbid: mini_dict - # - # 'dbid' is each media data object's .dbid - # 'mini_dict' is a dictionary of values representing a media data - # object - # - # The same 'mini_dict' structure is used during export and - # import procedures. Its keys are: - # - # type - set to 'video', 'channel', 'playlist' or 'folder' - # dbid - set to the media data object's .dbid - # vid - set to the media data object's .vid (or None for - # channels, playlists and folders) - # name - set to the media data object's .name IV - # nickname - set to the media data object's .nickname IV (or - # None for videos) - # file - set to the filename and extension of a video - # (e.g. 'video.mp4'; None for channels, playlists - # and folders) - # source - set to the media data object's .source IV (or - # None for folders) - # db_dict - the children of this media data object, stored in - # the form described above - # - # The import process adds some extra keys to a 'mini_dict' while - # processing it, but only for channels/playlists/folders. The extra - # keys are: - # - # display_name - # - the media data object's name, indented for display - # in mainwin.ImportDialogue - # video_count - # - the number of videos this media data object contains - # import_flag - # - True if the user has selected this media data object - # to be imported, False if they have deselected it - db_dict = {} - - # Compile the contents of the 'db_dict' to export - # If the media_list argument is empty, use the whole database. - # Otherwise, use only the specified media data objects (and any media - # data objects they contain) - if preserve_folder_flag: - - if media_list: - - for media_data_obj in media_list: - - mini_dict = media_data_obj.prepare_export( - self, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - if mini_dict: - db_dict[media_data_obj.dbid] = mini_dict - - else: - - for dbid in self.container_top_level_list: - - media_data_obj = self.media_reg_dict[dbid] - - mini_dict = media_data_obj.prepare_export( - self, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - if mini_dict: - db_dict[media_data_obj.dbid] = mini_dict - - else: - - if media_list: - - for media_data_obj in media_list: - - db_dict = media_data_obj.prepare_flat_export( - self, - db_dict, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - else: - - for dbid in self.container_top_level_list: - - media_data_obj = self.media_reg_dict[dbid] - - db_dict = media_data_obj.prepare_flat_export( - self, - db_dict, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - if not db_dict: - - self.dialogue_manager_obj.show_msg_dialogue( - _('There is nothing to export!'), - 'error', - 'ok', - ) - - return - - # Export a JSON file - if json_flag: - - # The exported JSON file has the same metadata as a config file, - # with only the 'file_type' being different - - # Prepare values - local = utils.get_local_time() - - # Prepare a dictionary of data to save as a JSON file - json_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(local.strftime('%d %b %Y')), - 'save_time': str(local.strftime('%H:%M:%S')), - 'file_type': 'db_export', - # Data - 'db_dict': db_dict, - } - - # Try to save the file - try: - with open(file_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - -# # DEBUG: Git 143: provide more information on the exception -# except: -# return self.dialogue_manager_obj.show_msg_dialogue( -# _('Failed to save the database export file'), -# 'error', -# 'ok', -# ) - except Exception as e: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to save the database export file:') \ - + '\n\n' + str(e), - 'error', - 'ok', - ) - - return - - # Export a CSV file - elif csv_flag: - - # Update the CSV separator, before writing the file - self.export_csv_separator = separator - - # Lines in the CSV file are in the following format: - # type|name|url|container_name|vid|file - # - # 'type' is one of 'video', 'channel', 'playlist' or 'folder' - # For folders, 'url' is unspecified - # 'vid' and 'file' are only specified for videos - # If there is no parent container, 'container_name' is unspecified. - # The parent container must be listed before its children - # The separator is specified by self.export_csv_separator; the - # default value is '|' - - # Prepare the list of lines - line_list = self.export_from_db_to_csv_insert( - db_dict, - [], - include_video_flag, - ) - - # Try to save the file - try: - with open(file_path, 'w') as outfile: - for line in line_list: - outfile.write(line + '\n') - -# # DEBUG: Git 143: provide more information on the exception -# except: -# return self.dialogue_manager_obj.show_msg_dialogue( -# _('Failed to save the database export file'), -# 'error', -# 'ok', -# ) - except Exception as e: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to save the database export file:') \ - + '\n\n' + str(e), - 'error', - 'ok', - ) - - return - - # Export a plain text file - else: - - # v2.3.208: In a change from earlier versions, the text file now - # contains lines in groups of four, in the following format: - # - # @type - # - # - # - # - # For videos, a group of six is used, in the format - # - # @type - # - # - # - # - # - # - # '@type' is one of '@video', '@channel', '@playlist' or '@folder' - # For folders, is an empty line - # If there is no parent container, is an empty - # line. The parent container is always listed before its children - - # Prepare the list of lines - line_list = self.export_from_db_to_text_insert( - db_dict, - [], - include_video_flag, - ) - - # Try to save the file - try: - with open(file_path, 'w') as outfile: - for line in line_list: - outfile.write(line + '\n') - -# # DEBUG: Git 143: provide more information on the exception -# except: -# return self.dialogue_manager_obj.show_msg_dialogue( -# _('Failed to save the database export file'), -# 'error', -# 'ok', -# ) - except Exception as e: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to save the database export file:') \ - + '\n\n' + str(e), - 'error', - 'ok', - ) - - return - - # Export was successful - self.dialogue_manager_obj.show_simple_msg_dialogue( - _('Database export file saved to:') + '\n\n' + file_path, - 'info', - 'ok', - ) - - - def export_from_db_to_text_insert(self, db_dict, line_list, \ - include_video_flag): - - """Called by self.export_from_db(), and then by this function - recursively. - - self.export_from_db() is trying to convert the dictionary 'db_dict' (in - the form described in the comments in self.export_from_db() ) into a - flat list of lines, in groups of four, to be saved as the plaint text - export file. - - The 'db_dict' passed as an argument to this function is either the - overall dictionary, or a sub-dictionary inside the original, in the - same format. Again, this is described in self.export_from_db(). - - This file is called recursively to walk the overall dictionary, and to - insert four items into 'line_list' for every media data object. - - Args: - - db_dict (dict): Either the original 'db_dict', or one of its - sub-dictionaries in the same format - - line_list (list): The list of lines to be saved as the text export - file; this call adds more lines to the list, before returning - it - - include_video_flag (bool): If True, media.Video objects are to be - included in the export file; if False, they are ignored - - Return values: - - The updated 'line_list' - - """ - - for dbid in db_dict.keys(): - - mini_dict = db_dict[dbid] - media_data_obj = self.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Video): - # (Child videos of this media data object have already been - # handled, by the code below) - continue - - else: - - if isinstance(media_data_obj, media.Folder): - line_list.append('@folder') - line_list.append(media_data_obj.name) - # Folders have no URL - line_list.append('') - - else: - - if media_data_obj.source is not None: - source = media_data_obj.source - else: - source = '' - - if isinstance(media_data_obj, media.Channel): - line_list.append('@channel') - line_list.append(media_data_obj.name) - line_list.append(source) - - else: - line_list.append('@playlist') - line_list.append(media_data_obj.name) - line_list.append(source) - - if media_data_obj.parent_obj: - line_list.append(media_data_obj.parent_obj.name) - else: - line_list.append('') - - if include_video_flag: - - for child_obj in media_data_obj.child_list: - - if isinstance(child_obj, media.Video): - - line_list.append('@video') - line_list.append(child_obj.name) - - if child_obj.source is not None: - line_list.append(child_obj.source) - else: - line_list.append('') - - line_list.append(media_data_obj.name) - - if child_obj.vid is not None: - line_list.append(child_obj.vid) - else: - line_list.append('') - - if child_obj.file_name is not None \ - and child_obj.file_ext is not None: - line_list.append( - child_obj.file_name + child_obj.file_ext, - ) - else: - line_list.append('') - - if mini_dict['db_dict']: - line_list = self.export_from_db_to_text_insert( - mini_dict['db_dict'], - line_list, - include_video_flag, - ) - - return line_list - - - def export_from_db_to_csv_insert(self, db_dict, line_list, \ - include_video_flag): - - """Called by self.export_from_db(), and then by this function - recursively. - - self.export_from_db() is trying to convert the dictionary 'db_dict' (in - the form described in the comments in self.export_from_db() ) into a - flat list of lines, to be saved as the CSV export file. - - The 'db_dict' passed as an argument to this function is either the - overall dictionary, or a sub-dictionary inside the original, in the - same format. Again, this is described in self.export_from_db(). - - This file is called recursively to walk the overall dictionary, and to - insert a line into 'line_list' for every media data object. - - Args: - - db_dict (dict): Either the original 'db_dict', or one of its - sub-dictionaries in the same format - - line_list (list): The list of lines to be saved as the CSV export - file; this call adds more lines to the list, before returning - it - - include_video_flag (bool): If True, media.Video objects are to be - included in the export file; if False, they are ignored - - Return values: - - The updated 'line_list' - - """ - - separator = self.export_csv_separator - - for dbid in db_dict.keys(): - - mini_dict = db_dict[dbid] - media_data_obj = self.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Video): - # (Child videos of this media data object have already been - # handled, by the code below) - continue - - else: - - if isinstance(media_data_obj, media.Folder): - # Folders have no URL - line = 'folder' + separator + media_data_obj.name \ - + separator + separator - - else: - - if media_data_obj.source is not None: - source = media_data_obj.source - else: - source = '' - - if isinstance(media_data_obj, media.Channel): - line = 'channel' + separator + media_data_obj.name \ - + separator + source + separator - - else: - line = 'playlist' + separator + media_data_obj.name \ - + separator + source + separator - - if media_data_obj.parent_obj: - line += media_data_obj.parent_obj.name - - # (Channels, playlists and folders do not have the .vid, - # .file_name or .file_ext IVs - line += separator + separator - - line_list.append(line) - - if include_video_flag: - - for child_obj in media_data_obj.child_list: - - if isinstance(child_obj, media.Video): - - # Create a line in the form - # type|name|url|container_name|vid|file - line = 'video' + separator + child_obj.name \ - + separator - - if child_obj.source is not None: - line += child_obj.source + separator - else: - line += separator - - line += media_data_obj.name + separator - - if child_obj.vid is not None: - line += child_obj.vid + separator - else: - line += separator - - if child_obj.file_name is not None \ - and child_obj.file_ext is not None: - line += child_obj.file_name \ - + child_obj.file_ext - - line_list.append(line) - - if mini_dict['db_dict']: - line_list = self.export_from_db_to_csv_insert( - mini_dict['db_dict'], - line_list, - include_video_flag, - ) - - return line_list - - - def import_into_db(self): - - """Called by self.on_menu_import_db(). - - Imports the contents of a JSON/CSV/plain text export file generated by - a call to self.export_from_db(). - - After prompting the user, creates new media.Video, media.Channel, - media.Playlist and/or media.Folder objects. Checks for duplicates and - handles them appropriately. - - A JSON export file contains a dictionary, 'db_dict', containing further - dictionaries, 'mini_dict', whose formats are described in the comments - in self.export_from_db(). - - A CSV export file contains lines in the format - 'type|name|url|container_name|vid|file', as described in the comments - in self.export_from_db(). - - A plain text export file contains lines in groups of four (or groups of - six for videos), in the format described in the comments in - self.export_from_db(). - """ - - # Prompt the user for the export file to load - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Select the database export'), - self.main_win_obj, - 'open', - ) - - response = dialogue_win.run() - if response != Gtk.ResponseType.OK: - dialogue_win.destroy() - return - - file_path = dialogue_win.get_filename() - dialogue_win.destroy() - if not file_path: - return - - file_name, file_ext = os.path.splitext(file_path) - if file_ext != '.json' and file_ext != '.csv' and file_ext != '.txt': - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to load the database export file'), - 'error', - 'ok', - ) - - return - - # Try to load the export file - if file_ext == '.json': - - json_dict = self.file_manager_obj.load_json(file_path) - if not json_dict: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to load the database export file'), - 'error', - 'ok', - ) - - return - - # Do some basic checks on the loaded data - # (At the moment, JSON export files are compatible with all - # versions of Tartube after v1.0.0; this may change in future) - if not json_dict \ - or not 'script_name' in json_dict \ - or not 'script_version' in json_dict \ - or not 'save_date' in json_dict \ - or not 'save_time' in json_dict \ - or not 'file_type' in json_dict \ - or json_dict['script_name'] != __main__.__packagename__ \ - or json_dict['file_type'] != 'db_export': - self.dialogue_manager_obj.show_msg_dialogue( - _('The database export file is invalid'), - 'error', - 'ok', - ) - - return - - # Retrieve the database data itself. db_dict is in the form - # described in the comments in self.export_from_db() - # However, json.dump() has converted integer keys to string keys. - # Each key is a 'fake' dbid, so this doesn't affect the outcome - # (and it's not worth converting them back to integers) - db_dict = json_dict['db_dict'] - - else: - - text = self.file_manager_obj.load_text(file_path) - if text is None: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to load the database export file'), - 'error', - 'ok', - ) - - return - - # Parse the text file, creating a db_dict in the form described in - # the comments in self.export_from_db() - if file_ext == '.csv': - db_dict = self.parse_csv_import(text) - else: - db_dict = self.parse_text_import(text) - - if not db_dict: - self.dialogue_manager_obj.show_msg_dialogue( - _('The database export file is invalid (or empty)'), - 'error', - 'ok', - ) - - return - - # Prompt the user to allow them to select which videos/channels/ - # playlists/folders to actually import, and how to deal with - # duplicate channels/playlists/folders - dialogue_win = mainwin.ImportDialogue(self.main_win_obj, db_dict) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying the - # dialogue window - # 'flat_db_dict' is a flattened version of the imported 'db_dict' (i.e. - # with its folder structure removed), and with additional key-value - # pairs added to each 'mini_dict'. (The new key-value pairs are also - # described in the comments in self.export_from_db() ) - import_videos_flag = dialogue_win.checkbutton.get_active() - merge_duplicates_flag = dialogue_win.checkbutton.get_active() - flat_db_dict = dialogue_win.flat_db_dict - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - # Process the imported 'db_dict', creating new videos/channels/ - # playlists/folders as required, and dealing appropriately with - # any duplicates - (video_count, channel_count, playlist_count, folder_count) \ - = self.process_import( - db_dict, # The imported data - flat_db_dict, # The flattened version of that dictionary - None, # No parent 'mini_dict' yet - import_videos_flag, - merge_duplicates_flag, - False, # Do not import into the selected folder - 0, # video_count - 0, # channel_count - 0, # playlist count - 0, # folder_count - ) - - if not video_count and not channel_count and not playlist_count \ - and not folder_count: - self.dialogue_manager_obj.show_msg_dialogue( - _('Nothing was imported from the database export file'), - 'error', - 'ok', - ) - - else: - - # Update the Video Catalogue, in case any new videos have been - # imported into it - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - ) - - # Show a confirmation - msg = _('Imported into database') \ - + ':\n\n' + _('Videos') + ': ' + str(video_count) \ - + '\n' + _('Channels') + ': ' + str(channel_count) \ - + '\n' + _('Playlists') + ': ' + str(playlist_count) \ - + '\n' + _('Folders') + ': ' + str(folder_count) - - self.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - ) - - - def parse_csv_import(self, text): - - """Called by self.import_into_db(). - - Given the contents of a CSV database export, which has been loaded into - memory, convert the contents into the db_dict format described in the - comments in self.export_from_db(), as if a JSON database export had - been loaded. - - A CSV export file contains lines in the format - 'type|name|url|container_name|vid|file', as described in the comments - in self.export_from_db(). - - 'type' is one of 'video', 'channel', 'playlist' or 'folder'. - - For folders, is not specified. 'vid' and 'file' are only - specified for videos. - - If there is no parent container, is not specified. The - parent container must be listed before its children - - Args: - - text (str): The contents of the loaded CSV file - - Return values: - - db_dict (dict): The converted data in the form described in the - comments in self.export_from_db() - - """ - - db_dict = {} - fake_dbid = 0 - check_flag = False - separator = '\\' + self.export_csv_separator - regex = '^(.*)' + separator + '(.*)' + separator + '(.*)' + separator \ - + '(.*)' + separator + '(.*)' + separator + '(.*)' - - # Create entries corresponding to the fixed folders 'Unsorted Videos' - # and 'Video Clips' - fake_dbid += 1 - db_dict[fake_dbid] = { - 'type': 'folder', - 'dbid': fake_dbid, - 'vid': None, - 'name': self.fixed_misc_folder.name, - 'nickname': self.fixed_misc_folder.nickname, - 'file': None, - 'source': None, - 'db_dict': {}, - } - - fake_dbid += 1 - db_dict[fake_dbid] = { - 'type': 'folder', - 'dbid': fake_dbid, - 'vid': None, - 'name': self.fixed_clips_folder.name, - 'nickname': self.fixed_clips_folder.nickname, - 'file': None, - 'source': None, - 'db_dict': {}, - } - - # Split text into separate lines - line_list = text.splitlines() - - # Extract fields from each line, and check they are valid - # If any line is invalid, ignore that line and any subsequent lines, - # and just use the data already extracted - for line in line_list: - - match = re.search(regex, line) - if not match: - break - - media_type = match.groups()[0] - name = match.groups()[1] - source = match.groups()[2] - container_name = match.groups()[3] - vid = match.groups()[4] - filename = match.groups()[5] - - if ( - media_type != 'video' and media_type != 'channel' \ - and media_type != 'playlist' and media_type != 'folder' \ - ) \ - or name == '' \ - or ( - media_type != 'folder' \ - and source != '' \ - and not utils.check_url(source) \ - ): - break - - # If the original media data object's .source/.vid/.file_name/ - # .file_ext IVs were set to None, they were saved as empty lines - if source == '': - source = None - if vid == '': - vid = None - if filename == '': - filename = None - - # (We have already created entries for 'Unsorted Videos' and - # 'Video Clips') - if name != self.fixed_misc_folder.name \ - and name != self.fixed_clips_folder.name: - - # A valid line; add an entry to db_dict using a fake dbid - fake_dbid += 1 - - mini_dict = { - 'type': media_type, - 'dbid': fake_dbid, - 'vid': vid, - 'name': name, - 'nickname': name, - 'file': filename, - 'source': source, - 'db_dict': {}, - } - - # If the name for a parent container was specified, look for a - # match in 'db_dict'. (The parent should have been specified - # earlier in the file, so it will already exist in db_dict) - # If found, self.parse_import_insert() inserts mini_dict into - # db_dict at the correct location - # If not found, a video is inserted into 'Unsorted Videos', and - # everything else is not given a parent container - if not re.search(r'\S', container_name): - - # No parent container specified - db_dict[fake_dbid] = mini_dict - - elif not self.parse_import_insert( - db_dict, - container_name, - fake_dbid, - mini_dict, - ): - # No parent container found - if media_type == '@video': - - # As specified above, the 'Unsorted Videos' folder uses - # the 'fake_dbid' of 1 - db_dict[1]['db_dict'][fake_dbid] = mini_dict - - else: - - # This channel/playlist/folder goes into the top level - # of the media data registry - db_dict[fake_dbid] = mini_dict - - # Procedure complete - if fake_dbid == 2: - return {} # Nothing was actually imported - else: - return db_dict - - - def parse_text_import(self, text): - - """Called by self.import_into_db(). - - Given the contents of a plain text database export, which has been - loaded into memory, convert the contents into the db_dict format - described in the comments in self.export_from_db(), as if a JSON - database export had been loaded. - - The text file contains lines, in groups of four, in the following - format: - - @type - - - - - For videos, a group of six is used, in the format: - - @type - - - - - - - '@type' is one of '@video', '@channel', '@playlist' or '@folder' - For folders, is an empty line - If there is no parent container, is an empty line. The - parent container must be listed before its children - - N.B. The export format changed in v2.3.208, and again in v2.3.337. This - function will try to recognise exports from earlier versions, but it's - not guaranteed to work. - - Args: - - text (str): The contents of the loaded plain text file - - Return values: - - db_dict (dict): The converted data in the form described in the - comments in self.export_from_db() - - """ - - db_dict = {} - fake_dbid = 0 - check_flag = False - video_check_flag = False - - # Create entries corresponding to the fixed folders 'Unsorted Videos' - # and 'Video Clips' - fake_dbid += 1 - db_dict[fake_dbid] = { - 'type': 'folder', - 'dbid': fake_dbid, - 'vid': None, - 'name': self.fixed_misc_folder.name, - 'nickname': self.fixed_misc_folder.nickname, - 'file': None, - 'source': None, - 'db_dict': {}, - } - - fake_dbid += 1 - db_dict[fake_dbid] = { - 'type': 'folder', - 'dbid': fake_dbid, - 'vid': None, - 'name': self.fixed_clips_folder.name, - 'nickname': self.fixed_clips_folder.nickname, - 'file': None, - 'source': None, - 'db_dict': {}, - } - - # Split text into separate lines - line_list = text.splitlines() - - # Extract each group of four lines, and check they are valid - # If a group of four/six is invalid (or if we reach the end of the file - # in the middle of a group of), ignore that group and any subsequent - # groups, and just use the data already extracted - # Spltting by '\n' will create a group with just one empty line, so - # we don't check that line_list is empty - while len(line_list) > 1: - - media_type = line_list[0] - name = line_list[1] - source = line_list[2] - container_name = line_list[3] - vid = None - filename = None - - # Basic checks - if media_type is None \ - or ( - media_type != '@video' and media_type != '@channel' \ - and media_type != '@playlist' and media_type != '@folder' \ - ) \ - or name is None \ - or name == '' \ - or source is None \ - or ( - media_type != '@folder' \ - and source != '' \ - and not utils.check_url(source) \ - ) \ - or container_name is None: - break - - # The export format changed in v2.3.208. Try to detect exports - # from earlier versions (this is not guaranteed to work) - if not check_flag: - - check_flag = True - - if container_name == '@video' \ - or container_name == '@channel' \ - or container_name == '@playlist' \ - or container_name == '@folder': - break - - # The export format changed again in v2.3.337, to use groups of six - # for videos - if media_type != '@video': - line_list = line_list[4:] - - else: - vid = line_list[4] - filename = line_list[5] - line_list = line_list[6:] - - # More basic checks, and try to detect exports from earlier - # versions - if vid is None or filename is None: - break - - if not video_check_flag: - - video_check_flag = True - - if vid == '@video' \ - or vid == '@channel' \ - or vid == '@playlist' \ - or vid == '@folder': - break - - # If the original media data object's .source/.vid/.file_name/ - # .file_ext IVs were set to None, they were saved as empty lines - if source == '': - source = None - if vid == '': - vid = None - if filename == '': - filename = None - - # (We have already created entries for 'Unsorted Videos' and - # 'Video Clips') - if name != self.fixed_misc_folder.name \ - and name != self.fixed_clips_folder.name: - - # A valid group of four/six; add an entry to db_dict using a - # fake dbid - fake_dbid += 1 - - mini_dict = { - 'type': media_type[1:], # Remove initial @ - 'dbid': fake_dbid, - 'vid': vid, - 'name': name, - 'nickname': name, - 'file': filename, - 'source': source, - 'db_dict': {}, - } - - # If the name for a parent container was specified, look for a - # match in 'db_dict'. (The parent should have been specified - # earlier in the file, so it will already exist in db_dict) - # If found, self.parse_import_insert() inserts mini_dict into - # db_dict at the correct location - # If not found, a video is inserted into 'Unsorted Videos', and - # everything else is not given a parent container - if not re.search(r'\S', container_name): - - # No parent container specified - db_dict[fake_dbid] = mini_dict - - elif not self.parse_import_insert( - db_dict, - container_name, - fake_dbid, - mini_dict, - ): - # No parent container found - if media_type == '@video': - - # As specified above, the 'Unsorted Videos' folder uses - # the 'fake_dbid' of 1 - db_dict[1]['db_dict'][fake_dbid] = mini_dict - - else: - - # This channel/playlist/folder goes into the top level - # of the media data registry - db_dict[fake_dbid] = mini_dict - - # Procedure complete - if fake_dbid == 2: - return {} # Nothing was actually imported - else: - return db_dict - - - def parse_import_insert(self, db_dict, container_name, - insert_fake_dbid, insert_mini_dict): - - """Called by self.parse_text_import(), and then by this function - recursively. - - self.parse_text_import() is trying to convert a text export file into - a dictionary, 'db_dict', in the form described in the comments in - self.export_from_db(). - - The 'db_dict' passed as an argument to this function is either the - overall dictionary, or a sub-dictionary inside the original, in the - same format. Again, this is described in self.export_from_db(). - - This file is called to find the entry corresponding to a channel/ - playlist/folder called 'container_name', so an entry for a new video/ - channel/playlist/folder can be inserted into it as a child object. - - Search the original 'db_dict' recursively until the correct entry is - found. - - Args: - - db_dict (dict): Either the original 'db_dict', or one of its - sub-dictionaries in the same format - - container_name (str): The name of the parent container - - insert_fake-dbid (int): A fake .dbid for the child object to be - inserted - - insert_mini_dict (dict): The 'db_dict' for the child object to be - inserted, specifying its attributes - - Return values: - - True if the correct entry has been found (either by this function - call, or by one of its recursive function calls); False if the - correct entry hasn't been found yet - - """ - - for this_fake_dbid in db_dict: - - this_mini_dict = db_dict[this_fake_dbid] - if this_mini_dict['type'] != 'video' \ - and this_mini_dict['name'] == container_name: - this_mini_dict['db_dict'][insert_fake_dbid] = insert_mini_dict - return True - - elif self.parse_import_insert( - this_mini_dict['db_dict'], - container_name, - insert_fake_dbid, - insert_mini_dict, - ): - # mini_dict has been inserted at its correct location, so now - # we can stop checking - return True - - # mini_dict not inserted at its correct location yet - return False - - - def process_import(self, db_dict, flat_db_dict, parent_obj, - import_videos_flag, merge_duplicates_flag, selected_is_parent_flag, - video_count, channel_count, playlist_count, folder_count): - - """Called by self.import_into_db(). Subsequently called by this - function recursively. - - Also called by wizwin.ImportYTWizWin.applychanges(). - - Process a 'db_dict' (in the format described in the comments in - self.export_from_db() ). - - Create new videos/channels/playlists/folders as required, and deal - appropriately with any duplicates. - - Args: - - db_dict (dict): The dictionary described in self.export_from_db(); - if called from self.import_into_db(), the original imported - dictionary; if called recursively, a dictionary from somewhere - inside the original imported dictionary - - flat_db_dict (dict): A flattened version of the original imported - 'db_dict' (not necessarily the same 'db_dict' provided by the - argument above). Flattened means that the folder structure has - been removed, and additional key-value pairs have been added to - each 'mini_dict' (described in comments in - self.export_from_db() ) - - parent_obj (media.Channel, media.Playlist, media.Folder or None): - The contents of db_dict are all children of this parent media - data object - - import_videos_flag (bool): If True, any video objects are imported. - If False, video objects are ignored - - merge_duplicates_flag (bool): If True, imported channels/playlists/ - folders with the same name (and source URL) as an existing - channel/playlist/folder are merged with them. If False, the - imported channel/playlist/folder is renamed - - selected_is_parent_flag (bool): If True, and if a non-system folder - is selected in the Video Index, then everything is imported - into that folder - - video_count, channel_count, playlist_count, folder_count (int): The - total number of videos/channels/playlists/folders imported so - far - - Return values: - - video_count, channel_count, playlist_count, folder_count (int): The - updated counts after importing videos/channels/playlists/ - folders - - """ - - url_check_dict = {} - if parent_obj: - - # To optimise the code below, compile a dictionary for quick - # lookup, containing the source URLs for all videos in the parent - # channel/playlist/folder - for child_obj in parent_obj.child_list: - if isinstance(child_obj, media.Video) \ - and child_obj.source is not None: - url_check_dict[child_obj.source] = None - - elif selected_is_parent_flag \ - and self.main_win_obj.video_index_current_dbid is not None: - - # When called from the 'Import YouTube subscriptions' wizard - # window, import everything into a non-system folder, if one is - # selected - selected_obj \ - = self.media_reg_dict[self.main_win_obj.video_index_current_dbid] - - if isinstance(selected_obj, media.Folder) \ - and not selected_obj.fixed_flag: - - parent_obj = selected_obj - # Because of recursion, reset the flag; we only need to do this - # once - selected_is_parent_flag = False - - # Deal in turn with each video/channel/playlist/folder stored at the - # top level of 'db_dict' - # The 'fake_dbid' is the one used in the database from which the export - # file was generated. Once imported into our database, the new media - # data object will be given a different (real) .dbid - # (In other words, we can't compare this 'fake_dbid' with those used in - # self.media_reg_dict) - for fake_dbid in db_dict.keys(): - - media_data_obj = None - merge_flag = False - - # Each 'mini_dict' contains details for a single video/channel/ - # playlist/folder - mini_dict = db_dict[fake_dbid] - - # Check whether the user has marked this item to be imported, or - # not - if int(fake_dbid) in flat_db_dict: - - check_dict = flat_db_dict[int(fake_dbid)] - if not check_dict['import_flag']: - - # Don't import this one - continue - - # This item is marked to be imported - if mini_dict['type'] == 'video': - - if import_videos_flag: - - # Check that a video with the same URL doesn't already - # exist in the parent channel/playlist/folder. If so, - # don't import this duplicate video - if not mini_dict['source'] in url_check_dict: - - # This video isn't a duplicate, so we can import it - video_obj = self.add_video( - parent_obj, - mini_dict['source'], - ) - - if video_obj: - video_count += 1 - video_obj.set_name(mini_dict['name']) - video_obj.set_nickname(mini_dict['nickname']) - video_obj.set_vid(mini_dict['vid']) - - if mini_dict['file'] is not None: - filename, ext = os.path.splitext( - mini_dict['file'], - ) - video_obj.set_file(filename, ext) - - else: - - # Check for existing containers with the same name in the - # same parent folder - duplicate_obj = self.find_duplicate_name_in_container( - parent_obj, - mini_dict['name'], - ) - - if duplicate_obj: - - # A channel/playlist/folder with the same name already - # exists in the same parent folder - # Rename the imported container if the user has specified - # that, or if the existing container and the imported - # container have different source URLs - # Exception: 'Unsorted Videos' and 'Video Clips' is always - # merged with itself - if duplicate_obj != self.fixed_misc_folder \ - and duplicate_obj != self.fixed_clips_folder \ - and ( - not merge_duplicates_flag \ - or ( - not isinstance(duplicate_obj, media.Folder) \ - and duplicate_obj.source != mini_dict['source'] - ) - ): - # Rename the imported channel/playlist/folder - mini_dict['name'] = self.rename_imported_container( - parent_obj, - mini_dict['name'], - ) - - mini_dict['nickname'] = mini_dict['name'] - - else: - - # Use the existing channel/playlist/folder of the same - # name, thereby merging the two - media_data_obj = duplicate_obj - merge_flag = True - - # Import the channel/playlist/folder - if not media_data_obj: - - if mini_dict['type'] == 'channel': - media_data_obj = self.add_channel( - mini_dict['name'], - parent_obj, - mini_dict['source'], - ) - - if media_data_obj: - channel_count += 1 - - elif mini_dict['type'] == 'playlist': - media_data_obj = self.add_playlist( - mini_dict['name'], - parent_obj, - mini_dict['source'], - ) - - if media_data_obj: - playlist_count += 1 - - elif mini_dict['type'] == 'folder': - media_data_obj = self.add_folder( - mini_dict['name'], - parent_obj, - ) - - if media_data_obj: - folder_count += 1 - - # If the channel/playlist/folder was successfully imported, - # set its nickname, update the Video Index, then deal with - # any children by calling this function recursively - if media_data_obj is not None: - - if not merge_flag: - media_data_obj.set_nickname(mini_dict['nickname']) - self.main_win_obj.video_index_add_row(media_data_obj) - - if mini_dict['db_dict']: - - ( - video_count, channel_count, playlist_count, - folder_count, - ) = self.process_import( - mini_dict['db_dict'], - flat_db_dict, - media_data_obj, - import_videos_flag, - merge_duplicates_flag, - selected_is_parent_flag, - video_count, - channel_count, - playlist_count, - folder_count, - ) - - # Procedure complete - return video_count, channel_count, playlist_count, folder_count - - - def rename_imported_container(self, parent_obj, name): - - """Called by self.process_import(). - - When importing a channel/playlist/folder whose name is the same as an - existing container in the same parent folder (or in the top level - list), this function is called to rename the imported one (when - necessary). - - For example, converts 'Comedy' to 'Comedy (2)'. - - Args: - - parent_obj (media.Folder or None): The parent folder, or None if - the imported container is being added to the top-level list - - name (str): The name of the imported container - - Return values: - - The converted name - - """ - - count = 1 - while True: - - count += 1 - new_name = name + ' (' + str(count) + ')' - - if not find_duplicate_name_in_container(self, parent_obj, name): - return new_name - - - # (Interact with media data objects) - - - def watch_video_in_player(self, video_obj): - - """Can be called by anything. - - Watch a video using the system's default media player, first checking - that a file actually exists. - - Args: - - video_obj (media.Video): The video to watch - - """ - - path = video_obj.get_actual_path(self) - if os.path.isfile(path): - - utils.open_file(self, path) - - else: - - name, ext = os.path.splitext(path) - - # Because it's so easy to convert the original video to a different - # format (including audio formats), search for one of those, - # before reporting an error - for test_ext in (formats.VIDEO_FORMAT_LIST): - test_path = name + '.' + test_ext - if os.path.isfile(test_path): - utils.open_file(self, test_path) - return - - for test_ext in (formats.AUDIO_FORMAT_LIST): - test_path = name + '.' + test_ext - if os.path.isfile(test_path): - utils.open_file(self, test_path) - return - - # Video is completely missing - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'The video file is missing from Tartube\'s data folder' \ - + ' (try downloading the video again!)', - ), - 'error', - 'ok', - ) - - - def download_watch_videos(self, video_list, watch_flag=True): - - """Can be called by anything. - - Download the specified videos and, when they have been downloaded, - launch them in the system's default media player. - - Args: - - video_list (list): List of media.Video objects to download and - watch - - watch_flag (bool): If False, the video(s) are not launched in the - system's default media player after being downloaded - - """ - - # Sanity check: this function is only for videos - for video_obj in video_list: - if not isinstance(video_obj, media.Video): - return self.system_error( - 171, - 'Download and watch video request failed sanity check', - ) - - # Add the video to the list of videos to be launched in the system's - # default media player, the next time a download operation finishes - if watch_flag: - for video_obj in video_list: - self.watch_after_dl_list.append(video_obj) - - if self.download_manager_obj: - - # Download operation already in progress. Add these videos to its - # list - for video_obj in video_list: - download_item_obj \ - = self.download_manager_obj.download_list_obj.create_item( - video_obj, - None, # media.Scheduled object - 'real', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.main_win_obj.progress_list_add_row( - download_item_obj.item_id, - video_obj, - ) - - # Update the main window's progress bar - self.download_manager_obj.nudge_progress_bar() - - else: - - # Start a new download operation to download this video - self.download_manager_start('real', False, video_list) - - - # (Custom download manager objects) - - - def delete_custom_dl_manager(self, custom_dl_obj): - - """Called by callback in - config.SystemPrefWin.on_custom_dl_delete_button_clicked(). - - Deletes the specified custom download manager object - (downloads.CustomDLManager), which might be applied to the Classic Mode - tab (or not). - - Args: - - custom_dl_obj (downloads.CustomDLManager): The object to delete - - """ - - # Sanity check - if self.current_manager_obj \ - or self.general_custom_dl_obj == custom_dl_obj: - return self.system_error( - 172, - 'Delete custom download manager request failed sanity check', - ) - - # Any media.Scheduled object which references the custom download - # manager must be updated - for scheduled_obj in self.scheduled_list: - if scheduled_obj.custom_dl_uid is not None \ - and scheduled_obj.custom_dl_uid == custom_dl_obj.uid: - scheduled_obj.reset_custom_dl_uid() - - # Destroy the downloads.CustomDLManager object itself - del self.custom_dl_reg_dict[custom_dl_obj.uid] - - if self.classic_custom_dl_obj \ - and self.classic_custom_dl_obj == custom_dl_obj: - self.classic_custom_dl_obj = None - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_operations_custom_dl_tab_update_treeview() - - # Update the main menu (which lists custom downloads) - self.main_win_obj.update_menu() - - - def apply_classic_custom_dl_manager(self, custom_dl_obj): - - """Called by - config.SystemPrefWin.on_custom_dl_use_classic_button_clicked(). - - Applies a specified custom download manager object - (downloads.CustomDLManager) for use in the Classic Mode tab. - - Args: - - custom_dl_obj (downloads.CustomDLManager): The custom download - manager object to apply - - """ - - if self.current_manager_obj: - return self.system_error( - 173, - 'Apply custom download manager request failed sanity check', - ) - - # Apply the custom download manager - self.classic_custom_dl_obj = custom_dl_obj - - # The manager for the Classic Mode tab must do a simulated download - # before a real download - custom_dl_obj.set_dl_precede_flag(True) - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_operations_custom_dl_tab_update_treeview() - - - def disapply_classic_custom_dl_manager(self): - - """Called by - config.SystemPrefWin.on_custom_dl_use_classic_button_clicked(). - - Disapplies the custom download manager object - (downloads.CustomDLManager) used in the Classic Mode tab, but doesn't - destroy the object. - """ - - if self.current_manager_obj or not self.classic_custom_dl_obj: - return self.system_error( - 174, - 'Disapply custom download manager request failed sanity check', - ) - - # Disapply the custom download manager - self.classic_custom_dl_obj = None - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_operations_custom_dl_tab_update_treeview() - - - def create_custom_dl_manager(self, name): - - """Can be called by anything. - - Create a new downloads.CustomDLManager object, and updates the IVs - self.custom_dl_reg_count and self.custom_dl_reg_dict. - - (It is up to the calling code to update self.general_custom_dl_obj or - self.classic_custom_dl_obj, if required.) - - Args: - - name (str): A non-unique name for the custom manager - - Return values: - - Returns the downloads.CustomDLManager object created - - """ - - self.custom_dl_reg_count += 1 - - custom_dl_obj = downloads.CustomDLManager( - self.custom_dl_reg_count, - name, - ) - - self.custom_dl_reg_dict[custom_dl_obj.uid] = custom_dl_obj - - # Update the main menu (which lists custom downloads) - self.main_win_obj.update_menu() - - return custom_dl_obj - - - def clone_custom_dl_manager_from_window(self, data_list): - - """Called by config.CustomDLEditWin.on_clone_settings_clicked(). - - Clones settings from the current custom download manager into the - specified one. - - Args: - - data_list (list): List of values supplied by the dialogue window. - The first is the edit window for the custom download manager - object (which must be reset). The second value is the custom - download manager object, into which new settings will be - cloned - - """ - - edit_win_obj = data_list.pop(0) - custom_dl_obj = data_list.pop(0) - - # Clone values from the current custom download manager - custom_dl_obj.clone_settings(self.general_custom_dl_obj) - # Reset the edit window to display the new (cloned) values - edit_win_obj.reset_with_new_edit_obj(custom_dl_obj) - - - def clone_custom_dl_manager(self, old_custom_dl_obj): - - """Can be called by anything. - - Clones a custom download manager object, and returns the clone, which - has the same name as the original, but a different .uid. - - Args: - - old_custom_dl_obj (downloads.CustomDLManager): The object to clone. - Any custom download manager object (including the General - Custom Download Manager) can be cloned - - Return values: - - The new cloned object - - """ - - # Work out a name for the clone that's not already in use - # (custom download manager objects don't have unique names, but in this - # case we'll give it a unique name, so that the user can clearly see - # what has happened) - match = re.search(r'^(.*)\s+(\d+)$', old_custom_dl_obj.name) - if match: - base_name = match.group(1) - index = int(match.group(2)) - else: - base_name = old_custom_dl_obj.name - index = 1 - - match_flag = True - while match_flag: - - match_flag = False - index += 1 - - test_name = base_name + ' ' + str(index) - for this_obj in self.custom_dl_reg_dict.values(): - - if this_obj.name == test_name: - match_flag = True - break - - new_name = base_name + ' ' + str(index) - - # Create a new custom download manager object - new_custom_dl_obj = self.create_custom_dl_manager(new_name) - # Copy the original's values into the new object - new_custom_dl_obj.clone_settings(old_custom_dl_obj) - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_operations_custom_dl_tab_update_treeview() - - # Update the main menu (which lists custom downloads) - self.main_win_obj.update_menu() - - return new_custom_dl_obj - - - def reset_custom_dl_manager(self, data_list): - - """Called by config.CustomDLEditWin.on_reset_settings_clicked(). - - Resets the specified custom download manager object, setting its IVs to - their default values. - - Args: - - data_list (list): List of values supplied by the dialogue window, - the first of which is the edit window for the custom download - manager object (which must be reset) - - """ - - edit_win_obj = data_list.pop(0) - old_custom_dl_obj = edit_win_obj.edit_obj - - # Replace the old object with a new one, which has the effect of - # resetting its settings to the default values - new_custom_dl_obj \ - = self.create_custom_dl_manager(old_custom_dl_obj.name) - - # Update IVs - del self.custom_dl_reg_dict[old_custom_dl_obj.uid] - if self.general_custom_dl_obj == old_custom_dl_obj: - self.general_custom_dl_obj = new_custom_dl_obj - - # Reset the edit window to display the new (default) values - edit_win_obj.reset_with_new_edit_obj(new_custom_dl_obj) - - # Update the main menu (which lists custom downloads) - self.main_win_obj.update_menu() - - - def export_custom_dl_manager(self, custom_dl_obj): - - """Called by callback in - config.SystemPrefWin.on_custom_dl_export_button_clicked(). - - Exports data from the specified downloads.CustomDLManager object as a - JSON file. The data can be-imported (probably when a different - Tartube database is loaded) in a call to - self.import_custom_dl_manager(). - - Args: - - custom_dl_obj (downloads.CustomDLManager): The object whose data - should be exported - - """ - - # Prompt the user for the file path to use - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Select where to save the custom download export'), - self.main_win_obj, - 'save', - custom_dl_obj.name + '.json', - ) - - response = dialogue_win.run() - if response != Gtk.ResponseType.OK: - dialogue_win.destroy() - return - - file_path = dialogue_win.get_filename() - dialogue_win.destroy() - if not file_path: - return - - # Compile a dictionary of data to export. Each key matches an IV in - # the downloads.CustomDLManager object - export_dict = { - 'name': custom_dl_obj.name, - - 'dl_by_video_flag': custom_dl_obj.dl_by_video_flag, - 'split_flag': custom_dl_obj.split_flag, - 'slice_flag': custom_dl_obj.slice_flag, - 'slice_dict': custom_dl_obj.slice_dict.copy(), - 'delay_flag': custom_dl_obj.delay_flag, - 'delay_max': custom_dl_obj.delay_max, - 'delay_min': custom_dl_obj.delay_min, - 'divert_mode': custom_dl_obj.divert_mode, - 'divert_website': custom_dl_obj.divert_website, - } - - # The exported JSON file has the same metadata as a config file, with - # only the 'file_type' being different - - # Prepare values - local = utils.get_local_time() - - # Prepare a dictionary of data to save as a JSON file - json_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(local.strftime('%d %b %Y')), - 'save_time': str(local.strftime('%H:%M:%S')), - 'file_type': 'custom_dl_export', - # Data - 'export_dict': export_dict, - } - - # Try to save the file - try: - with open(file_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - - except Exception as e: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to save the custom download export file:') \ - + '\n\n' + str(e), - 'error', - 'ok', - ) - - return - - # Export was successful - self.dialogue_manager_obj.show_msg_dialogue( - _('Custom download exported to:') + '\n\n' + file_path, - 'info', - 'ok', - ) - - - def import_custom_dl_manager(self, custom_dl_name=None): - - """Called by a callback in - config.SystemPrefWin.on_custom_dl_import_button_clicked(). - - Imports the contents of a JSON export file generated by a call to - self.export_custom_dl_manager(). - - Creates a new downloads.CustomDLManager object, and copies the imported - data into it. - - Args: - - custom_dl_name (str or None): If specified, the new - downloads.CustomDLManager object is given that name. If not - specified, the new object is given the name specified by the - export file - - """ - - # Prompt the user for the export file to load - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Select the custom download export file'), - self.main_win_obj, - 'open', - ) - - response = dialogue_win.run() - if response != Gtk.ResponseType.OK: - dialogue_win.destroy() - return - - file_path = dialogue_win.get_filename() - dialogue_win.destroy() - if not file_path: - return - - # Try to load the export file - json_dict = self.file_manager_obj.load_json(file_path) - if not json_dict: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to load the custom download export file'), - 'error', - 'ok', - ) - - return - - # Do some basic checks on the loaded data - # (At the moment, JSON export files are compatible with all - # versions of Tartube after v2.2.0; this may change in future) - if not json_dict \ - or not 'script_name' in json_dict \ - or not 'script_version' in json_dict \ - or not 'save_date' in json_dict \ - or not 'save_time' in json_dict \ - or not 'file_type' in json_dict \ - or json_dict['script_name'] != __main__.__packagename__ \ - or json_dict['file_type'] != 'custom_dl_export': - self.dialogue_manager_obj.show_msg_dialogue( - _('The custom download export file is invalid'), - 'error', - 'ok', - ) - - return - - # Retrieve the data itself. export_dict is in the form described in the - # comments in self.export_custom_dl_manager() - export_dict = json_dict['export_dict'] - - if not export_dict: - self.dialogue_manager_obj.show_msg_dialogue( - _('The custom download export file is invalid (or empty)'), - 'error', - 'ok', - ) - - return - - # Create a new custom download manager object. If a name was specified - # in the call to this function, use that; otherwise use the name - # specified by the export - if custom_dl_name is None or custom_dl_name == '': - custom_dl_name = export_dict['name'] - - custom_dl_obj = self.create_custom_dl_manager(custom_dl_name) - - # Set the new object's settings - custom_dl_obj.dl_by_video_flag = export_dict['dl_by_video_flag'] - custom_dl_obj.split_flag = export_dict['split_flag'] - custom_dl_obj.slice_flag = export_dict['slice_flag'] - custom_dl_obj.slice_dict = export_dict['slice_dict'] - custom_dl_obj.delay_flag = export_dict['delay_flag'] - custom_dl_obj.delay_max = export_dict['delay_max'] - custom_dl_obj.delay_min = export_dict['delay_min'] - custom_dl_obj.divert_mode = export_dict['divert_mode'] - custom_dl_obj.divert_website = export_dict['divert_website'] - - # Show a confirmation - self.dialogue_manager_obj.show_msg_dialogue( - ('Imported:') + ' ' + custom_dl_name, - 'info', - 'ok', - ) - - - def compile_custom_dl_manager_list(self): - - """Can be called by anything. - - Returns a list of download.CustomDLManager objects, sorted by name, but - excluding self.general_custom_dl_obj and self.classic_custom_dl_obj. - - Return values: - - The sorted list (may be empty) - - """ - - manager_list = [] - for this_obj in self.custom_dl_reg_dict.values(): - - if ( - not self.general_custom_dl_obj - or this_obj != self.general_custom_dl_obj - ) and ( - not self.classic_custom_dl_obj - or this_obj != self.classic_custom_dl_obj - ): - manager_list.append(this_obj) - - # (Sort alphabetically by name) - def get_name(obj): - return obj.name - - manager_list.sort(key=get_name) - - return manager_list - - - def check_custom_download_managers(self): - - """Can be called by anything. - - Checks whether any custom download managers besides the compulsory - General Custom Download Manager, and the optional one used in the - Classic Mode tab, have been created. - - Return values: - - True if any additional custom download managers have been created, - False otherwise - - """ - - for custom_dl_obj in self.custom_dl_reg_dict.values(): - if custom_dl_obj != self.general_custom_dl_obj \ - and ( - self.classic_custom_dl_obj is None \ - or custom_dl_obj != self.classic_custom_dl_obj - ): - return True - - return False - - - # (Profiles) - - - def add_profile(self, profile_name, dbid_list): - - """Called by self.on_menu_create_profile(). - - Creates a profile. - - Args: - - profile_name (str): A name for the new profile - - dbid_list (list): A list of .dbids for media.Channel, - media.Playlist and media.Folder objects. When this profile is - active, all of those items are marked for download - - """ - - if profile_name in self.profile_dict: - - return self.system_error( - 175, - 'Duplicate profile name \'{1}\''.format(profile_name), - ) - - elif len(self.profile_dict) >= self.profile_max: - - return self.system_error( - 176, - 'Number of profiles exceeds maximum', - ) - - self.profile_dict[profile_name] = dbid_list - self.last_profile = profile_name - - # Update the main menu (which lists profiles) - self.main_win_obj.update_menu() - - - def delete_profile(self, profile_name): - - """Called by mainwin.MainWin.on_delete_profile_menu_select(). - - Deletes the specified profile. - - - Args: - - profile_name (str): A key in self.profile_dict - - """ - - if not profile_name in self.profile_dict: - - return self.system_error( - 177, - 'Unrecognised profile \'{1}\''.format(profile_name), - ) - - del self.profile_dict[profile_name] - if self.last_profile == profile_name: - self.last_profile = None - - # Update the main menu (which lists profiles) - self.main_win_obj.update_menu() - - - # (Download options manager objects) - - - def apply_download_options(self, media_data_obj, options_obj=None): - - """Can be called by anything. - - Applies a download options manager (options.OptionsManager) to a media - data object. - - The download options themselves apply to the media data object and any - of its descendants which don't have their own applied download options - manager. - - The download options are passed to youtube-dl during a download - operation. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist or - media.Folder): The media data object to which the download - options are applied - - options_obj (options.OptionsManager or None): The download options - to apply, which must not have been applied to any other media - data object, and must not be the General Options Manager. If - not specified, a new download options manager is created - - """ - - if self.current_manager_obj \ - or media_data_obj.options_obj\ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ) \ - or (options_obj and options_obj == self.general_options_obj): - - return self.system_error( - 178, - 'Apply download options request failed sanity check', - ) - - # Create a new options manager, if none was specified - if not options_obj: - - options_obj = self.create_download_options( - media_data_obj.name, - media_data_obj.dbid, - ) - - # If required, clone download options from the General Options - # Manager into this new download options manager - if self.auto_clone_options_flag: - options_obj.clone_options(self.general_options_obj) - - # Apply download options to the specified media data object - media_data_obj.set_options_obj(options_obj) - options_obj.add_dbid(media_data_obj.dbid) - - # Update the Video Index or Video Catalogue, as required - if isinstance(media_data_obj, media.Video): - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - media_data_obj, - ) - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_icon, - media_data_obj, - ) - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_dl_list_tab_update_treeview() - - - def remove_download_options(self, media_data_obj, no_update_flag=False): - - """Can be called by anything. - - Removes a download options manager (options.OptionsManager) from a - media data object, an action which also affects its descendants (unless - they too have an applied download options manager). - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist or - media.Folder): The media data object from which the download - options are removed. - - no_update_flag (bool): If True, don't update the Video Index or - Video Catalogue (because the calling code will do that) - - """ - - # Sanity check. Removing an options manager object during an operation - # is not allowed, with one exception: during a download operation, - # a video marked as downloaded can have its options manager removed - if not ( - self.download_manager_obj \ - and isinstance(media_data_obj, media.Video) \ - and media_data_obj.dl_flag - ): - if self.current_manager_obj or not media_data_obj.options_obj: - return self.system_error( - 179, - 'Remove download options request failed sanity check', - ) - - # Remove download options from the media data object - options_obj = media_data_obj.options_obj - - media_data_obj.reset_options_obj() - options_obj.del_dbid(media_data_obj.dbid) - - # If the options.OptionsManager object is no longer attached to a media - # data object, and is not visible in the Drag and Drop tab, then - # delete it - if not options_obj.dbid_list \ - and not options_obj.uid in self.classic_dropzone_list \ - and ( - self.general_options_obj is None or - options_obj != self.general_options_obj - ) and ( - self.classic_options_obj is None or - options_obj != self.classic_options_obj - ): - del self.options_reg_dict[options_obj.uid] - options_obj = None - - # Update the row in the Video Index or Video Catalogue - if not no_update_flag: - - if isinstance(media_data_obj, media.Video): - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - media_data_obj, - ) - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_icon, - media_data_obj, - ) - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_dl_list_tab_update_treeview() - - - def delete_download_options(self, options_obj): - - """Called by callback in - config.SystemPrefWin.on_options_delete_button_clicked(). - - Deletes the specified download options manager - (options.OptionsManager), which might be applied to media data objects - (or not), and might be applied to the Classic Mode tab (or not), and - might be visible in the Drag and Drop tab (or not). - - If the options manager has been applied to media data objects, their - descendants are also affected (unless they too have an applied download - options manager). - - Note that the General Options Manager cannot be deleted. - - Args: - - options_obj (options.OptionsManager): The object to delete - - """ - - # Sanity check - if self.current_manager_obj \ - or self.general_options_obj == options_obj: - return self.system_error( - 180, - 'Delete download options request failed sanity check', - ) - - # Remove download options from all affected media data objects - for dbid in options_obj.dbid_list: - - media_data_obj = self.media_reg_dict[dbid] - media_data_obj.reset_options_obj() - - # Update the row in the Video Index or Video Catalogue - if isinstance(media_data_obj, media.Video): - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - media_data_obj, - ) - else: - GObject.timeout_add( - 0, - self.main_win_obj.video_index_update_row_icon, - media_data_obj, - ) - - # Destroy the options.OptionsManager object itself - del self.options_reg_dict[options_obj.uid] - - if self.classic_options_obj \ - and self.classic_options_obj == options_obj: - self.classic_options_obj = None - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_dl_list_tab_update_treeview() - - # Remove any associated dropzone, and update the Drag and Drop tab - if options_obj.uid in self.classic_dropzone_list: - self.classic_dropzone_list.remove(options_obj.uid) - self.main_win_obj.drag_drop_grid_reset() - - - def apply_classic_download_options(self, options_obj): - - """Called by - config.SystemPrefWin.on_options_use_classic_button_clicked(). - - A modified version of self.apply_download_options(). - - Applies a specified download options manager (options.OptionsManager) - for use in the Classic Mode tab. - - Args: - - options_obj (options.OptionsManager): The download options object - to apply - - """ - - if self.current_manager_obj: - return self.system_error( - 181, - 'Apply download options request failed sanity check', - ) - - # Apply download options - self.classic_options_obj = options_obj - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_dl_list_tab_update_treeview() - - - def remove_classic_download_options(self): - - """Called by mainwin.MainWin.on_classic_menu_use_general_options() - and config.SystemPrefWin.on_options_use_classic_button_clicked(). - - Removes the download options manager (options.OptionsManager) from use - in the Classic Mode tab, but doesn't destroy the manager itself. - """ - - if self.current_manager_obj or not self.classic_options_obj: - return self.system_error( - 182, - 'Disapply download options request failed sanity check', - ) - - # Disapply download options - self.classic_options_obj = None - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_dl_list_tab_update_treeview() - - - def create_download_options(self, name, dbid=None): - - """Can be called by anything. - - Create a new options.OptionsManager object, and updates the IVs - self.options_reg_count and self.options_reg_dict. - - (It is up to the calling code to update self.general_options_obj or - self.classic_options_obj, if required.) - - Args: - - name (str): A non-unique name for the options manager - - dbid (int or None): If specified, the .dbid of the media.Video, - media.Channel, media.Playlist or media.Folder to which these - options are attached - - Return values: - - Returns the options.OptionsManager object created - - """ - - self.options_reg_count += 1 - - options_obj = options.OptionsManager( - self.options_reg_count, - name, - dbid, - ) - - self.options_reg_dict[options_obj.uid] = options_obj - - return options_obj - - - def clone_download_options(self, old_options_obj): - - """Can be called by anything. - - Clones a download options manager, and returns the clone, which has the - same name as the original, but a different .uid. - - Args: - - old_options_obj (options.OptionsManager): The object to clone. Any - download options manager (including the General Options - Manager) can be cloned - - Return values: - - The new cloned object - - """ - - # Work out a name for the clone that's not already in use (either by - # an existing options manager, or by a channel/playlist/folder) - # (Options manager objects don't have unique names, but in this case - # we'll give it a unique name, so that the user can clearly see what - # has happened) - match = re.search(r'^(.*)\s+(\d+)$', old_options_obj.name) - if match: - base_name = match.group(1) - index = int(match.group(2)) - else: - base_name = old_options_obj.name - index = 1 - - match_flag = True - while match_flag: - - match_flag = False - index += 1 - - test_name = base_name + ' ' + str(index) - if not self.is_container(test_name): - - for this_obj in self.options_reg_dict.values(): - - if this_obj.name == test_name: - match_flag = True - break - - new_name = base_name + ' ' + str(index) - - # Create a new options manager, with the same name as the original - new_options_obj = self.create_download_options(new_name) - # Copy the original object's values into the new object - new_options_obj.clone_options(old_options_obj) - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_dl_list_tab_update_treeview() - - return new_options_obj - - - def clone_download_options_from_window(self, data_list): - - """Called by config.OptionsEditWin.on_clone_options_clicked(). - - (Not called by self.apply_download_options(), which can handle its own - cloning). - - Clones youtube-dl download options from the General Options manager - into the specified download options manager. - - This function is designed to be called from one particular place. For - general cloning, call self.clone_download_options() instead. - - Args: - - data_list (list): List of values supplied by the dialogue window. - The first is the edit window for the download options manager - (which must be reset). The second value is the download options - manager itself, into which new options will be cloned. - - """ - - edit_win_obj = data_list.pop(0) - options_obj = data_list.pop(0) - - # Clone values from the general download options manager - options_obj.clone_options(self.general_options_obj) - # Reset the edit window to display the new (cloned) values - edit_win_obj.reset_with_new_edit_obj(options_obj) - - - def reset_download_options(self, data_list): - - """Called by config.OptionsEditWin.on_reset_options_clicked(). - - Resets the specified download options manager, setting its options to - their default values. - - Args: - - data_list (list): List of values supplied by the dialogue window, - the first of which is the edit window for the download options - manager (which must be reset) - - """ - - edit_win_obj = data_list.pop(0) - old_options_obj = edit_win_obj.edit_obj - - # Replace the old object with a new one, which has the effect of - # resetting its download options to the default values - new_options_obj = self.create_download_options(old_options_obj.name) - - # Update IVs - del self.options_reg_dict[old_options_obj.uid] - if self.general_options_obj == old_options_obj: - self.general_options_obj = new_options_obj - elif self.classic_options_obj == old_options_obj: - self.classic_options_obj = new_options_obj - - mod_list = [] - for uid in self.classic_dropzone_list: - if uid == old_options_obj.uid: - mod_list.append(new_options_obj.uid) - else: - mod_list.append(uid) - - self.classic_dropzone_list = mod_list - - for dbid in old_options_obj.dbid_list: - - media_data_obj = self.media_reg_dict[dbid] - media_data_obj.set_options_obj(new_optins_obj) - new_options_obj.add_dbid(dbid) - - # Reset the edit window to display the new (default) values - edit_win_obj.reset_with_new_edit_obj(new_options_obj) - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_dl_list_tab_update_treeview() - - # Update the Drag and Drop tab - self.main_win_obj.drag_drop_grid_reset() - - - def export_download_options(self, options_obj): - - """Called by callback in - config.SystemPrefWin.on_options_export_button_clicked(). - - Exports data from the specified options.OptionsManager object as a - JSON file. The data can be-imported (probably when a different - Tartube database is loaded) in a call to - self.import_download_options(). - - Args: - - options_obj (options.OptionsManager): The object whose data should - be exported - - """ - - # Prompt the user for the file path to use - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Select where to save the options export'), - self.main_win_obj, - 'save', - options_obj.name + '.json', - ) - - response = dialogue_win.run() - if response != Gtk.ResponseType.OK: - dialogue_win.destroy() - return - - file_path = dialogue_win.get_filename() - dialogue_win.destroy() - if not file_path: - return - - # Compile a dictionary of data to export. Each key matches an IV in - # the options.OptionsManager object - export_dict = { - 'name': options_obj.name, - 'options_dict': options_obj.options_dict.copy(), - } - - # The exported JSON file has the same metadata as a config file, with - # only the 'file_type' being different - - # Prepare values - local = utils.get_local_time() - - # Prepare a dictionary of data to save as a JSON file - json_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(local.strftime('%d %b %Y')), - 'save_time': str(local.strftime('%H:%M:%S')), - 'file_type': 'options_export', - # Data - 'export_dict': export_dict, - } - - # Try to save the file - try: - with open(file_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - - except Exception as e: - self.dialogue_manager_obj.show_simple_msg_dialogue( - _('Failed to save the options export file:') \ - + '\n\n' + str(e), - 'error', - 'ok', - ) - - return - - # Export was successful - self.dialogue_manager_obj.show_simple_msg_dialogue( - _('Download options exported to to:') + '\n\n' + file_path, - 'info', - 'ok', - ) - - - def import_download_options(self, options_name=None): - - """Called by a callback in - config.SystemPrefWin.on_options_import_button_clicked(). - - Imports the contents of a JSON export file generated by a call to - self.export_download_options(). - - Creates a new options.OptionsManager object, and copies the imported - data into it. - - Args: - - options_name (str or None): If specified, the new - options.OptionsManager object is given that name. If not - specified, the new object is given the name specified by the - export file - - """ - - # Prompt the user for the export file to load - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Select the options export file'), - self.main_win_obj, - 'open', - ) - - response = dialogue_win.run() - if response != Gtk.ResponseType.OK: - dialogue_win.destroy() - return - - file_path = dialogue_win.get_filename() - dialogue_win.destroy() - if not file_path: - return - - # Try to load the export file - json_dict = self.file_manager_obj.load_json(file_path) - if not json_dict: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to load the options export file'), - 'error', - 'ok', - ) - - return - - # Do some basic checks on the loaded data - # (At the moment, JSON export files are compatible with all - # versions of Tartube after v2.2.0; this may change in future) - if not json_dict \ - or not 'script_name' in json_dict \ - or not 'script_version' in json_dict \ - or not 'save_date' in json_dict \ - or not 'save_time' in json_dict \ - or not 'file_type' in json_dict \ - or json_dict['script_name'] != __main__.__packagename__ \ - or json_dict['file_type'] != 'options_export': - self.dialogue_manager_obj.show_msg_dialogue( - _('The options export file is invalid'), - 'error', - 'ok', - ) - - return - - # Retrieve the data itself. export_dict is in the form described in the - # comments in self.export_download_options() - export_dict = json_dict['export_dict'] - - if not export_dict: - self.dialogue_manager_obj.show_msg_dialogue( - _('The options export file is invalid (or empty)'), - 'error', - 'ok', - ) - - return - - # Create a new options object. If a name was specified in the call to - # this function, use that; otherwise use the name specified by the - # export - if options_name is None or options_name == '': - options_name = export_dict['name'] - - options_obj = self.create_download_options(options_name) - - # Set the new object's options. To guard against future code updates, - # do it one key at a time - options_dict = export_dict['options_dict'] - for key in options_dict.keys(): - - value = options_dict[key] - if key in options_obj.options_dict: - options_obj.options_dict[key] = options_dict[key] - - # Show a confirmation - self.dialogue_manager_obj.show_msg_dialogue( - ('Imported:') + ' ' + options_name, - 'info', - 'ok', - ) - - - # (FFmpeg options manager objects) - - - def create_ffmpeg_options(self, name): - - """Can be called by anything. - - Create a new ffmpeg_tartube.FFmpegOptionsManager object, and updates - the IVs self.ffmpeg_reg_count and self.ffmpeg_reg_dict. - - (It is up to the calling code to update self.ffmpeg_options_obj, if - required.) - - Args: - - name (str): A non-unique name for the options manager - - Return values: - - Returns the ffmpeg_tartube.FFmpegOptionsManager object created - - """ - - self.ffmpeg_reg_count += 1 - - options_obj = ffmpeg_tartube.FFmpegOptionsManager( - self.ffmpeg_reg_count, - name, - ) - - self.ffmpeg_reg_dict[options_obj.uid] = options_obj - - return options_obj - - - def clone_ffmpeg_options_from_window(self, data_list): - - """Called by config.FFmpegOptionsEditWin.on_clone_options_clicked(). - - Clones FFmpeg download options from the current FFmpeg options manager - into the specified one. - - Args: - - data_list (list): List of values supplied by the dialogue window. - The first is the edit window for the FFmpeg options object - (which must be reset). The second value is the FFmpeg options - manager object, into which new options will be cloned. - - """ - - edit_win_obj = data_list.pop(0) - options_obj = data_list.pop(0) - - # Clone values from the current FFmpeg options manager - options_obj.clone_options(self.ffmpeg_options_obj) - # Reset the edit window to display the new (cloned) values - edit_win_obj.reset_with_new_edit_obj(options_obj) - - - def clone_ffmpeg_options(self, old_options_obj): - - """Can be called by anything. - - Clones an FFmpeg manager object, and returns the clone, which has the - same name as the original, but a different .uid. - - Args: - - old_options_obj (ffmpeg_tartube.FFmpegOptionsManager): The object - to clone. Any options manager object (including the current - one) can be cloned - - Return values: - - The new cloned object - - """ - - # Work out a name for the clone that's not already in use - # (FFmpeg Options manager objects don't have unique names, but in this - # case we'll give it a unique name, so that the user can clearly see - # what has happened) - match = re.search(r'^(.*)\s+(\d+)$', old_options_obj.name) - if match: - base_name = match.group(1) - index = int(match.group(2)) - else: - base_name = old_options_obj.name - index = 1 - - match_flag = True - while match_flag: - - match_flag = False - index += 1 - - test_name = base_name + ' ' + str(index) - for this_obj in self.ffmpeg_reg_dict.values(): - - if this_obj.name == test_name: - match_flag = True - break - - new_name = base_name + ' ' + str(index) - - # Create a new options object - new_options_obj = self.create_ffmpeg_options(new_name) - # Copy the original's values into the new object - new_options_obj.clone_options(old_options_obj) - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_ffmpeg_list_tab_update_treeview() - - return new_options_obj - - - def reset_ffmpeg_options(self, data_list): - - """Called by config.FFmpegOptionsEditWin.on_reset_options_clicked(). - - Resets the specified FFmpeg options manager object, setting its options - to their default values. - - Args: - - data_list (list): List of values supplied by the dialogue window, - the first of which is the edit window for the download options - object (which must be reset) - - """ - - edit_win_obj = data_list.pop(0) - old_options_obj = edit_win_obj.edit_obj - - # Replace the old object with a new one, which has the effect of - # resetting its FFmpeg options to the default values - new_options_obj = self.create_ffmpeg_options(old_options_obj.name) - - # Update IVs - del self.ffmpeg_reg_dict[old_options_obj.uid] - if self.ffmpeg_options_obj == old_options_obj: - self.ffmpeg_options_obj = new_options_obj - - # Reset the edit window to display the new (default) values - edit_win_obj.reset_with_new_edit_obj(new_options_obj) - - - def delete_ffmpeg_options(self, options_obj): - - """Called by callback in - config.SystemPrefWin.on_ffmpeg_delete_button_clicked(). - - Deletes the specified FFmpeg options object - (ffmpeg_tartube.FFmpegOptionsManager). - - Args: - - options_obj (ffmpeg_tartube.FFmpegOptionsManager): The object to - delete - - """ - - # Sanity check - if self.ffmpeg_options_obj == options_obj: - return self.system_error( - 183, - 'Delete FFmpeg options request failed sanity check', - ) - - # Destroy the ffmpeg_tartube.FFmpegOptionsManager object itself - del self.ffmpeg_reg_dict[options_obj.uid] - - # Update the list in any preference windows that are open - for config_win_obj in self.main_win_obj.config_win_list: - if isinstance(config_win_obj, config.SystemPrefWin): - config_win_obj.setup_options_ffmpeg_list_tab_update_treeview() - - - def export_ffmpeg_options(self, options_obj): - - """Called by callback in - config.SystemPrefWin.on_ffmpeg_export_button_clicked(). - - Exports data from the specified ffmpeg_tartube.FFmpegOptionsManager - object as a JSON file. The data can be-imported (probably when a - different Tartube database is loaded) in a call to - self.import_ffmpeg_options(). - - Args: - - options_obj (ffmpeg_tartube.FFmpegOptionsManager): The object whose - data should be exported. - - """ - - # Prompt the user for the file path to use - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Select where to save the options export'), - self.main_win_obj, - 'save', - options_obj.name + '.json', - ) - - response = dialogue_win.run() - if response != Gtk.ResponseType.OK: - dialogue_win.destroy() - return - - file_path = dialogue_win.get_filename() - dialogue_win.destroy() - if not file_path: - return - - # Compile a dictionary of data to export. Each key matches an IV in - # the ffmpeg_tartube.FFmpegOptionsManager object - export_dict = { - 'name': options_obj.name, - 'options_dict': options_obj.options_dict.copy(), - } - - # The exported JSON file has the same metadata as a config file, with - # only the 'file_type' being different - - # Prepare values - local = utils.get_local_time() - - # Prepare a dictionary of data to save as a JSON file - json_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(local.strftime('%d %b %Y')), - 'save_time': str(local.strftime('%H:%M:%S')), - 'file_type': 'ffmpeg_export', - # Data - 'export_dict': export_dict, - } - - # Try to save the file - try: - with open(file_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - - except Exception as e: - self.dialogue_manager_obj.show_simple_msg_dialogue( - _('Failed to save the options export file:') \ - + '\n\n' + str(e), - 'error', - 'ok', - ) - - return - - # Export was successful - self.dialogue_manager_obj.show_simple_msg_dialogue( - _('FFmpeg options exported to to:') + '\n\n' + file_path, - 'info', - 'ok', - ) - - - def import_ffmpeg_options(self, options_name=None): - - """Called by a callback in - config.SystemPrefWin.on_ffmpeg_import_button_clicked(). - - Imports the contents of a JSON export file generated by a call to - self.export_ffmpeg_options(). - - Creates a new ffmpeg_tartube.FFmpegOptionsManager object, and copies - the imported data into it. - - Args: - - options_name (str or None): If specified, the new - ffmpeg_tartube.FFmpegOptionsManager object is given that name. - If not specified, the new object is given the name specified by - the export file - - """ - - # Prompt the user for the export file to load - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Select the options export file'), - self.main_win_obj, - 'open', - ) - - response = dialogue_win.run() - if response != Gtk.ResponseType.OK: - dialogue_win.destroy() - return - - file_path = dialogue_win.get_filename() - dialogue_win.destroy() - if not file_path: - return - - # Try to load the export file - json_dict = self.file_manager_obj.load_json(file_path) - if not json_dict: - self.dialogue_manager_obj.show_msg_dialogue( - _('Failed to load the options export file'), - 'error', - 'ok', - ) - - return - - # Do some basic checks on the loaded data - # (At the moment, JSON export files are compatible with all - # versions of Tartube after v2.2.0; this may change in future) - if not json_dict \ - or not 'script_name' in json_dict \ - or not 'script_version' in json_dict \ - or not 'save_date' in json_dict \ - or not 'save_time' in json_dict \ - or not 'file_type' in json_dict \ - or json_dict['script_name'] != __main__.__packagename__ \ - or json_dict['file_type'] != 'ffmpeg_export': - self.dialogue_manager_obj.show_msg_dialogue( - _('The options export file is invalid'), - 'error', - 'ok', - ) - - return - - # Retrieve the data itself. export_dict is in the form described in the - # comments in self.export_ffmpeg_options() - export_dict = json_dict['export_dict'] - - if not export_dict: - self.dialogue_manager_obj.show_msg_dialogue( - _('The options export file is invalid (or empty)'), - 'error', - 'ok', - ) - - return - - # Create a new options object. If a name was specified in the call to - # this function, use that; otherwise use the name specified by the - # export - if options_name is None or options_name == '': - options_name = export_dict['name'] - - options_obj = self.create_ffmpeg_options(options_name) - - # Set the new object's options. To guard against future code updates, - # do it one key at a time - options_dict = export_dict['options_dict'] - for key in options_dict.keys(): - - value = options_dict[key] - if key in options_obj.options_dict: - options_obj.options_dict[key] = options_dict[key] - - # Show a confirmation - self.dialogue_manager_obj.show_msg_dialogue( - ('Imported:') + ' ' + options_name, - 'info', - 'ok', - ) - - - # (Sound effects) - - - def play_sound(self, sound_name=None): - - """Can be called by anything. - - Plays the specified sound effect. - - Args: - - sound_name (str): The sound effect to play, one of the items in - self.sound_list. If no sound effect is specified, plays the - user's chosen sound effect, self.sound_custom - - """ - - if self.sound_dir is None: - return - - if sound_name is None: - sound_name = self.sound_custom - - path = os.path.abspath( - os.path.join(self.sound_dir, sound_name), - ) - - if os.path.isfile(path) and HAVE_PLAYSOUND_FLAG: - - # v2.1.025 - on a system on which the playsound module is not - # installed, I'm seeing (very rarely) a 'name 'playsound' is not - # defined' error - # Cannot reproduce the problem, so enclose this code in a try block - # to prevent the error - try: - playsound.playsound(path) - except: - self.system_error( - 184, - 'System tried to play sound effect, even though Python' \ - + ' playsound module was not detected', - ) - - - # Callback class methods - - - # (Timers) - - - def script_slow_timer_callback(self): - - """Called by one of the GObject timers created by self.start(). - - A few times every minute, check whether it's time to perform a - scheduled download and, if so, perform it. - - Otherwise, check whether it's time to perform a scheduled livestream - operation and, if so, perform it. - - Return values: - - 1 to keep the timer going, or None to halt it - - """ - - # No point keeping the timer going, once load/save (and therefore all - # operations) are disabled - if self.disable_load_save_flag: - return None - - # Scheduled downloads do not take place after a failed call to - # self.save_db(), but can resume on a successful call to that - # function, or a successful call to self.load_db() - if self.disable_scheduled_dl_flag: - - # Return 1 to keep the timer going (or 0 to halt the once-only - # timer) - return self.script_slow_timer_get_return_value() - - # Depending on settings, one or several scheduled downloads may be - # started at the same time - # Compile a list of media.Scheduled objects, each one representing a - # scheduled download that is due to start now - first_list = [] - next_list = [] - all_obj = False - shutdown_flag = False - ignore_limits_flag = False - no_join_flag = False - - for scheduled_obj in self.scheduled_list: - - if scheduled_obj.check_start() \ - and ( - not self.download_manager_obj \ - or scheduled_obj.join_mode != 'skip' - ): - if scheduled_obj.exclusive_flag \ - or ( - scheduled_obj.dl_mode == 'custom_real' \ - and scheduled_obj.custom_dl_uid is not None - ): - # Only perform this scheduled download - first_list = [scheduled_obj] - next_list = [] - shutdown_flag = scheduled_obj.shutdown_flag - ignore_limits_flag = scheduled_obj.ignore_limits_flag - - if scheduled_obj.all_flag: - all_obj = scheduled_obj - - if scheduled_obj.dl_mode == 'custom_real' \ - and scheduled_obj.custom_dl_uid is not None: - no_join_flag = True - - break - - # 'start'/'start_after' should be done before 'repeat' and - # 'timetable' - if scheduled_obj.start_mode == 'start' \ - or scheduled_obj.start_mode == 'start_after': - first_list.append(scheduled_obj) - else: - next_list.append(scheduled_obj) - - if scheduled_obj.shutdown_flag: - shutdown_flag = True - if scheduled_obj.ignore_limits_flag: - ignore_limits_flag = True - - if scheduled_obj.all_flag: - all_obj = scheduled_obj - break - - start_list = first_list + next_list - - # In case there are different values for media.Scheduled.dl_mode and - # media.Scheduled.join_mode, then a custom download takes priority - # over a real download, which takes priority over a simulated - # download - dl_mode = None - for scheduled_obj in start_list: - - if dl_mode is None \ - or (dl_mode == 'sim' and scheduled_obj.dl_mode != 'sim') \ - or ( - dl_mode == 'real' - and scheduled_obj.dl_mode == 'custom_real' - ): - dl_mode = scheduled_obj.dl_mode - join_mode = scheduled_obj.join_mode - - # If any scheduled downloads are due to start, and any of the - # media.Scheduled objects have their .all_flag IV set, then we simply - # download everything - if start_list and all_obj and dl_mode is not None: - - # Download everything - - # If no download operation is in progress, start one (if we're - # allowed to) - if not self.download_manager_obj \ - and not self.current_manager_obj \ - and not self.main_win_obj.config_win_list: - - self.download_manager_start( - dl_mode, - True, # This function is the calling function - [all_obj], # Make sure performance limits respected - ) - - # Ignore operation limits, if required - if ignore_limits_flag: - self.download_manager_obj.apply_ignore_limits() - - # Shutdown Tartube after this d/l operation, if required - if self.download_manager_obj and shutdown_flag: - self.halt_after_operation_flag = True - - # Set the next download time for each scheduled download - self.script_slow_timer_reset_scheduled_dl(start_list) - # Return 1 to keep the timer going (or 0 to halt the once-only - # timer) - return self.script_slow_timer_get_return_value() - - # Otherwise, add all media data objects in the top-level list (all - # children are downloaded too) - elif self.download_manager_obj and join_mode != 'skip': - - for dbid in self.container_top_level_list: - - media_data_obj = self.media_reg_dict[dbid] - # (Don't try to download the 'All Videos' folder, etc) - if not isinstance(media_data_obj. media.Folder) \ - or not media_data_obj.priv_flag: - - self.script_slow_timer_insert_download( - media_data_obj, - all_obj, - dl_mode, - join_mode, - ignore_limits_flag, - ) - - # Shutdown Tartube after this d/l operation, if required - if shutdown_flag: - self.halt_after_operation_flag = True - - # Set the next download time for each scheduled download - self.script_slow_timer_reset_scheduled_dl(start_list) - # Return 1 to keep the timer going (or 0 to halt the once-only - # timer) - return self.script_slow_timer_get_return_value() - - # If any scheduled downloads are still due to start, and a download - # operation is already in progress, then we can simpy add new media - # data objects to it - if start_list and self.download_manager_obj and not no_join_flag: - - for scheduled_obj in start_list: - - for dbid in scheduled_obj.media_list: - - if not dbid in self.container_reg_dict: - - self.system_error( - 185, - 'Scheduled download contains a channel, playlist' \ - + ' or folder which no longer exists: #' + dbid, - ) - - else: - - media_data_obj = self.media_reg_dict[dbid] - self.script_slow_timer_insert_download( - media_data_obj, - scheduled_obj, - scheduled_obj.dl_mode, - scheduled_obj.join_mode, - scheduled_obj.ignore_limits_flag, - ) - - # Shutdown Tartube after this d/l operation, if required - if shutdown_flag: - self.halt_after_operation_flag = True - - # Set the next download time for each scheduled download - self.script_slow_timer_reset_scheduled_dl(start_list) - # Return 1 to keep the timer going (or 0 to halt the once-only - # timer) - return self.script_slow_timer_get_return_value() - - # If any scheduled downloads are still to start, and no download - # operation is already in progress, start one (if we're allowed to) - if start_list \ - and not self.download_manager_obj \ - and not self.current_manager_obj \ - and not self.main_win_obj.config_win_list: - - # Pass the list of media.Scheduled objects directly to the download - # manager, since each object might have different values for - # their .dl_mode and .join_mode IVs - self.download_manager_start( - # In this case, the default operation type does not matter, but - # it's still nice to display 'Checking...' in the Videos tab - # label, if we're only doing simulated downloads - dl_mode, - True, # This function is the calling function - start_list, - ) - - # Shutdown Tartube after this d/l operation, if required - if self.download_manager_obj and shutdown_flag: - self.halt_after_operation_flag = True - - # Set the next download time for each scheduled download - self.script_slow_timer_reset_scheduled_dl(start_list) - # Return 1 to keep the timer going (or 0 to halt the once-only - # timer) - return self.script_slow_timer_get_return_value() - - # Otherwise, we're free to start a livestream operation instead (but - # only if there is at least one media.Video object marked as a - # livestream) - if self.scheduled_livestream_flag and self.media_reg_live_dict: - - start_flag = False - - wait_time = self.scheduled_livestream_wait_mins * 60 - if (self.scheduled_livestream_last_time + wait_time) < time.time(): - - start_flag = True - - # If any livestreams are due to start soon, start a livestream - # operation once a minute (if allowed) - elif self.scheduled_livestream_extra_flag \ - and (self.scheduled_livestream_last_time + 60) < time.time(): - - for video_obj in self.media_reg_live_dict.values(): - - if video_obj.live_mode == 1 \ - and (video_obj.live_time - wait_time) < time.time(): - start_flag = True - break - - if start_flag: - self.livestream_manager_start() - - # Return 1 to keep the timer going (or 0 to halt the once-only timer) - return self.script_slow_timer_get_return_value() - - - def script_slow_timer_get_return_value(self): - - """Called by self.script_slow_timer_callback(). - - Provides a return value for the calling function: 1 to keep the - GObject timer going, or 0 to halt it. - """ - - if self.script_once_timer_id is not None: - - # Halt the once-only timer - self.script_once_timer_id = None - return 0 - - else: - - # The slow timer keeps going indefinitely - return 1 - - - def script_slow_timer_insert_download(self, media_data_obj, scheduled_obj, - dl_mode, join_mode, ignore_limits_flag): - - """Called by self.script_slow_timer_callback(), when a download - operation is already in progress. - - Adds a new download item to the download list. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - media data object to add. It, as well as all of its children, - are added to the download queue - - scheduled_obj (media.Scheduled): The scheduled download object - which wants to download media_data_obj (None if no scheduled - download applies in this case) - - dl_mode (str): 'sim', 'real' or 'multi', matching the value of - downloads.DownloadManager.operation_type - - join_mode (str): 'join', 'priority' or 'skip', matching the value - of media.Scheduled.join_mode - - ignore_limits_flag (bool): True if operation limits - (self.operation_limit_flag) should be ignored - - """ - - if self.download_manager_obj: - - if join_mode == 'priority': - priority_flag = True - else: - priority_flag = False - - download_item_obj \ - = self.download_manager_obj.download_list_obj.create_item( - media_data_obj, - scheduled_obj, - dl_mode, - priority_flag, - ignore_limits_flag, - ) - - if download_item_obj: - - # Add a row to the Progress List - self.main_win_obj.progress_list_add_row( - download_item_obj.item_id, - media_data_obj, - ) - - # Update the main window's progress bar - self.download_manager_obj.nudge_progress_bar() - - - def script_slow_timer_reset_scheduled_dl(self, scheduled_list): - - """Called by self.script_slow_timer_callback(). - - Given a list of media.Scheduled object(s)s which have just been - started, set the time at which the next scheduled download(s) should - start. - - Args: - - scheduled_list (list): A list of media.Scheduled objects - - """ - - current_time = time.time() - - for scheduled_obj in scheduled_list: - scheduled_obj.set_last_time(current_time) - scheduled_obj.set_only_time(0) - - - def script_fast_timer_callback(self): - - """Called by GObject timer created by self.start(). - - Several times a second, check whether there are any mainwin.Catalogue - objects to add to the Video Catalogue and, if so, adds them. - - Resets the position of main window sliders, if that is due to happen. - - Optionally checks the number of columns that can fit in the available - space of the Video Catalogue grid (when visible), and increase/ - reduces the size of the grid, if necessary. - - Resets any confirmation messages in the Drag and Drop tab. - - Return values: - - 1 to keep the timer going, or None to halt it - - """ - - # Reset the position of main window sliders (due to Gtk issues, for a - # second time), if required - if self.main_win_slider_reset_flag: - self.main_win_slider_reset_flag = False - self.main_win_obj.reset_sliders() - - # Update the Video Catalogue, increasing/decreasing the number of - # columns in the grid (if visible, and if necessary) - # This happens after the minimum required width of a gridbox is - # established - if self.main_win_obj.catalogue_grid_rearrange_flag: - self.main_win_obj.video_catalogue_grid_check_size() - - # If a list of videos, rather than a grid, is visible, then insert any - # rows that were to be inserted, but which could not be a few moments - # ago - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_retry_insert_items, - ) - - # Reset confirmation messages in the Drag and Drop tab, if it's time - # to reset them - for wrapper_obj in self.main_win_obj.drag_drop_dict.values(): - wrapper_obj.check_reset() - - # Return 1 to keep the timer going - return 1 - - - def dl_timer_callback(self): - - """Called by GObject timer created by self.download_manager_continue(). - - During a download operation, a GObject timer runs, so that the Progress - tab and Output tab can be updated at regular intervals. (When the - download operation is launched from the Classic Mode tab, the - Classic Progress List and Output tab are updated.) - - There is also a delay between the instant at which youtube-dl reports a - video file has been downloaded, and the instant at which it appears in - the filesystem. The timer checks for newly-existing files at regular - intervals, too. - - If required, this function periodically checks whether the device - containing self.data_dir is running out of space (and halts the - operation, if so.) - - Return values: - - 1 to keep the timer going, or None to halt it - - """ - - # This function behaves differently, if the download operation was - # launched from the Classic Mode tab - if not self.download_manager_obj \ - or not self.download_manager_obj.operation_classic_flag: - classic_mode_flag = False - else: - classic_mode_flag = True - - # Update the disk space visible in the Videos tab - disk_space = utils.disk_get_free_space(self.data_dir) - if self.download_manager_obj: - self.main_win_obj.update_free_space_msg(disk_space) - - # Periodically check (if required) whether the device is running out of - # disk space - if self.dl_timer_disk_space_check_time is None: - # First check occurs 60 seconds after the operation begins - self.dl_timer_disk_space_check_time \ - = time.time() + self.dl_timer_disk_space_time - - elif self.dl_timer_disk_space_check_time < time.time(): - - self.dl_timer_disk_space_check_time \ - = time.time() + self.dl_timer_disk_space_time - - if ( - self.disk_space_stop_flag \ - and self.disk_space_stop_limit != 0 \ - and disk_space <= self.disk_space_stop_limit - ) or disk_space < self.disk_space_abs_limit: - - # Stop the download operation - self.system_error( - 186, - 'Download operation halted because the device is running' \ - + ' out of space', - ) - - self.download_manager_obj.stop_download_operation() - # Return 1 to keep the timer going, which allows the operation - # to finish naturally - return 1 - - # Disk space check complete, now update main window widgets - check_time = self.dl_timer_check_time - if check_time is None or check_time > time.time(): - - if not classic_mode_flag: - self.main_win_obj.progress_list_display_dl_stats() - self.main_win_obj.results_list_update_row() - else: - self.main_win_obj.classic_mode_tab_display_dl_stats() - - if not classic_mode_flag and self.progress_list_hide_flag: - self.main_win_obj.progress_list_check_hide_rows() - - if check_time is None: - - # Download operation still in progress, return 1 to keep the - # timer going - return 1 - - elif self.main_win_obj.results_list_temp_list: - - # Not all downloaded files confirmed to exist yet, so return 1 - # to keep the timer going a little longer - return 1 - - # The download operation has finished. The call to - # self.download_manager_finished() destroys the timer - self.download_manager_finished() - - - def update_timer_callback(self): - - """Called by GObject timer created by self.update_manager_start(). - - During an update operation, a GObject timer runs, so that the Output - tab can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the update operation. - - Return values: - - 1 to keep the timer going - - """ - - if self.update_timer_check_time is None: - - # Update operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.update_timer_check_time > time.time(): - - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The update operation has finished. The call to - # self.update_manager_finished() destroys the timer - self.update_manager_finished() - - - def refresh_timer_callback(self): - - """Called by GObject timer created by self.refresh_manager_continue(). - - During a refresh operation, a GObject timer runs, so that the Output - tab can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the refresh operation. - - Return values: - - 1 to keep the timer going - - """ - - if self.refresh_timer_check_time is None: - - # Refresh operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.refresh_timer_check_time > time.time(): - - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The refresh operation has finished. The call to - # self.refresh_manager_finished() destroys the timer - self.refresh_manager_finished() - - - def info_timer_callback(self): - - """Called by GObject timer created by self.info_manager_start(). - - During an info operation, a GObject timer runs, so that the Output tab - can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the info operation. - - Return values: - - 1 to keep the timer going - - """ - - if self.info_timer_check_time is None: - - # Info operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.info_timer_check_time > time.time(): - - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The info operation has finished. The call to - # self.info_manager_finished() destroys the timer - self.info_manager_finished() - - - def tidy_timer_callback(self): - - """Called by GObject timer created by self.tidy_manager_start(). - - During a tidy operation, a GObject timer runs, so that the Output tab - can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the tidy operation. - - Return values: - - 1 to keep the timer going - - """ - - if self.tidy_timer_check_time is None: - - # Tidy operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.tidy_timer_check_time > time.time(): - - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The tidy operation has finished. The call to - # self.tidy_manager_finished() destroys the timer - self.tidy_manager_finished() - - - def process_timer_callback(self): - - """Called by GObject timer created by self.process_manager_continue(). - - During a process operation, a GObject timer runs, so that the Output - tab can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the process operation. - - Return values: - - 1 to keep the timer going - - """ - - if self.process_timer_check_time is None: - - # Process operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.process_timer_check_time > time.time(): - - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The process operation has finished. The call to - # self.process_manager_finished() destroys the timer - self.process_manager_finished() - - - # (Menu item and toolbar button callbacks) - - - def on_button_apply_error_filter(self, action, par): - - """Called from a callback in self.do_startup(). - - Applies a filter to the Errors List, hiding any messages which don't - match the search text specified by the user. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Apply the filter - self.main_win_obj.errors_list_apply_filter() - - - def on_button_apply_filter(self, action, par): - - """Called from a callback in self.do_startup(). - - Applies a filter to the Video Catalogue, hiding any videos which don't - match the search text specified by the user. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 187, - 'Apply filter request failed sanity check', - ) - - # Apply the filter - self.main_win_obj.video_catalogue_apply_filter() - - - def on_button_cancel_date(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the first one, after showing a page - matching a particular date. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.video_catalogue_unshow_date() - - - def on_button_cancel_error_filter(self, action, par): - - """Called from a callback in self.do_startup(). - - Cancels the filter, restoring filtered messages in the Errors List. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Apply the filter - self.main_win_obj.errors_list_cancel_filter() - - - def on_button_cancel_filter(self, action, par): - - """Called from a callback in self.do_startup(). - - Cancels the filter, restoring all hidden videos in the Video Catalogue. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 188, - 'Cancel filter request failed sanity check', - ) - - # Cancel the filter - self.main_win_obj.video_catalogue_cancel_filter() - - - def on_button_check_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new download operation (if allowed). - - Unlike the corresponding self.on_menu_check_all button, this function - will check only the marked items, if any. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - media_list = [] - for dbid in self.main_win_obj.video_index_marker_dict.keys(): - if dbid in self.container_reg_dict: - media_list.append(self.container_reg_dict[dbid]) - - self.download_manager_start( - 'sim', - False, # Not called from self.script_slow_timer_callback() - media_list, # May be empty, in which case everything is checked - ) - - - def on_button_classic_add_clips(self, action, par): - - """Called from a callback in self.do_startup(). - - In the Classic Mode tab, opens the usual 'Create video clips' dialogue, - tweaked for use with Classic Mode. Creates new dummy media.Video - objects for each item, and updates IVs. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.on_video_catalogue_process_clip_classic_mode() - - - def on_button_classic_add_urls(self, action, par): - - """Called from a callback in self.do_startup(). - - In the Classic Mode tab, transfers URLs in the textview into the - Classic Progress List, creating a new dummy media.Video object for each - URL, and updates IVs. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.classic_mode_tab_add_urls() - - - def on_button_classic_archive(self, action, par): - - """Called from a callback in self.do_startup(). - - Enables/disables the youtube-dl archive file in downloads from the - Classic Mode tab. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if self.main_win_obj.classic_archive_button.get_active(): - self.classic_ytdl_archive_flag = True - else: - self.classic_ytdl_archive_flag = False - - - def on_button_classic_dest_dir(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens the file chooser dialogue, so the user can set a new destination - directory for videos downloaded in the Classic Mode tab. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - dialogue_win = self.dialogue_manager_obj.show_file_chooser( - _('Please select a destination folder'), - self.main_win_obj, - 'folder', - ) - - response = dialogue_win.run() - dest_dir = dialogue_win.get_filename() - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # Update IVs. Don't add a duplicate directory, but do move a - # duplicate to the top (and apply the maximum size, if required) - mod_list = [dest_dir] - for item in self.classic_dir_list: - - if item != dest_dir: - mod_list.append(item) - - if len(mod_list) >= self.classic_dir_max: - break - - self.classic_dir_list = mod_list.copy() - - # Update the combo in the main window - self.main_win_obj.classic_mode_tab_add_dest_dir() - - - def on_button_classic_dest_dir_open(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens the directory for videos downloaded in the Classic Mode tab. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - utils.open_file(self, self.classic_dir_list[0]) - - - def on_button_classic_clear(self, action, par): - - """Called from a callback in self.do_startup(). - - Empties the Classic Progress List. Modified version of - self.on_button_classic_remove(), which removes only the selected - videos. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - dbid_list = list(self.main_win_obj.classic_media_dict.keys()) - if not dbid_list: - return - - # Prompt for confirmation - msg = _('Are you sure you want to clear this list?') - - self.dialogue_manager_obj.show_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'main_win_classic_mode_tab_remove_rows', - # Specified options - 'data': dbid_list, - }, - ) - - - def on_button_classic_clear_dl(self, action, par): - - """Called from a callback in self.do_startup(). - - Removes all downloaded lines from the Classic Progress List, leaving - any downloads that have not yet started (or which failed). Modified - version of self.on_button_classic_remove(), which removes only the - selected videos. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - video_list = list(self.main_win_obj.classic_media_dict.values()) - - # Filter out un-downloaded videos - dbid_list = [] - for dummy_obj in video_list: - if dummy_obj.dummy_path is not None: - dbid_list.append(dummy_obj.dbid) - - if not dbid_list: - return - - # Prompt for confirmation - msg = _('Are you sure you want to clear downloaded videos?') - - self.dialogue_manager_obj.show_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'main_win_classic_mode_tab_remove_rows', - # Specified options - 'data': dbid_list, - }, - ) - - - def on_button_classic_clips(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates video clips using the selected videos. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - selection = self.main_win_obj.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # Get the the dummy media.Video objects for each selected row. Filter - # out any whose filename is not known (so cannot be processed) - video_list = [] - for path in path_list: - - this_iter = model.get_iter(path) - dbid = model[this_iter][0] - video_obj = self.main_win_obj.classic_media_dict[dbid] - if video_obj.dummy_path is not None and video_obj.dummy_dl_flag: - video_list.append(video_obj) - - if not video_list: - - self.dialogue_manager_obj.show_msg_dialogue( - _('Only downloaded videos can be used to create video clips'), - 'error', - 'ok', - ) - - elif len(video_list) > 1: - - self.dialogue_manager_obj.show_msg_dialogue( - _('You can create video clips from only one video at a time!'), - 'error', - 'ok', - ) - - else: - - self.main_win_obj.on_video_catalogue_process_clip_classic_mode( - video_list[0], - ) - - - def on_button_classic_download(self, action, par): - - """Called from a callback in self.do_startup(). - - Starts a download operation for the URLs added to the Classic Progress - List. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.classic_mode_tab_start_download() - - - def on_button_classic_ffmpeg(self, action, par): - - """Called from a callback in self.do_startup(). - - Processes the selected videos using FFmpeg. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - selection = self.main_win_obj.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # Get the the dummy media.Video objects for each selected row. Filter - # out any whose filename is not known (so cannot be processed) - video_list = [] - for path in path_list: - - this_iter = model.get_iter(path) - dbid = model[this_iter][0] - video_obj = self.main_win_obj.classic_media_dict[dbid] - if video_obj.dummy_path is not None: - video_list.append(video_obj) - - if not video_list: - - self.dialogue_manager_obj.show_msg_dialogue( - _('Only checked/downloaded videos can be processed by FFmpeg'), - 'error', - 'ok', - ) - - else: - - # Create an edit window for the current FFmpegOptionsManager - # object. Supply it with the list of videos, so that the user can - # start the process operation from the edit window - config.FFmpegOptionsEditWin( - self, - self.ffmpeg_options_obj, - video_list, - ) - - - def on_button_classic_menu(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens a popup menu for the Classic Mode tab. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Open the popup menu - self.main_win_obj.classic_popup_menu() - - - def on_button_classic_move_up(self, action, par): - - """Called from a callback in self.do_startup(). - - In the Classic Progress List, moves the selected item(s) up. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.classic_mode_tab_move_row(True) - - - def on_button_classic_move_down(self, action, par): - - """Called from a callback in self.do_startup(). - - In the Classic Progress List, moves the selected item(s) down. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.classic_mode_tab_move_row(False) - - - def on_button_classic_open(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens the destination(s) of any videos downloaded from the selected - rows in the Classic Progress List. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - selection = self.main_win_obj.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # Get the the dummy media.Video objects for each selected row, and - # produce a list of destinations, ignoring duplicates - dir_list = [] - for path in path_list: - - this_iter = model.get_iter(path) - dbid = model[this_iter][0] - dummy_obj = self.main_win_obj.classic_media_dict[dbid] - - if dummy_obj.dummy_dir \ - and not dummy_obj.dummy_dir in dir_list: - dir_list.append(dummy_obj.dummy_dir) - - if not dir_list: - - self.dialogue_manager_obj.show_msg_dialogue( - _('No destination(s) to show'), - 'error', - 'ok', - ) - - else: - - for this_dir in dir_list: - utils.open_file(self, this_dir) - - - def on_button_classic_play(self, action, par): - - """Called from a callback in self.do_startup(). - - Plays any videos downloaded from the selected rows in the Classic - Progress List. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - selection = self.main_win_obj.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # Get the the dummy media.Video objects for each selected row, and - # filter out those for which no video(s) have been downloaded - video_list = [] - for path in path_list: - - this_iter = model.get_iter(path) - dbid = model[this_iter][0] - video_obj = self.main_win_obj.classic_media_dict[dbid] - if video_obj.dummy_path is not None: - video_list.append(video_obj.dummy_path) - - if not video_list: - - self.dialogue_manager_obj.show_msg_dialogue( - _('No video(s) have been downloaded'), - 'error', - 'ok', - ) - - else: - - for video_path in video_list: - utils.open_file(self, video_path) - - - def on_button_classic_redownload(self, action, par): - - """Called from a callback in self.do_startup(). - - Redownloads the selected rows in the Classic Progress List. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - selection = self.main_win_obj.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - if not self.download_manager_obj: - - # No download operation is currently in progress, so prepare a new - # one - - # Get the dummy media.Video objects for each selected row - video_list = [] - for path in path_list: - - this_iter = model.get_iter(path) - dbid = model[this_iter][0] - video_obj = self.main_win_obj.classic_media_dict[dbid] - video_list.append(video_obj) - - # Mark the video as not downloaded - video_obj.set_dl_flag(False) - # Delete the files associated with the video - self.delete_video_files(video_obj) - - # Start the download operation - if not self.classic_custom_dl_flag: - self.download_manager_start('classic_real', False, video_list) - else: - self.download_manager_start( - 'classic_custom', - False, - video_list, - ) - - else: - - # A download operation is already in progress. If any of the - # selected videos are being downloaded, halt that download. Then, - # mark the videos to be downloaded again - - # Get the .dbid of the dummy media.Video objects for each selected - # row - dbid_dict = {} - for path in path_list: - - this_iter = model.get_iter(path) - dbid_dict[model[this_iter][0]] = None - - # Stop any downloads matching one of these dbids - for worker_obj in self.download_manager_obj.worker_list: - - if worker_obj.running_flag \ - and worker_obj.download_item_obj \ - and worker_obj.download_item_obj.media_data_obj.dbid \ - in dbid_dict: - worker_obj.downloader_obj.stop() - - # Re-add the videos to the download list. The existing row in the - # Classic Progress List is automatically re-used - list_obj = self.download_manager_obj.download_list_obj - for dbid in dbid_dict.keys(): - - dummy_obj = self.main_win_obj.classic_media_dict[dbid] - download_item_obj = list_obj.create_dummy_item(dummy_obj) - if download_item_obj: - - # Update the main window's progress bar - self.download_manager_obj.nudge_progress_bar() - - - def on_button_classic_remove(self, action, par): - - """Called from a callback in self.do_startup(). - - Removes the selected rows from the Classic Progress List. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - selection = self.main_win_obj.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # Get the .dbid of the dummy media.Video objects for each selected - # row - dbid_list = [] - for path in path_list: - - this_iter = model.get_iter(path) - dbid_list.append(model[this_iter][0]) - - # Prompt for confirmation - msg = _('Are you sure you want to remove the selected item(s)?') - - self.dialogue_manager_obj.show_msg_dialogue( - msg, - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'main_win_classic_mode_tab_remove_rows', - # Specified options - 'data': dbid_list, - }, - ) - - - def on_button_classic_stop(self, action, par): - - """Called from a callback in self.do_startup(). - - If a download operation is in progress, halts downloads for any of - the selected rows in the Classic Progress List. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - selection = self.main_win_obj.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # Get the .dbid of the dummy media.Video objects for each selected - # row - dbid_dict = {} - for path in path_list: - - this_iter = model.get_iter(path) - dbid_dict[model[this_iter][0]] = None - - # Now, if a download operation is in progress, stop any downloads - # matching one of these dbids - if self.download_manager_obj: - - for worker_obj in self.download_manager_obj.worker_list: - - if worker_obj.running_flag \ - and worker_obj.download_item_obj \ - and worker_obj.download_item_obj.media_data_obj.dbid \ - in dbid_dict: - worker_obj.downloader_obj.stop() - - - def on_button_custom_dl_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new (custom) download operation (if - allowed). - - Unlike the corresponding self.on_menu_custom_dl_all button, this - function will custom download only the marked items, if any. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - media_data_list = [] - for dbid in self.main_win_obj.video_index_marker_dict.keys(): - if dbid in self.container_reg_dict: - media_data_list.append(self.media_reg_dict[dbid]) - - # If additional custom download managers have been created, prompt the - # user to choose one of them - if self.check_custom_download_managers(): - return self.main_win_obj.custom_dl_popup_menu(media_data_list) - - # Otherwise, use the General Custom Download Manager - if not self.general_custom_dl_obj.dl_by_video_flag \ - or not self.general_custom_dl_obj.dl_precede_flag: - - self.download_manager_start( - 'custom_real', - False, # Not called by the timer - media_data_list, # Download all media data objects - self.general_custom_dl_obj, - ) - - else: - - self.download_manager_start( - 'custom_sim', - False, # Not called by the timer - media_data_list, # Download all media data objects - self.general_custom_dl_obj, - ) - - - def on_button_download_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new download operation (if allowed). - - Unlike the corresponding self.on_menu_download_all button, this - function will download only the marked items, if any. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - media_list = [] - for dbid in self.main_win_obj.video_index_marker_dict.keys(): - if dbid in self.container_reg_dict: - media_list.append(self.media_reg_dict[dbid]) - - self.download_manager_start( - 'real', - False, # Not called from self.script_slow_timer_callback() - media_list, # May be empty, in which case everything is downloaded - ) - - - def on_button_drag_drop_add(self, action, par): - - """Called from a callback in self.do_startup(). - - Adds a new dropzone in the Drag and Drop tab. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Open the popup menu - self.main_win_obj.drag_drop_add_dropzone() - - - def on_button_find_date(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the first one containing a video - whose upload time is the first one on or after date specified by the - user. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 189, - 'Find videos by date request failed sanity check', - ) - - # Prompt the user for a new calendar date - dialogue_win = mainwin.CalendarDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - if response == Gtk.ResponseType.OK: - date_tuple = dialogue_win.calendar.get_date() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and date_tuple: - - year = date_tuple[0] # e.g. 2011 - month = date_tuple[1] + 1 # Values in range 0-11 - day = date_tuple[2] # Values in range 1-31 - - # Convert the specified date into the epoch time at the start of - # that day - epoch_time = datetime.datetime(year, month, day, 0, 0).timestamp() - - # Get the channel, playlist or folder currently visible in the - # Video Catalogue - container_obj \ - = self.media_reg_dict[self.main_win_obj.video_index_current_dbid] - - count = 0 - - if self.catalogue_sort_mode == 'receive': - - # Sort by download time - for child_obj in container_obj.child_list: - - if isinstance(child_obj, media.Video) \ - and child_obj.receive_time is not None \ - and child_obj.receive_time < epoch_time: - break - - else: - count += 1 - - else: - - # Sort by upload time - for child_obj in container_obj.child_list: - - if isinstance(child_obj, media.Video) \ - and child_obj.upload_time is not None \ - and child_obj.upload_time < epoch_time: - break - - else: - count += 1 - - # (If the date is newer than all videos, then use the first video) - if count == 0: - count = 1 - - # Find the corresponding page in the Video Catalogue, and make it - # visible - self.main_win_obj.video_catalogue_show_date( - math.ceil(count / self.catalogue_page_size), - ) - - - def on_button_first_page(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the first one. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_button_hide_system(self, action, par): - - """Called from a callback in self.do_startup(). - - Show or hide (most) system folders, depending on whether the - togglebutton is selected, or not. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Toggle the menu item, which sets the IV (and updates the toolbar - # button) - if not self.main_win_obj.hide_system_menu_item.get_active(): - self.main_win_obj.hide_system_menu_item.set_active(True) - else: - self.main_win_obj.hide_system_menu_item.set_active(False) - - - def on_button_last_page(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the last one. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - self.main_win_obj.catalogue_toolbar_last_page, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_button_next_page(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the next one. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - self.main_win_obj.catalogue_toolbar_current_page + 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_button_previous_page(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the previous one. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - self.main_win_obj.catalogue_toolbar_current_page - 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_button_resort_catalogue(self, action, par): - - """Called from a callback in self.do_startup(). - - Forces a resort of the channel/playlist/folder visible in the Video - Catalogue. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if self.main_win_obj.video_index_current_dbid is not None: - self.main_win_obj.video_catalogue_force_resort() - - - def on_button_reverse_sort_catalogue(self, action, par): - - """Called from a callback in self.do_startup(). - - Enables or disables reverse sorting of the catalogue, and forces a - resort of the channel/playlist/folder visible in the Video Catalogue. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if not self.catalogue_reverse_sort_flag: - self.catalogue_reverse_sort_flag = True - else: - self.catalogue_reverse_sort_flag = False - - self.main_win_obj.update_catalogue_reverse_sort_widgets() - if self.main_win_obj.video_index_current_dbid is not None: - self.main_win_obj.video_catalogue_force_resort() - - - def on_button_scroll_down(self, action, par): - - """Called from a callback in self.do_startup(). - - Scrolls the Video Catalogue page to the bottom. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - adjust = self.main_win_obj.catalogue_scrolled.get_vadjustment() - adjust.set_value(adjust.get_upper()) - - - def on_button_scroll_up(self, action, par): - - """Called from a callback in self.do_startup(). - - Scrolls the Video Catalogue page to the top. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.catalogue_scrolled.get_vadjustment().set_value(0) - - - def on_button_show_filter(self, action, par): - - """Called from a callback in self.do_startup(). - - Reveals or hides another toolbar just below the Video Catalogue. The - additional toolbar contains filter options. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if not self.catalogue_show_filter_flag: - self.catalogue_show_filter_flag = True - else: - self.catalogue_show_filter_flag = False - - # Update the button in the Video Catalogue's toolbar - self.main_win_obj.update_catalogue_filter_widgets() - - - def on_button_stop_operation(self, action, par): - - """Called from a callback in self.do_startup(). - - Stops the current download/update/refresh/info/tidy operation (but not - livestream operations, which run in the background and are halted - immediately, if a different type of operation wants to start). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.operation_halted_flag = True - - # (The livestream operation runs silently in the background, so the - # toolbar button is desensitised and can't be used to stop it) - if self.download_manager_obj: - self.download_manager_obj.stop_download_operation() - elif self.update_manager_obj: - self.update_manager_obj.stop_update_operation() - elif self.refresh_manager_obj: - self.refresh_manager_obj.stop_refresh_operation() - elif self.info_manager_obj: - self.info_manager_obj.stop_info_operation() - elif self.tidy_manager_obj: - self.tidy_manager_obj.stop_tidy_operation() - elif self.process_manager_obj: - self.process_manager_obj.stop_process_operation() - - - def on_button_switch_view(self, action, par): - - """Called from a callback in self.do_startup(). - - Switches between Video Catalogue modes. Each mode specifies how videos - are displayed in the Video Catalogue, and what data is displayed for - each video. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # self.catalogue_mode_list provides an ordered list of values for - # self.catalogue_mode and self.catalogue_mode_type - # Find the current setting in the list, and switch to the next setting - catalogue_mode_list = self.catalogue_mode_list.copy() - while catalogue_mode_list: - - mini_list = catalogue_mode_list.pop(0) - if mini_list[0] == self.catalogue_mode: - - if catalogue_mode_list: - - # Not at the end of the list yet - next_mini_list = catalogue_mode_list[0] - self.catalogue_mode = next_mini_list[0] - self.catalogue_mode_type = next_mini_list[1] - - else: - - # Go back to the beginning of the list - first_mini_list = self.catalogue_mode_list[0] - self.catalogue_mode = first_mini_list[0] - self.catalogue_mode_type = first_mini_list[1] - - break - - # In case we are switching between two settings for videos displayed on - # a grid, reset the minimum gridbox sizes for each thumbnail size - self.main_win_obj.video_catalogue_grid_reset_sizes() - - # Redraw the Video Catalogue, but only if something was already drawn - # there (and keep the current page number) - if self.main_win_obj.video_index_current_dbid is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - self.main_win_obj.catalogue_toolbar_current_page, - ) - - - def on_button_use_regex(self, action, par): - - """Called from a callback in self.do_startup(). - - When the user clicks the Regex togglebutton in the toolbar just below - the Video Catalogue, updates IVs. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 190, - 'Use regex request failed sanity check', - ) - - if not self.main_win_obj.catalogue_regex_togglebutton.get_active(): - self.catalogue_use_regex_flag = False - else: - self.catalogue_use_regex_flag = True - - - def on_menu_about(self, action, par): - - """Called from a callback in self.do_startup(). - - Show a standard 'about' dialogue window. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - dialogue_win = Gtk.AboutDialog() - dialogue_win.set_transient_for(self.main_win_obj) - dialogue_win.set_destroy_with_parent(True) - - dialogue_win.set_program_name(__main__.__packagename__.title()) - dialogue_win.set_version('v' + __main__.__version__) - dialogue_win.set_copyright(__main__.__copyright__) - dialogue_win.set_license(__main__.__license__) - dialogue_win.set_website(__main__.__website__) - dialogue_win.set_website_label( - __main__.__packagename__.title() + ' website' - ) - dialogue_win.set_comments(__main__.__description__) - dialogue_win.set_logo( - self.main_win_obj.pixbuf_dict['system_icon'], - ) - dialogue_win.set_authors(__main__.__author_list__) - dialogue_win.add_credit_section('Credits', __main__.__credit_list__) - dialogue_win.set_title('') - dialogue_win.connect('response', self.on_menu_about_close) - - dialogue_win.show() - - - def on_menu_about_close(self, action, par): - - """Called from a callback in self.do_startup(). - - Close the 'about' dialogue window. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - action.destroy() - - - def on_menu_add_bulk(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify new channels/ - playlists. If any are specifed, creates new media.Channel and/or - media.Playlist objects. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # If a folder is selected in the Video Index, the dialogue window - # should suggest that as the new folder's parent folder - suggest_parent_dbid = None - if self.main_win_obj.video_index_current_dbid: - container_obj \ - = self.media_reg_dict[self.main_win_obj.video_index_current_dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.fixed_flag \ - and container_obj.restrict_mode != 'full': - suggest_parent_dbid = container_obj.dbid - - # Open the dialogue window - dialogue_win = mainwin.AddBulkDialogue( - self.main_win_obj, - suggest_parent_dbid, - ) - - response = dialogue_win.run() - - # Halt its clipboard timer, if running - if dialogue_win.clipboard_timer_id: - GObject.source_remove(dialogue_win.clipboard_timer_id) - - # Retrieve user choices from the dialogue window - if response == Gtk.ResponseType.OK: - - # Find the parent media data object (a media.Folder), if one was - # specified... - # if one was specified... - parent_dbid = None - if hasattr(dialogue_win, 'parent_dbid'): - parent_dbid = dialogue_win.parent_dbid - elif suggest_parent_dbid is not None: - parent_dbid = suggest_parent_dbid - - # Find the parent media data object (a media.Folder), if specified - parent_obj = None - if parent_dbid and parent_dbid in self.container_reg_dict: - parent_obj = self.media_reg_dict[parent_dbid] - - # Create each new channel/playlist - for row in dialogue_win.liststore: - - container_type = row[0] - container_name = row[2] - container_url = row[3] - - if container_type == 'channel': - - container_obj = self.add_channel( - container_name, - parent_obj, - container_url, - ) - - else: - - container_obj = self.add_playlist( - container_name, - parent_obj, - container_url, - ) - - # Add the channel/playlist to Video Index - if container_obj: - - if suggest_parent_dbid is not None \ - and suggest_parent_dbid \ - == self.main_win_obj.video_index_current_dbid: - # The container has been added to the currently - # selected folder; the True argument tells the - # function not to select the container - self.main_win_obj.video_index_add_row( - container_obj, - True, - ) - - else: - # Do select the new container - self.main_win_obj.video_index_add_row(container_obj) - - # ...before destroying the dialogue window - dialogue_win.destroy() - - - def on_menu_add_channel(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify a new channel. - If the user specifies a channel, creates a media.Channel object. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - dl_sim_flag = False - monitor_flag = False - # (The while loop below must be run at least once) - keep_open_flag = True - - # If a folder (but not a channel/playlist) is selected in the Video - # Index, use that as the dialogue window's suggested parent folder - suggest_parent_dbid = None - if self.main_win_obj.video_index_current_dbid: - container_obj \ - = self.media_reg_dict[self.main_win_obj.video_index_current_dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.fixed_flag \ - and container_obj.restrict_mode == 'open': - suggest_parent_dbid = container_obj.dbid - - while keep_open_flag: - - dialogue_win = mainwin.AddChannelDialogue( - self.main_win_obj, - suggest_parent_dbid, - dl_sim_flag, - monitor_flag, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - name = dialogue_win.entry.get_text() - source = dialogue_win.entry2.get_text() - dl_sim_flag = dialogue_win.radiobutton2.get_active() - monitor_flag = dialogue_win.checkbutton.get_active() - - # ...and find the .dbid of the parent media data object (a - # media.Folder), if one was specified... - parent_dbid = dialogue_win.parent_dbid - - # ...and halt the timer, if running - if dialogue_win.clipboard_timer_id: - GObject.source_remove(dialogue_win.clipboard_timer_id) - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - elif name is None or re.search(r'^\s*$', name): - - self.dialogue_manager_obj.show_msg_dialogue( - _('You must give the channel a name'), - 'error', - 'ok', - ) - - return - - elif not self.check_container_name_is_legal(name): - - self.dialogue_manager_obj.show_msg_dialogue( - _('The name \'{0}\' is not allowed').format(name), - 'error', - 'ok', - ) - - return - - elif not source or not utils.check_url(source): - - self.dialogue_manager_obj.show_msg_dialogue( - _('You must enter a valid URL'), - 'error', - 'ok', - ) - - return - - # (Re-open the window for further additions, if required) - keep_open_flag = self.dialogue_keep_open_flag - - # Remove leading/trailing whitespace from the name; make sure the - # name is not excessively long; reject illegal names - name = utils.tidy_up_container_name( - self, - name, - self.container_name_max_len, - ) - if name == '': - self.dialogue_manager_obj.show_msg_dialogue( - _('That name is not permitted on your system'), - 'error', - 'ok', - ) - - return - - # Find the parent media data object (a media.Folder), if specified - parent_obj = None - if parent_dbid and parent_dbid in self.container_reg_dict: - parent_obj = self.container_reg_dict[parent_dbid] - - if self.dialogue_keep_open_flag \ - and self.dialogue_keep_container_flag: - suggest_parent_dbid = parent_dbid - - # If there is no parent folder, there must be no containers in the - # top-level list with the same name - # If there is a parent folder, it must not contain a container with - # the same name - duplicate_obj = self.find_duplicate_name_in_container( - parent_obj, - name, - ) - if duplicate_obj: - self.reject_container_name(name, parent_obj, duplicate_obj) - return - - # Otherwise, create the new channel - channel_obj = self.add_channel( - name, - parent_obj, - source, - dl_sim_flag, - ) - - # Add the channel to Video Index - if channel_obj: - - if suggest_parent_dbid is not None \ - and suggest_parent_dbid \ - == self.main_win_obj.video_index_current_dbid: - - # The channel has been added to the currently selected - # folder; the True argument tells the function not to - # select the channel - self.main_win_obj.video_index_add_row(channel_obj, True) - - else: - - # Do select the new channel - self.main_win_obj.video_index_add_row(channel_obj) - - - def on_menu_add_folder(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify a new folder. - If the user specifies a folder, creates a media.Folder object. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # If a folder (but not a channel/playlist) is selected in the Video - # Index, use that as the dialogue window's suggested parent folder - suggest_parent_dbid = None - if self.main_win_obj.video_index_current_dbid: - container_obj \ - = self.media_reg_dict[self.main_win_obj.video_index_current_dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.fixed_flag \ - and container_obj.restrict_mode == 'open': - suggest_parent_dbid = container_obj.dbid - - dialogue_win = mainwin.AddFolderDialogue( - self.main_win_obj, - suggest_parent_dbid, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - name = dialogue_win.entry.get_text() - dl_sim_flag = dialogue_win.radiobutton2.get_active() - - # ...and find the .dbid of the parent media data object (a - # media.Folder), if one was specified... - parent_dbid = dialogue_win.parent_dbid - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - elif name is None or re.search(r'^\s*$', name): - - self.dialogue_manager_obj.show_msg_dialogue( - _('You must give the folder a name'), - 'error', - 'ok', - ) - - return - - elif not self.check_container_name_is_legal(name): - - self.dialogue_manager_obj.show_msg_dialogue( - _('The name \'{0}\' is not allowed').format(name), - 'error', - 'ok', - ) - - return - - # Remove leading/trailing whitespace from the name; make sure the name - # is not excessively long; reject illegal names - name = utils.tidy_up_container_name( - self, - name, - self.container_name_max_len, - ) - if name == '': - self.dialogue_manager_obj.show_msg_dialogue( - _('That name is not permitted on your system'), - 'error', - 'ok', - ) - - return - - # Find the parent media data object (a media.Folder), if specified - parent_obj = None - if parent_dbid and parent_dbid in self.container_reg_dict: - parent_obj = self.container_reg_dict[parent_dbid] - - # If there is no parent folder, there must be no containers in the - # top-level list with the same name - # If there is a parent folder, it must not contain a container with the - # same name - duplicate_obj = self.find_duplicate_name_in_container( - parent_obj, - name, - ) - if duplicate_obj: - self.reject_container_name(name, parent_obj, duplicate_obj) - return - - # Otherwise, create the new folder - folder_obj = self.add_folder(name, parent_obj, dl_sim_flag) - - # Add the folder to the Video Index - if folder_obj: - - if suggest_parent_dbid is not None \ - and suggest_parent_dbid \ - == self.main_win_obj.video_index_current_dbid: - - # The new folder has been added to the currently selected - # folder; the True argument tells the function not to - # select the new folder - self.main_win_obj.video_index_add_row(folder_obj, True) - - else: - - # Do select the new folder - self.main_win_obj.video_index_add_row(folder_obj) - - - def on_menu_add_playlist(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify a new playlist. - If the user specifies a playlist, creates a media.Playlist object. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - dl_sim_flag = False - monitor_flag = False - # (The while loop below must be run at least once) - keep_open_flag = True - - # If a folder (but not a channel/playlist) is selected in the Video - # Index, use that as the dialogue window's suggested parent folder - suggest_parent_dbid = None - if self.main_win_obj.video_index_current_dbid: - container_obj \ - = self.media_reg_dict[self.main_win_obj.video_index_current_dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.fixed_flag \ - and container_obj.restrict_mode == 'open': - suggest_parent_dbid = container_obj.dbid - - while keep_open_flag: - - dialogue_win = mainwin.AddPlaylistDialogue( - self.main_win_obj, - suggest_parent_dbid, - dl_sim_flag, - monitor_flag, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - name = dialogue_win.entry.get_text() - source = dialogue_win.entry2.get_text() - dl_sim_flag = dialogue_win.radiobutton2.get_active() - monitor_flag = dialogue_win.checkbutton.get_active() - - # ...and find the .dbid of the parent media data object (a - # media.Folder), if one was specified... - parent_dbid = dialogue_win.parent_dbid - - # ...and halt the timer, if running - if dialogue_win.clipboard_timer_id: - GObject.source_remove(dialogue_win.clipboard_timer_id) - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - elif name is None or re.search(r'^\s*$', name): - - self.dialogue_manager_obj.show_msg_dialogue( - _('You must give the playlist a name'), - 'error', - 'ok', - ) - - return - - elif not self.check_container_name_is_legal(name): - - self.dialogue_manager_obj.show_msg_dialogue( - _('The name \'{0}\' is not allowed').format(name), - 'error', - 'ok', - ) - - return - - elif not source or not utils.check_url(source): - - self.dialogue_manager_obj.show_msg_dialogue( - _('You must enter a valid URL'), - 'error', - 'ok', - ) - - return - - # (Re-open the window for further additions, if required) - keep_open_flag = self.dialogue_keep_open_flag - - # Remove leading/trailing whitespace from the name; make sure the - # name is not excessively long; reject illegal names - name = utils.tidy_up_container_name( - self, - name, - self.container_name_max_len, - ) - if name == '': - self.dialogue_manager_obj.show_msg_dialogue( - _('That name is not permitted on your system'), - 'error', - 'ok', - ) - - return - - # Find the parent media data object (a media.Folder), if specified - parent_obj = None - if parent_dbid and parent_dbid in self.container_reg_dict: - parent_obj = self.container_reg_dict[parent_dbid] - - if self.dialogue_keep_open_flag \ - and self.dialogue_keep_container_flag: - suggest_parent_dbid = parent_dbid - - # If there is no parent folder, there must be no containers in the - # top-level list with the same name - # If there is a parent folder, it must not contain a container with - # the same name - duplicate_obj = self.find_duplicate_name_in_container( - parent_obj, - name, - ) - if duplicate_obj: - self.reject_container_name(name, parent_obj, duplicate_obj) - return - - # Otherwise, create the new playlist - playlist_obj = self.add_playlist( - name, - parent_obj, - source, - dl_sim_flag, - ) - - # Add the playlist to Video Index - if playlist_obj: - - if suggest_parent_dbid is not None \ - and suggest_parent_dbid \ - == self.main_win_obj.video_index_current_dbid: - - # The playlist has been added to the currently selected - # folder; the True argument tells the function not to - # select the playlist - self.main_win_obj.video_index_add_row(playlist_obj, True) - - else: - - # Do select the new playlist - self.main_win_obj.video_index_add_row(playlist_obj) - - - def on_menu_add_video(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify one or more - videos. If the user supplies some URLs, creates media.Video objects. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - dialogue_win = mainwin.AddVideoDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - text = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - False, - ) - - dl_sim_flag = dialogue_win.radiobutton2.get_active() - - # ...and find the .dbid of the parent media data object (a - # media.Channel, media.Playlist or media.Folder)... - parent_dbid = dialogue_win.parent_dbid - parent_obj = self.media_reg_dict[parent_dbid] - - # ...and halt the timer, if running - if dialogue_win.clipboard_timer_id: - GObject.source_remove(dialogue_win.clipboard_timer_id) - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # Split text into a list of lines and filter out invalid URLs - video_list = [] - duplicate_list = [] - for line in text.splitlines(): - - # Remove leading/trailing whitespace - line = utils.strip_whitespace(line) - - # Perform checks on the URL. If it passes, remove leading/ - # trailing whitespace - if utils.check_url(line): - video_list.append(utils.strip_whitespace(line)) - - # Check everything in the list against other media.Video objects - # within the same parent folder - for line in video_list: - if parent_obj.check_duplicate_video(line): - duplicate_list.append(line) - else: - self.add_video(parent_obj, line, dl_sim_flag) - - # In the Video Index, select the parent media data object, which - # updates both the Video Index and the Video Catalogue - self.main_win_obj.video_index_select_row(parent_obj) - - # If any duplicates were found, inform the user - if duplicate_list: - dialogue_win = mainwin.DuplicateVideoDialogue( - self.main_win_obj, - duplicate_list, - ) - dialogue_win.run() - dialogue_win.destroy() - - - def on_menu_auto_switch(self, action, par): - - """Called from a callback in self.do_startup(). - - Sets the flag which switches to a profile on startup. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if not self.auto_switch_profile_flag: - self.auto_switch_profile_flag = True - else: - self.auto_switch_profile_flag = True - - - def on_menu_create_profile(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a profile to remember items marked in the Video Index. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Don't create profiles if nothing marked - if not self.main_win_obj.video_index_marker_dict: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'No channels, playlists or folders are marked for' \ - + ' download', - ), - 'error', - 'ok', - ) - - return - - # A maxmimum number of profiles applies - elif len(self.profile_dict) >= self.profile_max: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'The maximum number of profiles permitted is {0}', - ).format(self.profile_max), - 'error', - 'ok', - ) - - return - - # Prompt the user to choose a profile name - dialogue_win = mainwin.CreateProfileDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - profile_name = dialogue_win.profile_name - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK or profile_name is None: - return - - # Check for duplicate names - if profile_name in self.profile_dict: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'A profile called \'{0}\' already exists', - ).format(profile_name), - 'error', - 'ok', - ) - - return - - # Get a list of marked items in the Video Index - dbid_list = [] - for this_dbid in self.main_win_obj.video_index_marker_dict.keys(): - dbid_list.append(dbid) - - # Create the profile - self.add_profile(profile_name, dbid_list) - - # Show confirmation dialogue - self.dialogue_manager_obj.show_msg_dialogue( - _('Created the profile \'{0}\'').format(profile_name), - 'info', - 'ok', - ) - - - def on_menu_cancel_live(self, action, par): - - """Called from a callback in self.do_startup(). - - Cancels all livestream actions (auto-notify, auto-open, download at - start, download at stop). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # The actions are stored in five different dictionaries. Compile a - # single dictionary, eliminating duplicates, so we can count how - # many media.Video objects are affected (and updte the Video - # Catalogue) - video_dict = {} - - for video_obj in self.media_reg_auto_notify_dict.values(): - video_dict[video_obj.dbid] = video_obj - - for video_obj in self.media_reg_auto_alarm_dict.values(): - video_dict[video_obj.dbid] = video_obj - - for video_obj in self.media_reg_auto_open_dict.values(): - video_dict[video_obj.dbid] = video_obj - - for video_obj in self.media_reg_auto_dl_start_dict.values(): - video_dict[video_obj.dbid] = video_obj - - for video_obj in self.media_reg_auto_dl_stop_dict.values(): - video_dict[video_obj.dbid] = video_obj - - # Cancel livestream actions by emptying the IVs - self.media_reg_auto_notify_dict = {} - self.media_reg_auto_alarm_dict = {} - self.media_reg_auto_open_dict = {} - self.media_reg_auto_dl_start_dict = {} - self.media_reg_auto_dl_stop_dict = {} - - # Update the Video Catalogue - for video_obj in video_dict.values(): - GObject.timeout_add( - 0, - self.main_win_obj.video_catalogue_update_video, - video_obj, - ) - - # Show confirmation - count = len(video_dict) - if not count: - msg = _('There were no livestream alerts to cancel') - elif count == 1: - msg = _('Livestream alerts for 1 video were cancelled') - else: - msg = _( - 'Livestream alerts for {0} videos were cancelled', - ).format(str(count)) - - self.dialogue_manager_obj.show_msg_dialogue( - msg, - 'info', - 'ok', - None, # Parent window is main window - ) - - - def on_menu_change_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens the preference window at the right tab, so that the user can - switch databases. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - config.SystemPrefWin(self, 'db') - - - def on_menu_change_theme(self, action, par): - - """Called from a callback in self.do_startup(). - - On MS Windows (only), changes the Gtk theme (requires a restart to take - effect). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - dialogue_win = mainwin.ChangeThemeDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - theme_name = dialogue_win.theme_name - theme_path = dialogue_win.theme_path - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - success_msg = _('Restart Tartube to see the new theme!') - fail_msg = _('Tartube failed to set the new theme') - - dest_folder = self.script_parent_dir \ - + '/../../../mingw64/etc/gtk-3.0' - if not os.path.isdir(dest_folder): - self.make_directory(dest_folder) - - dest_path = os.path.abspath( - os.path.join(dest_folder, 'settings.ini'), - ) - - if theme_name == 'default': - - # The 'default' theme is characterised by a lack of a - # settings.ini file - try: - os.remove(dest_path) - self.dialogue_manager_obj.show_msg_dialogue( - success_msg, - 'info', - 'ok', - ) - - except Exception as e: - self.dialogue_manager_obj.show_msg_dialogue( - fail_msg + ': ' + str(e), - 'error', - 'ok', - ) - - else: - - # The dialogue window has already checked that this source_path - # exists - source_path = os.path.abspath( - os.path.join( - self.script_parent_dir, - 'pack', - 'mswin_themes', - theme_path, - 'settings.ini', - ), - ) - - try: - shutil.copyfile(source_path, dest_path) - self.dialogue_manager_obj.show_msg_dialogue( - success_msg, - 'info', - 'ok', - ) - - except Exception as e: - self.dialogue_manager_obj.show_msg_dialogue( - fail_msg + ': ' + str(e), - 'error', - 'ok', - ) - - - def on_menu_check_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Runs a database integrity check, without the need to open the - preference window first. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.check_integrity_db() - - - def on_menu_check_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new download operation (if allowed). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.download_manager_start('sim') - - - def on_menu_check_version(self, action, par): - - """Called from a callback in self.do_startup(). - - Check for Tartube updates. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.info_manager_start('version') - - - def on_menu_close_tray(self, action, par): - - """Called from a callback in self.do_startup(). - - Closes the main window to the system tray. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.toggle_visibility() - - - def on_menu_create_profile(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a profile to remember items marked in the Video Index. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Don't create profiles if nothing marked - if not self.main_win_obj.video_index_marker_dict: - - dialogue_win = self.dialogue_manager_obj.show_msg_dialogue( - _( - 'No channels, playlists or folders are marked for' \ - + ' download', - ), - 'error', - 'ok', - ) - - return - - # A maxmimum number of profiles applies - elif len(self.profile_dict) >= self.profile_max: - - dialogue_win = self.dialogue_manager_obj.show_msg_dialogue( - _( - 'The maximum number of profiles permitted is {0}', - ).format(self.profile_max), - 'error', - 'ok', - ) - - return - - # Prompt the user to choose a profile name - dialogue_win = mainwin.CreateProfileDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - profile_name = dialogue_win.profile_name - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK or profile_name is None: - return - - # Check for duplicate names - if profile_name in self.profile_dict: - - dialogue_win = self.dialogue_manager_obj.show_msg_dialogue( - _( - 'A profile called \'{0}\' already exists', - ).format(profile_name), - 'error', - 'ok', - ) - - return - - # Get a list of marked items in the Video Index - dbid_list = [] - for this_dbid in self.main_win_obj.video_index_marker_dict.keys(): - dbid_list.append(dbid) - - # Create the profile - self.add_profile(profile_name, dbid_list) - - # Show confirmation dialogue - self.dialogue_manager_obj.show_msg_dialogue( - _('Created the profile \'{0}\'').format(profile_name), - 'info', - 'ok', - ) - - - def on_menu_custom_dl_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new (custom) download operation (if - allowed). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # If additional custom download managers have been created, prompt the - # user to choose one of them - if self.check_custom_download_managers(): - return self.main_win_obj.custom_dl_popup_menu() - - # Otherwise, use the General Custom Download Manager - if not self.general_custom_dl_obj.dl_by_video_flag \ - or not self.general_custom_dl_obj.dl_precede_flag: - - self.download_manager_start( - 'custom_real', - False, # Not called by the timer - [], # Download all media data objects - self.general_custom_dl_obj, - ) - - else: - - self.download_manager_start( - 'custom_sim', - False, # Not called by the timer - [], # Download all media data objects - self.general_custom_dl_obj, - ) - - - def on_menu_download_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new download operation (if allowed). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.download_manager_start('real') - - - def on_menu_export_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Exports data from the Tartube database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.export_from_db( [] ) - - - def on_menu_general_options(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens an edit window for the General Options Manager. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - config.OptionsEditWin(self, self.general_options_obj) - - - def on_menu_go_website(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens the Tartube website. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - utils.open_file(self, __main__.__website__) - - - def on_menu_hide_system(self, action, par): - - """Called from a callback in self.do_startup(). - - Show or hide (most) system folders, depending on whether the menu item - is selected, or not. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Update the IV - if not self.main_win_obj.hide_system_menu_item.get_active(): - self.toolbar_system_hide_flag = False - else: - self.toolbar_system_hide_flag = True - - # Update the main window, showing/hiding system folders as necessary - self.main_win_obj.update_window_after_show_hide() - - - def on_menu_import_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Imports data into the Tartube database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.import_into_db() - - - def on_menu_import_yt(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a wizard window to import YouTube subscriptions. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - wizwin.ImportYTWizWin(self) - - - def on_menu_install_ffmpeg(self, action, par): - - """Called from a callback in self.do_startup(). - - Start an update operation to install FFmpeg (on MS Windows only). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.update_manager_start('ffmpeg') - - - def on_menu_install_matplotlib(self, action, par): - - """Called from a callback in self.do_startup(). - - Start an update operation to install matplotlib (on MS Windows only). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.update_manager_start('matplotlib') - - - def on_menu_install_streamlink(self, action, par): - - """Called from a callback in self.do_startup(). - - Start an update operation to install streamlink (on MS Windows only). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.update_manager_start('streamlink') - - - def on_menu_live_preferences(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens a preference window to edit livestream preferences. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - config.SystemPrefWin(self, 'live') - - - def on_menu_mark_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Marks all items in the Video Index. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.video_index_set_marker() - - - def on_menu_open_msys2(self, action, par): - - """Called from a callback in self.do_startup(). - - On MS Windows, opens the MINGW terminal for MSYS2. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if 'PROGRAMFILES(X86)' in os.environ: - utils.open_file(self, '..\\..\\..\\mingw64.exe') - else: - utils.open_file(self, '..\\..\\..\\mingw32.exe') - - if self.show_msys2_dialogue_flag: - - dialogue_win = mainwin.MSYS2Dialogue(self.main_win_obj) - dialogue_win.run() - dialogue_win.destroy() - - - def on_menu_refresh_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Starts a refresh operation. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.refresh_manager_start() - - - def on_menu_reset_container(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to reset channel/playlist - names in Tartube's database, replacing them with names gathered from - their child video's metadata (i.e. the original channel/playlist names - used on the site from which the videos were downloaded). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # If the user needs to check/download channels/playlists first, then - # tell them about it - if not self.media_reset_container_dict: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Before trying to reset channel/playlist names, you must' \ - + ' check or download at one least one video from each!', - ), - 'error', - 'ok', - None, # Parent window is main window - ) - - return - - # Open the dialogue window - dialogue_win = mainwin.ResetContainerDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, and reset any - # channel/playlist names as appropriate - if response != Gtk.ResponseType.OK: - - dialogue_win.destroy() - - else: - - for dbid in dialogue_win.reset_dict.keys(): - - if dialogue_win.reset_dict[dbid] \ - and dbid in self.media_reg_dict \ - and dbid in self.media_reset_container_dict: - - # (As well as using the name extracted from the child - # videos' metadata, the user can type a new name - # directly) - if dbid in dialogue_win.custom_dict: - new_name = dialogue_win.custom_dict[dbid] - else: - new_name = self.media_reset_container_dict[dbid] - - if self.rename_container_silently( - self.media_reg_dict[dbid], - new_name, - ): - # (Remove this channel/playlist from the IV, so if the - # user opens the dialogue window again, it's not - # present) - del self.media_reset_container_dict[dbid] - - # ...before destroying the dialogue window - dialogue_win.destroy() - - # Reset the Video Index (since any changed names will affect the - # order in which channels/playlists/folders are listed) - self.main_win_obj.video_index_catalogue_reset() - - - def on_menu_save_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Save the config file, and then the Tartube database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if not self.disable_load_save_flag: - self.save_config() - if not self.disable_load_save_flag: - self.save_db() - - # Show a dialogue window for confirmation (unless file load/save has - # been disabled, in which case a dialogue has already appeared) - if not self.disable_load_save_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - _('All Tartube data has been saved'), - 'info', - 'ok', - ) - - - def on_menu_save_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Save the Tartube database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.save_db() - - # Show a dialogue window for confirmation (unless file load/save has - # been disabled, in which case a dialogue has already appeared) - if not self.disable_load_save_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - _('Database saved'), - 'info', - 'ok', - ) - - - def on_menu_send_feedback(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens the Tartube feedback website. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - utils.open_file(self, __main__.__website_bugs__) - - - def on_menu_show_hidden(self, action, par): - - """Called from a callback in self.do_startup(). - - Un-hides all hidden media.Folder objects (and their children). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - for media_data_obj in self.container_reg_dict.values(): - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - self.mark_folder_hidden(media_data_obj, False) - - - def on_menu_show_install(self, action, par): - - """Called from a callback in self.do_startup(). - - On MS Windows (only), opens Tartube's installation folder. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # (This path assumes that the standard NSIS installation script was - # used to install Tartube) - utils.open_file( - self, - self.script_parent_dir + '\\..\\..\\..\\..\\..', - ) - - - def on_menu_show_script(self, action, par): - - """Called from a callback in self.do_startup(). - - On MS Windows (only), opens Tartube's home folder. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - utils.open_file(self, self.script_parent_dir) - - - def on_menu_stop_soon(self, action, par): - - """Called from a callback in self.do_startup(). - - Stops the current download operation as soon as the current videos - have finished being checked/downloaded. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Sanity check - if not self.download_manager_obj: - return self.system_error( - 191, - 'Stop download operation soon request failed sanity check', - ) - - # Tell the download manager to continue downloading the current videos - # (if any), and then stop - self.download_manager_obj.stop_download_operation_soon() - - - def on_menu_system_preferences(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens a preference window to edit system preferences. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - config.SystemPrefWin(self) - - - def on_menu_test(self, action, par): - - """Called from a callback in self.do_startup(). - - Add a set of media data objects for testing. This function can only be - called if the debugging flags are set. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Add media data objects for testing: videos, channels, playlists and/ - # or folders - testing.add_test_media(self) - - # Clicking the Test button more than once just adds illegal duplicate - # channels/playlists/folders (and non-illegal duplicate videos), so - # just disable the button and the menu item - self.main_win_obj.desensitise_test_widgets() - - # Redraw the video catalogue, if a Video Index row is selected - if self.main_win_obj.video_index_current_dbid is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - ) - - - def on_menu_test_code(self, action, par): - - """Called from a callback in self.do_startup(). - - Executes some arbitrary test code. This function can only be called if - the debugging flags are set. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - result = testing.run_test_code(self) - - self.dialogue_manager_obj.show_simple_msg_dialogue( - 'Test code executed\n\nResult: ' + str(result), - 'info', - 'ok', - None, # Parent window is main window - ) - - - def on_menu_test_ytdl(self, action, par): - - """Called from a callback in self.do_startup(). - - Start an info operation to test a youtube-dl command. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Prompt the user for what should be tested - dialogue_win = mainwin.TestCmdDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - url_string = dialogue_win.entry.get_text() - options_string = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - False, - ) - - # ...before destroying it - dialogue_win.destroy() - - # If the user specified either (or both) a URL and youtube-dl options, - # then we can proceed - if response == Gtk.ResponseType.OK \ - and (url_string != '' or options_string != ''): - - # Start the info operation, which issues the youtube-dl command - # with the specified options - self.info_manager_start( - 'test_ytdl', - None, # No media.Video object in this case - url_string, # Use the source, if specified - options_string, # Use download options, if specified - ) - - - def on_menu_tidy_up(self, action, par): - - """Called from a callback in self.do_startup(). - - Start a tidy operation to tidy up Tartube's data directory. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Prompt the user to specify which actions should be applied to - # Tartube's data directory - dialogue_win = mainwin.TidyDialogue(self.main_win_obj) - response = dialogue_win.run() - - if response == Gtk.ResponseType.OK: - - # Retrieve user choices from the dialogue window - # N.B. Any changes to this section must be copied to - # mainwin.MainWin.on_video_index_tidy - choices_dict = { - 'media_data_obj': None, - 'corrupt_flag': dialogue_win.checkbutton.get_active(), - 'del_corrupt_flag': dialogue_win.checkbutton2.get_active(), - 'exist_flag': dialogue_win.checkbutton3.get_active(), - 'del_video_flag': dialogue_win.checkbutton4.get_active(), - 'del_others_flag': dialogue_win.checkbutton5.get_active(), - 'remove_no_url_flag': dialogue_win.checkbutton6.get_active(), - 'remove_duplicate_flag': \ - dialogue_win.checkbutton7.get_active(), - 'del_archive_flag': dialogue_win.checkbutton8.get_active(), - 'move_thumb_flag': dialogue_win.checkbutton9.get_active(), - 'del_thumb_flag': dialogue_win.checkbutton10.get_active(), - 'del_webp_flag': dialogue_win.checkbutton11.get_active(), - 'convert_webp_flag': dialogue_win.checkbutton12.get_active(), - 'move_data_flag': dialogue_win.checkbutton13.get_active(), - 'del_descrip_flag': dialogue_win.checkbutton14.get_active(), - 'del_json_flag': dialogue_win.checkbutton15.get_active(), - 'del_xml_flag': dialogue_win.checkbutton16.get_active(), - 'convert_ext_flag': dialogue_win.checkbutton17.get_active(), - } - - # Now destroy the window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # If nothing was selected, then there is nothing to do - selected_flag = False - for key in choices_dict.keys(): - if choices_dict[key]: - selected_flag = True - break - - if not selected_flag: - return - - # Prompt the user for confirmation, before deleting any files - if choices_dict['del_corrupt_flag'] \ - or choices_dict['del_video_flag'] \ - or choices_dict['del_archive_flag'] \ - or choices_dict['del_thumb_flag'] \ - or choices_dict['del_webp_flag'] \ - or choices_dict['del_descrip_flag'] \ - or choices_dict['del_json_flag'] \ - or choices_dict['del_xml_flag']: - - self.dialogue_manager_obj.show_msg_dialogue( - _( - 'Files cannot be recovered, after being deleted. Are you' \ - + ' sure you want to continue?', - ), - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'tidy_manager_start', - # Specified options - 'data': choices_dict, - }, - ) - - else: - - # Start the tidy operation now - self.tidy_manager_start(choices_dict) - - - def on_menu_tutorial(self, action, par): - - """Called from a callback in self.do_startup(). - - Show the tutorial wizard window. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - wizwin.TutorialWizWin(self) - - - def on_menu_unmark_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Unmarks all items in the Video Index. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.main_win_obj.video_index_reset_marker() - - - def on_menu_update_live(self, action, par): - - """Called from a callback in self.do_startup(). - - Forces the livestream operation to start. Ignored if any operation - (including an existing livestream operation) is running. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - # Because livestream operations run silently in the background, when - # the user goes to the trouble of clicking a menu item in the - # main window's menu, tell them why nothing is happening - msg = _('Cannot update existing livestreams because') - if (self.current_manager_obj and not self.download_manager_obj): - msg += ' ' + _('there is another operation running') - elif self.livestream_manager_obj: - msg += ' ' + _('they are currently being updated') - elif self.main_win_obj.config_win_list: - msg += ' ' + _('one or more configuration windows are open') - elif not self.media_reg_live_dict: - msg += ' ' + _('there are no livestreams to update') - else: - self.livestream_manager_start() - return - - self.dialogue_manager_obj.show_msg_dialogue(msg, 'error', 'ok') - - - def on_menu_update_ytdl(self, action, par): - - """Called from a callback in self.do_startup(). - - Start an update operation to update the system's youtube-dl. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.update_manager_start('ytdl') - - - def on_menu_quit(self, action, par): - - """Called from a callback in self.do_startup(). - - Terminates the Tartube app. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - self.stop() - - - # (Callback support functions) - - - def reject_container_name(self, name, parent_obj, duplicate_obj): - - """Called by self.on_menu_add_channel(), .on_menu_add_playlist() - and .on_menu_add_folder(). - - If the user specifies a name for a channel, playlist or folder that's - already in use by another container in the same parent folder (or in - the top level list), tell them why they can't use that name. - - Args: - - name (str): The name specified by the user - - parent_obj (media.Folder or None): The parent folder, or None if - duplicate_obj is in the top-level list - - duplicate_obj (media.Channel, media.Playlist, media.Folder): A - container with the same name, and in the same parent folder (or - top-level list) - - """ - - media_type = duplicate_obj.get_type() - - if not parent_obj: - - if media_type == 'channel': - msg = _('There is already a channel with that name') - elif media_type == 'playlist': - msg = _('There is already a playlist with that name') - else: - msg = _('There is already a folder with that name') - - else: - - if media_type == 'channel': - msg = _('The folder already contains a channel with that name') - - elif media_type == 'playlist': - msg = _( - 'The folder already contains a playlist with that name' - ) - - else: - msg = _( - 'The folder already contains another folder with that name' - ) - - self.dialogue_manager_obj.show_msg_dialogue( - msg + ' ' + _('(so please choose a different name)'), - 'error', - 'ok', - ) - - - # Set accessors - - - def set_add_blocked_videos_flag(self, flag): - - if not flag: - self.add_blocked_videos_flag = False - else: - self.add_blocked_videos_flag = True - - - def set_allow_ytdl_archive_flag(self, flag): - - if not flag: - self.allow_ytdl_archive_flag = False - else: - self.allow_ytdl_archive_flag = True - - - def set_allow_ytdl_archive_mode(self, value): - - self.allow_ytdl_archive_mode = value - - - def set_allow_ytdl_archive_path(self, value): - - self.allow_ytdl_archive_path = value - - - def set_alt_bandwidth(self, value): - - self.alt_bandwidth = value - - - def set_alt_bandwidth_apply_flag(self, flag): - - if not flag: - self.alt_bandwidth_apply_flag = False - else: - self.alt_bandwidth_apply_flag = True - - - def set_alt_day_string(self, value): - - self.alt_day_string = value - - - def set_alt_num_worker(self, value): - - self.alt_num_worker = value - - - def set_alt_num_worker_apply_flag(self, flag): - - if not flag: - self.alt_num_worker_apply_flag = False - else: - self.alt_num_worker_apply_flag = True - - - def set_alt_start_time(self, value): - - self.alt_start_time = value - - - def set_alt_stop_time(self, value): - - self.alt_stop_time = value - - - def set_apply_json_timeout_flag(self, flag): - - if not flag: - self.apply_json_timeout_flag = False - else: - self.apply_json_timeout_flag = True - - - def add_auto_alarm_dict(self, video_obj): - - self.media_reg_auto_alarm_dict[video_obj.dbid] = video_obj - - - def del_auto_alarm_dict(self, video_obj): - - if video_obj.dbid in self.media_reg_auto_alarm_dict: - del self.media_reg_auto_alarm_dict[video_obj.dbid] - - - def set_auto_assign_errors_warnings_flag(self, flag): - - if not flag: - self.auto_assign_errors_warnings_flag = False - else: - self.auto_assign_errors_warnings_flag = True - - - def set_auto_clone_options_flag(self, flag): - - if not flag: - self.auto_clone_options_flag = False - else: - self.auto_clone_options_flag = True - - - def set_auto_delete_asap_flag(self, flag): - - if not flag: - self.auto_delete_asap_flag = False - else: - self.auto_delete_asap_flag = True - - - def set_auto_delete_flag(self, flag): - - if not flag: - self.auto_delete_flag = False - else: - self.auto_delete_flag = True - - - def set_auto_delete_days(self, days): - - self.auto_delete_days = days - - - def set_auto_delete_options_flag(self, flag): - - if not flag: - self.auto_delete_options_flag = False - else: - self.auto_delete_options_flag = True - - - def set_auto_delete_watched_flag(self, flag): - - if not flag: - self.auto_delete_watched_flag = False - else: - self.auto_delete_watched_flag = True - - - def set_auto_expand_video_index_flag(self, flag): - - if not flag: - self.auto_expand_video_index_flag = False - else: - self.auto_expand_video_index_flag = True - - - def add_auto_dl_start_dict(self, video_obj): - - self.media_reg_auto_dl_start_dict[video_obj.dbid] = video_obj - - - def del_auto_dl_start_dict(self, video_obj): - - if video_obj.dbid in self.media_reg_auto_dl_start_dict: - del self.media_reg_auto_dl_start_dict[video_obj.dbid] - - - def add_auto_dl_stop_dict(self, video_obj): - - self.media_reg_auto_dl_stop_dict[video_obj.dbid] = video_obj - - - def del_auto_dl_stop_dict(self, video_obj): - - if video_obj.dbid in self.media_reg_auto_dl_stop_dict: - del self.media_reg_auto_dl_stop_dict[video_obj.dbid] - - - def add_auto_notify_dict(self, video_obj): - - self.media_reg_auto_notify_dict[video_obj.dbid] = video_obj - - - def del_auto_notify_dict(self, video_obj): - - if video_obj.dbid in self.media_reg_auto_notify_dict: - del self.media_reg_auto_notify_dict[video_obj.dbid] - - - def add_auto_open_dict(self, video_obj): - - self.media_reg_auto_open_dict[video_obj.dbid] = video_obj - - - def del_auto_open_dict(self, video_obj): - - if video_obj.dbid in self.media_reg_auto_open_dict: - del self.media_reg_auto_open_dict[video_obj.dbid] - - - def set_auto_remove_flag(self, flag): - - if not flag: - self.auto_remove_flag = False - else: - self.auto_remove_flag = True - - - def set_auto_remove_days(self, days): - - self.auto_remove_days = days - - - def set_auto_switch_output_flag(self, flag): - - if not flag: - self.auto_switch_output_flag = False - else: - self.auto_switch_output_flag = True - - - def set_autostop_size_flag(self, flag): - - if not flag: - self.autostop_size_flag = False - else: - self.autostop_size_flag = True - - - def set_autostop_size_unit(self, value): - - self.autostop_size_unit = value - - - def set_autostop_size_value(self, value): - - self.autostop_size_value = value - - - def set_autostop_time_flag(self, flag): - - if not flag: - self.autostop_time_flag = False - else: - self.autostop_time_flag = True - - - def set_autostop_time_unit(self, value): - - self.autostop_time_unit = value - - - def set_autostop_time_value(self, value): - - self.autostop_time_value = value - - - def set_autostop_videos_flag(self, flag): - - if not flag: - self.autostop_videos_flag = False - else: - self.autostop_videos_flag = True - - - def set_autostop_videos_value(self, value): - - self.autostop_videos_value = value - - - def set_avconv_path(self, path): - - self.avconv_path = path - - - def set_bandwidth_apply_flag(self, flag): - - """Called by mainwin.MainWin.on_bandwidth_checkbutton_changed(). - - Applies or releases the bandwidth limit. If a download operation is in - progress, the new setting is applied to the next download job. - """ - - if not flag: - self.bandwidth_apply_flag = False - else: - self.bandwidth_apply_flag = True - - - def set_bandwidth_default(self, value): - - """Called by mainwin.MainWin.on_bandwidth_spinbutton_changed(). - - Sets the new bandwidth limit. If a download operation is in progress, - the new value is applied to the next download job. - """ - - if value < self.bandwidth_min or value > self.bandwidth_max: - return self.system_error( - 192, - 'Set bandwidth request failed sanity check', - ) - - self.bandwidth_default = value - - - def set_block_livestreams_flag(self, flag): - - if not flag: - self.block_livestreams_flag = False - else: - self.block_livestreams_flag = True - - - def set_catalogue_draw_blocked_flag(self, flag): - - if not flag: - self.catalogue_draw_blocked_flag = False - else: - self.catalogue_draw_blocked_flag = True - - - def set_catalogue_draw_downloaded_flag(self, flag): - - if not flag: - self.catalogue_draw_downloaded_flag = False - else: - self.catalogue_draw_downloaded_flag = True - - - def set_catalogue_draw_frame_flag(self, flag): - - if not flag: - self.catalogue_draw_frame_flag = False - else: - self.catalogue_draw_frame_flag = True - - - def set_catalogue_draw_icons_flag(self, flag): - - if not flag: - self.catalogue_draw_icons_flag = False - else: - self.catalogue_draw_icons_flag = True - - - def set_catalogue_draw_undownloaded_flag(self, flag): - - if not flag: - self.catalogue_draw_undownloaded_flag = False - else: - self.catalogue_draw_undownloaded_flag = True - - - def set_catalogue_filter_comment_flag(self, flag): - - if not flag: - self.catalogue_filter_comment_flag = False - else: - self.catalogue_filter_comment_flag = True - - - def set_catalogue_filter_descrip_flag(self, flag): - - if not flag: - self.catalogue_filter_descrip_flag = False - else: - self.catalogue_filter_descrip_flag = True - - - def set_catalogue_filter_name_flag(self, flag): - - if not flag: - self.catalogue_filter_name_flag = False - else: - self.catalogue_filter_name_flag = True - - - def set_catalogue_page_size(self, size): - - self.catalogue_page_size = size - - - def set_classic_custom_dl_flag(self, flag): - - if not flag: - self.classic_custom_dl_flag = False - else: - self.classic_custom_dl_flag = True - - - def set_classic_dir_previous(self, directory): - - self.classic_dir_previous = directory - - - def set_classic_duplicate_remove_flag(self, flag): - - if not flag: - self.classic_duplicate_remove_flag = False - else: - self.classic_duplicate_remove_flag = True - - - def toggle_classic_pending_flag(self): - - if not self.classic_pending_flag: - self.classic_pending_flag = True - else: - self.classic_pending_flag = False - - - def set_catalogue_clickable_container_flag(self, flag): - - if not flag: - self.catalogue_clickable_container_flag = False - else: - self.catalogue_clickable_container_flag = True - - # Re-draw the Video Catalogue to implement the new setting - if self.main_win_obj.video_index_current_dbid is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - ) - - - def set_catalogue_mode(self, catalogue_mode, catalogue_mode_type): - - self.catalogue_mode = catalogue_mode - self.catalogue_mode_type = catalogue_mode_type - - # In case we are switching between two settings for videos displayed on - # a grid, reset the minimum gridbox sizes for each thumbnail size - self.main_win_obj.video_catalogue_grid_reset_sizes() - # Redraw the Video Catalogue, but only if something was already drawn - # there (and keep the current page number) - if self.main_win_obj.video_index_current_dbid is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - self.main_win_obj.catalogue_toolbar_current_page, - ) - - - def set_catalogue_show_nickname_flag(self, flag): - - if not flag: - self.catalogue_show_nickname_flag = False - else: - self.catalogue_show_nickname_flag = True - - # Re-draw the Video Catalogue to implement the new setting - if self.main_win_obj.video_index_current_dbid is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - ) - - - def set_catalogue_sort_mode(self, mode): - - self.catalogue_sort_mode = mode - - - def set_check_comment_fetch_flag(self, flag): - - if not flag: - self.check_comment_fetch_flag = False - else: - self.check_comment_fetch_flag = True - - - def add_classic_dropzone_list(self, value): - - self.classic_dropzone_list.append(value) - - - def del_classic_dropzone_list(self, value): - - self.classic_dropzone_list.remove(value) - - - def set_classic_format_convert_flag(self, flag): - - if not flag: - self.classic_format_convert_flag = False - else: - self.classic_format_convert_flag = True - - - def set_classic_format_selection(self, value): - - self.classic_format_selection = value - - - def set_classic_livestream_flag(self, flag): - - if not flag: - self.classic_livestream_flag = False - else: - self.classic_livestream_flag = True - - - def set_classic_resolution_selection(self, value): - - self.classic_resolution_selection = value - - - def set_classic_sblock_flag(self, flag): - - if not flag: - self.classic_sblock_flag = False - else: - self.classic_sblock_flag = True - - - def set_classic_ytdl_archive_flag(self, flag): - - if not flag: - self.classic_ytdl_archive_flag = False - else: - self.classic_ytdl_archive_flag = True - - - def set_close_to_tray_flag(self, flag): - - if not flag: - self.close_to_tray_flag = False - else: - self.close_to_tray_flag = True - - - def set_comment_show_formatted_flag(self, flag): - - if not flag: - self.comment_show_formatted_flag = False - else: - self.comment_show_formatted_flag = True - - - def set_comment_show_text_time_flag(self, flag): - - if not flag: - self.comment_show_text_time_flag = False - else: - self.comment_show_text_time_flag = True - - - def set_comment_store_flag(self, flag): - - if not flag: - self.comment_store_flag = False - else: - self.comment_store_flag = True - - - def set_complex_index_flag(self, flag): - - if not flag: - self.complex_index_flag = False - else: - self.complex_index_flag = True - - - def set_custom_bg(self, key, red, green, blue, alpha): - - self.custom_bg_table[key] = [ red, green, blue, alpha ] - - # Update the main window IV - self.main_win_obj.setup_bg_colour(key) - - - def reset_custom_bg(self, key): - - self.custom_bg_table[key] = self.default_bg_table[key] - - # Update the main window IV - self.main_win_obj.setup_bg_colour(key) - - - def set_custom_invidious_mirror(self, value): - - self.custom_invidious_mirror = value - - # The Video Catalogue must be redrawn to reset labels (but not when - # SimpleCatalogueItem are visible) - if self.catalogue_mode_type != 'simple' \ - and self.main_win_obj.video_index_current_dbid is not None: - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - self.main_win_obj.catalogue_toolbar_current_page, - ) - - - def reset_custom_invidious_mirror(self): - - self.custom_invidious_mirror = self.default_invidious_mirror - - # The Video Catalogue must be redrawn to reset labels (but not when - # SimpleCatalogueItems are visible) - if self.catalogue_mode_type != 'simple' \ - and self.main_win_obj.video_index_current_dbid is not None: - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - self.main_win_obj.catalogue_toolbar_current_page, - ) - - - def set_custom_sblock_mirror(self, value): - - self.custom_sblock_mirror = value - - - def reset_custom_sblock_mirror(self): - - self.custom_sblock_mirror = self.default_sblock_mirror - - - def set_data_dir(self, path): - - """Called by mainwin.MountDriveDialogue.on_button_clicked() and - wizwin.SetupWizWin.apply_changes() only; everything else should call - self.switch_db(). - - The call to this function resets the value of self.data_dir without - actually loading the database. - """ - - self.data_dir = path - - - def reset_data_dir(self): - - """Called by mainwin.MountDriveDialogue.on_button_clicked() only; - everything else should call self.switch_db(). - - The call to this function resets the value of self.data_dir without - actually loading the database. - """ - - self.data_dir = self.default_data_dir - - - def set_data_dir_add_from_list_flag(self, flag): - - if not flag: - self.data_dir_add_from_list_flag = False - else: - self.data_dir_add_from_list_flag = True - - - def set_data_dir_alt_list(self, dir_list): - - self.data_dir_alt_list = dir_list.copy() - - - def set_data_dir_use_first_flag(self, flag): - - if not flag: - self.data_dir_use_first_flag = False - else: - self.data_dir_use_first_flag = True - - - def set_data_dir_use_list_flag(self, flag): - - if not flag: - self.data_dir_use_list_flag = False - else: - self.data_dir_use_list_flag = True - - - def set_db_backup_mode(self, value): - - self.db_backup_mode = value - - - def set_delete_container_files_flag(self, flag): - - if not flag: - self.delete_container_files_flag = False - else: - self.delete_container_files_flag = True - - - def set_delete_on_shutdown_flag(self, flag): - - if not flag: - self.delete_on_shutdown_flag = False - else: - self.delete_on_shutdown_flag = True - - - def set_delete_video_files_flag(self, flag): - - if not flag: - self.delete_video_files_flag = False - else: - self.delete_video_files_flag = True - - - def set_dialogue_copy_clipboard_flag(self, flag): - - if not flag: - self.dialogue_copy_clipboard_flag = False - else: - self.dialogue_copy_clipboard_flag = True - - - def set_dialogue_disable_msg_flag(self, flag): - - if not flag: - self.dialogue_disable_msg_flag = False - else: - self.dialogue_disable_msg_flag = True - - - def set_dialogue_keep_open_flag(self, flag): - - if not flag: - self.dialogue_keep_open_flag = False - else: - self.dialogue_keep_open_flag = True - - - def set_dialogue_yt_remind_flag(self, flag): - - if not flag: - self.dialogue_yt_remind_flag = False - else: - self.dialogue_yt_remind_flag = True - - - def set_disable_dl_all_flag(self, flag): - - if not flag: - self.disable_dl_all_flag = False - self.main_win_obj.enable_dl_all_buttons() - - else: - self.disable_dl_all_flag = True - self.main_win_obj.disable_dl_all_buttons() - - - def set_disk_space_stop_flag(self, flag): - - if not flag: - self.disk_space_stop_flag = False - else: - self.disk_space_stop_flag = True - - - def set_disk_space_stop_limit(self, value): - - self.disk_space_stop_limit = value - - - def set_disk_space_warn_flag(self, flag): - - if not flag: - self.disk_space_warn_flag = False - else: - self.disk_space_warn_flag = True - - - def set_disk_space_warn_limit(self, value): - - self.disk_space_warn_limit = value - - - def set_dl_comment_fetch_flag(self, flag): - - if not flag: - self.dl_comment_fetch_flag = False - else: - self.dl_comment_fetch_flag = True - - - def set_dl_proxy_list(self, proxy_list): - - self.dl_proxy_list = proxy_list.copy() - - - def set_drag_error_msg_flag(self, flag): - - if not flag: - self.drag_error_msg_flag = False - else: - self.drag_error_msg_flag = True - - - def set_drag_error_name_flag(self, flag): - - if not flag: - self.drag_error_name_flag = False - else: - self.drag_error_name_flag = True - - - def set_drag_error_path_flag(self, flag): - - if not flag: - self.drag_error_path_flag = False - else: - self.drag_error_path_flag = True - - - def set_drag_error_separator_flag(self, flag): - - if not flag: - self.drag_error_separator_flag = False - else: - self.drag_error_separator_flag = True - - - def set_drag_error_source_flag(self, flag): - - if not flag: - self.drag_error_source_flag = False - else: - self.drag_error_source_flag = True - - - def set_drag_thumb_path_flag(self, flag): - - if not flag: - self.drag_thumb_path_flag = False - else: - self.drag_thumb_path_flag = True - - - def set_drag_video_msg_flag(self, flag): - - if not flag: - self.drag_video_msg_flag = False - else: - self.drag_video_msg_flag = True - - - def set_drag_video_name_flag(self, flag): - - if not flag: - self.drag_video_name_flag = False - else: - self.drag_video_name_flag = True - - - def set_drag_video_path_flag(self, flag): - - if not flag: - self.drag_video_path_flag = False - else: - self.drag_video_path_flag = True - - - def set_drag_video_separator_flag(self, flag): - - if not flag: - self.drag_video_separator_flag = False - else: - self.drag_video_separator_flag = True - - - def set_drag_video_source_flag(self, flag): - - if not flag: - self.drag_video_source_flag = False - else: - self.drag_video_source_flag = True - - - def set_enable_livestreams_flag(self, flag): - - if not flag: - self.enable_livestreams_flag = False - else: - self.enable_livestreams_flag = True - - - def set_export_csv_separator(self, value): - - self.export_csv_separator = value - - - def set_ffmpeg_convert_webp_flag(self, flag): - - if not flag: - self.ffmpeg_convert_webp_flag = False - else: - self.ffmpeg_convert_webp_flag = True - - - def set_ffmpeg_fail_flag(self, flag): - - if not flag: - self.ffmpeg_fail_flag = False - else: - self.ffmpeg_fail_flag = True - - - def set_ffmpeg_options_obj(self, obj): - - self.ffmpeg_options_obj = obj - - - def set_ffmpeg_path(self, path): - - self.ffmpeg_path = path - - - def set_ffmpeg_retain_webp_flag(self, flag): - - if not flag: - self.ffmpeg_retain_webp_flag = False - else: - self.ffmpeg_retain_webp_flag = True - - - def set_simple_ffmpeg_options_flag(self, flag): - - if not flag: - self.simple_ffmpeg_options_flag = False - else: - self.simple_ffmpeg_options_flag = True - - - def set_fixed_recent_folder_days(self, value): - - self.fixed_recent_folder_days = value - - - def set_full_expand_video_index_flag(self, flag): - - if not flag: - self.full_expand_video_index_flag = False - else: - self.full_expand_video_index_flag = True - - - def set_graph_values(self, combo_type, value): - - if combo_type == 'data_type': - self.graph_data_type = value - elif combo_type == 'plot_type': - self.graph_plot_type = value - elif combo_type == 'time_period': - self.graph_time_period_secs = value - elif combo_type == 'time_unit': - self.graph_time_unit_secs = value - elif combo_type == 'ink_colour': - self.graph_ink_colour = value - - - def set_ignore_child_process_exit_flag(self, flag): - - if not flag: - self.ignore_child_process_exit_flag = False - else: - self.ignore_child_process_exit_flag = True - - - def set_ignore_custom_msg_list(self, custom_list): - - self.ignore_custom_msg_list = custom_list.copy() - - - def set_ignore_custom_regex_flag(self, flag): - - if not flag: - self.ignore_custom_regex_flag = False - else: - self.ignore_custom_regex_flag = True - - - def set_ignore_data_block_error_flag(self, flag): - - if not flag: - self.ignore_data_block_error_flag = False - else: - self.ignore_data_block_error_flag = True - - - def set_ignore_http_404_error_flag(self, flag): - - if not flag: - self.ignore_http_404_error_flag = False - else: - self.ignore_http_404_error_flag = True - - - def set_ignore_merge_warning_flag(self, flag): - - if not flag: - self.ignore_merge_warning_flag = False - else: - self.ignore_merge_warning_flag = True - - - def set_ignore_missing_format_error_flag(self, flag): - - if not flag: - self.ignore_missing_format_error_flag = False - else: - self.ignore_missing_format_error_flag = True - - - def set_ignore_no_annotations_flag(self, flag): - - if not flag: - self.ignore_no_annotations_flag = False - else: - self.ignore_no_annotations_flag = True - - - def set_ignore_no_descrip_flag(self, flag): - - if not flag: - self.ignore_no_descrip_flag = False - else: - self.ignore_no_descrip_flag = True - - - def set_ignore_no_subtitles_flag(self, flag): - - if not flag: - self.ignore_no_subtitles_flag = False - else: - self.ignore_no_subtitles_flag = True - - - def set_ignore_page_given_flag(self, flag): - - if not flag: - self.ignore_page_given_flag = False - else: - self.ignore_page_given_flag = True - - - def set_ignore_thumb_404_flag(self, flag): - - if not flag: - self.ignore_thumb_404_flag = False - else: - self.ignore_thumb_404_flag = True - - - def set_ignore_yt_age_restrict_mode(self, value): - - self.ignore_yt_age_restrict_mode = value - - - def set_ignore_yt_copyright_flag(self, flag): - - if not flag: - self.ignore_yt_copyright_flag = False - else: - self.ignore_yt_copyright_flag = True - - - def set_ignore_yt_payment_flag(self, flag): - - if not flag: - self.ignore_yt_payment_flag = False - else: - self.ignore_yt_payment_flag = True - - - def set_ignore_yt_uploader_deleted_flag(self, flag): - - if not flag: - self.ignore_yt_uploader_deleted_flag = False - else: - self.ignore_yt_uploader_deleted_flag = True - - - def set_json_timeout_no_comments_time(self, value): - - self.json_timeout_no_comments_time = value - - - def set_json_timeout_with_comments_time(self, value): - - self.json_timeout_with_comments_time = value - - - def set_last_profile(self, value): - - self.last_profile = value - - - def set_livestream_max_days(self, value): - - self.livestream_max_days = value - - - def set_livestream_auto_alarm_flag(self, flag): - - if not flag: - self.livestream_auto_alarm_flag = False - else: - self.livestream_auto_alarm_flag = True - - - def set_livestream_auto_dl_start_flag(self, flag): - - if not flag: - self.livestream_auto_dl_start_flag = False - else: - self.livestream_auto_dl_start_flag = True - - - def set_livestream_auto_dl_stop_flag(self, flag): - - if not flag: - self.livestream_auto_dl_stop_flag = False - else: - self.livestream_auto_dl_stop_flag = True - - - def set_livestream_auto_notify_flag(self, flag): - - if not flag: - self.livestream_auto_notify_flag = False - else: - self.livestream_auto_notify_flag = True - - - def set_livestream_auto_open_flag(self, flag): - - if not flag: - self.livestream_auto_open_flag = False - else: - self.livestream_auto_open_flag = True - - - def set_livestream_dl_mode(self, value): - - self.livestream_dl_mode = value - - - def set_livestream_dl_timeout(self, value): - - self.livestream_dl_timeout = value - - - def set_livestream_force_check_flag(self, flag): - - if not flag: - self.livestream_force_check_flag = False - else: - self.livestream_force_check_flag = True - - - def set_livestream_replace_flag(self, flag): - - if not flag: - self.livestream_replace_flag = False - else: - self.livestream_replace_flag = True - - - def set_livestream_simple_colour_flag(self, flag): - - if not flag: - self.livestream_simple_colour_flag = False - else: - self.livestream_simple_colour_flag = True - - - def set_livestream_stop_is_final_flag(self, flag): - - if not flag: - self.livestream_stop_is_final_flag = False - else: - self.livestream_stop_is_final_flag = True - - - def set_livestream_use_colour_flag(self, flag): - - if not flag: - self.livestream_use_colour_flag = False - else: - self.livestream_use_colour_flag = True - - - def set_main_win_save_size_flag(self, flag): - - if not flag: - self.main_win_save_size_flag = False - self.main_win_save_width = self.main_win_width - self.main_win_save_height = self.main_win_height - self.main_win_videos_slider_posn = self.paned_default_size - self.main_win_progress_slider_posn = self.paned_default_size - self.main_win_classic_slider_posn = self.paned_default_size - - else: - self.main_win_save_size_flag = True - - - def set_main_win_save_slider_flag(self, flag): - - if not flag: - self.main_win_save_slider_flag = False - self.main_win_videos_slider_posn = self.paned_default_size - self.main_win_progress_slider_posn = self.paned_default_size - self.main_win_classic_slider_posn = self.paned_default_size - else: - self.main_win_save_slider_flag = True - - - def set_main_win_slider_reset_flag(self, flag): - - if not flag: - self.main_win_slider_reset_flag = False - else: - self.main_win_slider_reset_flag = True - - - def set_match_first_chars(self, num_chars): - - self.match_first_chars = num_chars - - - def set_match_ignore_chars(self, num_chars): - - self.match_ignore_chars = num_chars - - - def set_match_method(self, method): - - self.match_method = method - - - def set_match_nickname_flag(self, flag): - - if not flag: - self.match_nickname_flag = False - else: - self.match_nickname_flag = True - - - def del_container_unavailable_dict(self, name): - - del self.container_unavailable_dict[name] - - - def add_media_reg_live_vanished_dict(self, video_obj): - - self.media_reg_live_vanished_dict[video_obj.dbid] = video_obj - - - def set_num_worker_apply_flag(self, flag): - - """Called by mainwin.MainWin.on_num_worker_checkbutton_changed(). - - Applies or releases the simultaneous download limit. If a download - operation is in progress, the new setting is applied to the next - download job. - """ - - if not flag: - self.num_worker_apply_flag = False - else: - self.num_worker_apply_flag = True - - - def set_num_worker_default(self, value): - - """Called by mainwin.MainWin.on_num_worker_spinbutton_changed() and - .on_num_worker_checkbutton_changed(). - - Sets the new value for the number of simultaneous downloads allowed. If - a download operation is in progress, informs the download manager - object, so the number of download workers can be adjusted. Also - increases the number of pages in the Output tab, if necessary. - """ - - if value < self.num_worker_min or value > self.num_worker_max: - return self.system_error( - 193, - 'Set simultaneous downloads request failed sanity check', - ) - - old_value = self.num_worker_default - self.num_worker_default = value - - if old_value != value \ - and self.download_manager_obj \ - and not self.download_manager_obj.alt_limits_flag: - self.download_manager_obj.change_worker_count(value) - - if value > self.main_win_obj.output_page_count: - self.main_win_obj.output_tab_setup_pages() - - - def set_num_worker_bypass_flag(self, flag): - - if not flag: - self.num_worker_bypass_flag = False - else: - self.num_worker_bypass_flag = True - - - def set_open_temp_on_desktop_flag(self, flag): - - if not flag: - self.open_temp_on_desktop_flag = False - else: - self.open_temp_on_desktop_flag = True - - - def set_open_in_tray_flag(self, flag): - - if not flag: - self.open_in_tray_flag = False - else: - self.open_in_tray_flag = True - - - def set_operation_auto_restart_flag(self, flag): - - if not flag: - self.operation_auto_restart_flag = False - else: - self.operation_auto_restart_flag = True - - - def set_operation_auto_restart_max(self, value): - - self.operation_auto_restart_max = value - - - def set_operation_auto_restart_time(self, value): - - self.operation_auto_restart_time = value - - - def set_operation_auto_update_flag(self, flag): - - if not flag: - self.operation_auto_update_flag = False - else: - self.operation_auto_update_flag = True - - - def set_operation_check_limit(self, value): - - self.operation_check_limit = value - - - def set_operation_convert_mode(self, mode): - - if mode == 'disable' or mode == 'multi' or mode == 'channel' \ - or mode == 'playlist': - self.operation_convert_mode = mode - - - def set_operation_dialogue_mode(self, mode): - - if mode == 'default' or mode == 'desktop' or mode == 'dialogue': - self.operation_dialogue_mode = mode - - - def set_operation_download_limit(self, value): - - self.operation_download_limit = value - - - def set_operation_error_show_flag(self, flag): - - if not flag: - self.operation_error_show_flag = False - else: - self.operation_error_show_flag = True - - - def set_operation_halted_flag(self, flag): - - if not flag: - self.operation_halted_flag = False - else: - self.operation_halted_flag = True - - - def set_operation_limit_flag(self, flag): - - if not flag: - self.operation_limit_flag = False - else: - self.operation_limit_flag = True - - - def set_operation_save_flag(self, flag): - - if not flag: - self.operation_save_flag = False - else: - self.operation_save_flag = True - - - def set_operation_sim_shortcut_flag(self, flag): - - if not flag: - self.operation_sim_shortcut_flag = False - else: - self.operation_sim_shortcut_flag = True - - - def set_operation_warning_show_flag(self, flag): - - if not flag: - self.operation_warning_show_flag = False - else: - self.operation_warning_show_flag = True - - - def set_output_size_apply_flag(self, flag): - - if not flag: - self.output_size_apply_flag = False - else: - self.output_size_apply_flag = True - - - def set_output_size_default(self, value): - - if value < self.output_size_min or value > self.output_size_max: - return self.system_error( - 194, - 'Set Output tab page size request failed sanity check', - ) - - old_value = self.output_size_default - self.output_size_default = value - - if self.output_size_apply_flag: - self.main_win_obj.output_tab_update_page_size() - - - def set_override_local(self, value): - - self.override_locale = value - - - def reset_override_local(self): - - self.override_locale = None - - - def set_progress_list_hide_flag(self, flag): - - if not flag: - self.progress_list_hide_flag = False - else: - self.progress_list_hide_flag = True - # If a download operation is in progress, hide any hideable rows - # immediately - if self.download_manager_obj: - self.main_win_obj.progress_list_check_hide_rows(True) - - - def set_progress_list_remember_width_flag(self, flag): - - if not flag: - self.progress_list_remember_width_flag = False - self.progress_list_width_source = None - self.progress_list_width_incoming = None - self.results_list_width_video = None - self.classic_progress_list_width_source = None - self.classic_progress_list_width_incoming = None - - else: - self.progress_list_remember_width_flag = True - # (self.progress_list_width_source, etc, are set by - # self.save_config() ) - - - def set_refresh_moviepy_timeout(self, value): - - self.refresh_moviepy_timeout = value - - - def set_refresh_output_verbose_flag(self, flag): - - if not flag: - self.refresh_output_verbose_flag = False - else: - self.refresh_output_verbose_flag = True - - - def set_refresh_output_videos_flag(self, flag): - - if not flag: - self.refresh_output_videos_flag = False - else: - self.refresh_output_videos_flag = True - - - def set_restore_posn_from_tray_flag(self, flag): - - if not flag: - self.restore_posn_from_tray_flag = False - else: - self.restore_posn_from_tray_flag = True - - - def set_results_list_reverse_flag(self, flag): - - if not flag: - self.results_list_reverse_flag = False - else: - self.results_list_reverse_flag = True - - - def set_sblock_fetch_flag(self, flag): - - if not flag: - self.sblock_fetch_flag = False - else: - self.sblock_fetch_flag = True - - - def set_sblock_obfuscate_flag(self, flag): - - if not flag: - self.sblock_obfuscate_flag = False - else: - self.sblock_obfuscate_flag = True - - - def set_sblock_re_extract_flag(self, flag): - - if not flag: - self.sblock_re_extract_flag = False - else: - self.sblock_re_extract_flag = True - - - def set_sblock_replace_flag(self, flag): - - if not flag: - self.sblock_replace_flag = False - else: - self.sblock_replace_flag = True - - - def add_scheduled_list(self, scheduled_obj): - - self.scheduled_list.append(scheduled_obj) - - - def del_scheduled_list(self, data_list): - - scheduled_obj = data_list[0] - edit_win = data_list[1] - - if scheduled_obj in self.scheduled_list: - self.scheduled_list.remove(scheduled_obj) - - if edit_win is not None: - edit_win.setup_scheduling_start_tab_update_treeview() - - - def move_scheduled_list(self, name, flag): - - """'flag' is False to move an item up the list, True to move it down - the list.""" - - for scheduled_obj in self.scheduled_list: - if scheduled_obj.name == name: - - index = self.scheduled_list.index(scheduled_obj) - if not flag and index > 0: - - self.scheduled_list.insert( - index - 1, - self.scheduled_list.pop(index) - ) - - elif flag and index < (len(self.scheduled_list) - 1): - - self.scheduled_list.insert( - index + 1, - self.scheduled_list.pop(index) - ) - - break - - - def set_scheduled_livestream_flag(self, flag): - - if not flag: - self.scheduled_livestream_flag = False - else: - self.scheduled_livestream_flag = True - - - def set_scheduled_livestream_extra_flag(self, flag): - - if not flag: - self.scheduled_livestream_extra_flag = False - else: - self.scheduled_livestream_extra_flag = True - - - def set_scheduled_livestream_wait_mins(self, value): - - self.scheduled_livestream_wait_mins = value - - - def set_simple_options_flag(self, flag): - - if not flag: - self.simple_options_flag = False - else: - self.simple_options_flag = True - - - def set_simple_prefs_flag(self, flag): - - if not flag: - self.simple_prefs_flag = False - else: - self.simple_prefs_flag = True - - - def set_show_classic_tab_on_startup_flag(self, flag): - - if not flag: - self.show_classic_tab_on_startup_flag = False - else: - self.show_classic_tab_on_startup_flag = True - - - def set_show_custom_dl_button_flag(self, flag): - - if not flag: - self.show_custom_dl_button_flag = False - else: - self.show_custom_dl_button_flag = True - - - def set_show_custom_icons_flag(self, flag): - - if not flag: - self.show_custom_icons_flag = False - else: - self.show_custom_icons_flag = True - - - def set_show_delete_container_dialogue_flag(self, flag): - - if not flag: - self.show_delete_container_dialogue_flag = False - else: - self.show_delete_container_dialogue_flag = True - - - def set_show_delete_video_dialogue_flag(self, flag): - - if not flag: - self.show_delete_video_dialogue_flag = False - else: - self.show_delete_video_dialogue_flag = True - - - def set_show_free_space_flag(self, flag): - - if not flag: - self.show_free_space_flag = False - else: - self.show_free_space_flag = True - - - def set_show_msys2_dialogue_flag(self, flag): - - if not flag: - self.show_msys2_dialogue_flag = False - else: - self.show_msys2_dialogue_flag = True - - - def set_show_newbie_dialogue_flag(self, flag): - - if not flag: - self.show_newbie_dialogue_flag = False - else: - self.show_newbie_dialogue_flag = True - - - def set_show_pretty_dates_flag(self, flag): - - if not flag: - self.show_pretty_dates_flag = False - else: - self.show_pretty_dates_flag = True - - # Redraw the Video Catalogue, but only if something was already drawn - # there (and keep the current page number) - if self.main_win_obj.video_index_current_dbid is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current_dbid, - self.main_win_obj.catalogue_toolbar_current_page, - ) - - - def set_show_marker_in_index_flag(self, flag): - - if not flag: - self.show_marker_in_index_flag = False - else: - self.show_marker_in_index_flag = True - - # Reset all markers in the Video Index - self.main_win_obj.video_index_reset_marker() - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - - def set_show_small_icons_in_index_flag(self, flag): - - if not flag: - self.show_small_icons_in_index_flag = False - else: - self.show_small_icons_in_index_flag = True - - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - - def set_show_status_icon_flag(self, flag): - - """Called by config.SystemPrefWin.on_show_status_icon_toggled(). - - Shows/hides the status icon in the system tray. - """ - - if not flag: - self.show_status_icon_flag = False - if self.status_icon_obj: - self.status_icon_obj.hide_icon() - - else: - self.show_status_icon_flag = True - if self.status_icon_obj: - self.status_icon_obj.show_icon() - - - def set_show_tooltips_flag(self, flag): - - if not flag: - self.show_tooltips_flag = False - # (The True argument forces the Video Catalogue to be redrawn) - self.main_win_obj.disable_tooltips(True) - - else: - self.show_tooltips_flag = True - self.main_win_obj.enable_tooltips(True) - - - def set_show_tooltips_extra_flag(self, flag): - - if not flag: - self.show_tooltips_extra_flag = False - else: - self.show_tooltips_extra_flag = True - - - def set_slice_video_cleanup_flag(self, flag): - - if not flag: - self.slice_video_cleanup_flag = False - else: - self.slice_video_cleanup_flag = True - - - def set_slice_video_force_keyframe_flag(self, flag): - - if not flag: - self.slice_video_force_keyframe_flag = False - else: - self.slice_video_force_keyframe_flag = True - - - def set_sound_custom(self, value): - - self.sound_custom = value - - - def set_split_video_add_db_flag(self, flag): - - if not flag: - self.split_video_add_db_flag = False - else: - self.split_video_add_db_flag = True - - - def set_split_video_auto_delete_flag(self, flag): - - if not flag: - self.split_video_auto_delete_flag = False - else: - self.split_video_auto_delete_flag = True - - - def set_split_video_auto_open_flag(self, flag): - - if not flag: - self.split_video_auto_open_flag = False - else: - self.split_video_auto_open_flag = True - - - def set_split_video_clips_dir_flag(self, flag): - - if not flag: - self.split_video_clips_dir_flag = False - else: - self.split_video_clips_dir_flag = True - - - def set_split_video_copy_thumb_flag(self, flag): - - if not flag: - self.split_video_copy_thumb_flag = False - else: - self.split_video_copy_thumb_flag = True - - - def set_split_video_custom_title(self, value): - - self.split_video_custom_title = value - - - def set_split_video_force_keyframe_flag(self, flag): - - if not flag: - self.split_video_force_keyframe_flag = False - else: - self.split_video_force_keyframe_flag = True - - - def set_split_video_name_mode(self, value): - - self.split_video_name_mode = value - - - def set_split_video_subdir_flag(self, flag): - - if not flag: - self.split_video_subdir_flag = False - else: - self.split_video_subdir_flag = True - - - def set_store_playlist_id_flag(self, flag): - - if not flag: - self.store_playlist_id_flag = False - else: - self.store_playlist_id_flag = True - - - def set_streamlink_path(self, value): - - self.streamlink_path = value - - - def set_system_error_show_flag(self, flag): - - if not flag: - self.system_error_show_flag = False - else: - self.system_error_show_flag = True - - - def set_system_msg_keep_totals_flag(self, flag): - - if not flag: - self.system_msg_keep_totals_flag = False - else: - self.system_msg_keep_totals_flag = True - - - def set_system_msg_show_container_flag(self, flag): - - if not flag: - self.system_msg_show_container_flag = False - else: - self.system_msg_show_container_flag = True - - - def set_system_msg_show_date_flag(self, flag): - - if not flag: - self.system_msg_show_date_flag = False - else: - self.system_msg_show_date_flag = True - - - def set_system_msg_show_multi_line_flag(self, flag): - - if not flag: - self.system_msg_show_multi_line_flag = False - else: - self.system_msg_show_multi_line_flag = True - - - def set_system_msg_show_video_flag(self, flag): - - if not flag: - self.system_msg_show_video_flag = False - else: - self.system_msg_show_video_flag = True - - - def set_system_warning_show_flag(self, flag): - - if not flag: - self.system_warning_show_flag = False - else: - self.system_warning_show_flag = True - - - def add_temp_output_override_dict(self, dbid, name): - - self.temp_output_override_dict[dbid] = name - - - def del_temp_output_override_dict(self, dbid): - - del self.temp_output_override_dict[dbid] - - - def add_temp_slice_buffer_dict(self, dbid, mini_list): - - self.temp_slice_buffer_dict[dbid] = mini_list - - - def del_temp_slice_buffer_dict(self, dbid): - - del self.temp_slice_buffer_dict[dbid] - - - def add_temp_stamp_buffer_dict(self, dbid, mini_list): - - self.temp_stamp_buffer_dict[dbid] = mini_list - - - def del_temp_stamp_buffer_dict(self, dbid): - - del self.temp_stamp_buffer_dict[dbid] - - - def set_thumb_size_custom(self, value): - - self.thumb_size_custom = value - - - def set_toolbar_hide_flag(self, flag): - - if not flag: - self.toolbar_hide_flag = False - else: - self.toolbar_hide_flag = True - - - def set_toolbar_squeeze_flag(self, flag): - - if not flag: - self.toolbar_squeeze_flag = False - else: - self.toolbar_squeeze_flag = True - - if self.main_win_obj and self.main_win_obj.main_toolbar \ - and not self.toolbar_hide_flag: - self.main_win_obj.redraw_main_toolbar() - - - def set_track_missing_time_days(self, value): - - self.track_missing_time_days = value - - - def set_track_missing_time_flag(self, flag): - - if not flag: - self.track_missing_time_flag = False - else: - self.track_missing_time_flag = True - - - def set_track_missing_videos_flag(self, flag): - - if not flag: - self.track_missing_videos_flag = False - else: - self.track_missing_videos_flag = True - - - def set_url_change_confirm_flag(self, flag): - - if not flag: - self.url_change_confirm_flag = False - else: - self.url_change_confirm_flag = True - - - def set_url_change_regex_flag(self, flag): - - if not flag: - self.url_change_regex_flag = False - else: - self.url_change_regex_flag = True - - - def set_use_module_moviepy_flag(self, flag): - - if not flag: - self.use_module_moviepy_flag = False - else: - self.use_module_moviepy_flag = True - - - def set_video_res_apply_flag(self, flag): - - """Called by mainwin.MainWin.on_video_res_checkbutton_changed(). - - Applies or releases the video resolution limit. If a download operation - is in progress, the new setting is applied to the next download job. - """ - - if not flag: - self.video_res_apply_flag = False - else: - self.video_res_apply_flag = True - - - def set_video_res_default(self, value): - - """Called by mainwin.MainWin.set_video_res_limit() and - .on_video_res_combobox_changed()(). - - Sets the new video resolution limit. If a download operation is in - progress, the new value is applied to the next download job. - - Args: - - value (str): The new video resolution limit (a key in - formats.VIDEO_RESOLUTION_DICT, e.g. '720p') - - """ - - if not value in formats.VIDEO_RESOLUTION_DICT: - return self.system_error( - 195, - 'Set video resolution request failed sanity check', - ) - - self.video_res_default = value - - - def set_video_timestamps_dl_mode(self, value): - - self.video_timestamps_dl_mode = value - - - def set_video_timestamps_extract_descrip_flag(self, flag): - - if not flag: - self.video_timestamps_extract_descrip_flag = False - else: - self.video_timestamps_extract_descrip_flag = True - - - def set_video_timestamps_extract_json_flag(self, flag): - - if not flag: - self.video_timestamps_extract_json_flag = False - else: - self.video_timestamps_extract_json_flag = True - - - def set_video_timestamps_re_extract_flag(self, flag): - - if not flag: - self.video_timestamps_re_extract_flag = False - else: - self.video_timestamps_re_extract_flag = True - - - def set_video_timestamps_replace_flag(self, flag): - - if not flag: - self.video_timestamps_replace_flag = False - else: - self.video_timestamps_replace_flag = True - - - def set_ytdl_fork(self, value): - - self.ytdl_fork = value - - # Update main window menu items - self.main_win_obj.update_menu() - - - def set_ytdl_fork_no_dependency_flag(self, flag): - - if not flag: - self.ytdl_fork_no_dependency_flag = False - else: - self.ytdl_fork_no_dependency_flag = True - - - def set_ytdl_log_ignore_json_flag(self, flag): - - if not flag: - self.ytdl_log_ignore_json_flag = False - else: - self.ytdl_log_ignore_json_flag = True - - - def set_ytdl_log_ignore_progress_flag(self, flag): - - if not flag: - self.ytdl_log_ignore_progress_flag = False - else: - self.ytdl_log_ignore_progress_flag = True - - - def set_ytdl_log_stderr_flag(self, flag): - - if not flag: - self.ytdl_log_stderr_flag = False - else: - self.ytdl_log_stderr_flag = True - - - def set_ytdl_log_stdout_flag(self, flag): - - if not flag: - self.ytdl_log_stdout_flag = False - else: - self.ytdl_log_stdout_flag = True - - - def set_ytdl_log_system_cmd_flag(self, flag): - - if not flag: - self.ytdl_log_system_cmd_flag = False - else: - self.ytdl_log_system_cmd_flag = True - - - def set_ytdl_output_ignore_json_flag(self, flag): - - if not flag: - self.ytdl_output_ignore_json_flag = False - else: - self.ytdl_output_ignore_json_flag = True - - - def set_ytdl_output_ignore_progress_flag(self, flag): - - if not flag: - self.ytdl_output_ignore_progress_flag = False - else: - self.ytdl_output_ignore_progress_flag = True - - - def set_ytdl_output_show_summary_flag(self, flag): - - if not flag: - self.ytdl_output_show_summary_flag = False - else: - self.ytdl_output_show_summary_flag = True - - - def set_ytdl_output_start_empty_flag(self, flag): - - if not flag: - self.ytdl_output_start_empty_flag = False - else: - self.ytdl_output_start_empty_flag = True - - - def set_ytdl_output_stderr_flag(self, flag): - - if not flag: - self.ytdl_output_stderr_flag = False - else: - self.ytdl_output_stderr_flag = True - - - def set_ytdl_output_stdout_flag(self, flag): - - if not flag: - self.ytdl_output_stdout_flag = False - else: - self.ytdl_output_stdout_flag = True - - - def set_ytdl_output_system_cmd_flag(self, flag): - - if not flag: - self.ytdl_output_system_cmd_flag = False - else: - self.ytdl_output_system_cmd_flag = True - - - def set_ytdl_path(self, path): - - self.ytdl_path = path - - - def set_ytdl_path_custom_flag(self, flag): - - if not flag: - self.ytdl_path_custom_flag = False - else: - self.ytdl_path_custom_flag = True - - - def set_ytdl_update_current(self, string): - - self.ytdl_update_current = string - - - def set_ytdl_write_ignore_json_flag(self, flag): - - if not flag: - self.ytdl_write_ignore_json_flag = False - else: - self.ytdl_write_ignore_json_flag = True - - - def set_ytdl_write_ignore_progress_flag(self, flag): - - if not flag: - self.ytdl_write_ignore_progress_flag = False - else: - self.ytdl_write_ignore_progress_flag = True - - - def set_ytdl_write_stderr_flag(self, flag): - - if not flag: - self.ytdl_write_stderr_flag = False - else: - self.ytdl_write_stderr_flag = True - - - def set_ytdl_write_stdout_flag(self, flag): - - if not flag: - self.ytdl_write_stdout_flag = False - else: - self.ytdl_write_stdout_flag = True - - - def set_ytdl_write_system_cmd_flag(self, flag): - - if not flag: - self.ytdl_write_system_cmd_flag = False - else: - self.ytdl_write_system_cmd_flag = True - - - def set_ytdl_write_verbose_flag(self, flag): - - if not flag: - self.ytdl_write_verbose_flag = False - else: - self.ytdl_write_verbose_flag = True diff --git a/build/lib/tartube/mainwin.py b/build/lib/tartube/mainwin.py deleted file mode 100644 index bcac33c6..00000000 --- a/build/lib/tartube/mainwin.py +++ /dev/null @@ -1,39151 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Main window class and related classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, Gdk, GdkPixbuf - - -# Import other modules -import datetime -import functools -from gi.repository import Gio -import os -import platform -from gi.repository import Pango -import math -import re -import sys -import threading -import time -import urllib.parse - - -# Import our modules -import config -import formats -import html -import __main__ -import mainapp -import media -import options -import utils -import wizwin -# Use same gettext translations -from mainapp import _ - -# (Desktop notifications don't work on MS Windows/MacOS, so no need to import -# Notify) -if mainapp.HAVE_NOTIFY_FLAG: - gi.require_version('Notify', '0.7') - from gi.repository import Notify - - -# Classes -class MainWin(Gtk.ApplicationWindow): - - """Called by mainapp.TartubeApp.start(). - - Python class that handles the main window. - - The main window has three tabs - the Videos tab, the Progress tab and the - Errors tab. - - In the Videos tab, the Video Index is visible on the left, and the Video - Catalogue is visible on the right. - - In the Progress tab, the Progress List is visible at the top, and the - Results List is visible at the bottom. - - In the Errors tab, any errors generated by youtube-dl are displayed. (The - display is not reset at the beginning of every download operation). - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - super(MainWin, self).__init__( - title=__main__.__packagename__.title(), - application=app_obj - ) - - # IV list - class objects - # ----------------------- - # The main application - self.app_obj = app_obj - - - # IV list - Gtk widgets - # --------------------- - # (from self.setup_grid) - self.grid = None # Gtk.Grid - # (from self.setup_main_menubar) - self.menubar = None # Gtk.MenuBar - self.change_db_menu_item = None # Gtk.MenuItem - self.check_db_menu_item = None # Gtk.MenuItem - self.save_db_menu_item = None # Gtk.MenuItem - self.save_all_menu_item = None # Gtk.MenuItem - self.system_prefs_menu_item = None # Gtk.MenuItem - self.open_msys2_menu_item = None # Gtk.MenuItem - self.show_install_menu_item = None # Gtk.MenuItem - self.show_script_menu_item = None # Gtk.MenuItem - self.change_theme_menu_item = None # Gtk.MenuItem - self.add_video_menu_item = None # Gtk.MenuItem - self.add_channel_menu_item = None # Gtk.MenuItem - self.add_playlist_menu_item = None # Gtk.MenuItem - self.add_folder_menu_item = None # Gtk.MenuItem - self.add_bulk_menu_item = None # Gtk.MenuItem - self.export_db_menu_item = None # Gtk.MenuItem - self.import_db_menu_item = None # Gtk.MenuItem - self.import_yt_menu_item = None # Gtk.MenuItem - self.switch_view_menu_item = None # Gtk.MenuItem - self.hide_system_menu_item = None # Gtk.MenuItem - self.show_hidden_menu_item = None # Gtk.MenuItem - self.show_hide_menu_item = None # Gtk.MenuItem - self.switch_profile_menu_item = None # Gtk.MenuItem - self.auto_switch_menu_item = None # Gtk.MenuItem - self.create_profile_menu_item = None # Gtk.MenuItem - self.delete_profile_menu_item = None # Gtk.MenuItem - self.profile_menu_item = None # Gtk.MenuItem - self.mark_containers_menu_item = None # Gtk.MenuItem - self.unmark_containers_menu_item = None # Gtk.MenuItem - self.test_menu_item = None # Gtk.MenuItem - self.test_code_menu_item = None # Gtk.MenuItem - self.check_all_menu_item = None # Gtk.MenuItem - self.download_all_menu_item = None # Gtk.MenuItem - self.custom_dl_all_menu_item = None # Gtk.MenuItem - self.refresh_db_menu_item = None # Gtk.MenuItem - self.update_ytdl_menu_item = None # Gtk.MenuItem - self.test_ytdl_menu_item = None # Gtk.MenuItem - self.install_ffmpeg_menu_item = None # Gtk.MenuItem - self.install_matplotlib_menu_item = None - # Gtk.MenuItem - self.install_streamlink_menu_item = None - # Gtk.MenuItem - self.tidy_up_menu_item = None # Gtk.MenuItem - self.stop_operation_menu_item = None # Gtk.MenuItem - self.stop_soon_menu_item = None # Gtk.MenuItem - self.live_prefs_menu_item = None # Gtk.MenuItem - self.update_live_menu_item = None # Gtk.MenuItem - self.cancel_live_menu_item = None # Gtk.MenuItem - # (from self.setup_main_toolbar) - self.main_toolbar = None # Gtk.Toolbar - self.add_video_toolbutton = None # Gtk.ToolButton - self.add_channel_toolbutton = None # Gtk.ToolButton - self.add_playlist_toolbutton = None # Gtk.ToolButton - self.add_folder_toolbutton = None # Gtk.ToolButton - self.check_all_toolbutton = None # Gtk.ToolButton - self.download_all_toolbutton = None # Gtk.ToolButton - self.stop_operation_toolbutton = None # Gtk.ToolButton - self.system_preferences_toolbutton = None - # Gtk.ToolButton - self.general_options_toolbutton = None # Gtk.ToolButton - self.switch_view_toolbutton = None # Gtk.ToolButton - self.hide_system_toolbutton = None # Gtk.ToolButton - self.test_toolbutton = None # Gtk.ToolButton - # (from self.setup_notebook) - self.notebook = None # Gtk.Notebook - self.videos_tab = None # Gtk.Box - self.videos_label = None # Gtk.Label - self.progress_tab = None # Gtk.Box - self.progress_label = None # Gtk.Label - self.classic_tab = None # Gtk.Box - self.classic_label = None # Gtk.Label - self.drag_drop_tab = None # Gtk.Box - self.drag_drop_label = None # Gtk.Label - self.output_tab = None # Gtk.Box - self.output_label = None # Gtk.Label - self.errors_tab = None # Gtk.Box - self.errors_label = None # Gtk.Label - # (from self.setup_videos_tab) - self.video_index_vbox = None # Gtk.VBox - self.videos_paned = None # Gtk.HPaned - self.video_index_scrolled = None # Gtk.ScrolledWindow - self.video_index_frame = None # Gtk.Frame - self.video_index_treeview = None # Gtk.TreeView - self.video_index_treestore = None # Gtk.TreeStore - self.video_index_sortmodel = None # Gtk.TreeModelSort - self.video_index_tooltip_column = 2 - self.button_box = None # Gtk.VBox - self.check_media_button = None # Gtk.Button - self.download_media_button = None # Gtk.Button - self.custom_dl_media_button = None # Gtk.Button - self.progress_box = None # Gtk.HBox - self.progress_bar = None # Gtk.ProgressBar - self.progress_label = None # Gtk.Label - self.video_catalogue_vbox = None # Gtk.VBox - self.catalogue_scrolled = None # Gtk.ScrolledWindow - self.catalogue_frame = None # Gtk.Frame - self.catalogue_listbox = None # Gtk.ListBox - self.catalogue_grid = None # Gtk.Grid - self.catalogue_toolbar = None # Gtk.Toolbar - self.catalogue_page_entry = None # Gtk.Entry - self.catalogue_last_entry = None # Gtk.Entry - self.catalogue_size_entry = None # Gtk.Entry - self.catalogue_first_button = None # Gtk.ToolButton - self.catalogue_back_button = None # Gtk.ToolButton - self.catalogue_forwards_button = None # Gtk.ToolButton - self.catalogue_last_button = None # Gtk.ToolButton - self.catalogue_scroll_up_button = None # Gtk.ToolButton - self.catalogue_scroll_down_button = None - # Gtk.ToolButton - self.catalogue_show_filter_button = None - # Gtk.ToolButton - self.catalogue_toolbar2 = None # Gtk.Toolbar - self.catalogue_sort_combo = None # Gtk.ComboBox - self.catalogue_reverse_toolbutton = None - # Gtk.ToolButton - self.catalogue_resort_button = None # Gtk.ToolButton - self.catalogue_find_date_button = None # Gtk.ToolButton - self.catalogue_cancel_date_button = None - # Gtk.ToolButton - - self.catalogue_toolbar3 = None # Gtk.Toolbar - self.catalogue_filter_entry = None # Gtk.Entry - self.catalogue_regex_togglebutton = None - # Gtk.ToggleButton - self.catalogue_filter_name_button = None - # Gtk.CheckButton - self.catalogue_filter_descrip_button = None - # Gtk.CheckButton - self.catalogue_filter_comment_button = None - # Gtk.CheckButton - self.catalogue_apply_filter_button = None - # Gtk.ToolButton - self.catalogue_cancel_filter_button = None - # Gtk.ToolButton - self.catalogue_thumb_combo = None # Gtk.ComboBox - self.catalogue_frame_button = None # Gtk.CheckButton - self.catalogue_icons_button = None # Gtk.CheckButton - self.catalogue_blocked_button = None # Gtk.CheckButton - # (from self.setup_progress_tab) - self.progress_paned = None # Gtk.VPaned - self.progress_list_scrolled = None # Gtk.ScrolledWindow - self.progress_list_treeview = None # Gtk.TreeView - self.progress_list_liststore = None # Gtk.ListStore - self.progress_list_tooltip_column = 2 - self.results_list_scrolled = None # Gtk.Frame - self.results_list_treeview = None # Gtk.TreeView - self.results_list_liststore = None # Gtk.ListStore - self.results_list_tooltip_column = 1 - self.num_worker_checkbutton = None # Gtk.CheckButton - self.num_worker_spinbutton = None # Gtk.SpinButton - self.bandwidth_checkbutton = None # Gtk.CheckButton - self.bandwidth_spinbutton = None # Gtk.SpinButton - self.alt_limits_frame = None # Gtk.Frame - self.alt_limits_image = None # Gtk.Image - self.video_res_checkbutton = None # Gtk.CheckButton - self.video_res_combobox = None # Gtk.ComboBox - self.hide_finished_checkbutton = None # Gtk.CheckButton - self.reverse_results_checkbutton = None # Gtk.CheckButton - self.progress_update_label = None # Gtk.Label - # (from self.setup_classic_mode_tab) - self.classic_paned = None # Gtk.VPaned - self.classic_banner_img = None # Gtk.Image - self.classic_banner_label = None # Gtk.Label - self.classic_banner_label2 = None # Gtk.Label - self.classic_menu_button = None # Gtk.Button - self.classic_textview = None # Gtk.TextView - self.classic_textbuffer = None # Gtk.TextBuffer - self.classic_mark_start = None # Gtk.TextMark - self.classic_mark_end = None # Gtk.TextMark - self.classic_dest_dir_liststore = None # Gtk.ListStore - self.classic_dest_dir_combo = None # Gtk.ComboBox - self.classic_dest_dir_button = None # Gtk.Button - self.classic_dest_dir_open_button = None - # Gtk.Button - self.classic_format_liststore = None # Gtk.ListStore - self.classic_format_combo = None # Gtk.ComboBox - self.classic_resolution_liststore = None - # Gtk.ListStore - self.classic_resolution_combo = None # Gtk.ComboBox - self.classic_convert_liststore = None # Gtk.ListStore - self.classic_convert_combo = None # Gtk.ComboBox - self.classic_livestream_checkbutton = None - # Gtk.CheckButton - self.classic_sblock_checkbutton = None # Gtk.CheckButton - self.classic_add_clips_button = None # Gtk.Button - self.classic_add_urls_button = None # Gtk.Button - self.classic_progress_treeview = None # Gtk.TreeView - self.classic_progress_liststore = None # Gtk.ListStore - self.classic_progress_tooltip_column = 1 - self.classic_play_button = None # Gtk.Button - self.classic_open_button = None # Gtk.Button - self.classic_redownload_button = None # Gtk.Button - self.classic_archive_button = None # Gtk.ToggleButton - self.classic_stop_button = None # Gtk.Button - self.classic_clips_button = None # Gtk.Button - self.classic_ffmpeg_button = None # Gtk.Button - self.classic_move_up_button = None # Gtk.Button - self.classic_move_down_button = None # Gtk.Button - self.classic_remove_button = None # Gtk.Button - self.classic_download_button = None # Gtk.Button - self.classic_clear_button = None # Gtk.Button - self.classic_clear_dl_button = None # Gtk.Button - # (from self.setup_drag_drop_tab) - self.drag_drop_menu_button = None # Gtk.Button - self.drag_drop_frame = None # Gtk.Frame - self.drag_drop_grid = None # Gtk.Grid - # (from self.setup_output_tab) - self.output_notebook = None # Gtk.Notebook - self.output_size_checkbutton = None # Gtk.CheckButton - self.output_size_spinbutton = None # Gtk.SpinButton - # (from self.setup_errors_tab) - self.errors_list_frame = None # Gtk.Frame - self.errors_list_scrolled = None # Gtk.ScrolledWindow - self.errors_list_treeview = None # Gtk.TreeView - self.errors_list_liststore = None # Gtk.ListStore - self.show_system_error_checkbutton = None - # Gtk.CheckButton - self.show_system_warning_checkbutton = None - # Gtk.CheckButton - self.show_operation_error_checkbutton = None - # Gtk.CheckButton - self.show_operation_warning_checkbutton = None - # Gtk.CheckButton - self.show_system_date_checkbutton = None - # Gtk.CheckButton - self.show_system_container_checkbutton = None - # Gtk.CheckButton - self.show_system_video_checkbutton = None - # Gtk.CheckButton - self.show_system_multi_line_checkbutton = None - # Gtk.CheckButton - self.error_list_entry = None # Gtk Entry - self.error_list_togglebutton = None # Gtk.ToggleButton - self.error_list_container_checkbutton = None - # Gtk.CheckButton - self.error_list_video_checkbutton = None - # Gtk.CheckButton - self.error_list_msg_checkbutton = None # Gtk.CheckButton - self.error_list_filter_toolbutton = None - # Gtk.ToolButton - self.error_list_cancel_toolbutton = None - self.error_list_button = None # Gtk.Button - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between main window widgets - self.spacing_size = self.app_obj.default_spacing_size - - # IVs used when videos in the Video Index are displayed in a grid. The - # size of the grid changes as the window is resized. Each location in - # the grid can be occupied by a gridbox (mainwin.CatalogueGridBox), - # containing a single video - # The size of the window, the last time a certain signal connect fired, - # so we can spot real changes to its size, when the same signal fires - # in the future - self.win_last_width = None - self.win_last_height = None - # Also keep track of the position of the slider in the Video tab's - # Gtk.HPaned, so that when the user actually drags the slider, we can - # adjust the size of the grid - self.paned_last_width = None - # IVs used when the window is closed to the tray, recording its - # position on the desktop (so that position can be restored when the - # window is opened from the tray). The IVs are reset when the window - # becomes visible againH - # NB Detecting the window's position on the desktop does not work on - # Wayland (according to the Gtk documentation) - self.win_last_xpos = None - self.win_last_ypos = None - # Standard minimum column width for the treeviews that are the Progress - # List, Results List and Classic Progress List - self.min_column_width = 20 - - # Paths to Tartube standard icon files. Dictionary in the form - # key - a string like 'video_both_large' - # value - full filepath to the icon file - # N.B. In this dictionary, composite pixbufs created by - # self.setup_composite_pixbufs() use the value 'None' - self.icon_dict = {} - # Loading icon files whenever they're neeeded causes frequent Gtk - # crashes. Instead, we create a GdkPixbuf.Pixbuf for all standard - # icon files at the beginning - # A dictionary of those pixbufs, created by self.setup_pixbufs() - # Dictionary in the form - # key - a string like 'video_both_large' (the same key set used by - # self.icon_dict) - # value - A GdkPixbuf.Pixbuf object - self.pixbuf_dict = {} - # List of pixbufs used as the main window's icon list - self.win_pixbuf_list = [] - # List of pixbufs used as other windows' icon lists - self.config_win_pixbuf_list = [] - # The full path to the directory in which self.setup_pixbufs() found - # the icons; stores so that StatusIcon can use it - self.icon_dir_path = None - - # Standard limits for the length of strings displayed in various - # widgets - self.exceedingly_long_string_max_len = 80 - self.very_long_string_max_len = 64 - self.long_string_max_len = 48 - self.quite_long_string_max_len = 40 - self.medium_string_max_len = 32 - self.short_string_max_len = 24 - self.tiny_string_max_len = 16 - # Use a separate IV for video descriptions (so we can tweak it - # specifically). A limit exists because descriptions in ALL CAPS are - # too big for the Video Catalogue, otherwise - self.descrip_line_max_len = 80 - # Use a separate IV for tooltips in the Video Index/Video Catalogue - self.tooltip_max_len = 60 - # Limits (number of videos) at which the code will prompt the user - # before bookmarking videos (etc) - # Take shortcuts, but don't prompt the user - self.mark_video_lower_limit = 50 - # Take shortcuts, and prompt the user - self.mark_video_higher_limit = 1000 - - # Dictionary of tabs in the main window's notebook (self.notebook), and - # their corresponding page numbers. If __pkg_no_download_flag__ is - # set, the Classic Mode tab is not visible, so page numbers will - # differ - # Dictionary in the form - # key - the string 'videos', 'progress', 'classic', 'drag_drop', - # 'output' or 'error' - # value - The tab number, in the range 0-5 - self.notebook_tab_dict = {} # Set below - # The number of the tab in self.notebook that is currently visible - # (only required to test whether the Errors/Warnings tab is the - # visible one) - self.visible_tab_num = 0 - - # Videos tab IVs - # The Video Index is the left-hand side of the main window, and - # displays only channels, playlists and folders - # The Video Index uses a Gtk.TreeView to display media data objects - # (channels, playlist and folders, but not videos). This dictionary - # keeps track of which row in the Gtk.TreeView is displaying which - # media data object - # Dictionary in the form - # key = .dbid of the media data object - # value = Gtk.TreeRowReference - self.video_index_row_dict = {} - # Dictionary keeping track of which rows have their markers activated - # A subset of key/value pairs in self.video_index_row_dict. Rows whose - # markers are not activated are not in this dictionary - self.video_index_marker_dict = {} - # A call to self.video_index_reset() redraws the Video Index, but calls - # to other functions repopulate it - # The call to .video_index_reset() resets self.video_index_marker_dict, - # moving its pairs temporarily into this dictionary, so that they can - # be retrieved during the subsequent call to - # self.video_index_populate() - self.video_index_old_marker_dict = {} - - # The call to self.video_index_add_row() causes the auto-sorting - # function self.video_index_auto_sort() to be called before we're - # ready, due to some Gtk problem I don't understand - # Temporary solution is to disable auto-sorting during calls to that - # function - self.video_index_no_sort_flag = False - # The .dbid of the channel, playlist or folder currently visible in the - # Video Catalogue (None if no channel, playlist or folder is - # selected) - self.video_index_current_dbid = None - # Don't update the Video Catalogue during certain procedures, such as - # removing a row from the Video Index (in which case, this flag will - # be set to True - self.ignore_video_index_select_flag = False - - # The Video Catalogue is the right-hand side of the main window. When - # the user clicks on a channel, playlist or folder, all the videos - # it contains are displayed in the Video Catalogue (replacing any - # previous contents) - # Dictionary of mainwin.SimpleCatalogueItem, - # mainwin.ComplexCatalogueItem or mainwin.GridCatalogueItem objects - # (depending on the current value of - # mainapp.TartubeApp.catalogue_mode) - # There is one catalogue item object for each row that's currently - # visible in the Video Catalogue - # Dictionary in the form - # key = .dbid (of the mainwin.SimpleCatalogueItem, - # mainwin.ComplexCatalogueItem or mainwin.GridCatalogueItem which - # matches the dbid of its media.Video object) - # value = the catalogue item itself - self.video_catalogue_dict = {} - # Catalogue itme objects are added to the catalogue in a call to - # self.video_catalogue_insert_video() - # If Gtk issues a warning, complaining that the Gtk.ListBox is being - # sorted, the row (actually a CatalogueRow object) is added to this - # list temporarily, and then periodic calls to - # self.video_catalogue_retry_insert_items() try again, until the - # list is empty - self.video_catalogue_temp_list = [] - # Flag set to True if a filter is currently applied to the Video - # Catalogue, hiding some videos, and showing only videos that match - # the search text; False if not - self.video_catalogue_filtered_flag = False - # When the filter is applied, a list of video objects to show (may be - # an empty list) - self.video_catalogue_filtered_list = [] - - # Dragging videos from the Video Catalogue into the Video Index - # requires some trickery. At the start of such a drag, this list is - # filled with all media.Video objects involved in the drag; when the - # drag ends, it is emptied - # Therefore, self.on_video_index_drag_data_received() knows that it is - # receiving media.Videos when this list is not empty - self.video_catalogue_drag_list = [] - - # Background colours used in the Video Catalogue to highlight - # livestream videos (we use different colours for a debut video) - # Each value is a Gdk.RGBA object, whose initial colours are set (in - # the call to self.setup_bg_colour() below) using - # mainapp.TartubeApp.custom_bg_table - self.live_wait_colour = None # Red - self.live_now_colour = None # Green - self.debut_wait_colour = None # Yellow - self.debut_now_colour = None # Cyan - # Background colours used in the Video Catalogue, in grid mode, to - # highlight selected livestream/debut videos - self.grid_select_colour = None # Blue - self.grid_select_wait_colour = None # Purple - self.grid_select_live_colour = None # Purple - # Background colours used in the Drag and Drop tab - self.drag_drop_notify_colour = None # Purple - self.drag_drop_odd_colour = None # Orange - self.drag_drop_even_colour = None # Pale orange - - # The Video Catalogue splits its video list into pages (as Gtk - # struggles with a list of hundreds, or thousands, of videos) - # The number of videos per page is specified by - # mainapp.TartubeApp.catalogue_page_size - # The current page number (minimum 1, maximum 9999) - self.catalogue_toolbar_current_page = 1 - # The number of pages currently in use (minimum 1, maximum 9999) - self.catalogue_toolbar_last_page = 1 - - # The horizontal size of the grid (the number of gridboxes that can - # fit on a single row of the grid). This value is set automatically - # as the available space changes (for example, when the user resizes - # the main window, or when the user drags the paned handled in the - # Videos tab) - self.catalogue_grid_column_count = 1 - # The vertical size of the grid. The Video Catalogue scrolls to - # accommodate extra rows, so this value is only set when new - # CatalogueGridBox objects are added to the grid (and not in - # response to changes in the window size, for example) - # (The value is only checked when the grid actually contains videos, so - # its minimum size is 1 even when the grid is empty) - self.catalogue_grid_row_count = 1 - # mainapp.TartubeApp defines several thumbnail sizes, from 'tiny' to - # 'enormous' - # In order to work out how many gridboxes can fit on a single row of - # the grid, we have to know the minimum required size for each - # gridbox. That size is different for each thumbnail size - # After drawing the first gridbox(es), the minimum required size is not - # available immediately (for obscure Gtk reasons). Therefore, we - # initially prevent each gridbox from expanding horizontally until - # the size has been obtained; at that point, the grid is redrawn - # A dictionary of minimum required sizes, in the form - # key: thumbnail size (one of the keys in - # mainapp.TartubeApp.thumb_size_dict) - # value: The minimum required size for a gridbox (in pixels), or - # 'None' if that value is not known yet - self.catalogue_grid_width_dict = {} # Initialised below - # Gridboxes may be allowed to expand horizontally to fill the available - # space, or not, depending on aesthetic requirements. This flag is - # True if gridboxes are allowed to expand, False if not - self.catalogue_grid_expand_flag = False - # Flag set to True by self.video_catalogue_grid_set_gridbox_width(), so - # that mainapp.TartubeApp.script_fast_timer_callback() knows it must - # call self.video_catalogue_grid_check_size() - self.catalogue_grid_rearrange_flag = False - # When the grid is visible, the selected gridbox (if any) intercepts - # cursor and page up/down keys - # A dictionary of Gdk.keyval_name values that should be intercepted, - # for quick lookup - self.catalogue_grid_intercept_dict = { - 'Up': None, - 'Down': None, - 'Left': None, - 'Right': None, - 'Page_Up': None, - 'Page_Down': None, - 'a': None, # Intercepts CTRL+A - } - - # Progress tab IVs - # The Progress List uses a Gtk.TreeView display download jobs, whether - # they are waiting to start, currently in progress, or finished. This - # dictionary keeps track of which row in the Gtk.TreeView is handling - # which download job - # Dictionary in the form - # key = The downloads.DownloadItem.item_id for the download item - # handling the media data object - # value = the row number (0 is the first row) - self.progress_list_row_dict = {} - # The number of rows added to the treeview - self.progress_list_row_count = 0 - # During a download operation, self.progress_list_receive_dl_stats() is - # called every time youtube-dl writes some output to STDOUT. This can - # happen many times a second - # Updating data displayed in the Progress List several times a second, - # and irregularly, doesn't look very nice. Instead, we only update - # the displayed data at fixed intervals - # Thus, when self.progress_list_receive_dl_stats() is called, it - # temporarily stores the download statistics it has received in this - # IV. The statistics are received in a dictionary in the standard - # format described in the comments to - # downloads.VideoDownloader.extract_stdout_data() - # Then, during calls at fixed intervals to - # self.progress_list_display_dl_stats(), those download statistics - # are displayed - # Dictionary of download statistics yet to be displayed, emptied after - # every call to self.progress_list_display_dl_stats() - # Dictionary in the form - # key = The downloads.DownloadItem.item_id for the download item - # handling the media data object - # value = A dictionary of download statistics dictionary in the - # standard format - self.progress_list_temp_dict = {} - # During a download operation, we keep track of rows that are finished, - # so they can be hidden, if required - # Dictionary in the form - # key = The downloads.DownloadItem.item_id for the download item - # handling the media data object - # value = The time at which it should be hidden (matches time.time()) - # (As soon as a row is hidden, all of these IVs are updated, removing - # them from all three dictionaries) - self.progress_list_finish_dict = {} - # The time (in seconds) after which a row which can be hidden, should - # actually be hidden - # (The code assumes it is at least twice the value of - # mainapp.TartubeApp.dl_timer_time) - self.progress_list_hide_time = 3 - # Dictionay of download speeds for each active row in the Progress - # List, used to produce a rolling average. Inactive (or finished) - # rows are removed from the table - # Dictionary in the form - # key = The downloads.DownloadItem.item_id for the download item - # handling the media data object - # value = a list of mini-lists. Each mini-list records an - # instantaneous download speed (as reported by youtube-dl), and - # the epoch time at which this speed was displayed - self.progress_list_average_speed_dict = {} - # The time (in seconds) after which instantaneous download speeds are - # removed from the dictionary - # N.B. 10 seconds seems to be a reasonable value, given that youtube-dl - # typically supplies an instaneous speed at least once a second, and - # given that some videos might be downloaded faster than 10 seconds - self.progress_list_average_speed_length = 10 - # !!! DEBUG Git #479 - # Dictionary recording system errors in - # self.progress_list_receive_dl_stats() and - # .progress_list_do_hide_row() (due to unresolved issues) - # Instead of repeating the same system error innumerable times, add an - # entry to this dictionary so it can be shown only the first time. - # The dictionary is reset every time the Progress List is reset - # Dictionary in the form - # key = The downloads.DownloadItem.item_id for the download item - # handling the media data object - # value = False - self.progress_list_broken_dict = {} - - # Whenever a video is downloaded (in reality, or just in simulation), - # a row is added to Gtk.TreeView in the Results List - # The number of rows added to the treeview - self.results_list_row_count = 0 - # At the instant youtube-dl reports that a video has been downloaded, - # the file doesn't yet exist in Tartube's data directory (so the - # Python test for the existence of the file fails) - # Therefore, self.results_list_add_row() adds a temporary entry to this - # list. Items in the list are checked by - # self.results_list_update_row() and removed from the list, as soon - # as the file is confirmed to exist, at which time the Results List - # is updated - # (For simulated downloads, the entry is checked by - # self.results_list_update_row() just once. For real downloads, it - # is checked many times until either the file exists or the - # download operation halts) - # List of python dictionaries, one for each downloaded video. Each of - # those dictionaries are in the form: - # 'video_obj': a media.Video object - # 'row_num': the row on the treeview, matching - # self.results_list_row_count - # 'keep_description', 'keep_info', 'keep_annotations', - # 'keep_thumbnail', 'move_description', 'move_info', - # 'move_annotations', 'move_thumbnail': flags from the - # options.OptionsManager object used for to download the - # video ('keep_description', etc, are not not added to the - # dictionary at all for simulated downloads) - self.results_list_temp_list = [] - # When a video is deleted, the row in the Results List containing that - # video must be updated. So this can be done efficiently, we also - # compile a dictionary of media.Video objects and the rows they - # occupy - # Dictionary in the form - # key = The .dbid of the media.Video for the row - # value = The row number on the treeview - self.results_list_row_dict = {} - - # Classic Mode tab IVs - # During a normal download operation, stats are displayed in the - # Progress tab - # During a download operation launched from the Classic Mode tab, stats - # are displayed in the Classic Progress List instead. In addition, we - # create a set of dummy media.Video objects, one for each URL to - # download. Each dummy media.Video object has a negative .dbid, and - # none of them are added to the media data registry - # The dummy media.Video object's URL may be a single video, or even a - # channel or playlist (Tartube doesn't really care which) - # Dictionary in the form - # key = The unique ID (dbid) for the dummy media.Video object - # handling the URL - # value = The dummy media.Video object itself - self.classic_media_dict = {} - # The total number of dummy media.Video objects created since Tartube - # started (used to give each one a unique ID) - self.classic_media_total = 0 - # During a download operation launched from the Classic Mode tab, - # incoming stats are stored in this dictionary, just as they are - # stored in self.progress_list_temp_dict during a normal download - # operation - # Dictionary in the form - # key = The downloads.DownloadItem.item_id for the download item - # handling the media data object - # value = A dictionary of download statistics dictionary in the - # standard format - self.classic_temp_dict = {} - # Flag set to True when automatic copy/paste has been enabled (always - # disabled on startup) - self.classic_auto_copy_flag = False - # Flag set to True when one-click downloads have been enabled (always - # disabled on startup) - self.classic_one_click_dl_flag = False - # The last text that was copy/pasted from the clipboard. Storing it - # here prevents self.classic_mode_tab_timer_callback() from - # continually re-pasting the same text (for example, when the user - # manually empties the textview) - self.classic_auto_copy_text = None - # Temporary flag set to prevent a second call to - # self.classic_mode_tab_add_urls() before the first one has finished - self.classic_auto_copy_check_flag = False - # Flag set to True just before a call to - # self.classic_mode_tab_add_urls() so that it can't call itself - # IVs for clipboard monitoring, when required - self.classic_clipboard_timer_id = None - self.classic_clipboard_timer_time = 250 - - # Drag and Drop tab IVs - # Dictionary of mainwin.DropZoneBox objects that currently exist in - # the tab (ignoring any blank ones used to fill space) - # Dictionary in the form - # key = the .uid of the equivalent options.OptionsManager object - # value = the mainwin.DropZoneBox object - self.drag_drop_dict = {} - # The maximum number of dropzones (minimum value = 1; must not be a - # prime number) - self.drag_drop_max = 16 - # The time (in seconds) after which confirmation messages in each - # dropzone should be reset - self.drag_drop_reset_time = 5 - - # Output tab IVs - # Flag set to True when the summary tab is added, during the first call - # to self.output_tab_setup_pages() (might not be added at all, if - # mainapp.TartubeApp.ytdl_output_show_summary_flag is False) - self.output_tab_summary_flag = False - # The number of pages in the Output tab's notebook (not including the - # summary tab). The number matches the highest value of - # mainapp.TartubeApp.num_worker_default during this session (i.e. if - # the user increases the value, new page(s) are created, but if the - # user reduces the value, no pages are destroyed) - self.output_page_count = 0 - # Dictionary of Gtk.TextView objects created in the Output tab; one for - # each page - # Dictionary in the form - # key = The page number (the summary page is #0, the first page for a - # thread is #1, regardless of whether the summary page is - # visible) - # value = The corresponding Gtk.TextView object - self.output_textview_dict = {} - # Colours used in the output tab - self.output_tab_bg_colour = '#000000' - self.output_tab_text_colour = '#FFFFFF' - self.output_tab_stderr_colour = 'cyan' - self.output_tab_system_cmd_colour = 'yellow' - - # Errors / Warnings tab IVs - # List of error/warning messages available to be shown in the Errors/ - # Warnings tab (so they can be made visible, or not, as required) - # Every item in the list is a dictionary of key/value pairs. We don't - # store and .dbids, in case the media data object gets deleted in the - # meantime (in which case, the error/warning isn't automatically - # deleted) - # The dictionary contains the keys - # dict['msg_type'] - 'system_error', 'system_warning', - # 'operation_error', 'operation_warning' - # dict['media_type'] - 'video', 'channel', 'playlist' - # dict['date_time'] - date and time at which the message was - # generated (a string) - # dict['time'] - time at which the message was generated (a string) - # dict['container_name'] - name of the parent channel/playlist/folder - # (if generated by a video, the name of the parent container) - # dict['video_name'] - name of the video. If generated by a channel/ - # playlist, an empty string - # dict['msg'] - The message, formatted into multiple lines with a - # maximum line length - # dict['short_msg'] - The first line of the formatted message, with - # an ellipsis appended if the message is too big for a single - # line - # dict['orig_msg'] - The original message with no formatting - # dict['count_flag'] - True if this message should count towards the - # totals displayed in the Errors/Warnings tab label; False if not - # dict['drag_path'] - # dict['drag_source'] - # dict['drag_name'] - Data for drag and drop operations - self.error_list_buffer_list = [] - # Settings set when the Error List filter is applied, and reset when - # it is cancelled - self.error_list_filter_flag = False - self.error_list_filter_text = None - self.error_list_filter_regex_flag = False - self.error_list_filter_container_flag = False - self.error_list_filter_video_flag = False - self.error_list_filter_msg_flag = False - - # List of configuration windows (anything inheriting from - # config.GenericConfigWin) and wizard windows (anything inheriting - # from wizwin.GenericWizWin) that are currently open - # An operation cannot start when one of these windows are open (and the - # windows cannot be opened during such an operation) - self.config_win_list = [] - # In addition. only one wizard window (inheriting wizwin.GenericWizWin) - # can be open at a time. The currently-open wizard window, if any - self.wiz_win_obj = None - # The number of pages in wizwin.TutorialWizWin - self.tutorial_page_count = 34 - - # Dialogue window IVs - # The SetDestinationDialogue dialogue window displays a list of - # channels/playlists/folders, and an external directory. When opening - # it repeatedly, it's handy to display the previous selections - # The .dbid of the previous channel/playlist/folder selected (or None, - # if SetDestinationDialogue hasn't been used yet) - # The value is set/reset by a call to self.set_previous_alt_dest_dbid() - self.previous_alt_dest_dbid = None - # The most recent external directory specified (or None, if - # SetDestinationDialogue hasn't been used yet) - self.previous_external_dir = None - - # Desktop notification IVs - # The desktop notification has an optional button to click. When the - # button is used, we need to retain a reference to the - # Notify.Notification, or the callback won't work - # The number of desktop notifications (with buttons) created during - # this session (used to give each one a unique ID) - self.notify_desktop_count = 0 - # Dictionary of Notify.Notification objects. Each entry is removed when - # the notification is closed - # Dictionary in the form - # key: unique ID for the notification (based on - # self.notify_desktop_count) - # value: the corresponding Notify.Notification object - self.notify_desktop_dict = {} - - # Other IVs - # Separator used (optionally) when draggind and dropping into an - # external application - self.drag_drop_separator = '-----' - - - # Code - # ---- - - # Set tab numbers for each visible tab in the main window - self.notebook_tab_dict['videos'] = 0 - self.notebook_tab_dict['progress'] = 1 - if not __main__.__pkg_no_download_flag__: - self.notebook_tab_dict['classic'] = 2 - self.notebook_tab_dict['drag_drop'] = 3 - self.notebook_tab_dict['output'] = 4 - self.notebook_tab_dict['errors'] = 5 - else: - self.notebook_tab_dict['classic'] = None - self.notebook_tab_dict['drag_drop'] = None - self.notebook_tab_dict['output'] = 2 - self.notebook_tab_dict['errors'] = 3 - - # Create GdkPixbuf.Pixbufs for all Tartube standard icons - self.setup_pixbufs() - - # Set (default) background colours for the Video Catalogue. Custom - # colours are set by a later call from mainapp.TartubeApp.load_config - for key in self.app_obj.custom_bg_table: - self.setup_bg_colour(key) - - # Initialise minimum sizes for gridboxes - self.video_catalogue_grid_reset_sizes() - - - # Public class methods - - - def setup_pixbufs(self): - - """Called by self.__init__(). - - Populates self.icon_dict and self.pixbuf.dict from the lists provided - by formats.py. - """ - - # The default location for icons is ../icons - # When installed via PyPI, the icons are moved to ../tartube/icons - # When installed via a Debian/RPM package, the icons are moved to - # /usr/share/tartube/icons - icon_dir_list = [] - icon_dir_list.append( - os.path.abspath( - os.path.join(self.app_obj.script_parent_dir, 'icons'), - ), - ) - - icon_dir_list.append( - os.path.abspath( - os.path.join( - os.path.dirname(os.path.realpath(__file__)), - 'icons', - ), - ), - ) - - icon_dir_list.append( - os.path.join( - '/', 'usr', 'share', __main__.__packagename__, 'icons', - ) - ) - - for icon_dir_path in icon_dir_list: - if os.path.isdir(icon_dir_path): - - for key in formats.DIALOGUE_ICON_DICT: - rel_path = formats.DIALOGUE_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'dialogue', rel_path), - ) - self.icon_dict[key] = full_path - - for key in formats.TOOLBAR_ICON_DICT: - rel_path = formats.TOOLBAR_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'toolbar', rel_path), - ) - self.icon_dict[key] = full_path - - for key in formats.LARGE_ICON_DICT: - rel_path = formats.LARGE_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'large', rel_path), - ) - self.icon_dict[key] = full_path - - for key in formats.SMALL_ICON_DICT: - rel_path = formats.SMALL_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'small', rel_path), - ) - self.icon_dict[key] = full_path - - for key in formats.THUMB_ICON_DICT: - rel_path = formats.THUMB_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'thumbs', rel_path), - ) - self.icon_dict[key] = full_path - - for key in formats.EXTERNAL_ICON_DICT: - rel_path = formats.EXTERNAL_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'external', rel_path), - ) - self.icon_dict[key] = full_path - - for key in formats.STOCK_ICON_DICT: - rel_path = formats.STOCK_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'stock', rel_path), - ) - self.icon_dict[key] = full_path - - for locale in formats.LOCALE_LIST: - full_path = os.path.abspath( - os.path.join( - icon_dir_path, - 'locale', - 'flag_' + locale + '.png', - ), - ) - self.icon_dict['flag_' + locale] = full_path - - for i in range (self.tutorial_page_count): - full_path = os.path.abspath( - os.path.join( - icon_dir_path, - 'tutorial', - 'tutorial' + str(i) + '.png', - ), - ) - self.icon_dict['tutorial' + str(i)] = full_path - - # Now create the pixbufs themselves - for key in self.icon_dict: - full_path = self.icon_dict[key] - - if not os.path.isfile(full_path): - self.pixbuf_dict[key] = None - else: - self.pixbuf_dict[key] \ - = GdkPixbuf.Pixbuf.new_from_file(full_path) - - for rel_path in formats.WIN_ICON_LIST: - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'win', rel_path), - ) - self.win_pixbuf_list.append( - GdkPixbuf.Pixbuf.new_from_file(full_path), - ) - - for rel_path in formats.CONFIG_WIN_ICON_LIST: - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'win', rel_path), - ) - self.config_win_pixbuf_list.append( - GdkPixbuf.Pixbuf.new_from_file(full_path), - ) - - # Composite icons using a base file and one or more overlays - self.setup_composite_pixbufs(icon_dir_path) - - # Store the correct icon_dir_path, so that StatusIcon can use - # it - self.icon_dir_path = icon_dir_path - - return - - # No icons directory found; this is a fatal error - print( - _('Tartube cannot start because it cannot find its icons folder'), - file=sys.stderr, - ) - - self.app_obj.do_shutdown() - - - def setup_composite_pixbufs(self, icon_dir_path): - - """Called by self.setup_pixbufs(). - - Most Tartube icons are loaded from a single file. The Video Index uses - composite icons, loaded from a base file and some optional overlays. - - The base icons are specified by formats.LARGE_ICON_COMPOSITE_LIST, - a subset of keys in formats.LARGE_ICON_DICT. - - This function creates the composites, and updates self.icon_dict and - self.pixbuf_dict. - - Args: - - icon_dir_path (str): Full path to a directory in which Tartube's - icons are stored (depends on the operating system and the - installation method) - - """ - - for base in formats.LARGE_ICON_COMPOSITE_LIST: - - # Produce an image whose name (in self.icon_dict) is in the form - # 'base_tl_tr_bl_br', where the last four components are optional - # and represent overlays adding an icon on the top-left, - # top-right, bottom-left and/or bottom-right - for tl in range(2): - for tr in range(2): - for bl in range(2): - for br in range(2): - for alt in range(2): - - icon_name = base - pixbuf = GdkPixbuf.Pixbuf.new_from_file( - os.path.abspath( - os.path.join( - icon_dir_path, - 'large', - formats.LARGE_ICON_DICT[base], - ), - ), - ) - - # Add the top-left overlay when tl = 1, don't - # add it when tl = 0 (etc) - if tl: - icon_name += '_tl' - pixbuf = self.apply_pixbuf_overlay( - icon_dir_path, - pixbuf, - '_tl', - ) - - if tr: - icon_name += '_tr' - pixbuf = self.apply_pixbuf_overlay( - icon_dir_path, - pixbuf, - '_tr', - ) - - if bl: - - # The bottom-left icon has two variants - if not alt: - icon_name += '_bl' - pixbuf = self.apply_pixbuf_overlay( - icon_dir_path, - pixbuf, - '_bl', - ) - - else: - icon_name += '_bl_alt' - pixbuf = self.apply_pixbuf_overlay( - icon_dir_path, - pixbuf, - '_bl_alt', - ) - - if br: - icon_name += '_br' - pixbuf = self.apply_pixbuf_overlay( - icon_dir_path, - pixbuf, - '_br', - ) - - # (Composite pixbufs have no file path) - self.icon_dict[icon_name] = None - self.pixbuf_dict[icon_name] = pixbuf - - - def apply_pixbuf_overlay(self, icon_dir_path, base_pixbuf, name): - - """Called by self.setup_composite_pixbufs(). - - Creates a composite pixbuf using a base pixbuf and an overlay pixbuf. - - Args: - - icon_dir_path (str): Full path to a directory in which Tartube's - icons are stored (depends on the operating system and the - installation method) - - base_pixbuf (GdkPixbuf.Pixbuf): The base pixbuf - - name (str): One of the strings '_tl', '_tr', '_bl', '_bl_alt' or - 'br', represnting icons in the ../icons/overlays directory - - Return values: - - Returns the composite pixbuf - - """ - - overlay_pixbuf = GdkPixbuf.Pixbuf.new_from_file( - os.path.abspath( - os.path.join( - icon_dir_path, - 'overlays', - 'overlay' + name + '.png', - ), - ), - ) - - overlay_pixbuf.composite( - base_pixbuf, - 0, - 0, - base_pixbuf.props.width, - base_pixbuf.props.height, - 0, - 0, - 1, - 1, - GdkPixbuf.InterpType.BILINEAR, - 250, - ) - - return base_pixbuf - - - def setup_bg_colour(self, bg_name): - - """Called initially by self.__init__(), and then again by - mainapp.TartubeApp.load_config(), set_custom_bg() and - .reset_custom_bg(). - - Sets the value of the IVs self.live_wait_colour, etc. The colours are - used as backgrounds in the Video Catalogue. - - Args: - - bg_name (str): One of the keys in - mainapp.TartubeApp.custom_bg_table - - """ - - if bg_name in self.app_obj.custom_bg_table: - - mini_list = self.app_obj.custom_bg_table[bg_name] - rgba_obj = Gdk.RGBA( - mini_list[0], - mini_list[1], - mini_list[2], - mini_list[3], - ) - - if bg_name == 'live_wait': - self.live_wait_colour = rgba_obj - elif bg_name == 'live_now': - self.live_now_colour = rgba_obj - elif bg_name == 'debut_wait': - self.debut_wait_colour = rgba_obj - elif bg_name == 'debut_now': - self.debut_now_colour = rgba_obj - elif bg_name == 'select': - self.grid_select_colour = rgba_obj - elif bg_name == 'select_wait': - self.grid_select_wait_colour = rgba_obj - elif bg_name == 'select_live': - self.grid_select_live_colour = rgba_obj - elif bg_name == 'drag_drop_notify': - self.drag_drop_notify_colour = rgba_obj - elif bg_name == 'drag_drop_odd': - self.drag_drop_odd_colour = rgba_obj - elif bg_name == 'drag_drop_even': - self.drag_drop_even_colour = rgba_obj - - - # (Create main window widgets) - - - def setup_win(self): - - """Called by mainapp.TartubeApp.start_continue(). - - Sets up the main window, calling various function to create its - widgets. - """ - - # Set the default window size - self.set_default_size( - self.app_obj.main_win_width, - self.app_obj.main_win_height, - ) - - # Set the window's Gtk icon list - self.set_icon_list(self.win_pixbuf_list) - - # Intercept the user's attempts to close the window, so we can close to - # the system tray, if required - self.connect('delete-event', self.on_delete_event) - - # Detect window resize events, so the size of the Video Catalogue grid - # (when visible) can be adjusted smoothly - self.connect('size-allocate', self.on_window_size_allocate) - - # Allow the user to drag-and-drop videos (for example, from the web - # browser) into the main window, adding it the currently selected - # folder (or to 'Unsorted Videos' if something else is selected, or - # into the Classic Mode tab if it is visible) - self.connect('drag-data-received', self.on_window_drag_data_received) - # (Without this line, we get Gtk warnings on some systems) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - # (Continuing) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Set up desktop notifications. Notifications can be sent by calling - # self.notify_desktop() - if mainapp.HAVE_NOTIFY_FLAG: - Notify.init('Tartube') - - # Create main window widgets - self.setup_grid() - self.setup_main_menubar() - self.setup_main_toolbar() - self.setup_notebook() - self.setup_videos_tab() - self.setup_progress_tab() - self.setup_classic_mode_tab() - self.setup_drag_drop_tab() - self.setup_output_tab() - self.setup_errors_tab() - - - def setup_grid(self): - - """Called by self.setup_win(). - - Sets up a Gtk.Grid on which all the main window's widgets are placed. - """ - - self.grid = Gtk.Grid() - self.add(self.grid) - - - def setup_main_menubar(self): - - """Called by self.setup_win(). - - Sets up a Gtk.Menu at the top of the main window. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window menu starts here' - ) - - self.menubar = Gtk.MenuBar() - self.grid.attach(self.menubar, 0, 0, 1, 1) - - # File column - file_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_File')) - self.menubar.add(file_menu_column) - - file_sub_menu = Gtk.Menu() - file_menu_column.set_submenu(file_sub_menu) - - self.change_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Database preferences...'), - ) - file_sub_menu.append(self.change_db_menu_item) - self.change_db_menu_item.set_action_name('app.change_db_menu') - - self.check_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Check database integrity'), - ) - file_sub_menu.append(self.check_db_menu_item) - self.check_db_menu_item.set_action_name('app.check_db_menu') - - # Separator - file_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.save_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Save database'), - ) - file_sub_menu.append(self.save_db_menu_item) - self.save_db_menu_item.set_action_name('app.save_db_menu') - - self.save_all_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Save _all'), - ) - file_sub_menu.append(self.save_all_menu_item) - self.save_all_menu_item.set_action_name('app.save_all_menu') - - # Separator - file_sub_menu.append(Gtk.SeparatorMenuItem()) - - close_tray_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Close to _tray'), - ) - file_sub_menu.append(close_tray_menu_item) - close_tray_menu_item.set_action_name('app.close_tray_menu') - - quit_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Quit')) - file_sub_menu.append(quit_menu_item) - quit_menu_item.set_action_name('app.quit_menu') - - # Edit column - edit_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Edit')) - self.menubar.add(edit_menu_column) - - edit_sub_menu = Gtk.Menu() - edit_menu_column.set_submenu(edit_sub_menu) - - self.system_prefs_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_System preferences...'), - ) - edit_sub_menu.append(self.system_prefs_menu_item) - self.system_prefs_menu_item.set_action_name('app.system_prefs_menu') - - self.gen_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_General download options...'), - ) - edit_sub_menu.append(self.gen_options_menu_item) - self.gen_options_menu_item.set_action_name('app.gen_options_menu') - - # System column (MS Windows only) - if os.name == 'nt': - - system_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_System')) - self.menubar.add(system_menu_column) - - system_sub_menu = Gtk.Menu() - system_menu_column.set_submenu(system_sub_menu) - - self.open_msys2_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Open _MSYS2 terminal...'), - ) - system_sub_menu.append(self.open_msys2_menu_item) - self.open_msys2_menu_item.set_action_name('app.open_msys2_menu') - - # Separator - system_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.show_install_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Show Tartube _install folder'), - ) - system_sub_menu.append(self.show_install_menu_item) - self.show_install_menu_item.set_action_name( - 'app.show_install_menu', - ) - - self.show_script_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Show Tartube _script folder'), - ) - system_sub_menu.append(self.show_script_menu_item) - self.show_script_menu_item.set_action_name('app.show_script_menu') - - # Separator - system_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.change_theme_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Change theme...'), - ) - system_sub_menu.append(self.change_theme_menu_item) - self.change_theme_menu_item.set_action_name( - 'app.change_theme_menu', - ) - - # Media column - media_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Media')) - self.menubar.add(media_menu_column) - - media_sub_menu = Gtk.Menu() - media_menu_column.set_submenu(media_sub_menu) - - self.add_video_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add _videos...'), - ) - media_sub_menu.append(self.add_video_menu_item) - self.add_video_menu_item.set_action_name('app.add_video_menu') - - self.add_channel_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add _channel...'), - ) - media_sub_menu.append(self.add_channel_menu_item) - self.add_channel_menu_item.set_action_name('app.add_channel_menu') - - self.add_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add _playlist...'), - ) - media_sub_menu.append(self.add_playlist_menu_item) - self.add_playlist_menu_item.set_action_name('app.add_playlist_menu') - - self.add_folder_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add _folder...'), - ) - media_sub_menu.append(self.add_folder_menu_item) - self.add_folder_menu_item.set_action_name('app.add_folder_menu') - - # Separator - media_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.add_bulk_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Add many channels/playlists...'), - ) - media_sub_menu.append(self.add_bulk_menu_item) - self.add_bulk_menu_item.set_action_name('app.add_bulk_menu') - - self.reset_container_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Reset channel/playlist names...'), - ) - media_sub_menu.append(self.reset_container_menu_item) - self.reset_container_menu_item.set_action_name( - 'app.reset_container_menu', - ) - - # Separator - media_sub_menu.append(Gtk.SeparatorMenuItem()) - - export_import_submenu = Gtk.Menu() - - self.export_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Export from database...'), - ) - export_import_submenu.append(self.export_db_menu_item) - self.export_db_menu_item.set_action_name('app.export_db_menu') - - self.import_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Import into database...'), - ) - export_import_submenu.append(self.import_db_menu_item) - self.import_db_menu_item.set_action_name('app.import_db_menu') - - # Separator - export_import_submenu.append(Gtk.SeparatorMenuItem()) - - self.import_yt_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Import _YouTube subscriptions...'), - ) - export_import_submenu.append(self.import_yt_menu_item) - self.import_yt_menu_item.set_action_name( - 'app.import_yt_menu', - ) - - export_import_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Export/import')) - export_import_menu_item.set_submenu(export_import_submenu) - media_sub_menu.append(export_import_menu_item) - - # Separator - media_sub_menu.append(Gtk.SeparatorMenuItem()) - - switch_view_submenu = Gtk.Menu() - - last_type = None - # (mainapp.TartubeApp.catalogue_mode_list specifies catalogues modes - # in groups of three, in the form [mode, mode_type, description] - for mini_list in self.app_obj.catalogue_mode_list: - - # Catalogue modes are already sorted by mode type. Place a - # separator when we move from one mode type to the next - if last_type is not None and mini_list[1] != last_type: - - # Separator - switch_view_submenu.append(Gtk.SeparatorMenuItem()) - - last_type = mini_list[1] - - menu_item = Gtk.MenuItem.new_with_mnemonic(mini_list[2]) - switch_view_submenu.append(menu_item) - menu_item.connect( - 'activate', - self.on_switch_view, - mini_list[0], - mini_list[1], - ) - - self.switch_view_menu_item = \ - Gtk.MenuItem.new_with_mnemonic(_('_Switch between views')) - self.switch_view_menu_item.set_submenu(switch_view_submenu) - media_sub_menu.append(self.switch_view_menu_item) - - # Separator - media_sub_menu.append(Gtk.SeparatorMenuItem()) - - show_hide_submenu = Gtk.Menu() - - self.hide_system_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Hide (most) system folders'), - ) - show_hide_submenu.append(self.hide_system_menu_item) - self.hide_system_menu_item.set_active( - self.app_obj.toolbar_system_hide_flag, - ) - self.hide_system_menu_item.set_action_name('app.hide_system_menu') - - self.show_hidden_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Show hidden folders'), - ) - show_hide_submenu.append(self.show_hidden_menu_item) - self.show_hidden_menu_item.set_action_name('app.show_hidden_menu') - - self.show_hide_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('S_how/hide')) - self.show_hide_menu_item.set_submenu(show_hide_submenu) - media_sub_menu.append(self.show_hide_menu_item) - - profile_submenu = Gtk.Menu() - - self.switch_profile_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Switch profile'), - ) - profile_submenu.append(self.switch_profile_menu_item) - self.switch_profile_menu_item.set_submenu( - self.switch_profile_popup_submenu(), - ) - - self.auto_switch_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Remember last profile')) - profile_submenu.append(self.auto_switch_menu_item) - self.auto_switch_menu_item.set_active( - self.app_obj.auto_switch_profile_flag, - ) - self.auto_switch_menu_item.set_action_name( - 'app.auto_switch_menu', - ) - - # Separator - profile_submenu.append(Gtk.SeparatorMenuItem()) - - self.create_profile_menu_item = \ - Gtk.MenuItem.new_with_mnemonic( - _('_Create profile'), - ) - profile_submenu.append(self.create_profile_menu_item) - self.create_profile_menu_item.set_action_name( - 'app.create_profile_menu', - ) - - self.delete_profile_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Delete profile'), - ) - profile_submenu.append(self.delete_profile_menu_item) - self.delete_profile_menu_item.set_submenu( - self.delete_profile_popup_submenu(), - ) - - # Separator - profile_submenu.append(Gtk.SeparatorMenuItem()) - - self.mark_containers_menu_item = \ - Gtk.MenuItem.new_with_mnemonic( - _('_Mark all for download'), - ) - profile_submenu.append(self.mark_containers_menu_item) - self.mark_containers_menu_item.set_action_name( - 'app.mark_all_menu', - ) - - self.unmark_containers_menu_item = \ - Gtk.MenuItem.new_with_mnemonic( - _('_Unmark all for download'), - ) - profile_submenu.append(self.unmark_containers_menu_item) - self.unmark_containers_menu_item.set_action_name( - 'app.unmark_all_menu', - ) - - self.profile_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Pr_ofiles')) - self.profile_menu_item.set_submenu(profile_submenu) - media_sub_menu.append(self.profile_menu_item) - - if self.app_obj.debug_test_media_menu_flag \ - or self.app_obj.debug_test_code_menu_flag: - - # Separator - media_sub_menu.append(Gtk.SeparatorMenuItem()) - - if self.app_obj.debug_test_media_menu_flag: - - self.test_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Ad_d test media'), - ) - media_sub_menu.append(self.test_menu_item) - self.test_menu_item.set_action_name('app.test_menu') - - if self.app_obj.debug_test_code_menu_flag: - - self.test_code_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Run _test code'), - ) - media_sub_menu.append(self.test_code_menu_item) - self.test_code_menu_item.set_action_name('app.test_code_menu') - - # Operations column - ops_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Operations')) - self.menubar.add(ops_menu_column) - - ops_sub_menu = Gtk.Menu() - ops_menu_column.set_submenu(ops_sub_menu) - - self.check_all_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Check all'), - ) - ops_sub_menu.append(self.check_all_menu_item) - self.check_all_menu_item.set_action_name('app.check_all_menu') - - self.download_all_menu_item = \ - Gtk.MenuItem.new_with_mnemonic(_('_Download all')) - ops_sub_menu.append(self.download_all_menu_item) - self.download_all_menu_item.set_action_name('app.download_all_menu') - - self.custom_dl_all_menu_item = \ - Gtk.MenuItem.new_with_mnemonic(_('C_ustom download all')) - ops_sub_menu.append(self.custom_dl_all_menu_item) - self.custom_dl_all_menu_item.set_submenu( - self.custom_dl_popup_submenu(), - ) - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.refresh_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Refresh database...'), - ) - ops_sub_menu.append(self.refresh_db_menu_item) - self.refresh_db_menu_item.set_action_name('app.refresh_db_menu') - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - downloader = self.app_obj.get_downloader() - self.update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('U_pdate') + ' ' + downloader, - ) - ops_sub_menu.append(self.update_ytdl_menu_item) - self.update_ytdl_menu_item.set_action_name('app.update_ytdl_menu') - - self.test_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Test') + ' ' + downloader + '...', - ) - ops_sub_menu.append(self.test_ytdl_menu_item) - self.test_ytdl_menu_item.set_action_name('app.test_ytdl_menu') - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - if os.name == 'nt': - - self.install_ffmpeg_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Install _FFmpeg...'), - ) - ops_sub_menu.append(self.install_ffmpeg_menu_item) - self.install_ffmpeg_menu_item.set_action_name( - 'app.install_ffmpeg_menu', - ) - - self.install_matplotlib_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Install _matplotlib...'), - ) - ops_sub_menu.append(self.install_matplotlib_menu_item) - self.install_matplotlib_menu_item.set_action_name( - 'app.install_matplotlib_menu', - ) - - self.install_streamlink_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Install _streamlink...'), - ) - ops_sub_menu.append(self.install_streamlink_menu_item) - self.install_streamlink_menu_item.set_action_name( - 'app.install_streamlink_menu', - ) - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.tidy_up_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Tidy up _files...'), - ) - ops_sub_menu.append(self.tidy_up_menu_item) - self.tidy_up_menu_item.set_action_name( - 'app.tidy_up_menu', - ) - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.stop_operation_menu_item = \ - Gtk.MenuItem.new_with_mnemonic(_('_Stop current operation')) - ops_sub_menu.append(self.stop_operation_menu_item) - self.stop_operation_menu_item.set_action_name( - 'app.stop_operation_menu', - ) - self.stop_operation_menu_item.set_sensitive(False) - - self.stop_soon_menu_item = \ - Gtk.MenuItem.new_with_mnemonic(_('Stop _after current videos')) - ops_sub_menu.append(self.stop_soon_menu_item) - self.stop_soon_menu_item.set_action_name( - 'app.stop_soon_menu', - ) - self.stop_soon_menu_item.set_sensitive(False) - - # Livestreams column - live_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Livestreams')) - self.menubar.add(live_menu_column) - - live_sub_menu = Gtk.Menu() - live_menu_column.set_submenu(live_sub_menu) - - self.live_prefs_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Livestream preferences...'), - ) - live_sub_menu.append(self.live_prefs_menu_item) - self.live_prefs_menu_item.set_action_name('app.live_prefs_menu') - - # Separator - live_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.update_live_menu_item = \ - Gtk.MenuItem.new_with_mnemonic(_('_Update existing livestreams')) - live_sub_menu.append(self.update_live_menu_item) - self.update_live_menu_item.set_action_name('app.update_live_menu') - - self.cancel_live_menu_item = \ - Gtk.MenuItem.new_with_mnemonic(_('_Cancel all livestream alerts')) - live_sub_menu.append(self.cancel_live_menu_item) - self.cancel_live_menu_item.set_action_name('app.cancel_live_menu') - - # Help column - help_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Help')) - self.menubar.add(help_menu_column) - - help_sub_menu = Gtk.Menu() - help_menu_column.set_submenu(help_sub_menu) - - about_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_About...')) - help_sub_menu.append(about_menu_item) - about_menu_item.set_action_name('app.about_menu') - - # Separator - help_sub_menu.append(Gtk.SeparatorMenuItem()) - - tutorial_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Show _tutorial...') - ) - help_sub_menu.append(tutorial_menu_item) - tutorial_menu_item.set_action_name('app.tutorial_menu') - - # Separator - help_sub_menu.append(Gtk.SeparatorMenuItem()) - - check_version_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Check for _updates'), - ) - help_sub_menu.append(check_version_menu_item) - check_version_menu_item.set_action_name('app.check_version_menu') - - go_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Go to _website'), - ) - help_sub_menu.append(go_website_menu_item) - go_website_menu_item.set_action_name('app.go_website_menu') - - # Separator - help_sub_menu.append(Gtk.SeparatorMenuItem()) - - send_feedback_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Send _feedback'), - ) - help_sub_menu.append(send_feedback_menu_item) - send_feedback_menu_item.set_action_name('app.send_feedback_menu') - - - def setup_main_toolbar(self): - - """Called by self.setup_win(). Also called by - self.redraw_main_toolbar(). - - Sets up a Gtk.Toolbar near the top of the main window, below the menu, - replacing the previous one, if it exists. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window toolbar starts here' - ) - - # If a toolbar already exists, destroy it to make room for the new one - if self.main_toolbar: - self.grid.remove(self.main_toolbar) - - # Create a new toolbar (hidden, if required) - self.main_toolbar = Gtk.Toolbar() - if not self.app_obj.toolbar_hide_flag: - self.grid.attach(self.main_toolbar, 0, 1, 1, 1) - - # Toolbar items. If mainapp.TartubeApp.toolbar_squeeze_flag is True, - # we don't display labels in the toolbuttons - squeeze_flag = self.app_obj.toolbar_squeeze_flag - - if not squeeze_flag: - self.add_video_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_video_small'], - ), - ) - self.add_video_toolbutton.set_label(_('Videos')) - self.add_video_toolbutton.set_is_important(True) - else: - self.add_video_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_video_large'], - ), - ) - - self.main_toolbar.insert(self.add_video_toolbutton, -1) - self.add_video_toolbutton.set_tooltip_text(_('Add new videos')) - self.add_video_toolbutton.set_action_name('app.add_video_toolbutton') - - if not squeeze_flag: - self.add_channel_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_channel_small'], - ), - ) - self.add_channel_toolbutton.set_label(_('Channel')) - self.add_channel_toolbutton.set_is_important(True) - else: - self.add_channel_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_channel_large'], - ), - ) - - self.main_toolbar.insert(self.add_channel_toolbutton, -1) - self.add_channel_toolbutton.set_tooltip_text(_('Add a new channel')) - self.add_channel_toolbutton.set_action_name( - 'app.add_channel_toolbutton', - ) - - if not squeeze_flag: - self.add_playlist_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_playlist_small'], - ), - ) - self.add_playlist_toolbutton.set_label(_('Playlist')) - self.add_playlist_toolbutton.set_is_important(True) - else: - self.add_playlist_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_playlist_large'], - ), - ) - - self.main_toolbar.insert(self.add_playlist_toolbutton, -1) - self.add_playlist_toolbutton.set_tooltip_text(_('Add a new playlist')) - self.add_playlist_toolbutton.set_action_name( - 'app.add_playlist_toolbutton', - ) - - if not squeeze_flag: - self.add_folder_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_folder_small'], - ), - ) - self.add_folder_toolbutton.set_label(_('Folder')) - self.add_folder_toolbutton.set_is_important(True) - else: - self.add_folder_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_folder_large'], - ), - ) - - self.main_toolbar.insert(self.add_folder_toolbutton, -1) - self.add_folder_toolbutton.set_tooltip_text(_('Add a new folder')) - self.add_folder_toolbutton.set_action_name('app.add_folder_toolbutton') - -# # (Conversely, if there are no labels, then we have enough room for a -# # separator) -# if squeeze_flag: -# self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1) - - if not squeeze_flag: - self.check_all_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_check_small'], - ), - ) - self.check_all_toolbutton.set_label(_('Check')) - self.check_all_toolbutton.set_is_important(True) - else: - self.check_all_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_check_large'], - ), - ) - - self.main_toolbar.insert(self.check_all_toolbutton, -1) - self.check_all_toolbutton.set_tooltip_text( - _('Check all videos, channels, playlists and folders'), - ) - self.check_all_toolbutton.set_action_name('app.check_all_toolbutton') - - if not squeeze_flag: - self.download_all_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_download_small'], - ), - ) - self.download_all_toolbutton.set_label(_('Download')) - self.download_all_toolbutton.set_is_important(True) - else: - self.download_all_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_download_large'], - ), - ) - - self.main_toolbar.insert(self.download_all_toolbutton, -1) - self.download_all_toolbutton.set_tooltip_text( - _('Download all videos, channels, playlists and folders'), - ) - self.download_all_toolbutton.set_action_name( - 'app.download_all_toolbutton', - ) - -# if squeeze_flag: -# self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1) - - if not squeeze_flag: - self.stop_operation_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_stop_small'], - ), - ) - self.stop_operation_toolbutton.set_label(_('Stop')) - self.stop_operation_toolbutton.set_is_important(True) - else: - self.stop_operation_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_stop_large'], - ), - ) - - self.main_toolbar.insert(self.stop_operation_toolbutton, -1) - self.stop_operation_toolbutton.set_sensitive(False) - self.stop_operation_toolbutton.set_tooltip_text( - _('Stop the current operation'), - ) - self.stop_operation_toolbutton.set_action_name( - 'app.stop_operation_toolbutton', - ) - - if not squeeze_flag: - - # (There's only enough room in the toolbar for buttons to open the - # preferences window/download options window, when labels aren't - # visible) - self.system_prefs_toolbutton = None - self.gen_options_toolbutton = None - - else: - - self.system_prefs_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_preferences_large'], - ), - ) - - self.main_toolbar.insert(self.system_prefs_toolbutton, -1) - self.system_prefs_toolbutton.set_tooltip_text( - _('System preferences'), - ) - self.system_prefs_toolbutton.set_action_name( - 'app.system_prefs_toolbutton', - ) - - self.gen_options_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_options_large'], - ), - ) - - self.main_toolbar.insert(self.gen_options_toolbutton, -1) - self.gen_options_toolbutton.set_tooltip_text( - _('General download options'), - ) - self.gen_options_toolbutton.set_action_name( - 'app.gen_options_toolbutton', - ) - - if not squeeze_flag: - self.switch_view_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_switch_small'], - ), - ) - self.switch_view_toolbutton.set_label(_('Switch')) - self.switch_view_toolbutton.set_is_important(True) - else: - self.switch_view_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_switch_large'], - ), - ) - - self.main_toolbar.insert(self.switch_view_toolbutton, -1) - self.switch_view_toolbutton.set_tooltip_text( - _('Switch between simple and complex views'), - ) - self.switch_view_toolbutton.set_action_name( - 'app.switch_view_toolbutton', - ) - - if not squeeze_flag: - self.hide_system_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_hide_small'], - ), - ) - if not self.app_obj.toolbar_system_hide_flag: - self.hide_system_toolbutton.set_label(_('Hide')) - else: - self.hide_system_toolbutton.set_label(_('Show')) - self.hide_system_toolbutton.set_is_important(True) - else: - self.hide_system_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_hide_large'], - ), - ) - - self.main_toolbar.insert(self.hide_system_toolbutton, -1) - if not self.app_obj.toolbar_system_hide_flag: - self.hide_system_toolbutton.set_tooltip_text( - _('Hide (most) system folders'), - ) - else: - self.hide_system_toolbutton.set_tooltip_text( - _('Show all system folders'), - ) - self.hide_system_toolbutton.set_action_name( - 'app.hide_system_toolbutton', - ) - -# if squeeze_flag: -# self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1) - - if not squeeze_flag: - quit_button = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_quit_small'], - ), - ) - quit_button.set_label(_('Quit')) - quit_button.set_is_important(True) - else: - quit_button = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_quit_large'], - ), - ) - - self.main_toolbar.insert(quit_button, -1) - quit_button.set_tooltip_text(_('Close Tartube')) - quit_button.set_action_name('app.quit_toolbutton') - - - def setup_notebook(self): - - """Called by self.setup_win(). - - Sets up a Gtk.Notebook occupying all the space below the menu and - toolbar. Creates two tabs, the Videos tab and the Progress tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window tabs are defined here' - ) - - self.notebook = Gtk.Notebook() - self.grid.attach(self.notebook, 0, 2, 1, 1) - self.notebook.set_border_width(self.spacing_size) - self.notebook.connect('switch-page', self.on_notebook_switch_page) - - # Videos tab - self.videos_tab = Gtk.Box() - self.videos_label = Gtk.Label.new_with_mnemonic(_('_Videos')) - self.notebook.append_page(self.videos_tab, self.videos_label) - self.videos_tab.set_hexpand(True) - self.videos_tab.set_vexpand(True) - self.videos_tab.set_border_width(self.spacing_size) - - # Progress tab - self.progress_tab = Gtk.Box() - self.progress_label = Gtk.Label.new_with_mnemonic(_('_Progress')) - self.notebook.append_page(self.progress_tab, self.progress_label) - self.progress_tab.set_hexpand(True) - self.progress_tab.set_vexpand(True) - self.progress_tab.set_border_width(self.spacing_size) - - # Classic Mode tab - self.classic_tab = Gtk.Box() - self.classic_label = Gtk.Label.new_with_mnemonic(_('_Classic Mode')) - if not __main__.__pkg_no_download_flag__: - self.notebook.append_page(self.classic_tab, self.classic_label) - self.classic_tab.set_hexpand(True) - self.classic_tab.set_vexpand(True) - self.classic_tab.set_border_width(self.spacing_size) - - # Drag and Drop tab - self.drag_drop_tab = Gtk.Box() - self.drag_drop_label = Gtk.Label.new_with_mnemonic(_('_Drag and Drop')) - if not __main__.__pkg_no_download_flag__: - self.notebook.append_page(self.drag_drop_tab, self.drag_drop_label) - self.drag_drop_tab.set_hexpand(True) - self.drag_drop_tab.set_vexpand(True) - self.drag_drop_tab.set_border_width(self.spacing_size) - - # Output tab - self.output_tab = Gtk.Box() - self.output_label = Gtk.Label.new_with_mnemonic(_('_Output')) - self.notebook.append_page(self.output_tab, self.output_label) - self.output_tab.set_hexpand(True) - self.output_tab.set_vexpand(True) - self.output_tab.set_border_width(self.spacing_size) - - # Errors/Warnings tab - self.errors_tab = Gtk.Box() - self.errors_label = Gtk.Label.new_with_mnemonic( - _('_Errors / Warnings'), - ) - self.notebook.append_page(self.errors_tab, self.errors_label) - self.errors_tab.set_hexpand(True) - self.errors_tab.set_vexpand(True) - self.errors_tab.set_border_width(self.spacing_size) - - - def setup_videos_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Videos tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Videos tab' - ) - - self.videos_paned = Gtk.HPaned() - self.videos_tab.pack_start(self.videos_paned, True, True, 0) - self.videos_paned.set_position( - self.app_obj.main_win_videos_slider_posn, - ) - self.videos_paned.set_wide_handle(True) - - # Left-hand side - self.video_index_vbox = Gtk.VBox.new(False, self.spacing_size) - self.videos_paned.pack1(self.video_index_vbox, True, False) - # (Detect the user dragging the paned slider by checking the size of - # the vbox) - self.video_index_vbox.connect( - 'size-allocate', - self.on_paned_size_allocate, - ) - - self.video_index_frame = Gtk.Frame() - self.video_index_vbox.pack_start( - self.video_index_frame, - True, - True, - 0, - ) - - self.video_index_scrolled = Gtk.ScrolledWindow() - self.video_index_frame.add(self.video_index_scrolled) - self.video_index_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # Video index - self.video_index_reset() - - # 'Check all', 'Download all' and 'Custom download all' buttons - self.button_box = Gtk.VBox.new(True, self.spacing_size) - self.video_index_vbox.pack_start(self.button_box, False, False, 0) - - self.check_media_button = Gtk.Button() - self.button_box.pack_start(self.check_media_button, True, True, 0) - self.check_media_button.set_label(_('Check all')) - self.check_media_button.set_tooltip_text( - _('Check all videos, channels, playlists and folders'), - ) - self.check_media_button.set_action_name('app.check_all_button') - - self.download_media_button = Gtk.Button() - self.button_box.pack_start(self.download_media_button, True, True, 0) - self.download_media_button.set_label(_('Download all')) - self.download_media_button.set_tooltip_text( - _('Download all videos, channels, playlists and folders'), - ) - self.download_media_button.set_action_name('app.download_all_button') - - if self.app_obj.show_custom_dl_button_flag: - - self.custom_dl_media_button = Gtk.Button() - self.button_box.pack_start( - self.custom_dl_media_button, - True, - True, - 0 - ) - self.custom_dl_media_button.set_label(_('Custom download all')) - self.custom_dl_media_button.set_tooltip_text( - _( - 'Perform a custom download of all videos, channels,' \ - + ' playlists and folders', - ), - ) - self.custom_dl_media_button.set_action_name( - 'app.custom_dl_all_button', - ) - - # Right-hand side - self.video_catalogue_vbox = Gtk.VBox() - self.videos_paned.pack2(self.video_catalogue_vbox, True, True) - - # Video catalogue - self.catalogue_frame = Gtk.Frame() - self.video_catalogue_vbox.pack_start( - self.catalogue_frame, - True, - True, - 0, - ) - - self.catalogue_scrolled = Gtk.ScrolledWindow() - self.catalogue_frame.add(self.catalogue_scrolled) - self.catalogue_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # (An invisible VBox adds a bit of space between the Video Catalogue - # and its toolbar) - self.video_catalogue_vbox.pack_start( - Gtk.VBox(), - False, - False, - self.spacing_size / 2, - ) - - # Video catalogue toolbar - self.catalogue_toolbar_frame = Gtk.Frame() - self.video_catalogue_vbox.pack_start( - self.catalogue_toolbar_frame, - False, - False, - 0, - ) - - self.catalogue_toolbar_vbox = Gtk.VBox() - self.catalogue_toolbar_frame.add(self.catalogue_toolbar_vbox) - - self.catalogue_toolbar = Gtk.Toolbar() - self.catalogue_toolbar_vbox.pack_start( - self.catalogue_toolbar, - False, - False, - 0, - ) - - toolitem = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem, -1) - toolitem.add(Gtk.Label(_('Page') + ' ')) - - toolitem2 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem2, -1) - self.catalogue_page_entry = Gtk.Entry() - toolitem2.add(self.catalogue_page_entry) - self.catalogue_page_entry.set_text( - str(self.catalogue_toolbar_current_page), - ) - self.catalogue_page_entry.set_width_chars(4) - self.catalogue_page_entry.set_sensitive(False) - self.catalogue_page_entry.set_tooltip_text(_('Set visible page')) - self.catalogue_page_entry.connect( - 'activate', - self.on_video_catalogue_page_entry_activated, - ) - - toolitem3 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem3, -1) - toolitem3.add(Gtk.Label(' / ')) - - toolitem4 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem4, -1) - self.catalogue_last_entry = Gtk.Entry() - toolitem4.add(self.catalogue_last_entry) - self.catalogue_last_entry.set_text( - str(self.catalogue_toolbar_last_page), - ) - self.catalogue_last_entry.set_width_chars(4) - self.catalogue_last_entry.set_sensitive(False) - self.catalogue_last_entry.set_editable(False) - - # Separator. In this instance, empty labels look better than - # Gtk.SeparatorToolItem -# self.catalogue_toolbar.insert(Gtk.SeparatorToolItem(), -1) - toolitem = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem, -1) - toolitem.add(Gtk.Label(' ')) - - toolitem5 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem5, -1) - toolitem5.add(Gtk.Label(' ' + _('Size') + ' ')) - - toolitem6 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem6, -1) - self.catalogue_size_entry = Gtk.Entry() - toolitem6.add(self.catalogue_size_entry) - self.catalogue_size_entry.set_text( - str(self.app_obj.catalogue_page_size), - ) - self.catalogue_size_entry.set_width_chars(4) - self.catalogue_size_entry.set_tooltip_text(_('Set page size')) - self.catalogue_size_entry.connect( - 'activate', - self.on_video_catalogue_size_entry_activated, - ) - - # Separator - toolitem = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem, -1) - toolitem.add(Gtk.Label(' ')) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_first_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GOTO_FIRST) - else: - self.catalogue_first_button = Gtk.ToolButton.new() - self.catalogue_first_button.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_goto_first'], - ), - ) - self.catalogue_toolbar.insert(self.catalogue_first_button, -1) - self.catalogue_first_button.set_sensitive(False) - self.catalogue_first_button.set_tooltip_text(_('Go to first page')) - self.catalogue_first_button.set_action_name( - 'app.first_page_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_back_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_BACK) - else: - self.catalogue_back_button = Gtk.ToolButton.new() - self.catalogue_back_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_back']), - ) - self.catalogue_toolbar.insert(self.catalogue_back_button, -1) - self.catalogue_back_button.set_sensitive(False) - self.catalogue_back_button.set_tooltip_text(_('Go to previous page')) - self.catalogue_back_button.set_action_name( - 'app.previous_page_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_forwards_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_FORWARD) - else: - self.catalogue_forwards_button = Gtk.ToolButton.new() - self.catalogue_forwards_button.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_go_forward'], - ), - ) - self.catalogue_toolbar.insert(self.catalogue_forwards_button, -1) - self.catalogue_forwards_button.set_sensitive(False) - self.catalogue_forwards_button.set_tooltip_text(_('Go to next page')) - self.catalogue_forwards_button.set_action_name( - 'app.next_page_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_last_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GOTO_LAST) - else: - self.catalogue_last_button = Gtk.ToolButton.new() - self.catalogue_last_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_goto_last']), - ) - self.catalogue_toolbar.insert(self.catalogue_last_button, -1) - self.catalogue_last_button.set_sensitive(False) - self.catalogue_last_button.set_tooltip_text(_('Go to last page')) - self.catalogue_last_button.set_action_name( - 'app.last_page_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_scroll_up_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_UP) - else: - self.catalogue_scroll_up_button = Gtk.ToolButton.new() - self.catalogue_scroll_up_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_up']), - ) - self.catalogue_toolbar.insert(self.catalogue_scroll_up_button, -1) - self.catalogue_scroll_up_button.set_sensitive(False) - self.catalogue_scroll_up_button.set_tooltip_text(_('Scroll to top')) - self.catalogue_scroll_up_button.set_action_name( - 'app.scroll_up_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_scroll_down_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_DOWN) - else: - self.catalogue_scroll_down_button = Gtk.ToolButton.new() - self.catalogue_scroll_down_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_down']), - ) - self.catalogue_toolbar.insert(self.catalogue_scroll_down_button, -1) - self.catalogue_scroll_down_button.set_sensitive(False) - self.catalogue_scroll_down_button.set_tooltip_text( - _('Scroll to bottom'), - ) - self.catalogue_scroll_down_button.set_action_name( - 'app.scroll_down_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_show_filter_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SORT_ASCENDING) - else: - self.catalogue_show_filter_button = Gtk.ToolButton.new() - self.catalogue_show_filter_button.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_hide_filter'] - ), - ) - self.catalogue_toolbar.insert(self.catalogue_show_filter_button, -1) - if not self.app_obj.catalogue_show_filter_flag: - self.catalogue_show_filter_button.set_sensitive(False) - self.catalogue_show_filter_button.set_tooltip_text( - _('Show more settings'), - ) - self.catalogue_show_filter_button.set_action_name( - 'app.show_filter_toolbutton', - ) - - # Second toolbar, which is not actually added to the VBox until the - # call to self.update_catalogue_filter_widgets() - self.catalogue_toolbar2 = Gtk.Toolbar() - self.catalogue_toolbar2.set_visible(False) - - toolitem7 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem7, -1) - toolitem7.add(Gtk.Label(_('Sort') + ' ')) - - toolitem8 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem8, -1) - - store = Gtk.ListStore(str, str) - store.append( [ _('Upload time') , 'default'] ) - store.append( [ _('Name') , 'alpha'] ) - store.append( [ _('Download time') , 'receive'] ) - store.append( [ _('Database ID') , 'dbid'] ) - - self.catalogue_sort_combo = Gtk.ComboBox.new_with_model(store) - toolitem8.add(self.catalogue_sort_combo) - renderer_text = Gtk.CellRendererText() - self.catalogue_sort_combo.pack_start(renderer_text, True) - self.catalogue_sort_combo.add_attribute(renderer_text, 'text', 0) - self.catalogue_sort_combo.set_entry_text_column(0) - self.catalogue_sort_combo.set_sensitive(False) - # (Can't use a named action with a Gtk.ComboBox, so use a callback - # instead) - self.catalogue_sort_combo.connect( - 'changed', - self.on_video_catalogue_sort_combo_changed, - ) - - if not self.app_obj.catalogue_reverse_sort_flag: - if not self.app_obj.show_custom_icons_flag: - self.catalogue_reverse_toolbutton \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SORT_ASCENDING) - else: - self.catalogue_reverse_toolbutton = Gtk.ToolButton.new() - self.catalogue_reverse_toolbutton.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_sort_ascending'], - ), - ) - self.catalogue_reverse_toolbutton.set_tooltip_text( - _('Reverse sort'), - ) - else: - if not self.app_obj.show_custom_icons_flag: - self.catalogue_reverse_toolbutton \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SORT_DESCENDING) - else: - self.catalogue_reverse_toolbutton = Gtk.ToolButton.new() - self.catalogue_reverse_toolbutton.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_sort_descending'], - ), - ) - self.catalogue_reverse_toolbutton.set_tooltip_text( - _('Undo reverse sort'), - ) - self.catalogue_toolbar2.insert(self.catalogue_reverse_toolbutton, -1) - self.catalogue_reverse_toolbutton.set_sensitive(False) - self.catalogue_reverse_toolbutton.set_action_name( - 'app.reverse_sort_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_resort_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_REDO) - else: - self.catalogue_resort_button = Gtk.ToolButton.new() - self.catalogue_resort_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_redo']), - ) - self.catalogue_toolbar2.insert(self.catalogue_resort_button, -1) - self.catalogue_resort_button.set_sensitive(False) - self.catalogue_resort_button.set_tooltip_text( - _('Resort videos'), - ) - self.catalogue_resort_button.set_action_name( - 'app.resort_toolbutton', - ) - - # Separator - toolitem = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem, -1) -# toolitem.add(Gtk.Label(' ')) - toolitem.add(Gtk.Label(' ')) - - toolitem9 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem9, -1) - toolitem9.add(Gtk.Label(_('Find date'))) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_find_date_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND) - else: - self.catalogue_find_date_button = Gtk.ToolButton.new() - self.catalogue_find_date_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_find']), - ) - self.catalogue_toolbar2.insert(self.catalogue_find_date_button, -1) - self.catalogue_find_date_button.set_sensitive(False) - self.catalogue_find_date_button.set_tooltip_text( - _('Find videos by date'), - ) - self.catalogue_find_date_button.set_action_name( - 'app.find_date_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_cancel_date_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CANCEL) - else: - self.catalogue_cancel_date_button = Gtk.ToolButton.new() - self.catalogue_cancel_date_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_cancel']), - ) - self.catalogue_toolbar2.insert(self.catalogue_cancel_date_button, -1) - self.catalogue_cancel_date_button.set_sensitive(False) - self.catalogue_cancel_date_button.set_tooltip_text( - _('Cancel find videos by date'), - ) - self.catalogue_cancel_date_button.set_action_name( - 'app.cancel_date_toolbutton', - ) - - # Separator - toolitem = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem, -1) -# toolitem.add(Gtk.Label(' ')) - toolitem.add(Gtk.Label(' ')) - - toolitem16 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem16, -1) - toolitem16.add(Gtk.Label(_('Thumbnail size') + ' ')) - - toolitem17 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem17, -1) - - store2 = Gtk.ListStore(str, str) - thumb_size_list = self.app_obj.thumb_size_list.copy() - while thumb_size_list: - store2.append( [ thumb_size_list.pop(0), thumb_size_list.pop(0)] ) - - self.catalogue_thumb_combo = Gtk.ComboBox.new_with_model(store2) - toolitem17.add(self.catalogue_thumb_combo) - renderer_text = Gtk.CellRendererText() - self.catalogue_thumb_combo.pack_start(renderer_text, True) - self.catalogue_thumb_combo.add_attribute(renderer_text, 'text', 0) - self.catalogue_thumb_combo.set_entry_text_column(0) - self.catalogue_thumb_combo.set_sensitive(False) - self.catalogue_thumb_combo.connect( - 'changed', - self.on_video_catalogue_thumb_combo_changed, - ) - - # Third toolbar, which is likewise not added to the VBox until the call - # to self.update_catalogue_filter_widgets() - self.catalogue_toolbar3 = Gtk.Toolbar() - self.catalogue_toolbar3.set_visible(False) - - toolitem10 = Gtk.ToolItem.new() - self.catalogue_toolbar3.insert(toolitem10, -1) - toolitem10.add(Gtk.Label(_('Filter') + ' ')) - - toolitem11 = Gtk.ToolItem.new() - self.catalogue_toolbar3.insert(toolitem11, -1) - self.catalogue_filter_entry = Gtk.Entry() - toolitem11.add(self.catalogue_filter_entry) - self.catalogue_filter_entry.set_width_chars(16) - self.catalogue_filter_entry.set_sensitive(False) - self.catalogue_filter_entry.set_tooltip_text(_('Enter search text')) - - toolitem12 = Gtk.ToolItem.new() - self.catalogue_toolbar3.insert(toolitem12, -1) - self.catalogue_regex_togglebutton \ - = Gtk.ToggleButton(_('Regex')) - toolitem12.add(self.catalogue_regex_togglebutton) - self.catalogue_regex_togglebutton.set_sensitive(False) - if not self.app_obj.catalogue_use_regex_flag: - self.catalogue_regex_togglebutton.set_active(False) - else: - self.catalogue_regex_togglebutton.set_active(True) - self.catalogue_regex_togglebutton.set_tooltip_text( - _('Select if search text is a regex'), - ) - self.catalogue_regex_togglebutton.set_action_name( - 'app.use_regex_togglebutton', - ) - - # Separator - toolitem = Gtk.ToolItem.new() - self.catalogue_toolbar3.insert(toolitem, -1) - toolitem.add(Gtk.Label(' ')) - - toolitem13 = Gtk.ToolItem.new() - self.catalogue_toolbar3.insert(toolitem13, -1) - - self.catalogue_filter_name_button = Gtk.CheckButton() - toolitem13.add(self.catalogue_filter_name_button) - self.catalogue_filter_name_button.set_label(_('Names')) - self.catalogue_filter_name_button.set_active( - self.app_obj.catalogue_filter_name_flag, - ) - self.catalogue_filter_name_button.connect( - 'toggled', - self.on_filter_name_checkbutton_changed, - ) - - toolitem14 = Gtk.ToolItem.new() - self.catalogue_toolbar3.insert(toolitem14, -1) - - self.catalogue_filter_descrip_button = Gtk.CheckButton() - toolitem14.add(self.catalogue_filter_descrip_button) - self.catalogue_filter_descrip_button.set_label(_('Descriptions')) - self.catalogue_filter_descrip_button.set_active( - self.app_obj.catalogue_filter_descrip_flag, - ) - self.catalogue_filter_descrip_button.connect( - 'toggled', - self.on_filter_descrip_checkbutton_changed, - ) - - toolitem15 = Gtk.ToolItem.new() - self.catalogue_toolbar3.insert(toolitem15, -1) - - self.catalogue_filter_comment_button = Gtk.CheckButton() - toolitem15.add(self.catalogue_filter_comment_button) - self.catalogue_filter_comment_button.set_label(_('Comments')) - self.catalogue_filter_comment_button.set_active( - self.app_obj.catalogue_filter_comment_flag, - ) - self.catalogue_filter_comment_button.connect( - 'toggled', - self.on_filter_comment_checkbutton_changed, - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_apply_filter_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND) - else: - self.catalogue_apply_filter_button = Gtk.ToolButton.new() - self.catalogue_apply_filter_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_find']), - ) - self.catalogue_toolbar3.insert(self.catalogue_apply_filter_button, -1) - self.catalogue_apply_filter_button.set_sensitive(False) - self.catalogue_apply_filter_button.set_tooltip_text( - _('Filter videos'), - ) - self.catalogue_apply_filter_button.set_action_name( - 'app.apply_filter_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_cancel_filter_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CANCEL) - else: - self.catalogue_cancel_filter_button = Gtk.ToolButton.new() - self.catalogue_cancel_filter_button.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_cancel']), - ) - self.catalogue_toolbar3.insert(self.catalogue_cancel_filter_button, -1) - self.catalogue_cancel_filter_button.set_sensitive(False) - self.catalogue_cancel_filter_button.set_tooltip_text( - _('Cancel filter'), - ) - self.catalogue_cancel_filter_button.set_action_name( - 'app.cancel_filter_toolbutton', - ) - - # Fourth toolbar, which is likewise not added to the VBox until the - # call to self.update_catalogue_filter_widgets() - self.catalogue_toolbar4 = Gtk.Toolbar() - self.catalogue_toolbar4.set_visible(False) - - toolitem18 = Gtk.ToolItem.new() - self.catalogue_toolbar4.insert(toolitem18, -1) - - self.catalogue_frame_button = Gtk.CheckButton() - toolitem18.add(self.catalogue_frame_button) - self.catalogue_frame_button.set_label(_('Draw frames')) - self.catalogue_frame_button.set_active( - self.app_obj.catalogue_draw_frame_flag, - ) - self.catalogue_frame_button.connect( - 'toggled', - self.on_draw_frame_checkbutton_changed, - ) - - toolitem19 = Gtk.ToolItem.new() - self.catalogue_toolbar4.insert(toolitem19, -1) - - self.catalogue_icons_button = Gtk.CheckButton() - toolitem19.add(self.catalogue_icons_button) - self.catalogue_icons_button.set_label(_('Draw icons')) - self.catalogue_icons_button.set_active( - self.app_obj.catalogue_draw_icons_flag, - ) - self.catalogue_icons_button.connect( - 'toggled', - self.on_draw_icons_checkbutton_changed, - ) - - toolitem20 = Gtk.ToolItem.new() - self.catalogue_toolbar4.insert(toolitem20, -1) - - self.catalogue_downloaded_button = Gtk.CheckButton() - toolitem20.add(self.catalogue_downloaded_button) - self.catalogue_downloaded_button.set_label(_('Show downloaded')) - self.catalogue_downloaded_button.set_active( - self.app_obj.catalogue_draw_downloaded_flag, - ) - self.catalogue_downloaded_button.connect( - 'toggled', - self.on_draw_downloaded_checkbutton_changed, - ) - - toolitem21 = Gtk.ToolItem.new() - self.catalogue_toolbar4.insert(toolitem21, -1) - - self.catalogue_undownloaded_button = Gtk.CheckButton() - toolitem21.add(self.catalogue_undownloaded_button) - self.catalogue_undownloaded_button.set_label( - _('Show undownloaded'), - ) - self.catalogue_undownloaded_button.set_active( - self.app_obj.catalogue_draw_undownloaded_flag, - ) - self.catalogue_undownloaded_button.connect( - 'toggled', - self.on_draw_undownloaded_checkbutton_changed, - ) - - toolitem22 = Gtk.ToolItem.new() - self.catalogue_toolbar4.insert(toolitem22, -1) - - self.catalogue_blocked_button = Gtk.CheckButton() - toolitem22.add(self.catalogue_blocked_button) - self.catalogue_blocked_button.set_label(_('Show blocked')) - self.catalogue_blocked_button.set_active( - self.app_obj.catalogue_draw_blocked_flag, - ) - self.catalogue_blocked_button.connect( - 'toggled', - self.on_draw_blocked_checkbutton_changed, - ) - - # Set up the Video catalogue - self.video_catalogue_reset() - - - def setup_progress_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Progress tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Progress tab' - ) - - vbox = Gtk.VBox() - self.progress_tab.pack_start(vbox, True, True, 0) - - self.progress_paned = Gtk.VPaned() - vbox.pack_start(self.progress_paned, True, True, 0) - self.progress_paned.set_position( - self.app_obj.main_win_progress_slider_posn, - ) - self.progress_paned.set_wide_handle(True) - - # Upper half - frame = Gtk.Frame() - self.progress_paned.pack1(frame, True, False) - - self.progress_list_scrolled = Gtk.ScrolledWindow() - frame.add(self.progress_list_scrolled) - self.progress_list_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # Progress List - self.progress_list_treeview = Gtk.TreeView() - self.progress_list_scrolled.add(self.progress_list_treeview) - self.progress_list_treeview.set_can_focus(False) - # (Tooltips are initially enabled, and if necessary are disabled by a - # call to self.disable_tooltips() shortly afterwards) - self.progress_list_treeview.set_tooltip_column( - self.progress_list_tooltip_column, - ) - # (Detect right-clicks on the treeview) - self.progress_list_treeview.connect( - 'button-press-event', - self.on_progress_list_right_click, - ) - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Ext is short for a file extension, e.g. .EXE', - ) - - for i, column_title in enumerate( - [ - 'hide', 'hide', 'hide', '', _('Source'), '#', _('Status'), - _('Incoming file'), _('Ext'), '%', _('Speed'), _('ETA'), - _('Size'), - ] - ): - if not column_title: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - '', - renderer_pixbuf, - pixbuf=i, - ) - self.progress_list_treeview.append_column(column_pixbuf) - column_pixbuf.set_resizable(False) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.progress_list_treeview.append_column(column_text) - column_text.set_resizable(True) - column_text.set_min_width(self.min_column_width) - if column_title == 'hide': - column_text.set_visible(False) - - self.progress_list_liststore = Gtk.ListStore( - int, int, str, - GdkPixbuf.Pixbuf, - str, str, str, str, str, str, str, str, str, - ) - self.progress_list_treeview.set_model(self.progress_list_liststore) - - # Set the size of the 'Source' and 'Incoming file' columns. The - # others always contain few characters, so let them expand as they - # please - source_column = self.progress_list_treeview.get_column(4) - if self.app_obj.progress_list_width_source is not None \ - and self.app_obj.progress_list_width_source >= self.min_column_width: - source_column.set_fixed_width( - self.app_obj.progress_list_width_source, - ) - else: - source_column.set_fixed_width(200) - - incoming_column = self.progress_list_treeview.get_column(7) - if self.app_obj.progress_list_width_incoming is not None \ - and self.app_obj.progress_list_width_incoming >= self.min_column_width: - incoming_column.set_fixed_width( - self.app_obj.progress_list_width_incoming, - ) - else: - incoming_column.set_fixed_width(200) - - # Lower half - frame2 = Gtk.Frame() - self.progress_paned.pack2(frame2, True, False) - - self.results_list_scrolled = Gtk.ScrolledWindow() - frame2.add(self.results_list_scrolled) - self.results_list_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # Results List. Use a modified Gtk.TreeView which permits drag-and-drop - # for multiple rows - self.results_list_treeview = MultiDragDropTreeView() - self.results_list_scrolled.add(self.results_list_treeview) - # (Tooltips are initially enabled, and if necessary are disabled by a - # call to self.disable_tooltips() shortly afterwards) - self.results_list_treeview.set_tooltip_column( - self.results_list_tooltip_column, - ) - # (Detect right-clicks on the treeview) - self.results_list_treeview.connect( - 'button-press-event', - self.on_results_list_right_click, - ) - - # Allow multiple selection... - self.results_list_treeview.set_can_focus(True) - selection = self.results_list_treeview.get_selection() - selection.set_mode(Gtk.SelectionMode.MULTIPLE) - # ...and then set up drag and drop from the treeview to an external - # application (for example, an FFmpeg batch converter) - self.results_list_treeview.enable_model_drag_source( - Gdk.ModifierType.BUTTON1_MASK, - [], - Gdk.DragAction.COPY, - ) - self.results_list_treeview.drag_source_add_text_targets() - self.results_list_treeview.connect( - 'drag-data-get', - self.on_results_list_drag_data_get, - ) - - # Set up the treeview's model - for i, column_title in enumerate( - [ - 'hide', 'hide', '', _('New videos'), _('Duration'), _('Size'), - _('Date'), _('File'), '', _('Downloaded to'), - ] - ): - if not column_title: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - column_title, - renderer_pixbuf, - pixbuf=i, - ) - self.results_list_treeview.append_column(column_pixbuf) - column_pixbuf.set_resizable(False) - - elif i == 7: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - self.results_list_treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.results_list_treeview.append_column(column_text) - column_text.set_resizable(True) - column_text.set_min_width(self.min_column_width) - if column_title == 'hide': - column_text.set_visible(False) - - self.results_list_liststore = Gtk.ListStore( - int, str, - GdkPixbuf.Pixbuf, - str, str, str, str, - bool, - GdkPixbuf.Pixbuf, - str, - ) - self.results_list_treeview.set_model(self.results_list_liststore) - - # Set the size of the 'New videos' column. The others always contain - # few characters, so let them expand as they please - videos_column = self.results_list_treeview.get_column(3) - if self.app_obj.results_list_width_video is not None \ - and self.app_obj.results_list_width_video >= self.min_column_width: - videos_column.set_fixed_width( - self.app_obj.results_list_width_video, - ) - - else: - videos_column.set_fixed_width(300) - - # Strip of widgets at the bottom, arranged in a grid - grid = Gtk.Grid() - vbox.pack_start(grid, False, False, 0) - grid.set_vexpand(False) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - self.num_worker_checkbutton = Gtk.CheckButton() - grid.attach(self.num_worker_checkbutton, 0, 0, 1, 1) - self.num_worker_checkbutton.set_label(_('Max downloads')) - self.num_worker_checkbutton.set_active( - self.app_obj.num_worker_apply_flag, - ) - self.num_worker_checkbutton.connect( - 'toggled', - self.on_num_worker_checkbutton_changed, - ) - - self.num_worker_spinbutton = Gtk.SpinButton.new_with_range( - self.app_obj.num_worker_min, - self.app_obj.num_worker_max, - 1, - ) - grid.attach(self.num_worker_spinbutton, 1, 0, 1, 1) - self.num_worker_spinbutton.set_value(self.app_obj.num_worker_default) - self.num_worker_spinbutton.connect( - 'value-changed', - self.on_num_worker_spinbutton_changed, - ) - - self.bandwidth_checkbutton = Gtk.CheckButton() - grid.attach(self.bandwidth_checkbutton, 2, 0, 1, 1) - self.bandwidth_checkbutton.set_label(_('D/L speed (KiB/s)')) - self.bandwidth_checkbutton.set_active( - self.app_obj.bandwidth_apply_flag, - ) - self.bandwidth_checkbutton.connect( - 'toggled', - self.on_bandwidth_checkbutton_changed, - ) - - self.bandwidth_spinbutton = Gtk.SpinButton.new_with_range( - self.app_obj.bandwidth_min, - self.app_obj.bandwidth_max, - 1, - ) - grid.attach(self.bandwidth_spinbutton, 3, 0, 1, 1) - self.bandwidth_spinbutton.set_value(self.app_obj.bandwidth_default) - self.bandwidth_spinbutton.connect( - 'value-changed', - self.on_bandwidth_spinbutton_changed, - ) - - # (To stop the remaining widgets on this line from constantly resizing - # themselves during a download operation, place them inside an inner - # grid) - grid2 = Gtk.Grid() - grid.attach(grid2, 4, 0, 1, 1) - - self.alt_limits_frame = Gtk.Frame() - grid2.attach(self.alt_limits_frame, 0, 0, 1, 1) - self.alt_limits_frame.set_tooltip_text( - _('Alternative limits do not currently apply'), - ) - - self.alt_limits_image = Gtk.Image() - self.alt_limits_frame.add(self.alt_limits_image) - self.alt_limits_image.set_from_pixbuf( - self.pixbuf_dict['limits_off_large'], - ) - - self.video_res_checkbutton = Gtk.CheckButton() - grid2.attach(self.video_res_checkbutton, 1, 0, 1, 1) - self.video_res_checkbutton.set_label(_('Video resolution')) - self.video_res_checkbutton.set_active( - self.app_obj.video_res_apply_flag, - ) - self.video_res_checkbutton.connect( - 'toggled', - self.on_video_res_checkbutton_changed, - ) - - store = Gtk.ListStore(str) - for string in formats.VIDEO_RESOLUTION_LIST: - store.append( [string] ) - - self.video_res_combobox = Gtk.ComboBox.new_with_model(store) - grid2.attach(self.video_res_combobox, 2, 0, 1, 1) - renderer_text = Gtk.CellRendererText() - self.video_res_combobox.pack_start(renderer_text, True) - self.video_res_combobox.add_attribute(renderer_text, 'text', 0) - self.video_res_combobox.set_entry_text_column(0) - # (Check we're using a recognised value) - resolution = self.app_obj.video_res_default - if not resolution in formats.VIDEO_RESOLUTION_LIST: - resolution = formats.VIDEO_RESOLUTION_DEFAULT - # (Set the active item) - self.video_res_combobox.set_active( - formats.VIDEO_RESOLUTION_LIST.index(resolution), - ) - self.video_res_combobox.connect( - 'changed', - self.on_video_res_combobox_changed, - ) - - self.hide_finished_checkbutton = Gtk.CheckButton() - grid.attach(self.hide_finished_checkbutton, 0, 1, 2, 1) - self.hide_finished_checkbutton.set_label( - _('Hide rows when they are finished'), - ) - self.hide_finished_checkbutton.set_active( - self.app_obj.progress_list_hide_flag, - ) - self.hide_finished_checkbutton.connect( - 'toggled', - self.on_hide_finished_checkbutton_changed, - ) - - self.reverse_results_checkbutton = Gtk.CheckButton() - grid.attach(self.reverse_results_checkbutton, 2, 1, 4, 1) - self.reverse_results_checkbutton.set_label( - _('Add newest videos to the top')) - self.reverse_results_checkbutton.set_active( - self.app_obj.results_list_reverse_flag, - ) - self.reverse_results_checkbutton.connect( - 'toggled', - self.on_reverse_results_checkbutton_changed, - ) - - self.progress_update_label = Gtk.Label() - grid.attach(self.progress_update_label, 4, 1, 1, 1) - self.progress_update_label.set_alignment(0, 0.5) - self.progress_update_label.set_hexpand(False) - - - def setup_classic_mode_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Classic Mode tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Classic Mode tab' - ) - - self.classic_paned = Gtk.VPaned() - self.classic_tab.pack_start(self.classic_paned, True, True, 0) - self.classic_paned.set_position( - self.app_obj.main_win_classic_slider_posn, - ) - self.classic_paned.set_wide_handle(True) - - # Upper half - # ---------- - grid = Gtk.Grid() - self.classic_paned.pack1(grid, True, False) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size * 2) - - grid_width = 7 - - # First row - some decoration, and a button to open a popup menu - # -------------------------------------------------------------------- - - hbox = Gtk.HBox() - grid.attach(hbox, 0, 0, grid_width, 1) - - # (The youtube-dl-gui icon looks neat, but also solves spacing issues - # on this grid row) - frame = Gtk.Frame() - hbox.pack_start(frame, False, False, 0) - frame.set_hexpand(False) - - hbox2 = Gtk.HBox() - frame.add(hbox2) - hbox2.set_border_width(self.spacing_size) - - self.classic_banner_img = Gtk.Image() - hbox2.pack_start(self.classic_banner_img, False, False, 0) - - frame2 = Gtk.Frame() - hbox.pack_start(frame2, True, True, self.spacing_size) - frame2.set_hexpand(True) - - vbox = Gtk.VBox() - frame2.add(vbox) - vbox.set_border_width(self.spacing_size) - - self.classic_banner_label = Gtk.Label() - vbox.pack_start(self.classic_banner_label, True, True, 0) - - self.classic_banner_label2 = Gtk.Label() - vbox.pack_start(self.classic_banner_label2, True, True, 0) - - self.update_classic_mode_tab_update_banner() - - if not self.app_obj.show_custom_icons_flag: - self.classic_menu_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_INDEX, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_menu_button = Gtk.Button.new() - self.classic_menu_button.set_image( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_properties_large'], - ), - ) - hbox.pack_start(self.classic_menu_button, False, False, 0) - self.classic_menu_button.set_action_name( - 'app.classic_menu_button', - ) - self.classic_menu_button.set_tooltip_text( - _('Open the Classic Mode menu'), - ) - - # Second row - a textview for entering URLs. If automatic copy/paste is - # enabled, URLs are automatically copied into this textview - # -------------------------------------------------------------------- - - label3 = Gtk.Label( - _( - 'Enter URLs below, choose your settings, click the' \ - + ' \'Add URLs\' button, then click \'Download all\''), - ) - grid.attach(label3, 0, 1, grid_width, 1) - label3.set_alignment(0, 0.5) - - frame3 = Gtk.Frame() - grid.attach(frame3, 0, 2, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame3.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - self.classic_textview = Gtk.TextView() - scrolled.add(self.classic_textview) - self.classic_textbuffer = self.classic_textview.get_buffer() - - # (Some callbacks will complain about invalid iterators, if we try to - # use Gtk.TextIters, so use Gtk.TextMarks instead) - self.classic_mark_start = self.classic_textbuffer.create_mark( - 'mark_start', - self.classic_textbuffer.get_start_iter(), - True, # Left gravity - ) - self.classic_mark_end = self.classic_textbuffer.create_mark( - 'mark_end', - self.classic_textbuffer.get_end_iter(), - False, # Not left gravity - ) - - # (When the user copy-pastes URLs into the textview, insert an - # initial newline character, so they don't have to continuously - # do that themselves) - self.classic_textview.connect( - 'paste-clipboard', - self.on_classic_textview_paste, - ) - # (If the setting is enabled, start a download operation for any valid - # URL(s), or add the URL(s) to an existing download operation) - self.classic_textbuffer.connect( - 'changed', - self.on_classic_textbuffer_changed, - ) - - # Third row - widgets to set the download destination and video/audio - # format. The user clicks the 'Add URLs' button to create dummy - # media.Video objects for each URL. Each object is associated with - # the specified destination and format - # -------------------------------------------------------------------- - - # Destination directory - label4 = Gtk.Label(_('Destination:')) - grid.attach(label4, 0, 3, 1, 1) - label4.set_xalign(0) - label4.set_hexpand(False) - - self.classic_dest_dir_liststore = Gtk.ListStore(str) - for string in self.app_obj.classic_dir_list: - self.classic_dest_dir_liststore.append( [string] ) - - self.classic_dest_dir_combo = Gtk.ComboBox.new_with_model( - self.classic_dest_dir_liststore, - ) - grid.attach(self.classic_dest_dir_combo, 1, 3, 4, 1) - renderer_text = Gtk.CellRendererText() - self.classic_dest_dir_combo.pack_start(renderer_text, True) - self.classic_dest_dir_combo.add_attribute(renderer_text, 'text', 0) - self.classic_dest_dir_combo.set_entry_text_column(0) - self.classic_dest_dir_combo.set_active(0) - self.classic_dest_dir_combo.set_hexpand(True) - self.classic_dest_dir_combo.connect( - 'changed', - self.on_classic_dest_dir_combo_changed, - ) - - if not self.app_obj.show_custom_icons_flag: - self.classic_dest_dir_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_ADD, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_dest_dir_button = Gtk.Button.new() - self.classic_dest_dir_button.set_image( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_add']), - ) - grid.attach(self.classic_dest_dir_button, 5, 3, 1, 1) - self.classic_dest_dir_button.set_action_name( - 'app.classic_dest_dir_button', - ) - self.classic_dest_dir_button.set_tooltip_text( - _('Add a new destination folder'), - ) - self.classic_dest_dir_button.set_hexpand(False) - - if not self.app_obj.show_custom_icons_flag: - self.classic_dest_dir_open_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_OPEN, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_dest_dir_open_button = Gtk.Button.new() - self.classic_dest_dir_open_button.set_image( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_open']), - ) - grid.attach(self.classic_dest_dir_open_button, 6, 3, 1, 1) - self.classic_dest_dir_open_button.set_action_name( - 'app.classic_dest_dir_open_button', - ) - self.classic_dest_dir_open_button.set_tooltip_text( - _('Open the destination folder'), - ) - self.classic_dest_dir_open_button.set_hexpand(False) - - # Fourth row - # -------------------------------------------------------------------- - - # Video/audio format - label5 = Gtk.Label(_('Format:')) - grid.attach(label5, 0, 4, 1, 1) - label5.set_xalign(0) - label5.set_hexpand(False) - - combo_list = [_('Default') + ' ', _('Video:')] - for item in formats.VIDEO_FORMAT_LIST: - combo_list.append(' ' + item) - - combo_list.append(_('Audio:')) - for item in formats.AUDIO_FORMAT_LIST: - combo_list.append(' ' + item) - - self.classic_format_liststore = Gtk.ListStore(str) - for string in combo_list: - self.classic_format_liststore.append( [string] ) - - self.classic_format_combo = Gtk.ComboBox.new_with_model( - self.classic_format_liststore, - ) - grid.attach(self.classic_format_combo, 1, 4, 1, 1) - renderer_text = Gtk.CellRendererText() - self.classic_format_combo.pack_start(renderer_text, True) - self.classic_format_combo.add_attribute(renderer_text, 'text', 0) - self.classic_format_combo.set_entry_text_column(0) - self.classic_format_combo.set_hexpand(False) - # (Signal connect appears below) - - # (The None value represents the first line in the combo, 'Default') - if self.app_obj.classic_format_selection is None: - self.classic_format_combo.set_active(0) - else: - for i in range(len(combo_list)): - if combo_list[i] == ' ' \ - + self.app_obj.classic_format_selection: - self.classic_format_combo.set_active(i) - break - - # Video resolution - combo_list2 = [_('Highest')] - for item in formats.VIDEO_RESOLUTION_LIST: - combo_list2.append(' ' + item) - - self.classic_resolution_liststore = Gtk.ListStore(str) - for string in combo_list2: - self.classic_resolution_liststore.append( [string] ) - - self.classic_resolution_combo = Gtk.ComboBox.new_with_model( - self.classic_resolution_liststore, - ) - grid.attach(self.classic_resolution_combo, 2, 4, 1, 1) - renderer_text = Gtk.CellRendererText() - self.classic_resolution_combo.pack_start(renderer_text, True) - self.classic_resolution_combo.add_attribute(renderer_text, 'text', 0) - self.classic_resolution_combo.set_entry_text_column(0) - self.classic_resolution_combo.set_hexpand(False) - # (Signal connect appears below) - - # (The None value represents the first line in the combo, 'Resolution') - if self.app_obj.classic_resolution_selection is None: - self.classic_resolution_combo.set_active(0) - else: - for i in range(len(combo_list2)): - if combo_list2[i] == ' ' \ - + self.app_obj.classic_resolution_selection: - self.classic_resolution_combo.set_active(i) - break - - # Clarifiers - combo_list3 = [ - _('Convert to this format'), - _('Download in this format'), - ] - - self.classic_convert_liststore = Gtk.ListStore(str) - for string in combo_list3: - self.classic_convert_liststore.append( [string] ) - - self.classic_convert_combo = Gtk.ComboBox.new_with_model( - self.classic_convert_liststore, - ) - grid.attach(self.classic_convert_combo, 3, 4, 1, 1) - renderer_text = Gtk.CellRendererText() - self.classic_convert_combo.pack_start(renderer_text, True) - self.classic_convert_combo.add_attribute(renderer_text, 'text', 0) - self.classic_convert_combo.set_entry_text_column(0) - # (Signal connect appears below) - if self.app_obj.classic_format_convert_flag: - self.classic_convert_combo.set_active(0) - else: - self.classic_convert_combo.set_active(1) - self.classic_convert_combo.set_hexpand(False) - if not self.app_obj.classic_format_selection: - self.classic_convert_combo.set_sensitive(False) - - # (Invisible label for spacing) - label6 = Gtk.Label('') - grid.attach(label6, 4, 4, 1, 1) - label6.set_hexpand(True) - - # (Signal connects from above) - # If the user selects the 'Default' item, desensitise the radiobuttons - # If the user selects the 'Video:' or 'Audio:' items, automatically - # select the first item below that - self.classic_format_combo.connect( - 'changed', - self.on_classic_format_combo_changed, - ) - self.classic_resolution_combo.connect( - 'changed', - self.on_classic_resolution_combo_changed, - ) - self.classic_convert_combo.connect( - 'changed', - self.on_classic_convert_combo_changed, - ) - - # Add clips button - self.classic_add_clips_button = Gtk.Button( - ' ' + _('Add video clips') + ' ', - ) - grid.attach(self.classic_add_clips_button, 5, 4, 2, 1) - self.classic_add_clips_button.set_action_name( - 'app.classic_add_clips_button', - ) - self.classic_add_clips_button.set_tooltip_text(_('Add video clips')) - self.classic_add_clips_button.set_hexpand(False) - - # Fifth row - # -------------------------------------------------------------------- - - # Other settings - label7 = Gtk.Label(_('Other:')) - grid.attach(label7, 0, 5, 1, 1) - label7.set_xalign(0) - label7.set_hexpand(False) - - self.classic_livestream_checkbutton = Gtk.CheckButton() - grid.attach(self.classic_livestream_checkbutton, 1, 5, 1, 1) - self.classic_livestream_checkbutton.set_label(_('Is a livestream')) - if self.app_obj.classic_livestream_flag: - self.classic_livestream_checkbutton.set_active(True) - self.classic_livestream_checkbutton.connect( - 'toggled', - self.on_classic_livestream_checkbutton_toggled, - ) - - self.classic_sblock_checkbutton = Gtk.CheckButton() - grid.attach(self.classic_sblock_checkbutton, 2, 5, 2, 1) - self.classic_sblock_checkbutton.set_label( - _('Use SponsorBlock (requires FFmpeg v5.1+)'), - ) - if self.app_obj.classic_sblock_flag: - self.classic_sblock_checkbutton.set_active(True) - self.classic_sblock_checkbutton.connect( - 'toggled', - self.on_classic_sblock_checkbutton_toggled, - ) - - # Add URLs button - self.classic_add_urls_button = Gtk.Button( - ' ' + _('Add URLs') + ' ', - ) - grid.attach(self.classic_add_urls_button, 5, 5, 2, 1) - self.classic_add_urls_button.set_action_name( - 'app.classic_add_urls_button', - ) - self.classic_add_urls_button.set_tooltip_text(_('Add these URLs')) - self.classic_add_urls_button.set_hexpand(False) - - # Bottom half - # ----------- - grid2 = Gtk.Grid() - self.classic_paned.pack2(grid2, True, False) - grid2.set_column_spacing(self.spacing_size) - grid2.set_row_spacing(self.spacing_size * 2) - - # Sixth row - the Classic Progress List. A treeview to display the - # progress of downloads (in Classic Mode, ongoing download - # information is displayed here, rather than in the Progress tab) - # -------------------------------------------------------------------- - - frame4 = Gtk.Frame() - grid2.attach(frame4, 0, 1, 1, 1) - frame4.set_hexpand(True) - frame4.set_vexpand(True) - - scrolled2 = Gtk.ScrolledWindow() - frame4.add(scrolled2) - scrolled2.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - self.classic_progress_treeview = MultiDragDropTreeView() - scrolled2.add(self.classic_progress_treeview) - # (Tooltips are initially enabled, and if necessary are disabled by a - # call to self.disable_tooltips() shortly afterwards) - self.classic_progress_treeview.set_tooltip_column( - self.classic_progress_tooltip_column, - ) - # (Detect right-clicks on the treeview) - self.classic_progress_treeview.connect( - 'button-press-event', - self.on_classic_progress_list_right_click, - ) - - # (Enable selection of multiple lines) - self.classic_progress_treeview.set_can_focus(True) - selection = self.classic_progress_treeview.get_selection() - selection.set_mode(Gtk.SelectionMode.MULTIPLE) - # ...and then set up drag and drop from the treeview to an external - # application (for example, an FFmpeg batch converter) - self.classic_progress_treeview.enable_model_drag_source( - Gdk.ModifierType.BUTTON1_MASK, - [], - Gdk.DragAction.COPY, - ) - self.classic_progress_treeview.drag_source_add_text_targets() - self.classic_progress_treeview.connect( - 'drag-data-get', - self.on_classic_progress_list_drag_data_get, - ) - - for i, column_title in enumerate( - [ - 'hide', 'hide', _('Source'), '#', _('Status'), - _('Incoming file'), _('Ext'), '%', _('Speed'), _('ETA'), - _('Size'), - ] - ): - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.classic_progress_treeview.append_column(column_text) - column_text.set_resizable(True) - column_text.set_min_width(self.min_column_width) - if column_title == 'hide': - column_text.set_visible(False) - - self.classic_progress_liststore = Gtk.ListStore( - int, str, str, str, str, str, str, str, str, str, str, - ) - self.classic_progress_treeview.set_model( - self.classic_progress_liststore, - ) - - # Set the size of the 'Source' and 'Incoming file' columns. The - # others always contain few characters, so let them expand as they - # please - source_column = self.classic_progress_treeview.get_column(2) - if self.app_obj.classic_progress_list_width_source is not None \ - and self.app_obj.classic_progress_list_width_source \ - >= self.min_column_width: - source_column.set_fixed_width( - self.app_obj.classic_progress_list_width_source, - ) - - else: - source_column.set_fixed_width(200) - - incoming_column = self.classic_progress_treeview.get_column(5) - if self.app_obj.classic_progress_list_width_incoming is not None \ - and self.app_obj.classic_progress_list_width_incoming \ - >= self.min_column_width: - incoming_column.set_fixed_width( - self.app_obj.classic_progress_list_width_incoming, - ) - - else: - incoming_column.set_fixed_width(200) - - # Seventh row - a strip of buttons that apply to rows in the Classic - # Progress List. We use another new hbox to avoid messing up the - # grid layout - # -------------------------------------------------------------------- - - hbox3 = Gtk.HBox() - grid2.attach(hbox3, 0, 2, 1, 1) - - if not self.app_obj.show_custom_icons_flag: - self.classic_play_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_MEDIA_PLAY, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_play_button = Gtk.Button.new() - self.classic_play_button.set_image( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_media_play'], - ), - ) - hbox3.pack_start(self.classic_play_button, False, False, 0) - self.classic_play_button.set_action_name( - 'app.classic_play_button', - ) - self.classic_play_button.set_tooltip_text(_('Play video')) - - if not self.app_obj.show_custom_icons_flag: - self.classic_open_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_OPEN, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_open_button = Gtk.Button.new() - self.classic_open_button.set_image( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_open'], - ), - ) - hbox3.pack_start( - self.classic_open_button, - False, - False, - self.spacing_size, - ) - self.classic_open_button.set_action_name( - 'app.classic_open_button', - ) - self.classic_open_button.set_tooltip_text(_('Open destination(s)')) - - if not self.app_obj.show_custom_icons_flag: - self.classic_stop_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_MEDIA_STOP, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_stop_button = Gtk.Button.new() - self.classic_stop_button.set_image( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_media_stop'], - ), - ) - hbox3.pack_start(self.classic_stop_button, False, False, 0) - self.classic_stop_button.set_action_name( - 'app.classic_stop_button', - ) - self.classic_stop_button.set_tooltip_text(_('Stop download')) - - if not self.app_obj.show_custom_icons_flag: - self.classic_redownload_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_REFRESH, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_redownload_button = Gtk.Button.new() - self.classic_redownload_button.set_image( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_refresh']), - ) - hbox3.pack_start( - self.classic_redownload_button, - False, - False, - self.spacing_size, - ) - self.classic_redownload_button.set_action_name( - 'app.classic_redownload_button', - ) - self.classic_redownload_button.set_tooltip_text(_('Re-download')) - - self.classic_archive_button = Gtk.ToggleButton.new() - self.classic_archive_button.set_image( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_file']), - ) - hbox3.pack_start(self.classic_archive_button, False, False, 0) - self.classic_archive_button.set_action_name( - 'app.classic_archive_button', - ) - self.classic_archive_button.set_tooltip_text( - utils.tidy_up_long_string( - _( - 'Allow downloader to create an archive file (enable this' \ - + ' only when downloading channels and playlists)', - ), - self.long_string_max_len, - ), - ) - if self.app_obj.classic_ytdl_archive_flag: - self.classic_archive_button.set_active(True) - - if not self.app_obj.show_custom_icons_flag: - self.classic_clips_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_CUT, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_clips_button = Gtk.Button.new() - self.classic_clips_button.set_image( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_cut'], - ), - ) - hbox3.pack_start( - self.classic_clips_button, - False, - False, - self.spacing_size, - ) - self.classic_clips_button.set_action_name( - 'app.classic_clips_button', - ) - self.classic_clips_button.set_tooltip_text(_('Create video clips')) - - if not self.app_obj.show_custom_icons_flag: - self.classic_ffmpeg_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_EXECUTE, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_ffmpeg_button = Gtk.Button.new() - self.classic_ffmpeg_button.set_image( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_execute'], - ), - ) - hbox3.pack_start(self.classic_ffmpeg_button, False, False, 0) - self.classic_ffmpeg_button.set_action_name( - 'app.classic_ffmpeg_button', - ) - self.classic_ffmpeg_button.set_tooltip_text(_('Process with FFmpeg')) - - if not self.app_obj.show_custom_icons_flag: - self.classic_move_up_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_GO_UP, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_move_up_button = Gtk.Button.new() - self.classic_move_up_button.set_image( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_up']), - ) - hbox3.pack_start( - self.classic_move_up_button, - False, - False, - self.spacing_size, - ) - self.classic_move_up_button.set_action_name( - 'app.classic_move_up_button', - ) - self.classic_move_up_button.set_tooltip_text(_('Move up')) - - if not self.app_obj.show_custom_icons_flag: - self.classic_move_down_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_GO_DOWN, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_move_down_button = Gtk.Button.new() - self.classic_move_down_button.set_image( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_down']), - ) - hbox3.pack_start(self.classic_move_down_button, False, False, 0) - self.classic_move_down_button.set_action_name( - 'app.classic_move_down_button', - ) - self.classic_move_down_button.set_tooltip_text(_('Move down')) - - if not self.app_obj.show_custom_icons_flag: - self.classic_remove_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_DELETE, - Gtk.IconSize.BUTTON, - ) - else: - self.classic_remove_button = Gtk.Button.new() - self.classic_remove_button.set_image( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_delete']), - ) - hbox3.pack_start( - self.classic_remove_button, - False, - False, - self.spacing_size, - ) - self.classic_remove_button.set_action_name( - 'app.classic_remove_button', - ) - self.classic_remove_button.set_tooltip_text(_('Remove from list')) - - if not self.app_obj.classic_custom_dl_flag: - self.classic_download_button = Gtk.Button( - ' ' + _('Download all') + ' ', - ) - else: - self.classic_download_button = Gtk.Button( - ' ' + _('Custom download all') + ' ', - ) - hbox3.pack_end(self.classic_download_button, False, False, 0) - self.classic_download_button.set_action_name( - 'app.classic_download_button', - ) - if not self.app_obj.classic_custom_dl_flag: - self.classic_download_button.set_tooltip_text( - _('Download the URLs above'), - ) - else: - self.classic_download_button.set_tooltip_text( - _('Perform a custom download on the URLs above'), - ) - - self.classic_clear_button = Gtk.Button( - ' ' + _('Clear all') + ' ', - ) - hbox3.pack_end( - self.classic_clear_button, - False, - False, - self.spacing_size, - ) - self.classic_clear_button.set_action_name( - 'app.classic_clear_button', - ) - self.classic_clear_button.set_tooltip_text( - _('Clear the URLs above'), - ) - - self.classic_clear_dl_button = Gtk.Button( - ' ' + _('Clear downloaded') + ' ', - ) - hbox3.pack_end(self.classic_clear_dl_button, False, False, 0) - self.classic_clear_dl_button.set_action_name( - 'app.classic_clear_dl_button', - ) - self.classic_clear_dl_button.set_tooltip_text( - _('Clear the URLs above'), - ) - - - def setup_drag_drop_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Drag and Drop tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Drag and Drop tab' - ) - - grid = Gtk.Grid() - self.drag_drop_tab.pack_start(grid, True, True, 0) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - # Upper strip - # ----------- - - hbox = Gtk.HBox() - grid.attach(hbox, 0, 0, 1, 1) - - frame = Gtk.Frame() - hbox.pack_start(frame, False, False, 0) - frame.set_hexpand(False) - - hbox2 = Gtk.HBox() - frame.add(hbox2) - hbox2.set_border_width(self.spacing_size) - - img = Gtk.Image() - hbox2.pack_start(img, False, False, 0) - img.set_from_pixbuf(self.pixbuf_dict['cursor_large']) - - frame2 = Gtk.Frame() - hbox.pack_start(frame2, True, True, self.spacing_size) - frame2.set_hexpand(True) - - vbox2 = Gtk.VBox() - frame2.add(vbox2) - vbox2.set_border_width(self.spacing_size) - - label = Gtk.Label() - vbox2.pack_start(label, True, True, 0) - label.set_markup( - '' + _( - 'When you drag a video here, it is added to the Classic Mode' \ - + ' tab', - ) + '', - ) - - label2 = Gtk.Label() - vbox2.pack_start(label2, True, True, 0) - label2.set_markup( - '' + _( - 'Each zone represents a set of download options', - ) + '', - ) - - if os.name == 'nt': - - label3 = Gtk.Label() - vbox2.pack_start(label3, True, True, 0) - label3.set_markup( - '' + _( - 'Warning: Drag and drop does not work well on MS Windows', - ) + '', - ) - - if not self.app_obj.show_custom_icons_flag: - self.drag_drop_add_button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_ADD, - Gtk.IconSize.BUTTON, - ) - else: - self.drag_drop_add_button = Gtk.Button.new() - self.drag_drop_add_button.set_image( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_add'], - ), - ) - hbox.pack_start(self.drag_drop_add_button, False, False, 0) - self.drag_drop_add_button.set_action_name( - 'app.drag_drop_add_button', - ) - self.drag_drop_add_button.set_tooltip_text( - _('Add a new dropzone'), - ) - - # Drag and Drop Grid - # ------------------ - - # Use a frame, containing a grid. The frame is more convenient, because - # its border can be made invisible, and we can use .get_child() and - # .remove() - self.drag_drop_frame = Gtk.Frame() - grid.attach(self.drag_drop_frame, 0, 1, 1, 1) - self.drag_drop_frame.set_border_width(0) - self.drag_drop_frame.set_hexpand(True) - self.drag_drop_frame.set_vexpand(True) - self.drag_drop_frame.set_shadow_type(Gtk.ShadowType.NONE) - - self.drag_drop_grid_reset() - - - def setup_output_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Output tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Output tab' - ) - - grid = Gtk.Grid() - self.output_tab.pack_start(grid, True, True, 0) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - grid_width = 3 - - # During a download operation, each page in the Output tab's - # Gtk.Notebook displays output from a single downloads.DownloadWorker - # object - # The pages are added later, via a call to - # self.output_tab_setup_pages() - self.output_notebook = Gtk.Notebook() - grid.attach(self.output_notebook, 0, 0, grid_width, 1) - self.output_notebook.set_border_width(0) - self.output_notebook.set_scrollable(True) - - # When the user switches between notebook pages, scroll the visible - # page's textview to the bottom (otherwise it gets confusing) - self.output_notebook.connect( - 'switch-page', - self.on_output_notebook_switch_page, - ) - - # Strip of widgets at the bottom, visible for all tabs - self.output_size_checkbutton = Gtk.CheckButton() - grid.attach(self.output_size_checkbutton, 1, 1, 1, 1) - self.output_size_checkbutton.set_label(_('Maximum page size')) - self.output_size_checkbutton.set_active( - self.app_obj.output_size_apply_flag, - ) - self.output_size_checkbutton.set_hexpand(False) - self.output_size_checkbutton.connect( - 'toggled', - self.on_output_size_checkbutton_changed, - ) - - self.output_size_spinbutton = Gtk.SpinButton.new_with_range( - self.app_obj.output_size_min, - self.app_obj.output_size_max, - 1, - ) - grid.attach(self.output_size_spinbutton, 2, 1, 1, 1) - self.output_size_spinbutton.set_value(self.app_obj.output_size_default) - self.output_size_spinbutton.set_hexpand(False) - self.output_size_spinbutton.connect( - 'value-changed', - self.on_output_size_spinbutton_changed, - ) - - # (Add an empty label for spacing) - label = Gtk.Label() - grid.attach(label, 0, 1, 1, 1) - label.set_hexpand(True) - - - def setup_errors_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Errors/Warnings tab. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Errors / Warnings tab' - ) - - vbox = Gtk.VBox() - self.errors_tab.pack_start(vbox, True, True, 0) - - # Errors List. Use a modified Gtk.TreeView which permits drag-and-drop - # for multiple rows - self.errors_list_frame = Gtk.Frame() - vbox.pack_start(self.errors_list_frame, True, True, 0) - - self.errors_list_reset() - - # Strips of widgets at the bottom - - # (First row) - hbox = Gtk.HBox() - vbox.pack_start(hbox, False, False, 0) - hbox.set_border_width(self.spacing_size) - - self.show_system_error_checkbutton = Gtk.CheckButton() - hbox.pack_start(self.show_system_error_checkbutton, False, False, 0) - self.show_system_error_checkbutton.set_label( - _('Show Tartube errors'), - ) - self.show_system_error_checkbutton.set_active( - self.app_obj.system_error_show_flag, - ) - self.show_system_error_checkbutton.connect( - 'toggled', - self.on_system_error_checkbutton_changed, - ) - - self.show_system_warning_checkbutton = Gtk.CheckButton() - hbox.pack_start(self.show_system_warning_checkbutton, False, False, 0) - self.show_system_warning_checkbutton.set_label( - _('Show Tartube warnings'), - ) - self.show_system_warning_checkbutton.set_active( - self.app_obj.system_warning_show_flag, - ) - self.show_system_warning_checkbutton.connect( - 'toggled', - self.on_system_warning_checkbutton_changed, - ) - - self.show_operation_error_checkbutton = Gtk.CheckButton() - hbox.pack_start(self.show_operation_error_checkbutton, False, False, 0) - self.show_operation_error_checkbutton.set_label( - _('Show operation errors'), - ) - self.show_operation_error_checkbutton.set_active( - self.app_obj.operation_error_show_flag, - ) - self.show_operation_error_checkbutton.connect( - 'toggled', - self.on_operation_error_checkbutton_changed, - ) - - self.show_operation_warning_checkbutton = Gtk.CheckButton() - hbox.pack_start( - self.show_operation_warning_checkbutton, - False, - False, - 0, - ) - self.show_operation_warning_checkbutton.set_label( - _('Show operation warnings'), - ) - self.show_operation_warning_checkbutton.set_active( - self.app_obj.operation_warning_show_flag, - ) - self.show_operation_warning_checkbutton.connect( - 'toggled', - self.on_operation_warning_checkbutton_changed, - ) - - # (Second row) - hbox2 = Gtk.HBox() - vbox.pack_start(hbox2, False, False, 0) - hbox2.set_border_width(self.spacing_size) - - self.show_system_date_checkbutton = Gtk.CheckButton() - hbox2.pack_start( - self.show_system_date_checkbutton, - False, - False, - 0, - ) - self.show_system_date_checkbutton.set_label( - _('Show dates'), - ) - self.show_system_date_checkbutton.set_active( - self.app_obj.system_msg_show_date_flag, - ) - self.show_system_date_checkbutton.connect( - 'toggled', - self.on_system_date_checkbutton_changed, - ) - - self.show_system_container_checkbutton = Gtk.CheckButton() - hbox2.pack_start( - self.show_system_container_checkbutton, - False, - False, - 0, - ) - self.show_system_container_checkbutton.set_label( - _('Show channel/playlist/folder names'), - ) - self.show_system_container_checkbutton.set_active( - self.app_obj.system_msg_show_container_flag, - ) - self.show_system_container_checkbutton.connect( - 'toggled', - self.on_system_container_checkbutton_changed, - ) - - self.show_system_video_checkbutton = Gtk.CheckButton() - hbox2.pack_start( - self.show_system_video_checkbutton, - False, - False, - 0, - ) - self.show_system_video_checkbutton.set_label( - _('Show video names'), - ) - self.show_system_video_checkbutton.set_active( - self.app_obj.system_msg_show_video_flag, - ) - self.show_system_video_checkbutton.connect( - 'toggled', - self.on_system_video_checkbutton_changed, - ) - - self.show_system_multi_line_checkbutton = Gtk.CheckButton() - hbox2.pack_start( - self.show_system_multi_line_checkbutton, - False, - False, - 0, - ) - self.show_system_multi_line_checkbutton.set_label( - _('Show full messages'), - ) - self.show_system_multi_line_checkbutton.set_active( - self.app_obj.system_msg_show_multi_line_flag, - ) - self.show_system_multi_line_checkbutton.connect( - 'toggled', - self.on_system_multi_line_checkbutton_changed, - ) - - # (Third row) - hbox3 = Gtk.HBox() - vbox.pack_start(hbox3, False, False, 0) - hbox3.set_border_width(self.spacing_size) - - label = Gtk.Label(_('Filter') + ' ') - hbox3.pack_start(label, False, False, 0) - - self.error_list_entry = Gtk.Entry() - hbox3.pack_start(self.error_list_entry, False, False, 0) - self.error_list_entry.set_width_chars(16) - self.error_list_entry.set_tooltip_text(_('Enter search text')) - - self.error_list_togglebutton = Gtk.ToggleButton(_('Regex')) - hbox3.pack_start(self.error_list_togglebutton, False, False, 0) - self.error_list_togglebutton.set_tooltip_text( - _('Select if search text is a regex'), - ) - - # (Empty label for spacing) - label2 = Gtk.Label(' ') - hbox3.pack_start(label2, False, False, 0) - - self.error_list_container_checkbutton = Gtk.CheckButton() - hbox3.pack_start( - self.error_list_container_checkbutton, - False, - False, - 0, - ) - self.error_list_container_checkbutton.set_label('Container names') - self.error_list_container_checkbutton.set_active(True) - - self.error_list_video_checkbutton = Gtk.CheckButton() - hbox3.pack_start(self.error_list_video_checkbutton, False, False, 0) - self.error_list_video_checkbutton.set_label('Video names') - self.error_list_video_checkbutton.set_active(True) - - self.error_list_msg_checkbutton = Gtk.CheckButton() - hbox3.pack_start(self.error_list_msg_checkbutton, False, False, 0) - self.error_list_msg_checkbutton.set_label('Messages') - self.error_list_msg_checkbutton.set_active(True) - - if not self.app_obj.show_custom_icons_flag: - self.error_list_filter_toolbutton \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND) - else: - self.error_list_filter_toolbutton = Gtk.ToolButton.new() - self.error_list_filter_toolbutton.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_find']), - ) - hbox3.pack_start(self.error_list_filter_toolbutton, False, False, 0) - self.error_list_filter_toolbutton.set_tooltip_text( - _('Filter messages'), - ) - self.error_list_filter_toolbutton.set_action_name( - 'app.apply_error_filter_toolbutton', - ) - - if not self.app_obj.show_custom_icons_flag: - self.error_list_cancel_toolbutton \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CANCEL) - else: - self.error_list_cancel_toolbutton = Gtk.ToolButton.new() - self.error_list_cancel_toolbutton.set_icon_widget( - Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_cancel']), - ) - hbox3.pack_start(self.error_list_cancel_toolbutton, False, False, 0) - self.error_list_cancel_toolbutton.set_sensitive(False) - self.error_list_cancel_toolbutton.set_tooltip_text( - _('Cancel filter'), - ) - - self.error_list_button = Gtk.Button() - hbox3.pack_end(self.error_list_button, False, False, 0) - self.error_list_button.set_label(' ' + _('Clear list') + ' ') - self.error_list_button.connect( - 'clicked', - self.on_errors_list_clear, - ) - self.error_list_cancel_toolbutton.set_action_name( - 'app.cancel_error_filter_toolbutton', - ) - - - # (Moodify main window widgets) - - - def desensitise_test_widgets(self): - - """Called by mainapp.TartubeApp.on_menu_test(). - - Clicking the Test menu item / toolbutton more than once just adds - illegal duplicate channels/playlists/folders (and non-illegal duplicate - videos), so this function is called to just disable both widgets. - """ - - if self.test_menu_item: - self.test_menu_item.set_sensitive(False) - if self.test_toolbutton: - self.test_toolbutton.set_sensitive(False) - - - def disable_dl_all_buttons(self): - - """Called by mainapp.TartubeApp.start() and - set_disable_dl_all_flag(). - - Disables (desensitises) the 'Download all' buttons and menu items. - """ - - # This setting doesn't apply during an operation. The calling code - # should have checked that mainapp.TartubeApp.disable_dl_all_flag is - # True - if not self.app_obj.current_manager_obj \ - or __main__.__pkg_no_download_flag__: - self.download_all_menu_item.set_sensitive(False) - self.custom_dl_all_menu_item.set_sensitive(False) - self.download_all_toolbutton.set_sensitive(False) - self.download_media_button.set_sensitive(False) - if self.custom_dl_media_button: - self.custom_dl_media_button.set_sensitive(False) - - - def disable_tooltips(self, update_catalogue_flag=False): - - """Called by mainapp.TartubeApp.load_config() and - .set_show_tooltips_flag(). - - Disables tooltips in the Video Index, Video Catalogue, Progress List, - Results List and Classic Mode tab (only). - - Args: - - update_catalogue_flag (bool): True when called by - .set_show_tooltips_flag(), in which case the Video Catalogue - must be redrawn - - """ - - # Update the Video Index. Using a dummy column makes the tooltips - # invisible - self.video_index_treeview.set_tooltip_column(-1) - - # Update the Video Catalogue, if a playlist/channel/folder is selected - if update_catalogue_flag and self.video_index_current_dbid: - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - self.catalogue_toolbar_current_page, - ) - - # Update the Progress List - self.progress_list_treeview.set_tooltip_column(-1) - - # Update the Results List - self.results_list_treeview.set_tooltip_column(-1) - - # Update the Classic Mode tab - self.classic_progress_treeview.set_tooltip_column(-1) - - - def enable_dl_all_buttons(self): - - """Called by mainapp.TartubeApp.set_disable_dl_all_flag(). - - Enables (sensitises) the 'Download all' buttons and menu items. - """ - - # This setting doesn't apply during an operation. The calling code - # should have checked that mainapp.TartubeApp.disable_dl_all_flag is - # False - if not self.app_obj.current_manager_obj \ - and not __main__.__pkg_no_download_flag__: - self.download_all_menu_item.set_sensitive(True) - self.custom_dl_all_menu_item.set_sensitive(True) - self.download_all_toolbutton.set_sensitive(True) - self.download_media_button.set_sensitive(True) - if self.custom_dl_media_button: - self.custom_dl_media_button.set_sensitive(True) - - - def enable_tooltips(self, update_catalogue_flag=False): - - """Called by mainapp.TartubeApp.set_show_tooltips_flag(). - - Enables tooltips in the Video Index, Video Catalogue, Progress List, - Results List and Classic Mode tab (only). - - Args: - - update_catalogue_flag (bool): True when called by - .set_show_tooltips_flag(), in which case the Video Catalogue - must be redrawn - - """ - - # Update the Video Index - self.video_index_treeview.set_tooltip_column( - self.video_index_tooltip_column, - ) - - # Update the Video Catalogue, if a playlist/channel/folder is selected - if update_catalogue_flag and self.video_index_current_dbid: - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - self.catalogue_toolbar_current_page, - ) - - # Update the Progress List - self.progress_list_treeview.set_tooltip_column( - self.progress_list_tooltip_column, - ) - - # Update the Results List - self.results_list_treeview.set_tooltip_column( - self.results_list_tooltip_column, - ) - - # Update the Classic Mode tab - self.classic_progress_treeview.set_tooltip_column( - self.classic_progress_tooltip_column, - ) - - - def force_invisible(self): - - """Called by mainapp.TartubeApp.start_continue(). - - An alternative to self.toggle_visibility(), in which the window is - made invisible (during startup). - - The calling code must check that Tartube is visible in the system tray, - or the user will be in big trouble. - """ - - self.set_visible(False) - - - def hide_progress_bar(self, skip_check_flag=False): - - """Can be called by anything. - - Called after an operation has finished to replace the progress bar in - the Videos tab with download buttons. - - Called by several ofther functions to update the existing download - buttons (to avoid Gtk crashes). - - Args: - - skip_check_flag (bool): If True, don't perform a sanity check; just - remove old widgets and replace them with new ones - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Videos tab' - ) - - if not self.progress_bar and not skip_check_flag: - return self.app_obj.system_error( - 201, - 'Videos tab progress bar is not already visible', - ) - - # Remove existing widgets. In previous code, we simply changed the - # label on on self.check_media_button, but this causes frequent - # crashes - # Get around the crashes by destroying the old widget and creating a - # new one - if self.check_media_button: - self.button_box.remove(self.check_media_button) - self.check_media_button = None - - if self.download_media_button: - self.button_box.remove(self.download_media_button) - self.check_media_button = None - - if self.custom_dl_media_button: - self.button_box.remove(self.custom_dl_media_button) - self.custom_dl_media_button = None - - if self.progress_box: - self.button_box.remove(self.progress_box) - self.progress_box = None - self.progress_bar = None - - # Add replacement widgets - self.check_media_button = Gtk.Button() - self.button_box.pack_start(self.check_media_button, True, True, 0) - if not self.video_index_marker_dict: - self.check_media_button.set_label(_('Check all')) - self.check_media_button.set_tooltip_text( - _('Check all videos, channels, playlists and folders'), - ) - else: - self.check_media_button.set_label(_('Check marked items')) - self.check_media_button.set_tooltip_text( - _('Check marked videos, channels, playlists and folders'), - ) - self.check_media_button.set_action_name('app.check_all_button') - - self.download_media_button = Gtk.Button() - self.button_box.pack_start(self.download_media_button, True, True, 0) - if not self.video_index_marker_dict: - self.download_media_button.set_label(_('Download all')) - self.download_media_button.set_tooltip_text( - _('Download all videos, channels, playlists and folders'), - ) - else: - self.download_media_button.set_label(_('Download marked items')) - self.download_media_button.set_tooltip_text( - _('Download marked videos, channels, playlists and folders'), - ) - self.download_media_button.set_action_name('app.download_all_button') - - if self.app_obj.show_custom_dl_button_flag: - - self.custom_dl_media_button = Gtk.Button() - self.button_box.pack_start( - self.custom_dl_media_button, - True, - True, - 0 - ) - if not self.video_index_marker_dict: - self.custom_dl_media_button.set_label(_('Custom download all')) - self.custom_dl_media_button.set_tooltip_text( - _( - 'Perform a custom download of all videos, channels,' \ - + ' playlists and folders', - ), - ) - else: - self.custom_dl_media_button.set_label( - _('Custom download marked items'), - ) - self.custom_dl_media_button.set_tooltip_text( - _( - 'Perform a custom download of marked videos, channels,' \ - + ' playlists and folders', - ), - ) - self.custom_dl_media_button.set_action_name( - 'app.custom_dl_all_button', - ) - - # (For some reason, the button must be desensitised after setting the - # action name) - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.download_media_button.set_sensitive(False) - else: - self.download_media_button.set_sensitive(True) - - # Make the changes visible - self.button_box.show_all() - - - def notify_desktop(self, title=None, msg=None, icon_path=None, url=None): - - """Can be called by anything. - - Creates a desktop notification (but not on MS Windows / MacOS) - - Args: - - title (str): The notification title. If None, 'Tartube' is used - used - - msg (str): The message to show. If None, 'Tartube' is used - - icon_path (str): The absolute path to the icon file to use. If - None, a default icon is used - - url (str): If specified, a 'Click to open' button is added to the - desktop notification. Clicking the button opens the URL - - """ - - # Desktop notifications don't work on MS Windows/MacOS - if mainapp.HAVE_NOTIFY_FLAG: - - if title is None: - title = 'Tartube' - - if msg is None: - # Emergency fallback - better than an empty message - msg = 'Tartube' - - if icon_path is None: - icon_path = os.path.abspath( - os.path.join( - self.icon_dir_path, - 'dialogue', - formats.DIALOGUE_ICON_DICT['system_icon'], - ), - ) - - notify_obj = Notify.Notification.new(title, msg, icon_path) - - if url is not None: - - # We need to retain a reference to the Notify.Notification, or - # the callback won't work - self.notify_desktop_count += 1 - self.notify_desktop_dict[self.notify_desktop_count] \ - = notify_obj - - notify_obj.add_action( - 'action_click', - 'Watch', - self.on_notify_desktop_clicked, - self.notify_desktop_count, - url, - ) - - notify_obj.connect( - 'closed', - self.on_notify_desktop_closed, - self.notify_desktop_count, - ) - - # Notification is ready; show it - notify_obj.show() - - - def redraw_main_toolbar(self): - - """Called by mainapp.TartubeApp.set_toolbar_squeeze_flag(). - - Redraws the main toolbar, with or without labels, depending on the - value of the flag. - """ - - if self.app_obj.toolbar_hide_flag: - # Toolbar is not visible - return - - else: - - self.setup_main_toolbar() - - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.download_all_menu_item.set_sensitive(False) - self.custom_dl_all_menu_item.set_sensitive(False) - - self.show_all() - - - def reset_sliders(self): - - """Called by config.SystemPrefWin.setup_windows_main_window_tab(). - - Resets paned sliders in various tabs to their default positions. - """ - - self.videos_paned.set_position( - self.app_obj.paned_default_size, - ) - - self.progress_paned.set_position( - self.app_obj.paned_default_size, - ) - - self.classic_paned.set_position( - self.app_obj.paned_default_size + 50, - ) - - - def resize_self(self, width, height): - - """Can be called by anything. - - Resizes the main window. - - Args: - - width, height (int): The new size in pixels of the main window. - If either (or both) values are lower than 100, then 100 is - used instead - - """ - - if width < 100: - width = 100 - if height < 100: - height = 100 - - self.resize(width, height) - - - def sensitise_check_dl_buttons(self, finish_flag, operation_type=None): - - """Called by mainapp.TartubeApp.update_manager_start(), - .update_manager_finished(), .info_manager_start() and - .info_manager_finished(). - - Modify and de(sensitise) widgets during an update or info operation. - - Args: - - finish_flag (bool): False at the start of the update operation, - True at the end of it - - operation_type (str): 'ffmpeg' for an update operation to install - FFmpeg, 'matplotlib' for an update operation to install - matplotlib, 'streamlink' for an update operation to install - streamlink, 'ytdl' for an update operation to install/update - youtube-dl, 'formats' for an info operation to fetch available - video formats, 'subs' for an info operation to fetch - available subtitles, 'test_ytdl' for an info operation in which - youtube-dl is tested, 'version' for an info operation to check - for new Tartube releases, or None when finish_flag is True - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Videos tab' - ) - - if operation_type is not None \ - and operation_type != 'ffmpeg' and operation_type != 'matplotlib' \ - and operation_type != 'streamlink' and operation_type != 'ytdl' \ - and operation_type != 'formats' and operation_type != 'subs' \ - and operation_type != 'test_ytdl' and operation_type != 'version': - return self.app_obj.system_error( - 202, - 'Invalid update/info operation argument', - ) - - # Remove existing widgets. In previous code, we simply changed the - # label on on self.check_media_button, but this causes frequent - # crashes - # Get around the crashes by destroying the old widgets and creating new - # ones - if self.check_media_button: - self.button_box.remove(self.check_media_button) - self.check_media_button = None - - if self.download_media_button: - self.button_box.remove(self.download_media_button) - self.download_media_button = None - - if self.custom_dl_media_button: - self.button_box.remove(self.custom_dl_media_button) - self.custom_dl_media_button = None - - if self.progress_box: - self.button_box.remove(self.progress_box) - self.progress_box = None - self.progress_bar = None - - # Add replacement widgets - self.check_media_button = Gtk.Button() - self.button_box.pack_start(self.check_media_button, True, True, 0) - self.check_media_button.set_action_name('app.check_all_button') - - self.download_media_button = Gtk.Button() - self.button_box.pack_start(self.download_media_button, True, True, 0) - self.download_media_button.set_action_name('app.download_all_button') - - if self.app_obj.show_custom_dl_button_flag: - - self.custom_dl_media_button = Gtk.Button() - self.button_box.pack_start( - self.custom_dl_media_button, - True, - True, - 0 - ) - self.custom_dl_media_button.set_sensitive(False) - - # Set labels on the replacement buttons - if not finish_flag: - - downloader = self.app_obj.get_downloader(); - - if operation_type == 'ffmpeg': - msg = _('Installing FFmpeg') - elif operation_type == 'matplotlib': - msg = _('Installing matplotlib') - elif operation_type == 'streamlink': - msg = _('Installing streamlink') - elif operation_type == 'ytdl': - msg = _('Updating downloader') - elif operation_type == 'formats': - msg = _('Fetching formats') - elif operation_type == 'subs': - msg = _('Fetching subtitles') - elif operation_type == 'test_ytdl': - msg = _('Testing downloader') - else: - msg = _('Contacting website') - - self.check_media_button.set_label(msg) - self.download_media_button.set_label('...') - if self.custom_dl_media_button: - self.custom_dl_media_button.set_label(msg) - - self.check_media_button.set_sensitive(False) - self.download_media_button.set_sensitive(False) - - self.sensitise_operation_widgets(False, True) - - else: - - if not self.video_index_marker_dict: - self.check_media_button.set_label(_('Check all')) - self.check_media_button.set_tooltip_text( - _('Check all videos, channels, playlists and folders'), - ) - else: - self.check_media_button.set_label(_('Check marked items')) - self.check_media_button.set_tooltip_text( - _( - 'Check marked videos, channels, playlists and' \ - + ' folders', - ), - ) - - self.check_media_button.set_sensitive(True) - - if not self.video_index_marker_dict: - self.download_media_button.set_label('Download all') - self.download_media_button.set_tooltip_text( - _('Download all videos, channels, playlists and folders'), - ) - else: - self.download_media_button.set_label('Download marked items') - self.download_media_button.set_tooltip_text( - _( - 'Download marked videos, channels, playlists and' \ - + ' folders', - ), - ) - - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.download_media_button.set_sensitive(False) - else: - self.download_media_button.set_sensitive(True) - - if self.custom_dl_media_button: - - if not self.video_index_marker_dict: - self.custom_dl_media_button.set_label( - 'Custom download all', - ) - self.custom_dl_media_button.set_tooltip_text( - _( - 'Perform a custom download of all videos, channels,' \ - + ' playlists and folders', - ), - ) - else: - self.custom_dl_media_button.set_label( - 'Custom download marked items', - ) - self.custom_dl_media_button.set_tooltip_text( - _( - 'Perform a custom download of marked videos, ' \ - + ' channels, playlists and folders', - ), - ) - - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.custom_dl_media_button.set_sensitive(False) - else: - self.custom_dl_media_button.set_sensitive(True) - - self.sensitise_operation_widgets(True, True) - - # Make the widget changes visible - self.show_all() - - - def sensitise_operation_widgets(self, sens_flag, \ - not_dl_operation_flag=False): - - """Can by called by anything. - - (De)sensitises widgets that must not be sensitised during a download/ - update/refresh/info/tidy operation. - - Args: - - sens_flag (bool): False to desensitise widget at the start of an - operation, True to re-sensitise widgets at the end of the - operation - - not_dl_operation_flag (True, False or None): False when called by - download operation functions, True when called by everything - else - - """ - - self.system_prefs_menu_item.set_sensitive(sens_flag) - self.gen_options_menu_item.set_sensitive(sens_flag) - self.reset_container_menu_item.set_sensitive(sens_flag) - self.export_db_menu_item.set_sensitive(sens_flag) - self.import_db_menu_item.set_sensitive(sens_flag) - self.import_yt_menu_item.set_sensitive(sens_flag) - self.check_all_menu_item.set_sensitive(sens_flag) - - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.download_all_menu_item.set_sensitive(False) - self.custom_dl_all_menu_item.set_sensitive(False) - else: - self.download_all_menu_item.set_sensitive(sens_flag) - self.custom_dl_all_menu_item.set_sensitive(sens_flag) - - self.refresh_db_menu_item.set_sensitive(sens_flag) - self.check_all_toolbutton.set_sensitive(sens_flag) - - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.download_all_toolbutton.set_sensitive(False) - else: - self.download_all_toolbutton.set_sensitive(sens_flag) - - if __main__.__pkg_strict_install_flag__: - self.update_ytdl_menu_item.set_sensitive(False) - else: - self.update_ytdl_menu_item.set_sensitive(sens_flag) - - self.test_ytdl_menu_item.set_sensitive(sens_flag) - - if os.name == 'nt': - self.install_ffmpeg_menu_item.set_sensitive(sens_flag) - self.install_matplotlib_menu_item.set_sensitive(sens_flag) - self.install_streamlink_menu_item.set_sensitive(sens_flag) - - if not_dl_operation_flag: - self.update_live_menu_item.set_sensitive(sens_flag) - else: - self.update_live_menu_item.set_sensitive(True) - - # (The 'Add videos', 'Add channel' etc menu items/buttons are - # sensitised during a download operation, but desensitised during - # other operations) - if not_dl_operation_flag: - self.add_video_menu_item.set_sensitive(sens_flag) - self.add_channel_menu_item.set_sensitive(sens_flag) - self.add_playlist_menu_item.set_sensitive(sens_flag) - self.add_folder_menu_item.set_sensitive(sens_flag) - self.add_video_toolbutton.set_sensitive(sens_flag) - self.add_channel_toolbutton.set_sensitive(sens_flag) - self.add_playlist_toolbutton.set_sensitive(sens_flag) - self.add_folder_toolbutton.set_sensitive(sens_flag) - - # (The 'Change database', etc menu items must remain desensitised if - # file load/save is disabled) - if not self.app_obj.disable_load_save_flag: - self.change_db_menu_item.set_sensitive(sens_flag) - self.save_db_menu_item.set_sensitive(sens_flag) - self.save_all_menu_item.set_sensitive(sens_flag) - - # (The 'Stop' button/menu item are only sensitised during a download/ - # update/refresh/info/tidy operation) - if not sens_flag: - self.stop_operation_menu_item.set_sensitive(True) - self.stop_operation_toolbutton.set_sensitive(True) - else: - self.stop_operation_menu_item.set_sensitive(False) - self.stop_operation_toolbutton.set_sensitive(False) - - if not not_dl_operation_flag and not sens_flag: - self.stop_soon_menu_item.set_sensitive(True) - else: - self.stop_soon_menu_item.set_sensitive(False) - - # (The 'System preferences' and 'General download options' buttons are - # only visible in the toolbar, when labels are not visibel) - if self.system_prefs_toolbutton: - self.system_prefs_toolbutton.set_sensitive(sens_flag) - self.gen_options_toolbutton.set_sensitive(sens_flag) - - # The corresponding buttons in the Classic Mode tab must also be - # updated - self.classic_stop_button.set_sensitive(not sens_flag) - self.classic_clips_button.set_sensitive(sens_flag) - self.classic_ffmpeg_button.set_sensitive(sens_flag) - self.classic_clear_button.set_sensitive(sens_flag) - self.classic_clear_dl_button.set_sensitive(sens_flag) - if __main__.__pkg_no_download_flag__: - self.classic_redownload_button.set_sensitive(False) - self.classic_download_button.set_sensitive(False) - elif not not_dl_operation_flag: - self.classic_redownload_button.set_sensitive(True) - self.classic_download_button.set_sensitive(sens_flag) - else: - self.classic_redownload_button.set_sensitive(sens_flag) - self.classic_download_button.set_sensitive(sens_flag) - - - def sensitise_progress_bar(self, sens_flag): - - """Called by mainapp.TartubeApp.download_manager_continue(). - - When a download operation is launched from the Classic Mode tab, we - don't replace the main Check all/Download all buttons with a progress - bar; instead, we just (de)sensitise the existing buttons. - - Args: - - sens_flag (bool): True to sensitise the buttons, False to - desensitise them - - """ - - self.check_media_button.set_sensitive(sens_flag) - self.classic_clear_button.set_sensitive(sens_flag) - self.classic_clear_dl_button.set_sensitive(sens_flag) - - if __main__.__pkg_no_download_flag__: - self.download_media_button.set_sensitive(False) - self.classic_download_button.set_sensitive(False) - self.classic_redownload_button.set_sensitive(False) - if self.custom_dl_media_button: - self.custom_dl_media_button.set_sensitive(False) - - elif self.app_obj.disable_dl_all_flag: - self.download_media_button.set_sensitive(False) - self.classic_download_button.set_sensitive(False) - self.classic_redownload_button.set_sensitive(sens_flag) - if self.custom_dl_media_button: - self.custom_dl_media_button.set_sensitive(False) - - else: - self.download_media_button.set_sensitive(sens_flag) - self.classic_download_button.set_sensitive(sens_flag) - self.classic_redownload_button.set_sensitive(sens_flag) - if self.custom_dl_media_button: - self.custom_dl_media_button.set_sensitive(sens_flag) - - - def sensitise_widgets_if_database(self, sens_flag): - - """Called by mainapp.TartubeApp.start(), .load_db(), .save_db() and - .disable_load_save(). - - When no database file has been loaded into memory, most main window - widgets should be desensitised. This function is called to sensitise - or desensitise the widgets after a change in state. - - Args: - - sens_flag (bool): True to sensitise most widgets, False to - desensitise most widgets - - """ - - # Menu items - self.change_db_menu_item.set_sensitive(sens_flag) - self.save_db_menu_item.set_sensitive(sens_flag) - self.save_all_menu_item.set_sensitive(sens_flag) - - self.system_prefs_menu_item.set_sensitive(sens_flag) - self.gen_options_menu_item.set_sensitive(sens_flag) - - self.add_video_menu_item.set_sensitive(sens_flag) - self.add_channel_menu_item.set_sensitive(sens_flag) - self.add_playlist_menu_item.set_sensitive(sens_flag) - self.add_folder_menu_item.set_sensitive(sens_flag) - - self.add_bulk_menu_item.set_sensitive(sens_flag) - self.reset_container_menu_item.set_sensitive(sens_flag) - - self.export_db_menu_item.set_sensitive(sens_flag) - self.import_db_menu_item.set_sensitive(sens_flag) - self.import_yt_menu_item.set_sensitive(sens_flag) - self.show_hide_menu_item.set_sensitive(sens_flag) - self.profile_menu_item.set_sensitive(sens_flag) - - self.check_all_menu_item.set_sensitive(sens_flag) - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.download_all_menu_item.set_sensitive(False) - self.custom_dl_all_menu_item.set_sensitive(False) - else: - self.download_all_menu_item.set_sensitive(sens_flag) - self.custom_dl_all_menu_item.set_sensitive(sens_flag) - self.refresh_db_menu_item.set_sensitive(sens_flag) - - if __main__.__pkg_strict_install_flag__: - self.update_ytdl_menu_item.set_sensitive(False) - else: - self.update_ytdl_menu_item.set_sensitive(sens_flag) - - self.test_ytdl_menu_item.set_sensitive(sens_flag) - - if os.name == 'nt': - self.install_ffmpeg_menu_item.set_sensitive(sens_flag) - self.install_matplotlib_menu_item.set_sensitive(sens_flag) - self.install_streamlink_menu_item.set_sensitive(sens_flag) - - self.stop_operation_menu_item.set_sensitive(False) - self.stop_soon_menu_item.set_sensitive(False) - - if self.test_menu_item: - self.test_menu_item.set_sensitive(sens_flag) - - # Toolbuttons - self.add_video_toolbutton.set_sensitive(sens_flag) - self.add_channel_toolbutton.set_sensitive(sens_flag) - self.add_playlist_toolbutton.set_sensitive(sens_flag) - self.add_folder_toolbutton.set_sensitive(sens_flag) - - self.check_all_toolbutton.set_sensitive(sens_flag) - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.download_all_toolbutton.set_sensitive(False) - else: - self.download_all_toolbutton.set_sensitive(sens_flag) - self.stop_operation_toolbutton.set_sensitive(False) - - if self.system_prefs_toolbutton: - self.system_prefs_toolbutton.set_sensitive(sens_flag) - self.gen_options_toolbutton.set_sensitive(sens_flag) - - self.switch_view_toolbutton.set_sensitive(sens_flag) - self.hide_system_toolbutton.set_sensitive(sens_flag) - - if self.test_toolbutton: - self.test_toolbutton.set_sensitive(sens_flag) - - # Videos tab - if self.check_media_button: - self.check_media_button.set_sensitive(sens_flag) - if self.download_media_button: - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.download_media_button.set_sensitive(False) - else: - self.download_media_button.set_sensitive(sens_flag) - if self.custom_dl_media_button: - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.disable_dl_all_flag: - self.custom_dl_media_button.set_sensitive(False) - else: - self.custom_dl_media_button.set_sensitive(sens_flag) - - # Progress tab - self.num_worker_checkbutton.set_sensitive(sens_flag) - self.num_worker_spinbutton.set_sensitive(sens_flag) - self.bandwidth_checkbutton.set_sensitive(sens_flag) - self.bandwidth_spinbutton.set_sensitive(sens_flag) - self.video_res_checkbutton.set_sensitive(sens_flag) - self.video_res_combobox.set_sensitive(sens_flag) - - # Classic Mode tab - self.classic_menu_button.set_sensitive(sens_flag) - self.classic_stop_button.set_sensitive(False) - self.classic_archive_button.set_sensitive(sens_flag) - self.classic_clips_button.set_sensitive(sens_flag) - self.classic_ffmpeg_button.set_sensitive(sens_flag) - self.classic_clear_button.set_sensitive(sens_flag) - self.classic_clear_dl_button.set_sensitive(sens_flag) - if __main__.__pkg_no_download_flag__: - self.classic_redownload_button.set_sensitive(False) - self.classic_download_button.set_sensitive(False) - else: - self.classic_redownload_button.set_sensitive(sens_flag) - self.classic_download_button.set_sensitive(sens_flag) - - # Output tab - self.output_size_checkbutton.set_sensitive(sens_flag) - self.output_size_spinbutton.set_sensitive(sens_flag) - - # Errors/Warnings tab - self.show_system_error_checkbutton.set_sensitive(sens_flag) - self.show_system_warning_checkbutton.set_sensitive(sens_flag) - self.show_operation_error_checkbutton.set_sensitive(sens_flag) - self.show_operation_warning_checkbutton.set_sensitive(sens_flag) - self.show_system_date_checkbutton.set_sensitive(sens_flag) - self.show_system_container_checkbutton.set_sensitive(sens_flag) - self.show_system_video_checkbutton.set_sensitive(sens_flag) - self.show_system_multi_line_checkbutton.set_sensitive(sens_flag) - self.error_list_entry.set_sensitive(sens_flag) - self.error_list_togglebutton.set_sensitive(sens_flag) - self.error_list_container_checkbutton.set_sensitive(sens_flag) - self.error_list_video_checkbutton.set_sensitive(sens_flag) - self.error_list_msg_checkbutton.set_sensitive(sens_flag) - if self.error_list_filter_flag: - self.error_list_filter_toolbutton.set_sensitive(False) - self.error_list_cancel_toolbutton.set_sensitive(sens_flag) - else: - self.error_list_filter_toolbutton.set_sensitive(sens_flag) - self.error_list_cancel_toolbutton.set_sensitive(False) - - - def show_progress_bar(self, operation_type): - - """Called by mainapp.TartubeApp.download_manager_continue(), - .refresh_manager_continue(), .tidy_manager_start(), - .process_manager_start(). - - At the start of a download/refresh/tidy/process operation, replace - self.download_media_button with a progress bar (and a label just above - it). - - Args: - - operation_type (str): The type of operation: 'download' for a - download operation, 'check' for a download operation with - simulated downloads, 'refresh' for a refresh operation, 'tidy' - for a tidy operation, or 'process' for a process operation - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Videos tab' - ) - - if self.progress_bar: - return self.app_obj.system_error( - 203, - 'Videos tab progress bar is already visible', - ) - - elif operation_type != 'check' \ - and operation_type != 'download' \ - and operation_type != 'refresh' \ - and operation_type != 'tidy' \ - and operation_type != 'process': - return self.app_obj.system_error( - 204, - 'Invalid operation type supplied to progress bar', - ) - - # Remove existing widgets. In previous code, we simply changed the - # label on on self.check_media_button, but this causes frequent - # crashes - # Get around the crashes by destroying the old widgets and creating new - # ones - if self.check_media_button: - self.button_box.remove(self.check_media_button) - self.check_media_button = None - - if self.download_media_button: - self.button_box.remove(self.download_media_button) - self.download_media_button = None - - if self.custom_dl_media_button: - self.button_box.remove(self.custom_dl_media_button) - self.custom_dl_media_button = None - - # Display a holding message in the replacement buttons, and initially - # in the progress bar (the latter is replaced after a very short - # interval) - free_msg = ' [' \ - + str(round(utils.disk_get_free_space(self.app_obj.data_dir), 1)) \ - + ' GiB]' - - if operation_type == 'check': - temp_msg = msg = _('Checking...') - if self.app_obj.show_free_space_flag: - msg = _('Checking') + free_msg - - elif operation_type == 'download': - temp_msg = msg = _('Downloading...') - if self.app_obj.show_free_space_flag: - msg = _('Downloading') + free_msg - - elif operation_type == 'refresh': - temp_msg = msg = _('Refreshing...') - - elif operation_type == 'tidy': - temp_msg = msg = _('Tidying...') - - else: - temp_msg = msg = _('FFmpeg processing...') - - # Add replacement widgets - self.check_media_button = Gtk.Button() - self.button_box.pack_start(self.check_media_button, True, True, 0) - self.check_media_button.set_action_name('app.check_all_button') - self.check_media_button.set_sensitive(False) - self.check_media_button.set_label(msg) - - # (Put the progress bar inside a box, so it doesn't touch the divider, - # because that doesn't look nice) - self.progress_box = Gtk.HBox() - self.button_box.pack_start(self.progress_box, True, True, 0) - - self.progress_bar = Gtk.ProgressBar() - self.progress_box.pack_start( - self.progress_bar, - True, - True, - (self.spacing_size * 2), - ) - self.progress_bar.set_fraction(0) - self.progress_bar.set_show_text(True) - self.progress_bar.set_text(temp_msg) - - # (The 'Custom download all' buttons, if they were visible, are - # replaced by empty buttons) - if self.app_obj.show_custom_dl_button_flag: - - self.custom_dl_media_button = Gtk.Button() - self.button_box.pack_start( - self.custom_dl_media_button, - True, - True, - 0 - ) - self.custom_dl_media_button.set_label(temp_msg) - self.custom_dl_media_button.set_sensitive(False) - - # Make the changes visible - self.button_box.show_all() - - - def switch_profile(self, profile_name): - - """Called from a callback in self.on_switch_profile_menu_select() and - mainapp.TartubeApp.load_db(). - - Switches to the specified profile. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - profile_name (str): The specified profile (a key in - mainapp.TartubeApp.profile_dict). - - """ - - if not profile_name in self.app_obj.profile_dict: - - return self.app_obj.system_error( - 205, - 'Unrecognised profile \'{0}\''.format(profile_name), - ) - - this_dict = self.app_obj.profile_dict[profile_name] - - # Add or remove markers from everything in the Video Index - for dbid in self.app_obj.container_reg_dict.keys(): - - if dbid in this_dict: - self.video_index_set_marker(dbid) - else: - self.video_index_reset_marker(dbid) - - self.app_obj.set_last_profile(profile_name) - - - def toggle_alt_limits_image(self, on_flag): - - """Can be called by anything. - - Toggles the icon in the Progress tab. - - Args: - - on_flag (bool): True for a normal image (signifying that - alternative performance limits currently apply), False for a - greyed-out image - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Progress tab' - ) - - if on_flag: - - self.alt_limits_image.set_from_pixbuf( - self.pixbuf_dict['limits_on_large'], - ) - - self.alt_limits_frame.set_tooltip_text( - _('Alternative limits currently apply'), - ) - - else: - - self.alt_limits_image.set_from_pixbuf( - self.pixbuf_dict['limits_off_large'], - ) - - self.alt_limits_frame.set_tooltip_text( - _('Alternative limits do not currently apply'), - ) - - - def toggle_visibility(self): - - """Called by self.on_delete_event, StatusIcon.on_button_press_event and - mainapp.TartubeApp.on_menu_close_tray(). - - Toggles the main window's visibility (usually after the user has left- - clicked the status icon in the system tray). - """ - - if self.is_visible(): - - # Record the window's position, so its position can be restored - # when the window is made visible again - posn = self.get_position() - self.win_last_xpos = posn.root_x - self.win_last_ypos = posn.root_y - # Close the window to the tray - self.set_visible(False) - - else: - - self.set_visible(True) - if self.app_obj.restore_posn_from_tray_flag \ - and self.win_last_xpos is not None: - self.move(self.win_last_xpos, self.win_last_ypos) - - - def update_catalogue_filter_widgets(self): - - """Called by mainapp.TartubeApp.start() and .on_button_show_filter(). - - The toolbar just below the Video Catalogue consists of three rows. Only - the first is visible by default. Show or hide the remaining rows, as - required. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Videos tab' - ) - - if not self.app_obj.catalogue_show_filter_flag: - - # Hide the second/third rows - if not self.app_obj.show_custom_icons_flag: - self.catalogue_show_filter_button.set_stock_id( - Gtk.STOCK_ADD, - ) - else: - self.catalogue_show_filter_button.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_show_filter'] - ), - ) - - self.catalogue_show_filter_button.set_tooltip_text( - _('Show more settings'), - ) - - if self.catalogue_toolbar2 \ - in self.catalogue_toolbar_vbox.get_children(): - self.catalogue_toolbar_vbox.remove(self.catalogue_toolbar2) - self.catalogue_toolbar_vbox.remove(self.catalogue_toolbar3) - self.catalogue_toolbar_vbox.remove(self.catalogue_toolbar4) - self.catalogue_toolbar_vbox.show_all() - - # If nothing has been selected in the Video Index, then we can - # hide rows, but not reveal them again - if self.video_index_current_dbid is None: - self.catalogue_show_filter_button.set_sensitive(False) - - else: - - # Show the second/third rows - if not self.app_obj.show_custom_icons_flag: - self.catalogue_show_filter_button.set_stock_id( - Gtk.STOCK_CLOSE, - ) - else: - self.catalogue_show_filter_button.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_hide_filter'] - ), - ) - - self.catalogue_show_filter_button.set_tooltip_text( - _('Show fewer settings'), - ) - - if not self.catalogue_toolbar2 \ - in self.catalogue_toolbar_vbox.get_children(): - - self.catalogue_toolbar_vbox.pack_start( - self.catalogue_toolbar2, - False, - False, - 0, - ) - - self.catalogue_toolbar_vbox.pack_start( - self.catalogue_toolbar3, - False, - False, - 0, - ) - - self.catalogue_toolbar_vbox.pack_start( - self.catalogue_toolbar4, - False, - False, - 0, - ) - - self.catalogue_toolbar_vbox.show_all() - - # After the parent self.catalogue_toolbar2 is added to its - # VBox, the 'Regex' button is not desensitised correctly - # (for reasons unknown) - # Desensitise it, if it should be desensitised - if self.video_index_current_dbid is None \ - or not self.video_catalogue_dict: - self.catalogue_regex_togglebutton.set_sensitive(False) - - - def update_catalogue_sort_widgets(self): - - """Called by mainapp.TartubeApp.start(). - - Videos in the Video Catalogue can be sorted in various ways. When - required, set the combobox to its correct state. - """ - - mode = self.app_obj.catalogue_sort_mode - if mode == 'default': - self.catalogue_sort_combo.set_active(0) - elif mode == 'alpha': - self.catalogue_sort_combo.set_active(1) - elif mode == 'receive': - self.catalogue_sort_combo.set_active(2) - else: - self.catalogue_sort_combo.set_active(3) - - - def update_catalogue_reverse_sort_widgets(self): - - """Called by mainapp.TartubeApp.start() and - .on_button_reverse_sort_catalogue() - - Videos in the Video Catalogue can be sorted in various ways. When - required, set the reverse sort button to its correct state. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Videos tab' - ) - - if not self.app_obj.catalogue_reverse_sort_flag: - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_reverse_toolbutton.set_stock_id( - Gtk.STOCK_SORT_ASCENDING, - ) - else: - self.catalogue_reverse_toolbutton.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_sort_ascending'] - ), - ) - - self.catalogue_reverse_toolbutton.set_tooltip_text( - _('Reverse sort'), - ) - - else: - - if not self.app_obj.show_custom_icons_flag: - self.catalogue_reverse_toolbutton.set_stock_id( - Gtk.STOCK_SORT_DESCENDING, - ) - else: - self.catalogue_reverse_toolbutton.set_icon_widget( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['stock_sort_descending'] - ), - ) - - self.catalogue_reverse_toolbutton.set_tooltip_text( - _('Undo reverse sort'), - ) - - self.catalogue_reverse_toolbutton.show_all() - - - def update_catalogue_thumb_widgets(self): - - """Called by mainapp.TartubeApp.start(). - - When arranged on a grid, thumbnails in the Video Catalogue can be shown - in a variety of different sizes. On startup, set the correct value of - the combobox. - """ - - # (IV is in groups of two, in the form [translation, actual value]) - self.catalogue_thumb_combo.set_active( - int( - self.app_obj.thumb_size_list.index( - self.app_obj.thumb_size_custom, - ) / 2, - ), - ) - - - def update_classic_mode_tab_update_banner(self): - - """Called initially by self.setup_classic_mode_tab(), and then by - several callbacks. - - Updates the layout of the banner at the top of the Classic Mode tab, - according to current settings. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Classic Mode tab' - ) - - if self.app_obj.classic_format_selection is None \ - or self.app_obj.classic_format_convert_flag: - - self.classic_banner_img.set_from_pixbuf( - self.pixbuf_dict['ytdl_gui'], - ) - self.classic_banner_label.set_markup( - '' + _( - 'This tab emulates the classic youtube-dl-gui interface', - ) + '', - ) - self.classic_banner_label2.set_markup( - '' + _( - 'Videos downloaded here are not added to Tartube\'s' \ - + ' database', - ) + '', - ) - - else: - - self.classic_banner_img.set_from_pixbuf( - self.pixbuf_dict['warning_large'], - ) - self.classic_banner_label.set_markup( - '' + _( - 'If your preferred formats are not available, the' \ - + ' download will fail!', - ) + '', - ) - self.classic_banner_label2.set_markup( - '' + _( - 'If you want a specific format, install FFMpeg and' \ - + ' select \'Convert to this format\'!', - ) + '', - ) - - - def update_menu(self): - - """Can be called by anything. - - Updates several main menu items after a change in conditions. - - Note that other code modifies the state of the main menu and toolbar; - for example, see the code in mainapp.TartubeApp.load_db(). - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window menu' - ) - - if self.update_ytdl_menu_item is not None: - - downloader = self.app_obj.get_downloader() - - self.update_ytdl_menu_item.set_label( - ('U_pdate') + ' ' + downloader, - ) - - self.test_ytdl_menu_item.set_label( - _('_Test') + ' ' + downloader, - ) - - if self.custom_dl_all_menu_item is not None: - - self.custom_dl_all_menu_item.set_submenu( - self.custom_dl_popup_submenu(), - ) - - if self.switch_profile_menu_item is not None: - - self.switch_profile_menu_item.set_submenu( - self.switch_profile_popup_submenu(), - ) - - if self.delete_profile_menu_item is not None: - - self.delete_profile_menu_item.set_submenu( - self.delete_profile_popup_submenu(), - ) - - # Make the changes visible - if self.menubar: - self.menubar.show_all() - - - def update_window_after_show_hide(self): - - """Called by mainapp.TartubeApp.on_menu_hide_system(). - - Shows or hides system folders, as required. - - Updates the appearance of the main window's toolbutton, depending on - the current setting of mainapp.TartubeApp.toolbar_system_hide_flag. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window toolbar' - ) - - # Update the appearance of the toolbar button - if not self.app_obj.toolbar_system_hide_flag: - - self.hide_system_toolbutton.set_label(_('Hide')) - self.hide_system_toolbutton.set_tooltip_text( - _('Hide (most) system folders'), - ) - - else: - - self.hide_system_toolbutton.set_label(_('Show')) - self.hide_system_toolbutton.set_tooltip_text( - _('Show all system folders'), - ) - - # After system folders are revealed/hidden, Gtk helpfully selects a - # new channel/playlist/folder in the Video Index for us - # Not sure how to stop it, other than by temporarily preventing - # selections altogether - selection = self.video_index_treeview.get_selection() - selection.set_mode(Gtk.SelectionMode.NONE) - - # Show/hide system folders - for dbid in self.app_obj.container_reg_dict: - - media_data_obj = self.app_obj.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag \ - and media_data_obj != self.app_obj.fixed_all_folder: - self.app_obj.mark_folder_hidden( - media_data_obj, - self.app_obj.toolbar_system_hide_flag, - ) - - # Re-enable selections, and select the previously-selected channel/ - # playlist/folder (if any) - selection = self.video_index_treeview.get_selection() - selection.set_mode(Gtk.SelectionMode.SINGLE) - - if self.video_index_current_dbid is not None: - - media_data_obj \ - = self.app_obj.media_reg_dict[self.video_index_current_dbid] - self.video_index_select_row(media_data_obj) - - - def update_progress_bar(self, text, count, total): - - """Called by downloads.DownloadManager.run(), - refresh.RefreshManager.refresh_from_default_destination(), - .refresh_from_actual_destination(), tidy.TidyManager.tidy_directory() - and process.Processmanager.process_video(). - - During a download/refresh/tidy/process operation, updates the progress - bar just below the Video Index. - - Args: - - text (str): The text of the progress bar's label, matching the name - of the media data object which has just been passed to - youtube-dl - - count (int): The number of media data objects passed to youtube-dl - so far. Note that a channel or a playlist counts as one media - data object, as far as youtube-dl is concerned - - total (int): The total number of media data objects to be passed - to youtube-dl - - """ - - if not self.progress_bar: - return self.app_obj.system_error( - 206, - 'Videos tab progress bar is missing and cannot be updated', - ) - - # (The 0.5 guarantees that the progress bar is never empty. If - # downloading a single video, the progress bar is half full. If - # downloading the first out of 3 videos, it is 16% full, and so on) - self.progress_bar.set_fraction(float(count - 0.5) / total) - self.progress_bar.set_text( - utils.shorten_string(text, self.short_string_max_len) \ - + ' ' + str(count) + '/' + str(total) - ) - - - def update_free_space_msg(self, disk_space=None): - - """Called by mainapp.TartubeApp.dl_timer_callback() during a download - operation to update the amount of free disk space visible in the - Videos tab. - - Args: - - disk_space (float or None): The amount of free disk space on the - drive containing Tartube's data directory. If None, the - button's label is reset to its default state - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Main window\'s Videos tab' - ) - - if self.check_media_button is None: - return - - elif disk_space is None: - - if not self.video_index_marker_dict: - self.check_media_button.set_label(_('Check all')) - self.check_media_button.set_tooltip_text( - _('Check all videos, channels, playlists and folders'), - ) - else: - self.check_media_button.set_label(_('Check marked items')) - self.check_media_button.set_tooltip_text( - _('Check marked videos, channels, playlists and folders'), - ) - - else: - - msg = ' [' + str(round(disk_space, 1)) + ' GiB]' - - if self.app_obj.download_manager_obj \ - and self.app_obj.show_free_space_flag: - - operation_type \ - = self.app_obj.download_manager_obj.operation_type - - if operation_type == 'sim' \ - or operation_type == 'custom_sim' \ - or operation_type == 'classic_sim': - self.check_media_button.set_label(_('Checking') + msg) - else: - self.check_media_button.set_label(_('Downloading') + msg) - self.check_media_button.set_tooltip_text() - - - # (Auto-sort functions for main window widgets) - - - def video_index_auto_sort(self, treestore, row_iter1, row_iter2, data): - - """Sorting function created by self.video_index_reset(). - - Automatically sorts rows in the Video Index, two at a time. - - Args: - - treestore (Gtk.TreeStore): Rows in the Video Index are stored in - this treestore. - - row_iter1, row_iter2 (Gtk.TreeIter): Iters pointing at two rows - in the treestore, one of which must be sorted before the other - - data (None): Ignored - - Return values: - - -1 if row_iter1 comes before row_iter2, 1 if row_iter2 comes before - row_iter1, 0 if their order should not be changed - - """ - - # If auto-sorting is disabled temporarily, we can prevent the list - # being sorted by returning -1 for all cases - if self.video_index_no_sort_flag: - return -1 - - # Get the media data objects on both rows - obj1 = self.app_obj.media_reg_dict[treestore.get_value(row_iter1, 0)] - obj2 = self.app_obj.media_reg_dict[treestore.get_value(row_iter2, 0)] - - # Perform the sort - # Treat media.Channel and media.Playlist objects as the same type of - # thing, so that all folders appear first (sorted alphabetically), - # followed by all channels/playlists (sorted alphabetically) - if str(obj1.__class__) == str(obj2.__class__) \ - or ( - isinstance(obj1, media.GenericRemoteContainer) \ - and isinstance(obj2, media.GenericRemoteContainer) - ): - # Private folders are shown first, then (public) fixed folders, - # then user-created folders - if isinstance(obj1, media.Folder): - if obj1.priv_flag and not obj2.priv_flag: - return -1 - elif not obj1.priv_flag and obj2.priv_flag: - return 1 - elif obj1.fixed_flag and not obj2.fixed_flag: - return -1 - elif not obj1.fixed_flag and obj2.fixed_flag: - return 1 - - # Media data objects can't have the same name, but they might have - # the same nickname - # If two nicknames both start with an index, e.g. '1 Music' and - # '11 Comedy' then make sure the one with the lowest index comes - # first - index1_list = re.findall(r'^(\d+)', obj1.nickname) - index2_list = re.findall(r'^(\d+)', obj2.nickname) - if index1_list and index2_list: - if int(index1_list[0]) < int(index2_list[0]): - return -1 - else: - return 1 - elif obj1.nickname.lower() < obj2.nickname.lower(): - return -1 - else: - return 1 - - else: - - # (Folders displayed first, channels/playlists next, and of course - # videos aren't displayed here at all) - if isinstance(obj1, media.Folder): - return -1 - elif isinstance(obj2, media.Folder): - return 1 - else: - return 0 - - - def video_catalogue_generic_auto_sort(self, row1, row2, data, notify): - - """Sorting function created by self.video_catalogue_reset(), when - videos are displayed in a Gtk.ListBox. - - Automatically sorts rows in the Video Catalogue, by upload time - (default) or alphabetically, depending on settings. - - This is a wrapper function, so that self.video_catalogue_compare() can - be called, regardless of whether the Video Catalogue is using a listbox - or a grid. - - Args: - - row1, row2 (mainwin.CatalogueRow): Two rows in the Gtk.ListBox's - model, one of which must be sorted before the other - - data (None): Ignored - - notify (False): Ignored - - Return values: - - -1 if row1 comes before row2, 1 if row2 comes before row1 (the code - does not return 0) - - """ - - return self.app_obj.video_compare(row1.video_obj, row2.video_obj) - - - def video_catalogue_grid_auto_sort(self, gridbox1, gridbox2): - - """Sorting function created by self.video_catalogue_reset(), when - videos are displayed on a Gtk.Grid. - - Automatically sorts gridboxes in the Video Catalogue, by upload time - (default) or alphabetically, depending on settings. - - This is a wrapper function, so that self.video_catalogue_compare() can - be called, regardless of whether the Video Catalogue is using a listbox - or a grid. - - Args: - - gridbox1, gridbox2 (mainwin.CatalogueGridBox): Two gridboxes, one - of which must be sorted before the other - - Return values: - - -1 if gridbox1 comes before gridbox2, 1 if gridbox2 comes before - gridbox1 (the code does not return 0) - - """ - - return self.app_obj.video_compare( - gridbox1.video_obj, - gridbox2.video_obj, - ) - - - # (Popup menu functions for main window widgets) - - - def video_index_popup_menu(self, event, dbid): - - """Called by self.on_video_index_right_click(). - - When the user right-clicks on the Video Index, shows a - context-sensitive popup menu. - - Args: - - event (Gdk.EventButton): The mouse click event - - dbid (int): The .dbid of the clicked media data object - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video Index popup menu starts here. In' \ - + ' the Videos tab, right-click any channel/playlist/folder' - ) - - # Find the right-clicked media data object (and a string to describe - # its type) - media_data_obj = self.app_obj.media_reg_dict[dbid] - media_type = media_data_obj.get_type() - # (If an external directory is set, but is not available, many items - # must be desensitised) - if media_data_obj.dbid in self.app_obj.container_unavailable_dict: - unavailable_flag = True - else: - unavailable_flag = False - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Check/download/refresh items - if media_type == 'channel': - msg = _('_Check channel') - elif media_type == 'playlist': - msg = _('_Check playlist') - else: - msg = _('_Check folder') - - check_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - check_menu_item.connect( - 'activate', - self.on_video_index_check, - media_data_obj, - ) - if ( - self.app_obj.current_manager_obj \ - and not self.app_obj.download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.operation_classic_flag - ) or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ) or ( - not isinstance(media_data_obj, media.Folder) \ - and media_data_obj.source is None - ) or unavailable_flag \ - or media_data_obj.dl_no_db_flag: - check_menu_item.set_sensitive(False) - popup_menu.append(check_menu_item) - - if media_type == 'channel': - msg = _('_Download channel') - elif media_type == 'playlist': - msg = _('_Download playlist') - else: - msg = _('_Download folder') - - download_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - download_menu_item.connect( - 'activate', - self.on_video_index_download, - media_data_obj, - ) - if __main__.__pkg_no_download_flag__ \ - or ( - self.app_obj.current_manager_obj \ - and not self.app_obj.download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.operation_classic_flag - ) or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ) or ( - not isinstance(media_data_obj, media.Folder) \ - and media_data_obj.source is None - ) or unavailable_flag: - download_menu_item.set_sensitive(False) - popup_menu.append(download_menu_item) - - if media_type == 'channel': - msg = _('C_ustom download channel') - elif media_type == 'playlist': - msg = _('C_ustom download playlist') - else: - msg = _('C_ustom download folder') - - custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - if not self.app_obj.check_custom_download_managers(): - custom_dl_menu_item.connect( - 'activate', - self.on_video_index_custom_dl, - media_data_obj, - ) - - else: - custom_dl_submenu = self.custom_dl_popup_submenu([media_data_obj]) - custom_dl_menu_item.set_submenu( - self.custom_dl_popup_submenu([media_data_obj]), - ) - - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ) or ( - not isinstance(media_data_obj, media.Folder) \ - and media_data_obj.source is None - ) or unavailable_flag: - custom_dl_menu_item.set_sensitive(False) - popup_menu.append(custom_dl_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Contents - contents_submenu = Gtk.Menu() - - if not isinstance(media_data_obj, media.Folder): - - self.video_index_setup_contents_submenu( - contents_submenu, - media_data_obj, - False, - ) - - else: - - # All contents - all_contents_submenu = Gtk.Menu() - - self.video_index_setup_contents_submenu( - all_contents_submenu, - media_data_obj, - False, - ) - - # Separator - all_contents_submenu.append(Gtk.SeparatorMenuItem()) - - empty_folder_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Empty folder'), - ) - empty_folder_menu_item.connect( - 'activate', - self.on_video_index_empty_folder, - media_data_obj, - ) - all_contents_submenu.append(empty_folder_menu_item) - if not media_data_obj.child_list or media_data_obj.priv_flag: - empty_folder_menu_item.set_sensitive(False) - - all_contents_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_All contents'), - ) - all_contents_menu_item.set_submenu(all_contents_submenu) - contents_submenu.append(all_contents_menu_item) - - # Just folder videos - just_videos_submenu = Gtk.Menu() - - self.video_index_setup_contents_submenu( - just_videos_submenu, - media_data_obj, - True, - ) - - # Separator - just_videos_submenu.append(Gtk.SeparatorMenuItem()) - - empty_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Remove videos'), - ) - empty_videos_menu_item.connect( - 'activate', - self.on_video_index_remove_videos, - media_data_obj, - ) - just_videos_submenu.append(empty_videos_menu_item) - if not media_data_obj.child_list or media_data_obj.priv_flag: - empty_videos_menu_item.set_sensitive(False) - - just_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Just folder videos'), - ) - just_videos_menu_item.set_submenu(just_videos_submenu) - contents_submenu.append(just_videos_menu_item) - - if media_type == 'channel': - string = _('Channel c_ontents') - elif media_type == 'playlist': - string = _('Playlist c_ontents') - else: - string = _('Folder c_ontents') - - contents_menu_item = Gtk.MenuItem.new_with_mnemonic(string) - contents_menu_item.set_submenu(contents_submenu) - popup_menu.append(contents_menu_item) - if not media_data_obj.child_list: - contents_menu_item.set_sensitive(False) - - # Actions - actions_submenu = Gtk.Menu() - - move_top_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Move to top level'), - ) - move_top_menu_item.connect( - 'activate', - self.on_video_index_move_to_top, - media_data_obj, - ) - actions_submenu.append(move_top_menu_item) - if not media_data_obj.parent_obj \ - or self.app_obj.current_manager_obj \ - or unavailable_flag: - move_top_menu_item.set_sensitive(False) - - # Separator - actions_submenu.append(Gtk.SeparatorMenuItem()) - - if isinstance(media_data_obj, media.Folder): - - hide_folder_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Hide folder'), - ) - hide_folder_menu_item.connect( - 'activate', - self.on_video_index_hide_folder, - media_data_obj, - ) - actions_submenu.append(hide_folder_menu_item) - - if media_type == 'channel': - msg = _('_Rename channel...') - elif media_type == 'playlist': - msg = _('_Rename playlist...') - else: - msg = _('_Rename folder...') - - rename_location_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - rename_location_menu_item.connect( - 'activate', - self.on_video_index_rename_location, - media_data_obj, - ) - actions_submenu.append(rename_location_menu_item) - if self.app_obj.current_manager_obj or self.config_win_list \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag - ) or unavailable_flag: - rename_location_menu_item.set_sensitive(False) - - set_nickname_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Set _nickname...'), - ) - set_nickname_menu_item.connect( - 'activate', - self.on_video_index_set_nickname, - media_data_obj, - ) - actions_submenu.append(set_nickname_menu_item) - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: - set_nickname_menu_item.set_sensitive(False) - - if not isinstance(media_data_obj, media.Folder): - - set_url_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Set _URL...'), - ) - set_url_menu_item.connect( - 'activate', - self.on_video_index_set_url, - media_data_obj, - ) - actions_submenu.append(set_url_menu_item) - if self.app_obj.current_manager_obj: - set_url_menu_item.set_sensitive(False) - - # Separator - actions_submenu.append(Gtk.SeparatorMenuItem()) - - if media_type == 'channel': - - insert_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Insert videos...'), - ) - insert_menu_item.connect( - 'activate', - self.on_video_index_insert_videos, - media_data_obj, - ) - actions_submenu.append(insert_menu_item) - if self.app_obj.current_manager_obj: - insert_menu_item.set_sensitive(False) - - # Separator - actions_submenu.append(Gtk.SeparatorMenuItem()) - - if media_type == 'channel': - msg = _('_Export channel...') - elif media_type == 'playlist': - msg = _('_Export playlist...') - else: - msg = _('_Export folder...') - - export_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - export_menu_item.connect( - 'activate', - self.on_video_index_export, - media_data_obj, - ) - actions_submenu.append(export_menu_item) - if self.app_obj.current_manager_obj: - export_menu_item.set_sensitive(False) - - if media_type == 'channel': - msg = _('Re_fresh channel') - elif media_type == 'playlist': - msg = _('Re_fresh playlist') - else: - msg = _('Re_fresh folder') - - refresh_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - refresh_menu_item.connect( - 'activate', - self.on_video_index_refresh, - media_data_obj, - ) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ): - refresh_menu_item.set_sensitive(False) - actions_submenu.append(refresh_menu_item) - - if media_type == 'channel': - msg = _('_Tidy up channel') - elif media_type == 'playlist': - msg = _('_Tidy up playlist') - else: - msg = _('_Tidy up folder') - - tidy_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - tidy_menu_item.connect( - 'activate', - self.on_video_index_tidy, - media_data_obj, - ) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ): - tidy_menu_item.set_sensitive(False) - actions_submenu.append(tidy_menu_item) - - # Separator - actions_submenu.append(Gtk.SeparatorMenuItem()) - - convert_text = None - if media_type == 'channel': - msg = _('_Convert to playlist') - elif media_type == 'playlist': - msg = _('_Convert to channel') - else: - msg = None - - if msg: - - convert_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - convert_menu_item.connect( - 'activate', - self.on_video_index_convert_container, - media_data_obj, - ) - actions_submenu.append(convert_menu_item) - if self.app_obj.current_manager_obj \ - or unavailable_flag: - convert_menu_item.set_sensitive(False) - - classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add to C_lassic Mode tab'), - ) - classic_dl_menu_item.connect( - 'activate', - self.on_video_index_add_classic, - media_data_obj, - ) - actions_submenu.append(classic_dl_menu_item) - if __main__.__pkg_no_download_flag__ \ - or isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.source: - classic_dl_menu_item.set_sensitive(False) - - if media_type == 'channel': - msg = _('Channel _actions') - elif media_type == 'playlist': - msg = _('Playlist _actions') - else: - msg = _('Folder _actions') - - actions_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - actions_menu_item.set_submenu(actions_submenu) - popup_menu.append(actions_menu_item) - - # Apply/remove/edit download options, disable downloads - downloads_submenu = Gtk.Menu() - - # (Desensitise these menu items, if an edit window is already open) - no_options_flag = False - for win_obj in self.config_win_list: - if isinstance(win_obj, config.OptionsEditWin) \ - and media_data_obj.options_obj == win_obj.edit_obj: - no_options_flag = True - break - - if not media_data_obj.options_obj: - - apply_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Apply download options...'), - ) - apply_options_menu_item.connect( - 'activate', - self.on_video_index_apply_options, - media_data_obj, - ) - downloads_submenu.append(apply_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ): - apply_options_menu_item.set_sensitive(False) - - else: - - remove_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Remove download options'), - ) - remove_options_menu_item.connect( - 'activate', - self.on_video_index_remove_options, - media_data_obj, - ) - downloads_submenu.append(remove_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ): - remove_options_menu_item.set_sensitive(False) - - edit_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Edit download options...'), - ) - edit_options_menu_item.connect( - 'activate', - self.on_video_index_edit_options, - media_data_obj, - ) - downloads_submenu.append(edit_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj \ - or not media_data_obj.options_obj: - edit_options_menu_item.set_sensitive(False) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - add_scheduled_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add to _scheduled download...'), - ) - add_scheduled_menu_item.connect( - 'activate', - self.on_video_index_add_to_scheduled, - media_data_obj, - ) - downloads_submenu.append(add_scheduled_menu_item) - - set_destination_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Set _download destination...'), - ) - set_destination_menu_item.connect( - 'activate', - self.on_video_index_set_destination, - media_data_obj, - ) - downloads_submenu.append(set_destination_menu_item) - if ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag - ): - set_destination_menu_item.set_sensitive(False) - - show_system_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Show system _command...'), - ) - show_system_menu_item.connect( - 'activate', - self.on_video_index_show_system_cmd, - media_data_obj, - ) - downloads_submenu.append(show_system_menu_item) - if isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.source: - show_system_menu_item.set_sensitive(False) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - # Only for the "Recent Videos" folder - if media_data_obj == self.app_obj.fixed_recent_folder: - - recent_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Set _removal time...'), - ) - recent_videos_menu_item.connect( - 'activate', - self.on_video_index_recent_videos_time, - media_data_obj, - ) - downloads_submenu.append(recent_videos_menu_item) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - marker_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Mark for checking/downloading'), - ) - marker_menu_item.set_active( - media_data_obj.dbid in self.video_index_marker_dict, - ) - marker_menu_item.connect( - 'activate', - self.on_video_index_marker, - media_data_obj, - ) - downloads_submenu.append(marker_menu_item) - if ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ) or media_data_obj.dl_disable_flag: - marker_menu_item.set_sensitive(False) - - no_db_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Don\'t add videos to Tartube\'s database'), - ) - no_db_menu_item.set_active(media_data_obj.dl_no_db_flag) - no_db_menu_item.connect( - 'activate', - self.on_video_index_dl_no_db, - media_data_obj, - ) - downloads_submenu.append(no_db_menu_item) - # (Widget sensitivity set below) - - disable_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('D_isable checking/downloading'), - ) - disable_menu_item.set_active(media_data_obj.dl_disable_flag) - disable_menu_item.connect( - 'activate', - self.on_video_index_dl_disable, - media_data_obj, - ) - downloads_submenu.append(disable_menu_item) - # (Widget sensitivity set below) - - enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Just disable downloading'), - ) - enforce_check_menu_item.set_active(media_data_obj.dl_sim_flag) - enforce_check_menu_item.connect( - 'activate', - self.on_video_index_dl_sim, - media_data_obj, - ) - downloads_submenu.append(enforce_check_menu_item) - # (Widget sensitivity set below) - - # (Widget sensitivity from above) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag - ): - no_db_menu_item.set_sensitive(False) - disable_menu_item.set_sensitive(False) - enforce_check_menu_item.set_sensitive(False) - - downloads_menu_item = Gtk.MenuItem.new_with_mnemonic(_('Down_loads')) - downloads_menu_item.set_submenu(downloads_submenu) - popup_menu.append(downloads_menu_item) - if __main__.__pkg_no_download_flag__ \ - or unavailable_flag: - downloads_menu_item.set_sensitive(False) - - # Show - show_submenu = Gtk.Menu() - - if media_type == 'channel': - msg = _('Channel _properties...') - elif media_type == 'playlist': - msg = _('Playlist _properties...') - else: - msg = _('Folder _properties...') - - show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - show_properties_menu_item.connect( - 'activate', - self.on_video_index_show_properties, - media_data_obj, - ) - show_submenu.append(show_properties_menu_item) - if self.app_obj.current_manager_obj: - show_properties_menu_item.set_sensitive(False) - - # Separator - show_submenu.append(Gtk.SeparatorMenuItem()) - - show_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Default location'), - ) - show_location_menu_item.connect( - 'activate', - self.on_video_index_show_location, - media_data_obj, - ) - show_submenu.append(show_location_menu_item) - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: - show_location_menu_item.set_sensitive(False) - - show_destination_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Actual location'), - ) - show_destination_menu_item.connect( - 'activate', - self.on_video_index_show_destination, - media_data_obj, - ) - show_submenu.append(show_destination_menu_item) - if ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ) or unavailable_flag: - show_destination_menu_item.set_sensitive(False) - - show_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Show')) - show_menu_item.set_submenu(show_submenu) - popup_menu.append(show_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Delete items - if media_type == 'channel': - msg = _('D_elete channel') - elif media_type == 'playlist': - msg = _('D_elete playlist') - else: - msg = _('D_elete folder') - - delete_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - delete_menu_item.connect( - 'activate', - self.on_video_index_delete_container, - media_data_obj, - ) - if self.app_obj.current_manager_obj \ - or (media_type == 'folder' and media_data_obj.fixed_flag) \ - or self.config_win_list: - delete_menu_item.set_sensitive(False) - popup_menu.append(delete_menu_item) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - def video_catalogue_popup_menu(self, event, video_obj): - - """Called by mainwin.SimpleCatalogueItem.on_right_click_row(), - mainwin.ComplexCatalogueItem.on_right_click_row() or - mainwin.GridCatalogueItem.on_click_box(). - - When the user right-clicks on the Video Catalogue, shows a context- - sensitive popup menu. - - Args: - - event (Gdk.EventButton): The mouse click event - - video_obj (media.Video): The video object displayed in the clicked - row - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video Catalogue popup menu starts here. In' \ - + ' the Videos tab, right-click any video' - ) - - # Use a different popup menu for multiple selected videos - video_list = [] - if self.app_obj.catalogue_mode_type != 'grid': - - # Because of Gtk weirdness, check that the clicked row is actually - # one of those selected - catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid] - row_list = self.catalogue_listbox.get_selected_rows() - if catalogue_item_obj.catalogue_row in row_list \ - and len(row_list) > 1: - - # Convert row_list, a list of mainwin.CatalogueRow objects, - # into a list of media.Video objects - video_list = [] - for row in row_list: - video_list.append(row.video_obj) - - return self.video_catalogue_multi_popup_menu(event, video_list) - - else: - - # Otherwise, right-clicking a row selects it (and unselects - # everything else) - self.catalogue_listbox.unselect_all() - self.catalogue_listbox.select_row( - catalogue_item_obj.catalogue_row, - ) - - else: - - # For our custom Gtk.Grid selection code, the same principle - # applies - for catalogue_item_obj in self.video_catalogue_dict.values(): - if catalogue_item_obj.selected_flag: - video_list.append(catalogue_item_obj.video_obj) - - if video_obj in video_list and len(video_list) > 1: - - return self.video_catalogue_multi_popup_menu(event, video_list) - - else: - - self.video_catalogue_grid_select( - self.video_catalogue_dict[video_obj.dbid], - 'default', # Like a left-click, with no SHIFT/CTRL key - ) - - # (If the parent channel/playlist/folder has external directory is set, - # but which is not available, many items must be desensitised) - if video_obj.parent_obj.dbid \ - in self.app_obj.container_unavailable_dict: - unavailable_flag = True - else: - unavailable_flag = False - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Check/download videos - check_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Check video'), - ) - check_menu_item.connect( - 'activate', - self.on_video_catalogue_check, - video_obj, - ) - # (We can add another video to the downloads.DownloadList object, even - # after a download operation has started) - if ( - self.app_obj.current_manager_obj \ - and not self.app_obj.download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.operation_classic_flag - ) or video_obj.source is None \ - or unavailable_flag \ - or video_obj.parent_obj.dl_no_db_flag: - check_menu_item.set_sensitive(False) - popup_menu.append(check_menu_item) - - if not video_obj.dl_flag: - - download_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Download video'), - ) - download_menu_item.connect( - 'activate', - self.on_video_catalogue_download, - video_obj, - ) - if __main__.__pkg_no_download_flag__ \ - or ( - self.app_obj.current_manager_obj \ - and not self.app_obj.download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.operation_classic_flag - ) or video_obj.source is None \ - or video_obj.live_mode == 1 \ - or unavailable_flag: - download_menu_item.set_sensitive(False) - popup_menu.append(download_menu_item) - - else: - - download_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Re-_download this video') - ) - download_menu_item.connect( - 'activate', - self.on_video_catalogue_re_download, - video_obj, - ) - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.current_manager_obj \ - or video_obj.source is None \ - or video_obj.live_mode == 1 \ - or unavailable_flag: - download_menu_item.set_sensitive(False) - popup_menu.append(download_menu_item) - - custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('C_ustom download video') - ) - if not self.app_obj.check_custom_download_managers(): - custom_dl_menu_item.connect( - 'activate', - self.on_video_index_custom_dl, - video_obj, - ) - - else: - custom_dl_menu_item.set_submenu( - self.custom_dl_popup_submenu([ video_obj ]), - ) - - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.current_manager_obj \ - or video_obj.source is None \ - or video_obj.live_mode != 0 \ - or unavailable_flag: - custom_dl_menu_item.set_sensitive(False) - popup_menu.append(custom_dl_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Watch video in player/download and watch - if not video_obj.dl_flag and not self.app_obj.current_manager_obj: - - dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Download and _watch'), - ) - dl_watch_menu_item.connect( - 'activate', - self.on_video_catalogue_dl_and_watch, - video_obj, - ) - popup_menu.append(dl_watch_menu_item) - if __main__.__pkg_no_download_flag__ \ - or video_obj.source is None \ - or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.process_manager_obj \ - or video_obj.live_mode != 0 \ - or unavailable_flag: - dl_watch_menu_item.set_sensitive(False) - - else: - - watch_player_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Watch in _player'), - ) - watch_player_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_video, - video_obj, - ) - popup_menu.append(watch_player_menu_item) - if video_obj.live_mode != 0: - watch_player_menu_item.set_sensitive(False) - - # Watch video online. For YouTube URLs, offer alternative websites - enhanced = utils.is_video_enhanced(video_obj) - if video_obj.source is None or video_obj.live_mode != 0: - - if not enhanced: - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on website'), - ) - if video_obj.source is None: - watch_website_menu_item.set_sensitive(False) - popup_menu.append(watch_website_menu_item) - - else: - - pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name'] - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on {0}').format(pretty), - ) - if video_obj.source is None: - watch_website_menu_item.set_sensitive(False) - popup_menu.append(watch_website_menu_item) - - else: - - if not enhanced: - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on website'), - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website, - video_obj, - ) - popup_menu.append(watch_website_menu_item) - - elif enhanced != 'youtube': - - pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name'] - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on {0}').format(pretty), - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website, - video_obj, - ) - popup_menu.append(watch_website_menu_item) - - else: - - alt_submenu = Gtk.Menu() - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_YouTube'), - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website, - video_obj, - ) - alt_submenu.append(watch_website_menu_item) - - watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_HookTube'), - ) - watch_hooktube_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_hooktube, - video_obj, - ) - alt_submenu.append(watch_hooktube_menu_item) - - watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Invidious'), - ) - watch_invidious_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_invidious, - video_obj, - ) - alt_submenu.append(watch_invidious_menu_item) - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Watch on YouTube, Watch on' \ - + ' HookTube, etc', - ) - - alt_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('W_atch on'), - ) - alt_menu_item.set_submenu(alt_submenu) - popup_menu.append(alt_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Special - special_submenu = Gtk.Menu() - - clip_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Create video clips...'), - ) - clip_menu_item.connect( - 'activate', - self.on_video_catalogue_process_clip, - video_obj, - ) - special_submenu.append(clip_menu_item) - if self.app_obj.current_manager_obj \ - or (video_obj.dl_flag and video_obj.file_name is None) \ - or video_obj.live_mode \ - or unavailable_flag: - clip_menu_item.set_sensitive(False) - - slice_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Remove video slices...'), - ) - slice_menu_item.connect( - 'activate', - self.on_video_catalogue_process_slice, - video_obj, - ) - special_submenu.append(slice_menu_item) - if self.app_obj.current_manager_obj \ - or (video_obj.dl_flag and video_obj.file_name is None) \ - or video_obj.live_mode \ - or unavailable_flag: - slice_menu_item.set_sensitive(False) - - process_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Process with FFmpeg...'), - ) - process_menu_item.connect( - 'activate', - self.on_video_catalogue_process_ffmpeg, - video_obj, - ) - special_submenu.append(process_menu_item) - if self.app_obj.current_manager_obj \ - or video_obj.file_name is None \ - or unavailable_flag: - process_menu_item.set_sensitive(False) - - # Separator - special_submenu.append(Gtk.SeparatorMenuItem()) - - if not video_obj.dl_flag: - output_override_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Download with _name...'), - ) - else: - output_override_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Re-download with _name...'), - ) - output_override_menu_item.connect( - 'activate', - self.on_video_catalogue_output_override, - video_obj, - ) - special_submenu.append(output_override_menu_item) - if not download_menu_item.get_sensitive() \ - or (video_obj.dl_flag and self.app_obj.current_manager_obj): - output_override_menu_item.set_sensitive(False) - - # Separator - special_submenu.append(Gtk.SeparatorMenuItem()) - - reload_metadata_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Reload _metadata'), - ) - reload_metadata_menu_item.connect( - 'activate', - self.on_video_catalogue_reload_metadata, - video_obj, - ) - special_submenu.append(reload_metadata_menu_item) - if self.app_obj.current_manager_obj or self.config_win_list: - reload_metadata_menu_item.set_sensitive(False) - - special_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Special'), - ) - special_menu_item.set_submenu(special_submenu) - popup_menu.append(special_menu_item) - if self.app_obj.current_manager_obj \ - or unavailable_flag: - special_menu_item.set_sensitive(False) - - # Add to Classic Mode tab - classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add to C_lassic Mode tab'), - ) - classic_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_add_classic, - video_obj, - ) - popup_menu.append(classic_dl_menu_item) - if __main__.__pkg_no_download_flag__ \ - or video_obj.source is None: - classic_dl_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - if video_obj.live_mode != 0: - - # Livestream - livestream_submenu = Gtk.Menu() - - auto_notify_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Auto _notify'), - ) - if video_obj.dbid in self.app_obj.media_reg_auto_notify_dict: - auto_notify_menu_item.set_active(True) - auto_notify_menu_item.connect( - 'activate', - self.on_video_catalogue_livestream_toggle, - video_obj, - 'notify', - ) - livestream_submenu.append(auto_notify_menu_item) - # Currently disabled on MS Windows - if os.name == 'nt': - auto_notify_menu_item.set_sensitive(False) - - auto_alarm_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Auto _sound alarm'), - ) - if video_obj.dbid in self.app_obj.media_reg_auto_alarm_dict: - auto_alarm_menu_item.set_active(True) - auto_alarm_menu_item.connect( - 'activate', - self.on_video_catalogue_livestream_toggle, - video_obj, - 'alarm', - ) - livestream_submenu.append(auto_alarm_menu_item) - if not mainapp.HAVE_PLAYSOUND_FLAG: - auto_alarm_menu_item.set_sensitive(False) - - auto_open_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Auto _open'), - ) - if video_obj.dbid in self.app_obj.media_reg_auto_open_dict: - auto_open_menu_item.set_active(True) - auto_open_menu_item.connect( - 'activate', - self.on_video_catalogue_livestream_toggle, - video_obj, - 'open', - ) - livestream_submenu.append(auto_open_menu_item) - - auto_dl_start_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Download on start'), - ) - if video_obj.dbid in self.app_obj.media_reg_auto_dl_start_dict: - auto_dl_start_menu_item.set_active(True) - auto_dl_start_menu_item.connect( - 'activate', - self.on_video_catalogue_livestream_toggle, - video_obj, - 'dl_start', - ) - livestream_submenu.append(auto_dl_start_menu_item) - if __main__.__pkg_no_download_flag__: - auto_dl_start_menu_item.set_sensitive(False) - - auto_dl_stop_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Download on _stop'), - ) - if video_obj.dbid in self.app_obj.media_reg_auto_dl_stop_dict: - auto_dl_stop_menu_item.set_active(True) - auto_dl_stop_menu_item.connect( - 'activate', - self.on_video_catalogue_livestream_toggle, - video_obj, - 'dl_stop', - ) - livestream_submenu.append(auto_dl_stop_menu_item) - if __main__.__pkg_no_download_flag__: - auto_dl_stop_menu_item.set_sensitive(False) - - # Separator - livestream_submenu.append(Gtk.SeparatorMenuItem()) - - not_live_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Not a _livestream'), - ) - not_live_menu_item.connect( - 'activate', - self.on_video_catalogue_not_livestream, - video_obj, - ) - livestream_submenu.append(not_live_menu_item) - - finalise_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Finalise livestream'), - ) - finalise_menu_item.connect( - 'activate', - self.on_video_catalogue_finalise_livestream, - video_obj, - ) - livestream_submenu.append(finalise_menu_item) - if video_obj.dl_flag \ - or video_obj.live_mode == 1 \ - or (video_obj.live_mode == 0 and not video_obj.was_live_flag): - finalise_menu_item.set_sensitive(False) - else: - output_path = video_obj.get_actual_path(self.app_obj) - if os.path.isfile(output_path) \ - or not os.path.isfile(output_path + '.part'): - finalise_menu_item.set_sensitive(False) - - livestream_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Livestream'), - ) - livestream_menu_item.set_submenu(livestream_submenu) - popup_menu.append(livestream_menu_item) - if unavailable_flag: - livestream_menu_item.set_sensitive(False) - - else: - - # Temporary - temp_submenu = Gtk.Menu() - - mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Mark for download')) - mark_temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_mark_temp_dl, - video_obj, - ) - temp_submenu.append(mark_temp_dl_menu_item) - - # Separator - temp_submenu.append(Gtk.SeparatorMenuItem()) - - temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download')) - temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_temp_dl, - video_obj, - False, - ) - temp_submenu.append(temp_dl_menu_item) - - temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Download and _watch'), - ) - temp_dl_watch_menu_item.connect( - 'activate', - self.on_video_catalogue_temp_dl, - video_obj, - True, - ) - temp_submenu.append(temp_dl_watch_menu_item) - - temp_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Temporary')) - temp_menu_item.set_submenu(temp_submenu) - popup_menu.append(temp_menu_item) - if __main__.__pkg_no_download_flag__ \ - or video_obj.source is None \ - or self.app_obj.current_manager_obj \ - or ( - isinstance(video_obj.parent_obj, media.Folder) - and video_obj.parent_obj.temp_flag - ) or video_obj.live_mode != 0 \ - or unavailable_flag: - temp_menu_item.set_sensitive(False) - - # Apply/remove/edit download options, show system command, disable - # downloads - downloads_submenu = Gtk.Menu() - - # (Desensitise these menu items, if an edit window is already open) - no_options_flag = False - for win_obj in self.config_win_list: - if isinstance(win_obj, config.OptionsEditWin) \ - and video_obj.options_obj == win_obj.edit_obj: - no_options_flag = True - break - - if not video_obj.options_obj: - - apply_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Apply download options...'), - ) - apply_options_menu_item.connect( - 'activate', - self.on_video_catalogue_apply_options, - video_obj, - ) - downloads_submenu.append(apply_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj: - apply_options_menu_item.set_sensitive(False) - - else: - - remove_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Remove download options'), - ) - remove_options_menu_item.connect( - 'activate', - self.on_video_catalogue_remove_options, - video_obj, - ) - downloads_submenu.append(remove_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj: - remove_options_menu_item.set_sensitive(False) - - edit_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Edit download options...'), - ) - edit_options_menu_item.connect( - 'activate', - self.on_video_catalogue_edit_options, - video_obj, - ) - downloads_submenu.append(edit_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj \ - or not video_obj.options_obj: - edit_options_menu_item.set_sensitive(False) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - show_system_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Show system command'), - ) - show_system_menu_item.connect( - 'activate', - self.on_video_catalogue_show_system_cmd, - video_obj, - ) - downloads_submenu.append(show_system_menu_item) - - test_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Test system command'), - ) - test_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_test_dl, - video_obj, - ) - downloads_submenu.append(test_dl_menu_item) - if self.app_obj.current_manager_obj: - test_dl_menu_item.set_sensitive(False) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Disable downloads'), - ) - enforce_check_menu_item.set_active(video_obj.dl_sim_flag) - enforce_check_menu_item.connect( - 'activate', - self.on_video_catalogue_enforce_check, - video_obj, - ) - downloads_submenu.append(enforce_check_menu_item) - # (Don't allow the user to change the setting of - # media.Video.dl_sim_flag if the video is in a channel or playlist, - # since media.Channel.dl_sim_flag or media.Playlist.dl_sim_flag - # applies instead) - if self.app_obj.current_manager_obj \ - or not isinstance(video_obj.parent_obj, media.Folder): - enforce_check_menu_item.set_sensitive(False) - - downloads_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('D_ownloads'), - ) - downloads_menu_item.set_submenu(downloads_submenu) - popup_menu.append(downloads_menu_item) - if __main__.__pkg_no_download_flag__ \ - or unavailable_flag: - downloads_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Mark video - mark_video_submenu = Gtk.Menu() - - archive_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Video is _archived'), - ) - archive_video_menu_item.set_active(video_obj.archive_flag) - archive_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_archived_video, - video_obj, - ) - mark_video_submenu.append(archive_video_menu_item) - if not video_obj.dl_flag: - archive_video_menu_item.set_sensitive(False) - - bookmark_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Video is _bookmarked'), - ) - bookmark_video_menu_item.set_active(video_obj.bookmark_flag) - bookmark_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_bookmark_video, - video_obj, - ) - mark_video_submenu.append(bookmark_video_menu_item) - - fav_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Video is _favourite'), - ) - fav_video_menu_item.set_active(video_obj.fav_flag) - fav_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_favourite_video, - video_obj, - ) - mark_video_submenu.append(fav_video_menu_item) - - missing_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Video is _missing'), - ) - missing_video_menu_item.set_active(video_obj.missing_flag) - missing_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_missing_video, - video_obj, - ) - mark_video_submenu.append(missing_video_menu_item) - if ( - not isinstance(video_obj.parent_obj, media.Channel) \ - and not isinstance(video_obj.parent_obj, media.Playlist) - ) or not video_obj.dl_flag: - missing_video_menu_item.set_sensitive(False) - - new_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Video is _new'), - ) - new_video_menu_item.set_active(video_obj.new_flag) - new_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_new_video, - video_obj, - ) - mark_video_submenu.append(new_video_menu_item) - if not video_obj.dl_flag: - new_video_menu_item.set_sensitive(False) - - playlist_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Video is in _waiting list'), - ) - playlist_video_menu_item.set_active(video_obj.waiting_flag) - playlist_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_waiting_video, - video_obj, - ) - mark_video_submenu.append(playlist_video_menu_item) - - mark_video_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Mark video'), - ) - mark_video_menu_item.set_submenu(mark_video_submenu) - popup_menu.append(mark_video_menu_item) - if video_obj.live_mode != 0: - mark_video_menu_item.set_sensitive(False) - - # Show location/properties - show_submenu = Gtk.Menu() - - show_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Location'), - ) - show_location_menu_item.connect( - 'activate', - self.on_video_catalogue_show_location, - video_obj, - ) - show_submenu.append(show_location_menu_item) - if unavailable_flag: - show_location_menu_item.set_sensitive(False) - - show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Properties...'), - ) - show_properties_menu_item.connect( - 'activate', - self.on_video_catalogue_show_properties, - video_obj, - ) - show_submenu.append(show_properties_menu_item) - if self.app_obj.current_manager_obj: - show_properties_menu_item.set_sensitive(False) - - show_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('S_how video'), - ) - show_menu_item.set_submenu(show_submenu) - popup_menu.append(show_menu_item) - - # Fetch formats/subtitles - fetch_submenu = Gtk.Menu() - - fetch_formats_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Available _formats'), - ) - fetch_formats_menu_item.connect( - 'activate', - self.on_video_catalogue_fetch_formats, - video_obj, - ) - fetch_submenu.append(fetch_formats_menu_item) - - fetch_subs_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Available _subtitles'), - ) - fetch_subs_menu_item.connect( - 'activate', - self.on_video_catalogue_fetch_subs, - video_obj, - ) - fetch_submenu.append(fetch_subs_menu_item) - - fetch_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Fetch'), - ) - fetch_menu_item.set_submenu(fetch_submenu) - popup_menu.append(fetch_menu_item) - if video_obj.source is None or self.app_obj.current_manager_obj: - fetch_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Delete video - delete_menu_item = Gtk.MenuItem.new_with_mnemonic(_('D_elete video')) - delete_menu_item.connect( - 'activate', - self.on_video_catalogue_delete_video, - video_obj, - ) - popup_menu.append(delete_menu_item) - if self.config_win_list: - delete_menu_item.set_sensitive(False) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - def video_catalogue_multi_popup_menu(self, event, video_list): - - """Called by self.video_catalogue_popup_menu(). - - When multiple videos are selected in the Video Catalogue and the user - right-clicks one of them, shows a context-sensitive popup menu. - - Args: - - event (Gdk.EventButton): The mouse click event - - video_list (list): List of media.Video objects that are currently - selected (each one corresponding to a single media.Video - object) - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Video Catalogue popup menu starts here. In' \ - + ' the Videos tab, select two or more videos, then righ-click' \ - + ' them' - ) - - # So we can desensitise some menu items, work out in advance whether - # any of the selected videos are marked as downloaded, or have a - # source URL, or are in a temporary folder - dl_flag = False - for video_obj in video_list: - if video_obj.dl_flag: - dl_flag = True - break - - not_dl_flag = False - for video_obj in video_list: - if not video_obj.dl_flag: - not_dl_flag = True - break - - not_check_flag = False - for video_obj in video_list: - if video_obj.parent_obj.dl_no_db_flag: - not_check_flag = True - break - - source_flag = False - for video_obj in video_list: - if video_obj.source is not None: - source_flag = True - break - - temp_folder_flag = False - for video_obj in video_list: - if isinstance(video_obj.parent_obj, media.Folder) \ - and video_obj.parent_obj.temp_flag: - temp_folder_flag = True - break - - # For 'missing' videos, work out if the selected videos are all inside - # a channel or playlist - any_folder_flag = False - for video_obj in video_list: - if isinstance(video_obj.parent_obj, media.Folder): - any_folder_flag = True - break - - # Also work out if any videos are waiting or broadcasting livestreams - live_flag = False - live_wait_flag = False - for video_obj in video_list: - if video_obj.live_mode == 1: - live_flag = True - live_wait_flag = True - break - - live_broadcast_flag = False - for video_obj in video_list: - if video_obj.live_mode == 2: - live_flag = True - live_broadcast_flag = True - break - - # (If the parent channel/playlist/folder has external directory is set, - # but which is not available, many items must be desensitised) - for video_obj in video_list: - if video_obj.parent_obj.dbid \ - in self.app_obj.container_unavailable_dict: - unavailable_flag = True - else: - unavailable_flag = False - - # (Only need to test one video) - break - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Check/download videos - check_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Check videos')) - check_menu_item.connect( - 'activate', - self.on_video_catalogue_check_multi, - video_list, - ) - # (We can add another video to the downloads.DownloadList object, even - # after a download operation has started) - if ( - self.app_obj.current_manager_obj \ - and not self.app_obj.download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.operation_classic_flag - ) or unavailable_flag \ - or not_check_flag: - check_menu_item.set_sensitive(False) - popup_menu.append(check_menu_item) - - download_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Download videos') - ) - download_menu_item.connect( - 'activate', - self.on_video_catalogue_download_multi, - video_list, - live_wait_flag, - ) - if __main__.__pkg_no_download_flag__ \ - or ( - self.app_obj.current_manager_obj \ - and not self.app_obj.download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.operation_classic_flag - ) or live_wait_flag \ - or unavailable_flag: - download_menu_item.set_sensitive(False) - popup_menu.append(download_menu_item) - - custom_dl_submenu = self.custom_dl_popup_submenu(video_list) - - custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('C_ustom download videos') - ) - custom_dl_menu_item.set_submenu(custom_dl_submenu) - if __main__.__pkg_no_download_flag__ \ - or self.app_obj.current_manager_obj \ - or live_flag \ - or unavailable_flag: - custom_dl_menu_item.set_sensitive(False) - popup_menu.append(custom_dl_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Watch video - self.add_watch_video_menu_items( - popup_menu, - not_dl_flag, - source_flag, - live_flag, - unavailable_flag, - video_list, - ) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Special - special_submenu = Gtk.Menu() - - process_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Process with FFmpeg...'), - ) - process_menu_item.connect( - 'activate', - self.on_video_catalogue_process_ffmpeg_multi, - video_list, - ) - special_submenu.append(process_menu_item) - if self.app_obj.current_manager_obj \ - or unavailable_flag: - process_menu_item.set_sensitive(False) - - # Separator - special_submenu.append(Gtk.SeparatorMenuItem()) - - reload_metadata_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Reload metadata'), - ) - reload_metadata_menu_item.connect( - 'activate', - self.on_video_catalogue_reload_metadata_multi, - video_list, - ) - special_submenu.append(reload_metadata_menu_item) - if self.app_obj.current_manager_obj or self.config_win_list: - reload_metadata_menu_item.set_sensitive(False) - - special_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Special'), - ) - special_menu_item.set_submenu(special_submenu) - popup_menu.append(special_menu_item) - if self.app_obj.current_manager_obj \ - or unavailable_flag: - special_menu_item.set_sensitive(False) - - # Add to Classic Mode tab - classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add to C_lassic Mode tab'), - ) - classic_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_add_classic_multi, - video_list, - ) - popup_menu.append(classic_dl_menu_item) - if __main__.__pkg_no_download_flag__: - classic_dl_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - if live_flag or live_wait_flag or live_broadcast_flag: - - # Livestream - livestream_submenu = Gtk.Menu() - - not_live_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as _not livestreams'), - ) - not_live_menu_item.connect( - 'activate', - self.on_video_catalogue_not_livestream_multi, - video_list, - ) - livestream_submenu.append(not_live_menu_item) - - finalise_live_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Finalise livestreams'), - ) - finalise_live_menu_item.connect( - 'activate', - self.on_video_catalogue_finalise_livestream_multi, - video_list, - ) - livestream_submenu.append(finalise_live_menu_item) - - livestream_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Livestream'), - ) - livestream_menu_item.set_submenu(livestream_submenu) - popup_menu.append(livestream_menu_item) - if unavailable_flag: - livestream_menu_item.set_sensitive(False) - - # Download to Temporary Videos - temp_submenu = Gtk.Menu() - - mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Mark for download'), - ) - mark_temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_mark_temp_dl_multi, - video_list, - ) - temp_submenu.append(mark_temp_dl_menu_item) - - # Separator - temp_submenu.append(Gtk.SeparatorMenuItem()) - - temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download')) - temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_temp_dl_multi, - video_list, - False, - ) - temp_submenu.append(temp_dl_menu_item) - - temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Download and _watch'), - ) - temp_dl_watch_menu_item.connect( - 'activate', - self.on_video_catalogue_temp_dl_multi, - video_list, - True, - ) - temp_submenu.append(temp_dl_watch_menu_item) - - temp_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Temporary'), - ) - temp_menu_item.set_submenu(temp_submenu) - popup_menu.append(temp_menu_item) - if __main__.__pkg_no_download_flag__ \ - or not source_flag \ - or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.process_manager_obj \ - or temp_folder_flag \ - or live_flag \ - or unavailable_flag: - temp_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Mark videos - mark_videos_submenu = Gtk.Menu() - - archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Archived'), - ) - archive_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_archived_video_multi, - True, - video_list, - ) - mark_videos_submenu.append(archive_menu_item) - - not_archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Not a_rchived'), - ) - not_archive_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_archived_video_multi, - False, - video_list, - ) - mark_videos_submenu.append(not_archive_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Bookmarked'), - ) - bookmark_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_bookmark_video_multi, - True, - video_list, - ) - mark_videos_submenu.append(bookmark_menu_item) - - not_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Not b_ookmarked'), - ) - not_bookmark_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_bookmark_video_multi, - False, - video_list, - ) - mark_videos_submenu.append(not_bookmark_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Favourite'), - ) - fav_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_favourite_video_multi, - True, - video_list, - ) - mark_videos_submenu.append(fav_menu_item) - - not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Not fa_vourite'), - ) - not_fav_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_favourite_video_multi, - False, - video_list, - ) - mark_videos_submenu.append(not_fav_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - missing_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Missing'), - ) - missing_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_missing_video_multi, - True, - video_list, - ) - if not dl_flag or any_folder_flag: - missing_menu_item.set_sensitive(False) - mark_videos_submenu.append(missing_menu_item) - - not_missing_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Not m_issing'), - ) - not_missing_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_missing_video_multi, - False, - video_list, - ) - if not dl_flag or any_folder_flag: - not_missing_menu_item.set_sensitive(False) - mark_videos_submenu.append(not_missing_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - new_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_New'), - ) - new_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_new_video_multi, - True, - video_list, - ) - mark_videos_submenu.append(new_menu_item) - - not_new_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Not n_ew'), - ) - not_new_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_new_video_multi, - False, - video_list, - ) - mark_videos_submenu.append(not_new_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('In _waiting list'), - ) - playlist_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_waiting_video_multi, - True, - video_list, - ) - mark_videos_submenu.append(playlist_menu_item) - - not_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Not in w_aiting list'), - ) - not_playlist_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_waiting_video_multi, - False, - video_list, - ) - mark_videos_submenu.append(not_playlist_menu_item) - - mark_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Mark videos'), - ) - mark_videos_menu_item.set_submenu(mark_videos_submenu) - popup_menu.append(mark_videos_menu_item) - if live_flag or not dl_flag: - mark_videos_menu_item.set_sensitive(False) - - # Show properties - show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Show p_roperties...'), - ) - show_properties_menu_item.connect( - 'activate', - self.on_video_catalogue_show_properties_multi, - video_list, - ) - popup_menu.append(show_properties_menu_item) - if self.app_obj.current_manager_obj: - show_properties_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Delete videos - delete_menu_item = Gtk.MenuItem.new_with_mnemonic(_('D_elete videos')) - delete_menu_item.connect( - 'activate', - self.on_video_catalogue_delete_video_multi, - video_list, - ) - popup_menu.append(delete_menu_item) - if self.config_win_list: - delete_menu_item.set_sensitive(False) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - def progress_list_popup_menu(self, event, item_id, dbid): - - """Called by self.on_progress_list_right_click(). - - When the user right-clicks on the Progress List, shows a context- - sensitive popup menu. - - Args: - - event (Gdk.EventButton): The mouse click event - - item_id (int): The .item_id of the clicked downloads.DownloadItem - object - - dbid (int): The .dbid of the corresponding media data object - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Progress List popup menu starts here. In' \ - + ' the Progress tab, in the list in the top half of the tab,' \ - + ' right-click any row' - ) - - # Find the downloads.VideoDownloader which is currently handling the - # clicked media data object (if any) - download_manager_obj = self.app_obj.download_manager_obj - download_list_obj = None - download_item_obj = None - worker_obj = None - downloader_obj = None - queuing_flag = False - - if download_manager_obj: - - download_list_obj = download_manager_obj.download_list_obj - download_item_obj = download_list_obj.download_item_dict[item_id] - - for this_worker_obj in download_manager_obj.worker_list: - if this_worker_obj.running_flag \ - and this_worker_obj.download_item_obj == download_item_obj \ - and this_worker_obj.downloader_obj is not None: - worker_obj = this_worker_obj - downloader_obj = this_worker_obj.downloader_obj - break - - if not downloader_obj: - queuing_flag = download_list_obj.is_queuing( - download_item_obj.item_id - ) - - if download_manager_obj \ - and ( - download_manager_obj.operation_type == 'custom_sim' \ - or download_manager_obj.operation_type == 'classic_sim' - ): - custom_sim_flag = True - else: - custom_sim_flag = False - - # Find the media data object itself. If the download operation has - # finished, the variables just above will not be set - media_data_obj = None - if dbid in self.app_obj.media_reg_dict: - media_data_obj = self.app_obj.media_reg_dict[dbid] - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Stop check/download - stop_now_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Stop now')) - stop_now_menu_item.connect( - 'activate', - self.on_progress_list_stop_now, - download_item_obj, - worker_obj, - downloader_obj, - ) - popup_menu.append(stop_now_menu_item) - if not download_manager_obj \ - or downloader_obj is None: - stop_now_menu_item.set_sensitive(False) - - # N.B. During the checking stage of a custom download (operation types - # 'custom_sim', 'classic_sim'), this menu option has a slightly - # different effect (so uses a diffirent label) - if custom_sim_flag: - msg = _('Stop checking _videos') - elif queuing_flag: - msg = _('Completely stop after this _video') - else: - msg = _('Stop after this _video') - - stop_soon_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) - stop_soon_menu_item.connect( - 'activate', - self.on_progress_list_stop_soon, - download_item_obj, - worker_obj, - downloader_obj, - ) - popup_menu.append(stop_soon_menu_item) - if not download_manager_obj \ - or (downloader_obj is None and not queuing_flag): - stop_soon_menu_item.set_sensitive(False) - - # N.B. During the checking stage of a custom download (operation types - # 'custom_sim', 'classic_sim'), this menu option is redundant (same - # effect as the 'Stop now' menu option) - stop_all_soon_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Completely stop after these v_ideos'), - ) - stop_all_soon_menu_item.connect( - 'activate', - self.on_progress_list_stop_all_soon, - ) - popup_menu.append(stop_all_soon_menu_item) - if not download_manager_obj or custom_sim_flag: - stop_all_soon_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Check/download next/last - dl_next_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Download _next'), - ) - dl_next_menu_item.connect( - 'activate', - self.on_progress_list_dl_next, - download_item_obj, - ) - popup_menu.append(dl_next_menu_item) - if not download_manager_obj or worker_obj: - dl_next_menu_item.set_sensitive(False) - - dl_last_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Download _last'), - ) - dl_last_menu_item.connect( - 'activate', - self.on_progress_list_dl_last, - download_item_obj, - ) - popup_menu.append(dl_last_menu_item) - if not download_manager_obj or worker_obj: - dl_last_menu_item.set_sensitive(False) - - # Watch on website - if media_data_obj \ - and isinstance(media_data_obj, media.Video) \ - and media_data_obj.source: - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # For YouTube videos, offer three websites (as usual) - enhanced = utils.is_video_enhanced(media_data_obj) - if not enhanced: - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on Website'), - ) - watch_website_menu_item.connect( - 'activate', - self.on_progress_list_watch_website, - media_data_obj, - ) - popup_menu.append(watch_website_menu_item) - - elif enhanced != 'youtube': - - pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name'] - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on {0}').format(pretty), - ) - watch_website_menu_item.connect( - 'activate', - self.on_progress_list_watch_website, - media_data_obj, - ) - popup_menu.append(watch_website_menu_item) - - else: - - watch_youtube_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on YouTube'), - ) - watch_youtube_menu_item.connect( - 'activate', - self.on_progress_list_watch_website, - media_data_obj, - ) - popup_menu.append(watch_youtube_menu_item) - - watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Watch on _HookTube'), - ) - watch_hooktube_menu_item.connect( - 'activate', - self.on_progress_list_watch_hooktube, - media_data_obj, - ) - popup_menu.append(watch_hooktube_menu_item) - - watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Watch on _Invidious'), - ) - watch_invidious_menu_item.connect( - 'activate', - self.on_progress_list_watch_invidious, - media_data_obj, - ) - popup_menu.append(watch_invidious_menu_item) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - def results_list_popup_menu(self, event, path): - - """Called by self.on_results_list_right_click(). - - When the user right-clicks on the Results List, shows a context- - sensitive popup menu. - - Unlike the popup menu functions above, here we use a single function - for single or multiple selections in the treeview. - - Args: - - event (Gdk.EventButton): The mouse click event - - path (Gtk.TreePath): Path to the clicked row in the treeview - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Results List popup menu starts here. In' \ - + ' the Progress tab, in the list in the bottom half of the tab,' \ - + ' right-click any row' - ) - - # Get the selected media.Video object(s) - video_list = self.get_selected_videos_in_treeview( - self.results_list_treeview, - 0, # Column 0 contains the media.Video's .dbid - ) - # Any videos which have been deleted (but which are still visible in - # the Results List) are not returned, so the list might be empty - if not video_list: - return - - # So we can desensitise some menu items, work out in advance whether - # any of the selected videos are marked as downloaded, or have a - # source URL, or are in a temporary folder - dl_flag = False - for video_obj in video_list: - if video_obj.dl_flag: - dl_flag = True - break - - not_dl_flag = False - for video_obj in video_list: - if not video_obj.dl_flag: - not_dl_flag = True - break - - source_flag = False - for video_obj in video_list: - if video_obj.source is not None: - source_flag = True - break - - temp_folder_flag = False - for video_obj in video_list: - if isinstance(video_obj.parent_obj, media.Folder) \ - and video_obj.parent_obj.temp_flag: - temp_folder_flag = True - break - - live_flag = False - for video_obj in video_list: - if video_obj.live_mode == 1: - live_flag = True - break - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Watch video - self.add_watch_video_menu_items( - popup_menu, - not_dl_flag, - source_flag, - live_flag, - False, # unavailable_flag does not apply here - video_list, - ) - - show_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Open destination(s)'), - ) - show_location_menu_item.connect( - 'activate', - self.on_video_catalogue_show_location_multi, - video_list, - ) - popup_menu.append(show_location_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Process with FFmpeg - process_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Process with FFmpeg...'), - ) - process_menu_item.connect( - 'activate', - self.on_video_catalogue_process_ffmpeg_multi, - video_list, - ) - popup_menu.append(process_menu_item) - if self.app_obj.current_manager_obj: - process_menu_item.set_sensitive(False) - - # Add to Classic Mode tab - classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Add to C_lassic Mode tab'), - ) - classic_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_add_classic_multi, - video_list, - ) - popup_menu.append(classic_dl_menu_item) - if __main__.__pkg_no_download_flag__: - classic_dl_menu_item.set_sensitive(False) - - # Livestreams - livestream_submenu = Gtk.Menu() - - not_live_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as _not livestreams'), - ) - not_live_menu_item.connect( - 'activate', - self.on_video_catalogue_not_livestream_multi, - video_list, - ) - livestream_submenu.append(not_live_menu_item) - - finalise_live_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Finalise livestreams'), - ) - finalise_live_menu_item.connect( - 'activate', - self.on_video_catalogue_finalise_livestream_multi, - video_list, - ) - livestream_submenu.append(finalise_live_menu_item) - - livestream_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Livestream'), - ) - livestream_menu_item.set_submenu(livestream_submenu) - popup_menu.append(livestream_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Download to Temporary Videos - temp_submenu = Gtk.Menu() - - mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Mark for download'), - ) - mark_temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_mark_temp_dl_multi, - video_list, - ) - temp_submenu.append(mark_temp_dl_menu_item) - - # Separator - temp_submenu.append(Gtk.SeparatorMenuItem()) - - temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download')) - temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_temp_dl_multi, - video_list, - False, - ) - temp_submenu.append(temp_dl_menu_item) - - temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Download and _watch'), - ) - temp_dl_watch_menu_item.connect( - 'activate', - self.on_video_catalogue_temp_dl_multi, - video_list, - True, - ) - temp_submenu.append(temp_dl_watch_menu_item) - - temp_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Temporary'), - ) - temp_menu_item.set_submenu(temp_submenu) - popup_menu.append(temp_menu_item) - if __main__.__pkg_no_download_flag__ \ - or not source_flag \ - or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.process_manager_obj \ - or temp_folder_flag \ - or live_flag: - temp_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Delete videos - delete_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Delete video(s)'), - ) - delete_menu_item.connect( - 'activate', - self.on_video_catalogue_delete_video_multi, - video_list, - ) - popup_menu.append(delete_menu_item) - if self.app_obj.current_manager_obj: - delete_menu_item.set_sensitive(False) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - def classic_popup_menu(self): - - """Called by mainapp.TartubeApp.on_button_classic_menu(). - - When the user right-clicks the menu button in the Classic Mode tab, - shows a context-sensitive popup menu. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Classic Mode popup menu starts here. In' \ - + ' the Classic Mode tab, click the button in the top-right' \ - + ' corner' - ) - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Automatic copy/paste - automatic_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Enable automatic copy/paste'), - ) - if self.classic_auto_copy_flag: - automatic_menu_item.set_active(True) - automatic_menu_item.connect( - 'toggled', - self.on_classic_menu_toggle_auto_copy, - ) - popup_menu.append(automatic_menu_item) - - # One-click downloads - one_click_dl_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('E_nable one-click downloads'), - ) - if self.classic_one_click_dl_flag: - one_click_dl_menu_item.set_active(True) - one_click_dl_menu_item.connect( - 'toggled', - self.on_classic_menu_toggle_one_click_dl, - ) - popup_menu.append(one_click_dl_menu_item) - - # Remember undownloaded URLs - remember_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('_Remember un-downloaded URLs'), - ) - if self.app_obj.classic_pending_flag: - remember_menu_item.set_active(True) - remember_menu_item.connect( - 'toggled', - self.on_classic_menu_toggle_remember_urls, - ) - popup_menu.append(remember_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Download options - set_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Set download options...'), - ) - set_menu_item.connect( - 'activate', - self.on_classic_menu_set_options, - ) - if self.app_obj.current_manager_obj: - set_menu_item.set_sensitive(False) - popup_menu.append(set_menu_item) - - default_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Use _default download options'), - ) - default_menu_item.connect( - 'activate', - self.on_classic_menu_use_general_options, - ) - if self.app_obj.current_manager_obj \ - or not self.app_obj.classic_options_obj: - default_menu_item.set_sensitive(False) - popup_menu.append(default_menu_item) - - edit_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Edit download _options...'), - ) - edit_menu_item.connect( - 'activate', - self.on_classic_menu_edit_options, - ) - if self.app_obj.current_manager_obj: - edit_menu_item.set_sensitive(False) - popup_menu.append(edit_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - custom_dl_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - _('Enable _custom downloads'), - ) - if self.app_obj.classic_custom_dl_flag: - custom_dl_menu_item.set_active(True) - custom_dl_menu_item.connect( - 'toggled', - self.on_classic_menu_toggle_custom_dl, - ) - if self.app_obj.current_manager_obj: - custom_dl_menu_item.set_sensitive(False) - popup_menu.append(custom_dl_menu_item) - - custom_pref_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Custom downloads _preferences...'), - ) - custom_pref_menu_item.connect( - 'activate', - self.on_classic_menu_custom_dl_prefs, - ) - if self.app_obj.current_manager_obj: - custom_pref_menu_item.set_sensitive(False) - popup_menu.append(custom_pref_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Update youtube-dl - update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Update') + ' ' + self.app_obj.get_downloader(), - ) - update_ytdl_menu_item.connect( - 'activate', - self.on_classic_menu_update_ytdl, - ) - popup_menu.append(update_ytdl_menu_item) - if self.app_obj.current_manager_obj \ - or __main__.__pkg_strict_install_flag__: - update_ytdl_menu_item.set_sensitive(False) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup( - None, - None, - None, - None, - 1, - Gtk.get_current_event_time(), - ) - - - def classic_progress_list_popup_menu(self, event, path): - - """Called by self.on_classic_progress_list_right_click(). - - When the user right-clicks on the Classic Progress List, shows a - context-sensitive popup menu. - - Args: - - event (Gdk.EventButton): The mouse click event - - path (Gtk.TreePath): Path to the clicked row in the treeview - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Classic Progress List popup menu starts' \ - + ' here. In the Classic Mode tab, in the list in the bottom' \ - + ' half of the tab, right-click any row' - ) - - # Get the selected dummy media.Video object(s) - video_list = self.get_selected_videos_in_classic_treeview() - # Because of Gtk weirdness, right-clicking a line might not select it - # in time for Gtk.Selection to know about it - if not video_list: - return - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Play video - play_video_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Play video'), - ) - play_video_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'play', - video_list, - ) - popup_menu.append(play_video_menu_item) - - # Open destination - open_destination_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Open destination(s)'), - ) - open_destination_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'open', - video_list, - ) - popup_menu.append(open_destination_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Re-download - re_download_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Re-download'), - ) - re_download_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'redownload', - video_list, - ) - popup_menu.append(re_download_menu_item) - - # Stop download - stop_download_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Stop download'), - ) - stop_download_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'stop', - video_list, - ) - popup_menu.append(stop_download_menu_item) - if not self.app_obj.current_manager_obj: - stop_download_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Create video clips - create_clips_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Create _video clips...'), - ) - create_clips_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'clips', - video_list, - ) - popup_menu.append(create_clips_menu_item) - if self.app_obj.current_manager_obj \ - or len(video_list) > 1: - create_clips_menu_item.set_sensitive(False) - - # Process with FFmpeg - process_ffmpeg_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Process with _FFmpeg...'), - ) - process_ffmpeg_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'ffmpeg', - video_list, - ) - popup_menu.append(process_ffmpeg_menu_item) - if self.app_obj.current_manager_obj: - process_ffmpeg_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Copy file path - copy_submenu = Gtk.Menu() - - copy_path_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_File path'), - ) - copy_path_menu_item.connect( - 'activate', - self.on_classic_progress_list_get_path, - video_list[0], - ) - if len(video_list) > 1: - copy_path_menu_item.set_sensitive(False) - copy_submenu.append(copy_path_menu_item) - - # Copy URL - copy_url_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_URL')) - copy_url_menu_item.connect( - 'activate', - self.on_classic_progress_list_get_url, - video_list[0], - ) - if len(video_list) > 1: - copy_url_menu_item.set_sensitive(False) - copy_submenu.append(copy_url_menu_item) - - # Copy system command - copy_cmd_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_System command'), - ) - copy_cmd_menu_item.connect( - 'activate', - self.on_classic_progress_list_get_cmd, - video_list[0], - ) - if len(video_list) > 1: - copy_cmd_menu_item.set_sensitive(False) - copy_submenu.append(copy_cmd_menu_item) - - copy_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Copy')) - copy_menu_item.set_submenu(copy_submenu) - popup_menu.append(copy_menu_item) - - # Move up - move_submenu = Gtk.Menu() - - move_up_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Up'), - ) - move_up_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'move_up', - video_list, - ) - move_submenu.append(move_up_menu_item) - - # Move down - move_down_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Down'), - ) - move_down_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'move_down', - video_list, - ) - move_submenu.append(move_down_menu_item) - - move_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Move')) - move_menu_item.set_submenu(move_submenu) - popup_menu.append(move_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Re-insert URL above - reinsert_url_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Re-_insert URL above'), - ) - reinsert_url_menu_item.connect( - 'activate', - self.on_classic_progress_list_reinsert_url, - video_list, - ) - popup_menu.append(reinsert_url_menu_item) - - # Remove from list - remove_from_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Re_move from list'), - ) - remove_from_menu_item.connect( - 'activate', - self.on_classic_progress_list_from_popup, - 'remove', - video_list, - ) - popup_menu.append(remove_from_menu_item) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - def add_watch_video_menu_items(self, popup_menu, not_dl_flag, source_flag, - live_flag, unavailable_flag, video_list): - - """Called by self.video_catalogue_multi_popup_menu() and - .results_list_popup_menu(). - - Adds common menu items to the popup menus. - - Args: - - popup_menu (Gtk.Menu): The popup menu - - not_dl_flag (bool): Flag set to True if any of the media.Video - objects do not have their .dl_flag IV set - - source_flag (bool): Flag set to True if any of the media.Video - objects have their .source IV set - - live_flag (bool): Flag set to True if any of the media.Video - objects have their .live_mode IV set to any value above 0 - - unavailable_flag (bool): Flag set to True if the videos' parent - channel/playlist/folder has an external directory that is - marked disabled - - video_list (list): List of one or more media.Video objects on - which this popup menu acts - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Extra items for the popup menu in the' \ - + ' Video Catalogue. In the videos tab, right-click any video' - ) - - # Watch video in player/download and watch - if not_dl_flag or live_flag: - - dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('D_ownload and watch'), - ) - dl_watch_menu_item.connect( - 'activate', - self.on_video_catalogue_dl_and_watch_multi, - video_list, - ) - popup_menu.append(dl_watch_menu_item) - if __main__.__pkg_no_download_flag__ \ - or not source_flag \ - or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.process_manager_obj \ - or live_flag \ - or unavailable_flag: - dl_watch_menu_item.set_sensitive(False) - - else: - - watch_player_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Watch in _player'), - ) - watch_player_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_video_multi, - video_list, - ) - popup_menu.append(watch_player_menu_item) - - if len(video_list) > 1 or not source_flag or live_flag: - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Watch on _website'), - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website_multi, - video_list, - ) - if not source_flag: - watch_website_menu_item.set_sensitive(False) - popup_menu.append(watch_website_menu_item) - - else: - - video_obj = video_list[0] - enhanced = utils.is_video_enhanced(video_obj) - - if not enhanced: - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on website'), - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website, - video_obj, - ) - popup_menu.append(watch_website_menu_item) - - elif enhanced != 'youtube': - - pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name'] - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Watch on {0}').format(pretty), - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website, - video_obj, - ) - popup_menu.append(watch_website_menu_item) - - else: - - alt_submenu = Gtk.Menu() - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_YouTube'), - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website, - video_obj, - ) - alt_submenu.append(watch_website_menu_item) - - watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_HookTube'), - ) - watch_hooktube_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_hooktube, - video_obj, - ) - alt_submenu.append(watch_hooktube_menu_item) - - watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Invidious'), - ) - watch_invidious_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_invidious, - video_obj, - ) - alt_submenu.append(watch_invidious_menu_item) - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Watch on YouTube, Watch on' \ - + ' HookTube, etc', - ) - - alt_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('W_atch on'), - ) - alt_menu_item.set_submenu(alt_submenu) - popup_menu.append(alt_menu_item) - - - def custom_dl_popup_menu(self, media_data_list=[]): - - """Called by mainapp.TartubeApp.on_button_custom_dl_all(). - - When the user right-clicks the custom download manager selection button - in the Videos tab, shows a context-sensitive popup menu. - - Args: - - media_data_list (list): List of media data objects to custom - download (may be an empty list) - - """ - - # Set up the popup menu - popup_menu = self.custom_dl_popup_submenu(media_data_list) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup( - None, - None, - None, - None, - 1, - Gtk.get_current_event_time(), - ) - - - def custom_dl_popup_submenu(self, media_data_list=[]): - - """Called by several functions to create a sub-menu, within a parent - popup menu. - - The sub-menu contains a list of downloads.CustomDLManager objects. If - the user selects one, a custom download is started using settings from - that object. - - Args: - - media_data_list (list): List of media data objects to custom - download (may be an empty list) - - Return values: - - The sub-menu created - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Popup menu for the \'Custom download all\'' \ - + ' button in the Videos tab (not always visible)' - ) - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Title - choose_menu_item = Gtk.MenuItem.new_with_label( - _('Choose a custom download:'), - ) - popup_menu.append(choose_menu_item) - choose_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # General Custom Download Manager - general_menu_item = Gtk.MenuItem.new_with_label( - self.app_obj.general_custom_dl_obj.name, - ) - popup_menu.append(general_menu_item) - general_menu_item.connect( - 'activate', - self.on_custom_dl_menu_select, - media_data_list, - self.app_obj.general_custom_dl_obj.uid, - ) - - # The custom download manager usually used in the Classic Mode tab - # (but don't use it if it's the same as the previous one) - if self.app_obj.classic_custom_dl_obj is not None \ - and self.app_obj.classic_custom_dl_obj \ - != self.app_obj.general_custom_dl_obj: - - classic_menu_item = Gtk.MenuItem.new_with_label( - self.app_obj.classic_custom_dl_obj.name, - ) - popup_menu.append(classic_menu_item) - classic_menu_item.connect( - 'activate', - self.on_custom_dl_menu_select, - media_data_list, - self.app_obj.classic_custom_dl_obj.uid, - ) - - # (Get a sorted list of custom download managers, excluding the default - # ones) - manager_list = self.app_obj.compile_custom_dl_manager_list() - if manager_list: - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Other custom download managers - for this_obj in manager_list: - - this_menu_item = Gtk.MenuItem.new_with_label(this_obj.name) - popup_menu.append(this_menu_item) - this_menu_item.connect( - 'activate', - self.on_custom_dl_menu_select, - media_data_list, - this_obj.uid, - ) - - return popup_menu - - - def delete_profile_popup_submenu(self): - - """Called by several functions to create a sub-menu, within a parent - popup menu. - - The sub-menu contains a list of profile names (keys in - mainapp.TartubeApp.profile_dict), If the user selects one, the profile - is deleted - - Return values: - - The sub-menu created - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Extra items for the main window menu,' \ - + ' can be found in Media > Profiles > Delete profile > ...' - ) - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Title - choose_menu_item = Gtk.MenuItem.new_with_label( - _('Choose a profile:'), - ) - popup_menu.append(choose_menu_item) - choose_menu_item.set_sensitive(False) - - if self.app_obj.profile_dict: - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - for profile_name in sorted(self.app_obj.profile_dict): - - this_menu_item = Gtk.MenuItem.new_with_label(profile_name) - popup_menu.append(this_menu_item) - this_menu_item.connect( - 'activate', - self.on_delete_profile_menu_select, - profile_name, - ) - - return popup_menu - - - def switch_profile_popup_submenu(self): - - """Called by several functions to create a sub-menu, within a parent - popup menu. - - The sub-menu contains a list of profile names (keys in - mainapp.TartubeApp.profile_dict), If the user selects one, we switch - to that profile, marking or unmarking items in the Video Index - accordingly. - - Return values: - - The sub-menu created - - """ - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Title - choose_menu_item = Gtk.MenuItem.new_with_label( - _('Choose a profile:'), - ) - popup_menu.append(choose_menu_item) - choose_menu_item.set_sensitive(False) - - if self.app_obj.profile_dict: - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - for profile_name in sorted(self.app_obj.profile_dict): - - this_menu_item = Gtk.MenuItem.new_with_label(profile_name) - popup_menu.append(this_menu_item) - this_menu_item.connect( - 'activate', - self.on_switch_profile_menu_select, - profile_name, - ) - - return popup_menu - - - def video_index_setup_contents_submenu(self, submenu, media_data_obj, - only_child_videos_flag=False): - - """Called by self.video_index_popup_menu(). - - Sets up a submenu for handling the contents of a channel, playlist - or folder. - - Args: - - submenu (Gtk.Menu): The submenu to set up, currently empty - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - channel, playlist or folder whose contents should be modified - by items in the sub-menu - - only_child_videos_flag (bool): Set to True when only a folder's - child videos (not anything in its child channels, playlists or - folders) should be modified by items in the sub-menu; False if - all child objects should be modified - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Extra items for the Video Index popup menu.' \ - + ' In the Videos tab, right-click any channel/playlist/folder' - ) - - mark_archived_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as _archived'), - ) - mark_archived_menu_item.connect( - 'activate', - self.on_video_index_mark_archived, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_archived_menu_item) - - mark_not_archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as not a_rchived'), - ) - mark_not_archive_menu_item.connect( - 'activate', - self.on_video_index_mark_not_archived, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_not_archive_menu_item) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as _bookmarked'), - ) - mark_bookmark_menu_item.connect( - 'activate', - self.on_video_index_mark_bookmark, - media_data_obj, - ) - submenu.append(mark_bookmark_menu_item) - if media_data_obj == self.app_obj.fixed_bookmark_folder: - mark_bookmark_menu_item.set_sensitive(False) - - mark_not_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as not b_ookmarked'), - ) - mark_not_bookmark_menu_item.connect( - 'activate', - self.on_video_index_mark_not_bookmark, - media_data_obj, - ) - submenu.append(mark_not_bookmark_menu_item) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as _favourite'), - ) - mark_fav_menu_item.connect( - 'activate', - self.on_video_index_mark_favourite, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_fav_menu_item) - if media_data_obj == self.app_obj.fixed_fav_folder: - mark_fav_menu_item.set_sensitive(False) - - mark_not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as not fa_vourite'), - ) - mark_not_fav_menu_item.connect( - 'activate', - self.on_video_index_mark_not_favourite, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_not_fav_menu_item) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_missing_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as _missing'), - ) - mark_missing_menu_item.connect( - 'activate', - self.on_video_index_mark_missing, - media_data_obj, - ) - submenu.append(mark_missing_menu_item) - # Only videos in channels/playlists can be marked as missing - if isinstance(media_data_obj, media.Folder): - mark_missing_menu_item.set_sensitive(False) - - mark_not_missing_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as not m_issing'), - ) - mark_not_missing_menu_item.connect( - 'activate', - self.on_video_index_mark_not_missing, - media_data_obj, - ) - submenu.append(mark_not_missing_menu_item) - # Only videos in channels/playlists can be marked as not missing - # (exception: the 'Missing Videos' folder) - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj != self.app_obj.fixed_missing_folder: - mark_not_missing_menu_item.set_sensitive(False) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_new_menu_item = Gtk.MenuItem.new_with_mnemonic(_('Mark as _new')) - mark_new_menu_item.connect( - 'activate', - self.on_video_index_mark_new, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_new_menu_item) - if media_data_obj == self.app_obj.fixed_new_folder: - mark_new_menu_item.set_sensitive(False) - - mark_old_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as not n_ew'), - ) - mark_old_menu_item.connect( - 'activate', - self.on_video_index_mark_not_new, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_old_menu_item) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as in _waiting list'), - ) - mark_playlist_menu_item.connect( - 'activate', - self.on_video_index_mark_waiting, - media_data_obj, - ) - submenu.append(mark_playlist_menu_item) - if media_data_obj == self.app_obj.fixed_waiting_folder: - mark_playlist_menu_item.set_sensitive(False) - - mark_not_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('Mark as not in waiting _list'), - ) - mark_not_playlist_menu_item.connect( - 'activate', - self.on_video_index_mark_not_waiting, - media_data_obj, - ) - submenu.append(mark_not_playlist_menu_item) - - - # (Video Index) - - - def video_index_catalogue_reset(self, reselect_flag=False): - - """Can be called by anything. - - A convenient way to redraw the Video Index and Video Catalogue with a - one-line call. - - Args: - - reselect_flag (bool): If True, the currently selected channel/ - playlist/folder in the Video Index is re-selected, which draws - any child videos in the Video Catalogue - - """ - - # Reset the Video Index and Video Catalogue - self.video_index_reset() - self.video_catalogue_reset() - self.video_index_populate() - - # Re-select the old selection, if required - if reselect_flag and self.video_index_current_dbid is not None: - - dbid = self.video_index_current_dbid - self.video_index_select_row(self.app_obj.media_reg_dict[dbid]) - - - def video_index_reset(self): - - """Can be called by anything. - - On the first call, sets up the widgets for the Video Index. - - On subsequent calls, replaces those widgets, ready for them to be - filled with new data. - """ - - # Reset IVs - self.video_index_current_dbid = None - if self.video_index_treeview: - self.video_index_row_dict = {} - # (Temporarily move key/value pairs in the 'current' IV into an - # 'old' one; the subsequent call to self.video_index_populate() - # restores them) - self.video_index_old_marker_dict \ - = self.video_index_marker_dict.copy() - self.video_index_marker_dict = {} - - # Remove the old widgets - if self.video_index_frame.get_child(): - self.video_index_frame.remove( - self.video_index_frame.get_child(), - ) - - # Set up the widgets - self.video_index_scrolled = Gtk.ScrolledWindow() - self.video_index_frame.add(self.video_index_scrolled) - self.video_index_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - self.video_index_treeview = Gtk.TreeView() - self.video_index_scrolled.add(self.video_index_treeview) - self.video_index_treeview.set_can_focus(False) - self.video_index_treeview.set_headers_visible(False) - if not self.app_obj.show_tooltips_flag: - self.video_index_treeview.set_tooltip_column(-1) - else: - self.video_index_treeview.set_tooltip_column( - self.video_index_tooltip_column, - ) - - # (Detect right-clicks on the treeview) - self.video_index_treeview.connect( - 'button-press-event', - self.on_video_index_right_click, - ) - - # Setup up drag and drop. Drag and drop within the Video Index - # (dragging one channel/playlist/folder) is handled by spotting the - # selected row. Dragging videos from the Video Catalogue into a - # channel/playlist/folder is handled by storing a list of videos - # involved, at the start of the drag. - # Therefore, we accept any incoming drag data, since it is not used - # anyway - drag_target_list = [('text/plain', 0, 0)] - self.video_index_treeview.enable_model_drag_source( - # Mask of mouse buttons allowed to start a drag - Gdk.ModifierType.BUTTON1_MASK, - # Table of targets the drag procedure supports, and array length - drag_target_list, - # Bitmask of possible actions for a drag from this widget - Gdk.DragAction.MOVE, - ) - self.video_index_treeview.enable_model_drag_dest( - # Table of targets the drag procedure supports, and array length - drag_target_list, - # Bitmask of possible actions for a drag from this widget - Gdk.DragAction.DEFAULT, - ) - self.video_index_treeview.connect( - 'drag-drop', - self.on_video_index_drag_drop, - ) - self.video_index_treeview.connect( - 'drag-data-received', - self.on_video_index_drag_data_received, - ) - - self.video_index_treestore = Gtk.TreeStore( - int, - str, str, - GdkPixbuf.Pixbuf, bool, str, - int, int, int, bool, # Column bindings - ) - self.video_index_sortmodel = Gtk.TreeModelSort( - self.video_index_treestore - ) - self.video_index_treeview.set_model(self.video_index_sortmodel) - self.video_index_sortmodel.set_sort_column_id(1, 0) - # (Sort by name, by default) - self.video_index_sortmodel.set_sort_func( - 1, - self.video_index_auto_sort, - None, - ) - - # (From https://stackoverflow.com/questions/49836499/ - # make-only-some-rows-bold-in-a-gtk-treeview) - # Column #5's properties are bound onto columns #6-#9 - # 6: style (int, Pango.Style.NORMAL or Pango.Style.ITALIC) - # 7: weight (int, Pango.Weight.NORMAL or Pango.Weight.BOLD) - # 8: underline (int, Pango.Underline.NONE, Pango.Underline.SINGLE or - # Pango.Underline.ERROR) - # 9: strikethrough (bool) - count = -1 - for item in [ - 'hide', 'hide', 'hide', 'pixbuf', 'mark', 'text', 'bind_int', - 'bind_int', 'bind_int', 'bind_bool', - ]: - count += 1 - - if item == 'hide' or item == 'bind_int': - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - None, - renderer_text, - text=count, - ) - self.video_index_treeview.append_column(column_text) - column_text.set_visible(False) - - elif item == 'pixbuf': - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - None, - renderer_pixbuf, - pixbuf=count, - ) - self.video_index_treeview.append_column(column_pixbuf) - - elif item == 'text': - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - None, - renderer_text, - text=count, - style=6, # Bind italics to column #6 - style_set=True, - weight=7, # Bind bold text to column #7 - weight_set=True, - underline=8, - underline_set=True, # Bind underline to column #8 - strikethrough=9, - strikethrough_set=True, # Bind strikethrough to column #9 - ) - self.video_index_treeview.append_column(column_text) - - elif item == 'mark' or item == 'bind_bool': - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - None, - renderer_toggle, - active=count, - ) - self.video_index_treeview.append_column(column_toggle) - if item == 'bind_bool': - column_toggle.set_visible(False) - else: - renderer_toggle.set_sensitive(True) - renderer_toggle.set_activatable(True) - renderer_toggle.connect( - 'toggled', - self.on_video_index_marker_toggled, - ) - if not self.app_obj.show_marker_in_index_flag: - column_toggle.set_visible(False) - - selection = self.video_index_treeview.get_selection() - selection.connect('changed', self.on_video_index_selection_changed) - - # Make the changes visible - self.video_index_frame.show_all() - - - def video_index_populate(self): - - """Can be called by anything. - - Repopulates the Video Index (assuming that it is already empty, either - because Tartube has just started, or because of an earlier call to - self.video_index_reset() ). - - After the call to this function, new rows can be added via a call to - self.video_index_add_row(). - """ - - for dbid in self.app_obj.container_top_level_list: - - media_data_obj = self.app_obj.media_reg_dict[dbid] - if not media_data_obj: - return self.app_obj.system_error( - 207, - 'Video Index initialisation failure', - ) - - else: - self.video_index_setup_row(media_data_obj, None) - - # Make the changes visible - self.video_index_treeview.show_all() - - # Update IVs. The calls to self.video_index_setup_row() will already - # have repopulated self.video_index_marker_dict - self.video_index_old_marker_dict = {} - - - def video_index_setup_row(self, media_data_obj, parent_pointer=None): - - """Called by self.video_index_populate(). Subsequently called by this - function recursively. - - Adds a row to the Video Index. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object for this row - - parent_pointer (Gtk.TreeIter): None if the media data object has no - parent. Otherwise, a pointer to the position of the parent - object in the treeview - - """ - - # Don't show a hidden folder, or any of its children - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - return - - # Prepare the icon - pixbuf = self.video_index_get_icon(media_data_obj) - if not pixbuf: - return self.app_obj.system_error( - 208, - 'Video Index setup row request failed sanity check', - ) - - # Prepare the text style - style, weight, underline, strike \ - = self.video_index_get_text_properties(media_data_obj) - - # Prepare the marker - if not media_data_obj.dbid in self.video_index_marker_dict \ - and not media_data_obj.dbid in self.video_index_old_marker_dict: - marker_flag = False - else: - marker_flag = True - - # Add a row to the treeview - new_pointer = self.video_index_treestore.append( - parent_pointer, - [ - media_data_obj.dbid, - media_data_obj.name, - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - pixbuf, - marker_flag, - self.video_index_get_text(media_data_obj), - style, - weight, - underline, - strike, - ], - ) - - # Create a reference to the row, so we can find it later - tree_ref = Gtk.TreeRowReference.new( - self.video_index_treestore, - self.video_index_treestore.get_path(new_pointer), - ) - - # Update IVs - self.video_index_row_dict[media_data_obj.dbid] = tree_ref - if media_data_obj.dbid in self.video_index_marker_dict: - self.video_index_marker_dict[media_data_obj.dbid] = tree_ref - if media_data_obj.dbid in self.video_index_old_marker_dict: - del self.video_index_old_marker_dict[media_data_obj.dbid] - - # Call this function recursively for any child objects that are - # channels, playlists or folders (videos are not displayed in the - # Video Index) - for child_obj in media_data_obj.child_list: - - if not(isinstance(child_obj, media.Video)): - self.video_index_setup_row(child_obj, new_pointer) - - - def video_index_add_row(self, media_data_obj, no_select_flag=False): - - """Can be called by anything. - - Adds a row to the Video Index. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object for this row - - no_select_flag (bool): True if the new row should NOT be - automatically selected, as if ordinarily would be - - """ - - # Don't add a hidden folder, or any of its children - if media_data_obj.is_hidden(): - return - - # Prepare the icon - pixbuf = self.video_index_get_icon(media_data_obj) - if not pixbuf: - return self.app_obj.system_error( - 209, - 'Video Index setup row request failed sanity check', - ) - - # Prepare the text style - style, weight, underline, strike \ - = self.video_index_get_text_properties(media_data_obj) - - # Prepare the marker - if not media_data_obj.dbid in self.video_index_marker_dict \ - and not media_data_obj.dbid in self.video_index_old_marker_dict: - marker_flag = False - else: - marker_flag = True - - # Add a row to the treeview - if media_data_obj.parent_obj: - - # This media data object has a parent, so we add a row inside the - # parent's row - - # Fetch the treeview reference to the parent media data object... - parent_ref \ - = self.video_index_row_dict[media_data_obj.parent_obj.dbid] - # ...and add the new object inside its parent - tree_iter = self.video_index_treestore.get_iter( - parent_ref.get_path(), - ) - - new_pointer = self.video_index_treestore.append( - tree_iter, - [ - media_data_obj.dbid, - media_data_obj.name, - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - pixbuf, - marker_flag, - self.video_index_get_text(media_data_obj), - style, - weight, - underline, - strike, - ], - ) - - else: - - # The media data object has no parent, so add a row to the - # treeview's top level - new_pointer = self.video_index_treestore.append( - None, - [ - media_data_obj.dbid, - media_data_obj.name, - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - pixbuf, - marker_flag, - self.video_index_get_text(media_data_obj), - style, - weight, - underline, - strike, - ], - ) - - # Create a reference to the row, so we can find it later - tree_ref = Gtk.TreeRowReference.new( - self.video_index_treestore, - self.video_index_treestore.get_path(new_pointer), - ) - - # Update IVs - self.video_index_row_dict[media_data_obj.dbid] = tree_ref - if media_data_obj.dbid in self.video_index_marker_dict: - self.video_index_marker_dict[media_data_obj.dbid] = tree_ref - if media_data_obj.dbid in self.video_index_old_marker_dict: - del self.video_index_old_marker_dict[media_data_obj.dbid] - - if media_data_obj.parent_obj: - - # Expand rows to make the new media data object visible... - self.video_index_treeview.expand_to_path( - self.video_index_sortmodel.convert_child_path_to_path( - parent_ref.get_path(), - ), - ) - - # Select the row (which clears the Video Catalogue) - if not no_select_flag: - selection = self.video_index_treeview.get_selection() - selection.select_path( - self.video_index_sortmodel.convert_child_path_to_path( - tree_ref.get_path(), - ), - ) - - # Make the changes visible - self.video_index_treeview.show_all() - - - def video_index_delete_row(self, media_data_obj): - - """Can be called by anything. - - Removes a row from the Video Index. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object for this row - - """ - - # Videos can't be shown in the Video Index - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 210, - 'Video Index delete row request failed sanity check', - ) - - # During this procedure, ignore any changes to the selected row (i.e. - # don't allow self.on_video_index_selection_changed() to redraw the - # catalogue) - self.ignore_video_index_select_flag = True - - # Remove the treeview row - tree_ref = self.video_index_row_dict[media_data_obj.dbid] - tree_path = tree_ref.get_path() - tree_iter = self.video_index_treestore.get_iter(tree_path) - self.video_index_treestore.remove(tree_iter) - - self.ignore_video_index_select_flag = False - - # If the deleted row was the previously selected one, the new selected - # row is the one just above/below that - # In this situation, unselect the row and then redraw the Video - # Catalogue - if self.video_index_current_dbid is not None \ - and self.video_index_current_dbid == media_data_obj.dbid: - - selection = self.video_index_treeview.get_selection() - selection.unselect_all() - - self.video_index_current_dbid = None - self.video_catalogue_reset() - - # Update IVs - if media_data_obj.dbid in self.video_index_marker_dict: - del self.video_index_marker_dict[media_data_obj.dbid] - - # Make the changes visible - self.video_index_treeview.show_all() - - - def video_index_select_row(self, media_data_obj): - - """Can be called by anything. - - Selects a row in the Video Index, as if the user had clicked it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be selected - - """ - - # Cannot select a hidden folder, or any of its children - if isinstance(media_data_obj, media.Video) \ - or media_data_obj.is_hidden(): - return self.app_obj.system_error( - 211, - 'Video Index select row request failed sanity check', - ) - - # Select the row, expanding the treeview path to make it visible, if - # necessary - if media_data_obj.parent_obj: - - # Expand rows to make the new media data object visible... - parent_ref \ - = self.video_index_row_dict[media_data_obj.parent_obj.dbid] - - self.video_index_treeview.expand_to_path( - self.video_index_sortmodel.convert_child_path_to_path( - parent_ref.get_path(), - ), - ) - - # Select the row - tree_ref = self.video_index_row_dict[media_data_obj.dbid] - - selection = self.video_index_treeview.get_selection() - selection.select_path( - self.video_index_sortmodel.convert_child_path_to_path( - tree_ref.get_path(), - ), - ) - - - def video_index_update_row_icon(self, media_data_obj): - - """Can be called by anything. - - The icons used in the Video Index must be changed when a media data - object is marked (or unmarked) favourite, and when download options - are applied/removed. - - This function updates a row in the Video Index to show the right icon. - - N.B. Because Gtk is not thread safe, this function must always be - called from within GObject.timeout_add(). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be updated - - """ - - # Videos can't be shown in the Video Index - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 212, - 'Video Index update row request failed sanity check', - ) - - # If media_data_obj is a hidden folder, then there's nothing to update - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - return - - # !!! DEBUG Git #523 - try: - # Update the treeview row - tree_ref = self.video_index_row_dict[media_data_obj.dbid] - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - model.set(tree_iter, 3, self.video_index_get_icon(media_data_obj)) - - # Make the changes visible - self.video_index_treeview.show_all() - - except: - return self.app_obj.system_error( - 272, - 'Video Index update row request failed because row missing' \ - + ' from registry', - ) - - - def video_index_update_row_text(self, media_data_obj): - - """Can be called by anything. - - The text used in the Video Index must be changed when a media data - object is updated, including when a child video object is added or - removed. - - This function updates a row in the Video Index to show the new text. - - N.B. Because Gtk is not thread safe, this function must always be - called from within GObject.timeout_add(). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be updated - - """ - - # Videos can't be shown in the Video Index - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 213, - 'Video Index update row request failed sanity check', - ) - - # If media_data_obj is a hidden folder, then there's nothing to update - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - return - - # !!! DEBUG Git #523 - try: - # Update the treeview row - tree_ref = self.video_index_row_dict[media_data_obj.dbid] - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - model.set(tree_iter, 5, self.video_index_get_text(media_data_obj)) - - style, weight, underline, strike \ - = self.video_index_get_text_properties(media_data_obj) - model.set(tree_iter, 6, style) - model.set(tree_iter, 7, weight) - model.set(tree_iter, 8, underline) - model.set(tree_iter, 9, strike) - - # Make the changes visible - self.video_index_treeview.show_all() - - except: - return self.app_obj.system_error( - 273, - 'Video Index update row request failed because row missing' \ - + ' from registry', - ) - - - def video_index_update_row_tooltip(self, media_data_obj): - - """Can be called by anything. - - The tooltips used in the Video Index must be changed when a media data - object is updated. - - This function updates the (hidden) row in the Video Index containing - the text for tooltips. - - N.B. Because Gtk is not thread safe, this function must always be - called from within GObject.timeout_add(). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be updated - - """ - - # Videos can't be shown in the Video Index - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 214, - 'Video Index update row request failed sanity check', - ) - - # If media_data_obj is a hidden folder, then there's nothing to update - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - return - - # !!! DEBUG Git #523 - try: - # Update the treeview row - tree_ref = self.video_index_row_dict[media_data_obj.dbid] - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - model.set( - tree_iter, - 2, - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - ) - - # Make the changes visible - self.video_index_treeview.show_all() - - except: - return self.app_obj.system_error( - 274, - 'Video Index update row request failed because row missing' \ - + ' from registry', - ) - - - def video_index_get_icon(self, media_data_obj): - - """Called by self.video_index_setup_row(), - .video_index_add_row() and .video_index_update_row_icon(). - - Finds the icon to display on a Video Index row for the specified media - data object. - - Looks up the GdkPixbuf which has already been created for that icon - and returns it (or None, if the icon file is missing or if no - corresponding pixbuf can be found.) - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be updated - - Return values: - - A GdkPixbuf or None - - """ - - icon = None - if not self.app_obj.show_small_icons_in_index_flag: - - # (The favourite icon for the red folder is a different colour) - alt_flag = False - - # Large icons, bigger selection - if isinstance(media_data_obj, media.Channel): - icon = 'channel_large' - elif isinstance(media_data_obj, media.Playlist): - icon = 'playlist_large' - elif isinstance(media_data_obj, media.Folder): - if media_data_obj.priv_flag: - icon = 'folder_private_large' - alt_flag = True - elif media_data_obj.temp_flag: - icon = 'folder_temp_large' - elif media_data_obj.fixed_flag: - icon = 'folder_fixed_large' - else: - icon = 'folder_large' - - # (Apply overlays) - if media_data_obj.dbid in self.app_obj.container_unavailable_dict: - icon += '_tl' - if media_data_obj.dl_no_db_flag \ - or media_data_obj.dl_disable_flag \ - or media_data_obj.dl_sim_flag: - icon += '_tr' - if media_data_obj.fav_flag: - if not alt_flag: - icon += '_bl' - else: - icon += '_bl_alt' - if media_data_obj.options_obj: - icon += '_br' - - else: - - # Small icons, smaller selection - if isinstance(media_data_obj, media.Channel): - icon = 'channel_small' - elif isinstance(media_data_obj, media.Playlist): - icon = 'playlist_small' - elif isinstance(media_data_obj, media.Folder): - if media_data_obj.priv_flag: - icon = 'folder_red_small' - elif media_data_obj.temp_flag: - icon = 'folder_blue_small' - elif media_data_obj.fixed_flag: - icon = 'folder_green_small' - else: - icon = 'folder_small' - - if icon is not None and icon in self.icon_dict: - return self.pixbuf_dict[icon] - else: - # Invalid 'icon', or file not found - return None - - - def video_index_get_text(self, media_data_obj): - - """Called by self.video_index_setup_row(), .video_index_add_row() and - .video_index_update_row_text(). - - Sets the text to display on a Video Index row for the specified media - data object. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - A media data object visible in the Video Index - - Return values: - - A string - - """ - - text = utils.shorten_string( - media_data_obj.nickname, - self.short_string_max_len, - ) - - if not self.app_obj.complex_index_flag: - - if media_data_obj.dl_count: - text += ' (' + str(media_data_obj.new_count) + '/' \ - + str(media_data_obj.dl_count) + ')' - - else: - - ignore_me = _( - 'TRANSLATOR\'S NOTE: V = number of videos B = (number of' \ - + ' videos) bookmarked D = downloaded F = favourite' \ - + ' L = live/livestream M = missing N = new W = in waiting' \ - + ' list E = (number of) errors W = warnings. Choose any' \ - + ' abbreviation you like.', - ) - - if media_data_obj.vid_count: - text += '\n' + _('V:') + str(media_data_obj.vid_count) \ - + ' ' + _('B:') + str(media_data_obj.bookmark_count) \ - + ' ' + _('D:') + str(media_data_obj.dl_count) \ - + ' ' + _('F:') + str(media_data_obj.fav_count) \ - + ' ' + _('L:') + str(media_data_obj.live_count) \ - + ' ' + _('M:') + str(media_data_obj.missing_count) \ - + ' ' + _('N:') + str(media_data_obj.new_count) \ - + ' ' + _('W:') + str(media_data_obj.waiting_count) - - if not isinstance(media_data_obj, media.Folder) \ - and (media_data_obj.error_list or media_data_obj.warning_list): - - if not media_data_obj.vid_count: - text += '\n' - else: - text += ' ' - - text += _('E:') + str(len(media_data_obj.error_list)) \ - + ' ' + _('W:') + str(len(media_data_obj.warning_list)) - - return text - - - def video_index_get_text_properties(self, media_data_obj): - - """Called by self.video_index_setup_row(), .video_index_add_row and - .video_index_update_row_text(). - - Returns a list of text style properties for displaying the name of the - specified media data object. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - item selected in the Video Index - - Return values: - - A list in the form (style, weight, underline, strike): - - style (int): Pango.Style.NORMAL or Pango.Style.ITALIC - weight (int): Pango.Weight.NORMAL or Pango.Weight.BOLD - underline (int): Pango.Underline.NONE, Pango.Underline.SINGLE - or Pango.Underline.ERROR - strikethrough (bool): True for strikethrough, False otherwise - - """ - - style = Pango.Style.NORMAL - weight = Pango.Weight.NORMAL - underline = Pango.Underline.NONE - strike = False - - # When large icons are visible, we only change the Pango.Weight - if not self.app_obj.show_small_icons_in_index_flag: - - # If marked new (unwatched), show as bold text - if media_data_obj.new_count: - weight = Pango.Weight.BOLD - - # When smaller icons are visible, use italics and strikethrough - else: - - # If an external directory is disabled, show as strikethrough - if media_data_obj.dbid in self.app_obj.container_unavailable_dict: - strike = True - - else: - # If marked new (unwatched), show as bold text - if media_data_obj.new_count: - weight = Pango.Weight.BOLD - - # If adding videos to the database or checking/downloading - # is disabled, show as italic text (perhaps in addition - # to bold text) - if media_data_obj.dl_no_db_flag: - style = Pango.Style.ITALIC - underline = Pango.Underline.ERROR - elif media_data_obj.dl_disable_flag: - style = Pango.Style.ITALIC - underline = Pango.Underline.SINGLE - elif media_data_obj.dl_sim_flag: - style = Pango.Style.ITALIC - - return style, weight, underline, strike - - - def video_index_set_marker(self, dbid=None): - - """Can be called by anything. - - Sets the marker on a specified row of the Video Index (or on all of - them for which the markes are allowed to be set). - - Args: - - dbid (str): The .dbid of a media.Channel, media.Playlist or - media.Folder; a key in mainapp.TartubeApp.container_reg_dict - - """ - - old_size = len(self.video_index_marker_dict) - - container_list = [] - if dbid is None: - - # Set all markers in the Video Index - container_list = list(self.video_index_row_dict.keys()) - - else: - - # Set the marker on the row for the specified channel/playlist/ - # folder - container_list = [ dbid ] - - for this_dbid in container_list: - - # System folders cannot be marked - # Channels/playlists/folders for which checking and downloading is - # disabled can't be marked - media_data_obj = self.app_obj.media_reg_dict[this_dbid] - if ( - not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.priv_flag - ) and not media_data_obj.dl_disable_flag: - - tree_ref = self.video_index_row_dict[this_dbid] - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - - model.set(tree_iter, 4, True) - - self.video_index_marker_dict[this_dbid] = tree_ref - - if not old_size and self.video_index_marker_dict: - # Update labels on the 'Check all' button, etc - # The True argument skips the check for the existence of a progress - # bar - self.hide_progress_bar(True) - - - def video_index_reset_marker(self, dbid=None): - - """Can be called by anything. - - Resets the marker on a specified row of the Video Index (or on all of - them). - - Args: - - dbid (str): The .dbid of a media.Channel, media.Playlist or - media.Folder; a key in mainapp.TartubeApp.container_reg_dict - - """ - - old_size = len(self.video_index_marker_dict) - - if dbid is None: - - # Reset all markers in the Video Index - for tree_ref in self.video_index_row_dict.values(): - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - - model.set(tree_iter, 4, False) - - self.video_index_marker_dict = {} - - elif dbid in self.app_obj.container_reg_dict: - - # Reset the marker on the row for the specified channel/playlist/ - # folder - tree_ref = self.video_index_row_dict[dbid] - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - - model.set(tree_iter, 4, False) - - if dbid in self.video_index_marker_dict: - del self.video_index_marker_dict[dbid] - - if old_size and not self.video_index_marker_dict: - - # Update labels on the 'Check all' button, etc - # The True argument skips the check for the existence of a progress - # bar - self.hide_progress_bar(True) - - - # (Video Catalogue) - - - def video_catalogue_reset(self): - - """Can be called by anything. - - On the first call, sets up the widgets for the Video Catalogue. On - subsequent calls, replaces those widgets, ready for them to be filled - with new data. - """ - - # If not called by self.setup_videos_tab()... - if self.catalogue_frame.get_child(): - self.catalogue_frame.remove(self.catalogue_frame.get_child()) - - # Reset IVs (when called by anything) - self.video_catalogue_dict = {} - self.video_catalogue_temp_list = [] - self.catalogue_listbox = None - self.catalogue_grid = None - self.catalogue_grid_expand_flag = False - # (self.catalogue_grid_column_count is not set here) - self.catalogue_grid_row_count = 1 - - # Set up the widgets - self.catalogue_scrolled = Gtk.ScrolledWindow() - self.catalogue_frame.add(self.catalogue_scrolled) - - if self.app_obj.catalogue_mode_type != 'grid': - - self.catalogue_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - self.catalogue_listbox = Gtk.ListBox() - self.catalogue_scrolled.add(self.catalogue_listbox) - self.catalogue_listbox.set_can_focus(False) - self.catalogue_listbox.set_selection_mode( - Gtk.SelectionMode.MULTIPLE, - ) - # (Without this line, it's not possible to unselect rows by - # clicking on one of them) - self.catalogue_listbox.set_activate_on_single_click(False) - - # (Drag and drop is now handled by mainwin.CatalogueRow directly) - - # Set up automatic sorting of rows in the listbox - self.catalogue_listbox.set_sort_func( - self.video_catalogue_generic_auto_sort, - None, - False, - ) - - else: - - # (No horizontal scrolling in grid mode) - self.catalogue_scrolled.set_policy( - Gtk.PolicyType.NEVER, - Gtk.PolicyType.AUTOMATIC, - ) - - self.catalogue_grid = Gtk.Grid() - self.catalogue_scrolled.add(self.catalogue_grid) - self.catalogue_grid.set_can_focus(False) - self.catalogue_grid.set_border_width(self.spacing_size) - self.catalogue_grid.set_column_spacing(self.spacing_size) - self.catalogue_grid.set_row_spacing(self.spacing_size) - - # (Video selection is handled by custom code, not calls to Gtk) - - # (Drag and drop is now handled by mainwin.CatalogueGridBox - # directly) - - # (Automatic sorting is handled by custom code, not calls to Gtk) - - # Make the changes visible - self.catalogue_frame.show_all() - - - def video_catalogue_redraw_all(self, dbid, page_num=1, - reset_scroll_flag=False, no_cancel_filter_flag=False): - - """Can be called by anything. - - When the user clicks on a media data object in the Video Index (a - channel, playlist or folder), this function is called to replace the - contents of the Video Catalogue with some or all of the video objects - stored as children in that channel, playlist or folder. - - Depending on the value of self.catalogue_mode, the Video Catalogue - consists of a list of mainwin.SimpleCatalogueItem or - mainwin.ComplexCatalogueItem objects, one for each row in the - Gtk.ListBox; or a mainwin.GridCatalogueItem, one for each gridbox in - the Gtk.Grid. Each row/gridbox corresponds to a single video. - - The video catalogue splits its video list into pages (as Gtk struggles - with a list of hundreds, or thousands, of videos). Only videos on the - specified page (or on the current page, if no page is specified) are - drawn. If mainapp.TartubeApp.catalogue_page_size is set to zero, all - videos are drawn on a single page. - - If a filter has been applied, only videos matching the search text - are visible in the catalogue. - - This function clears the previous contents of the Gtk.ListBox/Gtk.Grid - and resets IVs. - - Then, it adds new rows to the Gtk.ListBox, or new gridboxes to the - Gtk.Grid, and creates a new mainwin.SimpleCatalogueItem, - mainwin.ComplexCatalogueItem or mainwin.GridCatalogueItem object for - each video on the page. - - Args: - - dbid (str): The selected media data object's .dbid; one of the keys - in mainapp.TartubeApp.container_reg_dict - - page_num (int): The number of the page to be drawn (a value in the - range 1 to self.catalogue_toolbar_last_page) - - reset_scroll_flag (bool): True if the vertical scrollbar must be - reset (for example, when switching between channels/playlists/ - folders) - - no_cancel_filter_flag (bool): By default, if the filter is applied, - it is cancelled by this function. Set to True if the calling - function doesn't want that (for example, because it has just - set up the filter, and wants to show only matching videos) - - """ - - # If actually switching to a different channel/playlist/folder, or a - # different page on the same channel/playlist/folder, must reset the - # scrollbars later in the function - if not reset_scroll_flag: - if self.video_index_current_dbid is None \ - or self.video_index_current_dbid != dbid \ - or self.catalogue_toolbar_current_page != page_num: - reset_scroll_flag = True - - # The item selected in the Video Index is a media.Channel, - # media.playlist or media.Folder object - if not dbid in self.app_obj.container_reg_dict: - - return self.app_obj.system_error( - 215, - 'Cannot redraw Video Catalogue because container is missing' \ - + ' from database', - ) - - container_obj = self.app_obj.media_reg_dict[dbid] - - # Sanity check - the selected item should not be a media.Video object - if not container_obj or (isinstance(container_obj, media.Video)): - return self.app_obj.system_error( - 216, - 'Videos should not appear in the Video Index', - ) - - # Formerly, the selected container's children were only re-sorted when - # the sort mode had changed - # Due to continuing and hard-to-diagnose sort problems, just sort each - # container's children every time the Video Catalogue is redrawn - container_obj.sort_children(self.app_obj) - - # Reset the previous contents of the Video Catalogue, if any, and reset - # IVs - self.video_catalogue_reset() - # Temporarily reset widgets in the Video Catalogue toolbar (in case - # something goes wrong, or in case drawing the page takes a long - # time) - self.video_catalogue_toolbar_reset() - # If a filter had recently been applied, reset IVs to cancel it (unless - # the calling function doesn't want that) - # This makes sure that the filter is always reset when the user clicks - # on a different channel/playlist/folder in the Video Index - if not no_cancel_filter_flag: - self.video_catalogue_filtered_flag = False - self.video_catalogue_filtered_list = [] - - # The selected media data object has any number of child media data - # objects, but this function is only interested in those that are - # media.Video objects - video_count = 0 - page_size = self.app_obj.catalogue_page_size - # If the filter has been applied, use the prepared list of child videos - # specified by the IV... - if self.video_catalogue_filtered_flag: - child_list = self.video_catalogue_filtered_list.copy() - # ...otherwise use all child videos that are downloaded/undownloaded/ - # blocked (according to current settings) - else: - child_list = container_obj.get_visible_videos(self.app_obj) - - for child_obj in child_list: - if isinstance(child_obj, media.Video): - - # (We need the number of child videos when we update widgets in - # the toolbar) - video_count += 1 - - # Only draw videos on this page. If the page size is zero, all - # videos are drawn on a single page - if page_size \ - and ( - video_count <= ((page_num - 1) * page_size) \ - or video_count > (page_num * page_size) - ): - # Don't draw the video on this page - continue - - # Create a new catalogue item object for the video - if self.app_obj.catalogue_mode_type == 'simple': - catalogue_item_obj = SimpleCatalogueItem(self, child_obj) - elif self.app_obj.catalogue_mode_type == 'complex': - catalogue_item_obj = ComplexCatalogueItem(self, child_obj) - else: - catalogue_item_obj = GridCatalogueItem(self, child_obj) - - # Update IVs - self.video_catalogue_dict[catalogue_item_obj.dbid] = \ - catalogue_item_obj - - # Add the video to the Video Catalogue - if self.app_obj.catalogue_mode_type != 'grid': - - # Add a row to the Gtk.ListBox - - # Instead of using Gtk.ListBoxRow directly, use a wrapper - # class so we can quickly retrieve the video displayed on - # each row - wrapper_obj = CatalogueRow(self, child_obj) - self.catalogue_listbox.add(wrapper_obj) - - # Populate the row with widgets... - catalogue_item_obj.draw_widgets(wrapper_obj) - # ...and give them their initial appearance - catalogue_item_obj.update_widgets() - - else: - - # Add a gridbox to the Gtk.Grid - - # Instead of using Gtk.Frame directly, use a wrapper class - # so we can quickly retrieve the video displayed on each - # row - wrapper_obj = CatalogueGridBox(self, child_obj) - - # (Place the first video at 0, 0, so in that case, 'count' - # must be 0) - count = len(self.video_catalogue_dict) - 1 - y_pos = int(count / self.catalogue_grid_column_count) - x_pos = count % self.catalogue_grid_column_count - self.video_catalogue_grid_attach_gridbox( - wrapper_obj, - x_pos, - y_pos, - ) - - # Populate the gridbox with widgets... - catalogue_item_obj.draw_widgets(wrapper_obj) - # ...and give them their initial appearance - catalogue_item_obj.update_widgets() - - # Gridboxes could be made (un)expandable, depending on the - # number of gridboxes now on the grid - self.video_catalogue_grid_check_expand() - - # Update widgets in the toolbar, now that we know the number of child - # videos - self.video_catalogue_toolbar_update(page_num, video_count) - - # In all cases, sensitise the scroll up/down toolbar buttons - self.catalogue_scroll_up_button.set_sensitive(True) - self.catalogue_scroll_down_button.set_sensitive(True) - # Reset the scrollbar, if required - if reset_scroll_flag: - self.catalogue_scrolled.get_vadjustment().set_value(0) - - # Procedure complete - if self.app_obj.catalogue_mode_type != 'grid': - self.catalogue_listbox.show_all() - else: - self.catalogue_grid.show_all() - - - def video_catalogue_update_video(self, video_obj): - - """Can be called by anything. - - This function is called with a media.Video object. If that video is - already visible in the Video Catalogue, updates the corresponding - mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem (which - updates the widgets in the Gtk.ListBox), or the corresponding - mainwin.GridCatalogueItem (which updates the widgets in the Gtk.Grid). - - If the video is now yet visible in the Video Catalogue, but should be - drawn on the current page, creates a new mainwin.SimpleCatalogueItem, - mainwin.ComplexCatalogueItem or mainwin.GridCatalogueItem and adds it - to the Gtk.ListBox or Gtk.Grid, removing an existing catalogue item to - make room, if necessary. - - N.B. Because Gtk is not thread safe, this function must always be - called from within GObject.timeout_add(). - - Args: - - video_obj (media.Video): The video to update - - """ - - app_obj = self.app_obj - - # Is the video's parent channel, playlist or folder the one that is - # currently selected in the Video Index? If not, the video is not - # currently displayed in the Video Catalogue - if self.video_index_current_dbid is None \ - or not ( - self.video_index_current_dbid == video_obj.parent_obj.dbid - or self.video_index_current_dbid == app_obj.fixed_all_folder.dbid - or ( - self.video_index_current_dbid \ - == app_obj.fixed_bookmark_folder.dbid \ - and video_obj.bookmark_flag - ) or ( - self.video_index_current_dbid \ - == app_obj.fixed_fav_folder.dbid \ - and video_obj.fav_flag - ) or ( - self.video_index_current_dbid \ - == app_obj.fixed_live_folder.dbid \ - and video_obj.live_mode - ) or ( - self.video_index_current_dbid \ - == app_obj.fixed_missing_folder.dbid - and video_obj.missing_flag - ) or ( - self.video_index_current_dbid == app_obj.fixed_new_folder.dbid - and video_obj.new_flag - ) or ( - self.video_index_current_dbid \ - == app_obj.fixed_recent_folder.dbid - and video_obj in app_obj.fixed_recent_folder.child_list - ) or ( - self.video_index_current_dbid \ - == app_obj.fixed_waiting_folder.dbid \ - and video_obj.waiting_flag - ) - ): - return - - # Does a mainwin.SimpleCatalogueItem, mainwin.ComplexCatalogueItem or - # mainwin.GridCatalogueItem object already exist for this video? - already_exist_flag = False - if video_obj.dbid in self.video_catalogue_dict: - - already_exist_flag = True - - # Update the catalogue item object, which updates the widgets in - # the Gtk.ListBox/Gtk.Grid - catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid] - catalogue_item_obj.update_widgets() - - # Now, deal with the video's position in the catalogue. If a catalogue - # item object already existed, its position may have changed - # (perhaps staying on the current page, perhaps moving to another) - container_obj = app_obj.media_reg_dict[self.video_index_current_dbid] - - # Find the Video Catalogue page on which this video should be shown - page_num = 1 - current_page_num = self.catalogue_toolbar_current_page - page_size = app_obj.catalogue_page_size - # At the same time, reduce the parent container's list of children, - # eliminating those which are media.Channel, media.Playlist and - # media.Folder objects - # Exclude any downloaded/undownloaded/blocked videos, according to - # current settings - sibling_video_list = [] - - # If the filter has been applied, use the prepared list of child videos - # specified by the IV... - if self.video_catalogue_filtered_flag: - child_list = self.video_catalogue_filtered_list.copy() - # ...otherwise use all child videos that are downloaded/undownloaded/ - # blocked (according to current settings) - else: - child_list = container_obj.get_visible_videos(self.app_obj) - - for child_obj in child_list: - if isinstance(child_obj, media.Video) \ - and ( - ( - app_obj.catalogue_draw_downloaded_flag \ - and child_obj.dl_flag - ) or ( - app_obj.catalogue_draw_undownloaded_flag \ - and not child_obj.dl_flag - ) or ( - app_obj.catalogue_draw_blocked_flag \ - and child_obj.block_flag - ) - ): - sibling_video_list.append(child_obj) - - # (If the page size is 0, then all videos are drawn on one - # page, i.e. the current value of page_num, which is 1) - if child_obj == video_obj and page_size: - page_num = int( - (len(sibling_video_list) - 1) / page_size - ) + 1 - - sibling_video_count = len(sibling_video_list) - - # Decide whether to move any catalogue items from this page and, if so, - # what (if anything) should be moved into their place - # If a catalogue item was already visible for this video, then the - # video might need to be displayed on a different page, its position - # on this page being replaced by a different video - # If a catalogue item was not already visible for this video, and if - # it should be drawn on this page or any previous page, then we - # need to remove a catalogue item from this page and replace it with - # another - if (already_exist_flag and page_num != current_page_num) \ - or (not already_exist_flag and page_num <= current_page_num): - - # Compile a dictionary of videos which are currently visible on - # this page - visible_dict = {} - for catalogue_item in self.video_catalogue_dict.values(): - visible_dict[catalogue_item.video_obj.dbid] \ - = catalogue_item.video_obj - - # Check the videos which should be visible on this page. This - # code block leaves us with 'visible_dict' containing videos - # that should no longer be visible on the page, and - # 'missing_dict' containing videos that should be visible on - # the page, but are not - missing_dict = {} - for index in range ( - ((current_page_num - 1) * page_size), - (current_page_num * page_size), - ): - if index < sibling_video_count: - child_obj = sibling_video_list[index] - if not child_obj.dbid in visible_dict: - missing_dict[child_obj.dbid] = child_obj - else: - del visible_dict[child_obj.dbid] - - # Remove any catalogue items for videos that shouldn't be - # visible, but still are - for dbid in visible_dict: - catalogue_item_obj = self.video_catalogue_dict[dbid] - - if self.app_obj.catalogue_mode_type != 'grid': - - self.catalogue_listbox.remove( - catalogue_item_obj.catalogue_row, - ) - - else: - - self.catalogue_grid.remove( - catalogue_item_obj.catalogue_gridbox, - ) - - del self.video_catalogue_dict[dbid] - - # Add any new catalogue items for videos which should be - # visible, but aren't - for dbid in missing_dict: - - # Get the media.Video object - missing_obj = app_obj.media_reg_dict[dbid] - - # Create a new catalogue item - self.video_catalogue_insert_video(missing_obj) - - # Update widgets in the toolbar - self.video_catalogue_toolbar_update( - self.catalogue_toolbar_current_page, - sibling_video_count, - ) - - # Sort the visible list - if self.app_obj.catalogue_mode_type != 'grid': - - # Force the Gtk.ListBox to sort its rows, so that videos are - # displayed in the correct order - self.catalogue_listbox.invalidate_sort() - - else: - - # After sorting gridboxes, rearrange them on the Gtk.Grid - self.video_catalogue_grid_rearrange() - - # Procedure complete - if self.app_obj.catalogue_mode_type != 'grid': - self.catalogue_listbox.show_all() - else: - self.catalogue_grid.show_all() - - - def video_catalogue_insert_video(self, video_obj): - - """Called by self.video_catalogue_update_video() (only). - - Adds a new mainwin.SimpleCatalogueItem, mainwin.ComplexCatalogueItem - or mainwin.GridCatalogueItem to the Video Catalogue. Each catalogue - item handles a single video. - - Args: - - video_obj (media.Video): The video for which a new catalogue item - should be created - - """ - - # Create the new catalogue item - if self.app_obj.catalogue_mode_type == 'simple': - catalogue_item_obj = SimpleCatalogueItem(self, video_obj) - elif self.app_obj.catalogue_mode_type == 'complex': - catalogue_item_obj = ComplexCatalogueItem(self, video_obj) - else: - catalogue_item_obj = GridCatalogueItem(self, video_obj) - - self.video_catalogue_dict[video_obj.dbid] = catalogue_item_obj - - if self.app_obj.catalogue_mode_type != 'grid': - - # Add a row to the Gtk.ListBox - - # Instead of using Gtk.ListBoxRow directly, use a wrapper class so - # we can quickly retrieve the video displayed on each row - wrapper_obj = CatalogueRow(self, video_obj) - - # On rare occasions, the line below sometimes causes a warning, - # 'Accessing a sequence while it is being sorted or seached is - # not allowed' - # If this happens, add it to a temporary list of rows to be added - # to the listbox by self.video_catalogue_retry_insert_items() - try: - self.catalogue_listbox.add(wrapper_obj) - except: - self.video_catalogue_temp_list.append(wrapper_obj) - - # Populate the row with widgets... - catalogue_item_obj.draw_widgets(wrapper_obj) - # ...and give them their initial appearance - catalogue_item_obj.update_widgets() - - else: - - # Add a gridbox to the Gtk.Grid - - # Instead of using Gtk.Frame directly, use a wrapper class so we - # can quickly retrieve the video displayed on each row - wrapper_obj = CatalogueGridBox(self, video_obj) - - # (Place the first video at 0, 0, so in that case, count must be 0) - count = len(self.video_catalogue_dict) - 1 - y_pos = int(count / self.catalogue_grid_column_count) - x_pos = count % self.catalogue_grid_column_count - self.video_catalogue_grid_attach_gridbox( - wrapper_obj, - x_pos, - y_pos, - ) - - # Populate the gridbox with widgets... - catalogue_item_obj.draw_widgets(wrapper_obj) - # ...and give them their initial appearance - catalogue_item_obj.update_widgets() - - # Gridboxes could be made (un)expandable, depending on the number - # of gridboxes now on the grid, and the number of columns allowed - # in the grid - self.video_catalogue_grid_check_expand() - - - def video_catalogue_retry_insert_items(self): - - """Called by mainapp.TartubeApp.script_fast_timer_callback(). - - If an earlier call to self.video_catalogue_insert_video() failed, one - or more CatalogueRow objects are waiting to be added to the Video - Catalogue. Add them, if so. - - (Not called when videos are arranged on a grid.) - """ - - if self.video_catalogue_temp_list: - - while self.video_catalogue_temp_list: - - wrapper_obj = self.video_catalogue_temp_list.pop() - - try: - self.catalogue_listbox.add(wrapper_obj) - except: - # Still can't add the row; try again later - self.video_catalogue_temp_list.append(wrapper_obj) - return - - # All items added. Force the Gtk.ListBox to sort its rows, so that - # videos are displayed in the correct order - self.catalogue_listbox.invalidate_sort() - - # Procedure complete - self.catalogue_listbox.show_all() - - - def video_catalogue_delete_video(self, video_obj): - - """Can be called by anything. - - This function is called with a media.Video object. If that video is - already visible in the Video Catalogue, removes the corresponding - mainwin.SimpleCatalogueItem, mainwin.ComplexCatalogueItem or - mainwin.GridCatalogueItem. - - If the current page was already full of videos, create a new - catalogue item to fill the gap. - - Args: - - video_obj (media.Video): The video to remove - - """ - - # Is the video's parent channel, playlist or folder the one that is - # currently selected in the Video Index? If not, the video is not - # displayed in the Video Catalogue - app_obj = self.app_obj - - if self.video_index_current_dbid is None: - return - - elif self.video_index_current_dbid != video_obj.parent_obj.dbid \ - and self.video_index_current_dbid != app_obj.fixed_all_folder.dbid \ - and ( - self.video_index_current_dbid \ - != app_obj.fixed_bookmark_folder.dbid \ - or not video_obj.bookmark_flag - ) and ( - self.video_index_current_dbid != app_obj.fixed_fav_folder.dbid \ - or not video_obj.fav_flag - ) and ( - self.video_index_current_dbid != app_obj.fixed_live_folder.dbid \ - or not video_obj.live_mode - ) and ( - self.video_index_current_dbid \ - != app_obj.fixed_missing_folder.dbid \ - or not video_obj.missing_flag - ) and ( - self.video_index_current_dbid != app_obj.fixed_new_folder.dbid \ - or not video_obj.new_flag - ) and ( - self.video_index_current_dbid != app_obj.fixed_recent_folder.dbid \ - or video_obj in app_obj.fixed_recent_folder.child_list - ) and ( - self.video_index_current_dbid \ - != app_obj.fixed_waiting_folder.dbid \ - or not video_obj.waiting_flag - ): - return - - # Does a mainwin.SimpleCatalogueItem, mainwin.ComplexCatalogueItem or - # mainwin.GridCatalogueItem object exist for this video? - if video_obj.dbid in self.video_catalogue_dict: - - # Remove the catalogue item object and its mainwin.CatalogueRow or - # mainwin.CatalogueGridBox object (the latter being a wrapper for - # Gtk.ListBoxRow or Gtk.Frame) - catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid] - - # Remove the row from the Gtk.ListBox or Gtk.Grid - if self.app_obj.catalogue_mode_type != 'grid': - - self.catalogue_listbox.remove( - catalogue_item_obj.catalogue_row, - ) - - else: - - self.catalogue_grid.remove( - catalogue_item_obj.catalogue_gridbox, - ) - - # Update IVs - del self.video_catalogue_dict[video_obj.dbid] - - # If the current page is not the last one, we can create a new - # catalogue item to replace the removed one - move_obj = None - container_obj \ - = app_obj.media_reg_dict[self.video_index_current_dbid] - video_count = 0 - - if self.video_catalogue_dict \ - and self.catalogue_toolbar_current_page \ - < self.catalogue_toolbar_last_page: - - # Get the last catalogue object directly from its parent, as - # the parent is auto-sorted frequently - if self.app_obj.catalogue_mode_type != 'grid': - child_list = self.catalogue_listbox.get_children() - else: - child_list = self.catalogue_grid.get_children() - - last_obj = child_list[-1] - if last_obj: - last_video_obj = last_obj.video_obj - - # Find the video object that would be drawn after that, if the - # videos were all drawn on a single page - # At the same time, count the number of remaining child video - # objects so we can update the toolbar - next_flag = False - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - video_count += 1 - if child_obj.dbid == last_video_obj.dbid: - # (Use the next video after this one) - next_flag = True - - elif next_flag == True: - # (Use this video) - insert_obj = child_obj - next_flag = False - - # Create the new catalogue item - if insert_obj: - GObject.timeout_add( - 0, - self.video_catalogue_update_video, - insert_obj, - ) - - else: - - # We're already on the last (or only) page, so no need to - # replace anything. Just count the number of remaining child - # video objects - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - video_count += 1 - - # Update widgets in the Video Catalogue toolbar - self.video_catalogue_toolbar_update( - self.catalogue_toolbar_current_page, - video_count, - ) - - # Procedure complete - if self.app_obj.catalogue_mode_type != 'grid': - - self.catalogue_listbox.show_all() - - else: - - # Fill in any empty spaces on the grid - self.video_catalogue_grid_rearrange() - # Gridboxes could be made (un)expandable, depending on the - # number of gridboxes now on the grid, and the number of - # columns allowed in the grid - self.video_catalogue_grid_check_expand() - - - def video_catalogue_unselect_all(self): - - """Can be called by anything. - - Standard de-selection of all videos in the Video Catalogue (i.e. all - mainwin.SimpleCatalogueItem objects, or all - mainwin.ComplexCatalogueItem, or all - mainwin.GridCatalogueItem objects). - """ - - if self.app_obj.catalogue_mode_type != 'grid': - - self.catalogue_listbox.unselect_all() - - else: - - for this_catalogue_obj in self.video_catalogue_dict.values(): - this_catalogue_obj.do_select(False) - - - def video_catalogue_force_resort(self): - - """Called by mainapp.TartubeApp.on_button_resort_catalogue(). - - In case of incorrect sorting in the Video Catalogue, the user can click - the button to force a re-sort. - - Children of the visible media.Channel, media.Playlist or media.Folder - are resorted, then the Video Catalogue is redrawn. - """ - - if self.video_index_current_dbid is None: - return - - else: - # Redraw the Video Catalogue, switching to the first page - # (The container's children are automatically sorted during the - # call to that function) - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def video_catalogue_grid_set_gridbox_width(self, width): - - """Called by CatalogueGridBox.on_size_allocate(). - - Used only when the Video Catalogue is displaying videos on a grid. - Each grid location contains a single gridbox (mainwin.CatalogueGridBox) - handling a single video. - - As we start to add gridboxes to the grid, the minimum required width - (comprising the space needed for all widgets) is not immediately - available. Therefore, initially gridboxes are not allowed to expand - horizontally, filling all the available space. - - As soon as the width of one of the new gridboxes becomes available, - its callback calls this function. - - We set the minimum required width for the current thumbnail size - (specified by mainapp.TartubeApp.thumb_size_custom) and set a flag to - check the size of the grid, given that minimum width (because it might - be possible to put more or fewer gridboxes in each row). - - We also hide each gridbox's frame, if it should be hidden. (In order to - obtain the correct minimum width, the frame it is always visible at - first.) - - Args: - - width (int): The minimum required width for a gridbox, in pixels - - """ - - # Sanity check: Once the minimum gridbox width for each thumbnail size - # has been established, don't change it - thumb_size = self.app_obj.thumb_size_custom - if self.catalogue_grid_width_dict[thumb_size] is not None: - return self.app_obj.system_error( - 217, - 'Redundant setting of minimum gridbox width', - ) - - # Further sanity check: nothing to do if videos aren't arranged on a - # grid - if self.app_obj.catalogue_mode_type == 'grid': - - self.catalogue_grid_width_dict[thumb_size] = width - - # All gridboxes can now be drawn without a frame, if required - for catalogue_obj in self.video_catalogue_dict.values(): - - catalogue_obj.catalogue_gridbox.enable_visible_frame( - self.app_obj.catalogue_draw_frame_flag, - ) - - # Gtk is busy, so the horizontal size of the grid cannot be checked - # immediately. Set a flag to let Tartube's fast timer do that - self.catalogue_grid_rearrange_flag = True - - - def video_catalogue_grid_check_size(self): - - """Called by self.on_video_catalogue_thumb_combo_changed(), - self.on_window_size_allocate() and .on_paned_size_allocate(). - - Also called by mainapp.TartubeApp.script_fast_timer_callback(), after a - recent call to self.video_catalogue_grid_set_gridbox_width(). - - Used only when the Video Catalogue is displaying videos on a grid. - Each grid location contains a single gridbox (mainwin.CatalogueGridBox) - handling a single video. - - Check the available size of the grid. Given the minimum required width - for a gridbox, increase or decrease the number of columns in the grid, - if necessary. - """ - - # When working out the grid's actual width, take into account small - # gaps between each gridbox, and around the borders of other widgets - thumb_size = self.app_obj.thumb_size_custom - grid_width = self.win_last_width - self.videos_paned.get_position() \ - - (self.spacing_size * (self.catalogue_grid_column_count + 7)) - - if self.catalogue_grid_width_dict[thumb_size] is None: - column_count = 1 - - else: - - gridbox_width = self.catalogue_grid_width_dict[thumb_size] - column_count = int(grid_width / gridbox_width) - if column_count < 1: - column_count = 1 - - # (The flag is True only when called from - # mainapp.TartubeApp.script_fast_timer_callback(), in which case we - # need to rearrrange the grid, even if the column count hasn't - # changed) - if self.catalogue_grid_column_count != column_count \ - or self.catalogue_grid_rearrange_flag: - - self.catalogue_grid_rearrange_flag = False - - # Change the number of columns to fit more (of fewer) videos on - # each row - self.catalogue_grid_column_count = column_count - - # Gridboxes could be made (un)expandable, depending on the number - # of gridboxes now on the grid - self.video_catalogue_grid_check_expand() - - # Move video gridboxes to their new positions on the grid - # (Any gridboxes which are not expandable, will not appear expanded - # unless this function is called) - self.video_catalogue_grid_rearrange() - - # After maximising the window, Gtk refuses to do a redraw, meaning - # that the user sees a bigger window, but not a change in the - # number of columns - # Only solution I can find is to adjust the size of the paned - # temporarily - if column_count > 1: - posn = self.videos_paned.get_position() - self.videos_paned.set_position(posn + 1) - self.videos_paned.set_position(posn - 1) - - - def video_catalogue_grid_check_expand(self): - - """Called by self.video_catalogue_grid_check_size(), - .video_catalogue_redraw_all(), .video_catalogue_insert_video(), - .video_catalogue_delete_video(). - - Used only when the Video Catalogue is displaying videos on a grid. - Each grid location contains a single gridbox (mainwin.CatalogueGridBox) - handling a single video. - - For aesthetic reasons, gridboxes should expand to fill the available - space, or not. - - This function checks whether expansion should occur. If there has been - a change of state, then every gridbox is called to update it (either - enabling or disabling its horizontal expansion flag). - """ - - thumb_size = self.app_obj.thumb_size_custom - - # (Gridboxes never expand to fill the available space, if the minimum - # required width for a gridbox is not yet known) - if self.catalogue_grid_width_dict[thumb_size] is not None: - - toggle_flag = False - count = len(self.video_catalogue_dict) - - if ( - count < self.catalogue_grid_column_count - and self.catalogue_grid_expand_flag - ): - self.catalogue_grid_expand_flag = False - toggle_flag = True - - elif ( - count >= self.catalogue_grid_column_count - and not self.catalogue_grid_expand_flag - ): - self.catalogue_grid_expand_flag = True - toggle_flag = True - - if toggle_flag: - - # Change of state; update every gridbox - for catalogue_obj in self.video_catalogue_dict.values(): - catalogue_obj.catalogue_gridbox.set_expandable( - self.catalogue_grid_expand_flag, - ) - - - def video_catalogue_grid_attach_gridbox(self, wrapper_obj, x_pos, y_pos): - - """Called by self.video_catalogue_redraw_all(), - .video_catalogue_insert_video() and .video_catalogue_grid_rearrange(). - - Adds the specified CatalogueGridBox to the Video Catalogue's grid at - the specified coordinates, and updates IVs. - - Args: - - wrapper_obj (mainwin.CatalogueGridBox): The gridbox to be added to - the grid - - x_pos, y_pos (int): The coordinates at which to add it - - """ - - self.catalogue_grid.attach( - wrapper_obj, - x_pos, - y_pos, - 1, - 1, - ) - - # Update IVs - if self.catalogue_grid_row_count < (y_pos + 1): - self.catalogue_grid_row_count = y_pos + 1 - - wrapper_obj.set_posn(x_pos, y_pos) - - - def video_catalogue_grid_rearrange(self): - - """Called by self.video_catalogue_grid_check_size(), - .video_catalogue_update_video() and .video_catalogue_delete_video(). - - Used only when the Video Catalogue is displaying videos on a grid. - Each grid location contains a single gridbox (mainwin.CatalogueGridBox) - handling a single video. - - Removes every gridbox from the grid. Sorts the gridboxes, and then - puts them back onto the grid, using the number of columns specified by - self.catalogue_grid_column_count (which may have changed recently), and - filling any gaps (if a gridboxes have been removed from the grid). - """ - - # Each mainwin.CatalogueGridBox acts as a wrapper for a Gtk.Frame - wrapper_list = [] - - # Remove every gridbox from the grid - for wrapper_obj in self.catalogue_grid.get_children(): - self.catalogue_grid.remove(wrapper_obj) - wrapper_list.append(wrapper_obj) - - # (This IV's minimum value is 1, even when the grid is empty) - self.catalogue_grid_row_count = 1 - - # Sort the gridboxes, as if we were sorting the media.Video objects - # directly - wrapper_list.sort( - key=functools.cmp_to_key(self.video_catalogue_grid_auto_sort), - ) - - # Place gridboxes back on the grid, taking into account that the number - # of columns may have changed recently - x_pos = 0 - y_pos = 0 - for wrapper_obj in wrapper_list: - - self.video_catalogue_grid_attach_gridbox( - wrapper_obj, - x_pos, - y_pos, - ) - - x_pos += 1 - if x_pos >= self.catalogue_grid_column_count: - x_pos = 0 - y_pos += 1 - - - def video_catalogue_grid_reset_sizes(self): - - """Called by self.__init__() and - mainapp.TartubeApp.on_button_switch_view(). - - When the Video Catalogue displays videos on a grid, each grid location - contains a single gridbox (mainwin.CatalogueGridBox) handling a single - video. - - In that case, we need to know the minimum required space for a gridbox. - After changing mainapp.TartubeApp.catalogue_mode (i.e., after switching - between one of the several viewing modes), reset those minimum sizes, - so they can be calculated afresh the next time they are needed. - """ - - self.catalogue_grid_width_dict = {} - - for key in self.app_obj.thumb_size_dict: - self.catalogue_grid_width_dict[key] = None - - - def video_catalogue_grid_select(self, catalogue_obj, select_type, - rev_flag=False): - - """Can be called by anything. - - Used only when the Video Catalogue is displaying videos on a grid. - Each grid location contains a single gridbox (mainwin.CatalogueGridBox) - handling a single video. - - Widgets on a Gtk.Grid can't be selected just by clicking them (as we - might select a row in a Gtk.TreeView or Gtk.ListBox, just by clicking - it), so Tartube includes custom code to allow gridboxes to be selected - and unselected. - - Args: - - catalogue_obj (mainwin.GridCatalogueItem): The gridbox that was - clicked - - select_type (str): 'shift' if the SHIFT key is held down at the - moment of the click, 'ctrl' if the CTRL button is held down, - and 'default' if neither of those keys are held down - - rev_flag (bool): True when using the 'up' cursor key, or the page - up key; False for all other detectable keys - - """ - - # (Sorting function for the code immediately below) - def sort_by_grid_posn(obj1, obj2): - - if obj1.y_pos < obj2.y_pos: - return -1 - elif obj1.y_pos > obj2.y_pos: - return 1 - elif obj1.x_pos < obj2.x_pos: - return -1 - else: - return 1 - - # Select/deselect gridboxes - if select_type == 'shift': - - # The user is holding down the SHIFT key. Select one or more - # gridboxes - - # Get a list of gridboxes, and sort them by position on the - # Gtk.Grid (top left to bottom right) - gridbox_list = self.catalogue_grid.get_children() - gridbox_list.sort(key=functools.cmp_to_key(sort_by_grid_posn)) - if rev_flag: - gridbox_list.reverse() - - # Find the position of the first and last selected gridbox, and the - # position of the gridbox that handles the specified - # catalogue_obj - count = -1 - first_posn = None - last_posn = None - specified_posn = None - - for gridbox_obj in gridbox_list: - - count += 1 - this_catalogue_obj \ - = self.video_catalogue_dict[gridbox_obj.video_obj.dbid] - - if this_catalogue_obj.selected_flag: - - last_posn = count - if first_posn is None: - first_posn = count - - if gridbox_obj.video_obj == catalogue_obj.video_obj: - specified_posn = count - - # Sanity check - if specified_posn is None: - - return self.app_obj.system_error( - 218, - 'Gridbox not found in Video Catalogue', - ) - - # Select/deselect videos, as required - if not first_posn: - - start_posn = specified_posn - stop_posn = specified_posn - - elif not catalogue_obj.selected_flag: - - if specified_posn < first_posn: - - start_posn = specified_posn - stop_posn = first_posn - - elif specified_posn == first_posn: - - start_posn = specified_posn - stop_posn = specified_posn - - else: - - start_posn = first_posn - stop_posn = specified_posn - - else: - - if specified_posn < first_posn: - - start_posn = specified_posn - stop_posn = last_posn - - else: - - start_posn = first_posn - stop_posn = specified_posn - - count = -1 - for gridbox_obj in gridbox_list: - - count += 1 - this_catalogue_obj \ - = self.video_catalogue_dict[gridbox_obj.video_obj.dbid] - - if count >= start_posn and count <= stop_posn: - this_catalogue_obj.do_select(True) - else: - this_catalogue_obj.do_select(False) - - elif select_type == 'ctrl': - - # The user is holding down the CTRL key. Select this gridbox, in - # addition to any gridboxes that are already selected - catalogue_obj.toggle_select() - - else: - - # The user is holding down neither the SHIFT not CTRL keys. Select - # this gridbox, and unselect all other gridboxes - for this_catalogue_obj in self.video_catalogue_dict.values(): - - if this_catalogue_obj == catalogue_obj: - this_catalogue_obj.do_select(True) - else: - this_catalogue_obj.do_select(False) - - - def video_catalogue_grid_select_all(self): - - """Called by CatalogueGridBox.on_key_press_event(). - - When the user presses CTRL+A, select all gridboxes in the grid. - """ - - for catalogue_item_obj in self.video_catalogue_dict.values(): - catalogue_item_obj.do_select(True) - - - def video_catalogue_grid_scroll_on_select(self, gridbox_obj, keyval, - select_type): - - """Called by CatalogueGridBox.on_key_press_event(). - - Custom code to do scrolling, and to update the selection, in the - Video Catalogue grid when the user presses the cursor and page up/down - keys. - - Args: - - gridbox_obj (mainwin.CatalogueGridBox): The gridbox that - intercepted the keypress - - keyval (str): One of the keys in self.catalogue_grid_intercept_dict - (e.g. 'Up', 'Left', 'Page_Up') - - select_type (str): 'shift' if the SHIFT key is held down at the - moment of the click, 'ctrl' if the CTRL button is held down, - and 'default' if neither of those keys are held down - - """ - - # Get the GridCatalogueItem of the gridbox which intercepted the - # keypress - if not gridbox_obj.video_obj.dbid in self.video_catalogue_dict: - return - - catalogue_item_obj \ - = self.video_catalogue_dict[gridbox_obj.video_obj.dbid] - - # For cursor keys, find the grid location immediately beside this one - x_pos = gridbox_obj.x_pos - y_pos = gridbox_obj.y_pos - - # (Adjust values describing the size of the grid, to make the code a - # little simpler) - width = self.catalogue_grid_column_count - 1 - height = self.catalogue_grid_row_count - 1 - - # (With the SHIFT key, multiple successive up/page up keypresses won't - # have the intended effect. Tell self.video_catalogue_grid_select() - # to select individual gridboxes from bottom to top, which fixes the - # problem) - if keyval == 'Up' or keyval == 'Page_Up': - rev_flag = True - else: - rev_flag = False - - # Now interpret the keypress - if keyval == 'Page_Up' or keyval == 'Page_Down': - - # For page up/down keys, things are a bit trickier. Moving the - # scrollbars would be simple enough, but then we wouldn't know - # which of the visible gridboxes to select - # Instead, get the height of the parent scrollbar, then test the - # height of the old selected gridbox, and gridboxes above/below - # it, so we can decide which gridbox to select - - # Get the size of the parent scroller - rect = self.catalogue_scrolled.get_allocation() - scroller_height = rect.height - if scroller_height <= 1: - # Allocation not known yet (very unlikely after a selection) - return - - # Chop away at that height, starting with the height of the current - # gridbox - this_obj = gridbox_obj - while True: - - this_rect = this_obj.get_allocation() - this_height = this_rect.height - if this_height <= 1: - return - - scroller_height -= this_height - if scroller_height < 0: - break - - else: - - # On the next loop, check the gridbox above/below this one - if keyval == 'Page_Up': - - y_pos -= 1 - if y_pos < 0: - y_pos = 0 - - else: - - y_pos += 1 - if y_pos > height: - y_pos = height - - check_obj = self.catalogue_grid.get_child_at(x_pos, y_pos) - if check_obj: - this_obj = check_obj - - else: - # Either we have scrolled 'below' the bottom row, or we - # are at the bottom row, which is not full - # Select a video in the bottom row, as close to column - # y_pos as possible) - for this_x in range(x_pos, -1, -1): - - if self.catalogue_grid.get_child_at(this_x, y_pos): - x_pos = this_x - break - - if this_obj != gridbox_obj: - - self.video_catalogue_grid_select( - self.video_catalogue_dict[this_obj.video_obj.dbid], - select_type, - rev_flag, - ) - - else: - - if keyval == 'Left': - - x_pos -= 1 - if x_pos < 0: - y_pos -= 1 - if y_pos < 0: - # (Already in the top left corner) - return - else: - x_pos = width - - elif keyval == 'Right': - - x_pos += 1 - if x_pos > width: - y_pos += 1 - if y_pos > height: - # (Already in the bottom right corner) - return - else: - x_pos = 0 - - elif keyval == 'Up': - - y_pos -= 1 - if y_pos < 0: - # (Already at the top) - return - - elif keyval == 'Down': - - y_pos += 1 - grid_count = self.catalogue_grid_column_count \ - * self.catalogue_grid_row_count - video_count = len(self.video_catalogue_dict.keys()) - - if y_pos > height: - - # (The bottom row is full, and we are already at the - # bottom) - return - - elif y_pos == height and grid_count > video_count: - - # (One row above the bottom one, which is not full. Select - # a video in the bottom row, as close to column y_pos as - # possible) - for this_x in range(x_pos, -1, -1): - - if self.catalogue_grid.get_child_at(this_x, y_pos): - x_pos = this_x - break - - # Fetch the gridbox at the new location - new_gridbox_obj = self.catalogue_grid.get_child_at(x_pos, y_pos) - if not new_gridbox_obj: - return - - else: - - self.video_catalogue_grid_select( - self.video_catalogue_dict[new_gridbox_obj.video_obj.dbid], - select_type, - rev_flag, - ) - - - def video_catalogue_toolbar_reset(self): - - """Called by self.video_catalogue_redraw_all(). - - Just before completely redrawing the Video Catalogue, temporarily reset - widgets in the Video Catalogue toolbar (in case something goes wrong, - or in case drawing the page takes a long time). - """ - - self.catalogue_toolbar_current_page = 1 - self.catalogue_toolbar_last_page = 1 - - self.catalogue_page_entry.set_sensitive(True) - self.catalogue_page_entry.set_text( - str(self.catalogue_toolbar_current_page), - ) - - self.catalogue_last_entry.set_sensitive(True) - self.catalogue_last_entry.set_text( - str(self.catalogue_toolbar_last_page), - ) - - self.catalogue_first_button.set_sensitive(False) - self.catalogue_back_button.set_sensitive(False) - self.catalogue_forwards_button.set_sensitive(False) - self.catalogue_last_button.set_sensitive(False) - - self.catalogue_show_filter_button.set_sensitive(False) - - self.catalogue_reverse_toolbutton.set_sensitive(False) - self.catalogue_sort_combo.set_sensitive(False) - self.catalogue_resort_button.set_sensitive(False) - self.catalogue_thumb_combo.set_sensitive(False) - self.catalogue_frame_button.set_sensitive(False) - self.catalogue_icons_button.set_sensitive(False) - self.catalogue_downloaded_button.set_sensitive(False) - self.catalogue_undownloaded_button.set_sensitive(False) - self.catalogue_blocked_button.set_sensitive(False) - self.catalogue_filter_entry.set_sensitive(False) - self.catalogue_regex_togglebutton.set_sensitive(False) - self.catalogue_apply_filter_button.set_sensitive(False) - self.catalogue_cancel_filter_button.set_sensitive(False) - self.catalogue_find_date_button.set_sensitive(False) - - - def video_catalogue_toolbar_update(self, page_num, video_count): - - """Called by self.video_catalogue_redraw_all(), - self.video_catalogue_update_video() and - self.video_catalogue_delete_video(). - - After the Video Catalogue is redrawn or updated, update widgets in the - Video Catalogue toolbar. - - Args: - - page_num (int): The page number to draw (a value in the range 1 to - self.catalogue_toolbar_last_page) - - video_count (int): The number of videos that are children of the - selected channel, playlist or folder (may be 0) - - """ - - self.catalogue_toolbar_current_page = page_num - - # If the page size is 0, then all videos are drawn on one page - if not self.app_obj.catalogue_page_size: - self.catalogue_toolbar_last_page = page_num - else: - self.catalogue_toolbar_last_page \ - = int((video_count - 1) / self.app_obj.catalogue_page_size) + 1 - - self.catalogue_page_entry.set_sensitive(True) - self.catalogue_page_entry.set_text( - str(self.catalogue_toolbar_current_page), - ) - - self.catalogue_last_entry.set_sensitive(True) - self.catalogue_last_entry.set_text( - str(self.catalogue_toolbar_last_page), - ) - - if page_num == 1: - self.catalogue_first_button.set_sensitive(False) - self.catalogue_back_button.set_sensitive(False) - else: - self.catalogue_first_button.set_sensitive(True) - self.catalogue_back_button.set_sensitive(True) - - if page_num == self.catalogue_toolbar_last_page: - self.catalogue_forwards_button.set_sensitive(False) - self.catalogue_last_button.set_sensitive(False) - else: - self.catalogue_forwards_button.set_sensitive(True) - self.catalogue_last_button.set_sensitive(True) - - self.catalogue_show_filter_button.set_sensitive(True) - - if self.video_catalogue_filtered_flag: - self.catalogue_downloaded_button.set_sensitive(False) - self.catalogue_undownloaded_button.set_sensitive(False) - self.catalogue_blocked_button.set_sensitive(False) - else: - self.catalogue_downloaded_button.set_sensitive(True) - self.catalogue_undownloaded_button.set_sensitive(True) - self.catalogue_blocked_button.set_sensitive(True) - - # These widgets are sensitised when the filter is applied even if - # there are no matching videos - # (If not, the user would not be able to click the 'Cancel filter' - # button) - if not video_count and not self.video_catalogue_filtered_flag: - self.catalogue_reverse_toolbutton.set_sensitive(False) - self.catalogue_sort_combo.set_sensitive(False) - self.catalogue_resort_button.set_sensitive(False) - self.catalogue_thumb_combo.set_sensitive(False) - self.catalogue_frame_button.set_sensitive(False) - self.catalogue_icons_button.set_sensitive(False) - self.catalogue_filter_entry.set_sensitive(False) - self.catalogue_regex_togglebutton.set_sensitive(False) - self.catalogue_apply_filter_button.set_sensitive(False) - self.catalogue_cancel_filter_button.set_sensitive(False) - self.catalogue_find_date_button.set_sensitive(False) - self.catalogue_cancel_date_button.set_sensitive(False) - else: - self.catalogue_reverse_toolbutton.set_sensitive(True) - self.catalogue_sort_combo.set_sensitive(True) - self.catalogue_resort_button.set_sensitive(True) - - if self.app_obj.catalogue_mode_type != 'grid': - self.catalogue_thumb_combo.set_sensitive(False) - else: - self.catalogue_thumb_combo.set_sensitive(True) - - self.catalogue_frame_button.set_sensitive(True) - self.catalogue_icons_button.set_sensitive(True) - - self.catalogue_filter_entry.set_sensitive(True) - self.catalogue_regex_togglebutton.set_sensitive(True) - if self.video_catalogue_filtered_flag: - self.catalogue_apply_filter_button.set_sensitive(False) - self.catalogue_cancel_filter_button.set_sensitive(True) - else: - self.catalogue_apply_filter_button.set_sensitive(True) - self.catalogue_cancel_filter_button.set_sensitive(False) - self.catalogue_find_date_button.set_sensitive(True) - self.catalogue_cancel_date_button.set_sensitive(False) - - - def video_catalogue_apply_filter(self): - - """Called by mainapp.TartubeApp.on_button_apply_filter(). - - Applies a filter, so that all videos not matching the search text are - hidden in the Video Catalogue. - - Note that when a filter is applied, all matching videos are visible, - regardless of the value of - mainapp.TartubeApp.catalogue_draw_downloaded_flag, - .catalogue_draw_undownloaded_flag and .catalogue_draw_blocked_flag. - """ - - # Sanity check - something must be selected in the Video Index - parent_obj = None - if self.video_index_current_dbid is not None: - parent_obj \ - = self.app_obj.media_reg_dict[self.video_index_current_dbid] - - if not parent_obj or (isinstance(parent_obj, media.Video)): - return self.app_obj.system_error( - 219, - 'Tried to apply filter, but no channel/playlist/folder' \ - + ' selected in the Video Index', - ) - - # Get the search text from the entry box - search_text = self.catalogue_filter_entry.get_text() - if search_text is None or search_text == '': - # Applying an empty filter is the same as clicking the cancel - # filter button - return self.video_catalogue_cancel_filter() - - # Get a list of media.Video objects which are children of the - # currently selected channel, playlist or folder - # Then filter out every video whose name, description and/or comments - # don't match the filter text - # If filtering by name, filter out any videos that don't have an - # individual name set - video_list = [] - regex_flag = self.app_obj.catalogue_use_regex_flag - lower_text = search_text.lower() - - for child_obj in parent_obj.child_list: - - if isinstance(child_obj, media.Video): - - if ( - self.app_obj.catalogue_filter_name_flag \ - and child_obj.name != self.app_obj.default_video_name \ - and ( - ( - not regex_flag \ - and child_obj.name.lower().find(lower_text) > -1 - ) or ( - regex_flag \ - and re.search( - search_text, - child_obj.name, - re.IGNORECASE, - ) - ) - ) - ) or ( - self.app_obj.catalogue_filter_descrip_flag \ - and ( - not regex_flag \ - and child_obj.descrip.lower().find(lower_text) > -1 - ) or ( - regex_flag \ - and re.search( - search_text, - child_obj.name, - re.IGNORECASE, - ) - ) - ) or ( - self.app_obj.catalogue_filter_comment_flag \ - and child_obj.contains_comment(search_text, regex_flag) - ): - video_list.append(child_obj) - - # Set IVs... - self.video_catalogue_filtered_flag = True - self.video_catalogue_filtered_list = video_list.copy() - # ...and redraw the Video Catalogue - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - 1, # Display the first page - True, # Reset scrollbars - True, # Do not cancel the filter we've just applied - ) - - # Sensitise widgets, as appropriate - self.catalogue_apply_filter_button.set_sensitive(False) - self.catalogue_cancel_filter_button.set_sensitive(True) - # (Desensitise these widgets, to make it clear to the user that the - # settings don't apply when the filter is applied) - self.catalogue_downloaded_button.set_sensitive(False) - self.catalogue_undownloaded_button.set_sensitive(False) - self.catalogue_blocked_button.set_sensitive(False) - - - def video_catalogue_cancel_filter(self): - - """Called by mainapp.TartubeApp.on_button_cancel_filter() and - self.video_catalogue_apply_filter(). - - Cancels the filter, so that all videos which are children of the - currently selected channel/playlist/folder are shown in the Video - Catalogue. - """ - - # Reset IVs... - self.video_catalogue_filtered_flag = False - self.video_catalogue_filtered_list = [] - # ...and redraw the Video Catalogue - self.video_catalogue_redraw_all(self.video_index_current_dbid) - - # Sensitise widgets, as appropriate - self.catalogue_apply_filter_button.set_sensitive(True) - self.catalogue_cancel_filter_button.set_sensitive(False) - self.catalogue_downloaded_button.set_sensitive(True) - self.catalogue_undownloaded_button.set_sensitive(True) - self.catalogue_blocked_button.set_sensitive(True) - - - def video_catalogue_show_date(self, page_num): - - """Called by mainapp.TartubeApp.on_button_find_date(). - - Redraw the Video Catalogue to show the page containing the first video - uploaded on a specified date. - - (De)sensitise widgets, as appropriate. - - Args: - - page_num (int): The Video Catalogue page number to display (unlike - calls to self.video_catalogue_apply_filter(), no videos are - filtered out; we just show the first page containing videos - for the specified date) - - """ - - # Sanity check - something must be selected in the Video Index - parent_obj = None - if self.video_index_current_dbid is not None: - parent_obj \ - = self.app_obj.media_reg_dict[self.video_index_current_dbid] - - if not parent_obj or (isinstance(parent_obj, media.Video)): - return self.app_obj.system_error( - 220, - 'Tried to apply find videos by date, but no channel/' \ - + ' playlist/folder selected in the Video Index', - ) - - # Redraw the Video Catalogue - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - page_num, - True, # Reset scrollbars - True, # Do not cancel the filter, if one has been applied - ) - - # Sensitise widgets, as appropriate - self.catalogue_find_date_button.set_sensitive(False) - self.catalogue_cancel_date_button.set_sensitive(True) - - - def video_catalogue_unshow_date(self): - - """Called by mainapp.TartubeApp.on_button_find_date(). - - Having redrawn the Video Catalogue to show the page containing the - first video uploaded on a specified date, redraw it to show the first - page again. - - (De)sensitise widgets, as appropriate. - """ - - # Sanity check - something must be selected in the Video Index - parent_obj = None - if self.video_index_current_dbid is not None: - parent_obj \ - = self.app_obj.media_reg_dict[self.video_index_current_dbid] - - if not parent_obj or (isinstance(parent_obj, media.Video)): - return self.app_obj.system_error( - 221, - 'Tried to cancel find videos by date, but no channel/' \ - + ' playlist/folder selected in the Video Index', - ) - - # Redraw the Video Catalogue - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - 1, - True, # Reset scrollbars - True, # Do not cancel the filter, if one has been applied - ) - - # Sensitise widgets, as appropriate - self.catalogue_find_date_button.set_sensitive(True) - self.catalogue_cancel_date_button.set_sensitive(False) - - - # (Progress List) - - - def progress_list_reset(self): - - """Can be called by anything. - - Empties the Gtk.TreeView in the Progress List, ready for it to be - refilled. - - Also resets related IVs. - """ - - # Reset widgets - self.progress_list_liststore = Gtk.ListStore( - int, int, str, - GdkPixbuf.Pixbuf, - str, str, str, str, str, str, str, str, str, - ) - self.progress_list_treeview.set_model(self.progress_list_liststore) - - # Reset IVs - self.progress_list_row_dict = {} - self.progress_list_row_count = 0 - self.progress_list_temp_dict = {} - self.progress_list_finish_dict = {} - self.progress_list_broken_dict = {} - - - def progress_list_init(self, download_list_obj): - - """Called by mainapp.TartubeApp.download_manager_continue(). - - At the start of the download operation, a downloads.DownloadList - object is created, listing all the media data objects (channels, - playlists and videos) from which videos are to be downloaded. - - This function is then called to add each of those media data objects to - the Progress List. - - As the download operation progresses, - downloads.DownloadWorker.talk_to_mainwin() calls - self.progress_list_receive_dl_stats() to update the contents of the - Progress List. - - Args: - - download_list_obj (downloads.DownloadList): The download list - object that has just been created - - """ - - # For each download item object, add a row to the treeview, and store - # the download item's .dbid IV so that - # self.progress_list_receive_dl_stats() can update the correct row - for item_id in download_list_obj.download_item_list: - - download_item_obj = download_list_obj.download_item_dict[item_id] - - self.progress_list_add_row( - item_id, - download_item_obj.media_data_obj, - ) - - - def progress_list_add_row(self, item_id, media_data_obj): - - """Called by self.progress_list_init(), - mainapp.TartubeApp.download_watch_videos() and - downloads.VideoDownloader.convert_video_to_container(). - - Adds a row to the Progress List. - - Args: - - item_id (int): The downloads.DownloadItem.item_id - - media_data_obj (media.Video, media.Channel or media.Playlist): - The media data object for which a row should be added - - """ - - # Prepare the icon - if isinstance(media_data_obj, media.Channel): - pixbuf = self.pixbuf_dict['channel_small'] - elif isinstance(media_data_obj, media.Playlist): - pixbuf = self.pixbuf_dict['playlist_small'] - elif isinstance(media_data_obj, media.Folder): - pixbuf = self.pixbuf_dict['folder_small'] - elif media_data_obj.live_mode == 2: - pixbuf = self.pixbuf_dict['live_now_small'] - elif media_data_obj.live_mode == 1: - pixbuf = self.pixbuf_dict['live_wait_small'] - else: - pixbuf = self.pixbuf_dict['video_small'] - - # Prepare the new row in the treeview - row_list = [] - - row_list.append(item_id) # Hidden - row_list.append(media_data_obj.dbid) # Hidden - row_list.append( # Hidden - html.escape( - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - True, # Show errors/warnings - ), - ), - ) - row_list.append(pixbuf) - row_list.append(media_data_obj.name) - row_list.append(None) - row_list.append(_('Waiting')) - row_list.append(None) - row_list.append(None) - row_list.append(None) - row_list.append(None) - row_list.append(None) - row_list.append(None) - - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.progress_list_treeview.show_all() - self.progress_list_liststore.append(row_list) - - # Store the row's details so we can update it later - self.progress_list_row_dict[item_id] \ - = self.progress_list_row_count - self.progress_list_row_count += 1 - - - def progress_list_receive_dl_stats(self, download_item_obj, dl_stat_dict, - finish_flag=False): - - """Called by downloads.DownloadWorker.data_callback(). - - During a download operation, this function is called every time - youtube-dl writes some output to STDOUT. - - Updating data displayed in the Progress List several times a second, - and irregularly, doesn't look very nice. Instead, we only update the - displayed data at fixed intervals. - - Thus, when this function is called, it is passed a dictionary of - download statistics in a standard format (the one described in the - comments to downloads.VideoDownloader.extract_stdout_data() ). - - We store that dictionary temporarily. During periodic calls to - self.progress_list_display_dl_stats(), the contents of any stored - dictionaries are displayed and then the dictionaries themselves are - destroyed. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object handling a download for a media data object - - dl_stat_dict (dict): The dictionary of download statistics - described above - - finish_flag (bool): True if the worker has finished with its - media data object, meaning that dl_stat_dict is the final set - of statistics, and that the progress list row can be hidden, - if required - - """ - - # Check that the Progress List actually has a row for the specified - # downloads.DownloadItem object - if not download_item_obj.item_id in self.progress_list_row_dict: - - # !!! DEBUG Git #479 - # Because of an unresolved issue, only show this message once for - # each downloads.DownloadItem object - if not download_item_obj.item_id in self.progress_list_broken_dict: - - self.app_obj.system_error( - 222, - 'Missing row in Progress List', - ) - - self.progress_list_broken_dict[download_item_obj.item_id] \ - = False - - return - - # Temporarily store the dictionary of download statistics - if not download_item_obj.item_id in self.progress_list_temp_dict: - new_dl_stat_dict = {} - else: - new_dl_stat_dict \ - = self.progress_list_temp_dict[download_item_obj.item_id] - - for key in dl_stat_dict: - new_dl_stat_dict[key] = dl_stat_dict[key] - - self.progress_list_temp_dict[download_item_obj.item_id] \ - = new_dl_stat_dict - - # If it's the final set of download statistics, set the time at which - # the row can be hidden (if required) - if finish_flag: - self.progress_list_finish_dict[download_item_obj.item_id] \ - = time.time() + self.progress_list_hide_time - - - def progress_list_display_dl_stats(self): - - """Called by downloads.DownloadManager.run() and - mainapp.TartubeApp.dl_timer_callback(). - - As the download operation progresses, youtube-dl writes statistics to - its STDOUT. Those statistics have been interpreted and stored in - self.progress_list_temp_dict, waiting for periodic calls to this - function to display them. - """ - - # Import some objects from downloads.py (for convenience) - dl_obj = self.app_obj.download_manager_obj - # Import the contents of the IV (in case it gets updated during the - # call to this function), and use the imported copy - temp_dict = self.progress_list_temp_dict - self.progress_list_temp_dict = {} - - # For each media data object displayed in the Progress List... - for item_id in temp_dict: - - # Get a dictionary of download statistics for this media data - # object - # The dictionary is in the standard format described in the - # comments to downloads.VideoDownloader.extract_stdout_data() - dl_stat_dict = temp_dict[item_id] - - # Get the corresponding treeview row - tree_path = Gtk.TreePath(self.progress_list_row_dict[item_id]) - - # Get the media data object - # Git 34 reports that the .get_iter() call causes a crash, when - # finished rows are being hidden. This may be a Gtk issue, so - # intercept the error directly - try: - tree_iter = self.progress_list_liststore.get_iter(tree_path) - dbid = self.progress_list_liststore[tree_iter][1] - media_data_obj = self.app_obj.media_reg_dict[dbid] - - except: - # Don't try to update hidden rows - return - - # Any downloads.DownloadItem objects not in their - # ACTIVE_STAGE_DOWNLOAD stage can be removed from the rolling - # average data immediately - if 'status' in dl_stat_dict \ - and dl_stat_dict['status'] != '' \ - and dl_stat_dict['status'] != formats.ACTIVE_STAGE_DOWNLOAD \ - and item_id in self.progress_list_average_speed_dict: - del self.progress_list_average_speed_dict[item_id] - - # Otherwise, store the instantaneous download speed, so a rolling - # average can be calculated - elif 'speed' in dl_stat_dict and dl_stat_dict['speed'] != '': - - if item_id in self.progress_list_average_speed_dict: - self.progress_list_average_speed_dict[item_id].append([ - utils.convert_string_to_bytes( - dl_stat_dict['speed'], - ), - int(time.time()), - ]) - - else: - self.progress_list_average_speed_dict[item_id] = [[ - utils.convert_string_to_bytes( - dl_stat_dict['speed'], - ), - int(time.time()), - ]] - - # When the download concludes, instead of overwriting the filename, - # show the video's name - if 'filename' in dl_stat_dict \ - and dl_stat_dict['filename'] == '' \ - and isinstance(media_data_obj, media.Video) \ - and media_data_obj.file_name is not None: - dl_stat_dict['filename'] = media_data_obj.file_name - - # Update the tooltip - try: - tree_iter = self.progress_list_liststore.get_iter(tree_path) - self.progress_list_liststore.set( - tree_iter, - self.progress_list_tooltip_column, - html.escape( - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - True, # Show errors/warnings - ), - ), - ) - - except: - return - - # Update statistics displayed in this row - # (Columns 0, 1 and 3 are not modified, once the row has been added - # to the treeview) - column = 4 - - for key in ( - 'playlist_index', - 'status', - 'filename', - 'extension', - 'percent', - 'speed', - 'eta', - 'filesize', - ): - column += 1 - - if key in dl_stat_dict: - - if key == 'playlist_index': - - if dl_stat_dict['playlist_index'] == 0: - msg = '' - - elif 'dl_sim_flag' in dl_stat_dict \ - and dl_stat_dict['dl_sim_flag']: - # (Don't know how many videos there are in a - # channel/playlist, so ignore value of - # 'playlist_size') - msg = str(dl_stat_dict['playlist_index']) - - else: - msg = str(dl_stat_dict['playlist_index']) - if 'playlist_size' in dl_stat_dict \ - and dl_stat_dict['playlist_size'] > 0: - msg = msg + '/' \ - + str(dl_stat_dict['playlist_size']) - else: - msg = msg + '/1' - - else: - msg = dl_stat_dict[key] - - try: - tree_iter = self.progress_list_liststore.get_iter( - tree_path - ) - - self.progress_list_liststore.set( - tree_iter, - column, - msg, - ) - - except: - return - - # Display ongoing statistics, including the rolling average d/l speed - ignore_me = _( - 'TRANSLATOR\'S NOTE: D/L = download' - ) - - if not dl_obj: - msg = '' - else: - msg = '' + _('Check') + ': ' \ - + str(dl_obj.total_sim_count) \ - + ' ' + _('D/L') + ': ' \ - + str(dl_obj.total_dl_count) - - if dl_obj.total_clip_count or dl_obj.total_slice_count: - msg = msg + ' ' + _('Other') + ': ' \ - + str(dl_obj.total_clip_count + dl_obj.total_slice_count) - - msg = msg + ' ' + _('Size') + ': ' \ - + str(utils.convert_bytes_to_string(dl_obj.total_size_count)) \ - + ' ' + _('Speed') + ': ' \ - + str(utils.convert_bytes_to_string( - self.progress_list_get_rolling_average(), - ) + '/s') - - self.progress_update_label.set_markup(msg) - - - def progress_list_check_hide_rows(self, force_flag=False): - - """Called by mainapp.TartubeApp.download_manager_finished, - .dl_timer_callback() and .set_progress_list_hide_flag(). - - Called only when mainapp.TartubeApp.progress_list_hide_flag is True. - - Any rows in the Progress List which are finished are stored in - self.progress_list_finish_dict. When a row is finished, it is given a - time (three seconds afterwards, by default) at which the row can be - deleted. - - Check each row, and if it's time to delete it, do so. - - Args: - - force_flag (bool): Set to True if all finished rows should be - hidden immediately, rather than waiting for the (by default) - three seconds - - """ - - current_time = time.time() - hide_list = [] - - for item_id in self.progress_list_finish_dict.keys(): - finish_time = self.progress_list_finish_dict[item_id] - - if force_flag or current_time > finish_time: - hide_list.append(item_id); - - # Now we've finished walking the dictionary, we can hide rows - for item_id in hide_list: - self.progress_list_do_hide_row(item_id) - - - def progress_list_do_hide_row(self, item_id): - - """Called by self.progress_list_check_hide_rows(). - - If it's time to delete a row in the Progress List, delete the row and - update IVs. - - Args: - - item_id (int): The downloads.DownloadItem.item_id that was - displaying statistics in the row to be deleted - - """ - - row_num = self.progress_list_row_dict[item_id] - - # Remove the row. Very rarely this generates a Python error (for - # unknown reasons) - try: - - path = Gtk.TreePath(row_num) - tree_iter = self.progress_list_liststore.get_iter(path) - self.progress_list_liststore.remove(tree_iter) - - # Prepare new values for Progress List IVs. Everything after this - # row must have its row number decremented by one - row_dict = {} - for this_item_id in self.progress_list_row_dict.keys(): - this_row_num = self.progress_list_row_dict[this_item_id] - - if this_row_num > row_num: - row_dict[this_item_id] = this_row_num - 1 - elif this_row_num < row_num: - row_dict[this_item_id] = this_row_num - - row_count = self.progress_list_row_count - 1 - - - # Apply updated IVs - self.progress_list_row_dict = row_dict.copy() - if item_id in self.progress_list_temp_dict: - del self.progress_list_temp_dict[item_id] - if item_id in self.progress_list_finish_dict: - del self.progress_list_finish_dict[item_id] - - except: - - # !!! DEBUG Git #479 - # Because of an unresolved issue, only show this message once for - # each downloads.DownloadItem object - if not item_id in self.progress_list_broken_dict: - - self.app_obj.system_error( - 271, - 'Cannot remove row in Progress List (row does not exist)', - ) - - self.progress_list_broken_dict[item_id] = False - - - def progress_list_update_video_name(self, download_item_obj, video_obj): - - """Called by self.results_list_add_row(). - - In the Progress List, an individual video (one inside a media.Folder) - will be visible using the system's default video name, rather than the - video's actual name. The final call to - self.progress_list_display_dl_stats() cannot set the actual name, as it - might not be available yet. - - The Results List is updated some time after the last call to the - Progress List. If the video has a non-default name, then display it in - the Progress List now. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object handling a download for a media data object - - video_obj (media.Video): The media data object for the downloaded - video - - """ - - if download_item_obj.item_id in self.progress_list_row_dict \ - and download_item_obj.media_data_obj == video_obj: - - # Get the Progress List treeview row - tree_path = Gtk.TreePath( - self.progress_list_row_dict[download_item_obj.item_id], - ) - - self.progress_list_liststore.set( - self.progress_list_liststore.get_iter(tree_path), - 4, - video_obj.name, - ) - - - def progress_list_get_column_widths(self): - - """Called by mainapp.TartubeApp.save_config(). - - Fetches the width of the 'Source' and 'Incoming file' columns in the - Progress List. - - Return values: - - The two widths, or None if the list doesn't exist yet - - """ - - if self.progress_list_treeview is None: - return None, None - - else: - source_column = self.progress_list_treeview.get_column(4) - source_width = source_column.get_width() - # (Shouldn't be possible to reduce the width below the minimum, - # but we'll check anyway) - if source_width < self.min_column_width: - source_width = self.min_column_width - - incoming_column = self.progress_list_treeview.get_column(7) - incoming_width = incoming_column.get_width() - if incoming_width < self.min_column_width: - incoming_width = self.min_column_width - - return source_width, incoming_width - - - def progress_list_get_rolling_average(self): - - """Called by self.progress_list_display_dl_stats(). - - Calculates the rolling average download speed, and updates the - dictionary storing instantaneous download speed, removing older values. - - Return values: - - Returns the rolling average speed, in bytes - - """ - - # Import some objects from downloads.py (for convenience) - dl_list_obj = self.app_obj.download_manager_obj.download_list_obj - - # Update the data, and calculate a new rolling average - old_dict = self.progress_list_average_speed_dict - self.progress_list_average_speed_dict = {} - - current_time = time.time() - total_speed = 0 - - for item_id in old_dict: - new_list = [] - combined_speed = 0 - combined_values = 0 - - for mini_list in old_dict[item_id]: - - # List in the form [instantaneous d/l speed, time received] - if mini_list[1] \ - >= current_time - self.progress_list_average_speed_length: - new_list.append(mini_list) - combined_speed += mini_list[0] - combined_values += 1 - - self.progress_list_average_speed_dict[item_id] = new_list - # The total average speed is the sum of the average speed for - # each channel/playlist/video - if combined_values: - total_speed += (combined_speed / combined_values) - - return total_speed - - - def progress_list_reset_rolling_average(self): - - """Called by mainapp.TartubeApp.download_manager_finished() at the end - of a download operation. - - Resets our dictionary of instantaneous download speeds, used to - calculate the rolling average. - - Also resets the Gtk.Label. - """ - - self.progress_list_average_speed_dict = {} - self.progress_update_label.set_markup('') - - - # (Results List) - - - def results_list_reset(self): - - """Can be called by anything. - - Empties the Gtk.TreeView in the Results List, ready for it to be - refilled. (There are no IVs to reset.) - """ - - # Reset widgets - self.results_list_liststore = Gtk.ListStore( - int, str, - GdkPixbuf.Pixbuf, - str, str, str, str, - bool, - GdkPixbuf.Pixbuf, - str, - ) - self.results_list_treeview.set_model(self.results_list_liststore) - - # Reset IVs - self.results_list_row_count = 0 - self.results_list_temp_list = [] - self.results_list_row_dict = {} - - - def results_list_add_row(self, download_item_obj, video_obj, \ - mini_options_dict): - - """Called by mainapp.TartubeApp.announce_video_download(). - - At the instant when youtube-dl completes a video download, the standard - python test for the existence of a file fails. - - Therefore, when this function is called, we display the downloaded - video in the Results List immediately, but we also add the video to a - temporary list. - - Thereafter, periodic calls to self.results_list_update_row() check - whether the file actually exists yet, and updates the Results List - accordingly. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object handling a download for a media data object - - video_obj (media.Video): The media data object for the downloaded - video - - mini_options_dict (dict): A dictionary containing a subset of - download options from the the options.OptionsManager object - used to download the video. It contains zero, some or all of - the following download options: - - keep_description keep_info keep_annotations keep_thumbnail - move_description move_info move_annotations move_thumbnail - - """ - - # Prepare the icons - if video_obj.live_mode == 1: - if not video_obj.live_debut_flag: - pixbuf = self.pixbuf_dict['live_wait_small'] - else: - pixbuf = self.pixbuf_dict['debut_wait_small'] - elif video_obj.live_mode == 2: - if not video_obj.live_debut_flag: - pixbuf = self.pixbuf_dict['live_now_small'] - else: - pixbuf = self.pixbuf_dict['debut_now_small'] - elif video_obj.split_flag: - pixbuf = self.pixbuf_dict['split_file_small'] - elif download_item_obj.operation_type == 'sim' \ - or download_item_obj.media_data_obj.dl_sim_flag: - pixbuf = self.pixbuf_dict['check_small'] - else: - pixbuf = self.pixbuf_dict['download_small'] - - if isinstance(video_obj.parent_obj, media.Channel): - pixbuf2 = self.pixbuf_dict['channel_small'] - elif isinstance(video_obj.parent_obj, media.Playlist): - pixbuf2 = self.pixbuf_dict['playlist_small'] - elif isinstance(video_obj.parent_obj, media.Folder): - pixbuf2 = self.pixbuf_dict['folder_small'] - else: - return self.app_obj.system_error( - 223, - 'Results List add row request failed sanity check', - ) - - # Prepare the new row in the treeview - row_list = [] - - # Set the row's initial contents - row_list.append(video_obj.dbid) # Hidden - row_list.append( # Hidden - html.escape( - video_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - True, # Show errors/warnings - ), - ), - ) - row_list.append(pixbuf) - row_list.append(video_obj.nickname) - - # (For a simulated download, the video duration (etc) will already be - # available, so we can display those values) - if video_obj.duration is not None: - row_list.append( - utils.convert_seconds_to_string(video_obj.duration), - ) - else: - row_list.append(None) - - if video_obj.file_size is not None: - row_list.append(video_obj.get_file_size_string()) - else: - row_list.append(None) - - if video_obj.upload_time is not None: - row_list.append(video_obj.get_upload_date_string()) - else: - row_list.append(None) - - row_list.append(video_obj.dl_flag) - row_list.append(pixbuf2) - row_list.append(video_obj.parent_obj.name) - - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.results_list_treeview.show_all() - if not self.app_obj.results_list_reverse_flag: - self.results_list_liststore.append(row_list) - else: - self.results_list_liststore.prepend(row_list) - - # Store some information about this download so that periodic calls to - # self.results_list_update_row() can retrieve it, and check whether - # the file exists yet - temp_dict = { - 'video_obj': video_obj, - 'row_num': self.results_list_row_count, - } - - for key in mini_options_dict.keys(): - temp_dict[key] = mini_options_dict[key] - - # Update IVs - self.results_list_temp_list.append(temp_dict) - self.results_list_row_dict[video_obj.dbid] \ - = self.results_list_row_count - # (The number of rows has just increased, so increment the IV for the - # next call to this function) - self.results_list_row_count += 1 - - # Special measures for individual videos. The video name may not have - # been known when the Progress List was updated for the last time - # (but is known now). Update the name displayed in the Progress List, - # just to be sure - self.progress_list_update_video_name(download_item_obj, video_obj) - - - def results_list_update_row(self): - - """Called by mainapp.TartubeApp.dl_timer_callback(). - - self.results_list_temp_list contains a set of dictionaries, one for - each video download whose file has not yet been confirmed to exist. - - Go through each of those dictionaries. If the file still doesn't exist, - re-insert the dictionary back into self.results_list_temp_list, ready - for it to be checked by the next call to this function. - - If the file does now exist, update the corresponding media.Video - object. Then update the Video Catalogue and the Progress List. - """ - - new_temp_list = [] - - while self.results_list_temp_list: - - temp_dict = self.results_list_temp_list.pop(0) - - # For convenience, retrieve the media.Video object, leaving the - # other values in the dictionary until we need them - video_obj = temp_dict['video_obj'] - # Get the video's full file path now, as we use it several times - video_path = video_obj.get_actual_path(self.app_obj) - - # Because of the 'Requested formats are incompatible for merge and - # will be merged into mkv' warning, we have to check for that - # extension, too - mkv_flag = False - if not os.path.isfile(video_path) and video_obj.file_ext == '.mp4': - - mkv_flag = True - video_path = video_obj.get_actual_path_by_ext( - self.app_obj, - '.mkv', - ) - - # Does the downloaded file now exist on the user's hard drive? - if os.path.isfile(video_path): - - # Update the media.Video object using the temporary dictionary - self.app_obj.update_video_when_file_found( - video_obj, - video_path, - temp_dict, - mkv_flag, - ) - - # The parent container objects can now be sorted - video_obj.parent_obj.sort_children(self.app_obj) - self.app_obj.fixed_all_folder.sort_children(self.app_obj) - - if video_obj.bookmark_flag: - self.app_obj.fixed_bookmark_folder.sort_children( - self.app_obj, - ) - - if video_obj.fav_flag: - self.app_obj.fixed_fav_folder.sort_children(self.app_obj) - - if video_obj.live_mode: - self.app_obj.fixed_live_folder.sort_children(self.app_obj) - - if video_obj.missing_flag: - self.app_obj.fixed_missing_folder.sort_children( - self.app_obj, - ) - - if video_obj.new_flag: - self.app_obj.fixed_new_folder.sort_children(self.app_obj) - - if video_obj in self.app_obj.fixed_recent_folder.child_list: - self.app_obj.fixed_recent_folder.sort_children( - self.app_obj, - ) - - if video_obj.waiting_flag: - self.app_obj.fixed_waiting_folder.sort_children( - self.app_obj, - ) - - # Update the video catalogue in the 'Videos' tab - GObject.timeout_add( - 0, - self.video_catalogue_update_video, - video_obj, - ) - - # Prepare icons - if isinstance(video_obj.parent_obj, media.Channel): - pixbuf = self.pixbuf_dict['channel_small'] - elif isinstance(video_obj.parent_obj, media.Channel): - pixbuf = self.pixbuf_dict['playlist_small'] - else: - pixbuf = self.pixbuf_dict['folder_small'] - - # Update the corresponding row in the Results List - row_num = temp_dict['row_num'] - # New rows are being added to the top, so the real row number - # changes on every call to self.results_list_add_row() - if self.app_obj.results_list_reverse_flag: - row_num = self.results_list_row_count - 1 - row_num - - tree_path = Gtk.TreePath(row_num) - row_iter = self.results_list_liststore.get_iter(tree_path) - - self.results_list_liststore.set( - row_iter, - self.results_list_tooltip_column, - html.escape( - video_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - True, # Show errors/warnings - ), - ), - ) - - self.results_list_liststore.set( - row_iter, - 3, - video_obj.nickname, - ) - - if video_obj.duration is not None: - self.results_list_liststore.set( - row_iter, - 4, - utils.convert_seconds_to_string( - video_obj.duration, - ), - ) - - if video_obj.file_size: - self.results_list_liststore.set( - row_iter, - 5, - video_obj.get_file_size_string(), - ) - - if video_obj.upload_time: - self.results_list_liststore.set( - row_iter, - 6, - video_obj.get_upload_date_string(), - ) - - self.results_list_liststore.set(row_iter, 7, video_obj.dl_flag) - self.results_list_liststore.set(row_iter, 8, pixbuf) - - self.results_list_liststore.set( - row_iter, - 9, - video_obj.parent_obj.name, - ) - - else: - - # File not found - - # If this was a simulated download, the key 'keep_description' - # won't exist in temp_dict - # For simulated downloads, we only check once (in case the - # video file already existed on the user's filesystem) - # For real downloads, we check again on the next call to this - # function - if 'keep_description' in temp_dict: - new_temp_list.append(temp_dict) - - # Any files that don't exist yet must be checked on the next call to - # this function - self.results_list_temp_list = new_temp_list - - - def results_list_update_row_on_delete(self, dbid): - - """Called by mainapp.TartubeApp.delete_video(). - - When a video is deleted, this function is called. If the video is - visible in the Results List, we change the icon to mark it as deleted. - - Args: - - dbid (int): The .dbid of the media.Video object which has just been - deleted - - """ - - if dbid in self.results_list_row_dict: - - row_num = self.results_list_row_dict[dbid] - if self.app_obj.results_list_reverse_flag: - # New rows are being added to the top, so the real row number - # changes on every call to self.results_list_add_row() - row_num = self.results_list_row_count - 1 - row_num - - tree_path = Gtk.TreePath(row_num) - row_iter = self.results_list_liststore.get_iter(tree_path) - if row_iter: - - self.results_list_liststore.set( - row_iter, - 2, - self.pixbuf_dict['delete_small'], - ) - - self.results_list_liststore.set(row_iter, 7, False) - - self.results_list_liststore.set( - row_iter, - 8, - self.pixbuf_dict['delete_small'], - ) - - self.results_list_liststore.set(row_iter, 9, '') - - - def results_list_update_tooltip(self, video_obj): - - """Called by downloads.DownloadWorker.data_callback(). - - When downloading a video individually, the tooltips in the Results - List are only updated when the video file is actually downloaded. This - function is called to update the tooltips at the end of every download, - ensuring that any errors/warnings are visible in it. - - Args: - - video_obj (media.Video): The video which has just been downloaded - individually - - """ - - if video_obj.dbid in self.results_list_row_dict: - - # Update the corresponding row in the Results List - row_num = self.results_list_row_dict[video_obj.dbid] - # New rows are being added to the top, so the real row number - # changes on every call to self.results_list_add_row() - if self.app_obj.results_list_reverse_flag: - row_num = self.results_list_row_count - 1 - row_num - - tree_path = Gtk.TreePath(row_num) - row_iter = self.results_list_liststore.get_iter(tree_path) - - self.results_list_liststore.set( - row_iter, - 1, - html.escape( - video_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - True, # Show errors/warnings - ), - ), - ) - - - def results_list_get_column_widths(self): - - """Called by mainapp.TartubeApp.save_config(). - - Fetches the width of the 'New videos' column in the Results List. - - Return values: - - The width, or None if the list doesn't exist yet - - """ - - if self.results_list_treeview is None: - return None, None - - else: - video_column = self.results_list_treeview.get_column(3) - video_width = video_column.get_width() - # (Shouldn't be possible to reduce the width below the minimum, - # but we'll check anyway) - if video_width < self.min_column_width: - video_width = self.min_column_width - - return video_width - - - # (Classic Mode tab) - - - def classic_mode_tab_add_dest_dir(self): - - """Called by mainapp.TartubeApp.on_button_classic_dest_dir(). - - A new destination directory has been added, so add it to the combobox - in the Classic Mode tab. - """ - - # Reset the contents of the combobox - self.classic_dest_dir_liststore = Gtk.ListStore(str) - for string in self.app_obj.classic_dir_list: - self.classic_dest_dir_liststore.append( [string] ) - - self.classic_dest_dir_combo.set_model(self.classic_dest_dir_liststore) - self.classic_dest_dir_combo.set_active(0) - self.show_all() - - - def classic_mode_tab_add_row(self, dummy_obj): - - """Called by self.classic_mode_tab_add_urls(). - - Adds a row to the Classic Progress List. - - Args: - - dummy_obj (media.Video): The dummy media.Video object handling the - download of a single URL (which might represent a video, - channel or playlist) - - """ - - # Prepare the new row in the treeview - row_list = [] - - row_list.append(dummy_obj.dbid) # Hidden - row_list.append( # Hidden - html.escape( - dummy_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - True, # Show errors/warnings - ), - ), - ) - - # (Don't display the https:// bit, that's just wasted space - source = dummy_obj.source - match = re.search(r'^https?\:\/\/(.*)', source) - if match: - source = match.group(1) - - row_list.append(source) - row_list.append(None) - row_list.append(_('Waiting')) - row_list.append(None) - row_list.append(None) - row_list.append(None) - row_list.append(None) - row_list.append(None) - row_list.append(None) - - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.classic_progress_treeview.show_all() - self.classic_progress_liststore.append(row_list) - - - def classic_mode_tab_move_row(self, up_flag): - - """Called by mainapp.TartubeApp.on_button_classic_move_up() and - .on_button_classic_move_down(). - - Moves the selected row(s) up/down in the Classic Progress List. - - Args: - - up_flag (bool): True to move up, False to move down - - """ - - selection = self.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - # Move each selected row up (or down) - if up_flag: - - # Move up - for path in path_list: - - this_iter = model.get_iter(path) - if model.iter_previous(this_iter): - - self.classic_progress_liststore.move_before( - this_iter, - model.iter_previous(this_iter), - ) - - else: - - # If the first item won't move up, then successive items - # will be moved above this one (which is not what we - # want) - return - - else: - - # Move down - path_list.reverse() - - for path in path_list: - - this_iter = model.get_iter(path) - if model.iter_next(this_iter): - - self.classic_progress_liststore.move_after( - this_iter, - model.iter_next(this_iter), - ) - - else: - - return - - - def classic_mode_tab_remove_rows(self, dbid_list): - - """Called by mainapp.TartubeApp.on_button_classic_remove and - .on_button_classic_clear.(). - - Removes the selected rows from the Classic Progress List and updates - IVs. - - Args: - - dbid_list (list): The .dbids for the dummy media.Video object - corresponding to each selected row - - """ - - # (Import IVs for convenience) - manager_obj = self.app_obj.download_manager_obj - - # Check each row in turn - for dbid in dbid_list: - - # If there is a current download operation, we need to update it - if manager_obj: - - # If this dummy media.Video object is the one being downloaded, - # halt the download - for worker_obj in manager_obj.worker_list: - - if worker_obj.running_flag \ - and worker_obj.download_item_obj \ - and worker_obj.download_item_obj.media_data_obj.dbid \ - == dbid: - worker_obj.downloader_obj.stop() - - # Delete the dummy media.Video object - del self.classic_media_dict[dbid] - - # Remove the row from the treeview - row_iter = self.classic_mode_tab_find_row_iter(dbid) - if row_iter: - self.classic_progress_liststore.remove(row_iter) - - - def classic_mode_tab_add_urls(self): - - """Called by mainapp.TartubeApp.on_button_classic_add_urls(). - - Also called by self.on_classic_textbuffer_changed(). - - In the Classic Mode tab, transfers URLs from the textview into the - Classic Progress List (a treeview), creating a new dummy media.Video - object for each URL, and updating IVs. - - Return values: - - Returns a list of URLs added to the Classic Progress List (which - may be empty) - - """ - - # Get the specified download destination - tree_iter = self.classic_dest_dir_combo.get_active_iter() - model = self.classic_dest_dir_combo.get_model() - dest_dir = model[tree_iter][0] - - # Get the specified video/audio format, leaving the value as None if - # the 'Default' item is selected - tree_iter2 = self.classic_format_combo.get_active_iter() - model2 = self.classic_format_combo.get_model() - format_str = model2[tree_iter2][0] - # (Valid formats begin with whitespace) - if not re.search(r'^\s', format_str): - format_str = None - else: - format_str = re.sub(r'^\s*', '', format_str) - # (One last check for a valid video/audio format) - if not format_str in formats.VIDEO_FORMAT_LIST \ - and not format_str in formats.AUDIO_FORMAT_LIST: - format_str = None - - # Set the specified resolution, leaving the value as None if the - # 'Highest' item is selected - tree_iter3 = self.classic_resolution_combo.get_active_iter() - model3 = self.classic_resolution_combo.get_model() - resolution_str = model3[tree_iter3][0] - # (Selectable resolutions in the combo begin with whitespace) - if not re.search(r'^\s', resolution_str): - resolution_str = None - else: - resolution_str = utils.strip_whitespace(resolution_str) - # (One last check for a valid resolution) - if not resolution_str in formats.VIDEO_RESOLUTION_LIST: - resolution_str = None - - # If the combobox item is selected, we convert a downloaded video to - # the specified format with FFmpeg/AVConv. This is signified by - # adding 'convert_' to the beginning of the format string - tree_iter4 = self.classic_convert_combo.get_active_iter() - model4 = self.classic_convert_combo.get_model() - convert_str = model4[tree_iter4][0] - if format_str is not None \ - and convert_str == _('Convert to this format'): - format_str = 'convert_' + format_str - # The resolution, if specified, is added to the end of the format - # string - if resolution_str is not None: - if format_str is None: - format_str = resolution_str - else: - format_str += '_' + resolution_str - - # Extract a list of URLs from the textview - url_string = self.classic_textbuffer.get_text( - self.classic_textbuffer.get_iter_at_mark(self.classic_mark_start), - self.classic_textbuffer.get_iter_at_mark(self.classic_mark_end), - False, - ) - - # Split the string into lines, then split each line by whitespace. This - # allows us to recognise multiple valid URLs on the same line, and - # also to interpret a line containing a URL and miscellaneous text - url_list = [] - line_list = url_string.splitlines() - for line in line_list: - - for url in line.split(): - url_list.append(url) - - # Remove initial/final whitespace, and ignore invalid/duplicate links - mod_list = [] - invalid_url_string = '' - for url in url_list: - - # Strip whitespace - mod_url = utils.strip_whitespace(url) - - # Check for duplicates - invalid_flag = False - - if url in mod_list: - invalid_flag = True - - else: - - for other_obj in self.classic_media_dict.values(): - if other_obj.source == url: - invalid_flag = True - break - - if not invalid_flag and not utils.check_url(mod_url): - invalid_flag = True - - if not invalid_flag: - mod_list.append(mod_url) - else: - # Invalid links can stay in the textview. Hopefully it's - # obvious to the user why an invalid link hasn't been added - if not invalid_url_string: - invalid_url_string = mod_url - else: - invalid_url_string += '\n' + mod_url - - # For each valid link, create a dummy media.Video object. The dummy - # objects have negative .dbids, and are not added to the media data - # registry - for url in mod_list: - - self.classic_mode_tab_create_dummy_video( - url, - dest_dir, - format_str, - ) - - # Unless the flag is set, any invalid links remain in the textview (but - # in all cases, all valid links are removed from it) - # When this function is called by self.on_classic_textbuffer_changed(), - # Gtk generates a warning when we try to .set_text() - # The only way I can find to get around this is to replace the old - # textbuffer with a new one - self.classic_mode_tab_replace_textbuffer() - - if not self.app_obj.classic_duplicate_remove_flag: - self.classic_textbuffer.set_text(invalid_url_string) - else: - self.classic_textbuffer.set_text('') - - return mod_list - - - def classic_mode_tab_insert_url(self, url, options_obj): - - """Called by mainwin.DropZoneBox.on_drag_data_received(). - - A modified version of self.classic_mode_tab_add_urls(). - - Inserts a single URL into the Classic Progress List, creating a dummy - media.Video object for it. The URL is downloaded using the specified - options.OptionsManager object. - - The contents of the 'Destination' box is used, but the contents of - the 'Format' boxes are ignored. - - Args: - - url (str): The URL to download. This function assumes the calling - code has already stripped leading/trailing whitespace - - options_obj (options.OptionsManager): Download options for this URL - - Return values: - - True on success, False on failure - - """ - - # Sanity check - if url is None \ - or not utils.check_url(url) \ - or options_obj is None: - self.app_obj.system_error( - 224, - 'Invalid insert URL into Classic Progress List request', - ) - - return False - - # Get the specified download destination - tree_iter = self.classic_dest_dir_combo.get_active_iter() - model = self.classic_dest_dir_combo.get_model() - dest_dir = model[tree_iter][0] - - # Create the dummy media.Video object, which has a negative .dbid, and - # is not added to the media data registry - dummy_obj = self.classic_mode_tab_create_dummy_video( - url, - dest_dir, - ) - - if not dummy_obj: - return False - else: - dummy_obj.set_options_obj(options_obj) - return True - - - def classic_mode_tab_replace_textbuffer(self): - - """Called by self.classic_mode_tab_add_urls(), just before replacing - the contents of the Gtk.TextView at the top of the tab. - - When that function is called by self.on_classic_textbuffer_changed(), - Gtk generates a warning when we try to .set_text(). - - The only way I can find to get around this is to replace the old - textbuffer with a new one - """ - - self.classic_textbuffer = Gtk.TextBuffer() - self.classic_textview.set_buffer(self.classic_textbuffer) - - self.classic_mark_start = self.classic_textbuffer.create_mark( - 'mark_start', - self.classic_textbuffer.get_start_iter(), - True, # Left gravity - ) - self.classic_mark_end = self.classic_textbuffer.create_mark( - 'mark_end', - self.classic_textbuffer.get_end_iter(), - False, # Not left gravity - ) - - self.classic_textbuffer.connect( - 'changed', - self.on_classic_textbuffer_changed, - ) - - - def classic_mode_tab_create_dummy_video(self, url, dest_dir, \ - format_str=None, ignore_extras_flag=False): - - """Called by self.classic_mode_tab_add_urls(), - self.on_video_catalogue_process_clip_classic_mode() or - mainapp.TartubeApp.download_manager_finished(). - - Creates a dummy media.Video object. The dummy object has a negative - .dbid, and is not added to the media data registry. - - In the Classic Mode tab, adds a line to the Classic Progress List (a - treeview). - - Args: - - url (str): A URL representing a video, channel or playlist, to be - stored in the new dummy media.Video object - - dest_dir (str): Full path to the directory into which any videos - (and other files) are downloaded - - format_str (str or None): A string specifying the media format to - download, or None if the user didn't specify one. The string - is made up of three optional components in a fixed order and - separated by underlines: 'convert', the video/audio format, and - the video resolution, for example 'mp4', 'mp4_720p', - 'convert_mp4_720p'. Valid values are those specified by - formats.VIDEO_FORMAT_LIST, formats.AUDIO_FORMAT_LIST and - formats.VIDEO_RESOLUTION_LIST - - ignore_extras_flag (bool): If True, called from - self.on_video_catalogue_process_clip_classic_mode(), in which - livestream/SponsorBlock settings should be ignored - - Return values: - - The dummy media.Video object created - - """ - - self.classic_media_total += 1 - - new_obj = media.Video( - self.app_obj, - (self.classic_media_total) * -1, # Negative .dbid - self.app_obj.default_video_name, - ) - - new_obj.set_dummy(url, dest_dir, format_str) - - if not ignore_extras_flag: - if self.app_obj.classic_livestream_flag: - new_obj.set_live_mode(2) - - if self.app_obj.classic_sblock_flag: - new_obj.set_dummy_sblock_flag(True) - - # Add a line to the treeview - self.classic_mode_tab_add_row(new_obj) - - # Update IVs - self.classic_media_dict[new_obj.dbid] = new_obj - - # If a download operation, generated by the Classic Mode tab, is in - # progress, then we can add this URL directly to the - # downloads.DownloadList object - manager_obj = self.app_obj.download_manager_obj - - if manager_obj \ - and manager_obj.operation_classic_flag \ - and manager_obj.running_flag \ - and manager_obj.download_list_obj: - manager_obj.download_list_obj.create_dummy_item(new_obj) - - return new_obj - - - def classic_mode_tab_extract_pending_urls(self): - - """Called by mainapp.TartubeApp.save_config(). - - If the user wants to remember undownloaded URLs from a previous - session, extracts them from the textview at the top of the tab, and - the treeview at the bottom of it. - - Return values: - - A list of URLs (may be an empty list) - - """ - - # Extract a list of URLs from the textview - url_string = self.classic_textbuffer.get_text( - self.classic_textbuffer.get_iter_at_mark(self.classic_mark_start), - self.classic_textbuffer.get_iter_at_mark(self.classic_mark_end), - False, - ) - - # Split the string into lines, then split each line by whitespace. This - # allows us to recognise multiple valid URLs on the same line, and - # also to interpret a line containing a URL and miscellaneous text - url_list = [] - line_list = url_string.splitlines() - for line in line_list: - - for url in line.split(): - url_list.append(url) - - # Remove initial/final whitespace, and ignore invalid/duplicate links - mod_list = [] - for url in url_list: - - # Strip whitespace - mod_url = utils.strip_whitespace(url) - - if not mod_url in url_list \ - and utils.check_url(mod_url): - mod_list.append(mod_url) - - # From the treeview, check each dummy media.Video object, and add the - # URL for any undownloaded video (but ignore duplicates) - for dummy_obj in self.classic_media_dict.values(): - - if not dummy_obj.dummy_dl_flag \ - and dummy_obj.source is not None \ - and not dummy_obj.source in mod_list: - mod_list.append(dummy_obj.source) - - return mod_list - - - def classic_mode_tab_restore_urls(self, url_list): - - """Called by mainapp.TartubeApp.start(). - - If the user wants to remember undownloaded URLs from a previous - session, then restore them to the Classic Mode tab's textview. - - Args: - - url_list (list): A list of URLs to restore - - """ - - self.classic_textbuffer.set_text('\n'.join(url_list)) - - - def classic_mode_tab_find_row_iter(self, dbid): - - """Called by self.classic_mode_tab_remove_rows() and - .classic_mode_tab_display_dl_stats(). - - Finds the GtkTreeIter for the Classic Progress List row displaying the - specified data for the dummy media.Video object. - """ - - for row in self.classic_progress_liststore: - if self.classic_progress_liststore[row.iter][0] == dbid: - return row.iter - - - def classic_mode_tab_receive_dl_stats(self, download_item_obj, - dl_stat_dict, finish_flag=False): - - """Called by downloads.DownloadWorker.data_callback(). - - A modified form of self.progress_list_receive_dl_stats(), used during - a download operation launched from the Classic Mode tab. - - Stores download statistics until they can be displayed (as in the - original function) - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object handling a download for a dummy media.Video object - - dl_stat_dict (dict): The dictionary of download statistics - described in the original function - - finish_flag (bool): True if the worker has finished with its - dummy media.Video object, meaning that dl_stat_dict is the - final set of statistics, and that the progress list row can be - hidden, if required - - """ - - # Temporarily store the dictionary of download statistics - if not download_item_obj.item_id in self.classic_temp_dict: - new_dl_stat_dict = {} - else: - new_dl_stat_dict \ - = self.classic_temp_dict[download_item_obj.item_id] - - for key in dl_stat_dict: - new_dl_stat_dict[key] = dl_stat_dict[key] - - self.classic_temp_dict[download_item_obj.item_id] \ - = new_dl_stat_dict - - - def classic_mode_tab_display_dl_stats(self): - - """Called by downloads.DownloadManager.run() and - mainapp.TartubeApp.dl_timer_callback(). - - A modified form of self.progress_list_display_dl_stats(), used during - a download operation launched from the Classic Mode tab. - """ - - # Import the contents of the IV (in case it gets updated during the - # call to this function), and use the imported copy - temp_dict = self.classic_temp_dict - self.classic_temp_dict = {} - - # For each dummy media.Video object displayed in the download list... - for dbid in temp_dict: - - # Get a dictionary of download statistics for this dummy - # media.Video object - # The dictionary is in the standard format described in the - # comments to downloads.VideoDownloader.extract_stdout_data() - dl_stat_dict = temp_dict[dbid] - - # During pre-processing, make sure a filename from a previous call - # is not visible - if 'status' in dl_stat_dict \ - and dl_stat_dict['status'] == formats.ACTIVE_STAGE_PRE_PROCESS: - dl_stat_dict['filename'] = '' - pre_process_flag = True - else: - pre_process_flag = False - - # Get the dummy media.Video object itself - if not dbid in self.classic_media_dict: - # Row has already been deleted by the user - continue - else: - media_data_obj = self.classic_media_dict[dbid] - - # Get the corresponding treeview row - row_iter = self.classic_mode_tab_find_row_iter(dbid) - if not row_iter: - # Row has already been deleted by the user - continue - else: - row_path = self.classic_progress_liststore.get_path(row_iter) - - # Update the tooltip - self.classic_progress_liststore.set( - row_iter, - self.classic_progress_tooltip_column, - html.escape( - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - True, # Show errors/warnings - ), - ), - ) - - # Update statistics displayed in this row - # (Column 0 is not modified, once the row has been added to the - # treeview) - column = 2 - - for key in ( - 'playlist_index', - 'status', - 'filename', - 'extension', - 'percent', - 'speed', - 'eta', - 'filesize', - ): - column += 1 - - if key in dl_stat_dict: - - if key == 'playlist_index': - - if dl_stat_dict['playlist_index'] == 0: - string = '' - - elif 'dl_sim_flag' in dl_stat_dict \ - and dl_stat_dict['dl_sim_flag']: - # (Don't know how many videos there are in a - # channel/playlist, so ignore value of - # 'playlist_size') - string = str(dl_stat_dict['playlist_index']) - - else: - string = str(dl_stat_dict['playlist_index']) - if 'playlist_size' in dl_stat_dict \ - and dl_stat_dict['playlist_size'] > 0: - string = string + '/' \ - + str(dl_stat_dict['playlist_size']) - else: - string = string + '/1' - - elif key == 'filename': - - # Don't overwrite the filename, so that users can more - # easily identify failed downloads - # (Exception: when splitting a video into clips, - # always show the clip name) - if dl_stat_dict[key] == '' and not pre_process_flag: - continue - elif media_data_obj.file_name is not None \ - and not 'clip_flag' in dl_stat_dict: - string = media_data_obj.file_name - else: - string = dl_stat_dict[key] - - else: - string = dl_stat_dict[key] - - self.classic_progress_liststore.set( - self.classic_progress_liststore.get_iter(row_path), - column, - string, - ) - - - def classic_mode_tab_timer_callback(self): - - """Called from a callback in self.on_classic_menu_toggle_auto_copy(). - - Periodically checks the system's clipboard, and adds any new URLs to - the Classic Progress List. - """ - - # If the user manually empties the textview, don't re-paste whatever - # is currently in the clipboard - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - cliptext = clipboard.wait_for_text() - - if cliptext != '': - - if self.classic_auto_copy_text is not None \ - and cliptext == self.classic_auto_copy_text: - - # Return 1 to keep the timer going - return 1 - - else: - - self.classic_auto_copy_text = cliptext - - utils.add_links_to_textview_from_clipboard( - self.app_obj, - self.classic_textbuffer, - self.classic_mark_start, - self.classic_mark_end, - ) - - # Return 1 to keep the timer going - return 1 - - - def classic_mode_tab_start_download(self): - - """Called by mainapp.TartubeApp.on_button_classic_download() and - self.on_classic_textbuffer_changed(). - - Starts a download operation for the URLs added to the Classic Progress - List. - """ - - if self.app_obj.download_manager_obj: - - # Download already in progress - return - - elif not self.app_obj.classic_custom_dl_flag: - - # Start an (ordinary) download operation - self.app_obj.download_manager_start('classic_real') - - elif self.app_obj.classic_custom_dl_obj.dl_by_video_flag: - - # If the user has opted to download each video independently of its - # channel or playlist, then we have to do a simulated download - # first, in order to collect the URLs of each invidual video - # ('classic_sim') - # When that download operation has finished, we can do a (real) - # custom download for each video ('classic_custom') - self.app_obj.download_manager_start( - 'classic_sim', - False, # Not called by slow timer - [], # Download all URLs - self.app_obj.classic_custom_dl_obj, - ) - - else: - - # Otherwise, a full custom download can proceed immediately, - # without performing the simulated download first - self.app_obj.download_manager_start( - 'classic_custom', - False, # Not called by slow timer - [], # Download all URLs - self.app_obj.classic_custom_dl_obj, - ) - - - def classic_mode_tab_get_column_widths(self): - - """Called by mainapp.TartubeApp.save_config(). - - Fetches the width of the 'Source' and 'Incoming file' columns in the - Classic Progress List. - - Return values: - - The two widths, or None if the list doesn't exist yet - - """ - - if self.classic_progress_treeview is None: - return None, None - - else: - source_column = self.classic_progress_treeview.get_column(2) - source_width = source_column.get_width() - # (Shouldn't be possible to reduce the width below the minimum, - # but we'll check anyway) - if source_width < 20: - source_width = 20 - - incoming_column = self.classic_progress_treeview.get_column(5) - incoming_width = incoming_column.get_width() - if incoming_width < 20: - incoming_width = 20 - - return source_width, incoming_width - - - # (Drag and Drop tab) - - - def drag_drop_grid_empty(self): - - """Can be called by anything. - - Draws an empty grid in the Drag and Drop tab (replacing any grid that - already exists). - """ - - # If not called by self.setup_videos_tab()... - if self.drag_drop_frame.get_child(): - self.drag_drop_frame.remove(self.drag_drop_frame.get_child()) - - # Replace the grid - self.drag_drop_grid = Gtk.Grid() - self.drag_drop_frame.add(self.drag_drop_grid) - self.drag_drop_grid.set_column_spacing(self.spacing_size) - self.drag_drop_grid.set_row_spacing(self.spacing_size) - self.drag_drop_grid.set_column_homogeneous(True) - self.drag_drop_grid.set_row_homogeneous(True) - - # Procedure complete - self.drag_drop_grid.show_all() - - - def drag_drop_grid_reset(self): - - """Can be called by anything. - - Draws a grid of mainwin.DropZoneBox objects in the Drag and Drop tab - (replacing any grid that already exists). Each mainwin.DropZoneBox is - associated with a set of download options (options.OptionsManager). - - The code for the Drag and Drop tab is fairly simple. - self.drag_drop_add_dropzone() can be called to add a new dropzone, but - for everything else, we just call this function to reset the grid. - """ - - # If not called by self.setup_videos_tab()... - if self.drag_drop_frame.get_child(): - self.drag_drop_frame.remove(self.drag_drop_frame.get_child()) - - # (Temporarily retain the old dropzones, so we can preserve their - # confirmation messages and reset times) - old_dict = self.drag_drop_dict - self.drag_drop_dict = {} - - # Replace the grid - self.drag_drop_grid = Gtk.Grid() - self.drag_drop_frame.add(self.drag_drop_grid) - self.drag_drop_grid.set_column_spacing(self.spacing_size) - self.drag_drop_grid.set_row_spacing(self.spacing_size) - self.drag_drop_grid.set_column_homogeneous(True) - self.drag_drop_grid.set_row_homogeneous(True) - - # Set up dropzones on the grid. The minimum size is 1x1, maximum is - # self.drag_drop_max (we assume it is not a prime number, as - # discussed in the comments in self.__init__() ) - # If there aren't enough options.OptionsManager objects to fill a grid, - # then we use an empty dropzone (one whose .options_obj IV is set to - # None) - actual_size = grid_size = len(self.app_obj.classic_dropzone_list) - if grid_size > self.drag_drop_max: - grid_size = self.drag_drop_max - - # Create the smallest grid possible, checking for the suitability of - # grid sizes in the order 1x1, 2x1, 2x2, 3x2, 3x3... - w = None - h = None - dim = 0 - while w is None and h is None: - - dim += 1 - - if grid_size <= dim * dim: - w = dim - h = dim - elif grid_size <= dim * (dim + 1): - w = dim + 1 - h = dim - - # Add drop zones at every location in the grid - index = -1 - for y_pos in range(h): - for x_pos in range(w): - - index += 1 - if index < actual_size: - uid = self.app_obj.classic_dropzone_list[index] - options_obj = self.app_obj.options_reg_dict[uid] - else: - options_obj = None - - # Instead of using Gtk.Frame directly, use a wrapper class so - # we can quickly retrieve the options.OptionsManager object - # displayed in each dropzone - if not options_obj \ - or not options_obj.uid in self.app_obj.options_reg_dict \ - or not options_obj.uid in old_dict: - update_text = None - reset_time = None - else: - # Preserve the previous confirmation message - old_wrapper_obj = old_dict[options_obj.uid] - update_text = old_wrapper_obj.update_text - reset_time = old_wrapper_obj.reset_time - - wrapper_obj = DropZoneBox( - self, - options_obj, - x_pos, - y_pos, - h, - update_text, - reset_time, - ) - - if wrapper_obj: - self.drag_drop_grid.attach(wrapper_obj, x_pos, y_pos, 1, 1) - if options_obj: - self.drag_drop_dict[options_obj.uid] = wrapper_obj - - # (De)sensitie the add button, as appropriate - if actual_size >= self.drag_drop_max: - self.drag_drop_add_button.set_sensitive(False) - else: - self.drag_drop_add_button.set_sensitive(True) - - # Procedure complete - self.drag_drop_grid.show_all() - - - def drag_drop_add_dropzone(self): - - """Called by mainapp.TartubeApp.on_button_drag_drop_add() or by any - other code. - - Prompts the user to create a new set of download options, or to use - an existing set. - - Adds a new dropzone to the Drag and Drop tab's grid to accommodate it. - """ - - if len(self.drag_drop_dict) >= self.drag_drop_max: - return self.app_obj.system_error( - 225, - 'Drag and Drop tab out of space', - ) - - # Prompt the user to select one of existing options.OptionsManager - # objects, or to create a new one - dialogue_win = AddDropZoneDialogue(self) - response = dialogue_win.run() - # Get the specified options.OptionsManager object, before - # destroying the window - options_name = dialogue_win.options_name - options_obj = dialogue_win.options_obj - clone_flag = dialogue_win.clone_flag - dialogue_win.destroy() - - edit_win_flag = False - - if response == Gtk.ResponseType.OK \ - and ( - options_name is not None \ - or options_obj is not None \ - or clone_flag - ): - if options_name is not None: - - options_obj = self.app_obj.create_download_options( - options_name, - ) - - edit_win_flag = True - - elif clone_flag: - - options_obj = self.app_obj.clone_download_options( - options_obj, - ) - - edit_win_flag = True - - # Add the new dropzone - self.app_obj.add_classic_dropzone_list(options_obj.uid) - # Redraw the grid - self.drag_drop_grid_reset() - - if edit_win_flag: - # Open an edit window to show the new/cloned options - # immediately - config.OptionsEditWin( - self.app_obj, - options_obj, - ) - - - # (Output tab) - - - def output_tab_setup_pages(self): - - """Called by mainapp.TartubeApp.start() and .set_num_worker_default(). - - Makes sure there are enough pages in the Output tab's notebook for - each simultaneous download allowed (a value specified by - mainapp.TartubeApp.num_worker_default). - """ - - # The first page in the Output tab's notebook shows a summary of what - # the threads created by downloads.py are doing - if not self.output_tab_summary_flag \ - and self.app_obj.ytdl_output_show_summary_flag: - self.output_tab_add_page(True) - self.output_tab_summary_flag = True - - # The number of pages in the notebook (not including the summary page) - # should match the highest value of these two things during this - # session: - # - # - The maximum simultaneous downloads allowed - # - If a download operation is in progress, the actual number of - # download.DownloadWorker objects created - # - # Thus, if the user reduces the maximum, we don't remove pages, but we - # do add new pages if the maximum is increased - # Broadcasting livestreams might be exempt from the maximum, so the - # number of workers might be larger than it - count = self.app_obj.num_worker_default - if self.app_obj.download_manager_obj: - - worker_count = len(self.app_obj.download_manager_obj.worker_list) - if worker_count > count: - count = worker_count - - if self.output_page_count < count: - - for num in range(1, (count + 1)): - if not num in self.output_textview_dict: - self.output_tab_add_page() - - - def output_tab_add_page(self, summary_flag=False): - - """Called by self.output_tab_setup_pages(). - - Adds a new page to the Output tab's notebook, and updates IVs. - - Args: - - summary_flag (bool): If True, add the (first) summary page to the - notebook, showing what the threads are doing - - """ - - # Each page (except the summary page) corresponds to a single - # downloads.DownloadWorker object. The page number matches the - # worker's .worker_id. The first worker is numbered #1 - if not summary_flag: - self.output_page_count += 1 - - # Add the new page - tab = Gtk.Box() - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Thread\' means a computer processor' \ - + ' thread. If you\'re not sure how to translate it, just use' \ - + ' \'Page #\', as in Page #1, Page #2, etc', - ) - - if not summary_flag: - label = Gtk.Label.new_with_mnemonic( - _('Thread') + ' #_' + str(self.output_page_count), - ) - else: - label = Gtk.Label.new_with_mnemonic(_('_Summary')) - - self.output_notebook.append_page(tab, label) - tab.set_hexpand(True) - tab.set_vexpand(True) - tab.set_border_width(self.spacing_size) - - # Add a textview to the tab, using a css style sheet to provide - # monospaced white text on a black background - scrolled = Gtk.ScrolledWindow() - tab.pack_start(scrolled, True, True, 0) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - - frame = Gtk.Frame() - scrolled.add_with_viewport(frame) - - style_provider = self.output_tab_set_textview_css( - '#css_text_id_' + str(self.output_page_count) \ - + ', textview text {\n' \ - + ' background-color: ' + self.output_tab_bg_colour + ';\n' \ - + ' color: ' + self.output_tab_text_colour + ';\n' \ - + '}\n' \ - + '#css_label_id_' + str(self.output_page_count) \ - + ', textview {\n' \ - + ' font-family: monospace, monospace;\n' \ - + ' font-size: 10pt;\n' \ - + '}' - ) - - textview = Gtk.TextView() - frame.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_editable(False) - textview.set_cursor_visible(False) - - context = textview.get_style_context() - context.add_provider(style_provider, 600) - - # Reset css properties for the next Gtk.TextView created (for example, - # by AddVideoDialogue) so it uses default values, rather than the - # white text on black background used above - # To do that, create a dummy textview, and apply a css style to it - textview2 = Gtk.TextView() - style_provider2 = self.output_tab_set_textview_css( - '#css_text_id_default, textview text {\n' \ - + ' background-color: unset;\n' \ - + ' color: unset;\n' \ - + '}\n' \ - + '#css_label_id_default, textview {\n' \ - + ' font-family: unset;\n' \ - + ' font-size: unset;\n' \ - + '}' - ) - - context = textview2.get_style_context() - context.add_provider(style_provider2, 600) - - # Set up auto-scrolling - textview.connect( - 'size-allocate', - self.output_tab_do_autoscroll, - scrolled, - ) - - # Make the page visible - self.show_all() - - # Update IVs - if not summary_flag: - self.output_textview_dict[self.output_page_count] = textview - else: - self.output_textview_dict[0] = textview - - - def output_tab_set_textview_css(self, css_string): - - """Called by self.output_tab_add_page(). - - Applies a CSS style to the current screen. Called once to create a - white-on-black Gtk.TextView, then a second time to create a dummy - textview with default properties. - - Args: - - css_string (str): The CSS style to apply - - Return values: - - The Gtk.CssProvider created - - """ - - style_provider = Gtk.CssProvider() - style_provider.load_from_data(bytes(css_string.encode())) - Gtk.StyleContext.add_provider_for_screen( - Gdk.Screen.get_default(), - style_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ) - - return style_provider - - - def output_tab_write_stdout(self, page_num, msg): - - """Called by various functions in downloads.py, info.py, refresh.py, - tidy.py and updates.py. - - During a download operation, youtube-dl sends output to STDOUT. If - permitted, this output is displayed in the Output tab. Other operations - also call this function to display text in the default colour. - - Args: - - page_num (int): The page number on which this message should be - displayed. Matches a key in self.output_textview_dict - - msg (str): The message to display. A newline character will be - added by self.output_tab_write(). - - """ - - GObject.timeout_add( - 0, - self.output_tab_write, - page_num, - msg, - 'default', - ) - - - def output_tab_write_stderr(self, page_num, msg): - - """Called by various functions in downloads.py and info.py. - - During a download operation, youtube-dl sends output to STDERR. If - permitted, this output is displayed in the Output tab. Other operations - also call this function to display text in the non-default colour. - - Args: - - page_num (int): The page number on which this message should be - displayed. Matches a key in self.output_textview_dict - - msg (str): The message to display. A newline character will be - added by self.output_tab_write(). - - """ - - GObject.timeout_add( - 0, - self.output_tab_write, - page_num, - msg, - 'error_warning', - ) - - - def output_tab_write_system_cmd(self, page_num, msg): - - """Called by various functions in downloads.py, info.py and updates.py. - - During a download operation, youtube-dl system commands are displayed - in the Output tab (if permitted). Other operations also call this - function to display text in the non-default colour. - - Args: - - page_num (int): The page number on which this message should be - displayed. Matches a key in self.output_textview_dict - - msg (str): The message to display. A newline character will be - added by self.output_tab_write(). - - """ - - GObject.timeout_add( - 0, - self.output_tab_write, - page_num, - msg, - 'system_cmd', - ) - - - def output_tab_write(self, page_num, msg, msg_type): - - """Called by self.output_tab_write_stdout(), .output_tab_write_stderr() - and .output_tab_write_system_cmd(). - - Writes a message to the output tab. - - N.B. Because Gtk is not thread safe, this function must always be - called from within GObject.timeout_add(). - - Args: - - page_num (int): The page number on which this message should be - displayed. Matches a key in self.output_textview_dict - - msg (str): The message to display. A newline character will be - added by this function - - msg_type (str): 'default', 'error_warning' or 'system_cmd' - - """ - - # Add the text to the textview. STDERR messages and system commands are - # displayed in a different colour - # (Note that the summary page is not necessarily visible) - if page_num in self.output_textview_dict: - - textview = self.output_textview_dict[page_num] - textbuffer = textview.get_buffer() - - # If the buffer is too big, remove the first line to make way for - # the new one - if self.app_obj.output_size_apply_flag \ - and textbuffer.get_line_count() > self.app_obj.output_size_default: - textbuffer.delete( - textbuffer.get_start_iter(), - textbuffer.get_iter_at_line_offset(1, 0), - ) - - if msg_type != 'default': - - # The .markup_escape_text() call won't escape curly braces, so - # we need to replace those manually - msg = re.sub('{', '(', msg) - msg = re.sub('}', ')', msg) - - string = '' \ - + GObject.markup_escape_text(msg) + '\n' - - if msg_type == 'system_cmd': - - textbuffer.insert_markup( - textbuffer.get_end_iter(), - string.format(self.output_tab_system_cmd_colour), - -1, - ) - - else: - - # STDERR - textbuffer.insert_markup( - textbuffer.get_end_iter(), - string.format(self.output_tab_stderr_colour), - -1, - ) - - else: - - # STDOUT - textbuffer.insert( - textbuffer.get_end_iter(), - msg + '\n', - ) - - # Make the new output visible, and scroll to the bottom of every - # updated page - self.output_tab_scroll_visible_page(page_num) - - - def output_tab_update_page_size(self): - - """Called by mainapp.TartubeApp.set_output_size_default(). - - When a page size is applied, count the number of lines in each - textview, and remove the oldest remaining lines, if necessary. - """ - - if self.app_obj.output_size_apply_flag: - - for page_num in self.output_textview_dict: - - textview = self.output_textview_dict[page_num] - textbuffer = textview.get_buffer() - line_count = textbuffer.get_line_count() - - if line_count >= self.app_obj.output_size_default: - textbuffer.delete( - textbuffer.get_start_iter(), - textbuffer.get_iter_at_line_offset( - line_count - self.app_obj.output_size_default - 1, - 0, - ), - ) - - - def output_tab_do_autoscroll(self, textview, rect, scrolled): - - """Called from a callback in self.output_tab_add_page(). - - When one of the textviews in the Output tab is modified (text added or - removed), make sure the page is scrolled to the bottom. - - Args: - - textview (Gtk.TextView): The textview to scroll - - rect (Gdk.Rectangle): Object describing the window's new size - - scrolled (Gtk.ScrolledWindow): The scroller which contains the - textview - - """ - - adj = scrolled.get_vadjustment() - adj.set_value(adj.get_upper() - adj.get_page_size()) - - - def output_tab_scroll_visible_page(self, page_num): - - """Called by self.on_output_notebook_switch_page() and - .on_notebook_switch_page(). - - When the user switches between pages in the Output tab, scroll the - visible textview to the bottom (otherwise it gets confusing). - - Args: - - page_num (int): The page to be scrolled, matching a key in - self.output_textview_dict - - """ - - if page_num in self.output_textview_dict: - textview = self.output_textview_dict[page_num] - - frame = textview.get_parent() - viewport = frame.get_parent() - scrolled = viewport.get_parent() - - adj = scrolled.get_vadjustment() - adj.set_value(adj.get_upper() - adj.get_page_size()) - - textview.show_all() - - - def output_tab_show_first_page(self): - - """Called by mainapp.TartubeApp.update_manager_start(). - - Switches to the first tab of the Output tab (not including the summary - tab, if it's open). - """ - - self.notebook.set_current_page(self.notebook_tab_dict['output']) - if not self.output_tab_summary_flag: - self.output_notebook.set_current_page(0) - else: - self.output_notebook.set_current_page(1) - - - def output_tab_reset_pages(self): - - """Called by mainapp.TartubeApp.download_manager_continue(), - .update_manager_start(), .refresh_manager_continue(), - .info_manager_start() and .tidy_manager_start(). - - At the start of an operation, empty the pages in the Output tab (if - allowed). - """ - - for textview in self.output_textview_dict.values(): - textbuffer = textview.get_buffer() - textbuffer.set_text('') - textview.show_all() - - - # (Errors/Warnings tab) - - - def errors_list_reset(self): - - """Can be called by anything. - - On the first call, sets up the widgets for the Errors List. On - subsequent calls, replaces those widgets and re-populates the list, - making error/warning messages visible or not, depending on settings. - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Extra items for the Errors/Warnings tab' - ) - - # Import the main application (for convenience) - app_obj = self.app_obj - - # If not called by self.setup_errors_tab()... - if self.errors_list_frame.get_child(): - self.errors_list_frame.remove(self.errors_list_frame.get_child()) - - # Set up the widgets - self.errors_list_scrolled = Gtk.ScrolledWindow() - self.errors_list_frame.add(self.errors_list_scrolled) - self.errors_list_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - self.errors_list_treeview = MultiDragDropTreeView() - self.errors_list_scrolled.add(self.errors_list_treeview) - # Allow multiple selection... - self.errors_list_treeview.set_can_focus(True) - selection = self.errors_list_treeview.get_selection() - selection.set_mode(Gtk.SelectionMode.MULTIPLE) - # ...and then set up drag and drop from the treeview to an external - # application (for example, an FFmpeg batch converter) - self.errors_list_treeview.enable_model_drag_source( - Gdk.ModifierType.BUTTON1_MASK, - [], - Gdk.DragAction.COPY, - ) - self.errors_list_treeview.drag_source_add_text_targets() - self.errors_list_treeview.connect( - 'drag-data-get', - self.on_errors_list_drag_data_get, - ) - - # Column list: - # 0: [str] [hide] Video's full file path (used for drag and drop) - # 1: [str] [hide] Media data object's URL (used for drag and drop) - # 2: [str] [hide] Media data object's name (used for drag and drop) - # 3: [pibxuf] Message type icon - # 4: [pixbuf] Media type icon - # 5: [str] [switch] Date and time string - # 6: [str] [switch] Date string - # 7: [str] [switch] Container name - # 8: [str] [switch] Video name - # 9: [str] [switch] Full message, formatted across several lines - # 10: [str] [switch] Shortened (one-line) message - # We don't use the media data object's .dbid, because the media data - # object may have been deleted (but the error message will still be - # visible) - # N.B. If this layout changes, then - # self.on_system_container_checkbutton_changed(), etc, must also be - # updated - for i, column_title in enumerate( - [ - 'hide', 'hide', 'hide', - '', '', - _('Time'), _('Time'), _('Container'), _('Video'), _('Message'), - _('Message') - ], - ): - if not column_title: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - '', - renderer_pixbuf, - pixbuf=i, - ) - self.errors_list_treeview.append_column(column_pixbuf) - column_pixbuf.set_resizable(False) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.errors_list_treeview.append_column(column_text) - if i < 3 \ - or i == 5 and not app_obj.system_msg_show_date_flag \ - or i == 6 and app_obj.system_msg_show_date_flag \ - or i == 7 and not app_obj.system_msg_show_container_flag \ - or i == 8 and not app_obj.system_msg_show_video_flag \ - or i == 9 and not app_obj.system_msg_show_multi_line_flag \ - or i == 10 and app_obj.system_msg_show_multi_line_flag: - column_text.set_visible(False) - else: - column_text.set_resizable(True) - - # Reset widgets - self.errors_list_liststore = Gtk.ListStore( - str, str, str, - GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, - str, str, str, str, str, str, - ) - self.errors_list_treeview.set_model(self.errors_list_liststore) - - # Populate the list with any errors/warnings already added - for mini_dict in self.error_list_buffer_list: - self.errors_list_insert_row(mini_dict) - - # Update the Errors/Warnings tab label with message counts - self.errors_list_refresh_label() - - # Make the changes visible - self.errors_list_frame.show_all() - - - def errors_list_add_operation_msg(self, media_data_obj, last_flag=False): - - """Can be called by any operation. - - When an operation generates error and/or warning messages, this - function is called to display them in the Errors List (if settings - permit), and to update IVs. - - Args: - - media_data_obj (media.Video, media.Channel or media.Playlist): The - media data object whose download (real or simulated) generated - the error/warning messages - - last_flag (bool): If True, only the last error/warning message is - displayed (useful in case this function might be called several - times for a single media data object) - - """ - - if last_flag and media_data_obj.error_list: - error_list = [ media_data_obj.error_list[-1] ] - else: - error_list = media_data_obj.error_list - - if last_flag and media_data_obj.warning_list: - warning_list = [ media_data_obj.warning_list[-1] ] - else: - warning_list = media_data_obj.warning_list - - # Create a new row for every error and warning message - # Use the same time on each - time_str = datetime.datetime.today().strftime('%x %X') - local = utils.get_local_time() - short_time_str = str(local.strftime('%H:%M:%S')) - - for msg in error_list: - mini_dict = self.errors_list_prepare_operation_row( - media_data_obj, - 'error', - msg, - time_str, - short_time_str, - ) - - # Add the row to the treeview - self.errors_list_insert_row(mini_dict) - - for msg in warning_list: - mini_dict = self.errors_list_prepare_operation_row( - media_data_obj, - 'warning', - msg, - time_str, - short_time_str, - ) - - # Add the row to the treeview - self.errors_list_insert_row(mini_dict) - - # Update the tab's label to show the number of warnings/errors visible - if self.visible_tab_num != self.notebook_tab_dict['errors']: - self.errors_list_refresh_label() - - - def errors_list_prepare_operation_row(self, media_data_obj, msg_type, msg, - time_str, short_time_str): - - """Called by self.errors_list_add_operation_msg() (only). - - Errors/Warnings sent for display in the Error List are stored in an IV, - so the list can be filtered as required. - - Prepares a dictionary of values for this error/warning message, then - adds it to the IV. - - Args: - - media_data_obj (media.Video, media.Channel or media.Playlist): The - media data object whose download (real or simulated) generated - the error/warning messages - - msg_type (str): 'error' or 'warning' - - msg (str): The text of the message itself - - time_str (str): The current date and time, as a string - - short_time_str (str): The current time, as a string - - Return values: - - The dictionary created - - """ - - # Prepare the mini-dictionary to be added to the IV - mini_dict = {} - - if msg_type == 'error': - mini_dict['msg_type'] = 'operation_error' - else: - mini_dict['msg_type'] = 'operation_warning' - - mini_dict['date_time'] = time_str - mini_dict['time'] = short_time_str - - if isinstance(media_data_obj, media.Video): - mini_dict['media_type'] = 'video' - # ('Dummy' media.Video objects don't have a parent) - if not media_data_obj.parent_obj: - mini_dict['container_name'] = '' - else: - mini_dict['container_name'] = utils.shorten_string( - media_data_obj.parent_obj.name, - self.long_string_max_len, - ) - mini_dict['video_name'] = utils.shorten_string( - media_data_obj.name, - self.long_string_max_len, - ) - elif isinstance(media_data_obj, media.Channel): - mini_dict['media_type'] = 'channel' - mini_dict['container_name'] = utils.shorten_string( - media_data_obj.name, - self.long_string_max_len, - ) - mini_dict['video_name'] = '' - else: - mini_dict['media_type'] = 'playlist' - mini_dict['container_name'] = utils.shorten_string( - media_data_obj.name, - self.long_string_max_len, - ) - mini_dict['video_name'] = '' - - mini_dict['msg'] = utils.tidy_up_long_string(msg) - mini_dict['short_msg'] = utils.shorten_string( - msg, - self.long_string_max_len, - ) - mini_dict['orig_msg'] = msg - - if self.visible_tab_num != self.notebook_tab_dict['errors']: - mini_dict['count_flag'] = True - else: - mini_dict['count_flag'] = False - - drag_path, drag_source, drag_name = self.get_media_drag_data_as_list( - media_data_obj, - ) - mini_dict['drag_path'] = drag_path - mini_dict['drag_source'] = drag_source - mini_dict['drag_name'] = drag_name - - # Sanity check: the treeview will not accept None values - for key in mini_dict.keys(): - if mini_dict[key] is None: - mini_dict[key] = '' - - # Update the IV - self.error_list_buffer_list.append(mini_dict) - - return mini_dict - - - def errors_list_add_system_msg(self, error_type, error_code, msg): - - """Can be called by anything. The quickest way is to call - mainapp.TartubeApp.system_error(), which acts as a wrapper for this - function. - - Display a system error message in the Errors List. - - N.B. Because Gtk is not thread safe, this function must always be - called from within GObject.timeout_add(). - - Args: - - error_type (str): 'error' or 'warning' - - error_code (int): An error code in the range 100-999 (see the - .system_error() function) - - msg (str): The system error message to display - - """ - - # Create a new row for every error and warning message - # Use the same time on each - time_str = datetime.datetime.today().strftime('%x %X') - local = utils.get_local_time() - short_time_str = str(local.strftime('%H:%M:%S')) - - # Prepare the mini-dictionary to be added to the IV - mini_dict = {} - - if error_type == 'error': - mini_dict['msg_type'] = 'system_error' - mini_dict['container_name'] = _('Tartube error') - mini_dict['video_name'] = '' - elif error_type == 'warning': - mini_dict['msg_type'] = 'system_warning' - mini_dict['container_name'] = _('Tartube warning') - mini_dict['video_name'] = '' - else: - # Failsafe - return - - mini_dict['media_type'] = '' - mini_dict['date_time'] = time_str - mini_dict['time'] = short_time_str - - # (When called by mainapp.TartubeApp.system_exception(), preserve the - # formatting of the raised exception; otherwise tidy it up) - if error_code == 901: - mini_dict['msg'] = msg - else: - mini_dict['msg'] = utils.tidy_up_long_string( - '#' + str(error_code) + ': ' + msg, - ) - - mini_dict['short_msg'] = utils.shorten_string( - '#' + str(error_code) + ': ' + msg, - self.long_string_max_len, - ) - mini_dict['orig_msg'] = msg - - if self.visible_tab_num != self.notebook_tab_dict['errors']: - mini_dict['count_flag'] = True - else: - mini_dict['count_flag'] = False - - mini_dict['drag_path'] = '' - mini_dict['drag_source'] = '' - mini_dict['drag_name'] = '' - - # Update the IV - self.error_list_buffer_list.append(mini_dict) - - # Add the row to the treeview - self.errors_list_insert_row(mini_dict) - - - def errors_list_insert_row(self, mini_dict): - - """Called by self.errors_list_reset(), - self.errors_list_add_operation_msg() and - self.errors_list_add_system_msg(). - - Called with an error/warning message to be displayed in the Errors - List. - - Decided whether the message should be filitered out or not, depending - on settings. If not, adds the message to the treeview. - - Args: - - mini_dict (dict): Dictionary of values (retrieved from - self.error_list_buffer_list) representing a single error or - warning message - - """ - - # Emergency fallback - if the error is generated before the - # Errors/Warnings tab has been set up, do nothing (this will avoid - # an infinite cycle of raised exceptions) - if self.errors_list_treeview is None: - return - - # Depending on settings, this row should be visible, or not - if ( - mini_dict['msg_type'] == 'system_error' \ - and not self.app_obj.system_error_show_flag - ) or ( - mini_dict['msg_type'] == 'system_warning' \ - and not self.app_obj.system_warning_show_flag - ) or ( - mini_dict['msg_type'] == 'operation_error' \ - and not self.app_obj.operation_error_show_flag - ) or ( - mini_dict['msg_type'] == 'operation_warning' \ - and not self.app_obj.operation_warning_show_flag - ): - # Not visible - return - - if self.error_list_filter_flag: - - if self.error_list_filter_text == '': - # Empty search pattern doesn't match anything - return - - lower_text = self.error_list_filter_text.lower() - if not ( - ( - self.error_list_filter_container_flag \ - and ( - ( - not self.error_list_filter_regex_flag \ - and mini_dict['container_name'].lower().find( - lower_text, - ) > -1 - ) or ( - self.error_list_filter_regex_flag \ - and re.search( - self.error_list_filter_text, - mini_dict['container_name'], - re.IGNORECASE, - ) - ) - ) - ) or ( - self.error_list_filter_video_flag \ - and ( - ( - not self.error_list_filter_regex_flag \ - and mini_dict['video_name'].lower().find( - lower_text, - ) > -1 - ) or ( - self.error_list_filter_regex_flag \ - and re.search( - self.error_list_filter_text, - mini_dict['video_name'], - re.IGNORECASE, - ) - ) - ) - ) or ( - self.error_list_filter_msg_flag \ - and ( - ( - not self.error_list_filter_regex_flag \ - and mini_dict['orig_msg'].lower().find( - lower_text, - ) > -1 - ) or ( - self.error_list_filter_regex_flag \ - and re.search( - self.error_list_filter_text, - mini_dict['orig_msg'], - re.IGNORECASE, - ) - ) - ) - ) - ): - return - - # Prepare the icons - if mini_dict['msg_type'] == 'system_error': - pixbuf = self.pixbuf_dict['error_small'] - pixbuf2 = self.pixbuf_dict['system_error_small'] - - elif mini_dict['msg_type'] == 'system_warning': - pixbuf = self.pixbuf_dict['warning_small'] - pixbuf2 = self.pixbuf_dict['system_warning_small'] - - else: - if mini_dict['msg_type'] == 'operation_error': - pixbuf = self.pixbuf_dict['error_small'] - elif mini_dict['msg_type'] == 'operation_warning': - pixbuf = self.pixbuf_dict['warning_small'] - else: - # Failsafe - return - - if mini_dict['media_type'] == 'video': - pixbuf2 = self.pixbuf_dict['video_small'] - elif mini_dict['media_type'] == 'channel': - pixbuf2 = self.pixbuf_dict['channel_small'] - elif mini_dict['media_type'] == 'playlist': - pixbuf2 = self.pixbuf_dict['playlist_small'] - else: - # Failsafe - return - - # Prepare the new row in the treeview, starting with the three - # hidden columns - row_list = [ - mini_dict['drag_path'], - mini_dict['drag_source'], - mini_dict['drag_name'], - pixbuf, - pixbuf2, - mini_dict['date_time'], - mini_dict['time'], - mini_dict['container_name'], - mini_dict['video_name'], - mini_dict['msg'], - mini_dict['short_msg'], - ] - - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.errors_list_treeview.show_all() - self.errors_list_liststore.append(row_list) - - # (Don't update the Errors/Warnings tab label if it's the - # visible tab) - if self.visible_tab_num != self.notebook_tab_dict['errors']: - self.errors_list_refresh_label() - - - def errors_list_refresh_label(self, reset_flag=False): - - """Called by self.errors_list_reset(), - .errors_list_add_operation_msg(), .errors_list_insert_row() and - .on_notebook_switch_page(). - - The label for the Errors/Warnings tab can show the number of errors/ - warnings currently visible in the tab, or not, depending on conditions. - - Args: - - reset_flag (bool): True when all errors/warnings should be marked - as old (so, when counting the number of errors/warnings to - display in the tab label, they are not counted) - - """ - - error_count = 0 - warning_count = 0 - - for mini_dict in self.error_list_buffer_list: - - if reset_flag: - mini_dict['count_flag'] = False - - elif mini_dict['count_flag']: - - if ( - mini_dict['msg_type'] == 'system_error' \ - or mini_dict['msg_type'] == 'operation_error' - ): - error_count += 1 - else: - warning_count += 1 - - text = _('_Errors') - if error_count: - text += ' (' + str(error_count) + ')' - - text += ' / ' + _('Warnings') - if warning_count: - text += ' (' + str(warning_count) + ')' - - self.errors_label.set_text_with_mnemonic(text) - - - def errors_list_apply_filter(self): - - """Called by mainapp.TartubeApp.on_button_apply_error_filter(). - - Applies the filter. - """ - - # Set IVs once, so that multiple calls to self.errors_list_insert_row() - # can use them - self.error_list_filter_flag = True - self.error_list_filter_text = self.error_list_entry.get_text() - self.error_list_filter_regex_flag \ - = self.error_list_togglebutton.get_active() - self.error_list_filter_container_flag \ - = self.error_list_container_checkbutton.get_active() - self.error_list_filter_video_flag \ - = self.error_list_video_checkbutton.get_active() - self.error_list_filter_msg_flag \ - = self.error_list_msg_checkbutton.get_active() - - # ... and update the Error List - self.errors_list_reset() - - # Sensitise widgets, as appropriate - self.error_list_filter_toolbutton.set_sensitive(False) - self.error_list_cancel_toolbutton.set_sensitive(True) - - - def errors_list_cancel_filter(self): - - """Called by mainapp.TartubeApp.on_button_apply_error_filter(). - - Applies the filter. - """ - - # Reset IVs... - self.error_list_filter_flag = False - self.error_list_filter_text = None - self.error_list_filter_regex_flag = False - self.error_list_filter_container_flag = False - self.error_list_filter_video_flag = False - self.error_list_filter_msg_flag = False - - # ... and update the Error List - self.errors_list_reset() - - # Sensitise widgets, as appropriate - self.error_list_filter_toolbutton.set_sensitive(True) - self.error_list_cancel_toolbutton.set_sensitive(False) - - - # Callback class methods - - - def on_video_index_add_classic(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Adds the channel/playlist URL to the textview in the Classic Mode tab. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel or media.Playlist): The clicked media - data object - - """ - - if isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.source: - return self.app_obj.system_error( - 226, - 'Callback request denied due to current conditions', - ) - - utils.add_links_to_textview( - self.app_obj, - [ media_data_obj.source ], - self.classic_textbuffer, - self.classic_mark_start, - self.classic_mark_end, - ) - - - def on_video_index_add_to_scheduled(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens a dialogue window giving a list of scheduled downloads, to which - the specified media data object can be added. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by a popup menu.' \ - + ' In the Videos tab, right-click a channel and select' \ - + ' Downloads > Add to scheduled download...' - ) - - # Check that at least one scheduled download exists, that doesn't - # already contain the specified media data object - available_list = [] - for scheduled_obj in self.app_obj.scheduled_list: - if not media_data_obj.dbid in scheduled_obj.media_list: - available_list.append(scheduled_obj.name) - - if not available_list: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'There are no scheduled downloads that don\'t already' \ - + ' contain the channel/playlist/folder', - ), - 'error', - 'ok', - None, # Parent window is main window - ) - - return - - available_list.sort() - - # Show the dialogue window - dialogue_win = ScheduledDialogue(self, media_data_obj, available_list) - dialogue_win.run() - choice = dialogue_win.choice - dialogue_win.destroy() - - # Check for the possibility that the media data object and/or - # scheduled download may have been deleted, since the dialogue window - # opened - if choice is not None \ - and media_data_obj.dbid in self.app_obj.container_reg_dict: - - # Find the selected scheduled download - match_obj = None - for this_obj in self.app_obj.scheduled_list: - if this_obj.name == choice: - match_obj = this_obj - break - - if match_obj: - match_obj.add_media(media_data_obj.dbid) - - - def on_video_index_apply_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Adds a set of download options (handled by an - options.OptionsManager object) to the specified media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj \ - or media_data_obj.options_obj\ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ): - return self.app_obj.system_error( - 227, - 'Callback request denied due to current conditions', - ) - - # If there are any options manager objects besides the General Options - # Manager, and the manager attached to the Classic Mode tab, then we - # need to prompt the user to select one - prompt_flag = False - for options_obj in self.app_obj.options_reg_dict.values(): - - if options_obj != self.app_obj.general_options_obj \ - and ( - self.app_obj.classic_options_obj == None \ - or options_obj != self.app_obj.classic_options_obj - ): - prompt_flag = True - break - - if not prompt_flag: - - # Apply (new) download options to the media data object - self.app_obj.apply_download_options(media_data_obj) - - # Open an edit window to show the options immediately - config.OptionsEditWin( - self.app_obj, - media_data_obj.options_obj, - ) - - else: - - # Prompt the user to specify new or existing download options - dialogue_win = ApplyOptionsDialogue(self) - response = dialogue_win.run() - # Get the specified options.OptionsManager object, before - # destroying the window - options_obj = dialogue_win.options_obj - clone_flag = dialogue_win.clone_flag - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - if clone_flag: - - options_obj = self.app_obj.clone_download_options( - options_obj, - ) - - # Apply the specified (or new) download options to the media - # data object - self.app_obj.apply_download_options( - media_data_obj, - options_obj, - ) - - # Open an edit window to show (new) options immediately - if not options_obj or clone_flag: - config.OptionsEditWin( - self.app_obj, - media_data_obj.options_obj, - ) - - - def on_video_index_check(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Check the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - download_manager_obj = self.app_obj.download_manager_obj - - if ( - self.app_obj.current_manager_obj \ - and not self.app_obj.download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.operation_classic_flag - ): - return self.app_obj.system_error( - 228, - 'Callback request denied due to current conditions', - ) - - if not download_manager_obj: - - # Start a new download operation to download this channel/playlist/ - # folder - self.app_obj.download_manager_start( - 'sim', - False, - [media_data_obj], - ) - - return - - # Download operation already in progress. Check that this channel/ - # playlist/folder is not already in the download list - for this_obj \ - in download_manager_obj.download_list_obj.download_item_dict.values(): - - if this_obj.media_data_obj == media_data_obj: - return - - # Add the channel/playlist/folder to the download list - download_item_obj = download_manager_obj.download_list_obj.create_item( - media_data_obj, - None, # media.Scheduled object - 'sim', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.progress_list_add_row( - download_item_obj.item_id, - media_data_obj, - ) - - # Update the main window's progress bar - self.app_obj.download_manager_obj.nudge_progress_bar() - - - def on_video_index_convert_container(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Converts a channel to a playlist, or a playlist to a channel. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 229, - 'Callback request denied due to current conditions', - ) - - self.app_obj.convert_remote_container(media_data_obj) - - - def on_video_index_custom_dl(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Custom download the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 230, - 'Callback request denied due to current conditions', - ) - - # Start a custom download operation - if not self.app_obj.general_custom_dl_obj.dl_by_video_flag \ - or not self.app_obj.general_custom_dl_obj.dl_precede_flag: - - self.app_obj.download_manager_start( - 'custom_real', - False, - [media_data_obj], - self.app_obj.general_custom_dl_obj, - ) - - else: - - self.app_obj.download_manager_start( - 'custom_sim', - False, - [media_data_obj], - self.app_obj.general_custom_dl_obj, - ) - - - def on_video_index_delete_container(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Deletes the channel, playlist or folder. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The clicked media data object - - """ - - self.app_obj.delete_container(media_data_obj) - - - def on_video_index_dl_no_db(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Set the media data object's flag to disable adding videos to Tartube's - database. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 231, - 'Callback request denied due to current conditions', - ) - - if not media_data_obj.dl_no_db_flag: - media_data_obj.set_dl_no_db_flag(True) - else: - media_data_obj.set_dl_no_db_flag(False) - - GObject.timeout_add( - 0, - self.video_index_update_row_icon, - media_data_obj, - ) - GObject.timeout_add( - 0, - self.video_index_update_row_text, - media_data_obj, - ) - - - def on_video_index_dl_disable(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Set the media data object's flag to disable checking and downloading. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 232, - 'Callback request denied due to current conditions', - ) - - if not media_data_obj.dl_disable_flag: - media_data_obj.set_dl_disable_flag(True) - else: - media_data_obj.set_dl_disable_flag(False) - - GObject.timeout_add( - 0, - self.video_index_update_row_icon, - media_data_obj, - ) - GObject.timeout_add( - 0, - self.video_index_update_row_text, - media_data_obj, - ) - - - def on_video_index_dl_sim(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Set the media data object's flag to force checking of the channel/ - playlist/folder (disabling actual downloads). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 233, - 'Callback request denied due to current conditions', - ) - - if not media_data_obj.dl_sim_flag: - media_data_obj.set_dl_sim_flag(True) - else: - media_data_obj.set_dl_sim_flag(False) - - GObject.timeout_add( - 0, - self.video_index_update_row_icon, - media_data_obj, - ) - GObject.timeout_add( - 0, - self.video_index_update_row_text, - media_data_obj, - ) - - - def on_video_index_download(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Download the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - download_manager_obj = self.app_obj.download_manager_obj - - if __main__.__pkg_no_download_flag__ \ - or ( - self.app_obj.current_manager_obj \ - and not self.app_obj.download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.operation_classic_flag - ): - return self.app_obj.system_error( - 234, - 'Callback request denied due to current conditions', - ) - - if not download_manager_obj: - - # Start a new download operation to download this channel/playlist/ - # folder - self.app_obj.download_manager_start( - 'real', - False, - [media_data_obj], - ) - - return - - # Download operation already in progress. Check that this channel/ - # playlist/folder is not already in the download list - for this_obj \ - in download_manager_obj.download_list_obj.download_item_dict.values(): - - if this_obj.media_data_obj == media_data_obj: - return - - # Add the channel/playlist/folder to the download list - download_item_obj = download_manager_obj.download_list_obj.create_item( - media_data_obj, - None, # media.Scheduled object - 'real', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.progress_list_add_row( - download_item_obj.item_id, - media_data_obj, - ) - - # Update the main window's progress bar - self.app_obj.download_manager_obj.nudge_progress_bar() - - - def on_video_index_drag_data_received(self, treeview, drag_context, x, y, \ - selection_data, info, timestamp): - - """Called from callback in self.video_index_reset(). - - Retrieve the source and destination media data objects, and pass them - on to a function in the main application. - - Args: - - treeview (Gtk.TreeView): The Video Index's treeview - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - x, y (int): Cell coordinates in the treeview - - selection_data (Gtk.SelectionData): Data from the dragged row - - info (int): Ignored - - timestamp (int): Ignored - - """ - - # Must override the usual Gtk handler - treeview.stop_emission('drag_data_received') - # Import the treeview's sorted model (for convenience) - model = self.video_index_sortmodel - - # Extract the drop destination - drop_info = treeview.get_dest_row_at_pos(x, y) - if drop_info is None: - return - - # Get the destination media data object - drop_path, drop_posn = drop_info[0], drop_info[1] - drop_iter = model.get_iter(drop_path) - dest_id = model[drop_iter][0] - if dest_id is None or dest_id == '': - return - - if self.video_catalogue_drag_list: - - # media.Video(s) are being dragged from the Video Catalogue into - # the Video Index - # To avoid any unforeseen problems, retrieve the list and reset the - # IV immediately - video_list = self.video_catalogue_drag_list - self.video_catalogue_drag_list = [] - - # Move the video(s) - self.app_obj.move_videos( - self.app_obj.media_reg_dict[dest_id], - video_list, - ) - - else: - - # A media.Channel, media.Playlist or media.Folder is being dragged - # into another container within the Video Index - # Get the dragged media data object - old_selection = self.video_index_treeview.get_selection() - (model, start_iter) = old_selection.get_selected() - drag_id = model[start_iter][0] - if drag_id is None or drag_id == '': - return - - # On MS Windows, the system helpfully deletes the dragged row - # before we've had a chance to show the confirmation dialogue - # Could redraw the dragged row, but then MS Windows helpfully - # selects the row beneath it, again before we've had a chance to - # intervene - # Only way around it is to completely reset the Video Index - # (and Video Catalogue) - if os.name == 'nt': - self.video_index_catalogue_reset(True) - - # Now proceed with the drag - self.app_obj.move_container( - self.app_obj.media_reg_dict[drag_id], - self.app_obj.media_reg_dict[dest_id], - ) - - - def on_video_index_drag_drop(self, treeview, drag_context, x, y, time): - - """Called from callback in self.video_index_reset(). - - Override the usual Gtk handler, and allow - self.on_video_index_drag_data_received() to collect the results of the - drag procedure. - - Args: - - treeview (Gtk.TreeView): The Video Index's treeview - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - x, y (int): Cell coordinates in the treeview - - time (int): A timestamp - - """ - - # Must override the usual Gtk handler - treeview.stop_emission('drag_drop') - - # The second of these lines cause the 'drag-data-received' signal to be - # emitted - target_list = drag_context.list_targets() - treeview.drag_get_data(drag_context, target_list[-1], time) - - - def on_video_index_edit_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Edit the download options (handled by an - options.OptionsManager object) for the specified media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj or not media_data_obj.options_obj: - return self.app_obj.system_error( - 235, - 'Callback request denied due to current conditions', - ) - - # Open an edit window - config.OptionsEditWin( - self.app_obj, - media_data_obj.options_obj, - ) - - - def on_video_index_empty_folder(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Empties the folder. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Folder): The clicked media data object - - """ - - # The True flag tells the function to empty the container, rather than - # delete it - self.app_obj.delete_container(media_data_obj, True) - - - def on_video_index_export(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Exports a summary of the database, containing the selected channel/ - playlist/folder and its descendants. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - self.app_obj.export_from_db( [media_data_obj] ) - - - def on_video_index_hide_folder(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Hides the folder in the Video Index. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - self.app_obj.mark_folder_hidden(media_data_obj, True) - - - def on_video_index_insert_videos(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Creates a dialogue window to insert one or more videos into a channel. - - This is useful when the new videos are unlisted. Videos can be added to - a folder in the usual way. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist): - The clicked media data object - - """ - - # (Code adapated from mainapp.TartubeApp.on_menu_add_video() ) - - dialogue_win = InsertVideoDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - text = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - False, - ) - - # ...and halt the timer, if running - if dialogue_win.clipboard_timer_id: - GObject.source_remove(dialogue_win.clipboard_timer_id) - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # Split text into a list of lines and filter out invalid URLs - video_list = [] - duplicate_list = [] - for line in text.splitlines(): - - for item in line.split(): - - # Remove leading/trailing whitespace - item = utils.strip_whitespace(item) - - # Perform checks on the URL. If it passes, remove leading/ - # trailing whitespace - if utils.check_url(item): - video_list.append(utils.strip_whitespace(item)) - - # Check everything in the list against other media.Video objects - # with the same parent folder - for item in video_list: - if media_data_obj.check_duplicate_video(item): - duplicate_list.append(item) - else: - self.app_obj.add_video(media_data_obj, item) - - # In the Video Index, select the parent media data object, which - # updates both the Video Index and the Video Catalogue - self.video_index_select_row(media_data_obj) - - # If any duplicates were found, inform the user - if duplicate_list: - dialogue_win = mainwin.DuplicateVideoDialogue( - self, - duplicate_list, - ) - dialogue_win.run() - dialogue_win.destroy() - - - def on_video_index_mark_archived(self, menu_item, media_data_obj, - only_child_videos_flag): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as archived. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - - """ - - self.app_obj.mark_container_archived( - media_data_obj, - True, - only_child_videos_flag, - ) - - - def on_video_index_mark_not_archived(self, menu_item, media_data_obj, - only_child_videos_flag): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all videos in this folder (and in any child channels, playlists - and folders) as not archived. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - - """ - - self.app_obj.mark_container_archived( - media_data_obj, - False, - only_child_videos_flag, - ) - - - def on_video_index_mark_bookmark(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as bookmarked. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - # In earlier versions of Tartube, this action could take a very long - # time (perhaps hours) - count = len(media_data_obj.child_list) - if count < self.mark_video_lower_limit: - - # The procedure should be quick - for child_obj in media_data_obj.child_list: - if isinstance(child_obj, media.Video): - self.app_obj.mark_video_bookmark(child_obj, True) - - elif count < self.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.app_obj.prepare_mark_video( - ['bookmark', True, media_data_obj], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - self.get_take_a_while_msg(media_data_obj, count), - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': ['bookmark', True, media_data_obj], - }, - ) - - - def on_video_index_mark_not_bookmark(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all videos in this folder (and in any child channels, playlists - and folders) as not bookmarked. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - # In earlier versions of Tartube, this action could take a very long - # time (perhaps hours) - count = len(media_data_obj.child_list) - if count < self.mark_video_lower_limit: - - # The procedure should be quick - for child_obj in media_data_obj.child_list: - if isinstance(child_obj, media.Video): - self.app_obj.mark_video_bookmark(child_obj, False) - - elif count < self.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.app_obj.prepare_mark_video( - ['bookmark', False, media_data_obj], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - self.get_take_a_while_msg(media_data_obj, count), - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': ['bookmark', False, media_data_obj], - }, - ) - - - def on_video_index_mark_favourite(self, menu_item, media_data_obj, - only_child_videos_flag): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as favourite. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - - """ - - self.app_obj.mark_container_favourite( - media_data_obj, - True, - only_child_videos_flag, - ) - - - def on_video_index_mark_not_favourite(self, menu_item, media_data_obj, - only_child_videos_flag): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all videos in this folder (and in any child channels, playlists - and folders) as not favourite. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - - """ - - self.app_obj.mark_container_favourite( - media_data_obj, - False, - only_child_videos_flag, - ) - - - def on_video_index_mark_missing(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all of the children of this channel or playlist as missing. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if isinstance(media_data_obj, media.Video) \ - or isinstance(media_data_obj, media.Folder): - return self.app_obj.system_error( - 236, - 'Callback request denied due to current conditions', - ) - - self.app_obj.mark_container_missing(media_data_obj, True) - - - def on_video_index_mark_not_missing(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all of the children of this channel or playlist as not missing. - This function can't be called for folders (except for the fixed - 'Missing Videos' folder). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if isinstance(media_data_obj, media.Video) \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj != self.app_obj.fixed_missing_folder - ): - return self.app_obj.system_error( - 237, - 'Callback request denied due to current conditions', - ) - - self.app_obj.mark_container_missing(media_data_obj, False) - - - def on_video_index_mark_new(self, menu_item, media_data_obj, - only_child_videos_flag): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all videos in this channel, playlist or folder (and in any child - channels, playlists and folders) as new (but only if they have been - downloaded). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - - """ - - self.app_obj.mark_container_new( - media_data_obj, - True, - only_child_videos_flag, - ) - - - def on_video_index_mark_not_new(self, menu_item, media_data_obj, - only_child_videos_flag): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all videos in this channel, playlist or folder (and in any child - channels, playlists and folders) as not new. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - - """ - - self.app_obj.mark_container_new( - media_data_obj, - False, - only_child_videos_flag, - ) - - - def on_video_index_mark_waiting(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as in the waiting list. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - # In earlier versions of Tartube, this action could take a very long - # time (perhaps hours) - count = len(media_data_obj.child_list) - if count < self.mark_video_lower_limit: - - # The procedure should be quick - for child_obj in media_data_obj.child_list: - if isinstance(child_obj, media.Video): - self.app_obj.mark_video_waiting(child_obj, True) - - elif count < self.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.app_obj.prepare_mark_video( - ['waiting', True, media_data_obj], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - self.get_take_a_while_msg(media_data_obj, count), - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': ['waiting', True, media_data_obj], - }, - ) - - - def on_video_index_mark_not_waiting(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Mark all videos in this folder (and in any child channels, playlists - and folders) as not in the waiting list. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - # In earlier versions of Tartube, this action could take a very long - # time (perhaps hours) - count = len(media_data_obj.child_list) - if count < self.mark_video_lower_limit: - - # The procedure should be quick - for child_obj in media_data_obj.child_list: - if isinstance(child_obj, media.Video): - self.app_obj.mark_video_waiting(child_obj, False) - - elif count < self.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.app_obj.prepare_mark_video( - ['waiting', False, media_data_obj], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - self.get_take_a_while_msg(media_data_obj, count), - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': ['waiting', False, media_data_obj], - }, - ) - - - def on_video_index_move_to_top(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Moves a channel, playlist or folder to the top level (in other words, - removes its parent folder). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - self.app_obj.move_container_to_top(media_data_obj) - - - def on_video_index_recent_videos_time(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens a dialogue window so the user can set the time after which - videos are removed from the 'Recent videos' folder. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Show the dialogue window - dialogue_win = RecentVideosDialogue(self, media_data_obj) - dialogue_win.run() - - if dialogue_win.radiobutton.get_active(): - choice = 0 - else: - choice = dialogue_win.spinbutton.get_value() - - dialogue_win.destroy() - - self.app_obj.set_fixed_recent_folder_days(int(choice)) - - - def on_video_index_refresh(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Refresh the right-clicked media data object, checking the corresponding - directory on the user's filesystem against video objects in the - database. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 238, - 'Callback request denied due to current conditions', - ) - - # Start a refresh operation - self.app_obj.refresh_manager_start(media_data_obj) - - - def on_video_index_remove_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Removes a set of download options (handled by an - options.OptionsManager object) from the specified media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj \ - or not media_data_obj.options_obj: - return self.app_obj.system_error( - 239, - 'Callback request denied due to current conditions', - ) - - # Remove download options from the media data object - self.app_obj.remove_download_options(media_data_obj) - - - def on_video_index_remove_videos(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Empties all child videos of a folder object, but doesn't remove any - child channel, playlist or folder objects. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Folder): The clicked media data object - - """ - - for child_obj in media_data_obj.child_list: - if isinstance(child_obj, media.Video): - self.app_obj.delete_video(child_obj) - - - def on_video_index_rename_location(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Renames a channel, playlist or folder. Also renames the corresponding - directory in Tartube's data directory. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - self.app_obj.rename_container(media_data_obj) - - - def on_video_index_right_click(self, treeview, event): - - """Called from callback in self.video_index_reset(). - - When the user right-clicks an item in the Video Index, create a - context-sensitive popup menu. - - Args: - - treeview (Gtk.TreeView): The Video Index's treeview - - event (Gdk.EventButton): The event emitting the Gtk signal - - """ - - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - - # If the user right-clicked on empty space, the call to - # .get_path_at_pos returns None (or an empty list) - if not treeview.get_path_at_pos( - int(event.x), - int(event.y), - ): - return - - path, column, cellx, celly = treeview.get_path_at_pos( - int(event.x), - int(event.y), - ) - - tree_iter = self.video_index_sortmodel.get_iter(path) - if tree_iter is not None: - - # Pass the container's .dbid to the popup menu code - self.video_index_popup_menu( - event, - self.video_index_sortmodel[tree_iter][0], - ) - - - def on_video_index_selection_changed(self, selection): - - """Called from callback in self.video_index_reset(). - - Also called from callbacks in mainapp.TartubeApp.on_menu_test, - .on_button_switch_view() and .on_menu_add_video(). - - When the user clicks to select an item in the Video Index, call a - function to update the Video Catalogue. - - Args: - - selection (Gtk.TreeSelection): Data for the selected row - - """ - - (model, tree_iter) = selection.get_selected() - if tree_iter is not None: - if not model.iter_is_valid(tree_iter): - tree_iter = None - else: - dbid = model[tree_iter][0] - - # Don't update the Video Catalogue during certain procedures, such as - # removing a row from the Video Index (in which case, the flag will - # be set) - if not self.ignore_video_index_select_flag: - - if tree_iter is None: - self.video_index_current_dbid = None - self.video_catalogue_reset() - - else: - # Update IVs - self.video_index_current_dbid = dbid - media_data_obj = self.app_obj.media_reg_dict[dbid] - - # Expand the tree beneath the selected line, if allowed - if self.app_obj.auto_expand_video_index_flag: - if not self.video_index_treeview.row_expanded( - model.get_path(tree_iter), - ): - self.video_index_treeview.expand_row( - model.get_path(tree_iter), - self.app_obj.full_expand_video_index_flag, - ) - - else: - self.video_index_treeview.collapse_row( - model.get_path(tree_iter), - ) - - # Redraw the Video Catalogue, on the first page, and reset its - # scrollbars back to the top - self.video_catalogue_redraw_all( - dbid, - 1, # Display the first page - True, # Reset scrollbars - ) - - - def on_video_index_marker(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Toggle the Video Index marker for the specified media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - # (Using self.video_index_treestore) - tree_ref = self.video_index_row_dict[media_data_obj.dbid] - tree_path = tree_ref.get_path() - tree_iter = self.video_index_treestore.get_iter(tree_path) - - self.video_index_treestore[tree_path][4] \ - = not self.video_index_treestore[tree_path][4] - - # The media data object's .dbid is in column 0 - tree_iter = self.video_index_treestore.get_iter(tree_path) - dbid = self.video_index_treestore[tree_iter][0] - media_data_obj = self.app_obj.media_reg_dict[dbid] - - if not self.video_index_treestore[tree_path][4]: - - if media_data_obj.dbid in self.video_index_marker_dict: - del self.video_index_marker_dict[media_data_obj.dbid] - - else: - - self.video_index_marker_dict[media_data_obj.dbid] \ - = self.video_index_row_dict[media_data_obj.dbid] - - - def on_video_index_marker_toggled(self, renderer_toggle, sorted_path): - - """Called from callback in self.video_index_reset(). - - When the user toggles the marker checkbutton on a row, update the - treeview's model. - - Args: - - renderer_toggle (Gtk.CellRendererToggle): The widget clicked - - sorted_path (Gtk.TreePath): Path to the clicked row (in - self.video_index_sortmodel) - - """ - - # (Using self.video_index_sortmodel) - sorted_iter = self.video_index_sortmodel.get_iter(sorted_path) - dbid = self.video_index_sortmodel[sorted_iter][0] - media_data_obj = self.app_obj.media_reg_dict[dbid] - - # System folders cannot be marked - # Channels/playlists/folders for which checking and downloading is - # disabled can't be marked - if ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ) or media_data_obj.dl_disable_flag: - return - - # (Using self.video_index_treestore) - tree_ref = self.video_index_row_dict[media_data_obj.dbid] - tree_path = tree_ref.get_path() - tree_iter = self.video_index_treestore.get_iter(tree_path) - - self.video_index_treestore[tree_path][4] \ - = not self.video_index_treestore[tree_path][4] - - # The media data object's .dbid is in column 0 - tree_iter = self.video_index_treestore.get_iter(tree_path) - dbid = self.video_index_treestore[tree_iter][0] - media_data_obj = self.app_obj.media_reg_dict[dbid] - - # Update IVs - old_size = len(self.video_index_marker_dict) - if not self.video_index_treestore[tree_path][4]: - - if media_data_obj.dbid in self.video_index_marker_dict: - del self.video_index_marker_dict[media_data_obj.dbid] - - else: - - self.video_index_marker_dict[media_data_obj.dbid] \ - = self.video_index_row_dict[media_data_obj.dbid] - - if (old_size and not self.video_index_marker_dict) \ - or (not old_size and self.video_index_marker_dict): - # Update labels on the 'Check all' button, etc - # The True argument skips the check for the existence of a progress - # bar - self.hide_progress_bar(True) - - - def on_video_index_set_destination(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Sets (or resets) the alternative download destination, or the external - directory, for the selected channel, playlist or folder. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The clicked media data object - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by a popup menu.' \ - + ' In the Videos tab, right-click a channel and select' \ - + ' Downloads > Set download destination...' - ) - - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 240, - 'Cannot set the download destination of a video', - ) - - dialogue_win = SetDestinationDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - choice = dialogue_win.choice - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - if type(choice) == int: - - # 'choice' is a .dbid - if choice != media_data_obj.master_dbid: - media_data_obj.set_master_dbid(self.app_obj, choice) - - media_data_obj.set_external_dir(self.app_obj, None) - if media_data_obj.dbid \ - in self.app_obj.container_unavailable_dict: - self.app_obj.del_container_unavailable_dict( - media_data_obj.dbid, - ) - - else: - - # 'choice' is the full path to an external directory. If it - # doesn't exist, create it (and add the semaphore file) - if media_data_obj.set_external_dir(self.app_obj, choice): - - media_data_obj.set_master_dbid( - self.app_obj, - media_data_obj.dbid, - ) - - if media_data_obj.dbid \ - in self.app_obj.container_unavailable_dict: - self.app_obj.del_container_unavailable_dict( - media_data_obj.dbid, - ) - - else: - - if os.name == 'nt': - msg = _('The external folder is not available') - else: - msg = _('The external directory is not available') - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - msg, - 'error', - 'ok', - None, # Parent window is main window - ) - - # Update tooltips for this row - GObject.timeout_add( - 0, - self.video_index_update_row_tooltip, - media_data_obj, - ) - - - def on_video_index_set_nickname(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Sets (or resets) the nickname for the selected channel, playlist or - folder. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The clicked media data object - - """ - - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 241, - 'Cannot set the nickname of a video', - ) - - dialogue_win = SetNicknameDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - nickname = dialogue_win.entry.get_text() - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # If nickname is an empty string, then the call to .set_nickname() - # resets the .nickname IV to match the .name IV - media_data_obj.set_nickname(nickname) - - # Update the name displayed in the Video Index - GObject.timeout_add( - 0, - self.video_index_update_row_text, - media_data_obj, - ) - - - def on_video_index_set_url(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Sets (or resets) the URL for the selected channel or playlist. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist): The clicked media - data object - - """ - - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 242, - 'Cannot modify the URL of a video', - ) - - elif isinstance(media_data_obj, media.Folder): - return self.app_obj.system_error( - 243, - 'Cannot set the URL of a folder', - ) - - dialogue_win = SetURLDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - url = dialogue_win.entry.get_text() - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # Check the URL is valid, before updating the media.Video object - if url is None or not utils.check_url(url): - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('The URL is not valid'), - 'error', - 'ok', - None, # Parent window is main window - ) - - else: - - media_data_obj.set_source(url) - - - def on_video_index_show_destination(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens the sub-directory into which all files for the specified media - data object are downloaded (which might be the default sub-directory - for another media data object, if the media data object's .master_dbid - has been modified). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The clicked media data object - - """ - - if media_data_obj.external_dir is not None: - other_obj = media_data_obj - else: - other_obj = self.app_obj.media_reg_dict[media_data_obj.master_dbid] - - path = other_obj.get_actual_dir(self.app_obj) - utils.open_file(self.app_obj, path) - - - def on_video_index_show_location(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens the sub-directory into which all files for the specified media - data object are downloaded, by default (which might not be the actual - sub-directory, if the media data object's .master_dbid has been - modified). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - path = media_data_obj.get_default_dir(self.app_obj) - utils.open_file(self.app_obj, path) - - - def on_video_index_show_properties(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens an edit window for the media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 244, - 'Callback request denied due to current conditions', - ) - - # Open the edit window immediately - if isinstance(media_data_obj, media.Folder): - config.FolderEditWin(self.app_obj, media_data_obj) - else: - config.ChannelPlaylistEditWin(self.app_obj, media_data_obj) - - - def on_video_index_show_system_cmd(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens a dialogue window to show the system command that would be used - to download the clicked channel/playlist/folder. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Show the dialogue window - dialogue_win = SystemCmdDialogue(self, media_data_obj) - dialogue_win.run() - dialogue_win.destroy() - - - def on_video_index_tidy(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Perform a tidy operation on the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The clicked media data object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 245, - 'Callback request denied due to current conditions', - ) - - # Prompt the user to specify which actions should be applied to - # the media data object's directory - dialogue_win = TidyDialogue(self, media_data_obj) - response = dialogue_win.run() - - if response == Gtk.ResponseType.OK: - - # Retrieve user choices from the dialogue window - choices_dict = { - 'media_data_obj': media_data_obj, - 'corrupt_flag': dialogue_win.checkbutton.get_active(), - 'del_corrupt_flag': dialogue_win.checkbutton2.get_active(), - 'exist_flag': dialogue_win.checkbutton3.get_active(), - 'del_video_flag': dialogue_win.checkbutton4.get_active(), - 'del_others_flag': dialogue_win.checkbutton5.get_active(), - 'remove_no_url_flag': dialogue_win.checkbutton6.get_active(), - 'remove_duplicate_flag': \ - dialogue_win.checkbutton7.get_active(), - 'del_archive_flag': dialogue_win.checkbutton8.get_active(), - 'move_thumb_flag': dialogue_win.checkbutton9.get_active(), - 'del_thumb_flag': dialogue_win.checkbutton10.get_active(), - 'del_webp_flag': dialogue_win.checkbutton11.get_active(), - 'convert_webp_flag': dialogue_win.checkbutton12.get_active(), - 'move_data_flag': dialogue_win.checkbutton13.get_active(), - 'del_descrip_flag': dialogue_win.checkbutton14.get_active(), - 'del_json_flag': dialogue_win.checkbutton15.get_active(), - 'del_xml_flag': dialogue_win.checkbutton16.get_active(), - 'convert_ext_flag': dialogue_win.checkbutton17.get_active(), - } - - # Now destroy the window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # If nothing was selected, then there is nothing to do - selected_flag = False - for key in choices_dict.keys(): - if choices_dict[key]: - selected_flag = True - break - - if not selected_flag: - return - - # Prompt the user for confirmation, before deleting any files - if choices_dict['del_corrupt_flag'] \ - or choices_dict['del_video_flag'] \ - or choices_dict['del_archive_flag'] \ - or choices_dict['del_thumb_flag'] \ - or choices_dict['del_webp_flag'] \ - or choices_dict['del_descrip_flag'] \ - or choices_dict['del_json_flag'] \ - or choices_dict['del_xml_flag']: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Files cannot be recovered, after being deleted. Are you' \ - + ' sure you want to continue?', - ), - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'tidy_manager_start', - # Specified options - 'data': choices_dict, - }, - ) - - else: - - # Start the tidy operation now - self.app_obj.tidy_manager_start(choices_dict) - - - def on_video_catalogue_add_classic(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Adds the selected video's URL to the textview in the Classic Mode tab. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if media_data_obj.source: - - utils.add_links_to_textview( - self.app_obj, - [ media_data_obj.source ], - self.classic_textbuffer, - self.classic_mark_start, - self.classic_mark_end, - ) - - - def on_video_catalogue_add_classic_multi(self, menu_item, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Adds the selected videos' URLs to the textview in the Classic Mode tab. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - source_list = [] - for media_data_obj in media_data_list: - if media_data_obj.source: - source_list.append(media_data_obj.source) - - if source_list: - - utils.add_links_to_textview( - self.app_obj, - source_list, - self.classic_textbuffer, - self.classic_mark_start, - self.classic_mark_end, - ) - - - def on_video_catalogue_apply_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Adds a set of download options (handled by an - options.OptionsManager object) to the specified video object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if self.app_obj.current_manager_obj or media_data_obj.options_obj: - return self.app_obj.system_error( - 246, - 'Callback request denied due to current conditions', - ) - - # If there are any options manager objects besides the General Options - # Manager, and the manager attached to the Classic Mode tab, then we - # need to prompt the user to select one - prompt_flag = False - for options_obj in self.app_obj.options_reg_dict.values(): - - if options_obj != self.app_obj.general_options_obj \ - and ( - self.app_obj.classic_options_obj == None \ - or options_obj != self.app_obj.classic_options_obj - ): - prompt_flag = True - break - - if not prompt_flag: - - # Apply download options to the media data object - self.app_obj.apply_download_options(media_data_obj) - - # Open an edit window to show the options immediately - config.OptionsEditWin( - self.app_obj, - media_data_obj.options_obj, - ) - - else: - - # Prompt the user to specify new or existing download options - dialogue_win = ApplyOptionsDialogue(self) - response = dialogue_win.run() - # Get the specified options.OptionsManager object, before - # destroying the window - options_obj = dialogue_win.options_obj - clone_flag = dialogue_win.clone_flag - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - if clone_flag: - - options_obj = self.app_obj.clone_download_options( - options_obj, - ) - - # Apply the specified (or new) download options to the media - # data object - self.app_obj.apply_download_options( - media_data_obj, - options_obj, - ) - - # Open an edit window to show the options immediately - config.OptionsEditWin( - self.app_obj, - media_data_obj.options_obj, - ) - - - def on_video_catalogue_check(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Check the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - download_manager_obj = self.app_obj.download_manager_obj - - if ( - self.app_obj.current_manager_obj \ - and not download_manager_obj - ) or ( - download_manager_obj \ - and download_manager_obj.operation_classic_flag - ): - return self.app_obj.system_error( - 247, - 'Callback request denied due to current conditions', - ) - - if download_manager_obj: - - # Download operation already in progress. Add this video to its - # list - download_item_obj \ - = download_manager_obj.download_list_obj.create_item( - media_data_obj, - None, # media.Scheduled object - 'sim', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.progress_list_add_row( - download_item_obj.item_id, - media_data_obj, - ) - - # Update the main window's progress bar - self.app_obj.download_manager_obj.nudge_progress_bar() - - else: - - # Start a new download operation to download this video - self.app_obj.download_manager_start( - 'sim', - False, - [media_data_obj], - ) - - - def on_video_catalogue_check_multi(self, menu_item, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Check the right-clicked media data object(s). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - download_manager_obj = self.app_obj.download_manager_obj - - if ( - self.app_obj.current_manager_obj \ - and not download_manager_obj - ) or ( - download_manager_obj \ - and download_manager_obj.operation_classic_flag - ): - return self.app_obj.system_error( - 248, - 'Callback request denied due to current conditions', - ) - - if download_manager_obj: - - # Download operation already in progress. Add these video to its - # list - for media_data_obj in media_data_list: - download_item_obj \ - = download_manager_obj.download_list_obj.create_item( - media_data_obj, - None, # media.Scheduled object - 'sim', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.progress_list_add_row( - download_item_obj.item_id, - media_data_obj, - ) - - # Update the main window's progress bar - self.app_obj.download_manager_obj.nudge_progress_bar() - - else: - - # Start a new download operation to download these videos - self.app_obj.download_manager_start( - 'sim', - False, - media_data_list, - ) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_custom_dl(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Custom download the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 249, - 'Callback request denied due to current conditions', - ) - - # Start a custom download operation - if not self.app_obj.general_custom_dl_obj.dl_by_video_flag \ - or not self.app_obj.general_custom_dl_obj.dl_precede_flag: - - self.app_obj.download_manager_start( - 'custom_real', - False, - [media_data_obj], - self.app_obj.general_custom_dl_obj, - ) - - else: - - self.app_obj.download_manager_start( - 'custom_sim', - False, - [media_data_obj], - self.app_obj.general_custom_dl_obj, - ) - - - def on_video_catalogue_custom_dl_multi(self, menu_item, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Custom download the right-clicked media data objects(s). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 250, - 'Callback request denied due to current conditions', - ) - - # Start a download operation - if not self.app_obj.general_custom_dl_obj.dl_by_video_flag \ - or not self.app_obj.general_custom_dl_obj.dl_precede_flag: - - self.app_obj.download_manager_start( - 'custom_real', - False, - media_data_list, - self.app_obj.general_custom_dl_obj, - ) - - else: - - self.app_obj.download_manager_start( - 'custom_sim', - False, - media_data_list, - self.app_obj.general_custom_dl_obj, - ) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_delete_video(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Deletes the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if self.app_obj.show_delete_video_dialogue_flag: - - # Prompt the user for confirmation - dialogue_win = DeleteVideoDialogue(self, [ media_data_obj ]) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - if dialogue_win.button2.get_active(): - delete_file_flag = True - else: - delete_file_flag = False - - if dialogue_win.button3.get_active(): - show_win_flag = True - else: - show_win_flag = False - - # ...before destroying it - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # Update IVs - self.app_obj.set_delete_video_files_flag(delete_file_flag) - self.app_obj.set_show_delete_video_dialogue_flag(show_win_flag) - - # Delete the video - self.app_obj.delete_video(media_data_obj, delete_file_flag) - - else: - - # Delete the video without prompting - self.app_obj.delete_video( - media_data_obj, - self.app_obj.delete_video_files_flag, - ) - - - def on_video_catalogue_delete_video_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Deletes the right-clicked media data objects. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if self.app_obj.show_delete_video_dialogue_flag: - - # Prompt the user for confirmation - dialogue_win = DeleteVideoDialogue(self, media_data_list) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - if dialogue_win.button2.get_active(): - delete_file_flag = True - else: - delete_file_flag = False - - if dialogue_win.button3.get_active(): - show_win_flag = True - else: - show_win_flag = False - - # ...before destroying it - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # Update IVs - self.app_obj.set_delete_container_files_flag(delete_file_flag) - self.app_obj.set_show_delete_video_dialogue_flag(show_win_flag) - - # Delete the videos - for media_data_obj in media_data_list: - self.app_obj.delete_video(media_data_obj, delete_file_flag) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - else: - - # Delete the videos without prompting - for media_data_obj in media_data_list: - self.app_obj.delete_video( - media_data_obj, - self.app_obj.delete_video_files_flag, - ) - - - def on_video_catalogue_dl_and_watch(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Downloads a video and then opens it using the system's default media - player. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Can't download the video if it has no source, or if an update/ - # refresh/process operation has started since the popup menu was - # created - if not media_data_obj.dl_flag or not media_data_obj.source \ - or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.process_manager_obj: - - # Download the video, and mark it to be opened in the system's - # default media player as soon as the download operation is - # complete - # If a download operation is already in progress, the video is - # added to it - self.app_obj.download_watch_videos( [media_data_obj] ) - - - def on_video_catalogue_dl_and_watch_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Download the videos and then open them using the system's default media - player. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - # Only download videos which have a source URL - mod_list = [] - for media_data_obj in media_data_list: - if media_data_obj.source: - mod_list.append(media_data_obj) - - # Can't download the videos if none have no source, or if an update/ - # refresh/process operation has started since the popup menu was - # created - if mod_list \ - and not self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.process_manager_obj: - - # Download the videos, and mark them to be opened in the system's - # default media player as soon as the download operation is - # complete - # If a download operation is already in progress, the videos are - # added to it - self.app_obj.download_watch_videos(mod_list) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_download(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Download the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - download_manager_obj = self.app_obj.download_manager_obj - - if ( - self.app_obj.current_manager_obj \ - and not download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and download_manager_obj.operation_classic_flag - ) or media_data_obj.live_mode == 1: - return self.app_obj.system_error( - 251, - 'Callback request denied due to current conditions', - ) - - if download_manager_obj: - - # Download operation already in progress. Add this video to its - # list - download_item_obj \ - = download_manager_obj.download_list_obj.create_item( - media_data_obj, - None, # media.Scheduled object - 'real', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.progress_list_add_row( - download_item_obj.item_id, - media_data_obj, - ) - - # Update the main window's progress bar - self.app_obj.download_manager_obj.nudge_progress_bar() - - else: - - # Start a new download operation to download this video - self.app_obj.download_manager_start( - 'real', - False, - [media_data_obj], - ) - - - def on_video_catalogue_download_multi(self, menu_item, media_data_list, - live_wait_flag): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Download the right-clicked media data objects(s). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - live_wait_flag (bool): True if any of the videos in media_data_list - are livestreams that have not started; False otherwise - - """ - - download_manager_obj = self.app_obj.download_manager_obj - - if ( - self.app_obj.current_manager_obj \ - and not download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and download_manager_obj.operation_classic_flag - ) or live_wait_flag: - return self.app_obj.system_error( - 252, - 'Callback request denied due to current conditions', - ) - - if download_manager_obj: - - # Download operation already in progress. Add these videos to its - # list - for media_data_obj in media_data_list: - download_item_obj \ - = download_manager_obj.download_list_obj.create_item( - media_data_obj, - None, # media.Scheduled object - 'real', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.progress_list_add_row( - download_item_obj.item_id, - media_data_obj, - ) - - # Update the main window's progress bar - self.app_obj.download_manager_obj.nudge_progress_bar() - - else: - - # Start a new download operation to download this video - self.app_obj.download_manager_start( - 'real', - False, - media_data_list, - ) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_edit_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Edit the download options (handled by an - options.OptionsManager object) for the specified video object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if self.app_obj.current_manager_obj or not media_data_obj.options_obj: - return self.app_obj.system_error( - 253, - 'Callback request denied due to current conditions', - ) - - # Open an edit window - config.OptionsEditWin( - self.app_obj, - media_data_obj.options_obj, - ) - - - def on_video_catalogue_enforce_check(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Set the video object's flag to force checking (disabling an actual - downloads). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # (Don't allow the user to change the setting of - # media.Video.dl_sim_flag if the video is in a channel or playlist, - # since media.Channel.dl_sim_flag or media.Playlist.dl_sim_flag - # applies instead) - if self.app_obj.current_manager_obj \ - or not isinstance(media_data_obj.parent_obj, media.Folder): - return self.app_obj.system_error( - 254, - 'Callback request denied due to current conditions', - ) - - if not media_data_obj.dl_sim_flag: - media_data_obj.set_dl_sim_flag(True) - else: - media_data_obj.set_dl_sim_flag(False) - - GObject.timeout_add( - 0, - self.video_catalogue_update_video, - media_data_obj, - ) - - - def on_video_catalogue_fetch_formats(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Fetches a list of available video/audio formats for the specified - video, using an info operation. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Can't start an info operation if any type of operation has started - # since the popup menu was created - if media_data_obj.source \ - and not self.app_obj.current_manager_obj: - - # Fetch information about the video's available formats - self.app_obj.info_manager_start('formats', media_data_obj) - # Automatically switch to the Output tab, for convenience - if self.app_obj.auto_switch_output_flag: - self.output_tab_show_first_page() - - - def on_video_catalogue_fetch_subs(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Fetches a list of available subtitles for the specified video, using an - info operation. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Can't start an info operation if any type of operation has started - # since the popup menu was created - if media_data_obj.source \ - and not self.app_obj.current_manager_obj: - - # Fetch information about the video's available subtitles - self.app_obj.info_manager_start('subs', media_data_obj) - # Automatically switch to the Output tab, for convenience - if self.app_obj.auto_switch_output_flag: - self.output_tab_show_first_page() - - - def on_video_catalogue_finalise_livestream(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the specified video, which is a livestream whose download was - not completed, as a downloaded livestream, removing the .part from the - end of the video file. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - expect_path = media_data_obj.get_actual_path(self.app_obj) - part_path = expect_path + '.part' - self.app_obj.move_file_or_directory(part_path, expect_path) - - media_data_obj.set_file_from_path(expect_path) - self.app_obj.mark_video_downloaded(media_data_obj, True) - self.app_obj.mark_video_live(media_data_obj, 0) - - # Update the catalogue item - GObject.timeout_add( - 0, - self.video_catalogue_update_video, - media_data_obj, - ) - - - def on_video_catalogue_finalise_livestream_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Marks the specified videos, which are livestreams whose download was - not completed, as a downloaded livestreaj, removing the .part from the - end of the video file. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - for media_data_obj in media_data_list: - - expect_path = media_data_obj.get_actual_path(self.app_obj) - part_path = expect_path + '.part' - - if not media_data_obj.dl_flag \ - and ( - media_data_obj.live_mode == 2 \ - or ( - media_data_obj.live_mode == 0 \ - and media_data_obj.was_live_flag - ) - ) and not os.path.isfile(expect_path) \ - and os.path.isfile(part_path): - - self.app_obj.move_file_or_directory(part_path, expect_path) - - media_data_obj.set_file_from_path(expect_path) - self.app_obj.mark_video_downloaded(media_data_obj, True) - self.app_obj.mark_video_live(media_data_obj, 0) - - - def on_video_catalogue_livestream_toggle(self, menu_item, media_data_obj, - action): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Toggles one of five livestream action settings. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - action (str): 'notify', 'alarm', 'open', 'dl_start', 'dl_stop' - - """ - - # Update the IV - if action == 'notify': - if not media_data_obj.dbid \ - in self.app_obj.media_reg_auto_notify_dict: - self.app_obj.add_auto_notify_dict(media_data_obj) - else: - self.app_obj.del_auto_notify_dict(media_data_obj) - elif action == 'alarm': - if not media_data_obj.dbid \ - in self.app_obj.media_reg_auto_alarm_dict: - self.app_obj.add_auto_alarm_dict(media_data_obj) - else: - self.app_obj.del_auto_alarm_dict(media_data_obj) - elif action == 'open': - if not media_data_obj.dbid \ - in self.app_obj.media_reg_auto_open_dict: - self.app_obj.add_auto_open_dict(media_data_obj) - else: - self.app_obj.del_auto_open_dict(media_data_obj) - elif action == 'dl_start': - if not media_data_obj.dbid \ - in self.app_obj.media_reg_auto_dl_start_dict: - self.app_obj.add_auto_dl_start_dict(media_data_obj) - else: - self.app_obj.del_auto_dl_start_dict(media_data_obj) - elif action == 'dl_stop': - if not media_data_obj.dbid \ - in self.app_obj.media_reg_auto_dl_stop_dict: - self.app_obj.add_auto_dl_stop_dict(media_data_obj) - else: - self.app_obj.del_auto_dl_stop_dict(media_data_obj) - - # Update the catalogue item - GObject.timeout_add( - 0, - self.video_catalogue_update_video, - media_data_obj, - ) - - - def on_video_catalogue_mark_temp_dl(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Creates a media.Video object in the 'Temporary Videos' folder. The new - video object has the same source URL as the specified media_data_obj. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Can't mark the video for download if it has no source, or if an - # update/refresh/tidy/process operation has started since the popup - # menu was created - if media_data_obj.source \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj \ - and not self.app_obj.process_manager_obj: - - # Create a new media.Video object in the 'Temporary Videos' folder - # (but don't download anything now) - new_media_data_obj = self.app_obj.add_video( - self.app_obj.fixed_temp_folder, - media_data_obj.source, - ) - - if new_media_data_obj: - - # We can set the temporary video's name/description, if known - new_media_data_obj.set_cloned_name(media_data_obj) - # Remember the name of the original container object, for - # display in the Video catalogue - new_media_data_obj.set_orig_parent(media_data_obj.parent_obj) - - - def on_video_catalogue_mark_temp_dl_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Creates new media.Video objects in the 'Temporary Videos' folder. The - new video objects have the same source URL as the video objects in the - specified media_data_list. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - # Only download videos which have a source URL - mod_list = [] - for media_data_obj in media_data_list: - if media_data_obj.source: - mod_list.append(media_data_obj) - - # Can't mark the videos for download if they have no source, or if an - # update/refresh/tidy/process operation has started since the popup - # menu was created - if mod_list \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj \ - and not self.app_obj.process_manager_obj: - - for media_data_obj in mod_list: - - # Create a new media.Video object in the 'Temporary Videos' - # folder - new_media_data_obj = self.app_obj.add_video( - self.app_obj.fixed_temp_folder, - media_data_obj.source, - ) - - # We can set the temporary video's name/description, if known - new_media_data_obj.set_cloned_name(media_data_obj) - # Remember the name of the original container object, for - # display in the Video catalogue - if new_media_data_obj: - new_media_data_obj.set_orig_parent( - media_data_obj.parent_obj, - ) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_not_livestream(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the specified video as not a livestream after all. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Update the video - self.app_obj.mark_video_live( - media_data_obj, - 0, # Not a livestream - ) - - # Update the catalogue item - GObject.timeout_add( - 0, - self.video_catalogue_update_video, - media_data_obj, - ) - - - def on_video_catalogue_not_livestream_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Marks the specified videos as not livestreams after all. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - for media_data_obj in media_data_list: - if media_data_obj.live_mode: - self.app_obj.mark_video_live( - media_data_obj, - 0, # Not a livestream - ) - - - def on_video_catalogue_output_override(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Downloads (or re-downloads) the right-clicked media.Video object, - specifying a video name that overrides youtube-dl's output template. - - Based on code in self.on_video_catalogue_download() and - self.on_video_catalogue_re_download(). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Prompt the user - dialogue_win = OutputOverrideDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Get the user's choices, before destroying the window - name = dialogue_win.new_name - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - # Basic checks - download_manager_obj = self.app_obj.download_manager_obj - - if ( - self.app_obj.current_manager_obj \ - and not download_manager_obj - ) or ( - self.app_obj.download_manager_obj \ - and download_manager_obj.operation_classic_flag - ) or media_data_obj.live_mode == 1: - return self.app_obj.system_error( - 275, - 'Callback request denied due to current conditions', - ) - - if name is None or re.search(r'^\s*$', name): - return - - elif os.name != 'nt' and name in self.app_obj.illegal_name_mswin_list: - - self.dialogue_manager_obj.show_msg_dialogue( - _('The name \'{0}\' is not allowed').format(name), - 'error', - 'ok', - ) - - return - - # Update IVs - self.app_obj.add_temp_output_override_dict( - media_data_obj.dbid, - name, - ) - - # (Reset the video's nickname to match its name. This looks good when - # re-downloading a video, to which an output tempalte override had - # been applied) - media_data_obj.set_nickname(media_data_obj.name) - - # If the video is already downloaded... - if media_data_obj.dl_flag: - - # ...delete the files associated with the video - self.app_obj.delete_video_files(media_data_obj) - - # No download operation will start, if the media.Video object is - # marked as downloaded - self.app_obj.mark_video_downloaded(media_data_obj, False) - - # (Re-)download the video - if download_manager_obj: - - # Download operation already in progress. Add this video to its - # list - download_item_obj \ - = download_manager_obj.download_list_obj.create_item( - media_data_obj, - None, # media.Scheduled object - 'real', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.progress_list_add_row( - download_item_obj.item_id, - media_data_obj, - ) - - # Update the main window's progress bar - self.app_obj.download_manager_obj.nudge_progress_bar() - - else: - - # Start a new download operation to download this video - self.app_obj.download_manager_start( - 'real', - False, - [media_data_obj], - ) - - - def on_video_catalogue_page_entry_activated(self, entry): - - """Called from a callback in self.setup_videos_tab(). - - Switches to a different page in the Video Catalogue (or re-inserts the - current page number, if the user typed an invalid page number). - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - page_num = utils.strip_whitespace(entry.get_text()) - - if self.video_index_current_dbid is None \ - or not page_num.isdigit() \ - or int(page_num) < 1 \ - or int(page_num) > self.catalogue_toolbar_last_page: - # Invalid page number, so reinsert the number of the page that's - # actually visible - entry.set_text(str(self.catalogue_toolbar_current_page)) - - else: - # Switch to a different page - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - int(page_num), - ) - - - def on_video_catalogue_process_clip(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu() (only). - - Sends the right-clicked media.Video object to FFmpeg for - post-processing, first prompting the user for a set of timestamps which - are used to split the video into clips. - - Alternatively, instructs yt-dlp (when available) to download video - chapters, handling them as clips. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Prompt the user - dialogue_win = PrepareClipDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Get the user's choices, before destroying the window - clip_mode = dialogue_win.clip_mode - dl_mode = dialogue_win.dl_mode - start_stamp = dialogue_win.start_stamp - stop_stamp = dialogue_win.stop_stamp - clip_title = dialogue_win.clip_title - stamp_list = dialogue_win.stamp_list - start_dl_flag = dialogue_win.start_dl_flag - - dialogue_win.destroy() - - if not start_dl_flag: - return - - # Check the validity of any specified timestamps - regex = '^' + self.app_obj.timestamp_regex + '$' - if start_stamp is not None: - - if not re.search(regex, start_stamp) \ - or ( - stop_stamp is not None \ - and not re.search(regex, stop_stamp) - ) or not utils.timestamp_compare( - self.app_obj, - start_stamp, - stop_stamp, - ): - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid timestamp(s)'), - 'error', - 'ok', - ) - - return - - elif stamp_list: - - # Groups of 3, in the form [start, optional_stop, optional_title] - for mini_list in stamp_list: - - if not re.search(regex, mini_list[0]) \ - or ( - mini_list[1] is not None \ - and not re.search(regex, mini_list[1]) - ) or not utils.timestamp_compare( - self.app_obj, - mini_list[0], - mini_list[1], - ): - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid timestamp(s)'), - 'error', - 'ok', - ) - - return - - # Clip mode: 'downloader' to download clips using the downloader - # directly (currently only available for yt-dlp), 'ffmpeg' to - # download clips using FFmpeg, or 'create' to create clips using the - # already-downloaded video - # Download mode: 'chapters' to download chapters using yt-dlp directly - # (when available), 'single' to download/extract a single clip using - # specified timestamp(s), 'multiple' to download/extract multiple - # clips - if clip_mode == 'create': - - if dl_mode == 'single': - insert_list \ - = ['create', [start_stamp, stop_stamp, clip_title] ] - - elif dl_mode == 'multiple': - insert_list = ['create'] - for mini_list in stamp_list: - insert_list.append(mini_list) - - # Update IVs - self.app_obj.add_temp_stamp_buffer_dict( - media_data_obj.dbid, - insert_list, - ) - - # Start a process operation to split the clip from the already- - # downloaded video - self.app_obj.process_manager_start( - self.app_obj.ffmpeg_options_obj, - [media_data_obj], - ) - - else: - - if dl_mode == 'chapters': - insert_list = ['chapters'] - - elif dl_mode == 'single': - insert_list \ - = [clip_mode, [start_stamp, stop_stamp, clip_title] ] - - elif dl_mode == 'multiple': - insert_list = [clip_mode] - for mini_list in stamp_list: - insert_list.append(mini_list) - - # Update IVs - self.app_obj.set_video_timestamps_dl_mode(clip_mode) - self.app_obj.add_temp_stamp_buffer_dict( - media_data_obj.dbid, - insert_list, - ) - - # Start a (custom) download operation to download the clip. We - # don't need to specify a downloads.CustomDLManager in this case - self.app_obj.download_manager_start( - 'custom_real', - # Not called from .script_slow_timer_callback() - False, - [media_data_obj], - ) - - - def on_video_catalogue_process_clip_classic_mode(self, - media_data_obj=None): - - """Called from a callback in - mainapp.TartubeApp.on_button_classic_add_clips(). - - Also called by mainapp.TartubeApp.on_button_classic_clips(). - - Handles video clips in the Classic Mode tab, combining code from - self.on_video_catalogue_process_clip() and - self.classic_mode_tab_add_urls(). - - Opens the usual 'Create video clips' dialogue, then creates new dummy - media.Video objects for each item, and updates IVs. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video or None): When called in reponse to - clicking the 'Add clips' button, None. When called from the - popup menu in the Classic Progress List, a dummy media.Video - object - - """ - - # Prompt the user - dialogue_win = PrepareClipDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Get the user's choices, before destroying the window - clip_mode = dialogue_win.clip_mode - dl_mode = dialogue_win.dl_mode - start_stamp = dialogue_win.start_stamp - stop_stamp = dialogue_win.stop_stamp - clip_title = dialogue_win.clip_title - stamp_list = dialogue_win.stamp_list - start_dl_flag = dialogue_win.start_dl_flag - - classic_stamp_list = dialogue_win.classic_stamp_list - classic_url = dialogue_win.classic_url - classic_video_name = dialogue_win.classic_video_name - - dialogue_win.destroy() - - if not start_dl_flag: - return - - # Check the validity of any specified timestamps - regex = '^' + self.app_obj.timestamp_regex + '$' - if start_stamp is not None: - - if not re.search(regex, start_stamp) \ - or ( - stop_stamp is not None \ - and not re.search(regex, stop_stamp) - ) or not utils.timestamp_compare( - self.app_obj, - start_stamp, - stop_stamp, - ): - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid timestamp(s)'), - 'error', - 'ok', - ) - - return - - elif stamp_list: - - # Groups of 3, in the form [start, optional_stop, optional_title] - for mini_list in stamp_list: - - if not re.search(regex, mini_list[0]) \ - or ( - mini_list[1] is not None \ - and not re.search(regex, mini_list[1]) - ) or not utils.timestamp_compare( - self.app_obj, - mini_list[0], - mini_list[1], - ): - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid timestamp(s)'), - 'error', - 'ok', - ) - - return - - # Create the dummy media.Video object - if not media_data_obj: - - # Remove initial/final whitespace from the URL, and reject an - # invalid URL - classic_url = utils.strip_whitespace(classic_url) - if not utils.check_url(classic_url): - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid URL'), - 'error', - 'ok', - ) - - return - - # Get the specified download destination - tree_iter = self.classic_dest_dir_combo.get_active_iter() - model = self.classic_dest_dir_combo.get_model() - dest_dir = model[tree_iter][0] - - # Get the specified video/audio format, leaving the value as None - # if the 'Default' item is selected - tree_iter2 = self.classic_format_combo.get_active_iter() - model2 = self.classic_format_combo.get_model() - format_str = model2[tree_iter2][0] - # (Valid formats begin with whitespace) - if not re.search(r'^\s', format_str): - format_str = None - else: - format_str = re.sub(r'^\s*', '', format_str) - # (One last check for a valid video/audio format) - if not format_str in formats.VIDEO_FORMAT_LIST \ - and not format_str in formats.AUDIO_FORMAT_LIST: - format_str = None - - # Set the specified resolution, leaving the value as None if the - # 'Highest' item is selected - tree_iter3 = self.classic_resolution_combo.get_active_iter() - model3 = self.classic_resolution_combo.get_model() - resolution_str = model3[tree_iter3][0] - # (Selectable resolutions in the combo begin with whitespace) - if not re.search(r'^\s', resolution_str): - resolution_str = None - else: - resolution_str = utils.strip_whitespace(resolution_str) - # (One last check for a valid resolution) - if not resolution_str in formats.VIDEO_RESOLUTION_LIST: - resolution_str = None - - # If the combobox item is selected, we convert a downloaded video - # to the specified format with FFmpeg/AVConv. This is signified - # by adding 'convert_' to the beginning of the format string - tree_iter4 = self.classic_convert_combo.get_active_iter() - model4 = self.classic_convert_combo.get_model() - convert_str = model4[tree_iter4][0] - if format_str is not None \ - and convert_str == _('Convert to this format'): - format_str = 'convert_' + format_str - # The resolution, if specified, is added to the end of the format - # string - if resolution_str is not None: - if format_str is None: - format_str = resolution_str - else: - format_str += '_' + resolution_str - - # Create the dummy media.Video object. Dummy media.Video objects - # have negative .dbids, and are not added to the media data - # registry. The True argument tells the function to ignore the - # value of mainapp.TartubeApp.classic_sblock_flag - dummy_video_obj = self.classic_mode_tab_create_dummy_video( - classic_url, - dest_dir, - format_str, - True, - ) - - else: - - # Create a new dummy media.Video object, with the same properties - # as the existing one. The True argument tells the function to - # ignore the value of mainapp.TartubeApp.classic_sblock_flag - dummy_video_obj = self.classic_mode_tab_create_dummy_video( - media_data_obj.source, - media_data_obj.dummy_dir, - media_data_obj.dummy_format, - True, - ) - - # Set the dummy media.Video object's nickname, if the user provided - # one; utils.clip_prepare_title() and - # ClipDownloader.do_download_clips_with_downloader() will both look - # out for it, and use it in the video's file name - if classic_video_name is not None: - dummy_video_obj.set_nickname(classic_video_name) - - # Set the dummy media.Video object's timestamps, and start the - # download/process operation - if clip_mode == 'create': - - if dl_mode == 'single': - insert_list \ - = ['create', [start_stamp, stop_stamp, clip_title] ] - - elif dl_mode == 'multiple': - insert_list = ['create'] - for mini_list in stamp_list: - insert_list.append(mini_list) - - # Update IVs - self.app_obj.add_temp_stamp_buffer_dict( - dummy_video_obj.dbid, - insert_list, - ) - - # Start a process operation to split the clip from the already- - # downloaded video - self.app_obj.process_manager_start( - self.app_obj.ffmpeg_options_obj, - [dummy_video_obj], - ) - - else: - - if dl_mode == 'chapters': - insert_list = ['chapters'] - - elif dl_mode == 'single': - insert_list \ - = [clip_mode, [start_stamp, stop_stamp, clip_title] ] - - elif dl_mode == 'multiple': - insert_list = [clip_mode] - for mini_list in stamp_list: - insert_list.append(mini_list) - - # Update IVs - self.app_obj.set_video_timestamps_dl_mode(clip_mode) - self.app_obj.add_temp_stamp_buffer_dict( - dummy_video_obj.dbid, - insert_list, - ) - - - def on_video_catalogue_process_ffmpeg(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu() and - .results_list_popup_menu(). - - Sends the right-clicked media.Video object to FFmpeg for - post-processing. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Can't start a process operation if another operation has started - # since the popup menu was created, or if the video hasn't been - # downloaded - if not self.app_obj.current_manager_obj \ - and media_data_obj.file_name is not None: - - # (There is a lot of code, so use one function instead of two) - self.on_video_catalogue_process_ffmpeg_multi( - menu_item, - [ media_data_obj ], - ) - - - def on_video_catalogue_process_ffmpeg_multi(self, menu_item, \ - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - For efficiency, also called by - self.on_video_catalogue_process_ffmpeg(). - - Sends the right-clicked media.Video objects to FFmpeg for - post-processing. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by a popup menu.' \ - + ' In the Videos tab, right-click a video and select' \ - + ' Special > Process with FFmpeg...' - ) - - # Can't start a process operation if another operation has started - # since the popup menu was created - if self.app_obj.current_manager_obj: - return - - # Filter out any media.Video objects whose filename is not known (so - # cannot be processed) - mod_list = [] - for video_obj in media_data_list: - - if video_obj.file_name is not None: - mod_list.append(video_obj) - - if not mod_list: - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Only checked/downloaded videos can be processed by FFmpeg'), - 'error', - 'ok', - ) - - return - - # Create an edit window for the current FFmpegOptionsManager object. - # Supply it with the list of videos, so that the user can start the - # process operation from the edit window - config.FFmpegOptionsEditWin( - self.app_obj, - self.app_obj.ffmpeg_options_obj, - mod_list, - ) - - - def on_video_catalogue_process_slice(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu() (only). - - Sends the right-clicked media.Video object to FFmpeg for - post-processing, first prompting the user for a set of start/stop - times of slices to remove from the video. - - Alternatively, downloads the video with slices removed. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Prompt the user for start/stop times - dialogue_win = PrepareSliceDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Get the specified start/stop times, before destroying the window - slice_mode = dialogue_win.slice_mode - dl_mode = dialogue_win.dl_mode - start_time = utils.strip_whitespace(dialogue_win.start_time) - stop_time = utils.strip_whitespace(dialogue_win.stop_time) - slice_list = dialogue_win.slice_list - start_dl_flag = dialogue_win.start_dl_flag - - dialogue_win.destroy() - - if not start_dl_flag: - return - - # If the user clicked the button in the 'one slice' section, check the - # times are valid - if dl_mode == 'single': - - # Check times are valid - try: - start_time = float( - utils.timestamp_convert_to_seconds( - self.app_obj, - start_time, - ) - ) - - if stop_time is not None: - stop_time = float( - utils.timestamp_convert_to_seconds( - self.app_obj, - stop_time, - ) - ) - - except: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid start/stop times'), - 'error', - 'ok', - ) - - return - - if stop_time is not None and stop_time <= start_time: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid start/stop times'), - 'error', - 'ok', - ) - - return - - # Compile the mini-dictionary in the format described by - # media.Video.__init__() - mini_dict = { - 'category': 'sponsor', - 'action': 'skip', - 'start_time': start_time, - 'stop_time': stop_time, - 'duration': 0, - } - - insert_list = [mini_dict] - - else: - insert_list = slice_list - - - # Slice mode: 'ffmpeg' to remove slices using FFmpeg, or 'create' to - # remove slices using the already-downloaded video - # Download mode: 'single' to remove a single slice using specified - # timestamps/seconds, 'multiple' to remove multiple slices. Set to - # None when slice_mode is 'create' - if slice_mode == 'create': - - # Update IVs - insert_list.insert(0, 'create') - self.app_obj.add_temp_slice_buffer_dict( - media_data_obj.dbid, - insert_list, - ) - - # Start a process operation to remove the slices from the - # already-downloaded video - self.app_obj.process_manager_start( - self.app_obj.ffmpeg_options_obj, - [media_data_obj], - ) - - else: - - # Update IVs - insert_list.insert(0, 'default') - self.app_obj.add_temp_slice_buffer_dict( - media_data_obj.dbid, - insert_list, - ) - - # Start a (custom) download operation to download the sliced - # video. We don't need to specify a downloads.CustomDLManager - # in this case - self.app_obj.download_manager_start( - 'custom_real', - # Not called from .script_slow_timer_callback() - False, - [media_data_obj], - ) - - - def on_video_catalogue_re_download(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Re-downloads the right-clicked media data object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 255, - 'Callback request denied due to current conditions', - ) - - # Delete the files associated with the video - self.app_obj.delete_video_files(media_data_obj) - - # No download operation will start, if the media.Video object is marked - # as downloaded - self.app_obj.mark_video_downloaded(media_data_obj, False) - - # Now we're ready to start the download operation - self.app_obj.download_manager_start('real', False, [media_data_obj] ) - - - def on_video_catalogue_reload_metadata(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Reloads the .info.json file for the specified video, updating IVs in - the media.Video object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by a popup menu.' \ - + ' In the Videos tab, right-click a video and select' \ - + ' Special > Reload metadata...' - ) - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 256, - 'Callback request denied due to current conditions', - ) - - # (Use a confirmation dialogue in the same format as used by - # self.on_video_catalogue_reload_metadata_multi) - success_count = 0 - fail_count = 0 - - if media_data_obj.file_name is None \ - or not media_data_obj.check_actual_path_by_ext( - self.app_obj, - '.info.json', - ): - fail_count = 1 - - else: - - # (Just assume success, if the metadata file exists) - success_count = 1 - - # Extract video statistics from the metadata file - self.app_obj.update_video_from_json(media_data_obj) - - # Set the new file's size, duration, and so on. The True argument - # instructs the function to override existing values - if media_data_obj.dl_flag: - self.app_obj.update_video_from_filesystem( - media_data_obj, - media_data_obj.get_actual_path(self.app_obj), - True, - ) - - # Redraw the video (which serves as a confirmation, if anything has - # changed) - self.video_catalogue_update_video(media_data_obj) - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Files reloaded: {0}, not reloaded: {1}', - ).format(success_count, fail_count), - 'info', - 'ok', - None, # Parent window is main window - ) - - - def on_video_catalogue_reload_metadata_multi(self, menu_item, \ - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Reloads the .info.json file for the specified videos, updating IVs in - the media.Video objects. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if self.app_obj.current_manager_obj: - return - - # Filter out any media.Video objects whose filename is not known (so - # the .info.json file is also not known) - success_count = 0 - fail_count = 0 - for video_obj in media_data_list: - - if video_obj.file_name is None \ - or not video_obj.check_actual_path_by_ext( - self.app_obj, - '.info.json', - ): - fail_count += 1 - - else: - - # (Just assume success, if the metadata file exists) - success_count += 1 - - # Extract video statistics from the metadata file - self.app_obj.update_video_from_json(video_obj) - - # Set the new file's size, duration, and so on. The True - # argument instructs the function to override existing values - if video_obj.dl_flag: - self.app_obj.update_video_from_filesystem( - video_obj, - video_obj.get_actual_path(self.app_obj), - True, - ) - - # Redraw the video (which serves as a confirmation, if anything - # has changed) - self.video_catalogue_update_video(video_obj) - - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Files reloaded: {0}, not reloaded: {1}', - ).format(success_count, fail_count), - 'info', - 'ok', - None, # Parent window is main window - ) - - - def on_video_catalogue_remove_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Removes a set of download options (handled by an - options.OptionsManager object) from the specified video object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if self.app_obj.current_manager_obj or not media_data_obj.options_obj: - return self.app_obj.system_error( - 257, - 'Callback request denied due to current conditions', - ) - - # Remove download options from the media data object - media_data_obj.reset_options_obj() - - - def on_video_catalogue_size_entry_activated(self, entry): - - """Called from a callback in self.setup_videos_tab(). - - Sets the page size, and redraws the Video Catalogue (with the first - page visible). - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - size = utils.strip_whitespace(entry.get_text()) - - if size.isdigit() and int(size) > 0: - self.app_obj.set_catalogue_page_size(int(size)) - - # Need to completely redraw the video catalogue to take account of - # the new page size - if self.video_index_current_dbid is not None: - - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - 1, - ) - - else: - # Invalid page size, so reinsert the size that's already visible - entry.set_text(str(self.app_obj.catalogue_page_size)) - - - def on_video_catalogue_show_location(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Shows the actual sub-directory in which the specified video is stored - (which might be different from the default sub-directory, if the media - data object's .master_dbid has been modified). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - parent_obj = media_data_obj.parent_obj - other_obj = self.app_obj.media_reg_dict[parent_obj.master_dbid] - path = other_obj.get_actual_dir(self.app_obj) - utils.open_file(self.app_obj, path) - - - def on_video_catalogue_show_location_multi(self, menu_item, \ - media_data_list): - - """Called from a callback in self.results_list_popup_menu(). - - Shows the actual sub-directory in which the specified videos are stored - (which might be different from the default sub-directory, if the media - data object's .master_dbid has been modified). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - path_list = [] - - for media_data_obj in media_data_list: - - parent_obj = media_data_obj.parent_obj - other_obj = self.app_obj.media_reg_dict[parent_obj.master_dbid] - path = other_obj.get_actual_dir(self.app_obj) - - # Don't show duplicate download locations - if not path in path_list: - utils.open_file(self.app_obj, path) - path_list.append(path) - - - def on_video_catalogue_show_properties(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Opens an edit window for the video object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 258, - 'Callback request denied due to current conditions', - ) - - # Open the edit window immediately - config.VideoEditWin(self.app_obj, media_data_obj) - - - def on_video_catalogue_show_properties_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Opens an edit window for each video object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 259, - 'Callback request denied due to current conditions', - ) - - # Open the edit window immediately - for media_data_obj in media_data_list: - config.VideoEditWin(self.app_obj, media_data_obj) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_show_system_cmd(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Opens a dialogue window to show the system command that would be used - to download the clicked video. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Show the dialogue window - dialogue_win = SystemCmdDialogue(self, media_data_obj) - dialogue_win.run() - dialogue_win.destroy() - - - def on_video_catalogue_sort_combo_changed(self, combo): - - """Called from callback in self.setup_videos_tab(). - - In the Video Catalogue, set the sorting method for videos. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = self.catalogue_sort_combo.get_active_iter() - model = self.catalogue_sort_combo.get_model() - self.app_obj.set_catalogue_sort_mode(model[tree_iter][1]) - - # Redraw the Video Catalogue, switching to the first page - if self.video_index_current_dbid is not None: - - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_video_catalogue_thumb_combo_changed(self, combo): - - """Called from callback in self.setup_videos_tab(). - - In the Video Catalogue, when videos are arranged on a grid, set the - thumbnail size. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = self.catalogue_thumb_combo.get_active_iter() - model = self.catalogue_thumb_combo.get_model() - self.app_obj.set_thumb_size_custom(model[tree_iter][1]) - - # Redraw the Video Catalogue, retaining the current page (but only when - # in grid mode) - if self.video_index_current_dbid is not None \ - and self.app_obj.catalogue_mode_type == 'grid': - - self.video_catalogue_grid_check_size() - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - self.catalogue_toolbar_current_page, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_video_catalogue_temp_dl(self, menu_item, media_data_obj, \ - watch_flag=False): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Creates a media.Video object in the 'Temporary Videos' folder. The new - video object has the same source URL as the specified media_data_obj. - - Downloads the video and optionally opens it using the system's default - media player. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - watch_flag (bool): If True, the video is opened using the system's - default media player, after being downloaded - - """ - - # Can't download the video if it has no source, or if an update/ - # refresh/tidy/process operation has started since the popup menu was - # created - if media_data_obj.source \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj \ - and not self.app_obj.process_manager_obj: - - # Create a new media.Video object in the 'Temporary Videos' folder - new_media_data_obj = self.app_obj.add_video( - self.app_obj.fixed_temp_folder, - media_data_obj.source, - ) - - if new_media_data_obj: - - # We can set the temporary video's name/description, if known - new_media_data_obj.set_cloned_name(media_data_obj) - # Remember the name of the original container object, for - # display in the Video catalogue - if new_media_data_obj: - new_media_data_obj.set_orig_parent( - media_data_obj.parent_obj, - ) - - # Download the video. If a download operation is already in - # progress, the video is added to it - # Optionally open the video in the system's default media - # player - self.app_obj.download_watch_videos( - [new_media_data_obj], - watch_flag, - ) - - - def on_video_catalogue_temp_dl_multi(self, menu_item, - media_data_list, watch_flag=False): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Creates new media.Video objects in the 'Temporary Videos' folder. The - new video objects have the same source URL as the video objects in the - specified media_data_list. - - Downloads the videos and optionally opens them using the system's - default media player. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - watch_flag (bool): If True, the video is opened using the system's - default media player, after being downloaded - - """ - - # Only download videos which have a source URL - mod_list = [] - for media_data_obj in media_data_list: - if media_data_obj.source: - mod_list.append(media_data_obj) - - # Can't download the videos if none have no source, or if an update/ - # refresh/tidy/process operation has started since the popup menu was - # created - ready_list = [] - if mod_list \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj \ - and not self.app_obj.process_manager_obj: - - for media_data_obj in mod_list: - - # Create a new media.Video object in the 'Temporary Videos' - # folder - new_media_data_obj = self.app_obj.add_video( - self.app_obj.fixed_temp_folder, - media_data_obj.source, - ) - - if new_media_data_obj: - - ready_list.append(new_media_data_obj) - - # We can set the temporary video's name/description, if - # known - new_media_data_obj.set_cloned_name(media_data_obj) - # Remember the name of the original container object, for - # display in the Video catalogue - if new_media_data_obj: - new_media_data_obj.set_orig_parent( - media_data_obj.parent_obj, - ) - - if ready_list: - - # Download the videos. If a download operation is already in - # progress, the videos are added to it - # Optionally open the videos in the system's default media player - self.app_obj.download_watch_videos(ready_list, watch_flag) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_test_dl(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Prompts the user to specify a URL and youtube-dl options. If the user - specifies one or both, launches an info operation to test youtube-dl - using the specified values. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Can't start an info operation if any type of operation has started - # since the popup menu was created - if not self.app_obj.current_manager_obj: - - # Prompt the user for what should be tested - dialogue_win = TestCmdDialogue(self, media_data_obj.source) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - source = dialogue_win.entry.get_text() - options_string = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - False, - ) - - # ...before destroying it - dialogue_win.destroy() - - # If the user specified either (or both) a URL and youtube-dl - # options, then we can proceed - if response == Gtk.ResponseType.OK \ - and (re.search(r'\S', source) or re.search(r'\S', options_string)): - # Start the info operation, which issues the youtube-dl command - # with the specified options - self.app_obj.info_manager_start( - 'test_ytdl', - None, # No media.Video object in this case - source, # Use the source, if specified - options_string, # Use download options, if specified - ) - - - def on_video_catalogue_toggle_archived_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as archived or not archived. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if not media_data_obj.archive_flag: - media_data_obj.set_archive_flag(True) - else: - media_data_obj.set_archive_flag(False) - - GObject.timeout_add( - 0, - self.video_catalogue_update_video, - media_data_obj, - ) - - - def on_video_catalogue_toggle_archived_video_multi(self, menu_item, - archived_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as archived or not archived. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - archived_flag (bool): True to mark the videos as archived, False to - mark the videos as not archived - - media_data_list (list): List of one or more media.Video objects - - """ - - for media_data_obj in media_data_list: - media_data_obj.set_archive_flag(archived_flag) - GObject.timeout_add( - 0, - self.video_catalogue_update_video, - media_data_obj, - ) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_toggle_bookmark_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as bookmarked or not bookmarked. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if not media_data_obj.bookmark_flag: - self.app_obj.mark_video_bookmark(media_data_obj, True) - else: - self.app_obj.mark_video_bookmark(media_data_obj, False) - - - def on_video_catalogue_toggle_bookmark_video_multi(self, menu_item, - bookmark_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as bookmarked or not bookmarked. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - bookmark_flag (bool): True to mark the videos as bookmarked, False - to mark the videos as not bookmarked - - media_data_list (list): List of one or more media.Video objects - - """ - - for media_data_obj in media_data_list: - self.app_obj.mark_video_bookmark(media_data_obj, bookmark_flag) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_toggle_favourite_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as favourite or not favourite. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if not media_data_obj.fav_flag: - self.app_obj.mark_video_favourite(media_data_obj, True) - else: - self.app_obj.mark_video_favourite(media_data_obj, False) - - - def on_video_catalogue_toggle_favourite_video_multi(self, menu_item, - fav_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as favourite or not favourite. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - fav_flag (bool): True to mark the videos as favourite, False to - mark the videos as not favourite - - media_data_list (list): List of one or more media.Video objects - - """ - - for media_data_obj in media_data_list: - self.app_obj.mark_video_favourite(media_data_obj, fav_flag) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_toggle_missing_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as missing or not missing. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if not media_data_obj.missing_flag: - self.app_obj.mark_video_missing(media_data_obj, True) - else: - self.app_obj.mark_video_missing(media_data_obj, False) - - - def on_video_catalogue_toggle_missing_video_multi(self, menu_item, - missing_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as missing or not missing. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - missing_flag (bool): True to mark the videos as missing, False to - mark the videos as not missing - - media_data_list (list): List of one or more media.Video objects - - """ - - for media_data_obj in media_data_list: - self.app_obj.mark_video_missing(media_data_obj, missing_flag) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_toggle_new_video(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as new (unwatched) or not new (watched). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if not media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, True) - else: - self.app_obj.mark_video_new(media_data_obj, False) - - - def on_video_catalogue_toggle_new_video_multi(self, menu_item, - new_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as new (unwatched) or not new (watched). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - new_flag (bool): True to mark the videos as favourite, False to - mark the videos as not favourite - - media_data_list (list): List of one or more media.Video objects - - """ - - for media_data_obj in media_data_list: - self.app_obj.mark_video_new(media_data_obj, new_flag) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_toggle_waiting_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as in the waiting list or not in the waiting list. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - if not media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, True) - else: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_toggle_waiting_video_multi(self, menu_item, - waiting_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as in the waiting list or not in the waiting list. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - waiting_flag (bool): True to mark the videos as in the waiting - list, False to mark the videos as not in the waiting list - - media_data_list (list): List of one or more media.Video objects - - """ - - for media_data_obj in media_data_list: - self.app_obj.mark_video_waiting(media_data_obj, waiting_flag) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_watch_hooktube(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Watch a YouTube video on HookTube. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Launch the video - utils.open_file( - self.app_obj, - utils.convert_youtube_to_hooktube(media_data_obj.source), - ) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_watch_invidious(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Watch a YouTube video on Invidious. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Launch the video - utils.open_file( - self.app_obj, - utils.convert_youtube_to_invidious( - self.app_obj, - media_data_obj.source, - ), - ) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_watch_video(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Watch a video using the system's default media player, first checking - that a file actually exists. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Launch the video - self.app_obj.watch_video_in_player(media_data_obj) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_watch_video_multi(self, menu_item, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Watch the videos using the system's default media player, first - checking that the files actually exist. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - # Only watch videos which are marked as downloaded - for media_data_obj in media_data_list: - if media_data_obj.dl_flag: - - self.app_obj.watch_video_in_player(media_data_obj) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_video_catalogue_watch_website(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Watch a video on its primary website. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - """ - - # Launch the video - utils.open_file(self.app_obj, media_data_obj.source) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_watch_website_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Watch videos on their primary websites. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - # Only watch videos which have a source URL - for media_data_obj in media_data_list: - if media_data_obj.source is not None: - - # Launch the video - utils.open_file(self.app_obj, media_data_obj.source) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - # Standard de-selection of everything in the Video Catalogue - self.video_catalogue_unselect_all() - - - def on_progress_list_dl_last(self, menu_item, download_item_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Moves the selected media data object to the bottom of the - downloads.DownloadList, so it is assigned to the last available worker. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - download_item_obj (downloads.DownloadItem): The download item - object for the selected media data object - - """ - - # Check that, since the popup menu was created, the media data object - # hasn't been assigned a worker - for this_worker_obj in self.app_obj.download_manager_obj.worker_list: - if this_worker_obj.running_flag \ - and this_worker_obj.download_item_obj == download_item_obj \ - and this_worker_obj.downloader_obj is not None: - return - - # Assign this media data object to the last available worker - download_list_obj = self.app_obj.download_manager_obj.download_list_obj - download_list_obj.move_item_to_bottom(download_item_obj) - - # Change the row's icon to show that it will be checked/downloaded - # last - # (Because of the way the Progress List has been set up, borrowing from - # the design in youtube-dl-gui, reordering the rows in the list is - # not practial) - tree_path = Gtk.TreePath( - self.progress_list_row_dict[download_item_obj.item_id], - ) - - self.progress_list_liststore.set( - self.progress_list_liststore.get_iter(tree_path), - 3, - self.pixbuf_dict['arrow_down_small'], - ) - - - def on_progress_list_dl_next(self, menu_item, download_item_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Moves the selected media data object to the top of the - downloads.DownloadList, so it is assigned to the next available worker. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - download_item_obj (downloads.DownloadItem): The download item - object for the selected media data object - - """ - - # Check that, since the popup menu was created, the media data object - # hasn't been assigned a worker - for this_worker_obj in self.app_obj.download_manager_obj.worker_list: - if this_worker_obj.running_flag \ - and this_worker_obj.download_item_obj == download_item_obj \ - and this_worker_obj.downloader_obj is not None: - return - - # Assign this media data object to the next available worker - download_list_obj = self.app_obj.download_manager_obj.download_list_obj - download_list_obj.move_item_to_top(download_item_obj) - - # Change the row's icon to show that it will be checked/downloaded - # next - tree_path = Gtk.TreePath( - self.progress_list_row_dict[download_item_obj.item_id], - ) - - self.progress_list_liststore.set( - self.progress_list_liststore.get_iter(tree_path), - 3, - self.pixbuf_dict['arrow_up_small'], - ) - - - def on_progress_list_right_click(self, treeview, event): - - """Called from callback in self.setup_progress_tab(). - - When the user right-clicks an item in the Progress List, create a - context-sensitive popup menu. - - Args: - - treeview (Gtk.TreeView): The Progress List's treeview - - event (Gdk.EventButton): The event emitting the Gtk signal - - """ - - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - - # If the user right-clicked on empty space, the call to - # .get_path_at_pos returns None (or an empty list) - if not treeview.get_path_at_pos( - int(event.x), - int(event.y), - ): - return - - path, column, cellx, celly = treeview.get_path_at_pos( - int(event.x), - int(event.y), - ) - - tree_iter = self.progress_list_liststore.get_iter(path) - if tree_iter is not None: - self.progress_list_popup_menu( - event, - self.progress_list_liststore[tree_iter][0], - self.progress_list_liststore[tree_iter][1], - ) - - - def on_progress_list_stop_all_soon(self, menu_item): - - """Called from a callback in self.progress_list_popup_menu(). - - Halts checking/downloading the selected media data object, after the - current video check/download has finished. - - During the checking stage of a custom download (operation types - 'custom_sim' and 'classic_sim'), skips the remaining videos, and - proceeds directly to the download stage. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - """ - - # Check that, since the popup menu was created, the download operation - # hasn't finished - if not self.app_obj.download_manager_obj: - # Do nothing - return - - # Tell the download manager to continue downloading the current videos - # (if any), and then stop - self.app_obj.download_manager_obj.stop_download_operation_soon() - - - def on_progress_list_stop_now(self, menu_item, download_item_obj, - worker_obj, downloader_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Halts checking/downloading the selected media data object. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - download_item_obj (downloads.DownloadItem): The download item - object for the selected media data object - - worker_obj (downloads.DownloadWorker): The worker currently - handling checking/downloading this media data object - - downloader_obj (downloads.VideoDownloader or - downloads.StreamDownloader): The downloader handling checking/ - downloading this media data object - - """ - - # Check that, since the popup menu was created, the video downloader - # hasn't already finished checking/downloading the selected media - # data object - if not self.app_obj.download_manager_obj \ - or not worker_obj.running_flag \ - or worker_obj.download_item_obj != download_item_obj \ - or worker_obj.downloader_obj is None: - # Do nothing - return - - # Stop the video downloader (causing the worker to be assigned a new - # downloads.DownloadItem, if there are any left) - downloader_obj.stop() - - - def on_progress_list_stop_soon(self, menu_item, download_item_obj, - worker_obj, downloader_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Halts checking/downloading the selected media data object, after the - current video check/download has finished. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - download_item_obj (downloads.DownloadItem): The download item - object for the selected media data object - - worker_obj (downloads.DownloadWorker or None): The worker currently - handling checking/downloading this media data object (None if - the check/download hasn't started yet) - - downloader_obj (downloads.VideoDownloader, - downloads.StreamDownloader or None): The downloader handling - checking/downloading this media data object (None if the check/ - download hasn't started yet) - - """ - - download_manager_obj = self.app_obj.download_manager_obj - - # Check that, since the popup menu was created, the video downloader - # hasn't already finished checking/downloading the selected media - # data object - if not download_manager_obj or ( - worker_obj and ( - not worker_obj.running_flag \ - or worker_obj.download_item_obj != download_item_obj \ - or worker_obj.downloader_obj is None - ) - ): - # Do nothing - return - - if downloader_obj: - - # Media data object is checking/downloading now. Tell the video - # downloader to stop after the check/download has finished - # (If other workers are waiting to start, then they will) - downloader_obj.stop_soon() - - else: - - # Media data object is waiting to start checking/downloading - # Tell the download list to empty itself one the check/download - # starts, so that no more checks/downloads start after that - download_manager_obj.download_list_obj.set_final_item( - download_item_obj.item_id - ) - - - def on_progress_list_watch_hooktube(self, menu_item, media_data_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Opens the clicked video, which is a YouTube video, on the HookTube - website. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The corresponding media data object - - """ - - if isinstance(media_data_obj, media.Video): - - # Launch the video - utils.open_file( - self.app_obj, - utils.convert_youtube_to_hooktube(media_data_obj.source), - ) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_progress_list_watch_invidious(self, menu_item, media_data_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Opens the clicked video, which is a YouTube video, on the Invidious - website. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The corresponding media data object - - """ - - if isinstance(media_data_obj, media.Video): - - # Launch the video - utils.open_file( - self.app_obj, - utils.convert_youtube_to_invidious( - self.app_obj, - media_data_obj.source, - ), - ) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_progress_list_watch_website(self, menu_item, media_data_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Opens the clicked video's source URL in a web browser. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The corresponding media data object - - """ - - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.source: - - utils.open_file(self.app_obj, media_data_obj.source) - - - def on_results_list_delete_video(self, menu_item, media_data_obj, path): - - """Called from a callback in self.results_list_popup_menu(). - - Deletes the video, and removes a row from the Results List. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The video displayed on the clicked - row - - path (Gtk.TreePath): Path to the clicked row in the treeview - - """ - - # Delete the video - self.app_obj.delete_video(media_data_obj, True) - - # Remove the row from the Results List - tree_iter = self.results_list_liststore.get_iter(path) - self.results_list_liststore.remove(tree_iter) - - - def on_results_list_drag_data_get(self, treeview, drag_context, data, info, - time): - - """Called from callback in self.setup_progress_tab(). - - Set the data to be used when the user drags and drops rows from the - Results List to an external application (for example, an FFmpeg batch - converter). - - Args: - - treeview (Gtk.TreeView): The Results List treeview - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - data (Gtk.SelectionData): The object to be filled with drag data - - info (int): Info that has been registered with the target in the - Gtk.TargetList - - time (int): A timestamp - - """ - - # Get the selected media.Video object(s) - video_list = self.get_selected_videos_in_treeview( - self.results_list_treeview, - 0, # Column 0 contains the media.Video's .dbid - ) - - # Transfer to the external application a single string, containing one - # or more full file paths/URLs/video names, separated by newline - # characters - # If the path/URL/name isn't known for any videos, then an empty line - # is transferred - if info == 0: # TARGET_ENTRY_TEXT - data.set_text(self.get_video_drag_data(video_list), -1) - - - def on_results_list_right_click(self, treeview, event): - - """Called from callback in self.setup_progress_tab(). - - When the user right-clicks item(s) in the Results List, create a - context-sensitive popup menu. - - Args: - - treeview (Gtk.TreeView): The Results List's treeview - - event (Gdk.EventButton): The event emitting the Gtk signal - - """ - - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - - # If the user right-clicked on empty space, the call to - # .get_path_at_pos returns None (or an empty list) - if not treeview.get_path_at_pos( - int(event.x), - int(event.y), - ): - return - - path, column, cellx, celly = treeview.get_path_at_pos( - int(event.x), - int(event.y), - ) - - tree_iter = self.results_list_liststore.get_iter(path) - if tree_iter is not None: - self.results_list_popup_menu(event, path) - - - def on_errors_list_clear(self, button): - - """Called from callback in self.setup_errors_tab(). - - In the Errors/Warnings tab, when the user clicks the 'Clear the list' - button, clear the Errors List. - - Args: - - button (Gtk.Button): The clicked widget - - """ - - self.error_list_buffer_list = [] - self.errors_list_reset() - - - def on_errors_list_drag_data_get(self, treeview, drag_context, data, info, - time): - - """Called from callback in self.errors_list_reset(). - - Set the data to be used when the user drags and drops rows from the - Errors List to an external application (for example, an FFmpeg batch - converter). - - Args: - - treeview (Gtk.TreeView): The Errors List treeview - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - data (Gtk.SelectionData): The object to be filled with drag data - - info (int): Info that has been registered with the target in the - Gtk.TargetList - - time (int): A timestamp - - """ - - # For each selected line, retrieve values from the three hidden columns - text = '' - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for tree_path in path_list: - - tree_iter = model.get_iter(tree_path) - if tree_iter: - - file_path = model[tree_iter][0] - source = model[tree_iter][1] - name = model[tree_iter][2] - msg = model[tree_iter][9] - - # If the path, source and name are all empty strings, then it - # probably wasn't a media data object that generated the - # message on this line - if file_path != '' or source != '' or name != '': - - if self.app_obj.drag_error_separator_flag: - text += self.drag_drop_separator + '\n' - - if self.app_obj.drag_error_path_flag: - if file_path == '': - text += '(' + _('unknown path') + ')\n' - else: - text += file_path + '\n' - - if self.app_obj.drag_error_source_flag: - if source == '': - text += '(' + _('unknown URL') + ')\n' - else: - text += source + '\n' - - if self.app_obj.drag_error_name_flag: - if name == '': - text += self.app_obj.default_video_name + '\n' - else: - text += name + '\n' - - if self.app_obj.drag_error_msg_flag: - - # Strip newline characters; we want the whole message - # on a single line, on this occasion - # (name == '' should be impossible, but for - # completeness, we'll check it anyway) - if name == '': - text += '(' + _('unknown message') + ')\n' - else: - text += re.sub(r'\n+', ' ', msg) + '\n' - - # Transfer to the external application a single string, containing one - # or more full file paths/URLs/video names, separated by newline - # characters - if info == 0: # TARGET_ENTRY_TEXT - data.set_text(text, -1) - - - def on_classic_convert_combo_changed(self, combo): - - """Called from callback in self.setup_classic_mode_tab(). - - Update IVs. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = self.classic_convert_combo.get_active_iter() - model = self.classic_convert_combo.get_model() - text = utils.strip_whitespace(model[tree_iter][0]) - - # (Update the IV) - if text == _('Convert to this format'): - self.app_obj.set_classic_format_convert_flag(True) - else: - self.app_obj.set_classic_format_convert_flag(False) - - # (Update the banner at the top of the tab, according to current - # conditions) - self.update_classic_mode_tab_update_banner() - - - def on_classic_dest_dir_combo_changed(self, combo): - - """Called from callback in self.setup_classic_mode_tab(). - - In the combobox displaying destination directories, remember the most - recent directory specified by the user, so it can be restored when - Tartube restarts. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = self.classic_dest_dir_combo.get_active_iter() - model = self.classic_dest_dir_combo.get_model() - self.app_obj.set_classic_dir_previous(model[tree_iter][0]) - - - def on_classic_format_combo_changed(self, combo): - - """Called from callback in self.setup_classic_mode_tab(). - - In the combobox displaying video/audio formats, if the user selects the - 'Default' item, desensitise the second radio button. - - If the user selects the 'Video:' or 'Audio:' item, select the line - immediately below that (which should be a valid format). - - Update IVs. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Extra items for the \'Format\' drop-down' \ - + ' box in the Classic Mode tab' - ) - - tree_iter = self.classic_format_combo.get_active_iter() - model = self.classic_format_combo.get_model() - text = model[tree_iter][0] - - # (Dummy items in the combo) - default_item = _('Default') + ' ' - video_item = _('Video:') - audio_item = _('Audio:') - - if text == default_item: - self.classic_convert_combo.set_active(0) - self.classic_convert_combo.set_sensitive(False) - - else: - self.classic_convert_combo.set_sensitive(True) - - if text == video_item or text == audio_item: - self.classic_format_combo.set_active( - self.classic_format_combo.get_active() + 1, - ) - - # (Update the IV) - tree_iter = self.classic_format_combo.get_active_iter() - text = model[tree_iter][0] - # (Should only be possible to set the first of thse items, but we'll - # check anyway) - if text != default_item and text != video_item and text != audio_item: - # (Ignore the first two space characters) - self.app_obj.set_classic_format_selection(text[2:]) - else: - self.app_obj.set_classic_format_selection(None) - - # (If an audio format has been selected, then the resolution combo - # must be reset) - if self.app_obj.classic_format_selection is not None \ - and self.app_obj.classic_format_selection in formats.AUDIO_FORMAT_DICT: - self.classic_resolution_combo.set_active(0) - - # (Update the banner at the top of the tab, according to current - # conditions) - self.update_classic_mode_tab_update_banner() - - - def on_classic_livestream_checkbutton_toggled(self, checkbutton): - - """Called from callback in self.setup_classic_mode_tab(). - - Updates IVs. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - self.app_obj.set_classic_livestream_flag(checkbutton.get_active()) - - - def on_classic_menu_custom_dl_prefs(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Opens the system preferences window, at the tab for custom download - preferences. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - if self.app_obj.current_manager_obj: - - return self.app_obj.system_error( - 260, - 'Callback request denied due to current conditions', - ) - - # Open the system preferences window - config.SystemPrefWin(self.app_obj, 'custom_dl') - - - def on_classic_menu_edit_options(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Opens an edit window for the options.OptionsManager object currently - selected for use in the Classic Mode tab. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - if self.app_obj.current_manager_obj: - - return self.app_obj.system_error( - 261, - 'Callback request denied due to current conditions', - ) - - # Open an edit window - if self.app_obj.classic_options_obj is None: - - config.OptionsEditWin( - self.app_obj, - self.app_obj.general_options_obj, - ) - - else: - - config.OptionsEditWin( - self.app_obj, - self.app_obj.classic_options_obj, - ) - - - def on_classic_menu_set_options(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Sets the options.OptionsManager object for use in Classic Mode tab. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 262, - 'Callback request denied due to current conditions', - ) - - # Open the preferences window, at the tab showing a lot of download - # options. The user can choose one there - config.SystemPrefWin(self.app_obj, 'options') - - - def on_classic_menu_use_general_options(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Uses the General Options Manager in the Classic Mode tab, instead of - the other options.OptionsManager object currently set. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - if self.app_obj.current_manager_obj: - - return self.app_obj.system_error( - 263, - 'Callback request denied due to current conditions', - ) - - self.app_obj.remove_classic_download_options() - - - def on_classic_menu_toggle_auto_copy(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Toggles the auto copy/paste button in the Classic Mode tab. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - if not self.classic_auto_copy_flag: - - # Update IVs - self.classic_auto_copy_flag = True - - # Start a timer to periodically check the clipboard - self.classic_clipboard_timer_id = GObject.timeout_add( - self.classic_clipboard_timer_time, - self.classic_mode_tab_timer_callback, - ) - - else: - - # Update IVs - self.classic_auto_copy_flag = False - - # Stop the timer - GObject.source_remove(self.classic_clipboard_timer_id) - self.classic_clipboard_timer_id = None - - - def on_classic_menu_toggle_custom_dl(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Toggles custom downloads in the Classic Mode tab. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Extra items for the buttons at the bottom' \ - + ' of the Classic Mode tab' - ) - - if self.app_obj.current_manager_obj: - - return self.app_obj.system_error( - 264, - 'Callback request denied due to current conditions', - ) - - if not self.app_obj.classic_custom_dl_flag: - self.app_obj.set_classic_custom_dl_flag(True) - self.classic_download_button.set_label( - ' ' + _('Custom download all') + ' ', - ) - self.classic_download_button.set_tooltip_text( - _('Perform a custom download on the URLs above'), - ) - - else: - self.app_obj.set_classic_custom_dl_flag(False) - self.classic_download_button.set_label( - ' ' + _('Download all') + ' ', - ) - self.classic_download_button.set_tooltip_text( - _('Download the URLs above'), - ) - - - def on_classic_menu_toggle_one_click_dl(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Toggles the one-click download button in the Classic Mode tab. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - # Update IVs - if not self.classic_one_click_dl_flag: - self.classic_one_click_dl_flag = True - else: - self.classic_one_click_dl_flag = False - - - def on_classic_menu_toggle_remember_urls(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Toggles the setting to remember undownloaded URLs, when the config file - is saved. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - self.app_obj.toggle_classic_pending_flag() - - - def on_classic_menu_update_ytdl(self, menu_item): - - """Called from a callback in self.classic_popup_menu(). - - Starts an update operation. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - """ - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 265, - 'Callback request denied due to current conditions', - ) - - # Start the update operation - self.app_obj.update_manager_start('ytdl') - - - def on_classic_progress_list_drag_data_get(self, treeview, drag_context, - data, info, time): - - """Called from callback in self.setup_classic_mode_tab(). - - Set the data to be used when the user drags and drops rows from the - Classic Progress List to an external application (for example, an - FFmpeg batch converter). - - Args: - - treeview (Gtk.TreeView): The Classic Progress List treeview - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - data (Gtk.SelectionData): The object to be filled with drag data - - info (int): Info that has been registered with the target in the - Gtk.TargetList - - time (int): A timestamp - - """ - - # Get the selected dummy media.Video object(s) - video_list = self.get_selected_videos_in_classic_treeview() - - # Transfer to the external application a single string, containing one - # or more full file paths/URLs/video names, separated by newline - # characters - # If the path/URL/name isn't known for any videos, then an empty line - # is transferred - if info == 0 and video_list: # TARGET_ENTRY_TEXT - - data.set_text( - self.get_video_drag_data( - video_list, - True, # This is a dummy media.Video object - ), - -1, - ) - - - def on_classic_progress_list_from_popup(self, menu_item, menu_item_type, \ - video_list): - - """Called from a callback in self.classic_progress_list_popup_menu(). - - In the popup menu, some items duplicate the buttons at the bottom of - the tab. When items in the menu are selected, re-direct the request to - the code used by the buttons. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - menu_item_type (str): Identifies the menu item clicked, one of the - strings 'play', 'open', 'redownload', 'stop', 'clips', - 'ffmpeg', 'move_up', 'move_down', 'remove' - - video_list (list): List of media.Video objects to which the action - will apply - - """ - - # Re-direct the request to the button callbacks, supplying dummy - # action/par arguments (which are ignored anyway) - if menu_item_type == 'play': - self.app_obj.on_button_classic_play(None, None) - elif menu_item_type == 'open': - self.app_obj.on_button_classic_open(None, None) - elif menu_item_type == 'redownload': - self.app_obj.on_button_classic_redownload(None, None) - elif menu_item_type == 'stop': - self.app_obj.on_button_classic_stop(None, None) - elif menu_item_type == 'clips': - self.app_obj.on_button_classic_clips(None, None) - elif menu_item_type == 'ffmpeg': - self.app_obj.on_button_classic_ffmpeg(None, None) - elif menu_item_type == 'move_up': - self.app_obj.on_button_classic_move_up(None, None) - elif menu_item_type == 'move_down': - self.app_obj.on_button_classic_move_down(None, None) - elif menu_item_type == 'remove': - self.app_obj.on_button_classic_remove(None, None) - - - def on_classic_progress_list_get_cmd(self, menu_item, dummy_obj): - - """Called from a callback in self.classic_progress_list_popup_menu(). - - Copies the youtube-dl system command for the specified dummy - media.Video object to the clipboard. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The dummy media.Video object on the - clicked row - - """ - - # Generate the list of download options for the dummy media.Video - # object - options_parser_obj = options.OptionsParser(self.app_obj) - options_list = options_parser_obj.parse( - dummy_obj, - self.app_obj.general_options_obj, - 'classic', - ) - - # Obtain the system command used to download this media data object - cmd_list = utils.generate_ytdl_system_cmd( - self.app_obj, - dummy_obj, - options_list, - False, - True, # Classic Mode tab - ) - - # Copy it to the clipboard - if cmd_list: - char = ' ' - system_cmd = char.join(cmd_list) - - else: - system_cmd = '' - - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clipboard.set_text(system_cmd, -1) - - - def on_classic_progress_list_get_path(self, menu_item, dummy_obj): - - """Called from a callback in self.classic_progress_list_popup_menu(). - - Copies the full file path for the specified dummy media.Video object to - the clipboard. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The dummy media.Video object on the - clicked row - - """ - - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clipboard.set_text(dummy_obj.dummy_path, -1) - - - def on_classic_progress_list_get_url(self, menu_item, dummy_obj): - - """Called from a callback in self.classic_progress_list_popup_menu(). - - Copies the URL for the specified dummy media.Video object to the - clipboard. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The dummy media.Video object on the - clicked row - - """ - - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clipboard.set_text(dummy_obj.source, -1) - - - def on_classic_progress_list_reinsert_url(self, menu_item, dummy_list): - - """Called from a callback in self.classic_progress_list_popup_menu(). - - Re-inserts the selected rows' URLs into the list at the top of the - Classic Mode tab, then removes the selected rows from the Classic - Progress List and updates IVs. - - Based on code in self.classic_mode_tab_remove_rows(). - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - dummy_list (list): List of dummy media.Video objects on the - clicked row(s) - - """ - - # (Import IVs for convenience) - manager_obj = self.app_obj.download_manager_obj - - # Check each row in turn - source_list = [] - for dummy_video_obj in dummy_list: - - # If there is a current download operation, we need to update it - if manager_obj: - - # If this dummy media.Video object is the one being downloaded, - # halt the download - for worker_obj in manager_obj.worker_list: - - if worker_obj.running_flag \ - and worker_obj.download_item_obj \ - and worker_obj.download_item_obj.media_data_obj.dbid \ - == dummy_video_obj.dbid: - worker_obj.downloader_obj.stop() - - # Delete the dummy media.Video object - del self.classic_media_dict[dummy_video_obj.dbid] - - # Remove the row from the treeview - row_iter = self.classic_mode_tab_find_row_iter( - dummy_video_obj.dbid, - ) - - if row_iter: - self.classic_progress_liststore.remove(row_iter) - - # Prepare to re-insert the URL into the list at the top of the tab - if dummy_video_obj.source is not None: - source_list.append(dummy_video_obj.source) - - # Re-insert the URLS - if source_list: - utils.add_links_to_textview( - self.app_obj, - source_list, - self.classic_textbuffer, - self.classic_mark_start, - self.classic_mark_end, - ) - - - def on_classic_progress_list_right_click(self, treeview, event): - - """Called from callback in self.setup_classic_mode_tab(). - - When the user right-clicks an item in the Classic Progress List, opens - a context-sensitive popup menu. - - Args: - - treeview (Gtk.TreeView): The Results List's treeview - - event (Gdk.EventButton): The event emitting the Gtk signal - - """ - - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - - # If the user right-clicked on empty space, the call to - # .get_path_at_pos returns None (or an empty list) - if not treeview.get_path_at_pos( - int(event.x), - int(event.y), - ): - return - - path, column, cellx, celly = treeview.get_path_at_pos( - int(event.x), - int(event.y), - ) - - tree_iter = self.classic_progress_liststore.get_iter(path) - if tree_iter is not None: - self.classic_progress_list_popup_menu(event, path) - - - def on_classic_resolution_combo_changed(self, combo): - - """Called from callback in self.setup_classic_mode_tab(). - - Update IVs. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = self.classic_resolution_combo.get_active_iter() - model = self.classic_resolution_combo.get_model() - text = utils.strip_whitespace(model[tree_iter][0]) - - # (Dummy items in the combo) - ignore_me = _( - 'TRANSLATOR\'S NOTE: Highest video resolution' - ) - - default_item = _('Highest') - - # (Update the IV) - if text != default_item: - self.app_obj.set_classic_resolution_selection(text) - else: - self.app_obj.set_classic_resolution_selection(None) - - - def on_classic_sblock_checkbutton_toggled(self, checkbutton): - - """Called from callback in self.setup_classic_mode_tab(). - - Updates IVs. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - self.app_obj.set_classic_sblock_flag(checkbutton.get_active()) - - - def on_classic_textbuffer_changed(self, textbuffer): - - """Called from callback in self.setup_classic_mode_tab(). - - If the setting is enabled, start a download operation for any valid - URL(s), or add the URL(s) to an existing download operation. - - Args: - - textbuffer (Gtk.TextBuffer): The textbuffer for the modified - Gtk.TextView - - """ - - if self.classic_one_click_dl_flag \ - and not self.classic_auto_copy_check_flag: - - # (A second signal is received by this function, when the call to - # self.classic_mode_tab_add_urls() resets the textview. Setting - # this flag prevents a second call to that function, before the - # first one has finished) - self.classic_auto_copy_check_flag = True - url_list = self.classic_mode_tab_add_urls() - self.classic_auto_copy_check_flag = False - - if url_list and not self.app_obj.download_manager_obj: - self.classic_mode_tab_start_download() - - - def on_classic_textview_paste(self, textview): - - """Called from callback in self.setup_classic_mode_tab(). - - When the user copy-pastes URLs into the textview, insert an initial - newline character, so they don't have to continuously do that - themselves. - - Args: - - textview (Gtk.TextView): The clicked widget - - """ - - # (Don't bother, if the URLs are going to be downloaded immediately) - if not self.classic_one_click_dl_flag: - - text = self.classic_textbuffer.get_text( - self.classic_textbuffer.get_iter_at_mark( - self.classic_mark_start, - ), - self.classic_textbuffer.get_iter_at_mark( - self.classic_mark_end, - ), - # Don't include hidden characters - False, - ) - - # (Don't bother inserting the newline if the URLs are going to be - # sent straight to the download manager) - if not (re.search(r'^\S*$', text)) \ - and not (re.search(r'\n+\s*$', text)): - self.classic_textbuffer.set_text(text + '\n') - - - def on_bandwidth_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_progress_tab(). - - In the Progress tab, when the user sets the bandwidth limit, inform - mainapp.TartubeApp. The new setting is applied to the next download - job. - - Args: - - spinbutton (Gtk.SpinButton): The clicked widget - - """ - - if self.bandwidth_checkbutton.get_active(): - self.app_obj.set_bandwidth_default( - int(self.bandwidth_spinbutton.get_value()) - ) - - - def on_bandwidth_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_progress_tab(). - - In the Progress tab, when the user turns the bandwidth limit on/off, - inform mainapp.TartubeApp. The new setting is applied to the next - download job. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if self.bandwidth_checkbutton.get_active(): - - self.app_obj.set_bandwidth_apply_flag(True) - self.app_obj.set_bandwidth_default( - int(self.bandwidth_spinbutton.get_value()) - ) - - else: - - self.app_obj.set_bandwidth_apply_flag(False) - - - def on_custom_dl_menu_select(self, menu_item, media_data_list, uid): - - """Called from a callback in self.custom_dl_popup_menu(). - - Starts a custom download using the specified custom download manager. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of media.Video, media.Channel, - media.Playlist and media.Folder objects to download. If an - empty list, all media data objects are custom downloaded - - uid (int): Unique .uid of the downloads.CustomDLManager to use - - """ - - custom_dl_obj = self.app_obj.custom_dl_reg_dict[uid] - - if not custom_dl_obj.dl_by_video_flag \ - or not custom_dl_obj.dl_precede_flag: - - self.app_obj.download_manager_start( - 'custom_real', - False, # Not called by the slow timer - media_data_list, - custom_dl_obj, - ) - - else: - - self.app_obj.download_manager_start( - 'custom_sim', - False, # Not called by the slow timer - media_data_list, - custom_dl_obj, - ) - - - def on_delete_event(self, widget, event): - - """Called from callback in self.setup_win(). - - If the user click-closes the window, close to the system tray (if - required), rather than closing the application. - - Args: - - widget (mainwin.MainWin): The main window - - event (Gdk.Event): Ignored - - """ - - if self.app_obj.status_icon_obj \ - and self.app_obj.show_status_icon_flag \ - and self.app_obj.close_to_tray_flag \ - and self.is_visible(): - - # Close to the system tray - self.toggle_visibility() - return True - - else: - - # mainapp.TartubeApp.stop_continue() is not called, so let's save - # the config/database file right now - if not self.app_obj.disable_load_save_flag: - self.app_obj.save_config() - self.app_obj.save_db() - - # Allow the application to close as normal - return False - - - def on_delete_profile_menu_select(self, menu_item, profile_name): - - """Called from a callback in self.delete_profile_popup_submenu(). - - Deletes the specified profile - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - profile_name (str): The specified profile (a key in - mainapp.TartubeApp.profile_dict). - - """ - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Dialogue window, generated by main window' \ - + ' menu, Media > Profiles > Delete profile' - ) - - # Prompt for confirmation, before deleting - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'Are you sure you want to delete the profile \'{0}\'', - ).format(profile_name), - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'delete_profile', - # Specified options - 'data': profile_name, - }, - ) - - - def on_draw_blocked_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_videos_tab(). - - In the Videos tab, when the user toggles the checkbutton, enable/ - disable drawing blocked videos. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_catalogue_draw_blocked_flag(checkbutton.get_active()) - # Redraw the Video Catalogue - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_draw_downloaded_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_videos_tab(). - - In the Videos tab, when the user toggles the checkbutton, enable/ - disable drawing downloaded videos. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_catalogue_draw_downloaded_flag( - checkbutton.get_active(), - ) - # Redraw the Video Catalogue - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_draw_frame_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_videos_tab(). - - In the Videos tab, when the user toggles the checkbutton, enable/ - disable the visible frame around each video. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_catalogue_draw_frame_flag(checkbutton.get_active()) - # (No need to redraw the Video Catalogue, just to enable/disable the - # visible frame around each video) - if self.app_obj.catalogue_mode_type == 'complex': - - for catalogue_obj in self.video_catalogue_dict.values(): - catalogue_obj.enable_visible_frame( - self.app_obj.catalogue_draw_frame_flag, - ) - - elif self.app_obj.catalogue_mode_type == 'grid': - - for catalogue_obj in self.video_catalogue_dict.values(): - catalogue_obj.catalogue_gridbox.enable_visible_frame( - self.app_obj.catalogue_draw_frame_flag, - ) - - - def on_draw_icons_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_videos_tab(). - - In the Videos tab, when the user toggles the checkbutton, enable/ - disable drawing the status icons for each video. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_catalogue_draw_icons_flag(checkbutton.get_active()) - # (No need to redraw the Video Catalogue, just to make the status icons - # visible/invisible) - if self.app_obj.catalogue_mode_type != 'simple': - for catalogue_obj in self.video_catalogue_dict.values(): - catalogue_obj.update_status_images() - - - def on_draw_undownloaded_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_videos_tab(). - - In the Videos tab, when the user toggles the checkbutton, enable/ - disable drawing undownloaded videos. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_catalogue_draw_undownloaded_flag( - checkbutton.get_active(), - ) - # Redraw the Video Catalogue - self.video_catalogue_redraw_all( - self.video_index_current_dbid, - 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_filter_comment_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_videos_tab(). - - In the Videos tab, when the user toggles the checkbutton, enable/ - disable filtering by video comments. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_catalogue_filter_comment_flag( - checkbutton.get_active(), - ) - # (No need to redraw the Video Catalogue, just to make the status icons - # visible/invisible - if self.app_obj.catalogue_mode_type != 'simple': - for catalogue_obj in self.video_catalogue_dict.values(): - catalogue_obj.update_status_images() - - - def on_filter_descrip_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_videos_tab(). - - In the Videos tab, when the user toggles the checkbutton, enable/ - disable filtering by video description. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_catalogue_filter_descrip_flag( - checkbutton.get_active(), - ) - # (No need to redraw the Video Catalogue, just to make the status icons - # visible/invisible - if self.app_obj.catalogue_mode_type != 'simple': - for catalogue_obj in self.video_catalogue_dict.values(): - catalogue_obj.update_status_images() - - - def on_filter_name_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_videos_tab(). - - In the Videos tab, when the user toggles the checkbutton, enable/ - disable filtering by video name. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_catalogue_filter_name_flag(checkbutton.get_active()) - # (No need to redraw the Video Catalogue, just to make the status icons - # visible/invisible - if self.app_obj.catalogue_mode_type != 'simple': - for catalogue_obj in self.video_catalogue_dict.values(): - catalogue_obj.update_status_images() - - - def on_hide_finished_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_progress_tab(). - - Toggles hiding finished rows in the Progress List. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_progress_list_hide_flag(checkbutton.get_active()) - - - def on_notebook_switch_page(self, notebook, box, page_num): - - """Called from callback in self.setup_notebook(). - - The Errors / Warnings tab shows the number of errors/warnings in its - tab label. When the user switches to this tab, reset the tab label. - - Args: - - notebook (Gtk.Notebook): The main window's notebook, providing - several tabs - - box (Gtk.Box): The box in which the tab's widgets are placed - - page_num (int): The number of the newly-visible tab (the Videos tab - is number 0) - - """ - - self.visible_tab_num = page_num - - if page_num == self.notebook_tab_dict['output']: - - # Switching between tabs causes pages in the Output tab to scroll - # to the top. Make sure they're all scrolled back to the bottom - - # Take into account range()... - page_count = self.output_page_count + 1 - # ...take into account the summary page, if present - if self.output_tab_summary_flag: - page_count += 1 - - for page_num in range(1, page_count): - self.output_tab_scroll_visible_page(page_num) - - elif page_num == self.notebook_tab_dict['errors'] \ - and not self.app_obj.system_msg_keep_totals_flag: - - # Update the tab's label, marking all messages as not counting - # towards the total number of errors/warnings displayed in the - # future - self.errors_list_refresh_label(True) - - - def on_notify_desktop_clicked(self, notification, action_name, notify_id, \ - url): - - """Called from callback in self.notify_desktop(). - - When the user clicks the button in a desktop notification, open the - corresponding URL in the system's web browser. - - Args: - - notification: The Notify.Notification object - - action_name (str): 'action_click' - - notify_id (int): A key in self.notify_desktop_dict - - url (str): The URL to open - - """ - - utils.open_file(self.app_obj, url) - - # This callback isn't needed any more, so we don't need to retain a - # reference to the Notify.Notification - if notify_id in self.notify_desktop_dict: - del self.notify_desktop_dict[notify_id] - - - def on_notify_desktop_closed(self, notification, notify_id): - - """Called from callback in self.notify_desktop(). - - When the desktop notification (which includes a button) is closed, - we no longer need a reference to the Notify.Notification object, so - remove it. - - Args: - - notification: The Notify.Notification object - - notify_id (int): A key in self.notify_desktop_dict - - """ - - if notify_id in self.notify_desktop_dict: - del self.notify_desktop_dict[notify_id] - - - def on_num_worker_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_progress_tab(). - - In the Progress tab, when the user sets the number of simultaneous - downloads allowed, inform mainapp.TartubeApp, which in turn informs the - downloads.DownloadManager object. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if self.num_worker_checkbutton.get_active(): - - self.app_obj.set_num_worker_apply_flag(True) - self.app_obj.set_num_worker_default( - int(self.num_worker_spinbutton.get_value()) - ) - - else: - - self.app_obj.set_num_worker_apply_flag(False) - - - def on_num_worker_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_progress_tab(). - - In the Progress tab, when the user sets the number of simultaneous - downloads allowed, inform mainapp.TartubeApp, which in turn informs the - downloads.DownloadManager object. - - Args: - - spinbutton (Gtk.SpinButton): The clicked widget - - """ - - if self.num_worker_checkbutton.get_active(): - self.app_obj.set_num_worker_default( - int(self.num_worker_spinbutton.get_value()) - ) - - - def on_operation_error_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of operation error messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_operation_error_show_flag(checkbutton.get_active()) - self.errors_list_reset() - - - def on_operation_warning_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of operation warning messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_operation_warning_show_flag(checkbutton.get_active()) - self.errors_list_reset() - - - def on_output_notebook_switch_page(self, notebook, box, page_num): - - """Called from callback in self.setup_output_tab(). - - When the user switches between pages in the Output tab, scroll the - visible textview to the bottom (otherwise it gets confusing). - - Args: - - notebook (Gtk.Notebook): The Output tab's notebook, providing - several pages - - box (Gtk.Box): The box in which the page's widgets are placed - - page_num (int): The number of the newly-visible page (the first - page is number 0) - - """ - - # Output tab IVs number the first page as #1, and so on - self.output_tab_scroll_visible_page(page_num + 1) - - - def on_output_size_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_output_tab(). - - In the Output tab, when the user (dis)applies the maximum pages size, - inform mainapp.TartubeApp. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if self.output_size_checkbutton.get_active(): - - self.app_obj.set_output_size_apply_flag(True) - self.app_obj.set_output_size_default( - int(self.output_size_spinbutton.get_value()) - ) - - else: - - self.app_obj.set_output_size_apply_flag(False) - - - def on_output_size_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_output_tab(). - - In the Output tab, when the user sets the maximum page size, inform - mainapp.TartubeApp. - - Args: - - spinbutton (Gtk.SpinButton): The clicked widget - - """ - - if self.output_size_checkbutton.get_active(): - self.app_obj.set_output_size_default( - int(self.output_size_spinbutton.get_value()) - ) - - - def on_paned_size_allocate(self, widget, rect): - - """Called from callback in self.setup_videos_tab(). - - The size of the Video tab's slider affects the size of the Video - Catalogue grid (when visible). This function is called regularly; if - the slider has actually moved, then we need to check whether the grid - size needs to be changed. - - Args: - - widget (mainwin.MainWin): The widget the has been resized - - rect (Gdk.Rectangle): Object describing the window's new size - - """ - - if self.paned_last_width is None \ - or self.paned_last_width != rect.width: - - # Slider position has actually changed - self.paned_last_width = rect.width - - if self.video_index_current_dbid \ - and self.app_obj.catalogue_mode_type == 'grid': - - # Check whether the grid should be resized and, if so, resize - # it - self.video_catalogue_grid_check_size() - - - def on_reverse_results_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_progress_tab(). - - Toggles reversing the order of the Results List. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_results_list_reverse_flag(checkbutton.get_active()) - - - def on_switch_profile_menu_select(self, menu_item, profile_name): - - """Called from a callback in self.switch_profile_popup_submenu(). - - Switches to the specified profile. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - profile_name (str): The specified profile (a key in - mainapp.TartubeApp.profile_dict). - - """ - - self.switch_profile(profile_name) - - - def on_switch_view(self, menu_item, catalogue_mode, catalogue_mode_type): - - """Called from a callback in self.setup_main_menubar(). - - Switches to the new Video Catalogue mode. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - catalogue_mode (str): One of the recognised values for - mainapp.TartubeApp.catalogue_mode_list, e.g. - 'simple_hide_parent' - - catalogue_mode_type (str): The corresponding mode type, 'simple', - 'complex' or 'grid' - - """ - - self.app_obj.set_catalogue_mode(catalogue_mode, catalogue_mode_type) - - - def on_system_container_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of container names in the tab. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_system_msg_show_container_flag( - checkbutton.get_active(), - ) - - name_column = self.errors_list_treeview.get_column(7) - if not self.app_obj.system_msg_show_container_flag: - name_column.set_visible(False) - else: - name_column.set_visible(True) - - - def on_system_date_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of dates (as well as times) in the tab. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_system_msg_show_date_flag(checkbutton.get_active()) - - long_column = self.errors_list_treeview.get_column(5) - short_column = self.errors_list_treeview.get_column(6) - - if not self.app_obj.system_msg_show_date_flag: - long_column.set_visible(False) - short_column.set_visible(True) - else: - long_column.set_visible(True) - short_column.set_visible(False) - - - def on_system_error_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of system error messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_system_error_show_flag(checkbutton.get_active()) - self.errors_list_reset() - - - def on_system_multi_line_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of multi-line error/warning messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_system_msg_show_multi_line_flag( - checkbutton.get_active(), - ) - - long_column = self.errors_list_treeview.get_column(9) - short_column = self.errors_list_treeview.get_column(10) - - if not self.app_obj.system_msg_show_multi_line_flag: - long_column.set_visible(False) - short_column.set_visible(True) - else: - long_column.set_visible(True) - short_column.set_visible(False) - - - def on_system_video_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of video names in the tab. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_system_msg_show_video_flag(checkbutton.get_active()) - - name_column = self.errors_list_treeview.get_column(8) - if not self.app_obj.system_msg_show_video_flag: - name_column.set_visible(False) - else: - name_column.set_visible(True) - - - def on_system_warning_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of system warning messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_system_warning_show_flag(checkbutton.get_active()) - self.errors_list_reset() - - - def on_video_res_combobox_changed(self, combo): - - """Called from callback in self.setup_progress_tab(). - - In the Progress tab, when the user sets the video resolution limit, - inform mainapp.TartubeApp. The new setting is applied to the next - download job. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = self.video_res_combobox.get_active_iter() - model = self.video_res_combobox.get_model() - self.app_obj.set_video_res_default(model[tree_iter][0]) - - - def on_video_res_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_progress_tab(). - - In the Progress tab, when the user turns the video resolution limit - on/off, inform mainapp.TartubeApp. The new setting is applied to the - next download job. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - self.app_obj.set_video_res_apply_flag( - self.video_res_checkbutton.get_active(), - ) - - - def on_window_drag_data_received(self, widget, context, x, y, data, info, - time): - - """Called from callback in self.setup_win(). - - This function is required for detecting when the user drags and drops - videos (for example, from a web browser) into the main window. - - Args: - - widget (mainwin.MainWin): The widget into which something has been - dragged - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - x, y (int): Where the drop happened - - data (Gtk.SelectionData): The object to be filled with drag data - - info (int): Info that has been registered with the target in the - Gtk.TargetList - - time (int): A timestamp - - """ - - text = None - if info == 0: - text = data.get_text() - - if text is not None: - - # Hopefully, 'text' contains one or more valid URLs or paths to - # video/audio files - - # On MS Windows, drag and drop from an external application doesn't - # seem to work at all, so we don't have to worry about it - # On Linux, URLs are received as expected, but paths to media - # data files are received as 'file://PATH' - - # Split 'text' into two lists, and handle them separately. Filter - # out any duplicate paths or duplicate URLs. For URLs, eliminate - # any invalid URLs - path_list = [] - url_list = [] - duplicate_list = [] - - for line in text.splitlines(): - - for item in line.split(): - - # Remove leading/trailing whitespace - item = utils.strip_whitespace(item) - - match = re.search(r'^file\:\/\/(.*)', item) - if match: - - # (Only accept video/audio files with a supported file - # extension) - path = urllib.parse.unquote(match.group(1)) - name, ext = os.path.splitext(path) - # (Take account of the initial . in the extension) - if ext[1:] in formats.VIDEO_FORMAT_LIST: - - if not path in path_list: - path_list.append(path) - else: - duplicate_list.append(path) - - else: - - if not item in url_list: - if utils.check_url(item): - url_list.append(item) - else: - duplicate_list.append(item) - - # Decide where to add the video(s) - # If a suitable folder is selected in the Video Index, use - # that; otherwise, use 'Unsorted Videos' - # However, if the Classic Mode tab is selected, copy URL(s) into - # its textview (and ignore any file paths) - classic_tab = self.notebook_tab_dict['classic'] - if classic_tab is not None \ - and self.notebook.get_current_page == classic_tab \ - and url_list: - - # Classic Mode tab is selected. The final argument tells the - # called function to use that argument, instead of the - # clipboard - utils.add_links_to_textview_from_clipboard( - self.app_obj, - self.classic_textbuffer, - self.classic_mark_start, - self.classic_mark_end, - '\n'.join(url_list), - ) - - elif ( - classic_tab is None \ - or self.notebook.get_current_page != classic_tab - ) and (path_list or url_list): - - # Classic Mode tab is not selected, so behave as if the - # Videos tab is selected - parent_obj = None - current_dbid = self.video_index_current_dbid - if current_dbid is not None: - - parent_obj = self.app_obj.media_reg_dict[current_dbid] - if isinstance(parent_obj, media.Folder) \ - and parent_obj.priv_flag: - parent_obj = None - - if not parent_obj: - parent_obj = self.app_obj.fixed_misc_folder - - # Add videos by path - for path in path_list: - - # Check for duplicate media.Video objects in the same - # folder - if parent_obj.check_duplicate_video_by_path( - self.app_obj, - path, - ): - duplicate_list.append(path) - else: - new_video_obj = self.app_obj.add_video(parent_obj) - new_video_obj.set_file_from_path(path) - - # Add videos by URL - for url in url_list: - - # Check for duplicate media.Video objects in the same - # folder - if parent_obj.check_duplicate_video(url): - duplicate_list.append(url) - else: - self.app_obj.add_video(parent_obj, url) - - # In the Video Index, select the parent media data object, - # which updates both the Video Index and the Video Catalogue - self.video_index_select_row(parent_obj) - - # If any duplicates were found, inform the user - if duplicate_list: - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Duplicate URLs dragged into' \ - + ' the main window\'s Videos tab' - ) - - msg = _('The following items are duplicates:') - for line in duplicate_list: - msg += '\n\n' + line - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'warning', - 'ok', - ) - - # Without this line, the user's cursor is permanently stuck in drag - # and drop mode - context.finish(True, False, time) - - - def on_window_size_allocate(self, widget, rect): - - """Called from callback in self.setup_win(). - - The size of the window affects the size of the Video Catalogue grid - (when visible). This function is called regularly; if the window size - has actually changed, then we need to check whether the grid size needs - to be changed. - - Args: - - widget (mainwin.MainWin): The widget the has been resized - - rect (Gdk.Rectangle): Object describing the window's new size - - """ - - if self.win_last_width is None \ - or self.win_last_width != rect.width \ - or self.win_last_height != rect.height: - - # Window size has actually changed - self.win_last_width = rect.width - self.win_last_height = rect.height - - if self.video_index_current_dbid \ - and self.app_obj.catalogue_mode_type == 'grid': - - # Check whether the grid should be resized and, if so, resize - # it - self.video_catalogue_grid_check_size() - - - # (Callback support functions) - - - def get_media_drag_data_as_list(self, media_data_obj): - - """Called by self.errors_list_add_operation_msg(). - - When a media data object (video, channel or playlist) generates an - error, that error can be displayed in the Errors List. - - The user may want to drag-and-drop the error messages to an external - application, revealing information about the media data object that - generated the error (e.g. the URL of a video). However, the error - might still be visible after the media data object has been deleted. - - Therefore, we store any data that we might later want to drag-and-drop - in three hidden columns of the errors list. - - This function returns a list of three values, one for each column. Each - value may be an empty string or a useable value. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist): - The media data object that generated an error - - Return values: - - A list of three values, one for each hidden column. Each value is - a string (which might be empty): - - 1. Full file path for a video, or the full path to the - directory for a channel/playlist - 2. The media data object's source URL - 3. The media data object's name - - """ - - return_list = [] - - # Full file path - if not self.app_obj.drag_video_path_flag: - - return_list.append('') - - elif isinstance(media_data_obj, media.Video): - - if not media_data_obj.dummy_flag \ - and media_data_obj.file_name is not None: - - return_list.append( - media_data_obj.get_actual_path(self.app_obj), - ) - - elif media_data_obj.dummy_flag \ - and media_data_obj.dummy_path is not None: - return_list.append(media_data_obj.dummy_path) - - else: - - return_list.append('') - - else: - - return_list.append(media_data_obj.get_actual_dir(self.app_obj)) - - # Source URL. This function should not receive a media.Folder, but - # check for that possibility anyway - if isinstance(media_data_obj, media.Folder) \ - or media_data_obj.source is None: - return_list.append('') - else: - return_list.append(media_data_obj.source) - - # Name - return_list.append(media_data_obj.name) - - return return_list - - - def get_selected_videos_in_treeview(self, treeview, column): - - """Called by self.on_results_list_drag_data_get() and - .results_list_popup_menu(). - - Retrieves a list of media.Video objects, one for each selected line - in the treeview. - - Args: - - treeview (Gkt.TreeView): The treeview listing the videos - - column (int): The column containing the media.Video object's .dbid - - Return values: - - A list media.Video objects (may be an empty list) - - """ - - video_list = [] - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for tree_path in path_list: - - tree_iter = model.get_iter(tree_path) - if tree_iter: - - dbid = model[tree_iter][0] - - # (Guard against the possibility, that the video has been - # deleted in the meantime) - if dbid in self.app_obj.media_reg_dict: - media_data_obj = self.app_obj.media_reg_dict[dbid] - if isinstance(media_data_obj, media.Video): - video_list.append(media_data_obj) - - return video_list - - - def get_selected_videos_in_classic_treeview(self): - - """Called by self.on_results_list_drag_data_get() and - .classic_progress_list_popup_menu(). - - A modified version of self.get_selected_videos_in_treeview(), to fetch - a list of dummy media.Video objects, one for each selected line in the - Classic Progress List's treeview. - - Return values: - - A list media.Video objects (may be an empty list) - - """ - - video_list = [] - - selection = self.classic_progress_treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for tree_path in path_list: - - tree_iter = model.get_iter(tree_path) - if tree_iter: - - dbid = model[tree_iter][0] - - # (Guard against the very unlikely possibility that the row - # has been removed in the meantime) - if dbid in self.classic_media_dict: - media_data_obj = self.classic_media_dict[dbid] - if isinstance(media_data_obj, media.Video): - video_list.append(media_data_obj) - - return video_list - - - def get_take_a_while_msg(self, media_data_obj, count): - - """Called by self.on_video_index_mark_bookmark(), - .on_video_index_mark_not_bookmark(), .on_video_index_mark_waiting(), - .on_video_index_mark_not_waiting(). - - Composes a (translated) message to display in a dialogue window. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - media data object to be marked/unmarked - - count (int): The number of child media data objects in the - specified channel, playlist or folder - - """ - - media_type = media_data_obj.get_type() - if media_type == 'channel': - - msg = _( - 'The channel contains {0} items, so this action may take' \ - + ' a while', - ).format(str(count)) - - elif media_type == 'playlist': - - msg = _( - 'The playlist contains {0} items, so this action may take' \ - + ' a while', - ).format(str(count)) - - else: - - msg = _( - 'The folder contains {0} items, so this action may take' \ - + ' a while', - ).format(str(count)) - - msg += '\n\n' + _('Are you sure you want to continue?') - - return msg - - - def get_video_drag_data(self, video_list, dummy_flag=False): - - """Called by self.on_results_list_drag_data_get(), - .on_classic_progress_list_drag_data_get(), - CatalogueRow.on_drag_data_get() and - CatalogueGridBox.on_drag_data_get(). - - Returns the data to be transferred to an external application, when the - user drags a video there. - - Args: - - video_list (list): List of media.Video objects being dragged - - dummy_flag (bool): If True, these are dummy media.Video objects - (which are created by the Classic Mode tab, and are not stored - in mainapp.TartubeApp.media_reg_dict) - - Return values: - - A single string, containing one or more full file paths/URLs/video - names, separated by newline characters. If the path/URL/name - isn't known for any videos, then an empty line is added to the - string - - """ - - text = '' - for video_obj in video_list: - - if self.app_obj.drag_video_separator_flag: - text += self.drag_drop_separator + '\n' - - if self.app_obj.drag_video_path_flag: - - if not dummy_flag and video_obj.file_name is not None: - text += video_obj.get_actual_path(self.app_obj) + '\n' - elif dummy_flag and video_obj.dummy_path is not None: - text += video_obj.dummy_path + '\n' - else: - text += '(' + _('unknown path') + ')\n' - - if self.app_obj.drag_video_source_flag: - - if video_obj.source is not None: - text += video_obj.source + '\n' - else: - text += '(' + _('unknown URL') + ')\n' - - if self.app_obj.drag_video_name_flag: - - if video_obj.name is not None: - text += video_obj.name + '\n' - else: - text += self.app_obj.default_video_name + '\n' - - if self.app_obj.drag_video_msg_flag: - - if not video_obj.error_list and not video_obj.warning_list: - text += '(' + _('no errors/warnings') + ')\n' - - else: - for msg in video_obj.error_list: - text += msg + '\n' - for msg in video_obj.warning_list: - text += msg + '\n' - - if self.app_obj.drag_thumb_path_flag: - - if video_obj.file_name is None or dummy_flag: - # (Existing code won't be able to find the thumbnail, - # even if the file has been downloaded) - text += '(' + _('unknown thumbnail path') + ')\n' - - else: - - thumb_path = utils.find_thumbnail( - self.app_obj, - video_obj, - True, - ) - - if thumb_path is not None: - text += thumb_path + '\n' - else: - text += '(' + _('unknown thumbnail path') + ')\n' - - return text - - - # Set accessors - - - def add_child_window(self, config_win_obj): - - """Called by config.GenericConfigWin.setup(). - - When a configuration window opens, add it to our list of such windows. - - Args: - - config_win_obj (config.GenericConfigWin): The window to add - - """ - - # Check that the window isn't already in the list (unlikely, but check - # anyway) - if config_win_obj in self.config_win_list: - return self.app_obj.system_error( - 266, - 'Callback request denied due to current conditions', - ) - - # Update IVs - self.config_win_list.append(config_win_obj) - if isinstance(config_win_obj, wizwin.GenericWizWin): - - self.wiz_win_obj = config_win_obj - - - def del_child_window(self, config_win_obj): - - """Called by config.GenericConfigWin.close(). - - When a configuration window closes, remove it to our list of such - windows. - - Args: - - config_win_obj (config.GenericConfigWin): The window to remove - - """ - - # Update IVs - # (Don't show an error if the window isn't in the list, as it's - # conceivable this function might be called twice) - if config_win_obj in self.config_win_list: - - self.config_win_list.remove(config_win_obj) - - if self.wiz_win_obj is not None \ - and self.wiz_win_obj == config_win_obj: - self.wiz_win_obj = None - - - def reset_video_catalogue_drag_list(self): - - """Can be called by anything. - - Dragging from the Video Catalogue into the Video Index is handled by - storing a list of videos involved, at the start of the drag. The list - is no longer required, so reset it. - """ - - self.video_catalogue_drag_list = [] - - - def set_previous_alt_dest_dbid(self, value): - - """Called by functions in SetDestinationDialogue. - - The specified value may be a .dbid, or None. - """ - - self.previous_alt_dest_dbid = value - - - def set_previous_external_dir(self, value): - - """Called by functions in SetDestinationDialogue. - - The specified value may be a full path to a directory, or None. - """ - - self.previous_external_dir = value - - - def set_video_catalogue_drag_list(self, video_list): - - """Called by mainwin.CatalogueRow.on_drag_data_get() and - mainwin.CatalogueGridBox.on_drag_data_get(). - - Dragging from the Video Catalogue into the Video Index is handled by - storing a list of videos involved, at the start of the drag. - - Args: - - video_list (list): A list of media.Video objects to be dragged - - """ - - self.video_catalogue_drag_list = video_list.copy() - - -class SimpleCatalogueItem(object): - - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_video(). - - Python class that handles a single row in the Video Catalogue. - - Each mainwin.SimpleCatalogueItem object stores widgets used in that row, - and updates them when required. - - This class offers a simple view with a minimum of widgets (for example, no - video thumbnails). The mainwin.ComplexCatalogueItem class offers a more - complex view (for example, with video thumbnails). - - Args: - - main_win_obj (mainwin.MainWin): The main window object - - video_obj (media.Video): The media data object itself (always a video) - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: This section specifies text used to' \ - + ' display videos in the Videos tab. Videos are displayed in' \ - + ' several different formats. To switch format, in the main' \ - + ' menu click Media > Switch between views' - ) - - # IV list - class objects - # ----------------------- - # The main window object - self.main_win_obj = main_win_obj - # The media data object itself (always a video) - self.video_obj = video_obj - - - # IV list - Gtk widgets - # --------------------- - self.catalogue_row = None # mainwin.CatalogueRow - self.hbox = None # Gtk.HBox - self.status_image = None # Gtk.Image - self.name_label = None # Gtk.Label - self.parent_label = None # Gtk.Label - self.stats_label = None # Gtk.Label - self.comment_image = None # Gtk.Image - self.subs_image = None # Gtk.Image - self.slice_image = None # Gtk.Image - self.stamp_image = None # Gtk.Image - self.warning_image = None # Gtk.Image - self.error_image = None # Gtk.Image - self.options_image = None # Gtk.Image - - - # IV list - other - # --------------- - # Unique ID for this object, matching the .dbid for self.video_obj (an - # integer) - self.dbid = video_obj.dbid - # Size (in pixels) of gaps between various widgets - self.spacing_size = 5 - - # Whenever self.draw_widgets() or .update_widgets() is called, the - # background colour might be changed - # This IV shows the value of the self.video_obj.live_mode, the last - # time either of those functions was called. If the value has - # actually changed, then we ask Gtk to change the background - # (otherwise, we don't) - self.previous_live_mode = 0 - - - # Public class methods - - - def draw_widgets(self, catalogue_row): - - """Called by mainwin.MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_video(). - - After a Gtk.ListBoxRow has been created for this object, populate it - with widgets. - - Args: - - catalogue_row (mainwin.CatalogueRow): A wrapper for a - Gtk.ListBoxRow object, storing the media.Video object displayed - in that row. - - """ - - self.catalogue_row = catalogue_row - - event_box = Gtk.EventBox() - self.catalogue_row.add(event_box) - event_box.connect('button-press-event', self.on_right_click_row) - - self.hbox = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - event_box.add(self.hbox) - self.hbox.set_border_width(0) - - # Highlight livestreams by specifying a background colour - self.update_background() - - # Status icon - self.status_image = Gtk.Image() - self.hbox.pack_start(self.status_image, False, False, 0) - - # Box with two lines of text - vbox = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, - spacing=0, - ) - self.hbox.pack_start(vbox, True, True, self.spacing_size) - - # Video name - self.name_label = Gtk.Label('', xalign = 0) - vbox.pack_start(self.name_label, True, True, 0) - - # Parent channel/playlist/folder name (if allowed) - if self.main_win_obj.app_obj.catalogue_mode == 'simple_show_parent': - self.parent_label = Gtk.Label('', xalign = 0) - vbox.pack_start(self.parent_label, True, True, 0) - - # Video stats - self.stats_label = Gtk.Label('', xalign=0) - vbox.pack_start(self.stats_label, True, True, 0) - - # Remainining status icons - self.comment_image = Gtk.Image() - self.hbox.pack_end(self.comment_image, False, False, self.spacing_size) - - self.subs_image = Gtk.Image() - self.hbox.pack_end(self.subs_image, False, False, 0) - - self.slice_image = Gtk.Image() - self.hbox.pack_end(self.slice_image, False, False, self.spacing_size) - - self.stamp_image = Gtk.Image() - self.hbox.pack_end(self.stamp_image, False, False, 0) - - self.warning_image = Gtk.Image() - self.hbox.pack_end(self.warning_image, False, False, self.spacing_size) - - self.error_image = Gtk.Image() - self.hbox.pack_end(self.error_image, False, False, 0) - - self.options_image = Gtk.Image() - self.hbox.pack_end(self.options_image, False, False, self.spacing_size) - - - def update_widgets(self): - - """Called by mainwin.MainWin.video_catalogue_redraw_all(), - .video_catalogue_update_video() and .video_catalogue_insert_video(). - - Sets the values displayed by each widget. - """ - - self.update_background() - self.update_tooltips() - self.update_status_images() - self.update_video_name() - self.update_container_name() - self.update_video_stats() - - - def update_background(self): - - """Calledy by self.draw_widgets() and .update_widgets(). - - Updates the background colour to show which videos are livestreams - (but only when a video's livestream mode has changed). - """ - - if self.previous_live_mode != self.video_obj.live_mode: - - self.previous_live_mode = self.video_obj.live_mode - - if self.video_obj.live_mode == 0 \ - or not self.main_win_obj.app_obj.livestream_use_colour_flag: - - self.hbox.override_background_color( - Gtk.StateType.NORMAL, - None, - ) - - elif self.video_obj.live_mode == 1: - - if not self.video_obj.live_debut_flag \ - or self.main_win_obj.app_obj.livestream_simple_colour_flag: - - self.hbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.live_wait_colour, - ) - - else: - - self.hbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.debut_wait_colour, - ) - - elif self.video_obj.live_mode == 2: - - if not self.video_obj.live_debut_flag \ - or self.main_win_obj.app_obj.livestream_simple_colour_flag: - - self.hbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.live_now_colour, - ) - - else: - - self.hbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.debut_now_colour, - ) - - - def update_tooltips(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the tooltips for the Gtk.HBox that contains everything. - """ - - if self.main_win_obj.app_obj.show_tooltips_flag: - self.hbox.set_tooltip_text( - self.video_obj.fetch_tooltip_text( - self.main_win_obj.app_obj, - self.main_win_obj.tooltip_max_len, - ), - ) - - - def update_status_images(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Image widget to display the video's download status. - """ - - # Set the download status - if self.video_obj.live_mode == 1: - - if not self.video_obj.live_debut_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['live_wait_small'], - ) - - else: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['debut_wait_small'], - ) - - elif self.video_obj.live_mode == 2: - - if not self.video_obj.live_debut_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['live_now_small'], - ) - - else: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['debut_now_small'], - ) - - elif self.video_obj.dl_flag: - - if self.video_obj.archive_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['archived_small'], - ) - - elif self.video_obj.split_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['split_file_small'], - ) - - elif self.video_obj.was_live_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['live_old_small'], - ) - - else: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['have_file_small'], - ) - - elif self.video_obj.was_live_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['live_old_no_file_small'], - ) - - else: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['no_file_small'], - ) - - # The remaining status icons are not displayed at all, if the flag is - # not set - if not self.main_win_obj.app_obj.catalogue_draw_icons_flag: - self.status_image.clear() - self.warning_image.clear() - self.error_image.clear() - - else: - - # To prevent an unsightly gap between these images, use the first - # available Gtk.Image - image_list = [ - self.comment_image, - self.subs_image, - self.slice_image, - self.stamp_image, - self.warning_image, - self.error_image, - self.options_image, - ] - - if self.video_obj.comment_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['comment_small'], - ) - - if self.video_obj.subs_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['subs_small'], - ) - - if self.video_obj.slice_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['slice_small'], - ) - - if self.video_obj.stamp_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['stamp_small'], - ) - - if self.video_obj.warning_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['warning_small'], - ) - - if self.video_obj.error_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['error_small'], - ) - - if self.video_obj.options_obj: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['dl_options_small'], - ) - - for image in image_list: - image.clear() - - - def update_video_name(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the video's current name. - """ - - # For videos whose name is unknown, display the URL, rather than the - # usual '(video with no name)' string - if not self.main_win_obj.app_obj.catalogue_show_nickname_flag: - name = self.video_obj.name - else: - name = self.video_obj.nickname - - if name is None \ - or name == self.main_win_obj.app_obj.default_video_name: - - if self.video_obj.source is not None: - - # Using pango markup to display a URL is too risky, so just use - # ordinary text - self.name_label.set_text( - utils.shorten_string( - self.video_obj.source, - self.main_win_obj.very_long_string_max_len, - ), - ) - - return - - else: - - # No URL to show, so we're forced to use '(video with no name)' - name = self.main_win_obj.app_obj.default_video_name - - string = '' - if self.video_obj.new_flag: - string += ' font_weight="bold"' - - if self.video_obj.dl_sim_flag: - string += ' style="italic"' - - self.name_label.set_markup( - '' + \ - html.escape( - utils.shorten_string( - name, - self.main_win_obj.very_long_string_max_len, - ), - quote=True, - ) + '' - ) - - - def update_container_name(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the name of the parent channel, - playlist or folder. - """ - - if self.main_win_obj.app_obj.catalogue_mode != 'simple_show_parent': - return - - if self.video_obj.orig_parent is not None: - - string = _('Originally from:') + ' \'' - - string2 = html.escape( - utils.shorten_string( - self.video_obj.orig_parent, - self.main_win_obj.long_string_max_len, - ), - quote=True, - ) - - else: - - if isinstance(self.video_obj.parent_obj, media.Channel): - string = _('From channel') - elif isinstance(self.video_obj.parent_obj, media.Playlist): - string = _('From playlist') - else: - string = _('From folder') - - string2 = html.escape( - utils.shorten_string( - self.video_obj.parent_obj.name, - self.main_win_obj.long_string_max_len, - ), - quote=True, - ) - - self.parent_label.set_markup('' + string + ': ' + string2) - - - def update_video_stats(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the video's current side/ - duration/date information. - """ - - if self.video_obj.live_mode: - - if not self.video_obj.live_debut_flag: - - if self.video_obj.live_mode == 2: - msg = _('Livestream has started') - elif self.video_obj.live_msg == '': - msg = _('Livestream has not started yet') - else: - msg = self.video_obj.live_msg - - else: - - if self.video_obj.live_mode == 2: - msg = _('Debut has started') - elif self.video_obj.live_msg == '': - msg = _('Debut has not started yet') - else: - msg = self.video_obj.live_msg - - else: - - if self.video_obj.duration is not None: - msg = _('Duration:') + ' ' + utils.convert_seconds_to_string( - self.video_obj.duration, - True, - ) - - else: - msg = _('Duration:') + ' ' + _('unknown') + '' - - size = self.video_obj.get_file_size_string() - if size != "": - msg += ' - ' + _('Size:') + ' ' + size - else: - msg += ' - ' + _('Size:') + ' ' + _('unknown') + '' - - pretty_flag = self.main_win_obj.app_obj.show_pretty_dates_flag - if self.main_win_obj.app_obj.catalogue_sort_mode == 'receive': - date = self.video_obj.get_receive_date_string(pretty_flag) - else: - date = self.video_obj.get_upload_date_string(pretty_flag) - - if date is not None: - msg += ' - ' + _('Date:') + ' ' + date - else: - msg += ' - ' + _('Date:') + ' ' + _('unknown') + '' - - self.stats_label.set_markup(msg) - - - # Callback methods - - - def on_right_click_row(self, event_box, event): - - """Called from callback in self.draw_widgets(). - - When the user right-clicks an a row, create a context-sensitive popup - menu. - - Args: - - event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the - signal emitted by the click - - """ - - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - - self.main_win_obj.video_catalogue_popup_menu(event, self.video_obj) - - -class ComplexCatalogueItem(object): - - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_video(). - - Python class that handles a single row in the Video Catalogue. - - Each mainwin.ComplexCatalogueItem object stores widgets used in that row, - and updates them when required. - - The mainwin.SimpleCatalogueItem class offers a simple view with a minimum - of widgets (for example, no video thumbnails). This class offers a more - complex view (for example, with video thumbnails). - - Args: - - main_win_obj (mainwin.MainWin): The main window object - - video_obj (media.Video): The media data object itself (always a video) - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj): - - # IV list - class objects - # ----------------------- - # The main window object - self.main_win_obj = main_win_obj - # The media data object itself (always a video) - self.video_obj = video_obj - - - # IV list - Gtk widgets - # --------------------- - self.catalogue_row = None # mainwin.CatalogueRow - self.frame = None # Gtk.Frame - self.thumb_box = None # Gtk.Box - self.thumb_image = None # Gtk.Image - self.label_box = None # Gtk.Box - self.name_label = None # Gtk.Label - self.status_image = None # Gtk.Image - self.comment_image = None # Gtk.Image - self.subs_image = None # Gtk.Image - self.slice_image = None # Gtk.Image - self.stamp_image = None # Gtk.Image - self.warning_image = None # Gtk.Image - self.error_image = None # Gtk.Image - self.options_image = None # Gtk.Image - self.descrip_label = None # Gtk.Label - self.expand_label = None # Gtk.Label - self.stats_label = None # Gtk.Label - self.live_auto_notify_label = None # Gtk.Label - self.live_auto_alarm_label = None # Gtk.Label - self.live_auto_open_label = None # Gtk.Label - self.live_auto_dl_start_label = None - # Gtk.Label - self.live_auto_dl_stop_label = None # Gtk.Label - self.watch_label = None # Gtk.Label - self.watch_player_label = None # Gtk.Label - self.watch_web_label = None # Gtk.Label - self.watch_hooktube_label = None # Gtk.Label - self.watch_invidious_label = None # Gtk.Label - self.watch_other_label = None # Gtk.Label - self.temp_box = None # Gtk.Box - self.temp_label = None # Gtk.Label - self.temp_mark_label = None # Gtk.Label - self.temp_dl_label = None # Gtk.Label - self.temp_dl_watch_label = None # Gtk.Label - self.marked_box = None # Gtk.Box - self.marked_label = None # Gtk.Label - self.marked_archive_label = None # Gtk.Label - self.marked_bookmark_label = None # Gtk.Label - self.marked_fav_label = None # Gtk.Label - self.marked_missing_label = None # Gtk.Label - self.marked_new_label = None # Gtk.Label - self.marked_waiting_label = None # Gtk.Label - - - # IV list - other - # --------------- - # Unique ID for this object, matching the .dbid for self.video_obj (an - # integer) - self.dbid = video_obj.dbid - # Size (in pixels) of gaps between various widgets - self.spacing_size = 5 - # The state of the More/Less label. False if the video's short - # description (or no description at all) is visible, True if the - # video's full description is visible - self.expand_descrip_flag = False - # Flag set to True if the video's parent folder is a temporary folder, - # meaning that some widgets don't need to be drawn at all - self.no_temp_widgets_flag = False - - # Whenever self.draw_widgets() or .update_widgets() is called, the - # background colour might be changed - # This IV shows the value of the self.video_obj.live_mode, the last - # time either of those functions was called. If the value has - # actually changed, then we ask Gtk to change the background - # (otherwise, we don't) - self.previous_live_mode = 0 - # Flag set to True when the temporary labels box (self.temp_box) is - # visible, False when not - self.temp_box_visible_flag = False - # Flag set to True when the marked labels box (self.marked_box) is - # visible, False when not - self.marked_box_visible_flag = False - - - # Public class methods - - - def draw_widgets(self, catalogue_row): - - """Called by mainwin.MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_video(). - - After a Gtk.ListBoxRow has been created for this object, populate it - with widgets. - - Args: - - catalogue_row (mainwin.CatalogueRow): A wrapper for a - Gtk.ListBoxRow object, storing the media.Video object displayed - in that row. - - """ - - # If the video's parent folder is a temporary folder, then we don't - # need one row of widgets at all - parent_obj = self.video_obj.parent_obj - if isinstance(parent_obj, media.Folder) \ - and parent_obj.temp_flag: - self.no_temp_widgets_flag = True - else: - self.no_temp_widgets_flag = False - - # Draw the widgets - self.catalogue_row = catalogue_row - - event_box = Gtk.EventBox() - self.catalogue_row.add(event_box) - event_box.connect('button-press-event', self.on_right_click_row) - - self.frame = Gtk.Frame() - event_box.add(self.frame) - self.frame.set_border_width(self.spacing_size) - self.enable_visible_frame( - self.main_win_obj.app_obj.catalogue_draw_frame_flag, - ) - - # Highlight livestreams by specifying a background colour - self.update_background() - - hbox = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - self.frame.add(hbox) - hbox.set_border_width(self.spacing_size) - - # The thumbnail is in its own vbox, so we can keep it in the top-left - # when the video's description has multiple lines - self.thumb_box = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, - spacing=0, - ) - hbox.pack_start(self.thumb_box, False, False, 0) - - self.thumb_image = Gtk.Image() - self.thumb_box.pack_start(self.thumb_image, False, False, 0) - - # Everything to the right of the thumbnail is in a second vbox - self.label_box = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, - spacing=0, - ) - hbox.pack_start(self.label_box, True, True, self.spacing_size) - - # First row - video name - hbox2 = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - self.label_box.pack_start(hbox2, True, True, 0) - - self.name_label = Gtk.Label('', xalign = 0) - hbox2.pack_start(self.name_label, True, True, 0) - - # Status icons - self.status_image = Gtk.Image() - hbox2.pack_end(self.status_image, False, False, 0) - - self.comment_image = Gtk.Image() - hbox2.pack_end(self.comment_image, False, False, self.spacing_size) - - self.subs_image = Gtk.Image() - hbox2.pack_end(self.subs_image, False, False, 0) - - self.slice_image = Gtk.Image() - hbox2.pack_end(self.slice_image, False, False, self.spacing_size) - - self.stamp_image = Gtk.Image() - hbox2.pack_end(self.stamp_image, False, False, 0) - - self.warning_image = Gtk.Image() - hbox2.pack_end(self.warning_image, False, False, self.spacing_size) - - self.error_image = Gtk.Image() - hbox2.pack_end(self.error_image, False, False, 0) - - self.options_image = Gtk.Image() - hbox2.pack_end(self.options_image, False, False, self.spacing_size) - - # Second row - video description (incorporating the the More/Less - # label), or the name of the parent channel/playlist/folder, - # depending on settings - self.descrip_label = Gtk.Label('', xalign=0) - self.label_box.pack_start(self.descrip_label, True, True, 0) - self.descrip_label.connect( - 'activate-link', - self.on_click_descrip_label, - ) - - # Third row - video stats, or livestream notification options, - # depending on settings - hbox3 = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - self.label_box.pack_start(hbox3, True, True, 0) - - # (This label is visible in both situations) - self.stats_label = Gtk.Label('', xalign=0) - hbox3.pack_start(self.stats_label, False, False, 0) - - # (These labels are visible only for livestreams) - # Auto-notify - self.live_auto_notify_label = Gtk.Label('', xalign=0) - hbox3.pack_start( - self.live_auto_notify_label, - False, - False, - 0, - ) - self.live_auto_notify_label.connect( - 'activate-link', - self.on_click_live_auto_notify_label, - ) - - # Auto-sound alarm - self.live_auto_alarm_label = Gtk.Label('', xalign=0) - hbox3.pack_start( - self.live_auto_alarm_label, - False, - False, - (self.spacing_size * 2), - ) - self.live_auto_alarm_label.connect( - 'activate-link', - self.on_click_live_auto_alarm_label, - ) - - # Auto-open - self.live_auto_open_label = Gtk.Label('', xalign=0) - hbox3.pack_start( - self.live_auto_open_label, - False, - False, - 0, - ) - self.live_auto_open_label.connect( - 'activate-link', - self.on_click_live_auto_open_label, - ) - - # D/L on start - self.live_auto_dl_start_label = Gtk.Label('', xalign=0) - hbox3.pack_start( - self.live_auto_dl_start_label, - False, - False, - (self.spacing_size * 2), - ) - self.live_auto_dl_start_label.connect( - 'activate-link', - self.on_click_live_auto_dl_start_label, - ) - - # D/L on stop - self.live_auto_dl_stop_label = Gtk.Label('', xalign=0) - hbox3.pack_start( - self.live_auto_dl_stop_label, - False, - False, - 0, - ) - self.live_auto_dl_stop_label.connect( - 'activate-link', - self.on_click_live_auto_dl_stop_label, - ) - - # Fourth row - Watch... - hbox4 = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - self.label_box.pack_start(hbox4, True, True, 0) - - self.watch_label = Gtk.Label(_('Watch:') + ' ', xalign=0) - hbox4.pack_start(self.watch_label, False, False, 0) - - # Watch in player - self.watch_player_label = Gtk.Label('', xalign=0) - hbox4.pack_start(self.watch_player_label, False, False, 0) - self.watch_player_label.connect( - 'activate-link', - self.on_click_watch_player_label, - ) - - # Watch on website/YouTube - self.watch_web_label = Gtk.Label('', xalign=0) - hbox4.pack_start( - self.watch_web_label, - False, - False, - (self.spacing_size * 2), - ) - self.watch_web_label.connect( - 'activate-link', - self.on_click_watch_web_label, - ) - - # Watch on HookTube - self.watch_hooktube_label = Gtk.Label('', xalign=0) - hbox4.pack_start(self.watch_hooktube_label, False, False, 0) - self.watch_hooktube_label.connect( - 'activate-link', - self.on_click_watch_hooktube_label, - ) - - # Watch on Invidious - self.watch_invidious_label = Gtk.Label('', xalign=0) - hbox4.pack_start( - self.watch_invidious_label, - False, - False, - (self.spacing_size * 2), - ) - self.watch_invidious_label.connect( - 'activate-link', - self.on_click_watch_invidious_label, - ) - - # Watch on the other YouTube front-end (specified by the user) - self.watch_other_label = Gtk.Label('', xalign=0) - hbox4.pack_start( - self.watch_other_label, - False, - False, - 0, - ) - self.watch_other_label.connect( - 'activate-link', - self.on_click_watch_other_label, - ) - - # Optional rows - - # Fifth row: Temporary... - self.temp_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - if self.temp_box_is_visible(): - self.label_box.pack_start(self.temp_box, True, True, 0) - self.temp_box_visible_flag = True - - self.temp_label = Gtk.Label(_('Temporary:') + ' ', xalign=0) - self.temp_box.pack_start(self.temp_label, False, False, 0) - - # Mark for download - self.temp_mark_label = Gtk.Label('', xalign=0) - self.temp_box.pack_start(self.temp_mark_label, False, False, 0) - self.temp_mark_label.connect( - 'activate-link', - self.on_click_temp_mark_label, - ) - - # Download - self.temp_dl_label = Gtk.Label('', xalign=0) - self.temp_box.pack_start( - self.temp_dl_label, - False, - False, - (self.spacing_size * 2), - ) - self.temp_dl_label.connect( - 'activate-link', - self.on_click_temp_dl_label, - ) - - # Download and watch - self.temp_dl_watch_label = Gtk.Label('', xalign=0) - self.temp_box.pack_start(self.temp_dl_watch_label, False, False, 0) - self.temp_dl_watch_label.connect( - 'activate-link', - self.on_click_temp_dl_watch_label, - ) - - # Sixth row: Marked... - self.marked_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - if self.marked_box_is_visible(): - # (For the sixth row we use .pack_end, so that the fifth row can be - # added and removed, without affecting the visible order) - self.label_box.pack_end(self.marked_box, True, True, 0) - self.marked_box_visible_flag = True - - self.marked_label = Gtk.Label(_('Marked:') + ' ', xalign=0) - self.marked_box.pack_start(self.marked_label, False, False, 0) - - # Archived/not archived - self.marked_archive_label = Gtk.Label('', xalign=0) - self.marked_box.pack_start(self.marked_archive_label, False, False, 0) - self.marked_archive_label.connect( - 'activate-link', - self.on_click_marked_archive_label, - ) - - # Bookmarked/not bookmarked - self.marked_bookmark_label = Gtk.Label('', xalign=0) - self.marked_box.pack_start( - self.marked_bookmark_label, - False, - False, - (self.spacing_size * 2), - ) - self.marked_bookmark_label.connect( - 'activate-link', - self.on_click_marked_bookmark_label, - ) - - # Favourite/not favourite - self.marked_fav_label = Gtk.Label('', xalign=0) - self.marked_box.pack_start(self.marked_fav_label, False, False, 0) - self.marked_fav_label.connect( - 'activate-link', - self.on_click_marked_fav_label, - ) - - # Missing/not missing - self.marked_missing_label = Gtk.Label('', xalign=0) - self.marked_box.pack_start( - self.marked_missing_label, - False, - False, - (self.spacing_size * 2), - ) - self.marked_missing_label.connect( - 'activate-link', - self.on_click_marked_missing_label, - ) - - # New/not new - self.marked_new_label = Gtk.Label('', xalign=0) - self.marked_box.pack_start(self.marked_new_label, False, False, 0) - self.marked_new_label.connect( - 'activate-link', - self.on_click_marked_new_label, - ) - - # In waiting list/not in waiting list - self.marked_waiting_label = Gtk.Label('', xalign=0) - self.marked_box.pack_start( - self.marked_waiting_label, - False, - False, - (self.spacing_size * 2), - ) - self.marked_waiting_label.connect( - 'activate-link', - self.on_click_marked_waiting_list_label, - ) - - - def update_widgets(self): - - """Called by mainwin.MainWin.video_catalogue_redraw_all(), - .video_catalogue_update_video() and .video_catalogue_insert_video(). - - Sets the values displayed by each widget. - """ - - self.update_background() - self.update_tooltips() - self.update_thumb_image() - self.update_video_name() - self.update_status_images() - self.update_video_descrip() - self.update_video_stats() - self.update_watch_player() - self.update_watch_web() - - # If the fifth/sixth rows are not currently visible, but need to be - # visible, make them visible (and vice-versa) - if not self.temp_box_is_visible(): - - if self.temp_box_visible_flag: - self.label_box.remove(self.temp_box) - self.temp_box_visible_flag = False - - else: - - self.update_temp_labels() - if not self.temp_box_visible_flag: - self.label_box.pack_start(self.temp_box, True, True, 0) - self.temp_box_visible_flag = True - - if not self.marked_box_is_visible(): - - if self.marked_box_visible_flag: - self.label_box.remove(self.marked_box) - self.marked_box_visible_flag = False - - else: - - self.update_marked_labels() - if not self.marked_box_visible_flag: - self.label_box.pack_end(self.marked_box, True, True, 0) - self.marked_box_visible_flag = True - - - def update_background(self): - - """Calledy by self.draw_widgets() and .update_widgets(). - - Updates the background colour to show which videos are livestreams - (but only when a video's livestream mode has changed). - """ - - if self.previous_live_mode != self.video_obj.live_mode: - - self.previous_live_mode = self.video_obj.live_mode - - if self.video_obj.live_mode == 0 \ - or not self.main_win_obj.app_obj.livestream_use_colour_flag: - - self.frame.override_background_color( - Gtk.StateType.NORMAL, - None, - ) - - elif self.video_obj.live_mode == 1: - - if not self.video_obj.live_debut_flag \ - or self.main_win_obj.app_obj.livestream_simple_colour_flag: - - self.frame.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.live_wait_colour, - ) - - else: - - self.frame.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.debut_wait_colour, - ) - - elif self.video_obj.live_mode == 2: - - if not self.video_obj.live_debut_flag \ - or self.main_win_obj.app_obj.livestream_simple_colour_flag: - - self.frame.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.live_now_colour, - ) - - else: - - self.frame.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.debut_now_colour, - ) - - - def update_tooltips(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the tooltips for the Gtk.Frame that contains everything. - """ - - if self.main_win_obj.app_obj.show_tooltips_flag: - self.frame.set_tooltip_text( - self.video_obj.fetch_tooltip_text( - self.main_win_obj.app_obj, - self.main_win_obj.tooltip_max_len, - ), - ) - - - def update_thumb_image(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Image widget to display the video's thumbnail, if - available. - """ - - # See if the video's thumbnail file has been downloaded - thumb_flag = False - if self.video_obj.file_name: - - # No way to know which image format is used by all websites for - # their video thumbnails, so look for the most common ones - # The True argument means that if the thumbnail isn't found in - # Tartube's main data directory, look in the temporary directory - # too - path = utils.find_thumbnail( - self.main_win_obj.app_obj, - self.video_obj, - True, - ) - - if path: - - # Thumbnail file exists, so use it - app_obj = self.main_win_obj.app_obj - mini_list = app_obj.thumb_size_dict['tiny'] - # (Returns a tuple, who knows why) - arglist = app_obj.file_manager_obj.load_to_pixbuf( - path, - mini_list[0], # width - mini_list[1], # height - ), - - if arglist[0]: - self.thumb_image.set_from_pixbuf(arglist[0]) - thumb_flag = True - - # No thumbnail file found, so use a default file - if not thumb_flag: - if self.video_obj.fav_flag and self.video_obj.options_obj: - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['thumb_both_tiny'], - ) - elif self.video_obj.fav_flag: - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['thumb_left_tiny'], - ) - elif self.video_obj.options_obj: - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['thumb_right_tiny'], - ) - else: - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['thumb_none_tiny'], - ) - - - def update_video_name(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the video's current name. - """ - - # For videos whose name is unknown, display the URL, rather than the - # usual '(video with no name)' string - if not self.main_win_obj.app_obj.catalogue_show_nickname_flag: - name = self.video_obj.name - else: - name = self.video_obj.nickname - - if name is None \ - or name == self.main_win_obj.app_obj.default_video_name: - - if self.video_obj.source is not None: - - # Using pango markup to display a URL is too risky, so just use - # ordinary text - self.name_label.set_text( - utils.shorten_string( - self.video_obj.source, - self.main_win_obj.quite_long_string_max_len, - ), - ) - - return - - else: - - # No URL to show, so we're forced to use '(video with no name)' - name = self.main_win_obj.app_obj.default_video_name - - string = '' - if self.video_obj.new_flag: - string += ' font_weight="bold"' - - if self.video_obj.dl_sim_flag: - string += ' style="italic"' - - self.name_label.set_markup( - '' + \ - html.escape( - utils.shorten_string( - name, - self.main_win_obj.quite_long_string_max_len, - ), - quote=True, - ) + '' - ) - - - def update_status_images(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Image widgets to display the video's download status, - error and warning settings. - """ - - # Special case: don't display any images at all, if the flag is not - # set - if not self.main_win_obj.app_obj.catalogue_draw_icons_flag: - self.status_image.clear() - self.comment_image.clear() - self.subs_image.clear() - self.slice_image.clear() - self.stamp_image.clear() - self.warning_image.clear() - self.error_image.clear() - self.options_image.clear() - - else: - - # Set the download status - if self.video_obj.live_mode == 1: - - if not self.video_obj.live_debut_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['live_wait_small'], - ) - - else: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['debut_wait_small'], - ) - - elif self.video_obj.live_mode == 2: - - if not self.video_obj.live_debut_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['live_now_small'], - ) - - else: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['debut_now_small'], - ) - - elif self.video_obj.dl_flag: - - if self.video_obj.archive_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['archived_small'], - ) - - elif self.video_obj.split_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['split_file_small'], - ) - - elif self.video_obj.was_live_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['live_old_small'], - ) - - else: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['have_file_small'], - ) - - elif self.video_obj.was_live_flag: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['live_old_no_file_small'], - ) - - else: - - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['no_file_small'], - ) - - # Set the remaining status icons - # To prevent an unsightly gap between these images, use the first - # available Gtk.Image - image_list = [ - self.comment_image, - self.subs_image, - self.slice_image, - self.stamp_image, - self.warning_image, - self.error_image, - self.options_image, - ] - - if self.video_obj.comment_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['comment_small'], - ) - - if self.video_obj.subs_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['subs_small'], - ) - - if self.video_obj.slice_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['slice_small'], - ) - - if self.video_obj.stamp_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['stamp_small'], - ) - - if self.video_obj.warning_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['warning_small'], - ) - - if self.video_obj.error_list: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['error_small'], - ) - - if self.video_obj.options_obj: - image = image_list.pop(0) - image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['dl_options_small'], - ) - - for image in image_list: - image.clear() - - - def update_video_descrip(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the video's current - description. - """ - - if self.main_win_obj.app_obj.catalogue_mode == 'complex_hide_parent' \ - or self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_hide_parent_ext': - - # Show the first line of the video description, or all of it, - # depending on settings - if self.video_obj.short: - - # Work with a list of lines, displaying either the fist line, - # or all of them, as the user clicks the More/Less button - line_list = self.video_obj.descrip.splitlines() - - if not self.expand_descrip_flag: - - string = html.escape( - utils.shorten_string( - line_list[0], - self.main_win_obj.very_long_string_max_len, - ), - quote=True, - ) - - if len(line_list) > 1: - self.descrip_label.set_markup( - '' + _('More') + ' ' + string, - ) - else: - self.descrip_label.set_text(string) - - else: - - descrip = html.escape(self.video_obj.descrip, quote=True) - - if len(line_list) > 1: - self.descrip_label.set_markup( - '' + _('Less') + ' ' + descrip + '\n', - ) - else: - self.descrip_label.set_text(descrip) - - else: - self.descrip_label.set_markup('No description set') - - else: - - # Show the name of the parent channel/playlist/folder, optionally - # followed by the whole video description, depending on settings - if self.video_obj.orig_parent is not None: - - string = _('Originally from:') + ' \'' - - string += html.escape( - utils.shorten_string( - self.video_obj.orig_parent, - self.main_win_obj.very_long_string_max_len, - ), - quote=True, - ) + '\'' - - else: - - string = '' - if isinstance(self.video_obj.parent_obj, media.Channel): - string += _('From channel') - elif isinstance(self.video_obj.parent_obj, media.Playlist): - string += _('From playlist') - else: - string += _('From folder') - - string += ': ' + html.escape( - utils.shorten_string( - self.video_obj.parent_obj.name, - self.main_win_obj.long_string_max_len, - ), - quote=True, - ) - - if not self.video_obj.descrip: - self.descrip_label.set_markup(string) - - elif not self.expand_descrip_flag: - - self.descrip_label.set_markup( - '' + _('More') + ' ' + string, - ) - - else: - - descrip = html.escape(self.video_obj.descrip, quote=True) - self.descrip_label.set_markup( - '' + _('Less') + ' ' + descrip + '\n', - ) - - - def update_video_stats(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the video's current side/ - duration/date information. - - For livestreams, instead displays livestream options. - """ - - # Import the main application (for convenience) - app_obj = self.main_win_obj.app_obj - - if not self.video_obj.live_mode: - - if self.video_obj.duration is not None: - string = _('Duration:') + ' ' \ - + utils.convert_seconds_to_string( - self.video_obj.duration, - True, - ) - - else: - string = _('Duration:') + ' ' + _('unknown') + '' - - size = self.video_obj.get_file_size_string() - if size != "": - string = string + ' - ' + _('Size:') + ' ' + size - else: - string = string + ' - ' + _('Size:') + ' ' \ - + _('unknown') + '' - - pretty_flag = self.main_win_obj.app_obj.show_pretty_dates_flag - if app_obj.catalogue_sort_mode == 'receive': - - date = self.video_obj.get_receive_date_string(pretty_flag) - text = _('Received:') - - else: - - date = self.video_obj.get_upload_date_string(pretty_flag) - text = _('Date:') - - if date is not None: - string = string + ' - ' + text + ' ' + date - else: - string = string + ' - ' + text + ' ' + _('unknown') \ - + '' - - self.stats_label.set_markup(string) - - self.live_auto_notify_label.set_text('') - self.live_auto_alarm_label.set_text('') - self.live_auto_open_label.set_text('') - self.live_auto_dl_start_label.set_text('') - self.live_auto_dl_stop_label.set_text('') - - else: - - name = html.escape(self.video_obj.name) - dbid = self.video_obj.dbid - - if not self.video_obj.live_debut_flag: - - if self.video_obj.live_mode == 2: - self.stats_label.set_markup(_('Live now:') + ' ') - elif self.video_obj.live_msg == '': - self.stats_label.set_markup(_('Live soon:') + ' ') - else: - self.stats_label.set_markup( - self.video_obj.live_msg + ': ', - ) - - else: - - if self.video_obj.live_mode == 2: - self.stats_label.set_markup(_('Debut now:') + ' ') - elif self.video_obj.live_msg == '': - self.stats_label.set_markup(_('Debut soon:') + ' ') - else: - self.stats_label.set_markup( - self.video_obj.live_msg + ': ', - ) - - if dbid in app_obj.media_reg_auto_notify_dict: - label = '' + _('Notify') + '' - else: - label = _('Notify') - - # Currently disabled on MS Windows - if os.name == 'nt': - self.live_auto_notify_label.set_markup(_('Notify')) - else: - self.live_auto_notify_label.set_markup( - '' + label + '', - ) - - if not mainapp.HAVE_PLAYSOUND_FLAG: - - self.live_auto_alarm_label.set_markup('Alarm') - - else: - - if dbid in app_obj.media_reg_auto_alarm_dict: - label = '' + _('Alarm') + '' - else: - label = _('Alarm') - - self.live_auto_alarm_label.set_markup( - '' + label + '', - ) - - if dbid in app_obj.media_reg_auto_open_dict: - label = '' + _('Open') + '' - else: - label = _('Open') - - self.live_auto_open_label.set_markup( - '' + label + '', - ) - - if __main__.__pkg_no_download_flag__ \ - or self.video_obj.live_mode == 2: - - # (Livestream already broadcasting) - self.live_auto_dl_start_label.set_markup(_('D/L on start')) - - else: - - if dbid in app_obj.media_reg_auto_dl_start_dict: - label = '' + _('D/L on start') + '' - else: - label = _('D/L on start') - - self.live_auto_dl_start_label.set_markup( - '' + label + '', - ) - - if __main__.__pkg_no_download_flag__: - - self.live_auto_dl_stop_label.set_markup(_('D/L on stop')) - - else: - - if dbid in app_obj.media_reg_auto_dl_stop_dict: - label = '' + _('D/L on stop') + '' - else: - label = _('D/L on stop') - - self.live_auto_dl_stop_label.set_markup( - '' + label + '', - ) - - - def update_watch_player(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the clickable Gtk.Label widget for watching the video in an - external media player. - """ - - if self.video_obj.file_name: - watch_text = '' \ - + _('Player') + '' - - # (Many labels are not clickable when a channel/playlist/folder's - # external directory is marked disabled) - if self.video_obj.parent_obj.dbid \ - in self.main_win_obj.app_obj.container_unavailable_dict: - - # Link not clickable - self.watch_player_label.set_markup(_('Download')) - - elif __main__.__pkg_no_download_flag__: - - if self.video_obj.file_name and self.video_obj.dl_flag: - - # Link clickable - self.watch_player_label.set_markup(watch_text) - - else: - - # Link not clickable - self.watch_player_label.set_markup(_('Download')) - - elif self.video_obj.live_mode == 1: - - # Link not clickable - self.watch_player_label.set_markup(_('Download')) - - elif self.video_obj.live_mode == 2: - - # Link clickable - self.watch_player_label.set_markup( - '' \ - + _('Download') + '', - ) - - elif self.video_obj.file_name and self.video_obj.dl_flag: - - # Link clickable - self.watch_player_label.set_markup(watch_text) - - elif self.video_obj.source \ - and not self.main_win_obj.app_obj.update_manager_obj \ - and not self.main_win_obj.app_obj.refresh_manager_obj \ - and not self.main_win_obj.app_obj.process_manager_obj: - - ignore_me = _( - 'TRANSLATOR\'S NOTE: If you want to use &, use &' \ - + ' - if you want to use a different word (e.g. French et)' \ - + ', then just use that word', - ) - - # Link clickable - self.watch_player_label.set_markup( - '' + _('Download & watch') + '', - ) - - else: - - # Link not clickable - self.watch_player_label.set_markup( - '' + _('Not downloaded') + '', - ) - - - def update_watch_web(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the clickable Gtk.Label widget for watching the video in an - external web browser. - """ - - app_obj = self.main_win_obj.app_obj - - if self.video_obj.source: - - # For YouTube URLs, offer alternative links - source = self.video_obj.source - enhanced = utils.is_video_enhanced(self.video_obj) - if not enhanced: - - # Link clickable - self.watch_web_label.set_markup( - '' \ - + _('Website') + '', - ) - - # Links not clickable - self.watch_hooktube_label.set_text('') - self.watch_invidious_label.set_text('') - self.watch_other_label.set_text('') - - elif enhanced != 'youtube': - - pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name'] - - # Link clickable - self.watch_web_label.set_markup( - '' \ - + pretty + '', - ) - - # Links not clickable - self.watch_hooktube_label.set_text('') - self.watch_invidious_label.set_text('') - self.watch_other_label.set_text('') - - else: - - # Link clickable - self.watch_web_label.set_markup( - '' \ - + _('YouTube') + '', - ) - - if not self.video_obj.live_mode: - - # Links clickable - self.watch_hooktube_label.set_markup( - '' \ - + _('HookTube') + '', - ) - - self.watch_invidious_label.set_markup( - '' \ - + _('Invidious') + '', - ) - - if app_obj.general_custom_dl_obj.divert_mode == 'other' \ - and app_obj.custom_dl_obj.divert_website is not None \ - and len(app_obj.custom_dl_obj.divert_website) > 2: - - # Link clickable - self.watch_other_label.set_markup( - '' + _('Other') + '', - ) - - else: - - # Link not clickable - self.watch_other_label.set_text('') - - else: - - # Links not clickable - self.watch_hooktube_label.set_text('') - self.watch_invidious_label.set_text('') - self.watch_other_label.set_text('') - - else: - - # Links not clickable - self.watch_web_label.set_markup('' + _('No link') + '') - self.watch_hooktube_label.set_text('') - self.watch_invidious_label.set_text('') - self.watch_other_label.set_text('') - - - def update_livestream_labels(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the clickable Gtk.Label widget for video properties. - """ - - name = html.escape(self.video_obj.name) - app_obj = self.main_win_obj.app_obj - dbid = self.video_obj.dbid - - # (Many labels are not clickable when a channel/playlist/folder's - # external directory is marked disabled) - if self.video_obj.parent_obj.dbid \ - in self.main_win_obj.app_obj.container_unavailable_dict: - unavailable_flag = True - else: - unavailable_flag = False - - # Notify/don't notify - if not dbid in app_obj.media_reg_auto_notify_dict: - label = _('Notify') - else: - label = '' + _('Notify') + '' - - # Currently disabled on MS Windows - if os.name == 'nt' or unavailable_flag: - self.live_auto_notify_label.set_markup(_('Notify')) - else: - self.live_auto_notify_label.set_markup( - '' + label + '', - ) - - # Sound alarm/don't sound alarm - if not dbid in app_obj.media_reg_auto_alarm_dict: - label = _('Alarm') - else: - label = '' + _('Alarm') + '' - - if unavailable_flag: - self.live_auto_alarm_label.set_markup(_('Alarm')) - else: - self.live_auto_alarm_label.set_markup( - '' + label + '', - ) - - # Open/don't open - if not dbid in app_obj.media_reg_auto_open_dict: - label = _('Open') - else: - label = '' + _('Open') + '' - - if unavailable_flag: - self.live_auto_open_label.set_markup(_('Open')) - else: - self.live_auto_open_label.set_markup( - '' + label + '', - ) - - # D/L on start/Don't download - if not dbid in app_obj.media_reg_auto_dl_start_dict: - label = _('D/L on start') - else: - label = '' + _('D/L on start') + '' - - if unavailable_flag: - self.live_auto_dl_start_label.set_markup(_('D/L on start')) - else: - self.live_auto_dl_start_label.set_markup( - '' + label + '', - ) - - # D/L on stop/Don't download - if not dbid in app_obj.media_reg_auto_dl_stop_dict: - label = _('D/L on stop') - else: - label = '' + _('D/L on stop') + '' - - if unavailable_flag: - self.live_auto_dl_stop_label.set_markup(_('D/L on stop')) - else: - self.live_auto_dl_stop_label.set_markup( - '' + label + '', - ) - - - def update_temp_labels(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the clickable Gtk.Label widget for temporary video downloads. - """ - - # (Many labels are not clickable when a channel/playlist/folder's - # external directory is marked disabled) - if self.video_obj.parent_obj.dbid \ - in self.main_win_obj.app_obj.container_unavailable_dict: - unavailable_flag = True - else: - unavailable_flag = False - - if self.video_obj.file_name: - link_text = self.video_obj.get_actual_path( - self.main_win_obj.app_obj, - ) - elif self.video_obj.source: - link_text = self.video_obj.source - else: - link_text = '' - - # (Video can't be temporarily downloaded if it has no source URL) - if self.video_obj.source is not None and not unavailable_flag: - - self.temp_mark_label.set_markup( - '' + _('Mark for download') + '', - ) - - self.temp_dl_label.set_markup( - '' + _('Download') + '', - ) - - self.temp_dl_watch_label.set_markup( - '' + _('D/L & watch') + '', - ) - - else: - - self.temp_mark_label.set_text(_('Mark for download')) - self.temp_dl_label.set_text(_('Download')) - self.temp_dl_watch_label.set_text(_('D/L and watch')) - - - def update_marked_labels(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the clickable Gtk.Label widget for video properties. - """ - - if self.video_obj.file_name: - link_text = self.video_obj.get_actual_path( - self.main_win_obj.app_obj, - ) - elif self.video_obj.source: - link_text = self.video_obj.source - else: - link_text = '' - - # Archived/not archived - text = '' - - if not self.video_obj.archive_flag: - self.marked_archive_label.set_markup( - text + _('Archived') + '', - ) - else: - self.marked_archive_label.set_markup( - text + '' + _('Archived') + '', - ) - - # Bookmarked/not bookmarked - text = '' - - if not self.video_obj.bookmark_flag: - self.marked_bookmark_label.set_markup( -# text + _('Bookmarked') + '', - text + _('B/mark') + '', - ) - else: - self.marked_bookmark_label.set_markup( -# text + '' + _('Bookmarked') + '', - text + '' + _('B/mark') + '', - ) - - # Favourite/not favourite - text = '' - - if not self.video_obj.fav_flag: - self.marked_fav_label.set_markup( - text + _('Favourite') + '', - ) - else: - self.marked_fav_label.set_markup( - text + '' + _('Favourite') + '') - - # Missing/not missing - text = '' - - if not self.video_obj.missing_flag: - self.marked_missing_label.set_markup( - text + _('Missing') + '', - ) - else: - self.marked_missing_label.set_markup( - text + '' + _('Missing') + '', - ) - - # New/not new - text = '' - - if not self.video_obj.new_flag: - self.marked_new_label.set_markup( - text + _('New') + '', - ) - else: - self.marked_new_label.set_markup( - text + '' + _('New') + '', - ) - - # In waiting list/not in waiting list - text = '' - if not self.video_obj.waiting_flag: - self.marked_waiting_label.set_markup( - text + _('Waiting') + '', - ) - else: - self.marked_waiting_label.set_markup( - text + '' + _('Waiting') + '', - ) - - - def enable_visible_frame(self, visible_flag): - - """Called by self.draw_widgets() and - mainwin.MainWin.on_draw_frame_checkbutton_changed(). - - Enables or disables the visible frame around the edge of the video. - - Args: - - visible_flag (bool): True to enable the frame, False to disable it - - """ - - # Sanity check: don't let GridCatalogueItem use this inherited method; - # when displaying videos in a grid, the frame is visible (or not) - # around the mainwin.CatalogueGridBox object - if not isinstance(self, ComplexCatalogueItem): - return self.main_win_obj.app_obj.system_error( - 267, - 'CatalogueGridBox has no frame to toggle', - ) - - # (When we're still waiting to set the minimum width for a gridbox, - # then the frame must be visible) - thumb_size = self.main_win_obj.app_obj.thumb_size_custom - - if visible_flag \ - or ( - self.main_win_obj.app_obj.catalogue_mode_type == 'grid' - and self.main_win_obj.catalogue_grid_width_dict[thumb_size] is None - ): - self.frame.set_shadow_type(Gtk.ShadowType.IN) - else: - self.frame.set_shadow_type(Gtk.ShadowType.NONE) - - - def temp_box_is_visible(self): - - """Called by self.draw_widgets and .update_widgets(). - - Checks whether the fifth row of labels (for temporary actions) should - be visible, or not. - - Return values: - - True if the row should be visible, False if not - - """ - - if __main__.__pkg_no_download_flag__: - return False - elif ( - self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_hide_parent_ext' \ - or self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_show_parent_ext' - ) and not self.no_temp_widgets_flag \ - and not self.video_obj.live_mode: - return True - else: - return False - - - def marked_box_is_visible(self): - - """Called by self.draw_widgets and .update_widgets(). - - Checks whether the sixth row of labels (for marked video actions) - should be visible, or not. - - Return values: - - True if the row should be visible, False if not - - """ - - if ( - self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_hide_parent_ext' \ - or self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_show_parent_ext' - ) and not self.video_obj.live_mode: - return True - else: - return False - - - # Callback methods - - - def on_click_descrip_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - When the user clicks on the More/Less label, show more or less of the - video's description. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - """ - - if not self.expand_descrip_flag: - self.expand_descrip_flag = True - else: - self.expand_descrip_flag = False - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.descrip_label.set_text('') - GObject.timeout_add(0, self.update_video_descrip) - - - def on_click_live_auto_alarm_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Toggles auto-sounding alarms when a livestream starts. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Toggle the setting - if not self.video_obj.dbid \ - in self.main_win_obj.app_obj.media_reg_auto_alarm_dict: - self.main_win_obj.app_obj.add_auto_alarm_dict(self.video_obj) - else: - self.main_win_obj.app_obj.del_auto_alarm_dict(self.video_obj) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.live_auto_alarm_label.set_markup(_('Alarm')) - - GObject.timeout_add(0, self.update_livestream_labels) - - return True - - - def on_click_live_auto_dl_start_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Toggles auto-downloading the video when a livestream starts. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Toggle the setting - if not self.video_obj.dbid \ - in self.main_win_obj.app_obj.media_reg_auto_dl_start_dict: - self.main_win_obj.app_obj.add_auto_dl_start_dict(self.video_obj) - else: - self.main_win_obj.app_obj.del_auto_dl_start_dict(self.video_obj) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.live_auto_dl_start_label.set_markup(_('D/L on start')) - - GObject.timeout_add(0, self.update_livestream_labels) - - return True - - - def on_click_live_auto_dl_stop_label(self, label, uri): - - - """Called from callback in self.draw_widgets(). - - Toggles auto-downloading the video when a livestream stops. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Toggle the setting - if not self.video_obj.dbid \ - in self.main_win_obj.app_obj.media_reg_auto_dl_stop_dict: - self.main_win_obj.app_obj.add_auto_dl_stop_dict(self.video_obj) - else: - self.main_win_obj.app_obj.del_auto_dl_stop_dict(self.video_obj) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.live_auto_dl_stop_label.set_markup(_('D/L on stop')) - - GObject.timeout_add(0, self.update_livestream_labels) - - return True - - - def on_click_live_auto_notify_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Toggles auto-notification when a livestream starts. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Toggle the setting - if not self.video_obj.dbid \ - in self.main_win_obj.app_obj.media_reg_auto_notify_dict: - self.main_win_obj.app_obj.add_auto_notify_dict(self.video_obj) - else: - self.main_win_obj.app_obj.del_auto_notify_dict(self.video_obj) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.live_auto_notify_label.set_markup(_('Notify')) - - GObject.timeout_add(0, self.update_livestream_labels) - - return True - - - def on_click_live_auto_open_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Toggles auto-opening the video in the system's web browser when a - livestream starts. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Toggle the setting - if not self.video_obj.dbid \ - in self.main_win_obj.app_obj.media_reg_auto_open_dict: - self.main_win_obj.app_obj.add_auto_open_dict(self.video_obj) - else: - self.main_win_obj.app_obj.del_auto_open_dict(self.video_obj) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.live_auto_open_label.set_markup(_('Open')) - - GObject.timeout_add(0, self.update_livestream_labels) - - return True - - - def on_click_marked_archive_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as archived or not archived. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as archived/not archived - if not self.video_obj.archive_flag: - self.video_obj.set_archive_flag(True) - else: - self.video_obj.set_archive_flag(False) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_archive_label.set_markup(_('Archived')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_bookmark_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as bookmarked or not bookmarked. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as bookmarked/not bookmarked - if not self.video_obj.bookmark_flag: - self.main_win_obj.app_obj.mark_video_bookmark( - self.video_obj, - True, - ) - - else: - self.main_win_obj.app_obj.mark_video_bookmark( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_bookmark_label.set_markup(_('Bookmarked')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_fav_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as favourite or not favourite. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as favourite/not favourite - if not self.video_obj.fav_flag: - self.main_win_obj.app_obj.mark_video_favourite( - self.video_obj, - True, - ) - - else: - self.main_win_obj.app_obj.mark_video_favourite( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_fav_label.set_markup(_('Favourite')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_missing_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as missing or not missing. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as missing/not missing - if not self.video_obj.missing_flag: - self.main_win_obj.app_obj.mark_video_missing( - self.video_obj, - True, - ) - - else: - self.main_win_obj.app_obj.mark_video_missing( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_missing_label.set_markup(_('Missing')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_new_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as new or not new. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as new/not new - if not self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, True) - else: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_new_label.set_markup(_('New')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_waiting_list_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as in the waiting list or not in the waiting list. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as in waiting list/not in waiting list - if not self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - True, - ) - - else: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_waiting_label.set_markup(_('Waiting')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_temp_dl_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Download the video into the 'Temporary Videos' folder. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Can't download the video if an update/refresh/info/tidy/process - # operation is in progress - if not self.main_win_obj.app_obj.update_manager_obj \ - and not self.main_win_obj.app_obj.refresh_manager_obj \ - and not self.main_win_obj.app_obj.info_manager_obj \ - and not self.main_win_obj.app_obj.tidy_manager_obj \ - and not self.main_win_obj.app_obj.process_manager_obj: - - # Create a new media.Video object in the 'Temporary Videos' folder - new_media_data_obj = self.main_win_obj.app_obj.add_video( - self.main_win_obj.app_obj.fixed_temp_folder, - self.video_obj.source, - ) - - if new_media_data_obj: - - # Download the video. If a download operation is already in - # progress, the video is added to it - # Optionally open the video in the system's default media - # player - self.main_win_obj.app_obj.download_watch_videos( - [new_media_data_obj], - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.temp_dl_label.set_markup(_('Download')) - GObject.timeout_add(0, self.update_temp_labels) - - return True - - - def on_click_temp_dl_watch_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Download the video into the 'Temporary Videos' folder. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Can't download the video if an update/refresh/tidy/process operation - # is in progress - if not self.main_win_obj.app_obj.update_manager_obj \ - and not self.main_win_obj.app_obj.refresh_manager_obj \ - and not self.main_win_obj.app_obj.tidy_manager_obj \ - and not self.main_win_obj.app_obj.process_manager_obj: - - # Create a new media.Video object in the 'Temporary Videos' folder - new_media_data_obj = self.main_win_obj.app_obj.add_video( - self.main_win_obj.app_obj.fixed_temp_folder, - self.video_obj.source, - ) - - if new_media_data_obj: - - # Download the video. If a download operation is already in - # progress, the video is added to it - # Optionally open the video in the system's default media - # player - self.main_win_obj.app_obj.download_watch_videos( - [new_media_data_obj], - True, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.temp_dl_watch_label.set_markup(_('D/L and watch')) - GObject.timeout_add(0, self.update_temp_labels) - - return True - - - def on_click_temp_mark_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video for download into the 'Temporary Videos' folder. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Can't mark the video for download if an update/refresh/tidy/process - # operation is in progress - if not self.main_win_obj.app_obj.update_manager_obj \ - and not self.main_win_obj.app_obj.refresh_manager_obj \ - and not self.main_win_obj.app_obj.tidy_manager_obj \ - and not self.main_win_obj.app_obj.process_manager_obj: - - # Create a new media.Video object in the 'Temporary Videos' folder - new_media_data_obj = self.main_win_obj.app_obj.add_video( - self.main_win_obj.app_obj.fixed_temp_folder, - self.video_obj.source, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.temp_mark_label.set_markup(_('Mark for download')) - GObject.timeout_add(0, self.update_temp_labels) - - return True - - - def on_click_watch_hooktube_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a YouTube video on HookTube. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Launch the video - utils.open_file(self.main_win_obj.app_obj, uri) - - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.watch_hooktube_label.set_markup(_('HookTube')) - GObject.timeout_add(0, self.update_watch_web) - - return True - - - def on_click_watch_invidious_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a YouTube video on Invidious. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Launch the video - utils.open_file(self.main_win_obj.app_obj, uri) - - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.watch_invidious_label.set_markup(_('Invidious')) - GObject.timeout_add(0, self.update_watch_web) - - return True - - - def on_click_watch_other_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a YouTube video on the other YouTube front end (specified by the - user). - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Launch the video - utils.open_file(self.main_win_obj.app_obj, uri) - - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.watch_other_label.set_markup(_('Other')) - GObject.timeout_add(0, self.update_watch_web) - - return True - - - def on_click_watch_player_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a video using the system's default media player, first checking - that a file actually exists. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - if self.video_obj.live_mode == 2: - - # Download the video. If a download operation is in progress, the - # video is added to it - app_obj = self.main_win_obj.app_obj - - # If the livestream was downloaded when it was still broadcasting, - # then a new download must overwrite the original file - # As of April 2023, the youtube-dl --yes-overwrites option has been - # removed (but yt-dlp provides --force-overwrites) - # To keep things consistent for all forks, we will rename the - # original file (in case the download fails) - app_obj.prepare_overwrite_video(self.video_obj) - - if not app_obj.download_manager_obj: - - # Start a new download operation - app_obj.download_manager_start( - 'real', - False, - [ self.video_obj ], - ) - - else: - - # Download operation already in progress - download_item_obj \ - = app_obj.download_manager_obj.download_list_obj.create_item( - self.video_obj, - None, # media.Scheduled object - 'real', # override_operation_type - False, # priority_flag - False, # ignore_limits_flag - ) - - if download_item_obj: - - # Add a row to the Progress List - self.main_win_obj.progress_list_add_row( - download_item_obj.item_id, - self.video_obj, - ) - - # Update the main window's progress bar - app_obj.download_manager_obj.nudge_progress_bar() - - elif not self.video_obj.dl_flag and self.video_obj.source \ - and not self.main_win_obj.app_obj.update_manager_obj \ - and not self.main_win_obj.app_obj.refresh_manager_obj \ - and not self.main_win_obj.app_obj.process_manager_obj: - - # Download the video, and mark it to be opened in the system's - # default media player as soon as the download operation is - # complete - # If a download operation is already in progress, the video is - # added to it - self.main_win_obj.app_obj.download_watch_videos( [self.video_obj] ) - - else: - - # Launch the video in the system's media player - self.main_win_obj.app_obj.watch_video_in_player(self.video_obj) - - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.watch_player_label.set_markup(_('Player')) - GObject.timeout_add(0, self.update_watch_player) - - return True - - - def on_click_watch_web_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a video on its primary website. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Launch the video - utils.open_file(self.main_win_obj.app_obj, uri) - - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - enhanced = utils.is_video_enhanced(self.video_obj) - if not enhanced: - self.watch_web_label.set_markup(_('Website')) - else: - self.watch_web_label.set_markup( - formats.ENHANCED_SITE_DICT[enhanced]['pretty_name'], - ) - - GObject.timeout_add(0, self.update_watch_web) - - return True - - - def on_right_click_row(self, event_box, event): - - """Called from callback in self.draw_widgets(). - - When the user right-clicks an the box comprising this - ComplexCatalogueItem, create a context-sensitive popup menu. - - When the user right-clicks an a row, create a context-sensitive popup - menu. - - Args: - - event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the - signal emitted by the click - - """ - - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - - self.main_win_obj.video_catalogue_popup_menu(event, self.video_obj) - - -class GridCatalogueItem(ComplexCatalogueItem): - - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_video(). - - Python class that handles a single gridbox in the Video Catalogue. - - Each mainwin.GridCatalogueItem object stores widgets used in that gridbox, - and updates them when required. - - Args: - - main_win_obj (mainwin.MainWin): The main window object - - video_obj (media.Video): The media data object itself (always a video) - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj): - - # IV list - class objects - # ----------------------- - # The main window object - self.main_win_obj = main_win_obj - # The media data object itself (always a video) - self.video_obj = video_obj - - - # IV list - Gtk widgets - # --------------------- - self.catalogue_gridbox = None # mainwin.CatalogueGridBox - self.grid = None # Gtk.Grid - self.thumb_box = None # Gtk.HBox - self.thumb_image = None # Gtk.Image - self.status_hbox = None # Gtk.HBox - self.status_vbox = None # Gtk.VBox - self.status_image = None # Gtk.Image - self.comment_image = None # Gtk.Image - self.subs_image = None # Gtk.Image - self.slice_image = None # Gtk.Image - self.stamp_image = None # Gtk.Image - self.warning_image = None # Gtk.Image - self.error_image = None # Gtk.Image - self.options_image = None # Gtk.Image - self.grid2 = None # Gtk.Grid - self.name_label = None # Gtk.Label - self.container_label = None # Gtk.Label - self.grid3 = None # Gtk.Grid - self.live_auto_notify_label = None # Gtk.Label - self.live_auto_alarm_label = None # Gtk.Label - self.live_auto_open_label = None # Gtk.Label - self.live_auto_dl_start_label = None - # Gtk.Label - self.live_auto_dl_stop_label = None # Gtk.Label - self.grid4 = None # Gtk.Grid - self.watch_player_label = None # Gtk.Label - self.watch_web_label = None # Gtk.Label - self.watch_hooktube_label = None # Gtk.Label - self.watch_invidious_label = None # Gtk.Label - self.watch_other_label = None # Gtk.Label - self.grid5 = None # Gtk.Grid - self.temp_mark_label = None # Gtk.Label - self.temp_dl_label = None # Gtk.Label - self.temp_dl_watch_label = None # Gtk.Label - self.grid6 = None # Gtk.Grid - self.marked_archive_label = None # Gtk.Label - self.marked_bookmark_label = None # Gtk.Label - self.marked_fav_label = None # Gtk.Label - self.marked_missing_label = None # Gtk.Label - self.marked_new_label = None # Gtk.Label - self.marked_waiting_label = None # Gtk.Label - - - # IV list - other - # --------------- - # Unique ID for this object, matching the .dbid for self.video_obj (an - # integer) - self.dbid = video_obj.dbid - # Size (in pixels) of gaps between various widgets - self.spacing_size = 5 - # Flag set to True if the video's parent folder is a temporary folder, - # meaning that some widgets don't need to be drawn at all - self.no_temp_widgets_flag = False - - # Whenever self.draw_widgets() or .update_widgets() is called, the - # background colour might be changed - # This IV shows the value of the self.video_obj.live_mode, the last - # time either of those functions was called. If the value has - # actually changed, then we ask Gtk to change the background - # (otherwise, we don't) - self.previous_live_mode = 0 - # Flag set to True when the temporary labels box (self.temp_box) is - # visible, False when not - self.temp_box_visible_flag = False - # Flag set to True when the marked labels box (self.marked_box) is - # visible, False when not - self.marked_box_visible_flag = False - - # We can't select widgets on a Gtk.Grid directly, so Tartube implements - # its own 'selection' mechanism - self.selected_flag = False - - - # Public class methods - - - def draw_widgets(self, catalogue_gridbox): - - """Called by mainwin.MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_video(). - - After a Gtk.Frame has been created for this object, populate it with - widgets. - - Args: - - catalogue_gridbox (mainwin.CatalogueGridBox): A wrapper for a - Gtk.Frame object, storing the media.Video object displayed in - that row. - - """ - - # If the video's parent folder is a temporary folder, then we don't - # need one row of widgets at all - parent_obj = self.video_obj.parent_obj - if isinstance(parent_obj, media.Folder) \ - and parent_obj.temp_flag: - self.no_temp_widgets_flag = True - else: - self.no_temp_widgets_flag = False - - # Draw the widgets - self.catalogue_gridbox = catalogue_gridbox - - event_box = Gtk.EventBox() - self.catalogue_gridbox.add(event_box) - event_box.connect('button-release-event', self.on_click_box) - - self.grid = Gtk.Grid() - event_box.add(self.grid) - self.grid.set_border_width(self.spacing_size) - - # Highlight livestreams by specifying a background colour - self.update_background() - - # First row - thumbnail image and status/error/warning icons - self.thumb_box = Gtk.HBox() - self.grid.attach(self.thumb_box, 0, 0, 1, 1) - self.thumb_box.set_hexpand(True) - self.thumb_box.set_vexpand(False) - # (Grid looks better with a small gap under the thumbnail) - self.thumb_box.set_border_width(self.spacing_size) - - self.thumb_image = Gtk.Image() - # Add extra spacing to the side of the image, so that the status icons - # can be placed there - self.thumb_box.pack_start( - self.thumb_image, - True, - True, - (self.spacing_size * 4), - ) - - # Add a second box at the same grid location. This box contains the - # status icons, shifted to the far right. In this way, the - # thumbnail is still centred in the middle of the gridbox, and is - # not drawn over the top of the status icons - self.status_hbox = Gtk.HBox() - self.grid.attach(self.status_hbox, 0, 0, 1, 1) - self.status_hbox.set_hexpand(True) - self.status_hbox.set_vexpand(False) - self.status_hbox.set_border_width(self.spacing_size) - - self.status_vbox = Gtk.VBox() - self.status_hbox.pack_end(self.status_vbox, False, False, 0) - - self.status_image = Gtk.Image() - self.status_vbox.pack_start(self.status_image, False, False, 0) - self.status_image.set_hexpand(False) - - self.comment_image = Gtk.Image() - self.status_vbox.pack_start( - self.comment_image, - False, - False, - self.spacing_size, - ) - self.comment_image.set_hexpand(False) - - self.subs_image = Gtk.Image() - self.status_vbox.pack_start(self.subs_image, False, False, 0) - self.subs_image.set_hexpand(False) - - self.slice_image = Gtk.Image() - self.status_vbox.pack_start( - self.slice_image, - False, - False, - self.spacing_size, - ) - self.slice_image.set_hexpand(False) - - self.stamp_image = Gtk.Image() - self.status_vbox.pack_start(self.stamp_image, False, False, 0) - self.stamp_image.set_hexpand(False) - - self.warning_image = Gtk.Image() - self.status_vbox.pack_start( - self.warning_image, - False, - False, - self.spacing_size, - ) - self.warning_image.set_hexpand(False) - - self.error_image = Gtk.Image() - self.status_vbox.pack_start(self.error_image, False, False, 0) - self.error_image.set_hexpand(False) - - self.options_image = Gtk.Image() - self.status_vbox.pack_start( - self.options_image, - False, - False, - self.spacing_size, - ) - self.options_image.set_hexpand(False) - - # Second row - video name - # (Use sub-grids on several rows so that their column spacing remains - # independent of the others) - self.grid2 = Gtk.Grid() - self.grid.attach(self.grid2, 0, 1, 1, 1) - self.grid2.set_column_spacing(self.spacing_size) - - self.name_label = Gtk.Label('', xalign = 0) - self.grid2.attach(self.name_label, 0, 0, 1, 1) - self.name_label.set_hexpand(True) - - # Third row - parent channel/playlist/folder name - self.container_label = Gtk.Label('', xalign = 0) - self.grid2.attach(self.container_label, 0, 1, 1, 1) - self.container_label.set_hexpand(True) - - # Fourth row - video stats, or livestream notification options, - # depending on settings - self.grid3 = Gtk.Grid() - self.grid.attach(self.grid3, 0, 2, 1, 1) - self.grid3.set_column_spacing(self.spacing_size) - - # (These labels are visible only for livestreams) - # Auto-notify (this label doubles up as the label for video stats, - # when the video is not a livestream) - self.live_auto_notify_label = Gtk.Label('', xalign=0) - self.grid3.attach(self.live_auto_notify_label, 1, 0, 1, 1) - self.live_auto_notify_label.connect( - 'activate-link', - self.on_click_live_auto_notify_label, - ) - - # Auto-sound alarm - self.live_auto_alarm_label = Gtk.Label('', xalign=0) - self.grid3.attach(self.live_auto_alarm_label, 2, 0, 1, 1) - self.live_auto_alarm_label.connect( - 'activate-link', - self.on_click_live_auto_alarm_label, - ) - - # Auto-open - self.live_auto_open_label = Gtk.Label('', xalign=0) - self.grid3.attach(self.live_auto_open_label, 3, 0, 1, 1) - self.live_auto_open_label.connect( - 'activate-link', - self.on_click_live_auto_open_label, - ) - - # D/L on start - self.live_auto_dl_start_label = Gtk.Label('', xalign=0) - self.grid3.attach(self.live_auto_dl_start_label, 4, 0, 1, 1) - self.live_auto_dl_start_label.connect( - 'activate-link', - self.on_click_live_auto_dl_start_label, - ) - - # D/L on stop - self.live_auto_dl_stop_label = Gtk.Label('', xalign=0) - self.grid3.attach(self.live_auto_dl_stop_label, 5, 0, 1, 1) - self.live_auto_dl_stop_label.connect( - 'activate-link', - self.on_click_live_auto_dl_stop_label, - ) - - # Fifth row - Watch... - self.grid4 = Gtk.Grid() - self.grid.attach(self.grid4, 0, 3, 1, 1) - self.grid4.set_column_spacing(self.spacing_size) - - # Watch in player - self.watch_player_label = Gtk.Label('', xalign=0) - self.grid4.attach(self.watch_player_label, 0, 0, 1, 1) - self.watch_player_label.connect( - 'activate-link', - self.on_click_watch_player_label, - ) - - # Watch on website/YouTube - self.watch_web_label = Gtk.Label('', xalign=0) - self.grid4.attach(self.watch_web_label, 1, 0, 1, 1) - self.watch_web_label.connect( - 'activate-link', - self.on_click_watch_web_label, - ) - - # Watch on HookTube - self.watch_hooktube_label = Gtk.Label('', xalign=0) - self.grid4.attach(self.watch_hooktube_label, 2, 0, 1, 1) - self.watch_hooktube_label.connect( - 'activate-link', - self.on_click_watch_hooktube_label, - ) - - # Watch on Invidious - self.watch_invidious_label = Gtk.Label('', xalign=0) - self.grid4.attach(self.watch_invidious_label, 3, 0, 1, 1) - self.watch_invidious_label.connect( - 'activate-link', - self.on_click_watch_invidious_label, - ) - - # Watch on the other YouTube front-end (specified by the user) - self.watch_other_label = Gtk.Label('', xalign=0) - self.grid4.attach(self.watch_other_label, 4, 0, 1, 1) - self.watch_other_label.connect( - 'activate-link', - self.on_click_watch_other_label, - ) - - # Optional rows - - # Sixth row: Temporary... - self.grid5 = Gtk.Grid() - if self.temp_box_is_visible(): - self.grid.attach(self.grid5, 0, 4, 1, 1) - self.temp_box_visible_flag = True - - self.grid5.set_column_spacing(self.spacing_size) - - # Mark for download - self.temp_mark_label = Gtk.Label('', xalign=0) - self.grid5.attach(self.temp_mark_label, 0, 0, 1, 1) - self.temp_mark_label.connect( - 'activate-link', - self.on_click_temp_mark_label, - ) - - # Download - self.temp_dl_label = Gtk.Label('', xalign=0) - self.grid5.attach(self.temp_dl_label, 1, 0, 1, 1) - self.temp_dl_label.connect( - 'activate-link', - self.on_click_temp_dl_label, - ) - - # Download and watch - self.temp_dl_watch_label = Gtk.Label('', xalign=0) - self.grid5.attach(self.temp_dl_watch_label, 2, 0, 1, 1) - self.temp_dl_watch_label.connect( - 'activate-link', - self.on_click_temp_dl_watch_label, - ) - - # Seventh row: Marked... - self.grid6 = Gtk.Grid() - if self.marked_box_is_visible: - self.grid.attach(self.grid6, 0, 5, 1, 1) - self.marked_box_visible_flag = True - - self.grid6.set_column_spacing(self.spacing_size) - - # Archived/not archived - self.marked_archive_label = Gtk.Label('', xalign=0) - self.grid6.attach(self.marked_archive_label, 0, 0, 1, 1) - self.marked_archive_label.connect( - 'activate-link', - self.on_click_marked_archive_label, - ) - - # Bookmarked/not bookmarked - self.marked_bookmark_label = Gtk.Label('', xalign=0) - self.grid6.attach(self.marked_bookmark_label, 1, 0, 1, 1) - self.marked_bookmark_label.connect( - 'activate-link', - self.on_click_marked_bookmark_label, - ) - - # Favourite/not favourite - self.marked_fav_label = Gtk.Label('', xalign=0) - self.grid6.attach(self.marked_fav_label, 2, 0, 1, 1) - self.marked_fav_label.connect( - 'activate-link', - self.on_click_marked_fav_label, - ) - - # Missing/not missing - self.marked_missing_label = Gtk.Label('', xalign=0) - self.grid6.attach(self.marked_missing_label, 3, 0, 1, 1) - self.marked_missing_label.connect( - 'activate-link', - self.on_click_marked_missing_label, - ) - - # New/not new - self.marked_new_label = Gtk.Label('', xalign=0) - self.grid6.attach(self.marked_new_label, 4, 0, 1, 1) - self.marked_new_label.connect( - 'activate-link', - self.on_click_marked_new_label, - ) - - # In waiting list/not in waiting list - self.marked_waiting_label = Gtk.Label('', xalign=0) - self.grid6.attach(self.marked_waiting_label, 5, 0, 1, 1) - self.marked_waiting_label.connect( - 'activate-link', - self.on_click_marked_waiting_list_label, - ) - - - def update_widgets(self): - - """Called by mainwin.MainWin.video_catalogue_redraw_all(), - .video_catalogue_update_video() and .video_catalogue_insert_video(). - - Sets the values displayed by each widget. - """ - - self.update_background() - self.update_tooltips() - self.update_thumb_image() - self.update_status_images() - self.update_video_name() - self.update_container_name() - self.update_video_stats() - self.update_watch_player() - self.update_watch_web() - - # If the fifth/sixth rows are not currently visible, but need to be - # visible, make them visible (and vice-versa) - if not self.temp_box_is_visible(): - - if self.temp_box_visible_flag: - self.grid.remove(self.grid5) - self.temp_box_visible_flag = False - - else: - - self.update_temp_labels() - if not self.temp_box_visible_flag: - self.grid.attach(self.grid5, 0, 4, 1, 1) - self.temp_box_visible_flag = True - - if not self.marked_box_is_visible(): - - if self.marked_box_visible_flag: - self.grid.remove(self.grid6) - self.marked_box_visible_flag = False - - else: - - self.update_marked_labels() - if not self.marked_box_visible_flag: - self.grid.attach(self.grid6, 0, 5, 1, 1) - self.marked_box_visible_flag = True - - - def update_background(self, force_flag=False): - - """Calledy by self.draw_widgets(), .update_widgets(), .do_select() and - .toggle_select(). - - Updates the background colour to show which videos are livestreams - (but only when a video's livestream mode has changed). - - Note that calls to self.do_select() can also update the background - colour. - - Args: - - force_flag (bool): True when called from self.do_select() and - .toggle_select(), in which case the background is updated, - regardless of whether the media.Video's .live_mode IV has - changed - - """ - - if force_flag or self.previous_live_mode != self.video_obj.live_mode: - - self.previous_live_mode = self.video_obj.live_mode - - if not self.selected_flag: - - if self.video_obj.live_mode == 0 \ - or not self.main_win_obj.app_obj.livestream_use_colour_flag: - - self.catalogue_gridbox.override_background_color( - Gtk.StateType.NORMAL, - None, - ) - - elif self.video_obj.live_mode == 1: - - if not self.video_obj.live_debut_flag \ - or self.main_win_obj.app_obj.livestream_simple_colour_flag: - - self.catalogue_gridbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.live_wait_colour, - ) - - else: - - self.catalogue_gridbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.debut_wait_colour, - ) - - elif self.video_obj.live_mode == 2: - - if not self.video_obj.live_debut_flag \ - or self.main_win_obj.app_obj.livestream_simple_colour_flag: - - self.catalogue_gridbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.live_now_colour, - ) - - else: - - self.catalogue_gridbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.debut_now_colour, - ) - - else: - - # (For selected gridboxes, simplify the colour scheme by not - # distinguishing between debut and non-debut videos) - if self.video_obj.live_mode == 0 \ - or not self.main_win_obj.app_obj.livestream_use_colour_flag: - - self.catalogue_gridbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.grid_select_colour, - ) - - elif self.video_obj.live_mode == 1: - - self.catalogue_gridbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.grid_select_wait_colour, - ) - - elif self.video_obj.live_mode == 2: - - self.catalogue_gridbox.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.grid_select_live_colour, - ) - - - def update_tooltips(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the tooltips for the Gtk.Frame that contains everything. - """ - - if self.main_win_obj.app_obj.show_tooltips_flag: - self.catalogue_gridbox.set_tooltip_text( - self.video_obj.fetch_tooltip_text( - self.main_win_obj.app_obj, - self.main_win_obj.tooltip_max_len, - ), - ) - - - def update_thumb_image(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Image widget to display the video's thumbnail, if - available. - """ - - app_obj = self.main_win_obj.app_obj - thumb_size = app_obj.thumb_size_custom - gridbox_min_width \ - = self.main_win_obj.catalogue_grid_width_dict[thumb_size] - - # See if the video's thumbnail file has been downloaded - thumb_flag = False - if self.video_obj.file_name: - - # No way to know which image format is used by all websites for - # their video thumbnails, so look for the most common ones - # The True argument means that if the thumbnail isn't found in - # Tartube's main data directory, look in the temporary directory - # too - path = utils.find_thumbnail( - self.main_win_obj.app_obj, - self.video_obj, - True, - ) - - if path: - - # Thumbnail file exists, so use it - mini_list = app_obj.thumb_size_dict[thumb_size] - - # (Returns a tuple, who knows why) - arglist = app_obj.file_manager_obj.load_to_pixbuf( - path, - mini_list[0], # width - mini_list[1], # height - ), - - if arglist[0]: - self.thumb_image.set_from_pixbuf(arglist[0]) - thumb_flag = True - - # No thumbnail file found, so use a default icon file - if not thumb_flag: - - if not self.video_obj.block_flag: - thumb_type = 'default' - else: - thumb_type = 'block' - - pixbuf_name = 'thumb_' + thumb_type + '_' + thumb_size - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict[pixbuf_name], - ) - - - def update_video_name(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the video's current name. - """ - - app_obj = self.main_win_obj.app_obj - thumb_size = app_obj.thumb_size_custom - gridbox_min_width \ - = self.main_win_obj.catalogue_grid_width_dict[thumb_size] - - # For videos whose name is unknown, display the URL, rather than the - # usual '(video with no name)' string - if not self.main_win_obj.app_obj.catalogue_show_nickname_flag: - name = self.video_obj.name - else: - name = self.video_obj.nickname - - if name is None or name == app_obj.default_video_name: - - if self.video_obj.source is not None: - - # Using pango markup to display a URL is too risky, so just use - # ordinary text - self.name_label.set_text( - utils.shorten_string( - self.video_obj.source, - self.main_win_obj.quite_long_string_max_len, - ), - ) - - return - - else: - - # No URL to show, so we're forced to use '(video with no name)' - name = app_obj.default_video_name - - string = '' - if self.video_obj.new_flag: - string += ' font_weight="bold"' - - if self.video_obj.dl_sim_flag: - string += ' style="italic"' - - # The video name is split into two lines, if there is enough text. - # Set the length of a the lines matching the size of the thumbnail - if thumb_size == 'tiny': - max_line_length = self.main_win_obj.medium_string_max_len - elif thumb_size == 'small': - max_line_length = self.main_win_obj.quite_long_string_max_len - elif thumb_size == 'medium': - max_line_length = self.main_win_obj.long_string_max_len - elif thumb_size == 'large': - max_line_length = self.main_win_obj.very_long_string_max_len - else: - max_line_length = self.main_win_obj.exceedingly_long_string_max_len - - self.name_label.set_markup( - '' + \ - html.escape( - utils.shorten_string_two_lines( - name, - max_line_length, - ), - quote=True, - ) + '' - ) - - - def update_container_name(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the parent channel/playlist/ - folder name - """ - - if self.video_obj.orig_parent is not None: - parent_obj = self.video_obj.orig_parent - else: - parent_obj = self.video_obj.parent_obj - - if isinstance(parent_obj, media.Channel): - string = _('Channel') + ': ' - elif isinstance(parent_obj, media.Playlist): - string = _('Playlist') + ': ' - else: - string = _('Folder') + ': ' - - string2 = html.escape( - utils.shorten_string( - parent_obj.name, - self.main_win_obj.quite_long_string_max_len, - ), - quote=True, - ) - - if isinstance(parent_obj, media.Folder) \ - or parent_obj.source is None \ - or not self.main_win_obj.app_obj.catalogue_clickable_container_flag: - - self.container_label.set_markup( - '' + string + ' ' + string2, - ) - - else: - - self.container_label.set_markup( - '' + string + ' ' + string2 + '', - ) - - - def update_video_stats(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the Gtk.Label widget to display the video's current side/ - duration/date information. - - For livestreams, instead displays livestream options. - """ - - # Import the main application (for convenience) - app_obj = self.main_win_obj.app_obj - - if not self.video_obj.live_mode: - - if self.video_obj.duration is not None: - string = utils.convert_seconds_to_string( - self.video_obj.duration, - True, - ) - - else: - string = _('unknown') - - size = self.video_obj.get_file_size_string() - if size != "": - string = string + ' - ' + size - else: - string = string + ' - ' + _('unknown') - - pretty_flag = self.main_win_obj.app_obj.show_pretty_dates_flag - if app_obj.catalogue_sort_mode == 'receive': - date = self.video_obj.get_receive_date_string(pretty_flag) - else: - date = self.video_obj.get_upload_date_string(pretty_flag) - - if date is not None: - string = string + ' - ' + date - else: - string = string + ' - ' + _('unknown') - - self.live_auto_notify_label.set_markup(string) - self.live_auto_alarm_label.set_text('') - self.live_auto_open_label.set_text('') - self.live_auto_dl_start_label.set_text('') - self.live_auto_dl_stop_label.set_text('') - - else: - - name = html.escape(self.video_obj.name) - dbid = self.video_obj.dbid - - if dbid in app_obj.media_reg_auto_notify_dict: - label = '' + _('Notify') + '' - else: - label = _('Notify') - - # Currently disabled on MS Windows - if os.name == 'nt': - self.live_auto_notify_label.set_markup(_('Notify')) - else: - self.live_auto_notify_label.set_markup( - '' + label + '', - ) - - if not mainapp.HAVE_PLAYSOUND_FLAG: - - self.live_auto_alarm_label.set_markup('Alarm') - - else: - - if dbid in app_obj.media_reg_auto_alarm_dict: - label = '' + _('Alarm') + '' - else: - label = _('Alarm') - - self.live_auto_alarm_label.set_markup( - '' + label + '', - ) - - if dbid in app_obj.media_reg_auto_open_dict: - label = '' + _('Open') + '' - else: - label = _('Open') - - self.live_auto_open_label.set_markup( - '' + label + '', - ) - - if __main__.__pkg_no_download_flag__ \ - or self.video_obj.live_mode == 2: - - # (Livestream already broadcasting) - self.live_auto_dl_start_label.set_markup(_('D/L on start')) - - else: - - if dbid in app_obj.media_reg_auto_dl_start_dict: - label = '' + _('D/L on start') + '' - else: - label = _('D/L on start') - - self.live_auto_dl_start_label.set_markup( - '' + label + '', - ) - - if __main__.__pkg_no_download_flag__: - - self.live_auto_dl_stop_label.set_markup(_('D/L on stop')) - - else: - - if dbid in app_obj.media_reg_auto_dl_stop_dict: - label = '' + _('D/L on stop') + '' - else: - label = _('D/L on stop') - - self.live_auto_dl_stop_label.set_markup( - '' + label + '', - ) - - - def update_watch_player(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the clickable Gtk.Label widget for watching the video in an - external media player. - """ - - if self.video_obj.file_name: - watch_text = '' \ - + _('Player') + '' - - # (Many labels are not clickable when a channel/playlist/folder's - # external directory is marked disabled) - if self.video_obj.parent_obj.dbid \ - in self.main_win_obj.app_obj.container_unavailable_dict: - - # Link not clickable - self.watch_player_label.set_markup(_('Download')) - - elif __main__.__pkg_no_download_flag__: - - if self.video_obj.file_name and self.video_obj.dl_flag: - - # Link clickable - self.watch_player_label.set_markup(watch_text) - - else: - - # Link not clickable - self.watch_player_label.set_markup(_('Download')) - - elif self.video_obj.live_mode == 1: - - if self.video_obj.live_msg == '': - - # Link not clickable - if not self.video_obj.live_debut_flag: - self.watch_player_label.set_markup(_('Live soon:')) - else: - self.watch_player_label.set_markup(_('Debut soon:')) - - else: - - self.watch_player_label.set_markup( - self.video_obj.live_msg + ':', - ) - - elif self.video_obj.live_mode == 2: - - ignore_me = _('TRANSLATOR\'S NOTE: D/L means download') - - # Link clickable - self.watch_player_label.set_markup( - '' \ - + _('Download') + '', - ) - - elif self.video_obj.file_name and self.video_obj.dl_flag: - - # Link clickable - self.watch_player_label.set_markup(watch_text) - - elif self.video_obj.source \ - and not self.main_win_obj.app_obj.update_manager_obj \ - and not self.main_win_obj.app_obj.refresh_manager_obj \ - and not self.main_win_obj.app_obj.process_manager_obj: - - ignore_me = _( - 'TRANSLATOR\'S NOTE: If you want to use &, use &' \ - + ' - if you want to use a different word (e.g. French et)' \ - + ', then just use that word', - ) - - # Link clickable - self.watch_player_label.set_markup( - '' + _('D/L & watch') + '', - ) - - else: - - # Link not clickable - self.watch_player_label.set_markup( - '' + _('Can\'t D/L') + '', - ) - - - def update_marked_labels(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the clickable Gtk.Label widget for video properties. - """ - - if self.video_obj.file_name: - link_text = self.video_obj.get_actual_path( - self.main_win_obj.app_obj, - ) - elif self.video_obj.source: - link_text = self.video_obj.source - else: - link_text = '' - - ignore_me = _( - 'TRANSLATOR\'S NOTE: This section contains shortened' \ - + ' labels: Archive = Archived, B/Mark = Bookmarked,' \ - + ' Waiting: In waiting list', - ) - - # Archived/not archived - text = '' - - if not self.video_obj.archive_flag: - self.marked_archive_label.set_markup( - text + _('Archived') + '', - ) - else: - self.marked_archive_label.set_markup( - text + '' + _('Archived') + '', - ) - - # Bookmarked/not bookmarked - text = '' - - if not self.video_obj.bookmark_flag: - self.marked_bookmark_label.set_markup( - text + _('B/mark') + '', - ) - else: - self.marked_bookmark_label.set_markup( - text + '' + _('B/mark') + '', - ) - - # Favourite/not favourite - text = '' - - if not self.video_obj.fav_flag: - self.marked_fav_label.set_markup( - text + _('Favourite') + '', - ) - else: - self.marked_fav_label.set_markup( - text + '' + _('Favourite') + '') - - # Missing/not missing - text = '' - - if not self.video_obj.missing_flag: - self.marked_missing_label.set_markup( - text + _('Missing') + '', - ) - else: - self.marked_missing_label.set_markup( - text + '' + _('Missing') + '', - ) - - # New/not new - text = '' - - if not self.video_obj.new_flag: - self.marked_new_label.set_markup( - text + _('New') + '', - ) - else: - self.marked_new_label.set_markup( - text + '' + _('New') + '', - ) - - # In waiting list/not in waiting list - text = '' - if not self.video_obj.waiting_flag: - self.marked_waiting_label.set_markup( - text + _('Waiting') + '', - ) - else: - self.marked_waiting_label.set_markup( - text + '' + _('Waiting') + '', - ) - - - def temp_box_is_visible(self): - - """Called by self.draw_widgets and .update_widgets(). - - Checks whether the fifth row of labels (for temporary actions) should - be visible, or not. - - Return values: - - True if the row should be visible, False if not - - """ - - if __main__.__pkg_no_download_flag__: - return False - elif ( - self.main_win_obj.app_obj.catalogue_mode - == 'grid_show_parent_ext' - ) and not self.no_temp_widgets_flag \ - and not self.video_obj.live_mode: - return True - else: - return False - - - def marked_box_is_visible(self): - - """Called by self.draw_widgets and .update_widgets(). - - Checks whether the sixth row of labels (for marked video actions) - should be visible, or not. - - Return values: - - True if the row should be visible, False if not - - """ - - if ( - self.main_win_obj.app_obj.catalogue_mode \ - == 'grid_show_parent_ext' \ - ) and not self.video_obj.live_mode: - return True - else: - return False - - - # (Methods unique to this class) - - - def toggle_select(self): - - """Called by mainwin.MainWin.video_catalogue_grid_select(). - - Selects/unselects this catalogue object. - """ - - if not self.selected_flag: - - self.selected_flag = True - # (Grabbing keyboard focus enables selection using the cursor and - # page up/page down keys to work properly) - self.catalogue_gridbox.grab_focus() - - else: - - self.selected_flag = False - - # (The True argument marks this function as the caller) - self.update_background(True) - - - def do_select(self, select_flag): - - """Called by mainwin.MainWin.video_catalogue_unselect_all() and - .video_catalogue_grid_select(). - - Selects/unselects this catalogue object, and to instruct the - CatalogueGridBox to grab focus, if required. - - Args: - - select_flag (bool): True to select the catalogue object, False to - unselect it - - """ - - if not select_flag: - - self.selected_flag = False - - else: - - self.selected_flag = True - # (Grabbing keyboard focus enables selection using the cursor and - # page up/page down keys to work properly) - self.catalogue_gridbox.grab_focus() - - # (The True argument marks this function as the caller) - self.update_background(True) - - - # Callback methods - - - def on_click_marked_archive_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as archived or not archived. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as archived/not archived - if not self.video_obj.archive_flag: - self.video_obj.set_archive_flag(True) - else: - self.video_obj.set_archive_flag(False) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_archive_label.set_markup(_('Archived')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_bookmark_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as bookmarked or not bookmarked. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as bookmarked/not bookmarked - if not self.video_obj.bookmark_flag: - self.main_win_obj.app_obj.mark_video_bookmark( - self.video_obj, - True, - ) - - else: - self.main_win_obj.app_obj.mark_video_bookmark( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_bookmark_label.set_markup(_('B/mark')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_waiting_list_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as in the waiting list or not in the waiting list. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - Return values: - - True to show the action has been handled - - """ - - # Mark the video as in waiting list/not in waiting list - if not self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - True, - ) - - else: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # this function returns. Workaround is to make the label unclickable, - # then use a Glib timer to restore it (after some small fraction of a - # second) - self.marked_waiting_label.set_markup(_('Waiting')) - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_box(self, event_box, event): - - """Called from callback in self.draw_widgets(). - - When the user left-clicks the box comprising this GridCatalogueItem, - 'select' or 'unselect' it. - - When the user rights the box, create a context-sensitive popup menu.. - - Args: - - event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the - signal emitted by the click - - """ - - if event.type == Gdk.EventType.BUTTON_RELEASE: - - if event.button == 1: - - # We can't select widgets on a Gtk.Grid directly, so Tartube - # implements its own 'selection' mechanism - if (event.state & Gdk.ModifierType.SHIFT_MASK): - - self.main_win_obj.video_catalogue_grid_select( - self, - 'shift', - ) - - elif (event.state & Gdk.ModifierType.CONTROL_MASK): - - self.main_win_obj.video_catalogue_grid_select( - self, - 'ctrl', - ) - - else: - - self.main_win_obj.video_catalogue_grid_select( - self, - 'default', - ) - - elif event.button == 3: - - self.main_win_obj.video_catalogue_popup_menu( - event, - self.video_obj, - ) - - -class CatalogueRow(Gtk.ListBoxRow): - - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_video(). - - Python class acting as a wrapper for Gtk.ListBoxRow, so that we can - retrieve the media.Video object displayed in each row. - - Args: - - main_win_obj (mainwin.MainWin): The main window - - video_obj (media.Video): The video object displayed on this row - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj): - - super(Gtk.ListBoxRow, self).__init__() - - # IV list - class objects - # ----------------------- - self.main_win_obj = main_win_obj - self.video_obj = video_obj - - - # Code - # ---- - - # Set up drag and drop from the gridbox to an external application - # (for example, an FFmpeg batch converter), and also to the Video - # Index - self.drag_source_set( - Gdk.ModifierType.BUTTON1_MASK, - [], - Gdk.DragAction.COPY, - ) - self.drag_source_add_text_targets() - self.connect( - 'drag-data-get', - self.on_drag_data_get, - ) - self.connect( - 'drag-end', - self.on_drag_end, - ) - - - # Callback class methods - - - def on_drag_data_get(self, widget, drag_context, data, \ - info, time): - - """Called from callback in self.__init__(). - - Set the data to be used when the user drags and drops videos from the - Video Catalogue to an external application (for example, an FFmpeg - batch converter). - - Args: - - widget (mainwin.CatalogueRow): The widget handling the video in the - Video Catalogue - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - data (Gtk.SelectionData): The object to be filled with drag data - - info (int): Info that has been registered with the target in the - Gtk.TargetList - - time (int): A timestamp - - """ - - if info == 0: # TARGET_ENTRY_TEXT - - # If this row is selected, and other rows are also selected, they - # are all dragged together. Otherwise, only this row is dragged - selected_list = [] - for catalogue_item_obj \ - in self.main_win_obj.catalogue_listbox.get_selected_rows(): - selected_list.append(catalogue_item_obj.video_obj) - - if self.video_obj in selected_list: - video_list = selected_list.copy() - else: - video_list = [ self.video_obj ] - - # Transfer to the external application a single string, containing - # one or more full file paths/URLs/video names, separated by - # newline characters - # If the path/URL/name isn't known for any videos, then an empty - # line is transferred - if info == 0: # TARGET_ENTRY_TEXT - - data.set_text( - self.main_win_obj.get_video_drag_data(video_list), - -1, - ) - - # For the benefit of videos being dragged from the Video Catalogue - # into the Video Index, we also store the list of videos in the - # main window's IV temporarily - self.main_win_obj.set_video_catalogue_drag_list(video_list) - - - def on_drag_end(self, widget, drag_context): - - """Called from callback in self.__init__(). - - Resets the main window's list of videos being dragged from the Video - Catalogue (potentially into the Video Index). - - Args: - - widget (mainwin.CatalogueGridBox): The widget handling the video in - the Video Catalogue - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - """ - - # For the benefit of videos being dragged from the Video Catalogue - # into the Video Index, reset the main window's IV - self.main_win_obj.reset_video_catalogue_drag_list() - - -class CatalogueGridBox(Gtk.Frame): - - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_video(). - - Python class acting as a wrapper for Gtk.Frame, so that we can retrieve the - media.Video object displayed in each gridbox. - - Args: - - main_win_obj (mainwin.MainWin): The main window - - video_obj (media.Video): The video object displayed in this gridbox - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj): - - super(Gtk.Frame, self).__init__() - - # IV list - class objects - # ----------------------- - self.main_win_obj = main_win_obj - self.video_obj = video_obj - - - # IV list - other - # --------------- - # The coordinates of the gridbox on the Gtk.Grid (required so that - # selection can be handled correctly) - self.x_pos = None - self.y_pos = None - - - # Code - # ---- - - self.enable_visible_frame( - main_win_obj.app_obj.catalogue_draw_frame_flag, - ) - - # When the Tartube main window first opens, we don't know how much - # horizontal space will be consumed by a gridbox until some time - # after a gridbox is drawn - # Therefore, don't let gridboxes expand vertically until the minimum - # size has been determined - self.set_vexpand(False) - - thumb_size = main_win_obj.app_obj.thumb_size_custom - if not main_win_obj.catalogue_grid_expand_flag: - self.set_hexpand(False) - else: - self.set_hexpand(True) - - # This callback will set the size of the first CatalogueGridBox, which - # tells us the minimum required size for all future gridboxes - self.connect('size-allocate', self.on_size_allocate) - - # Intercept cursor and page up/down keys, and in response scroll the - # Video Catalogue up/down - self.set_can_focus(True) - self.connect('key-press-event', self.on_key_press_event) - - # Set up drag and drop from the gridbox to an external application - # (for example, an FFmpeg batch converter), and also to the Video - # Index - self.drag_source_set( - Gdk.ModifierType.BUTTON1_MASK, - [], - Gdk.DragAction.COPY, - ) - self.drag_source_add_text_targets() - self.connect( - 'drag-data-get', - self.on_drag_data_get, - ) - self.connect( - 'drag-end', - self.on_drag_end, - ) - - - # Public class methods - - - def set_posn(self, x_pos, y_pos): - - """Called by mainwin.MainWin.video_catalogue_redraw_all(), - .video_catalogue_insert_video() and .video_catalogue_grid_rearrange(). - - Sets the coordinates of this gridbox on the grid, so that selection - can be handled properly. - - Args: - - x_pos, y_pos (int): Coordinates on a Gtk.Grid - - """ - - self.x_pos = x_pos - self.y_pos = y_pos - - - def enable_visible_frame(self, visible_flag): - - """Called by self.__init__(), - mainwin.MainWin.video_catalogue_grid_set_gridbox_width() and - .on_draw_frame_checkbutton_changed(). - - Enables/disables the visible frame drawn around the edge of the - gridbox (if allowed). - - Args: - - visible_flag (bool): True to enable the frame, False to disable it - - """ - - thumb_size = self.main_win_obj.app_obj.thumb_size_custom - - # (When we're still waiting to set the minimum width for a gridbox, - # then the frame must be visible, regardless of the specified flag) - if visible_flag \ - or self.main_win_obj.catalogue_grid_width_dict[thumb_size] is None: - self.set_shadow_type(Gtk.ShadowType.IN) - else: - self.set_shadow_type(Gtk.ShadowType.NONE) - - - # Callback class methods - - - def on_drag_data_get(self, widget, drag_context, data, \ - info, time): - - """Called from callback in self.__init__(). - - Set the data to be used when the user drags and drops videos from the - Video Catalogue to an external application (for example, an FFmpeg - batch converter). - - Args: - - widget (mainwin.CatalogueGridBox): The widget handling the video in - the Video Catalogue - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - data (Gtk.SelectionData): The object to be filled with drag data - - info (int): Info that has been registered with the target in the - Gtk.TargetList - - time (int): A timestamp - - """ - - if info == 0: # TARGET_ENTRY_TEXT - - # If this gridbox is selected, and other gridboxes are also - # selected, they are all dragged together. Otherwise, only this - # gridbox is dragged - selected_list = [] - for catalogue_item_obj \ - in self.main_win_obj.video_catalogue_dict.values(): - if catalogue_item_obj.selected_flag: - selected_list.append(catalogue_item_obj.video_obj) - - if self.video_obj in selected_list: - video_list = selected_list.copy() - else: - video_list = [ self.video_obj ] - - # Transfer to the external application a single string, containing - # one or more full file paths/URLs/video names, separated by - # newline characters - # If the path/URL/name isn't known for any videos, then an empty - # line is transferred - if info == 0: # TARGET_ENTRY_TEXT - - data.set_text( - self.main_win_obj.get_video_drag_data(video_list), - -1, - ) - - # For the benefit of videos being dragged from the Video Catalogue - # into the Video Index, we also store the list of videos in the - # main window's IV temporarily - self.main_win_obj.set_video_catalogue_drag_list(video_list) - - - def on_drag_end(self, widget, drag_context): - - """Called from callback in self.__init__(). - - Resets the main window's list of videos being dragged from the Video - Catalogue (potentially into the Video Index). - - Args: - - widget (mainwin.CatalogueGridBox): The widget handling the video in - the Video Catalogue - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - """ - - # For the benefit of videos being dragged from the Video Catalogue - # into the Video Index, reset the main window's IV - self.main_win_obj.reset_video_catalogue_drag_list() - - - def on_key_press_event(self, widget, event): - - """Called from callback in self.__init__(). - - Intercept keypresses when this gridbox has keyboard focus. - - The cursor and page up/down keys are passed to the main window so that - the Video Catalogue can be scrolled; all other keys are ignored. - - Args: - - widget (mainwin.CatalogueGridBox): The clicked widget - - event (Gdk.EventButton): The event emitting the Gtk signal - - Return values: - - True to show the action has been handled, or False if the action - has been ignored - - """ - - if event.type != Gdk.EventType.KEY_PRESS: - return - - # 'Up', 'Left', 'Page_Up', etc. Also 'a' for CTRL+A - keyval = Gdk.keyval_name(event.keyval) - if not keyval in self.main_win_obj.catalogue_grid_intercept_dict: - return False - - else: - - if (event.state & Gdk.ModifierType.SHIFT_MASK): - select_type = 'shift' - elif (event.state & Gdk.ModifierType.CONTROL_MASK): - select_type = 'ctrl' - else: - select_type = 'default' - - if keyval == 'a': - - if select_type == 'ctrl': - - self.main_win_obj.video_catalogue_grid_select_all() - - # Return True to show that we have interfered with this - # keypress - return True - - else: - return False - - else: - - self.main_win_obj.video_catalogue_grid_scroll_on_select( - self, - keyval, - select_type, - ) - - # Return True to show that we have interfered with this - # keypress - return True - - - def on_size_allocate(self, widget, rect): - - """Called from callback in self.__init__(). - - When gridboxes are added to the Video Catalogue, the minimum horizontal - space required to fit all of its widgets is not available. - - When it becomes available, this function is called, so that the size - can be passed on to the main window's code. - - Args: - - widget (mainwin.CatalogueGridBox): The clicked widget - - rect (Gdk.Rectangle): Object describing the window's new size - - """ - - thumb_size = self.main_win_obj.app_obj.thumb_size_custom - min_width = self.main_win_obj.catalogue_grid_width_dict[thumb_size] - - if rect.width > 1 and min_width is None: - self.main_win_obj.video_catalogue_grid_set_gridbox_width( - rect.width, - ) - - - # Set accessors - - - def set_expandable(self, expand_flag): - - """Called by mainwin.MainWin.video_catalogue_grid_check_expand(). - - Allows/prevents this gridbox to expand horizontally in its parent - Gtk.Grid, depending on various aesthetic requirements. - - Args: - - expand_flag (bool): True to allow horizontal expansion, False to - prevent it - - """ - - if not expand_flag: - self.set_hexpand(False) - else: - self.set_hexpand(True) - - -class DropZoneBox(Gtk.Frame): - - """Called by MainWin.drag_drop_grid_reset(). - - Python class acting as a wrapper for Gtk.Frame, so that we can retrieve the - options.OptionsManager object displayed in each dropzone. - - Args: - - main_win_obj (mainwin.MainWin): The main window - - options_obj (options.OptionsManager or None): The download options - associated with this dropzone, or None for an empty dropzone - - update_text (str or None): When the grid is re-drawn, any messages - displayed inside the dropzone are copied across to each - replacment dropzone - - reset_time (int or None): The same applies to the time at which those - messages are to be removed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, options_obj, x_pos, y_pos, height, - update_text=None, reset_time=None): - - super(Gtk.Frame, self).__init__() - - # IV list - class objects - # ----------------------- - self.main_win_obj = main_win_obj - # (If this is a blank dropzone, then 'options_obj' is None) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Coordinates of this dropzone on the Drag and Drop tab's grid - self.x_pos = x_pos - self.y_pos = y_pos - # Height of the Drag and Drop tab's grid - self.height = height - - # This dropzone's own Gtk.Grid, on which widgets are drawn - self.grid = None - - # Current messages displayed in the update label (in case this box is - # replaced by a new one, before the message is reset) - self.update_text = update_text - # The time (matches time.time() at which those messages are due to be - # reset (None if no messages are visible) - self.reset_time = reset_time - - # Code - # ---- - - # Set up widgets - self.draw_widgets() - # Set up drag and drop into this frame - if self.options_obj: - self.connect('drag-data-received', self.on_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - - # Public class methods - - - def draw_widgets(self): - - """Called by self.__init__(). - - Populate the dropzone with widgets. - """ - - self.grid = Gtk.Grid() - self.add(self.grid) - self.grid.set_column_spacing(self.main_win_obj.spacing_size) - self.grid.set_row_spacing(self.main_win_obj.spacing_size * 2) - self.grid.set_border_width(self.main_win_obj.spacing_size) - - # (Depending on the size of the grid, add spacing between widgets, or - # not) - row = 0 - - if self.height < 4: - # (Empty box for spacing) - box = Gtk.Box() - self.grid.attach(box, 0, row, 1, 1) - box.set_vexpand(True) - row += 1 - - self.name_label = Gtk.Label() - self.grid.attach(self.name_label, 0, row, 1, 1) - self.name_label.set_hexpand(True) - row += 1 - - self.descrip_label = Gtk.Label() - self.grid.attach(self.descrip_label, 0, row, 1, 1) - self.descrip_label.set_hexpand(True) - row += 1 - - if self.height < 3: - # (Empty box for spacing) - box = Gtk.Box() - self.grid.attach(box, 0, row, 1, 1) - box.set_vexpand(True) - row += 1 - - self.update_label = Gtk.Label() - self.grid.attach(self.update_label, 0, row, 1, 1) - self.update_label.set_hexpand(True) - row += 1 - - # (Empty box for spacing) - box = Gtk.Box() - self.grid.attach(box, 0, row, 1, 1) - box.set_vexpand(True) - row += 1 - - # Strip of buttons at the bottom - hbox = Gtk.HBox() - self.grid.attach(hbox, 0, row, 1, 1) - row += 1 - - if self.options_obj: - - if not self.main_win_obj.app_obj.show_custom_icons_flag: - button = Gtk.Button.new_from_icon_name( - Gtk.STOCK_DELETE, - Gtk.IconSize.BUTTON, - ) - else: - button = Gtk.Button.new() - button.set_image( - Gtk.Image.new_from_pixbuf( - self.main_win_obj.pixbuf_dict['stock_delete'], - ), - ) - hbox.pack_end(button, False, False, 0) - button.connect('clicked', self.on_delete_button_clicked) - - if not self.main_win_obj.app_obj.show_custom_icons_flag: - button2 = Gtk.Button.new_from_icon_name( - Gtk.STOCK_INDEX, - Gtk.IconSize.BUTTON, - ) - else: - button2 = Gtk.Button.new() - button2.set_image( - Gtk.Image.new_from_pixbuf( - self.main_win_obj.pixbuf_dict['stock_properties'], - ), - ) - hbox.pack_end( - button2, - False, - False, - self.main_win_obj.spacing_size, - ) - button2.connect('clicked', self.on_edit_button_clicked) - - # Draw text on labels, as necessary - self.update_widgets() - - - def update_widgets(self): - - """Can be called by anything. - - Update the text displayed in the widgets created in the earlier call to - self.draw_widgets(). - """ - - size = len(self.main_win_obj.app_obj.classic_dropzone_list) - if size <= 1: - length = self.main_win_obj.exceedingly_long_string_max_len - elif size <= 4: - length = self.main_win_obj.very_long_string_max_len - elif size <= 9: - length = self.main_win_obj.long_string_max_len - else: - length = self.main_win_obj.medium_string_max_len - - if not self.options_obj: - - self.name_label.set_markup('') - self.descrip_label.set_markup('') - self.update_label.set_markup('') - - self.override_background_color( - Gtk.StateType.NORMAL, - None, - ) - - else: - - self.name_label.set_markup( - '' + self.options_obj.name \ - + '', - ) - self.descrip_label.set_markup( - html.escape( - utils.shorten_string_two_lines( - self.options_obj.descrip, - length, - ), - ), - ) - - if self.update_text is None: - - self.update_label.set_markup('') - - if self.y_pos % 2 == 0: - if self.x_pos % 2 == 0: - colour = self.main_win_obj.drag_drop_even_colour - else: - colour = self.main_win_obj.drag_drop_odd_colour - else: - if self.x_pos % 2 == 0: - colour = self.main_win_obj.drag_drop_odd_colour - else: - colour = self.main_win_obj.drag_drop_even_colour - - self.override_background_color( - Gtk.StateType.NORMAL, - colour, - ) - - else: - self.update_label.set_markup( - '' + html.escape( - utils.shorten_string_two_lines( - self.update_text, - length, - ), - ) + '', - ) - - self.override_background_color( - Gtk.StateType.NORMAL, - self.main_win_obj.drag_drop_notify_colour, - ) - - - def check_reset(self): - - """Called (several times a second) by - mainapp.TartubeApp.script_fast_timer_callback(). - - If it's time to remove the message displayed in the dropzone, then - remove it and update IVs. - """ - - if self.reset_time is not None \ - and self.reset_time < time.time(): - self.update_text = None - self.reset_time = None - self.update_widgets() - - - # Callback class methods - - - def on_delete_button_clicked(self, button): - - """Called a from callback in self.__init__(). - - Prompts the user to delete this dropzone and/or its associated - options.OptionsManager object. - """ - - app_obj = self.main_win_obj.app_obj - - dialogue_win = DeleteDropZoneDialogue( - self.main_win_obj, - self.options_obj, - ) - response = dialogue_win.run() - - # Get the clicked button, before destroying the window - del_dropzone_flag = dialogue_win.del_dropzone_flag - del_both_flag = dialogue_win.del_both_flag - dialogue_win.destroy() - - if response != Gtk.ResponseType.CANCEL \ - and response != Gtk.ResponseType.DELETE_EVENT: - - if del_dropzone_flag: - app_obj.del_classic_dropzone_list(self.options_obj.uid) - - elif del_both_flag: - options_obj = app_obj.options_reg_dict[self.options_obj.uid] - app_obj.delete_download_options(options_obj) - - - # Update the Drag and Drop tab - self.main_win_obj.drag_drop_grid_reset() - - - def on_edit_button_clicked(self, button): - - """Called a from callback in self.__init__(). - - Opens an edit window for this dropzone's options.OptionsManager object. - """ - - config.OptionsEditWin( - self.main_win_obj.app_obj, - self.options_obj, - ) - - - def on_drag_data_received(self, window, context, x, y, data, info, - this_time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dropzone, adding a valid and - non-duplicate URL to the Classic Progress List. - """ - - # Sanity check - if not self.options_obj: - return - - else: - - url = utils.strip_whitespace(data.get_text()) - - # Show a confirmation inside the dropzone - if not utils.check_url(url): - self.update_text = _('Invalid URL') - - else: - - duplicate_flag = False - for other_obj in self.main_win_obj.classic_media_dict.values(): - if other_obj.source == url: - duplicate_flag = True - break - - if duplicate_flag: - self.update_text = _('Duplicate URL') - - elif not self.main_win_obj.classic_mode_tab_insert_url( - url, - self.options_obj, - ): - self.update_text = _('Failed to add URL') - - else: - self.update_text = url - - self.reset_time = time.time() \ - + self.main_win_obj.drag_drop_reset_time - self.update_widgets() - - -class StatusIcon(Gtk.StatusIcon): - - """Called by mainapp.TartubeApp.start(). - - Python class acting as a wrapper for Gtk.StatusIcon. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - super(Gtk.StatusIcon, self).__init__() - - # IV list - class objects - # ----------------------- - # The main application - self.app_obj = app_obj - - - # IV list - other - # --------------- - # Flag set to True (by self.show_icon() ) when the status icon is - # actually visible - self.icon_visible_flag = False - - - # Code - # ---- - - self.setup() - - - # Public class methods - - - def setup(self): - - """Called by self.__init__. - - Sets up the Gtk widget, and creates signal connects for left- and - right-clicks on the status icon. - """ - - # Display the default status icon, to start with... - self.update_icon() - # ...but the status icon isn't visible straight away - self.set_visible(False) - - # Set the tooltip - self.set_has_tooltip(True) - self.set_tooltip_text('Tartube') - - # Signal connects - self.connect('button-press-event', self.on_button_press_event) - self.connect('popup-menu', self.on_popup_menu) - - - def show_icon(self): - - """Can be called by anything. - - Makes the status icon visible in the system tray (if it isn't already - visible). - """ - - if not self.icon_visible_flag: - self.icon_visible_flag = True - self.set_visible(True) - - - def hide_icon(self): - - """Can be called by anything. - - Makes the status icon invisible in the system tray (if it isn't already - invisible). - """ - - if self.icon_visible_flag: - self.icon_visible_flag = False - self.set_visible(False) - - - def update_icon(self): - - """Called by self.setup(), and then by mainapp.TartubeApp whenever am - operation starts or stops. - - Updates the status icon with the correct icon file. The icon file used - depends on whether an operation is in progress or not, and which one. - """ - - if self.app_obj.download_manager_obj: - if self.app_obj.download_manager_obj.operation_type == 'sim': - if not self.app_obj.livestream_manager_obj: - icon = formats.STATUS_ICON_DICT['check_icon'] - else: - icon = formats.STATUS_ICON_DICT['check_live_icon'] - else: - if not self.app_obj.livestream_manager_obj: - icon = formats.STATUS_ICON_DICT['download_icon'] - else: - icon = formats.STATUS_ICON_DICT['download_live_icon'] - elif self.app_obj.update_manager_obj: - icon = formats.STATUS_ICON_DICT['update_icon'] - elif self.app_obj.refresh_manager_obj: - icon = formats.STATUS_ICON_DICT['refresh_icon'] - elif self.app_obj.info_manager_obj: - icon = formats.STATUS_ICON_DICT['info_icon'] - elif self.app_obj.tidy_manager_obj: - icon = formats.STATUS_ICON_DICT['tidy_icon'] - elif self.app_obj.livestream_manager_obj: - icon = formats.STATUS_ICON_DICT['livestream_icon'] - elif self.app_obj.process_manager_obj: - icon = formats.STATUS_ICON_DICT['process_icon'] - else: - icon = formats.STATUS_ICON_DICT['default_icon'] - - self.set_from_file( - os.path.abspath( - os.path.join( - self.app_obj.main_win_obj.icon_dir_path, - 'status', - icon, - ), - ) - ) - - - # Callback class methods - - - # (Clicks on the status icon) - - - def on_button_press_event(self, widget, event_button): - - """Called from a callback in self.setup(). - - When the status icon is left-clicked, toggle the main window's - visibility. - - Args: - - widget (mainwin.StatusIcon): This object - - event_button (Gdk.EventButton): Ignored - - """ - - if event_button.button == 1: - self.app_obj.main_win_obj.toggle_visibility() - return True - - else: - return False - - - def on_popup_menu(self, widget, button, time): - - """Called from a callback in self.setup(). - - When the status icon is right-clicked, open a popup men. - - Args: - - widget (mainwin.StatusIcon): This object - - button_type (int): Ignored - - time (int): Ignored - - """ - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Check all - check_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Check all')) - check_menu_item.connect('activate', self.on_check_menu_item) - popup_menu.append(check_menu_item) - if self.app_obj.current_manager_obj: - check_menu_item.set_sensitive(False) - - # Download all - download_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download all')) - download_menu_item.connect('activate', self.on_download_menu_item) - popup_menu.append(download_menu_item) - if self.app_obj.current_manager_obj: - download_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Stop current operation - stop_menu_item = Gtk.MenuItem.new_with_mnemonic( - _('_Stop current operation'), - ) - stop_menu_item.connect('activate', self.on_stop_menu_item) - popup_menu.append(stop_menu_item) - if not self.app_obj.current_manager_obj: - stop_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Quit - quit_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Quit')) - quit_menu_item.connect('activate', self.on_quit_menu_item) - popup_menu.append(quit_menu_item) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, self, 3, time) - - - # (Menu item callbacks) - - - def on_check_menu_item(self, menu_item): - - """Called from a callback in self.popup_menu(). - - Starts the download manager. - - Args: - - menu_item (Gtk.MenuItem): The menu item clicked - - """ - - if not self.app_obj.current_manager_obj: - self.app_obj.download_manager_start('sim') - - - def on_download_menu_item(self, menu_item): - - """Called from a callback in self.popup_menu(). - - Starts the download manager. - - Args: - - menu_item (Gtk.MenuItem): The menu item clicked - - """ - - if not self.app_obj.current_manager_obj: - self.app_obj.download_manager_start('real') - - - def on_stop_menu_item(self, menu_item): - - """Called from a callback in self.popup_menu(). - - Stops the current operation (but not livestream operations, which run - in the background and are halted immediately, if a different type of - operation wants to start). - - Args: - - menu_item (Gtk.MenuItem): The menu item clicked - - """ - - if self.app_obj.current_manager_obj: - - self.app_obj.set_operation_halted_flag(True) - - if self.app_obj.download_manager_obj: - self.app_obj.download_manager_obj.stop_download_operation() - elif self.app_obj.update_manager_obj: - self.app_obj.update_manager_obj.stop_update_operation() - elif self.app_obj.refresh_manager_obj: - self.app_obj.refresh_manager_obj.stop_refresh_operation() - elif self.app_obj.info_manager_obj: - self.app_obj.info_manager_obj.stop_info_operation() - elif self.app_obj.tidy_manager_obj: - self.app_obj.tidy_manager_obj.stop_tidy_operation() - elif self.app_obj.process_manager_obj: - self.app_obj.processs_manager_obj.stop_process_operation() - - - def on_quit_menu_item(self, menu_item): - - """Called from a callback in self.popup_menu(). - - Close the application. - - Args: - - menu_item (Gtk.MenuItem): The menu item clicked - - """ - - self.app_obj.stop() - - -class MultiDragDropTreeView(Gtk.TreeView): - - """Called by MainWin.setup_progress_tab() and .setup_classic_mode_tab(). - - A modified version of Gtk.TreeView by Kevin Mehall, released under the MIT - license, and slightly modified to work with PyGObject. See: - https://kevinmehall.net/2010/pygtk_multi_select_drag_drop - - This treeview captures mouse events to make drag and drop work properly. - """ - - # Standard class methods - - - def __init__(self): - - super(MultiDragDropTreeView, self).__init__() - - # Code - # ---- - - self.connect('button-press-event', self.on_button_press) - self.connect('button-release-event', self.on_button_release) - self.defer_select = False - - - def on_button_press(self, widget, event): - - """Intercept mouse clicks on selected items so that we can drag - multiple items without the click selecting only one. - - Args: - - widget (mainwin.MultiDragDropTreeView): The widget clicked - - event (Gdk.EventButton): The event occuring as a result - - """ - - target = self.get_path_at_pos(int(event.x), int(event.y)) - - if ( - target - and event.type == Gdk.EventType.BUTTON_PRESS - and not ( - event.state \ - & (Gdk.ModifierType.CONTROL_MASK|Gdk.ModifierType.SHIFT_MASK) - ) - and self.get_selection().path_is_selected(target[0]) - ): - # Disable selection - self.get_selection().set_select_function(lambda *ignore: False) - self.defer_select = target[0] - - - def on_button_release(self, widget, event): - - """Re-enable selection. - - Args: - - widget (mainwin.MultiDragDropTreeView): The widget clicked - - event (Gdk.EventButton): The event occuring as a result - - """ - - self.get_selection().set_select_function(lambda *ignore: True) - - target = self.get_path_at_pos(int(event.x), int(event.y)) - if ( - self.defer_select - and target - and self.defer_select == target[0] - and not (event.x==0 and event.y==0) - ): - # Certain drag and drop - self.set_cursor(target[0], target[1], False) - - self.defer_select=False - - -# (Dialogue window classes) - - -class AddBulkDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_bulk(). - - Python class handling a dialogue window that adds channels/playlist to the - media registry in bulk. - - Much of the code in this dialogue window has been copied from - mainwin.AddVideoDialogue. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - suggest_parent_dbid (int): The .dbid of the parent media.Folder, or - None if the channels/playlists shouldn't be added inside a folder - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, suggest_parent_dbid=None): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Add many channels/playlists\' dialogue' \ - + ' starts here. In the main window menu, click' \ - + ' Media > Add many channels/playlists...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.textbuffer = None # Gtk.TextBuffer - self.mark_start = None # Gtk.TextMark - self.mark_end = None # Gtk.TextMark - self.checkbutton = None # Gtk.CheckButton - self.treeview = None # Gtk.TreeView - self.liststore = None # Gtk.ListStore - self.liststore2 = None # Gtk.ListStore - self.grid2 = None # Gtk.Grid - - - # IV list - other - # --------------- - # List of URLs added so far, for checking duplicates - self.url_list = [] - # Number of channels/playlists added so far, used to give channels/ - # playlists an initial name - # In order to eliminate duplicate names, these counts may be larger - # than the actual number added - self.channel_count = 0 - self.playlist_count = 0 - - # A list of media.Folder (.dbid values) whose names should be - # displayed in the Gtk.ComboBox - self.folder_list = [] - # The media.Folder selected in the combobox - self.parent_dbid = None - # Set up IVs for clipboard monitoring, if required - self.clipboard_timer_id = None - self.clipboard_timer_time = 250 - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Add many channels/playlists'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - self.grid = Gtk.Grid() - box.add(self.grid) - self.grid.set_border_width(main_win_obj.spacing_size) - self.grid.set_row_spacing(main_win_obj.spacing_size) - self.grid.set_column_homogeneous(True) - - grid_width = 2 - - # Initial widgets - label = Gtk.Label(_('Enter URLs below')) - self.grid.attach(label, 0, 0, 1, 1) - label.set_xalign(0) - - self.checkbutton = Gtk.CheckButton() - self.grid.attach(self.checkbutton, 1, 0, 1, 1) - self.checkbutton.set_label(_('Enable automatic copy/paste')) - self.checkbutton.connect('toggled', self.on_checkbutton_toggled) - - # Add a textview - frame = Gtk.Frame() - self.grid.attach(frame, 0, 1, grid_width, 1) - # (Set enough vertical room for several URLs) - frame.set_size_request( - main_win_obj.app_obj.config_win_width - 150, - 120, - ) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - - textview = Gtk.TextView() - scrolled.add(textview) - textview.set_hexpand(True) - self.textbuffer = textview.get_buffer() - - # Some callbacks will complain about invalid iterators, if we try to - # use Gtk.TextIters, so use Gtk.TextMarks instead - self.mark_start = self.textbuffer.create_mark( - 'mark_start', - self.textbuffer.get_start_iter(), - True, # Left gravity - ) - self.mark_end = self.textbuffer.create_mark( - 'mark_end', - self.textbuffer.get_end_iter(), - False, # Not left gravity - ) - - # Drag-and-drop onto the textview inevitably inserts a URL in the - # middle of another URL. No way to prevent that, but we can disable - # drag-and-drop in the textview altogether, and instead handle it - # from the dialogue window itself - textview.drag_dest_unset() - self.connect('destroy', self.close, textview) - - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Paste in the contents of the clipboard (if it contains valid URLs) - if main_win_obj.app_obj.dialogue_copy_clipboard_flag: - utils.add_links_to_textview_from_clipboard( - main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - ) - - # Buttons to add URLs as channels/playlists - channel_button = Gtk.Button.new_with_label(_('Add channels')) - self.grid.attach(channel_button, 0, 2, 1, 1) - channel_button.connect('clicked', self.on_add_urls_clicked, 'channel') - - playlist_button = Gtk.Button.new_with_label(_('Add playlists')) - self.grid.attach(playlist_button, 1, 2, 1, 1) - playlist_button.connect( - 'clicked', - self.on_add_urls_clicked, - 'playlist', - ) - - # Separator - self.grid.attach(Gtk.HSeparator(), 0, 4, grid_width, 1) - - # Add a treeview - label2 = Gtk.Label( - _('Double-click the names/URLs to customise them'), - ) - self.grid.attach(label2, 0, 5, grid_width, 1) - label2.set_xalign(0) - - label3 = Gtk.Label() - label3.set_markup( - _( - 'HINT: You can also click Media > Reset channel/' \ - + 'playlist names...', - ), - ) - self.grid.attach(label3, 0, 6, grid_width, 1) - label3.set_xalign(0) - - frame2 = Gtk.Frame() - self.grid.attach(frame2, 0, 7, grid_width, 1) - frame2.set_size_request(-1, 150) - - scrolled2 = Gtk.ScrolledWindow() - frame2.add(scrolled2) - scrolled2.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - scrolled2.set_vexpand(True) - - self.treeview = Gtk.TreeView() - scrolled2.add(self.treeview) - self.treeview.set_headers_visible(True) - # (Allow multiple selection) - self.treeview.set_can_focus(True) - selection = self.treeview.get_selection() - selection.set_mode(Gtk.SelectionMode.MULTIPLE) - - for i, column_title in enumerate( - [ 'hide', _('Type'), _('Name'), _('URL') ], - ): - if i == 1: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - column_title, - renderer_pixbuf, - pixbuf=i, - ) - self.treeview.append_column(column_pixbuf) - column_pixbuf.set_resizable(False) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - if i == 0: - column_text.set_visible(False) - else: - self.treeview.append_column(column_text) - column_text.set_resizable(True) - if i == 2: - renderer_text.set_property('editable', True) - renderer_text.connect( - 'edited', - self.on_container_name_edited, - ) - elif i == 3: - renderer_text.set_property('editable', True) - renderer_text.connect( - 'edited', - self.on_container_url_edited, - ) - - self.liststore = Gtk.ListStore( - str, GdkPixbuf.Pixbuf, str, str, - ) - self.treeview.set_model(self.liststore) - - # Add more buttons - switch_button = Gtk.Button.new_with_label(_('Toggle channel/playlist')) - self.grid.attach(switch_button, 0, 8, 1, 1) - switch_button.connect('clicked', self.on_convert_line_clicked) - - delete_button = Gtk.Button.new_with_label(_('Delete selected lines')) - self.grid.attach(delete_button, 1, 8, 1, 1) - delete_button.connect('clicked', self.on_delete_line_clicked) - - # Separator - self.grid.attach(Gtk.HSeparator(), 0, 9, grid_width, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folder 'Temporary Videos' - # If a folder is selected in the Video Index, then it is the first item - # in the list. If not, 'Temporary Videos' is the first item in the - # list - for media_data_obj in main_win_obj.app_obj.container_reg_dict.values(): - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and media_data_obj.restrict_mode == 'open' \ - and media_data_obj.get_depth() \ - < main_win_obj.app_obj.container_max_level \ - and ( - suggest_parent_dbid is None - or suggest_parent_dbid != media_data_obj.dbid - ): - self.folder_list.append(media_data_obj.dbid) - - self.folder_list.sort() - self.folder_list.insert(0, None) - self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.dbid) - - if suggest_parent_dbid is not None: - self.folder_list.insert(0, suggest_parent_dbid) - - # Store the combobox's selected item, so the calling function can - # retrieve it - self.parent_dbid = self.folder_list[0] - - # (Second grid, to avoid messing up the format of the previous one) - self.grid2 = Gtk.Grid() - self.grid.attach(self.grid2, 0, 10, grid_width, 1) - self.grid2.set_column_spacing(main_win_obj.spacing_size) - - label4 = Gtk.Label(_('Add to this folder:')) - self.grid2.attach(label4, 0, 0, 1, 1) - - self.liststore2 = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str) - for dbid in self.folder_list: - - if dbid is None: - pixbuf = main_win_obj.pixbuf_dict['slice_small'] - self.liststore2.append( - [pixbuf, dbid, ' ' + _('No parent folder')] - ) - - elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid: - pixbuf = main_win_obj.pixbuf_dict['folder_blue_small'] - this_obj = main_win_obj.app_obj.fixed_temp_folder - self.liststore2.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - else: - pixbuf = main_win_obj.pixbuf_dict['folder_small'] - this_obj = main_win_obj.app_obj.container_reg_dict[dbid] - self.liststore2.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - combo = Gtk.ComboBox.new_with_model(self.liststore2) - self.grid2.attach(combo, 1, 0, 1, 1) - combo.set_hexpand(True) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, False) - combo.add_attribute(renderer_text, 'text', 2) - - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Display the dialogue window - self.show_all() - - - def close(self, also_self, textview): - - """Called from callback in self.__init__(). - - We have disabled drag-and-drop directly into the textview, but this - generates a Gtk warning when the textview is destroyed. - - Workaround is to re-enable drag-and-drop into the textview immediately - before it (and its window) are destroyed. - - Args: - - also_self (mainwin.AddBulkDialogue): Another copy of this window - - textview (Gtk.TextView): The textview for which drag-and-drop has - been disabled - - """ - - textview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - - - # Public class methods - - - def get_channel_name(self): - - """Called by self.on_add_urls_clicked(). - - Generates an initial channel name that isn't already used by a channel/ - playlist/folder in the media data registry. - """ - - while 1: - - self.channel_count += 1 - name = 'channel_' + str(self.channel_count) - - if not self.main_win_obj.app_obj.is_container(name): - return name - - - def get_playlist_name(self): - - """Called by self.on_add_urls_clicked(). - - Generates an initial playlist name that isn't already used by a - channel/playlist/folder in the media data registry. - """ - - while 1: - - self.playlist_count += 1 - name = 'playlist_' + str(self.playlist_count) - - if not self.main_win_obj.app_obj.is_container(name): - return name - - - # Callback class methods - - - def on_add_urls_clicked(self, button, add_type): - - """Called from a callback in self.__init__(). - - Moves valid URLs from the textview to the treeview, then empties the - textview. - - Args: - - button (Gtk.Button): The widget clicked - - add_type (str): 'channel' or 'playlist' - - """ - - # Retrieve the list of URLs added by the user - text = self.textbuffer.get_text( - self.textbuffer.get_start_iter(), - self.textbuffer.get_end_iter(), - False, - ) - - # Split text into a list of lines and filter out invalid URLs - new_list = [] - for line in text.splitlines(): - - for item in line.split(): - - # Remove leading/trailing whitespace - item = utils.strip_whitespace(item) - - # Perform checks on the URL. If it passes, remove leading/ - # trailing whitespace - if utils.check_url(item) \ - and not item in self.url_list: - mod_item = utils.strip_whitespace(item) - new_list.append(mod_item) - self.url_list.append(mod_item) - - # Reset the clipboard... - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clipboard.set_text('', -1) - # ...so we can empty the textview - self.textbuffer.set_text('') - self.show_all() - - # Add the URLs to the treeview - for url in new_list: - - if add_type == 'channel': - - self.liststore.append([ - 'channel', - self.main_win_obj.pixbuf_dict['channel_small'], - self.get_channel_name(), - url, - ]) - - else: - - self.liststore.append([ - 'playlist', - self.main_win_obj.pixbuf_dict['playlist_small'], - self.get_playlist_name(), - url, - ]) - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables clipboard monitoring. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active() \ - and self.clipboard_timer_id is not None: - - # Stop the timer - GObject.source_remove(self.clipboard_timer_id) - self.clipboard_timer_id = None - - elif checkbutton.get_active() and self.clipboard_timer_id is None: - - # Start the timer - self.clipboard_timer_id = GObject.timeout_add( - self.clipboard_timer_time, - self.clipboard_timer_callback, - ) - - - def on_combo_changed(self, combo): - - """Called a from callback in self.__init__(). - - Updates the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - self.parent_dbid = self.folder_list[combo.get_active()] - - - def on_container_name_edited(self, widget, path, text): - - """Called a from callback in self.__init__(). - - Updates the name of a channel/playlist. - - Args: - - widget (Gtk.CellRendererText): The widget clicked - - path (int): Path to the treeview line that was edited - - text (str): The new contents of the cell - - """ - - app_obj = self.main_win_obj.app_obj - - # Check the entered text is a valid name - if text == '' \ - or re.search(r'^\s*$', text) \ - or not app_obj.check_container_name_is_legal(text): - return - - # Get the dbid for the selected line's channel/playlist - model = self.treeview.get_model() - tree_iter = model.get_iter(path) - if tree_iter is not None and model[tree_iter][2] != text: - - # Check that the parent folder doesn't already have a container - # with the same name - if ( - self.parent_dbid is not None \ - and app_obj.find_duplicate_name_in_container( - app_obj.media_reg_dict[self.parent_dbid], - text, - ) - ) or ( - self.parent_dbid is None \ - and app_obj.find_duplicate_name_in_container(None, text) - ): - return - - # Otherwise, we can update the channel/playlist name - model[tree_iter][2] = text - - - def on_container_url_edited(self, widget, path, text): - - """Called a from callback in self.__init__(). - - Updates the URL of a channel/playlist. - - Args: - - widget (Gtk.CellRendererText): The widget clicked - - path (int): Path to the treeview line that was edited - - text (str): The new contents of the cell - - """ - - # Check the entered text is a valid URL - if not utils.check_url(text): - return - - # Get the dbid for the selected line's channel/playlist - model = self.treeview.get_model() - tree_iter = model.get_iter(path) - if tree_iter is not None and model[tree_iter][3] != text: - model[tree_iter][3] = text - - - def on_convert_line_clicked(self, button): - - """Called from a callback in self.__init__(). - - Converts the selected channel(s) to playlist(s), or vice-versa. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = self.treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for path in path_list: - - this_iter = model.get_iter(path) - container_type = model[this_iter][0] - if container_type == 'channel': - - model.set_value(this_iter, 0, 'playlist') - model.set_value( - this_iter, - 1, - self.main_win_obj.pixbuf_dict['playlist_small'], - ) - - else: - - model.set_value(this_iter, 0, 'channel') - model.set_value( - this_iter, - 1, - self.main_win_obj.pixbuf_dict['channel_small'], - ) - - - def on_delete_line_clicked(self, button): - - """Called from a callback in self.__init__(). - - Removes the selected line(s) from the treeview. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - selection = self.treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - for path in path_list: - model.remove(model.get_iter(path)) - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - - def clipboard_timer_callback(self): - - """Called from a callback in self.on_checkbutton_toggled(). - - Periodically checks the system's clipboard, and adds any new URLs to - the dialogue window's textview. - """ - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - ) - - # Return 1 to keep the timer going - return 1 - - -class AddChannelDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_channel(). - - Python class handling a dialogue window that adds a channel to the media - registry. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - suggest_parent_dbid (int): The .dbid of the new channel's suggested - parent folder (which the user can change, if required), or None if - this dialogue window shouldn't suggest a parent folder - - dl_sim_flag (bool): True if the 'Don't download anything' radiobutton - should be made active immediately - - monitor_flag (bool): True if the 'Monitor the clipboard' checkbutton - should be selected immediately - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, suggest_parent_dbid=None, - dl_sim_flag=False, monitor_flag=False): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Add channel\' dialogue starts here. In' \ - + ' the main window toolbar, click the Channel button' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - self.entry2 = None # Gtk.Entry - self.frame = None # Gtk.Frame - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.checkbutton = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # A list of media.Folders (.dbid values) whose names should be - # displayed in the Gtk.ComboBox - self.folder_list = [] - # The media.Folder .dbid selected in the combobox (if any) - self.parent_dbid = None - # Set up IVs for clipboard monitoring, if required - self.clipboard_timer_id = None - self.clipboard_timer_time = 250 - self.clipboard_ignore_url = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Add channel'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label(_('Enter the channel name')) - grid.attach(label, 0, 0, 1, 1) - label2 = Gtk.Label() - grid.attach(label2, 0, 1, 1, 1) - label2.set_markup( - '' + _('(Use the channel\'s real name or a customised name)') \ - + '', - ) - - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 2, 1, 1) - self.entry.set_hexpand(True) - - label3 = Gtk.Label(_('Copy and paste a link to the channel')) - grid.attach(label3, 0, 3, 1, 1) - - self.entry2 = Gtk.Entry() - grid.attach(self.entry2, 0, 4, 1, 1) - self.entry2.set_hexpand(True) - self.entry2.connect('changed', self.on_entry2_changed, grid) - - # Drag-and-drop onto the entry inevitably inserts a URL in the - # middle of another URL. No way to prevent that, but we can disable - # drag-and-drop in the entry altogether, and instead handle it - # from the dialogue window itself - self.entry.drag_dest_unset() - self.entry2.drag_dest_unset() - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # (The frame and its image are invisible, unless self.entry2 contains - # a YouTube URL that doesn't end with /videos or /playlists2) - self.frame = Gtk.Frame() - self.frame.set_tooltip_text( - _( - 'Before adding the URL for a YouTube channel, first click the' \ - + ' Videos tab in your browser!', - ), - ) - - image = Gtk.Image() - self.frame.add(image) - if main_win_obj.app_obj.current_locale == 'es': - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['yt_remind_icon_es'], - ) - elif main_win_obj.app_obj.current_locale == 'fr': - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['yt_remind_icon_fr'], - ) - elif main_win_obj.app_obj.current_locale == 'ko_KR': - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['yt_remind_icon_kr'], - ) - elif main_win_obj.app_obj.current_locale == 'nl_NL': - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['yt_remind_icon_nl'], - ) - elif main_win_obj.app_obj.current_locale == 'ru': - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['yt_remind_icon_ru'], - ) - elif main_win_obj.app_obj.current_locale == 'vi': - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['yt_remind_icon_vi'], - ) - else: - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['yt_remind_icon_en'], - ) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 6, 1, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folder 'Temporary Videos' - # If a folder is selected in the Video Index, then it is the first item - # in the list. If not, 'No parent videos' is the first item in the - # list - for media_data_obj in main_win_obj.app_obj.container_reg_dict.values(): - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and media_data_obj.restrict_mode == 'open' \ - and media_data_obj.get_depth() \ - < main_win_obj.app_obj.container_max_level \ - and ( - suggest_parent_dbid is None - or suggest_parent_dbid != media_data_obj.dbid - ): - self.folder_list.append(media_data_obj.dbid) - - self.folder_list.sort() - self.folder_list.insert(0, None) - self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.dbid) - - if suggest_parent_dbid is not None: - self.folder_list.insert(0, suggest_parent_dbid) - - # Store the combobox's selected item, so the calling function can - # retrieve it - self.parent_dbid = self.folder_list[0] - - label4 = Gtk.Label(_('(Optional) Add this channel inside a folder')) - grid.attach(label4, 0, 7, 1, 1) - - listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str) - for dbid in self.folder_list: - - if dbid is None: - pixbuf = main_win_obj.pixbuf_dict['slice_small'] - listmodel.append( - [pixbuf, dbid, ' ' + _('No parent folder')] - ) - - elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid: - pixbuf = main_win_obj.pixbuf_dict['folder_blue_small'] - this_obj = main_win_obj.app_obj.fixed_temp_folder - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - else: - pixbuf = main_win_obj.pixbuf_dict['folder_small'] - this_obj = main_win_obj.app_obj.container_reg_dict[dbid] - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 0, 8, 1, 1) - combo.set_hexpand(True) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, False) - combo.add_attribute(renderer_text, 'text', 2) - - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 9, 1, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Enable video downloads for this channel'), - ) - grid.attach(self.radiobutton, 0, 10, 1, 1) - - self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) - grid.attach(self.radiobutton2, 0, 11, 1, 1) - self.radiobutton2.set_label( - _('Don\'t download the videos, just check them'), - ) - if dl_sim_flag: - self.radiobutton2.set_active(True) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 12, 1, 1) - - self.checkbutton = Gtk.CheckButton() - grid.attach(self.checkbutton, 0, 13, 1, 1) - self.checkbutton.set_label(_('Enable automatic copy/paste')) - self.checkbutton.connect('toggled', self.on_checkbutton_toggled) - if monitor_flag: - - # Get the URL that would have been added to the Gtk.Entry, if we - # had not specified a True argument - self.clipboard_ignore_url \ - = utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - None, - None, - True, - ) - - self.checkbutton.set_active(True) - - # Paste in the contents of the clipboard (if it contains at least one - # valid URL) - if main_win_obj.app_obj.dialogue_copy_clipboard_flag \ - and not main_win_obj.app_obj.dialogue_keep_open_flag: - utils.add_links_to_entry_from_clipboard( - main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - ) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables clipboard monitoring. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active() \ - and self.clipboard_timer_id is not None: - - # Stop the timer - GObject.source_remove(self.clipboard_timer_id) - self.clipboard_timer_id = None - - elif checkbutton.get_active() and self.clipboard_timer_id is None: - - # Start the timer - self.clipboard_timer_id = GObject.timeout_add( - self.clipboard_timer_time, - self.clipboard_timer_callback, - ) - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - self.parent_dbid = self.folder_list[combo.get_active()] - - - def on_entry2_changed(self, entry, grid): - - """Called from callback in self.__init__(). - - When the entry containing a URL is changed to one containing a YouTube - URL that doesn't end in /videos or /playlists, make the reminder icon - visible (and vice-versa). - - Args: - - entry (Gtk.Entry): The clicked widget - - grid (Gtk.Grid): The grid on which the dialogue window is arranged - - """ - - url = entry.get_text() - enhanced = utils.is_enhanced(url) - - if self.main_win_obj.app_obj.dialogue_yt_remind_flag \ - and enhanced == 'youtube' \ - and not re.search(r'\/videos\s*$', url)\ - and not re.search(r'\/playlists\s*$', url): - grid.attach(self.frame, 0, 5, 2, 1) - else: - for widget in grid.get_children(): - if widget == self.frame: - grid.remove(self.frame) - # The window is now too large vertically; this restores its - # proper size - self.resize(1, 1) - break - - self.show_all() - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - - def clipboard_timer_callback(self): - - """Called from a callback in self.on_checkbutton_toggled(). - - Periodically checks the system's clipboard, and adds any new URLs to - the dialogue window's entry. - """ - - utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - ) - - # Return 1 to keep the timer going - return 1 - - -class AddDropZoneDialogue(Gtk.Dialog): - - """Called by mainwin.MainWin.drag_drop_add_dropzone(). - - Prompt the user to select an existing options.OptionsManager object or to - create a new one. The choice, if any, is added to the Drag and Drop tab as - a mainwin.DropZoneBox. - - Based on code from mainwin.ApplyOptionsDialogue. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Add dropzone dialogue starts here. In the' \ - + ' Drag and Drop tab, click the button in the top-right corner' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - # Store the user's choices as IVs, so the calling function can retrieve - # them - self.options_name = None - self.options_obj = None - self.clone_flag = False - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Add dropzone'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - app_obj = self.main_win_obj.app_obj - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Create new download options called'), - ) - grid.attach(radiobutton, 0, 0, 1, 1) - # (Signal connect appears below) - - entry = Gtk.Entry() - grid.attach(entry, 0, 1, 1, 1) - entry.grab_focus() - # (Signal connect appears below) - - radiobutton2 = Gtk.RadioButton.new_with_label_from_widget( - radiobutton, - _('Use these download options'), - ) - grid.attach(radiobutton2, 0, 2, 1, 1) - radiobutton2.set_sensitive(False) - # (Signal connect appears below) - - # Add a combo, containing any options.OptionsManager objects that are - # not already assigned to a mainwin.DropZoneBox - store = Gtk.ListStore(str, int) - - for uid in sorted(app_obj.options_reg_dict): - options_obj = app_obj.options_reg_dict[uid] - - if not options_obj.uid in app_obj.classic_dropzone_list: - - store.append([ - '#' + str(options_obj.uid) + ': ' + options_obj.name, - options_obj.uid, - ]) - radiobutton2.set_sensitive(True) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 0, 3, 1, 1) - combo.set_hexpand(True) - combo.set_sensitive(False) - # (Signal connect appears below) - - cell = Gtk.CellRendererText() - combo.pack_start(cell, False) - combo.add_attribute(cell, 'text', 0) - combo.set_active(0) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 4, 1, 1) - - radiobutton3 = Gtk.RadioButton.new_with_label_from_widget( - radiobutton2, - _('Clone these download options'), - ) - grid.attach(radiobutton3, 0, 5, 1, 1) - # (Signal connect appears below) - - # Add a combo, containing all options.OptionsManager objects - store2 = Gtk.ListStore(str, int) - - for uid in sorted(app_obj.options_reg_dict): - - options_obj = app_obj.options_reg_dict[uid] - store2.append([ - '#' + str(options_obj.uid) + ': ' + options_obj.name, - options_obj.uid, - ]) - - combo2 = Gtk.ComboBox.new_with_model(store2) - grid.attach(combo2, 0, 6, 1, 1) - combo2.set_hexpand(True) - combo2.set_sensitive(False) - # (Signal connect appears below) - - cell = Gtk.CellRendererText() - combo2.pack_start(cell, False) - combo2.add_attribute(cell, 'text', 0) - combo2.set_active(0) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_radiobutton_toggled, - entry, - combo, - combo2, - ) - entry.connect('changed', self.on_entry_changed) - radiobutton2.connect( - 'toggled', - self.on_radiobutton2_toggled, - entry, - combo, - combo2, - ) - combo.connect('changed', self.on_combo_changed) - radiobutton3.connect( - 'toggled', - self.on_radiobutton3_toggled, - entry, - combo, - combo2, - ) - combo2.connect('changed', self.on_combo2_changed) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - uid = model[tree_iter][1] - - self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid] - self.clone_flag = False - - - def on_combo2_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - uid = model[tree_iter][1] - - self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid] - self.clone_flag = True - - - def on_entry_changed(self, entry): - - """Called from callback in self.__init__(). - - Store the entry's text, so the calling function can retrieve it. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - name = entry.get_text() - if name != '': - self.options_name = name - else: - self.options_name = None - - - def on_radiobutton_toggled(self, button, entry, combo, combo2): - - """Called from a callback in self.__init__(). - - User has selected to create a new options manager. - - Args: - - button (Gtk.RadioButton): The widget clicked - - entry (Gtk.Entry): Another widget to update - - combo, combo2 (Gtk.ComboBox): Other widgets to update - - """ - - if button.get_active(): - - self.options_obj = None - self.clone_flag = False - - entry.set_sensitive(True) - combo.set_sensitive(False) - combo2.set_sensitive(False) - - - def on_radiobutton2_toggled(self, button, entry, combo, combo2): - - """Called from a callback in self.__init__(). - - User wants to select an existing options manager. - - Args: - - button (Gtk.RadioButton): The widget clicked - - entry (Gtk.Entry): Another widget to update - - combo, combo2 (Gtk.ComboBox): Other widgets to update - - """ - - if button.get_active(): - - tree_iter = combo.get_active_iter() - model = combo.get_model() - uid = model[tree_iter][1] - - self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid] - self.clone_flag = False - - entry.set_text('') - entry.set_sensitive(False) - combo.set_sensitive(True) - combo2.set_sensitive(False) - - - def on_radiobutton3_toggled(self, button, entry, combo, combo2): - - """Called from a callback in self.__init__(). - - User wants to clone an existing options manager. - - Args: - - button (Gtk.RadioButton): The widget clicked - - entry (Gtk.Entry): Another widget to update - - combo, combo2 (Gtk.ComboBox): Other widgets to update - - """ - - if button.get_active(): - - tree_iter = combo2.get_active_iter() - model = combo2.get_model() - uid = model[tree_iter][1] - - self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid] - self.clone_flag = True - - entry.set_text('') - entry.set_sensitive(False) - combo.set_sensitive(False) - combo2.set_sensitive(True) - - -class AddFolderDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_folder(). - - Python class handling a dialogue window that adds a folder to the media - registry. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - suggest_parent_dbid (int): The .dbid of the new folder's suggested - parent folder (which the user can change, if required), or None if - this dialogue window shouldn't suggest a parent folder - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, suggest_parent_dbid=None): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Add folder\' dialogue starts here. In' \ - + ' the main window toolbar, click the Folder button' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - - - # IV list - other - # --------------- - # A list of media.Folders (.dbid values) whose names should be - # displayed in the Gtk.ComboBox - self.folder_list = [] - # The media.Folder .dbid selected in the combobox (if any) - self.parent_dbid = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Add folder'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label(_('Enter the folder name')) - grid.attach(label, 0, 0, 1, 1) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 1, 1) - self.entry.set_hexpand(True) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 2, 1, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folder 'Temporary Videos' - # If a folder is selected in the Video Index, then it is the first one - # in the list. If not, 'No parent videos' is the first one in the - # list - for media_data_obj in main_win_obj.app_obj.container_reg_dict.values(): - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and media_data_obj.restrict_mode != 'full' \ - and media_data_obj.get_depth() \ - < main_win_obj.app_obj.container_max_level \ - and ( - suggest_parent_dbid is None - or suggest_parent_dbid != media_data_obj.dbid - ): - self.folder_list.append(media_data_obj.dbid) - - self.folder_list.sort() - self.folder_list.insert(0, None) - self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.dbid) - - if suggest_parent_dbid is not None: - self.folder_list.insert(0, suggest_parent_dbid) - - # Store the combobox's selected item, so the calling function can - # retrieve it - self.parent_dbid = self.folder_list[0] - - label4 = Gtk.Label( - _('(Optional) Add this folder inside another folder'), - ) - grid.attach(label4, 0, 3, 1, 1) - - listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str) - for dbid in self.folder_list: - - if dbid is None: - pixbuf = main_win_obj.pixbuf_dict['slice_small'] - listmodel.append( - [pixbuf, dbid, ' ' + _('No parent folder')] - ) - - elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid: - pixbuf = main_win_obj.pixbuf_dict['folder_blue_small'] - this_obj = main_win_obj.app_obj.fixed_temp_folder - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - else: - pixbuf = main_win_obj.pixbuf_dict['folder_small'] - this_obj = main_win_obj.app_obj.container_reg_dict[dbid] - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 0, 4, 1, 1) - combo.set_hexpand(True) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, False) - combo.add_attribute(renderer_text, 'text', 2) - - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 5, 1, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Enable video downloads for this folder'), - ) - grid.attach(self.radiobutton, 0, 6, 1, 1) - - self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) - self.radiobutton2.set_label( - _('Don\'t download the videos, just check them'), - ) - grid.attach(self.radiobutton2, 0, 7, 1, 1) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - self.parent_dbid = self.folder_list[combo.get_active()] - - -class AddPlaylistDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_playlist(). - - Python class handling a dialogue window that adds a playlist to the - media registry. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - suggest_parent_dbid (int): The .dbid of the new playlist's suggested - parent folder (which the user can change, if required), or None if - this dialogue window shouldn't suggest a parent folder - - dl_sim_flag (bool): True if the 'Don't download anything' radiobutton - should be made active immediately - - monitor_flag (bool): True if the 'Monitor the clipboard' checkbutton - should be selected immediately - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, suggest_parent_dbid=None, - dl_sim_flag=False, monitor_flag=False): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Add playlist\' dialogue starts here. In' \ - + ' the main window toolbar, click the Playlist button' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - self.entry2 = None # Gtk.Entry - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.checkbutton = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # A list of media.Folders to display in the Gtk.ComboBox - self.folder_list = [] - # The media.Folder .dbid selected in the combobox (if any) - self.parent_dbid = None - # Set up IVs for clipboard monitoring, if required - self.clipboard_timer_id = None - self.clipboard_timer_time = 250 - self.clipboard_ignore_url = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Add playlist'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label(_('Enter the playlist name')) - grid.attach(label, 0, 0, 1, 1) - label2 = Gtk.Label() - grid.attach(label2, 0, 1, 1, 1) - label2.set_markup( - '' + _('(Use the playlist\'s real name or a customised name)') \ - + '', - ) - - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 2, 1, 1) - self.entry.set_hexpand(True) - - label3 = Gtk.Label(_('Copy and paste a link to the playlist')) - grid.attach(label3, 0, 3, 1, 1) - - self.entry2 = Gtk.Entry() - grid.attach(self.entry2, 0, 4, 1, 1) - self.entry2.set_hexpand(True) - - # Drag-and-drop onto the entry inevitably inserts a URL in the - # middle of another URL. No way to prevent that, but we can disable - # drag-and-drop in the entry altogether, and instead handle it - # from the dialogue window itself - self.entry.drag_dest_unset() - self.entry2.drag_dest_unset() - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Separator - grid.attach(Gtk.HSeparator(), 0, 5, 1, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folder 'Temporary Videos' - # If a folder is selected in the Video Index, then it is the first item - # in the list. If not, 'No parent videos' is the first item in the - # list - for media_data_obj in main_win_obj.app_obj.container_reg_dict.values(): - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and media_data_obj.restrict_mode == 'open' \ - and media_data_obj.get_depth() \ - < main_win_obj.app_obj.container_max_level \ - and ( - suggest_parent_dbid is None - or suggest_parent_dbid != media_data_obj.dbid - ): - self.folder_list.append(media_data_obj.dbid) - - self.folder_list.sort() - self.folder_list.insert(0, None) - self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.dbid) - - if suggest_parent_dbid is not None: - self.folder_list.insert(0, suggest_parent_dbid) - - # Store the combobox's selected item, so the calling function can - # retrieve it - self.parent_dbid = self.folder_list[0] - - label4 = Gtk.Label(_('(Optional) Add this playlist inside a folder')) - grid.attach(label4, 0, 6, 1, 1) - - listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str) - for dbid in self.folder_list: - - if dbid is None: - pixbuf = main_win_obj.pixbuf_dict['slice_small'] - listmodel.append( - [pixbuf, dbid, ' ' + _('No parent folder')] - ) - - elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid: - pixbuf = main_win_obj.pixbuf_dict['folder_blue_small'] - this_obj = main_win_obj.app_obj.fixed_temp_folder - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - else: - pixbuf = main_win_obj.pixbuf_dict['folder_small'] - this_obj = main_win_obj.app_obj.container_reg_dict[dbid] - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 0, 7, 1, 1) - combo.set_hexpand(True) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, False) - combo.add_attribute(renderer_text, 'text', 2) - - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 8, 1, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Enable video downloads for this playlist'), - ) - grid.attach(self.radiobutton, 0, 9, 1, 1) - - self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) - grid.attach(self.radiobutton2, 0, 10, 1, 1) - self.radiobutton2.set_label( - _('Don\'t download the videos, just check them'), - ) - if dl_sim_flag: - self.radiobutton2.set_active(True) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 11, 1, 1) - - self.checkbutton = Gtk.CheckButton() - grid.attach(self.checkbutton, 0, 12, 1, 1) - self.checkbutton.set_label(_('Enable automatic copy/paste')) - self.checkbutton.connect('toggled', self.on_checkbutton_toggled) - if monitor_flag: - - # Get the URL that would have been added to the Gtk.Entry, if we - # had not specified a True argument - self.clipboard_ignore_url \ - = utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - None, - None, - True, - ) - - self.checkbutton.set_active(True) - - # Paste in the contents of the clipboard (if it contains at least one - # valid URL) - if main_win_obj.app_obj.dialogue_copy_clipboard_flag \ - and not main_win_obj.app_obj.dialogue_keep_open_flag: - utils.add_links_to_entry_from_clipboard( - main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - ) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables clipboard monitoring. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active() \ - and self.clipboard_timer_id is not None: - - # Stop the timer - GObject.source_remove(self.clipboard_timer_id) - self.clipboard_timer_id = None - - elif checkbutton.get_active() and self.clipboard_timer_id is None: - - # Start the timer - self.clipboard_timer_id = GObject.timeout_add( - self.clipboard_timer_time, - self.clipboard_timer_callback, - ) - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - self.parent_dbid = self.folder_list[combo.get_active()] - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - - def clipboard_timer_callback(self): - - """Called from a callback in self.on_checkbutton_toggled(). - - Periodically checks the system's clipboard, and adds any new URLs to - the dialogue window's entry. - """ - - utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - ) - - # Return 1 to keep the timer going - return 1 - - -class AddStampDialogue(Gtk.Dialog): - - """Called by config.VideoEditWin.on_copy_stamp_button_clicked(). - - Python class handling a dialogue window that allows the user to copy/paste - timestamp data (for example, from a video's description), which is then - converted into media.Video.stamp_list. - - Args: - - parent_win_obj (mainwin.MainWin or config.OptionsEditWin): The parent - window - - main_win_obj (mainwin.MainWin): The main window (parent or not) - - """ - - - # Standard class methods - - - def __init__(self, parent_win_obj, main_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Reset timestamps\' dialogue starts here.' \ - + ' In the Videos tab, right-click a video and select Show Video' \ - + ' > Properties... > Timestamps > Reset list using copied text' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.parent_win_obj = parent_win_obj - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.textbuffer = None # Gtk.TextBuffer - self.mark_start = None # Gtk.TextMark - self.mark_end = None # Gtk.TextMark - - - # IV list - other - # --------------- - # (none) - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Reset timestamps'), - parent_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - self.grid = Gtk.Grid() - box.add(self.grid) - self.grid.set_border_width(main_win_obj.spacing_size) - self.grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label() - self.grid.attach(label, 0, 0, 1, 1) - label.set_markup( - _('Copy timestamps below, e.g.') \ - + ' 0:30 ' + _('Introduction') + '', - ) - label.set_xalign(0) - - # Add a textview - frame = Gtk.Frame() - self.grid.attach(frame, 0, 1, 1, 1) - # (Use the same textview width as AddBulkDialogue) - frame.set_size_request( - main_win_obj.app_obj.config_win_width - 150, - 250, - ) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - - textview = Gtk.TextView() - scrolled.add(textview) - textview.set_hexpand(True) - self.textbuffer = textview.get_buffer() - - # Some callbacks will complain about invalid iterators, if we try to - # use Gtk.TextIters, so use Gtk.TextMarks instead - self.mark_start = self.textbuffer.create_mark( - 'mark_start', - self.textbuffer.get_start_iter(), - True, # Left gravity - ) - self.mark_end = self.textbuffer.create_mark( - 'mark_end', - self.textbuffer.get_end_iter(), - False, # Not left gravity - ) - - # Drag-and-drop onto the textview inevitably inserts text inside - # existing text. No way to prevent that, but we can disable - # drag-and-drop in the textview altogether, and instead handle it - # from the dialogue window itself - textview.drag_dest_unset() - self.connect('destroy', self.close, textview) - - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Separator - self.grid.attach(Gtk.HSeparator(), 0, 2, 1, 1) - - # Display the dialogue window - self.show_all() - - - def close(self, also_self, textview): - - """Called from callback in self.__init__(). - - We have disabled drag-and-drop directly into the textview, but this - generates a Gtk warning when the textview is destroyed. - - Workaround is to re-enable drag-and-drop into the textview immediately - before it (and its window) are destroyed. - - Args: - - also_self (mainwin.AddStampDialogue): Another copy of this window - - textview (Gtk.TextView): The textview for which drag-and-drop has - been disabled - - """ - - textview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - - - # Callback class methods - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - -class AddVideoDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_video(). - - Python class handling a dialogue window that adds invidual video(s) to - the media registry. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Add video\' dialogue starts here. In' \ - + ' the main window toolbar, click the Video button' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.textbuffer = None # Gtk.TextBuffer - self.mark_start = None # Gtk.TextMark - self.mark_end = None # Gtk.TextMark - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.checkbutton = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # A list of media.Folders (.dbid values) whose names should be - # displayed in the Gtk.ComboBox - self.folder_list = [] - # The media.Folder .dbid selected in the combobox (if any) - self.parent_dbid = None - # Set up IVs for clipboard monitoring, if required - self.clipboard_timer_id = None - self.clipboard_timer_time = 250 - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Add videos'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label(_('Copy and paste the links to one or more videos')) - grid.attach(label, 0, 0, 1, 1) - - if main_win_obj.app_obj.operation_convert_mode == 'channel': - - text = _( - 'Links containing multiple videos will be converted to' \ - + ' a channel', - ) - - elif main_win_obj.app_obj.operation_convert_mode == 'playlist': - - text = _( - 'Links containing multiple videos will be converted to a' \ - + ' playlist', - ) - - elif main_win_obj.app_obj.operation_convert_mode == 'multi': - - text = _( - 'Links containing multiple videos will be downloaded' \ - + ' separately', - ) - - elif main_win_obj.app_obj.operation_convert_mode == 'disable': - - text = _( - 'Links containing multiple videos will not be downloaded' - + ' at all', - ) - - label = Gtk.Label() - label.set_markup('' + text + '') - grid.attach(label, 0, 1, 1, 1) - - frame = Gtk.Frame() - grid.attach(frame, 0, 2, 1, 1) - - scrolledwindow = Gtk.ScrolledWindow() - frame.add(scrolledwindow) - # (Set enough vertical room for at several URLs) - scrolledwindow.set_size_request(-1, 150) - - textview = Gtk.TextView() - scrolledwindow.add(textview) - textview.set_hexpand(True) - self.textbuffer = textview.get_buffer() - - # Some callbacks will complain about invalid iterators, if we try to - # use Gtk.TextIters, so use Gtk.TextMarks instead - self.mark_start = self.textbuffer.create_mark( - 'mark_start', - self.textbuffer.get_start_iter(), - True, # Left gravity - ) - self.mark_end = self.textbuffer.create_mark( - 'mark_end', - self.textbuffer.get_end_iter(), - False, # Not left gravity - ) - - # Drag-and-drop onto the textview inevitably inserts a URL in the - # middle of another URL. No way to prevent that, but we can disable - # drag-and-drop in the textview altogether, and instead handle it - # from the dialogue window itself - textview.drag_dest_unset() - self.connect('destroy', self.close, textview) - - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Separator - grid.attach(Gtk.HSeparator(), 0, 3, 1, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folders 'Unsorted Videos', 'Video Clips' and - # 'Temporary Videos' - # If a folder is selected in the Video Index, then it is the first one - # in the list. If not, 'Unsorted Videos' is the first one in the - # list - folder_obj = None - # The selected item in the Video Index could be a channel, playlist or - # folder, but here we only pay attention to folders - selected_dbid = main_win_obj.video_index_current_dbid - if selected_dbid: - container_obj = main_win_obj.app_obj.media_reg_dict[selected_dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.priv_flag: - folder_obj = container_obj - - for media_data_obj in main_win_obj.app_obj.container_reg_dict.values(): - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and media_data_obj.restrict_mode == 'open' \ - and (folder_obj is None or media_data_obj != folder_obj): - self.folder_list.append(media_data_obj.dbid) - - self.folder_list.sort() - self.folder_list.insert(0, main_win_obj.app_obj.fixed_misc_folder.dbid) - self.folder_list.insert( - 1, - main_win_obj.app_obj.fixed_clips_folder.dbid, - ) - self.folder_list.insert(2, main_win_obj.app_obj.fixed_temp_folder.dbid) - - if folder_obj: - self.folder_list.insert(0, folder_obj.dbid) - - # Store the combobox's selected item, so the calling function can - # retrieve it - self.parent_dbid = self.folder_list[0] - - label2 = Gtk.Label(_('Add the videos to this folder')) - grid.attach(label2, 0, 4, 1, 1) - - listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str) - for dbid in self.folder_list: - - if dbid is None: - pixbuf = main_win_obj.pixbuf_dict['slice_small'] - listmodel.append( - [pixbuf, dbid, ' ' + _('No parent folder')] - ) - - elif dbid == main_win_obj.app_obj.fixed_misc_folder.dbid: - pixbuf = main_win_obj.pixbuf_dict['folder_green_small'] - this_obj = main_win_obj.app_obj.fixed_misc_folder - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - elif dbid == main_win_obj.app_obj.fixed_clips_folder.dbid: - pixbuf = main_win_obj.pixbuf_dict['folder_green_small'] - this_obj = main_win_obj.app_obj.fixed_clips_folder - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid: - pixbuf = main_win_obj.pixbuf_dict['folder_blue_small'] - this_obj = main_win_obj.app_obj.fixed_temp_folder - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - else: - pixbuf = main_win_obj.pixbuf_dict['folder_small'] - this_obj = main_win_obj.app_obj.container_reg_dict[dbid] - listmodel.append( [pixbuf, dbid, ' ' + this_obj.name] ) - - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 0, 5, 1, 1) - combo.set_hexpand(True) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, False) - combo.add_attribute(renderer_text, 'text', 2) - - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 6, 1, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('I want to download these videos'), - ) - grid.attach(self.radiobutton, 0, 7, 1, 1) - - self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) - self.radiobutton2.set_label( - _('Don\'t download the videos, just check them'), - ) - grid.attach(self.radiobutton2, 0, 8, 1, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 9, 1, 1) - - self.checkbutton = Gtk.CheckButton() - grid.attach(self.checkbutton, 0, 10, 1, 1) - self.checkbutton.set_label(_('Enable automatic copy/paste')) - self.checkbutton.connect('toggled', self.on_checkbutton_toggled) - - # Paste in the contents of the clipboard (if it contains valid URLs) - if main_win_obj.app_obj.dialogue_copy_clipboard_flag: - utils.add_links_to_textview_from_clipboard( - main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - ) - - # Display the dialogue window - self.show_all() - - - def close(self, also_self, textview): - - """Called from callback in self.__init__(). - - We have disabled drag-and-drop directly into the textview, but this - generates a Gtk warning when the textview is destroyed. - - Workaround is to re-enable drag-and-drop into the textview immediately - before it (and its window) are destroyed. - - Args: - - also_self (mainwin.AddVideoDialogue): Another copy of this window - - textview (Gtk.TextView): The textview for which drag-and-drop has - been disabled - - """ - - textview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - - - # Callback class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables clipboard monitoring. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active() \ - and self.clipboard_timer_id is not None: - - # Stop the timer - GObject.source_remove(self.clipboard_timer_id) - self.clipboard_timer_id = None - - elif checkbutton.get_active() and self.clipboard_timer_id is None: - - # Start the timer - self.clipboard_timer_id = GObject.timeout_add( - self.clipboard_timer_time, - self.clipboard_timer_callback, - ) - - - def on_combo_changed(self, combo): - - """Called a from callback in self.__init__(). - - Updates the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - self.parent_dbid = self.folder_list[combo.get_active()] - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - - def clipboard_timer_callback(self): - - """Called from a callback in self.on_checkbutton_toggled(). - - Periodically checks the system's clipboard, and adds any new URLs to - the dialogue window's textview. - """ - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - ) - - # Return 1 to keep the timer going - return 1 - - -class ApplyOptionsDialogue(Gtk.Dialog): - - """Called by mainwin.MainWin.on_video_index_apply_options() and - .on_video_catalogue_apply_options(). - - Prompt the user to specify whether a new options.OptionsManager object or - and existing one should be applied to a specified media data object. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Apply download options\' dialogue starts' \ - + ' here. In the Videos tab, right-click a channel, playlist or' \ - + ' folder and select Downloads > Apply download options...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - # Store the user's choices as IVs, so the calling function can retrieve - # them - self.options_obj = None - self.clone_flag = False - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Apply download options'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - app_obj = self.main_win_obj.app_obj - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Create new download options'), - ) - grid.attach(radiobutton, 0, 0, 1, 1) - # (Signal connect appears below) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 1, 1, 1) - - radiobutton2 = Gtk.RadioButton.new_with_label_from_widget( - radiobutton, - _('Use these download options'), - ) - grid.attach(radiobutton2, 0, 2, 1, 1) - # (Signal connect appears below) - - # Add a combo, containing all options.OptionsManager objects (besides - # the General Options Manager) - store = Gtk.ListStore(str, int) - for uid in sorted(app_obj.options_reg_dict): - - options_obj = app_obj.options_reg_dict[uid] - if options_obj != app_obj.general_options_obj: - - store.append([ - '#' + str(options_obj.uid) + ': ' + options_obj.name, - options_obj.uid, - ]) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 0, 3, 1, 1) - combo.set_hexpand(True) - combo.set_sensitive(False) - # (Signal connect appears below) - - cell = Gtk.CellRendererText() - combo.pack_start(cell, False) - combo.add_attribute(cell, 'text', 0) - combo.set_active(0) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 4, 1, 1) - - radiobutton3 = Gtk.RadioButton.new_with_label_from_widget( - radiobutton2, - _('Clone these download options'), - ) - grid.attach(radiobutton3, 0, 5, 1, 1) - # (Signal connect appears below) - - # Add a combo, containing all options.OptionsManager objects - store2 = Gtk.ListStore(str, int) - for uid in sorted(app_obj.options_reg_dict): - - options_obj = app_obj.options_reg_dict[uid] - store2.append([ - '#' + str(options_obj.uid) + ': ' + options_obj.name, - options_obj.uid, - ]) - - combo2 = Gtk.ComboBox.new_with_model(store2) - grid.attach(combo2, 0, 6, 1, 1) - combo2.set_hexpand(True) - combo2.set_sensitive(False) - # (Signal connect appears below) - - cell = Gtk.CellRendererText() - combo2.pack_start(cell, False) - combo2.add_attribute(cell, 'text', 0) - combo2.set_active(0) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_radiobutton_toggled, - combo, - combo2, - ) - radiobutton2.connect( - 'toggled', - self.on_radiobutton2_toggled, - combo, - combo2, - ) - combo.connect('changed', self.on_combo_changed) - radiobutton3.connect( - 'toggled', - self.on_radiobutton3_toggled, - combo, - combo2, - ) - combo2.connect('changed', self.on_combo2_changed) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - uid = model[tree_iter][1] - - self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid] - self.clone_flag = False - - - def on_combo2_changed(self, combo2): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo2 (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = combo2.get_active_iter() - model = combo2.get_model() - uid = model[tree_iter][1] - - self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid] - self.clone_flag = True - - - def on_radiobutton_toggled(self, button, combo, combo2): - - """Called from a callback in self.__init__(). - - User has selected to create a new options manager. - - Args: - - button (Gtk.RadioButton): The widget clicked - - combo, combo2 (Gtk.ComboBox): Other widgets to update - - """ - - if button.get_active(): - - self.options_obj = None - self.clone_flag = False - - combo.set_sensitive(False) - combo2.set_sensitive(False) - - - def on_radiobutton2_toggled(self, button, combo, combo2): - - """Called from a callback in self.__init__(). - - User wants to select an existing options manager. - - Args: - - button (Gtk.RadioButton): The widget clicked - - combo, combo2 (Gtk.ComboBox): Other widgets to update - - """ - - if button.get_active(): - - tree_iter = combo.get_active_iter() - model = combo.get_model() - uid = model[tree_iter][1] - - self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid] - self.clone_flag = False - - combo.set_sensitive(True) - combo2.set_sensitive(False) - - - def on_radiobutton3_toggled(self, button, combo, combo2): - - """Called from a callback in self.__init__(). - - User wants to clone an existing options manager. - - Args: - - button (Gtk.RadioButton): The widget clicked - - combo, combo2 (Gtk.ComboBox): Other widgets to update - - """ - - if button.get_active(): - - tree_iter = combo2.get_active_iter() - model = combo2.get_model() - uid = model[tree_iter][1] - - self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid] - self.clone_flag = True - - combo.set_sensitive(False) - combo2.set_sensitive(True) - - -class CalendarDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_button_find_date() and - config.OptionsEditWin.on_button_set_date_clicked(). - - Python class handling a dialogue window that prompts the user to choose a - date on a calendar - - Args: - - parent_win_obj (mainwin.MainWin or config.OptionsEditWin): The parent - window - - date (str): A date in the form YYYYMMDD. If set, that date is - selected in the calendar. If an empty string or None, no date is - selected - - """ - - - # Standard class methods - - - def __init__(self, parent_win_obj, date=None): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Select a date\' dialogue starts here. In' \ - + ' the toolbar at the bottom of the Videos tab, click the' \ - + ' \'Find date\' button' - ) - - # IV list - class objects - # ----------------------- - self.parent_win_obj = parent_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.calendar = None # Gtk.Calendar - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Select a date'), - parent_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(parent_win_obj.spacing_size) - grid.set_row_spacing(parent_win_obj.spacing_size) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.calendar = Gtk.Calendar.new() - grid.attach(self.calendar, 0, 0, 1, 1) - - # If the date was specified, it should be a string in the form YYYYMMDD - if date: - year = int(date[0:3]) - month = int(date[4:5]) - day = int(date[6:7]) - - if day >= 1 and day <= 31 and month >= 1 and month <= 12 \ - and year >=1: - self.calendar.select_month(month, year) - self.calendar.select_day(day) - - # Display the dialogue window - self.show_all() - - -class ChangeThemeDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_change_theme(). - - Python class handling a dialogue window that allows a user on MS Windows to - change the Gtk theme. - - Args: - - parent_win_obj (mainwin.MainWin): The parent window - - """ - - - # Standard class methods - - - def __init__(self, parent_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Change window theme\' dialogue starts' \ - + ' here. In the main window menu, click System > Change theme.' \ - + ' Available on MS Windows only' - ) - - # IV list - class objects - # ----------------------- - self.parent_win_obj = parent_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.combo = None # Gtk.ComboBox - - - # IV list - other - # --------------- - # Theme name; the name of a sub-folder in ../pack/mswin_themes, e.g. - # 'basic' - self.theme_name = 'default' - # Full path to that folder. If the theme named 'default' is selected, - # the path remains set to None (as we don't use a sub-folder with - # that name) - self.theme_path = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Change window theme'), - parent_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - self.grid = Gtk.Grid() - box.add(self.grid) - self.grid.set_border_width(parent_win_obj.spacing_size) - self.grid.set_row_spacing(parent_win_obj.spacing_size) - - label = Gtk.Label() - self.grid.attach(label, 0, 0, 1, 1) - label.set_markup( - utils.tidy_up_long_descrip( - _('Choose a new window theme for Tartube'), - parent_win_obj.quite_long_string_max_len, - ), - ) - - # Add a combo, containing all available themes. The 'default' theme - # always appears at the top - store = Gtk.ListStore(str, str) - store.append([ 'default', '' ]) - - theme_dict = self.get_theme_dict() - for theme_name in sorted(theme_dict): - store.append([ theme_name, theme_dict[theme_name] ]) - - combo = Gtk.ComboBox.new_with_model(store) - self.grid.attach(combo, 0, 1, 1, 1) - combo.set_hexpand(True) - # (Signal connect appears below) - - cell = Gtk.CellRendererText() - combo.pack_start(cell, False) - combo.add_attribute(cell, 'text', 0) - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - label2 = Gtk.Label() - self.grid.attach(label2, 0, 2, 1, 1) - label2.set_markup( - '' + utils.tidy_up_long_descrip( - _( - 'Hint: choose \'default\' for the normal MS Windows theme', - ), - parent_win_obj.quite_long_string_max_len, - ) + '', - ) - - # Display the dialogue window - self.show_all() - - - # Support functions - - - def get_theme_dict(self): - - """Called by self.__init__(). - - In the MSYS2 environment, GTK themes can be set using a settings.ini - file. - - In ../pack/mswin_themes can be found several sub-folders, each one - named after a theme, and containing a settings.ini that will set that - theme. - - Compile a dictionary of available themes for the dialogue window to - use. - - Return values: - - Dictionary whose keys are a theme name like 'dark', and whose - corresponding values are the full path to the sub-folder with - that name - - """ - - app_obj = self.parent_win_obj.app_obj - - themes_path = os.path.abspath( - os.path.join( - app_obj.script_parent_dir, - 'pack', - 'mswin_themes', - ), - ) - - try: - file_list = os.listdir(path = themes_path) - except: - file_list = [] - - theme_dict = {} - for filename in file_list: - - # Ignore any folder called 'default' - if filename != 'default': - - dir_path = os.path.abspath( - os.path.join( - app_obj.script_parent_dir, - 'pack', - 'mswin_themes', - filename, - ), - ) - settings_path = os.path.abspath( - os.path.join(dir_path, 'settings.ini'), - ) - - if os.path.isdir(dir_path) \ - and not re.search(r'^\.', dir_path) \ - and os.path.isfile(settings_path): - theme_dict[filename] = dir_path - - return theme_dict - - - # Callback class methods - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - theme_name = model[tree_iter][0] - theme_path = model[tree_iter][1] - - self.theme_name = theme_name - if theme_name == 'default': - self.theme_path = None - else: - self.theme_path = theme_path - - -class CreateProfileDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_create_profile(). - - Python class handling a dialogue window that prompts the user to create a - profile, remembering which media.Channel, media.Playlist and media.Folder - objects are currently marked. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Create profile\' dialogue starts here.' \ - + ' In the main window menu, click Media > Profiles > Create' \ - + ' profile...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - other - # --------------- - # The user's choice of name. Set to None when the entry box is empty - self.profile_name = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Create profile'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label() - grid.attach(label, 0, 1, 1, 1) - label.set_markup(_('Items currently marked:')) - label.set_alignment(0, 0.5) - - scrolled = Gtk.ScrolledWindow() - grid.attach(scrolled, 0, 2, 1, 1) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_hexpand(True) - scrolled.set_vexpand(True) - scrolled.set_size_request(-1, 150) - - frame = Gtk.Frame() - scrolled.add_with_viewport(frame) - - treeview = Gtk.TreeView() - frame.add(treeview) - treeview.set_can_focus(False) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - _('Type'), - renderer_pixbuf, - pixbuf=0, - ) - treeview.append_column(column_pixbuf) - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - _('Name'), - renderer_text, - text=1, - ) - treeview.append_column(column_text) - - liststore = Gtk.ListStore(GdkPixbuf.Pixbuf, str) - treeview.set_model(liststore) - - for dbid in sorted(main_win_obj.video_index_marker_dict): - media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Channel): - pixbuf = main_win_obj.pixbuf_dict['channel_small'] - elif isinstance(media_data_obj, media.Playlist): - pixbuf = main_win_obj.pixbuf_dict['playlist_small'] - else: - pixbuf = main_win_obj.pixbuf_dict['folder_small'] - - liststore.append( [pixbuf, name] ) - - label2 = Gtk.Label() - grid.attach(label2, 0, 3, 1, 1) - label2.set_markup(_('Remember these items with a profile named:')) - label2.set_alignment(0, 0.5) - - entry = Gtk.Entry() - grid.attach(entry, 0, 4, 1, 1) - entry.set_hexpand(True) - entry.grab_focus() - entry.connect('changed', self.on_entry_changed) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_entry_changed(self, entry): - - """Called from callback in self.__init__(). - - Updates IVs. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - name = entry.get_text() - if name == '': - self.profile_name = None - else: - self.profile_name = name - - -class DeleteContainerDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.delete_container(). - - Python class handling a dialogue window that prompts the user for - confirmation, before removing a media.Channel, media.Playlist or - media.Folder object. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist or media.Folder): The - container media data object to be deleted - - empty_flag (bool): If True, the container media data object is to be - emptied, rather than being deleted - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj, empty_flag): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Delete channel\' dialogue starts here.' \ - + ' In the Videos tab, right-click a channel and select Delete' \ - + ' channel' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.button = None # Gtk.RadioButton - self.button2 = None # Gtk.RadioButton - - - # IV list - other - # --------------- - # Number of videos found in the container - self.video_count = 0 - - - # Code - # ---- - - # Prepare variables - spacing_size = self.main_win_obj.spacing_size - label_length = self.main_win_obj.long_string_max_len - - media_type = media_data_obj.get_type() - if media_type == 'video': - - return main_win_obj.app_obj.system_error( - 268, - 'Dialogue window setup failed sanity check', - ) - - # Count the container object's children - total_count, self.video_count, channel_count, playlist_count, \ - folder_count = media_data_obj.count_descendants( [0, 0, 0, 0, 0] ) - - # Create the dialogue window - if not empty_flag: - if media_type == 'channel': - title = _('Delete channel') - elif media_type == 'playlist': - title = _('Delete playlist') - else: - title = _('Delete folder') - else: - if media_type == 'channel': - title = _('Empty channel') - elif media_type == 'playlist': - title = _('Empty playlist') - else: - title = _('Empty folder') - - Gtk.Dialog.__init__( - self, - title, - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - self.set_resizable(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - - label = Gtk.Label() - grid.attach(label, 0, 0, 1, 1) - label.set_markup('' + media_data_obj.name + '') - - # Separator - grid.attach(Gtk.HSeparator(), 0, 1, 1, 1) - - if not total_count: - - if media_type == 'channel': - string = _('This channel does not contain any videos') - elif media_type == 'playlist': - string = _('This playlist does not contain any videos') - else: - string = _('This folder doesn\'t contain anything') - - - label2 = Gtk.Label( - utils.tidy_up_long_string( - string + ' ' + _( - '(but there might be some files in Tartube\'s data' - + ' folder)', - ), - label_length, - ), - ) - - grid.attach(label2, 0, 2, 1, 5) - label2.set_alignment(0, 0.5) - - else: - - if media_type == 'channel': - string = _('This channel contains:') - elif media_type == 'playlist': - string = _('This playlist contains:') - else: - string = _('This folder contains:') - - label2 = Gtk.Label(string) - grid.attach(label2, 0, 2, 1, 1) - label2.set_alignment(0, 0.5) - - if folder_count == 1: - label_string = _('1 folder') - else: - label_string = _('{0} folders').format(str(folder_count)) - - label3 = Gtk.Label() - grid.attach(label3, 0, 3, 1, 1) - label3.set_markup(label_string) - - if channel_count == 1: - label_string = _('1 channel') - else: - label_string = _('{0} channels').format(str(channel_count)) - - label4 = Gtk.Label() - grid.attach(label4, 0, 4, 1, 1) - label4.set_markup(label_string) - - if playlist_count == 1: - label_string = _('1 playlist') - else: - label_string = _('{0} playlists').format(str(playlist_count)) - - label5 = Gtk.Label() - grid.attach(label5, 0, 5, 1, 1) - label5.set_markup(label_string) - - if self.video_count == 1: - label_string = _('1 video') - else: - label_string = _('{0} videos').format(str(self.video_count)) - - label6 = Gtk.Label() - grid.attach(label6, 0, 6, 1, 1) - label6.set_markup(label_string) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 7, 1, 1) - - if not empty_flag: - - if media_type == 'channel': - string = _( - 'Do you want to remove the channel from your' \ - + ' filesystem, or do you just want to remove the' \ - + ' channel from this list?', - ) - string2 = _('Just remove the channel from this list') - - elif media_type == 'playlist': - string = _( - 'Do you want to remove the playlist from your' \ - + ' filesystem, or do you just want to remove the' \ - + ' playlist from this list?', - ) - string2 = _('Just remove the playlist from this list') - - else: - string = _( - 'Do you want to remove the folder from your' \ - + ' filesystem, or do you just want to remove the' \ - + ' folder from this list?', - ) - string2 = _('Just remove the folder from this list') - - else: - - if media_type == 'channel': - string = _( - 'Do you want to empty the channel on your filesystem,' \ - + ' or do you just want to empty the channel in this' \ - + ' list?', - ) - string2 = _('Just empty the channel in this list') - - elif media_type == 'playlist': - string = _( - 'Do you want to empty the playlist on your filesystem,' \ - + ' or do you just want to empty the playlist in this' \ - + ' list?', - ) - string2 = _('Just empty the playlist in this list') - - else: - string = _( - 'Do you want to empty the folder on your filesystem,' \ - + ' or do you just want to empty the folder in this' \ - + ' list?', - ) - string2 = _('Just empty the folder in this list') - - label7 = Gtk.Label( - utils.tidy_up_long_string( - string, - label_length, - ), - ) - grid.attach(label7, 0, 8, 1, 1) - label7.set_alignment(0, 0.5) - - self.button = Gtk.RadioButton.new_with_label_from_widget(None, string2) - grid.attach(self.button, 0, 9, 1, 1) - - self.button2 = Gtk.RadioButton.new_from_widget(self.button) - grid.attach(self.button2, 0, 10, 1, 1) - self.button2.set_label(_('Delete files on your computer')) - if main_win_obj.app_obj.delete_container_files_flag: - self.button2.set_active(True) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 11, 1, 1) - - self.button3 = Gtk.CheckButton.new_with_label( - _('Always show this window'), - ) - grid.attach(self.button3, 0, 12, 1, 1) - if main_win_obj.app_obj.show_delete_container_dialogue_flag: - self.button3.set_active(True) - - # Display the dialogue window - self.show_all() - - -class DeleteDropZoneDialogue(Gtk.Dialog): - - """Called by mainwin.DropZoneBox.on_delete_button_clicked(). - - Prompt the user to choose whether to delete just the dropzone, or its - associated options.OptionsManager object too. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - options_obj (options.OptionsManager): The download options whose - dropzone is to be deleted - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, options_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Delete dropzone\' dialogue starts here.' \ - + ' In the Drag and Drop tab, click a delete button in the' \ - + ' bottom-right corner of any dropzone' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - self.options_obj = options_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - self.del_dropzone_flag = False - self.del_both_flag = False - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Delete dropzone'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - ) - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - button = Gtk.Button.new_with_label(_('Just delete the dropzone')) - grid.attach(button, 0, 0, 1, 1) - button.set_hexpand(True) - button.connect('clicked', self.on_dropzone_button_clicked) - - button2 = Gtk.Button.new_with_label(_('Also delete download options')) - grid.attach(button2, 0, 1, 1, 1) - button2.set_hexpand(True) - button2.connect('clicked', self.on_both_button_clicked) - # Don't delete download options in the middle of a download operation, - # or if the options have been applied to a media data object - # Don't delete general download options - if self.main_win_obj.app_obj.current_manager_obj \ - or options_obj.dbid_list \ - or options_obj == self.main_win_obj.app_obj.general_options_obj: - button2.set_sensitive(False) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_dropzone_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Deletes the dropzone, but not the options.OptionsManager object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.del_dropzone_flag = True - self.destroy() - - - def on_both_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Deletes the dropzone and the options.OptionsManager object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.del_both_flag = True - self.destroy() - - -class DeleteVideoDialogue(Gtk.Dialog): - - """Called by mainwin.MainWin.on_video_catalogue_delete_video() and - .on_video_catalogue_delete_video_multi(). - - Python class handling a dialogue window that prompts the user for - confirmation, before removing one or more media.Video objects. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_list (list): List of media.Video objects to be deleted - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_list): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Delete videos\' dialogue starts here.' \ - + ' In the Videos tab, right-click a video and select Delete video' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.button = None # Gtk.RadioButton - self.button2 = None # Gtk.RadioButton - self.button3 = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # Number of videos found in the container - self.video_count = 0 - - - # Code - # ---- - - # Prepare variables - spacing_size = self.main_win_obj.spacing_size - label_length = self.main_win_obj.long_string_max_len - current_dbid = main_win_obj.video_index_current_dbid - - parent_obj = None - try: - parent_obj = main_win_obj.app_obj.media_reg_dict[current_dbid] - except: - pass - - if parent_obj is None or not media_list: - - return main_win_obj.app_obj.system_error( - 269, - 'Dialogue window setup failed sanity check', - ) - - for media_data_obj in media_list: - if media_data_obj.get_type() != 'video': - - return main_win_obj.app_obj.system_error( - 270, - 'Dialogue window setup failed sanity check', - ) - - # Create the dialogue window - Gtk.Dialog.__init__( - self, - _('Delete videos'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - self.set_resizable(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - - label = Gtk.Label() - grid.attach(label, 0, 0, 1, 1) - label.set_markup('' + parent_obj.name + '') - - if len(media_list) == 1: - label_string = _('1 selected video') - else: - label_string \ - = _('{0} selected videos').format(str(self.video_count)) - - label2 = Gtk.Label() - grid.attach(label2, 0, 1, 1, 1) - label2.set_markup(label_string) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 2, 1, 1) - - parent_type = parent_obj.get_type() - if parent_type == 'channel': - string = _( - 'Do you want to remove the video(s) from your filesystem,' \ - + ' or do you just want to remove them from this channel?', - ) - string2 = _('Just remove the video(s) from this channel') - - elif parent_type == 'playlist': - string = _( - 'Do you want to remove the video(s) from your filesystem,' \ - + ' or do you just want to remove them from this playlist?', - ) - string2 = _('Just remove the video(s) from this playlist') - - else: - string = _( - 'Do you want to remove the video(s) from your filesystem,' \ - + ' or do you just want to remove them from this folder?', - ) - string2 = _('Just remove the video(s) from this folder') - - label3 = Gtk.Label( - utils.tidy_up_long_string( - string, - label_length, - ), - ) - grid.attach(label3, 0, 3, 1, 1) - label3.set_alignment(0, 0.5) - - self.button = Gtk.RadioButton.new_with_label_from_widget(None, string2) - grid.attach(self.button, 0, 4, 1, 1) - - self.button2 = Gtk.RadioButton.new_from_widget(self.button) - grid.attach(self.button2, 0, 5, 1, 1) - self.button2.set_label(_('Delete files on your computer')) - if main_win_obj.app_obj.delete_video_files_flag: - self.button2.set_active(True) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 6, 1, 1) - - self.button3 = Gtk.CheckButton.new_with_label( - _('Always show this window'), - ) - grid.attach(self.button3, 0, 7, 1, 1) - if main_win_obj.app_obj.show_delete_video_dialogue_flag: - self.button3.set_active(True) - - # Display the dialogue window - self.show_all() - - -class DuplicateVideoDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_video(). - - Python class handling a dialogue window that shows the user any video URls - which are duplicates. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - duplicate_list (list): List of URLs - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, duplicate_list): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Duplicate URLs\' dialogue starts here.' \ - + ' In the main window toolbar, click the Video button. In the' \ - + ' top half of the first dialogue window, add duplicate URLs' \ - + ' and then click the OK button' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - # (none) - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Duplicate URLs'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - # (Set enough vertical/horizontal room for several URLs) - self.set_size_request( - main_win_obj.app_obj.config_win_width - 150, - 300, - ) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label(_('The following URLs are duplicates:')) - grid.attach(label, 0, 0, 1, 1) - - scrolled = Gtk.ScrolledWindow() - grid.attach(scrolled, 0, 1, 1, 1) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_hexpand(True) - scrolled.set_vexpand(True) - - frame = Gtk.Frame() - scrolled.add_with_viewport(frame) - - textview = Gtk.TextView() - frame.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_editable(False) - textview.set_cursor_visible(False) - - textbuffer = textview.get_buffer() - textbuffer.set_text('\n'.join(duplicate_list) + '\n') - - # Display the dialogue window - self.show_all() - - -class ExportDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.export_from_db(). - - Python class handling a dialogue window that prompts the user before - creating a database export. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - whole_flag (bool): True if the whole database is to be exported, False - if only part of the database is to be exported - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, whole_flag): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Export from database\' dialogue starts' \ - + ' here. In the main window menu, click Media > Export/Import' \ - + ' > Export from database...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.checkbutton = None # Gtk.CheckButton - self.checkbutton2 = None # Gtk.CheckButton - self.checkbutton3 = None # Gtk.CheckButton - self.checkbutton4 = None # Gtk.CheckButton - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.radiobutton3 = None # Gtk.RadioButton - - - # IV list - other - # --------------- - # The selected CSV separator - self.separator = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Export from database'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - spacing_size = self.main_win_obj.spacing_size - label_length = self.main_win_obj.long_string_max_len - - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - - grid_width = 2 - - if not whole_flag: - msg = _( - 'Tartube is ready to export a partial summary of its' \ - + ' database, containing a list of videos, channels,' \ - + ' playlists and/or folders (but not including the videos' \ - + ' themselves)', - ) - else: - msg = _( - 'Tartube is ready to export a summary of its database,' \ - + ' containing a list of videos, channels, playlists and/or' \ - + ' folders (but not including the videos themselves)', - ) - - label = Gtk.Label( - utils.tidy_up_long_string( - msg, - label_length, - ), - ) - grid.attach(label, 0, 0, grid_width, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 1, grid_width, 1) - - label = Gtk.Label(_('Choose what should be included:')) - grid.attach(label, 0, 2, grid_width, 1) - label.set_alignment(0, 0.5) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.checkbutton = Gtk.CheckButton() - grid.attach(self.checkbutton, 0, 3, grid_width, 1) - self.checkbutton.set_label(_('Include lists of videos')) - self.checkbutton.set_active(False) - - self.checkbutton2 = Gtk.CheckButton() - grid.attach(self.checkbutton2, 0, 4, grid_width, 1) - self.checkbutton2.set_label(_('Include channels')) - self.checkbutton2.set_active(True) - - self.checkbutton3 = Gtk.CheckButton() - grid.attach(self.checkbutton3, 0, 5, grid_width, 1) - self.checkbutton3.set_label(_('Include playlists')) - self.checkbutton3.set_active(True) - - self.checkbutton4 = Gtk.CheckButton() - grid.attach(self.checkbutton4, 0, 6, grid_width, 1) - self.checkbutton4.set_label(_('Preserve folder structure')) - self.checkbutton4.set_active(True) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 7, grid_width, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Export as JSON'), - ) - grid.attach(self.radiobutton, 0, 8, grid_width, 1) - - self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton, - _('Export as CSV using separator'), - ) - grid.attach(self.radiobutton2, 0, 9, 1, 1) - # (Signal connect appears below) - - # (At the moment, Tartube only offers two choices of CSV separator) - liststore = Gtk.ListStore(str) - liststore.append(['|']) - liststore.append([',']) - - combo = Gtk.ComboBox.new_with_model(liststore) - grid.attach(combo, 1, 9, 1, 1) - combo.set_hexpand(True) - - cell = Gtk.CellRendererText() - combo.pack_start(cell, False) - combo.add_attribute(cell, 'text', 0) - combo.connect('changed', self.on_combo_changed) - combo.set_sensitive(False) - - if main_win_obj.app_obj.export_csv_separator == ',': - combo.set_active(1) - else: - combo.set_active(0) - - self.radiobutton3 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton, - _('Export as plain text'), - ) - grid.attach(self.radiobutton3, 0, 10, grid_width, 1) - - # (Signal connects from above) - self.radiobutton2.connect( - 'toggled', - self.on_radiobutton_toggled, - combo, - ) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_combo_changed(self, combo): - - """Called a from callback in self.__init__(). - - Updates the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.separator = model[tree_iter][0] - - - def on_radiobutton_toggled(self, button, combo): - - """Called from a callback in self.__init__(). - - (De)sensitises the combobox, as required. - - Args: - - button (Gtk.RadioButton): The widget clicked - - combo (Gtk.ComboBox): Another widget to update - - """ - - if button.get_active(): - combo.set_sensitive(True) - else: - combo.set_sensitive(False) - - -class ExtractorCodeDialogue(Gtk.Dialog): - - """Called by config.OptionsEditWin.on_formats_tab_type_clicked(). - - Python class handling a dialogue window that prompts the user to type a - format extractor code. - - Args: - - parent_win_obj (config.SystemPrefWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, parent_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Type extractor code\' dialogue starts' \ - + ' here. In the main window menu, click Edit > General download' \ - + ' Options... > Formats > Preferred, then click the small' \ - + ' button next to the \'Add format\' button' - ) - - # IV list - class objects - # ----------------------- - self.parent_win_obj = parent_win_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - # The extractor code types, or None if nothing typed - self.extract_code = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Type extractor code'), - parent_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(parent_win_obj.spacing_size) - grid.set_row_spacing(parent_win_obj.spacing_size) - - # (Get the minimum and maximum numeric values specified in formats.py. - # Actually, the user can type a non-numeric value appearing as a key - # in formats.VIDEO_OPTION_TYPE_DICT, e.g. '144p' or 'mp4', but we - # don't advertise the fact - min_code = 1 - max_code = 1 - for value in formats.VIDEO_OPTION_TYPE_DICT.keys(): - - if value.isdigit(): - if int(value) < min_code: - min_code = int(value) - elif int(value) > max_code: - max_code = int(value) - - label = Gtk.Label() - grid.attach(label, 0, 0, 1, 1) - label.set_markup( - _( - 'Type an extractor code in the range {0}-{1}' - ).format(str(min_code), str(max_code)) \ - + '\n' + _( - '(mp3, mp4 etc are also acceptable)', - ) - ) - - label2 = Gtk.Label() - grid.attach(label, 0, 1, 1, 1) - label2.set_markup(_('e.g. 136 for mp4 720p')) - - entry = Gtk.Entry() - grid.attach(entry, 0, 2, 1, 1) - entry.connect('changed', self.on_entry_changed) - entry.connect('activate', self.on_entry_activated) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_entry_activated(self, entry): - - """Called from callback in self.__init__(). - - Sets the extractor code and closes the dialogue window. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - value = entry.get_text() - # (Unspecified extractor codes are stored as None, not empty strings) - if value == '': - self.extract_code = None - else: - self.extract_code = value - - self.response(Gtk.ResponseType.OK) - - - def on_entry_changed(self, entry): - - """Called from callback in self.__init__(). - - Sets the extractor code. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - value = entry.get_text() - # (Unspecified extractor codes are stored as None, not empty strings) - if value == '': - self.extract_code = None - else: - self.extract_code = value - - -class FormatsSubsDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.info_manager_finished(). - - Python class handling a dialogue window that prompts the user to set or - apply download options, having fetched available formats/subtitles for a - (single) video. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - video_obj (media.Video): The selected video - - info_type (str): The information fetched during the info operation: - 'formats' or 'subs' - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj, info_type): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Confirmation dialogue for fetching' \ - + ' formats/subtitles. In the Videos tab, right-click a video' \ - + ' and select Fetch > Available formats... or Fetch > Available' \ - + ' subtitles' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - # The selected video - self.video_obj = video_obj - # The information fetched during the info operation: 'formats' or - # 'subs' - self.info_type = info_type - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Operation complete'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - if info_type == 'formats': - msg = _('Click the Output tab to see available formats') - else: - msg = _('Click the Output tab to see available subtitles') - - label = Gtk.Label(msg) - grid.attach(label, 0, 0, 1, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 1, 1, 1) - - button = Gtk.Button.new_with_label( - _('Update general download options'), - ) - grid.attach(button, 0, 2, 1, 1) - button.connect('clicked', self.on_general_button_clicked) - - if not video_obj.options_obj: - - button2 = Gtk.Button.new_with_label( - _('Apply download options to this video only'), - ) - grid.attach(button2, 0, 3, 1, 1) - button2.connect('clicked', self.on_apply_button_clicked) - - else: - - button2 = Gtk.Button.new_with_label( - _('Update this video\'s download options'), - ) - grid.attach(button2, 0, 3, 1, 1) - button2.connect('clicked', self.on_update_button_clicked) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_apply_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the button is clicked, applies download options to the selected - video only. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Import the main application (for convenience) - app_obj = self.main_win_obj.app_obj - - # Check the video still exists - if not self.video_obj.dbid in app_obj.media_reg_dict \ - or app_obj.media_reg_dict[self.video_obj.dbid] != self.video_obj: - self.destroy() - - else: - - # Clone general download options... - options_obj = app_obj.clone_download_options( - app_obj.general_options_obj, - ) - - # ...and apply the cloned options to the video - app_obj.apply_download_options(self.video_obj, options_obj) - - # Open an edit window to show the options immediately - win_obj = config.OptionsEditWin( - app_obj, - options_obj, - self.info_type, - ) - - self.destroy() - win_obj.present() - - - def on_general_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the button is clicked, open the edit window for the current - options.OptionsManager object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Import the main application (for convenience) - app_obj = self.main_win_obj.app_obj - - # Check the video still exists - if not self.video_obj.dbid in app_obj.media_reg_dict \ - or app_obj.media_reg_dict[self.video_obj.dbid] != self.video_obj: - self.destroy() - - else: - - win_obj = config.OptionsEditWin( - app_obj, - app_obj.general_options_obj, - self.info_type, - ) - - self.destroy() - win_obj.present() - - - def on_update_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the button is clicked, open the edit window for the video's - existing options.OptionsManager object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Import the main application (for convenience) - app_obj = self.main_win_obj.app_obj - - # Check the video still exists - if not self.video_obj.dbid in app_obj.media_reg_dict \ - or app_obj.media_reg_dict[self.video_obj.dbid] != self.video_obj: - self.destroy() - - else: - - win_obj = config.OptionsEditWin( - app_obj, - self.video_obj.options_obj, - self.info_type, - ) - - self.destroy() - win_obj.present() - - -class ImportDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.import_into_db(). - - Python class handling a dialogue window that prompts the user before - hanlding an export file, created by mainapp.TartubeApp.export_from_db(). - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - db_dict (dict): The imported data, a dictionary described in the - comments in mainapp.TartubeApp.export_from_db() - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, db_dict): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Import into database\' dialogue starts' \ - + ' here. In the main window menu, click Media > Export/Import' \ - + ' > Import into database...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.treeview = None # Gtk.TreeView - self.liststore = None # Gtk.TreeView - self.checkbutton = None # Gtk.TreeView - self.checkbutton2 = None # Gtk.TreeView - - - # IV list - other - # --------------- - # A flattened dictionary of media data objects - self.flat_db_dict = {} - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Import into database'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - self.set_default_size( - main_win_obj.app_obj.config_win_width, - main_win_obj.app_obj.config_win_height, - ) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid_width = 4 - - label = Gtk.Label(_('Choose which items to import')) - grid.attach(label, 0, 0, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - grid.attach(scrolled, 0, 1, grid_width, 1) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_hexpand(True) - scrolled.set_vexpand(True) - - frame = Gtk.Frame() - scrolled.add_with_viewport(frame) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.treeview = Gtk.TreeView() - frame.add(self.treeview) - self.treeview.set_can_focus(False) - - renderer_toggle = Gtk.CellRendererToggle() - renderer_toggle.connect('toggled', self.on_checkbutton_toggled) - column_toggle = Gtk.TreeViewColumn( - _('Import'), - renderer_toggle, - active=0, - ) - self.treeview.append_column(column_toggle) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - _('Type'), - renderer_pixbuf, - pixbuf=1, - ) - self.treeview.append_column(column_pixbuf) - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - _('Name'), - renderer_text, - text=2, - ) - self.treeview.append_column(column_text) - - renderer_text2 = Gtk.CellRendererText() - column_text2 = Gtk.TreeViewColumn( - 'hide', - renderer_text2, - text=3, - ) - column_text2.set_visible(False) - self.treeview.append_column(column_text2) - - self.liststore = Gtk.ListStore(bool, GdkPixbuf.Pixbuf, str, int) - self.treeview.set_model(self.liststore) - - self.checkbutton = Gtk.CheckButton() - grid.attach(self.checkbutton, 0, 2, 1, 1) - self.checkbutton.set_label(_('Import videos')) - self.checkbutton.set_active(True) - - self.checkbutton2 = Gtk.CheckButton() - grid.attach(self.checkbutton2, 1, 2, 1, 1) - self.checkbutton2.set_label(_('Merge channels/playlists/folders')) - self.checkbutton2.set_active(False) - - button = Gtk.Button.new_with_label(_('Select all')) - grid.attach(button, 2, 2, 1, 1) - button.set_hexpand(False) - button.connect('clicked', self.on_select_all_clicked) - - button2 = Gtk.Button.new_with_label(_('Unselect all')) - grid.attach(button2, 3, 2, 1, 1) - button2.set_hexpand(False) - button2.connect('clicked', self.on_deselect_all_clicked) - - # The data is imported as a dictionary, perhaps preserving the original - # folder structure of the database, or perhaps not - # The 'db_dict' format is described in the comments in - # mainapp.TartubeApp.export_from_db() - # 'db_dict' contains mini-dictionaries, 'mini_dict', whose format is - # also described in that function. Each 'mini_dict' represents a - # single media data object - # - # Convert 'db_dict' to a list. Each item in the list is a 'mini_dict'. - # Each 'mini_dict' has some new key-value pairs (except those - # representing videos): - # - # - 'video_count': int (showing the number of videos the channel, - # playlist or folder contains) - # - 'display_name': str (the channel/playlist/folder name indented - # with extra whitespace (so the user can clearly see the folder - # structure) - # - 'import_flag': bool (True if this channel/playlist/folder should - # be imported, False if not) - converted_list = self.convert_to_list( db_dict, [] ) - - # Add a line to the treeview for each channel, playlist and folder - for mini_dict in converted_list: - - pixbuf = main_win_obj.pixbuf_dict[mini_dict['type'] + '_small'] - text = mini_dict['display_name'] - if mini_dict['video_count'] == 1: - text += ' [ ' + _('1 video') + ' ]' - elif mini_dict['video_count']: - text += ' [ ' \ - + _('{0} videos').format(str(mini_dict['video_count'])) + ' ]' - - self.liststore.append( [True, pixbuf, text, mini_dict['dbid']] ) - - # Compile a dictionary, a flattened version of the original 'db_dict' - # (i.e. which the original database's folder structure removed) - # This new dictionary contains a single key-value pair for every - # channel, playlist and folder. Dictionary in the form: - # - # key: the channel/playlist/folder dbid - # value: the 'mini_dict' for that channel/playlist/folder - # - # If the channel/playlist/folder has any child videos, then its - # 'mini_dict' still has some child 'mini_dicts', one for each video - for mini_dict in converted_list: - self.flat_db_dict[mini_dict['dbid']] = mini_dict - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def convert_to_list(self, db_dict, converted_list, - parent_mini_dict=None, recursion_level=0): - - """Called by self.__init__(). Subsequently called by this function - recursively. - - Converts the imported 'db_dict' into a list, with each item in the - list being a 'mini_dict' (the format of both dictionaries is described - in the comments in mainapp.TartubeApp.export_from_db() ). - - Args: - - db_dict (dict): The dictionary described in self.export_from_db(); - if called from self.__init__(), the original imported - dictionary; if called recursively, a dictionary from somewhere - inside the original imported dictionary - - converted_list (list): The converted list so far; this function - adds more 'mini_dict' items to the list - - parent_mini_dict (dict): The contents of db_dict all represent - children of the channel/playlist/folder represent by this - dictionary - - recursion_level (int): The number of recursive calls to this - function (so far) - - """ - - # (Sorting function for the code immediately below) - def sort_dict_by_name(this_dict): - return this_dict['name'] - - # Deal with importable videos/channels/playlists/folders in - # alphabetical order - for mini_dict in sorted(db_dict.values(), key=sort_dict_by_name): - - if mini_dict['type'] == 'video': - - # Videos are not displayed in the treeview (but we count the - # number of videos in each channel/playlist/folder) - if parent_mini_dict: - parent_mini_dict['video_count'] += 1 - - else: - - # In the treeview, the channel/playlist/folder name is - # indented, so the user can see the folder structure - mini_dict['display_name'] = (' ' * 3 * recursion_level) \ - + mini_dict['name'] - - # Count the number of videos this channel/playlist/folder - # contains - mini_dict['video_count'] = 0 - - # Import everything, until the user chooses otherwise - mini_dict['import_flag'] = True - - # Add this channel/playlist/folder to the list visible in the - # treeview - converted_list.append(mini_dict) - # Call this function to process any child videos/channels/ - # playlists/folders - converted_list = self.convert_to_list( - mini_dict['db_dict'], - converted_list, - mini_dict, - recursion_level + 1, - ) - - # Procedure complete - return converted_list - - - # Callback class methods - - - def on_checkbutton_toggled(self, checkbutton, path): - - """Called from a callback in self.__init__(). - - Respond when the user selects/deselects an item in the treeview. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - path (int): A number representing the widget's row - - """ - - # The user has clicked on the checkbutton widget, so toggle the widget - # itself - self.liststore[path][0] = not self.liststore[path][0] - - # Update the data to be returned (eventually) to the calling - # mainapp.TartubeApp.import_into_db() function - mini_dict = self.flat_db_dict[self.liststore[path][3]] - mini_dict['import_flag'] = self.liststore[path][0] - - - def on_select_all_clicked(self, button): - - """Called from a callback in self.__init__(). - - Mark all channels/playlists/folders to be imported. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.liststore)): - self.liststore[path][0] = True - - for mini_dict in self.flat_db_dict.values(): - mini_dict['import_flag'] = True - - - def on_deselect_all_clicked(self, button): - - """Called from a callback in self.__init__(). - - Mark all channels/playlists/folders to be not imported. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.liststore)): - self.liststore[path][0] = False - - for mini_dict in self.flat_db_dict.values(): - mini_dict['import_flag'] = False - - -class InsertVideoDialogue(Gtk.Dialog): - - """Called by mainwin.MainWin.on_video_index_insert_videos(). - - Python class handling a dialogue window that inserts invidual video(s) - into a channel. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - parent_obj (media.Channel, media.Playlist or media.Folder): Name of - the container into which videos are to be inserted. At the moment, - no calling code specifies a playlist or folder, but such a call is - nevertheless permitted - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, parent_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Insert videos\' dialogue starts here.' \ - + ' In the Videos tab, right-click a channel and select' \ - + ' Channel actions > Insert videos...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.textbuffer = None # Gtk.TextBuffer - self.mark_start = None # Gtk.TextMark - self.mark_end = None # Gtk.TextMark - self.checkbutton = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # The media.Channel or media.Playlist into which videos are to be - # inserted - self.parent_obj = parent_obj - # Set up IVs for clipboard monitoring, if required - self.clipboard_timer_id = None - self.clipboard_timer_time = 250 - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Insert videos'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label(_('Copy and paste the links to one or more videos')) - grid.attach(label, 0, 0, 1, 1) - - if main_win_obj.app_obj.operation_convert_mode == 'channel': - - text = _( - 'Links containing multiple videos will be converted to' \ - + ' a channel', - ) - - elif main_win_obj.app_obj.operation_convert_mode == 'playlist': - - text = _( - 'Links containing multiple videos will be converted to a' \ - + ' playlist', - ) - - elif main_win_obj.app_obj.operation_convert_mode == 'multi': - - text = _( - 'Links containing multiple videos will be downloaded' \ - + ' separately', - ) - - elif main_win_obj.app_obj.operation_convert_mode == 'disable': - - text = _( - 'Links containing multiple videos will not be downloaded' - + ' at all', - ) - - label = Gtk.Label() - label.set_markup('' + text + '') - grid.attach(label, 0, 1, 1, 1) - - frame = Gtk.Frame() - grid.attach(frame, 0, 2, 1, 1) - - scrolledwindow = Gtk.ScrolledWindow() - frame.add(scrolledwindow) - # (Set enough vertical room for at several URLs) - scrolledwindow.set_size_request(-1, 150) - - textview = Gtk.TextView() - scrolledwindow.add(textview) - textview.set_hexpand(True) - self.textbuffer = textview.get_buffer() - - # Some callbacks will complain about invalid iterators, if we try to - # use Gtk.TextIters, so use Gtk.TextMarks instead - self.mark_start = self.textbuffer.create_mark( - 'mark_start', - self.textbuffer.get_start_iter(), - True, # Left gravity - ) - self.mark_end = self.textbuffer.create_mark( - 'mark_end', - self.textbuffer.get_end_iter(), - False, # Not left gravity - ) - - # Drag-and-drop onto the textview inevitably inserts a URL in the - # middle of another URL. No way to prevent that, but we can disable - # drag-and-drop in the textview altogether, and instead handle it - # from the dialogue window itself - textview.drag_dest_unset() - self.connect('destroy', self.close, textview) - - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Separator - grid.attach(Gtk.HSeparator(), 0, 3, 1, 1) - - # Display the parent channel/playlist in a combo (so the layout of this - # window is the same as that for AddVideoDialogue) - label2 = Gtk.Label() - grid.attach(label2, 0, 4, 1, 1) - - if isinstance(parent_obj, media.Channel): - label2.set_text(_('Insert the videos into this channel:')) - pixbuf = main_win_obj.pixbuf_dict['channel_small'] - elif isinstance(parent_obj, media.Playlist): - label2.set_text(_('Insert the videos into this playlist:')) - pixbuf = main_win_obj.pixbuf_dict['playlist_small'] - else: - label2.set_text(_('Insert the videos into this folder:')) - pixbuf = main_win_obj.pixbuf_dict['folder_small'] - - listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, str) - listmodel.append( [pixbuf, ' ' + self.parent_obj.name] ) - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 0, 5, 1, 1) - combo.set_hexpand(True) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, False) - combo.add_attribute(renderer_text, 'text', 1) - - combo.set_active(0) -# combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 6, 1, 1) - - self.checkbutton = Gtk.CheckButton() - grid.attach(self.checkbutton, 0, 7, 1, 1) - self.checkbutton.set_label(_('Enable automatic copy/paste')) - self.checkbutton.connect('toggled', self.on_checkbutton_toggled) - - # Paste in the contents of the clipboard (if it contains valid URLs) - if main_win_obj.app_obj.dialogue_copy_clipboard_flag: - utils.add_links_to_textview_from_clipboard( - main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - ) - - # Display the dialogue window - self.show_all() - - - def close(self, also_self, textview): - - """Called from callback in self.__init__(). - - We have disabled drag-and-drop directly into the textview, but this - generates a Gtk warning when the textview is destroyed. - - Workaround is to re-enable drag-and-drop into the textview immediately - before it (and its window) are destroyed. - - Args: - - also_self (mainwin.InsertVideoDialogue): Another copy of this - window - - textview (Gtk.TextView): The textview for which drag-and-drop has - been disabled - - """ - - textview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - - - # Callback class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables clipboard monitoring. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active() \ - and self.clipboard_timer_id is not None: - - # Stop the timer - GObject.source_remove(self.clipboard_timer_id) - self.clipboard_timer_id = None - - elif checkbutton.get_active() and self.clipboard_timer_id is None: - - # Start the timer - self.clipboard_timer_id = GObject.timeout_add( - self.clipboard_timer_time, - self.clipboard_timer_callback, - ) - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - - def clipboard_timer_callback(self): - - """Called from a callback in self.on_checkbutton_toggled(). - - Periodically checks the system's clipboard, and adds any new URLs to - the dialogue window's textview. - """ - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - ) - - # Return 1 to keep the timer going - return 1 - - -class MountDriveDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.start(). - - Python class handling a dialogue window that asks the user what to do, - if the drive containing Tartube's data directory is not mounted or is - unwriteable. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - unwriteable_flag (bool): True if the data directory is unwriteable; - False if the data directory is missing altogether - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, unwriteable_flag=False): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Mount drive\' dialogue starts here.' \ - + ' Visible on startup, if Tartube\'s data folder does not' \ - + ' exist' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.combo = None # Gtk.ComboBox - self.radiobutton3 = None # Gtk.RadioButton - self.radiobutton4 = None # Gtk.RadioButton - self.radiobutton5 = None # Gtk.RadioButton - - - # IV list - other - # --------------- - # Flag set to True if the data directory specified by - # mainapp.TartubeApp.data_dir is now available - self.available_flag = False - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Mount drive'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid.set_column_spacing(main_win_obj.spacing_size) - # (Actually, the grid width of the area to the right of the Tartube - # logo) - grid_width = 2 - - image = Gtk.Image.new_from_pixbuf( - main_win_obj.pixbuf_dict['system_icon'], - ) - grid.attach(image, 0, 0, 1, 3) - - label = Gtk.Label( - _('The Tartube data folder is set to:'), - ) - grid.attach(label, 1, 0, grid_width, 1) - - label = Gtk.Label() - grid.attach(label, 1, 1, grid_width, 1) - label.set_markup( - '' \ - + utils.shorten_string(main_win_obj.app_obj.data_dir, 50) \ - + '', - ) - - if not unwriteable_flag: - label2 = Gtk.Label(_('...but this folder doesn\'t exist')) - else: - label2 = Gtk.Label( - _('...but Tartube cannot write to this folder'), - ) - - grid.attach(label2, 1, 2, grid_width, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 1, 3, grid_width, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('I have mounted the drive, please try again'), - ) - grid.attach(self.radiobutton, 1, 4, grid_width, 1) - - self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton, - _('Use this data folder:'), - ) - grid.attach(self.radiobutton2, 1, 5, grid_width, 1) - # (Signal connect appears below) - - store = Gtk.ListStore(str) - for item in self.main_win_obj.app_obj.data_dir_alt_list: - store.append([item]) - - self.combo = Gtk.ComboBox.new_with_model(store) - grid.attach(self.combo, 1, 6, grid_width, 1) - self.combo.set_hexpand(True) - renderer_text = Gtk.CellRendererText() - self.combo.pack_start(renderer_text, True) - self.combo.add_attribute(renderer_text, 'text', 0) - self.combo.set_entry_text_column(0) - self.combo.set_active(0) - self.combo.set_sensitive(False) - - # (Signal connect from above) - self.radiobutton2.connect( - 'toggled', - self.on_radiobutton_toggled, - ) - - self.radiobutton3 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton2, - _('Select a different data folder'), - ) - grid.attach(self.radiobutton3, 1, 7, grid_width, 1) - - self.radiobutton4 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton3, - _('Use the default data folder'), - ) - grid.attach(self.radiobutton4, 1, 8, grid_width, 1) - - self.radiobutton5 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton4, - _('Shut down Tartube'), - ) - grid.attach(self.radiobutton5, 1, 9, grid_width, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 1, 10, grid_width, 1) - - button = Gtk.Button.new_with_label(_('Cancel')) - grid.attach(button, 1, 11, 1, 1) - button.connect('clicked', self.on_cancel_button_clicked) - - button2 = Gtk.Button.new_with_label(_('OK')) - grid.attach(button2, 2, 11, 1, 1) - button2.connect('clicked', self.on_ok_button_clicked) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def do_try_again(self): - - """Called by self.on_ok_button_clicked(). - - The user has selected 'I have mounted the drive, please try again'. - """ - - app_obj = self.main_win_obj.app_obj - - if os.path.exists(app_obj.data_dir): - - # Data directory exists - self.available_flag = True - self.destroy() - - else: - - # Data directory still does not exist. Inform the user - mini_win = app_obj.dialogue_manager_obj.show_msg_dialogue( - _( - 'The folder still doesn\'t exist. Please try a' \ - + ' different option', - ), - 'error', - 'ok', - self, # Parent window is this window - ) - - mini_win.set_modal(True) - - - def do_select_dir(self): - - """Called by self.on_ok_button_clicked(). - - The user has selected 'Select a different data directory'. - """ - - if self.main_win_obj.app_obj.prompt_user_for_data_dir(): - - # New data directory selected - self.available_flag = True - self.destroy() - - - # Callback class methods - - - def on_ok_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the OK button is clicked, perform the selected action. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if self.radiobutton.get_active(): - self.do_try_again() - - elif self.radiobutton2.get_active(): - - tree_iter = self.combo.get_active_iter() - model = self.combo.get_model() - path = model[tree_iter][0] - self.main_win_obj.app_obj.set_data_dir(path) - self.available_flag = True - self.destroy() - - elif self.radiobutton3.get_active(): - self.do_select_dir() - - elif self.radiobutton4.get_active(): - - self.main_win_obj.app_obj.reset_data_dir() - self.available_flag = True - self.destroy() - - elif self.radiobutton5.get_active(): - self.available_flag = False - self.destroy() - - - def on_cancel_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the Cancel button is clicked, shut down Tartube. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.available_flag = False - self.destroy() - - - def on_radiobutton_toggled(self, button): - - """Called from a callback in self.__init__(). - - When the radiobutton just above it is toggled, (de)sensitise the - combobox. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if button.get_active(): - self.combo.set_sensitive(True) - else: - self.combo.set_sensitive(False) - - -class MSYS2Dialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_open_msys2(). - - Python class handling a dialogue window that advises users how the MINGW - terminal is used. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'MSYS2 terminal\' dialogue starts here.' \ - + ' In the main window menu, click System > Open MSYS2 terminal' \ - + ' (MS Windows only)' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - # (none) - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('MSYS2 terminal'), - None, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - # Without this line, this dialogue window is partially obscured beneath - # the MSYS2 window (the exact opposite of what we want) - self.set_keep_above(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label() - grid.attach(label, 0, 0, 1, 1) - label.set_markup( - _('On MS Windows, Tartube runs in the MSYS2 environment.'), - ) - label.set_xalign(0) - - label2 = Gtk.Label() - grid.attach(label2, 0, 1, 1, 1) - label2.set_markup( - _( - 'Advanced users can use the MSYS2 terminal to make changes to' \ - + ' the\nenvironment (for example, to tweak youtube-dl or FFmpeg)', - ), - ) - label2.set_xalign(0) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 2, 1, 1) - - checkbutton = Gtk.CheckButton() - grid.attach(checkbutton, 0, 3, 1, 1) - checkbutton.set_label(_('Always show this window')) - if main_win_obj.app_obj.show_msys2_dialogue_flag: - checkbutton.set_active(True) - checkbutton.connect('toggled', self.on_checkbutton_toggled) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables showing this dialogue window. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if checkbutton.get_active(): - self.main_win_obj.app_obj.set_show_msys2_dialogue_flag(True) - else: - self.main_win_obj.app_obj.set_show_msys2_dialogue_flag(False) - - -class NewbieDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.download_manager_finished(). - - Python class handling a dialogue window that advises a newbie what to do if - the download operation failed to check/download any videos. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - classic_mode_flag (bool): True if the download operation was launched - from the Classic Mode tab, False otherwise - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, classic_mode_flag): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Download failure dialogue starts here.' \ - + ' Visible when the user tries to check/download videos, but' \ - + ' no videos are checked/downloaded' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - # Flag set to True when various widgets are selected - self.update_flag = False - self.config_flag = False - self.change_flag = False - self.website_flag = False - self.issues_flag = False - self.show_flag = main_win_obj.app_obj.show_newbie_dialogue_flag - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Downloads'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid.set_column_spacing(main_win_obj.spacing_size) - # (Actually, the grid width of the area to the right of the Tartube - # logo) - grid_width = 2 - - image = Gtk.Image.new_from_pixbuf( - main_win_obj.pixbuf_dict['newbie_icon'], - ) - grid.attach(image, 0, 0, 1, 4) - - label = Gtk.Label() - grid.attach(label, 1, 0, grid_width, 1) - label.set_markup( - '' + _('Nothing happened?') + '', - ) - - # Separator - grid.attach(Gtk.HSeparator(), 1, 1, grid_width, 1) - - if not classic_mode_flag: - extra_rows = 0 - - else: - extra_rows = 3 - -# label2 = Gtk.Label( -# _('Check the video/audio file actually exists'), -# ) - label2 = Gtk.Label( - _('Check the requested format is actually available'), - ) - grid.attach(label2, 1, 2, grid_width, 1) - - label3 = Gtk.Label( - _('(Try converting instead of a direct download)'), - ) - grid.attach(label3, 1, 3, grid_width, 1) - - frame = Gtk.Frame() - grid.attach(frame, 1, 4, grid_width, 1) - - image2 = Gtk.Image.new_from_pixbuf( - main_win_obj.pixbuf_dict['newbie_classic_icon'], - ) - frame.add(image2) - - label4 = Gtk.Label( - ' ' + _('Check the downloader is installed and updated') + ' ', - ) - grid.attach(label4, 1, (extra_rows + 2), grid_width, 1) - - button = Gtk.Button.new_with_label( - _('Update') + ' ' + self.main_win_obj.app_obj.get_downloader(), - ) - grid.attach(button, 1, (extra_rows + 3), grid_width, 1) - button.connect('clicked', self.on_update_button_clicked) - - label5 = Gtk.Label( - _('Tell Tartube where to find the downloader'), - ) - grid.attach(label5, 1, (extra_rows + 4), grid_width, 1) - - button2 = Gtk.Button.new_with_label( - _('Set the downloader\'s file path'), - ) - grid.attach(button2, 1, (extra_rows + 5), grid_width, 1) - button2.connect('clicked', self.on_config_button_clicked) - - button3 = Gtk.Button.new_with_label( - _('Try a different downloader'), - ) - grid.attach(button3, 1, (extra_rows + 6), grid_width, 1) - button3.connect('clicked', self.on_change_button_clicked) - - label6 = Gtk.Label( - _('Find more help'), - ) - grid.attach(label6, 1, (extra_rows + 7), grid_width, 1) - - button4 = Gtk.Button.new_with_label( - _('Read the FAQ'), - ) - grid.attach(button4, 1, (extra_rows + 8), 1, 1) - button4.connect('clicked', self.on_website_button_clicked) - - button5 = Gtk.Button.new_with_label( - _('Ask for help'), - ) - grid.attach(button5, 2, (extra_rows + 8), 1, 1) - button5.connect('clicked', self.on_issues_button_clicked) - - # Separator - grid.attach(Gtk.HSeparator(), 1, (extra_rows + 9), grid_width, 1) - - label7 = Gtk.Label() - grid.attach(label7, 1, (extra_rows + 10), grid_width, 1) - label7.set_markup( - _( - 'Don\'t forget to check the Output tab and the\n' \ - 'Errors/Warnings tab for error messages!', - ), - ) - - # Separator - grid.attach(Gtk.HSeparator(), 1, (extra_rows + 11), grid_width, 1) - - checkbutton = Gtk.CheckButton() - grid.attach(checkbutton, 1, (extra_rows + 12), grid_width, 1) - checkbutton.set_label(_('Always show this window')) - if self.show_flag: - checkbutton.set_active(True) - checkbutton.connect('toggled', self.on_checkbutton_toggled) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_change_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the button is clicked, open the preferences window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.change_flag = True - self.destroy() - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables showing this dialogue window. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if checkbutton.get_active(): - self.show_flag = True - else: - self.show_flag = False - - - def on_config_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the button is clicked, open the preferences window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.config_flag = True - self.destroy() - - - def on_issues_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the button is clicked, open the Tartube issues page. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.issues_flag = True - self.destroy() - - - def on_update_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the button is clicked, perform an update operation. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.update_flag = True - self.destroy() - - - def on_website_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the button is clicked, open the Tartube website. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.website_flag = True - self.destroy() - - -class OutputOverrideDialogue(Gtk.Dialog): - - """Called by mainwin.on_video_catalogue_output_override(). - - Python class handling a dialogue window to prompt the user for a video - name. If specified, the name is used to override youtube-dl's output - template while downloading the video. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - video_obj (media.Video): The video to be renamed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Download with name\' and \'Re-download' \ - + ' with name\' dialogue starts here. Right-click a video and' \ - + ' select Special > Download with name...', - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - # The video to be renamed - self.video_obj = video_obj - - - # IV list - Gtk widgets - # --------------------- - # (none) - - - # IV list - other - # --------------- - # The new name specified by the user, or None if no name has been - # specified, or if the specified name matches the existing name - self.new_name = None - - - # Code - # ---- - - if not video_obj.dl_flag: - title = _('Download with name') - else: - title = _('Re-download with name') - - Gtk.Dialog.__init__( - self, - title, - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label( - _('Override the output template with this name'), - ) - grid.attach(label, 0, 0, 1, 1) - - entry = Gtk.Entry() - grid.attach(entry, 0, 1, 1, 1) - entry.connect('changed', self.on_entry_changed) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_entry_changed(self, entry): - - """Called from callback in self.__init__(). - - Updates IVs. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - text = entry.get_text() - if text == '' or text == self.video_obj.name: - self.new_name = None - else: - self.new_name = text - - -class PrepareClipDialogue(Gtk.Dialog): - - """Called by mainwin.MainWin.on_video_catalogue_process_clip(). - - Prompt the user to download or create video clips. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - video_obj (media.Video): The video from which video clips will be - downloaded or extracted - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj=None): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Create video clip\' dialogue starts' \ - + ' here. In the Videos tab, right-click a video and select' \ - + ' Special > Create video clips...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - # The media.Video to be used (may be None when called from the Classic - # Mode tab) - self.video_obj = video_obj - - - # IV list - Gtk widgets - # --------------------- - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.radiobutton3 = None # Gtk.RadioButton - self.label5 = None # Gtk.Label - self.button = None # Gtk.Button - self.label9 = None # Gtk.Label - self.timestamp_liststore = None # Gtk.ListStore - self.button10 = None # Gtk.Button - self.button11 = None # Gtk.Button - self.button12 = None # Gtk.Button - - - # IV list - other - # --------------- - # Store the user's choices as IVs, so the calling function can - # retrieve them - - # Clip mode: 'downloader' to download clips using the downloader - # directly (currently only available for yt-dlp), 'ffmpeg' to - # download clips using FFmpeg, or 'create' to create clips using the - # already-downloaded video - self.clip_mode = self.main_win_obj.app_obj.video_timestamps_dl_mode - # Download mode: 'chapters' to download chapters using yt-dlp directly - # (when available), 'single' to download/extract a single clip using - # specified timestamp(s), 'multiple' to download/extract multiple - # clips. Set to None when self.clip_mode is 'create' - self.dl_mode = None - # When self.dl_mode is 'chapters', an optional regex to apply. If not - # specified, remains set to None - self.optional_regex = None - # When self.dl_mode is 'single', the start/stop timestamps and the clip - # title. The start timestamp is compulsory, the others are optional, - # but timestamps must be in a valid format where specified) - self.start_stamp = None - self.stop_stamp = None - self.clip_title = None - # When self.dl_mode is 'multiple', a list of timestamps (in groups of - # 3), used to set mainapp.TartubeApp.temp_stamp_buffer_dict - self.stamp_list = [] - - # When called from the Classic Mode tab, and self.video_obj is None, - # an alternative timestamp list, in the same format as - # media.Video.stamp_list. Used in the dummy media.Video object, when - # it is created - self.classic_stamp_list = [] - # The URL entered, when the box is visible (set to None rather than an - # empty string) - self.classic_url = None - # Optional video name; if set, can be used with all clips (depending on - # the value of mainapp.TartubeApp.split_video_name_mode) - self.classic_video_name = None - - # Flag set to true when one of the 'Download' buttons is clicked, - # which closes the window (in the absence of an OK button) - self.start_dl_flag = False - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Create video clips'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - ) - ) - - self.set_modal(True) - app_obj = self.main_win_obj.app_obj - - # Set up the dialogue window - self.set_default_size( - app_obj.config_win_width, - -1, - ) - - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - grid_width = 2 - h_offset = 0 - - if self.video_obj is None: - - label = Gtk.Label() - grid.attach(label, 0, 0, grid_width, 1) - label.set_markup('' + _('Enter the video\'s URL:') + '') - label.set_alignment(0, 0.5) - - entry = Gtk.Entry() - grid.attach(entry, 0, 1, grid_width, 1) - entry.connect('changed', self.on_url_entry_changed) - - if app_obj.split_video_name_mode == 'num' \ - or app_obj.split_video_name_mode == 'clip' \ - or app_obj.split_video_name_mode == 'num_clip' \ - or app_obj.split_video_name_mode == 'clip_num': - - h_offset = 3 - - # Separator - grid.attach(Gtk.HSeparator(), 0, 2, grid_width, 1) - - else: - - h_offset = 5 - - label2 = Gtk.Label() - grid.attach(label2, 0, 2, grid_width, 1) - label2.set_markup( - _('Optional video name (will be added to all clips)'), - ) - label2.set_alignment(0, 0.5) - - entry2 = Gtk.Entry() - grid.attach(entry2, 0, 3, grid_width, 1) - entry2.connect('changed', self.on_optional_entry_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 4, grid_width, 1) - - label3 = Gtk.Label() - grid.attach(label3, 0, (0 + h_offset), grid_width, 1) - label3.set_markup('' + _('Select a method:') + '') - label3.set_alignment(0, 0.5) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Download new clips using yt-dlp'), - ) - grid.attach(self.radiobutton, 0, (1 + h_offset), 1, 1) - # (Signal connect appears below) - if app_obj.ytdl_fork != 'yt-dlp': - self.radiobutton.set_sensitive(False) - - self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) - grid.attach(self.radiobutton2, 0, (2 + h_offset), 1, 1) - # (Signal connect appears below) - self.radiobutton2.set_label( - _('Download new clips using FFmpeg'), - ) - if self.clip_mode == 'ffmpeg': - self.radiobutton2.set_active(True) - - self.radiobutton3 = Gtk.RadioButton.new_from_widget(self.radiobutton2) - grid.attach(self.radiobutton3, 1, (1 + h_offset), 1, 1) - # (Signal connect appears below) - self.radiobutton3.set_label( - _('Create clips from the downloaded video'), - ) - if self.clip_mode == 'create': - self.radiobutton3.set_active(True) - - if self.video_obj is None or not self.video_obj.dl_flag: - self.radiobutton3.set_sensitive(False) - - else: - label4 = Gtk.Label() - grid.attach(label4, 1, (2 + h_offset), 1, 1) - label4.set_markup( - '' + _( - 'Warning: Downloading new clips is usually MUCH quicker!', - ) + '', - ) - label4.set_alignment(0, 0.5) - - # Separator - grid.attach(Gtk.HSeparator(), 0, (3 + h_offset), grid_width, 1) - - # (Additional grid, to avoid messing up the format of the previous one) - grid2 = Gtk.Grid() - grid.attach(grid2, 0, (4 + h_offset), grid_width, 1) - grid2.set_column_spacing(main_win_obj.spacing_size) - grid2.set_row_spacing(main_win_obj.spacing_size) - - grid2_width = 4 - - self.label5 = Gtk.Label() - grid2.attach(self.label5, 0, 0, grid2_width, 1) - if self.clip_mode == 'create': - self.label5.set_markup( - '' + _('Create one clip') + ':', - ) - else: - self.label5.set_markup( - '' + _('Download one clip') + ':', - ) - self.label5.set_alignment(0, 0.5) - - label6 = Gtk.Label() - grid2.attach(label6, 0, 1, 1, 1) - label6.set_markup(_('Start timestamp (e.g. 15:29)')) - label6.set_alignment(0, 0.5) - - entry_width = 12 - - entry3 = Gtk.Entry() - grid2.attach(entry3, 1, 1, 1, 1) - # (Signal connect appears below) - entry3.set_width_chars(entry_width) - - label7 = Gtk.Label() - grid2.attach(label7, 2, 1, 1, 1) - label7.set_markup(_('Stop timestamp (optional)')) - label7.set_alignment(0, 0.5) - - entry4 = Gtk.Entry() - grid2.attach(entry4, 3, 1, 1, 1) - entry4.set_width_chars(entry_width) - - label8 = Gtk.Label() - grid2.attach(label8, 0, 2, 1, 1) - label8.set_markup(_('Clip title (optional)')) - label8.set_alignment(0, 0.5) - - entry5 = Gtk.Entry() - grid2.attach(entry5, 1, 2, 2, 1) - - self.button = Gtk.Button.new_with_label('') - grid2.attach(self.button, 3, 2, 1, 1) - # (Signal connect appears below) - if self.clip_mode == 'create': - self.set_bold_button_text(self.button, _('Create clip')) - else: - self.set_bold_button_text(self.button, _('Download clip')) - self.button.set_hexpand(True) - self.button.set_sensitive(False) - - # Separator - grid.attach(Gtk.HSeparator(), 0, (5 + h_offset), grid_width, 1) - - self.label9 = Gtk.Label() - grid.attach(self.label9, 0, (6 + h_offset), grid_width, 1) - if self.clip_mode == 'create': - self.label9.set_markup( - '' + _('Create multiple clips') + ':', - ) - else: - self.label9.set_markup( - '' + _('Download multiple clips') + ':', - ) - self.label9.set_alignment(0, 0.5) - - frame = Gtk.Frame() - grid.attach(frame, 0, (7 + h_offset), grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_hexpand(True) - scrolled.set_min_content_height(150) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate( - [ _('Marked'), _('Start'), _('Stop'), _('Clip title') ], - ): - if i == 0: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - renderer_toggle.set_sensitive(True) - renderer_toggle.set_activatable(True) - renderer_toggle.connect( - 'toggled', - self.on_treeview_button_toggled, - ) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.timestamp_liststore = Gtk.ListStore(bool, str, str, str) - treeview.set_model(self.timestamp_liststore) - - # Initialise the list - self.update_treeview() - - # (Additional grid, to avoid messing up the format of the previous one) - grid3 = Gtk.Grid() - grid.attach(grid3, 0, (8 + h_offset), grid_width, 1) - grid3.set_column_spacing(main_win_obj.spacing_size) - grid3.set_row_spacing(main_win_obj.spacing_size) - - grid3_width = 4 - - button2 = Gtk.Button.new_with_label(_('Add timestamp from above')) - grid3.attach(button2, 0, 0, 1, 1) - # (Signal connect appears below) - button2.set_hexpand(True) - - button3 = Gtk.Button.new_with_label(_('Delete timestamp')) - grid3.attach(button3, 1, 0, 1, 1) - # (Signal connect appears below) - button3.set_hexpand(True) - - button4 = Gtk.Button.new_with_label(_('Clip preferences')) - grid3.attach(button4, 2, 0, 1, 1) - # (Signal connect appears below) - button4.set_hexpand(True) - - button5 = Gtk.Button.new_with_label(_('Clear list')) - grid3.attach(button5, 3, 0, 1, 1) - # (Signal connect appears below) - button5.set_hexpand(True) - - # (Additional grid, to avoid messing up the format of the previous one) - grid4 = Gtk.Grid() - grid.attach(grid4, 0, (9 + h_offset), grid_width, 1) - grid4.set_column_spacing(main_win_obj.spacing_size) - grid4.set_row_spacing(main_win_obj.spacing_size) - - grid4_width = 4 - - button6 = Gtk.Button.new_with_label(_('Select all')) - grid4.attach(button6, 0, 0, 1, 1) - # (Signal connect appears below) - button6.set_hexpand(True) - - button7 = Gtk.Button.new_with_label(_('Select none')) - grid4.attach(button7, 1, 0, 1, 1) - # (Signal connect appears below) - button7.set_hexpand(True) - - button8 = Gtk.Button.new_with_label(_('Reset list using copied text')) - grid4.attach(button8, 2, 0, 1, 1) - # (Signal connect appears below) - button8.set_hexpand(True) - - button9 = Gtk.Button.new_with_label( - _('Reset list using video description') - ) - grid4.attach(button9, 3, 0, 1, 1) - # (Signal connect appears below) - button9.set_hexpand(True) - if self.video_obj is None: - button9.set_sensitive(False) - - # (Additional grid, to avoid messing up the format of the previous one) - grid5 = Gtk.Grid() - grid.attach(grid5, 0, (10 + h_offset), grid_width, 1) - grid5.set_column_spacing(main_win_obj.spacing_size) - grid5.set_row_spacing(main_win_obj.spacing_size) - - grid5_width = 3 - - self.button10 = Gtk.Button.new_with_label('') - grid5.attach(self.button10, 0, 0, 1, 1) - # (Signal connect appears below) - if self.clip_mode == 'create': - self.set_bold_button_text(self.button10, _('Create marked clips')) - else: - self.set_bold_button_text( - self.button10, - _('Download marked clips'), - ) - self.button10.set_hexpand(True) - - self.button11 = Gtk.Button.new_with_label('') - grid5.attach(self.button11, 1, 0, 1, 1) - # (Signal connect appears below) - if self.clip_mode == 'create': - self.set_bold_button_text(self.button11, _('Create all clips')) - else: - self.set_bold_button_text(self.button11, _('Download all clips')) - self.button11.set_hexpand(True) - - self.button12 = Gtk.Button.new_with_label('') - grid5.attach(self.button12, 2, 0, 1, 1) - # (Signal connect appears below) - self.set_bold_button_text(self.button12, _('Download all chapters')) - self.button12.set_hexpand(True) - if self.clip_mode != 'downloader': - self.button12.set_sensitive(False) - - # (Signal connects from above) - self.radiobutton.connect('toggled', self.on_radiobutton_toggled) - self.radiobutton2.connect('toggled', self.on_radiobutton2_toggled) - self.radiobutton3.connect('toggled', self.on_radiobutton3_toggled) - - entry3.connect('changed', self.on_start_entry_changed) - self.button.connect( - 'clicked', - self.on_dl_single_button_clicked, - entry3, - entry4, - entry5, - ) - - button2.connect( - 'clicked', - self.on_add_stamp_button_clicked, - entry3, - entry4, - entry5, - ) - button3.connect( - 'clicked', - self.on_delete_stamp_button_clicked, - treeview, - ) - button4.connect('clicked', self.on_prefs_button_clicked) - button5.connect('clicked', self.on_clear_stamp_button_clicked) - button6.connect('clicked', self.on_select_all_button_clicked) - button7.connect('clicked', self.on_select_none_button_clicked) - - button8.connect('clicked', self.on_copy_stamp_button_clicked) - button9.connect('clicked', self.on_extract_stamp_button_clicked) - - self.button10.connect('clicked', self.on_dl_marked_button_clicked) - self.button11.connect('clicked', self.on_dl_all_button_clicked) - self.button12.connect('clicked', self.on_dl_chapters_button_clicked) - - # Display the dialogue window - self.show_all() - - - # Support functions - - - def set_bold_button_text(self, button, text): - - """Can be called by anything. - - Sets bold label text for a Gtk.Button. - - Args: - - button (Gtk.Button): The button to update - - text (str): The (translated) text, which does not need to contain - ... tags - - """ - - for child in button.get_children(): - child.set_label('' + text + '') - child.set_use_markup(True) - - - def update_treeview(self): - - """Called by self.__init__(). - - Fills or updates the treeview. Based on - config.VideoEditWin.setup_timestamps_tab_update_treeview(). - """ - - self.timestamp_liststore.clear() - - if self.video_obj is not None: - stamp_list = self.video_obj.stamp_list - else: - stamp_list = self.classic_stamp_list - - # Add each timestamp/title to the treeview, one row at a time - for mini_list in stamp_list: - - start_stamp = mini_list[0] - - if mini_list[1] is None: - stop_stamp = '' - else: - stop_stamp = mini_list[1] - - if mini_list[2] is None: - clip_title = '' - else: - clip_title = mini_list[2] - - self.timestamp_liststore.append( - [ False, start_stamp, stop_stamp, clip_title ], - ) - - - # Callback class methods - - - def on_add_stamp_button_clicked(self, button, entry, entry2, entry3): - - """Called from a callback in self.__init__(). - - Adds a new timestamp to treeview, optionally with a clip title. - - Code adapted from config.VideoEditWin.on_add_stamp_button_clicked(). - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2, entry3 (Gtk.Entry): Other widgets to modify - - """ - - app_obj = self.main_win_obj.app_obj - - start_stamp = utils.strip_whitespace(entry.get_text()) - stop_stamp = utils.strip_whitespace(entry2.get_text()) - clip_title = utils.strip_whitespace(entry3.get_text()) - - # (Values are stored as None, rather than empty strings) - if stop_stamp == '': - stop_stamp = None - - if clip_title == '': - clip_title = None - - # Do nothing if specified timestamps aren't valid ('stop_stamp' is - # optional) - regex = '^' + app_obj.timestamp_regex + '$' - if re.search(regex, start_stamp) \ - and (stop_stamp is None or re.search(regex, stop_stamp)) \ - and utils.timestamp_compare(app_obj, start_stamp, stop_stamp): - - # Add leading zeroes to the minutes and seconds components, so - # that .stamp_list gets sorted correctly (and doesn't look - # weird) - start_stamp = utils.timestamp_format(app_obj, start_stamp) - if stop_stamp is not None: - stop_stamp = utils.timestamp_format(app_obj, stop_stamp) - - # Timestamps stored in groups of three, in the form - # (start_stamp, stop_stamp, clip_title) - # If a group with the same 'start_stamp' timestamp already exists, - # don't replace it; allow duplicates (as the user may actually - # want that) - if self.video_obj is not None: - - stamp_list = self.video_obj.stamp_list - stamp_list.append([ start_stamp, stop_stamp, clip_title ]) - - # (The called function will sort the list) - self.video_obj.set_timestamps(stamp_list) - - else: - - self.classic_stamp_list.append( - [ start_stamp, stop_stamp, clip_title ], - ) - self.classic_stamp_list.sort() - - # (Show changes, and empty entry boxes. The 'stop' timestamp, if - # specified, becomes the 'start' timestamp for the next group) - self.update_treeview() - - if stop_stamp is None: - entry.set_text('') - else: - entry.set_text( - utils.timestamp_add_second(app_obj, stop_stamp), - ) - - entry2.set_text('') - entry3.set_text('') - - else: - -# app_obj.dialogue_manager_obj.show_msg_dialogue( -# _('Invalid timestamp(s)'), -# 'error', -# 'ok', -# self, # Parent window is this window -# ) - # The intended dialogue window doesn't behave well, so instead, - # display a message in the entry boxes - if not re.search(regex, start_stamp): - entry.set_text(_('INVALID')) - - if stop_stamp is not None and not re.search(regex, stop_stamp): - entry2.set_text(_('INVALID')) - - - def on_clear_stamp_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Empties the video's timestamp list. - - Code adapted from config.VideoEditWin.on_clear_stamp_button_clicked(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if self.video_obj is not None: - self.video_obj.reset_timestamps() - else: - self.classic_stamp_list = [] - - self.update_treeview() - - - def on_copy_stamp_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Updates the video's timestamp list using text the user has copied and - pasted into a dialogue window. - - Code adapted from config.VideoEditWin.on_copy_stamp_button_clicked(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Open the dialogue window - dialogue_win = AddStampDialogue( - self, - self.main_win_obj, - ) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window - if response == Gtk.ResponseType.OK: - - text = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - # (Do not modify the existing list of timestamps, if no text was - # added to the dialogue window) - if text != '': - - if self.video_obj is not None: - self.video_obj.extract_timestamps_from_descrip( - self.main_win_obj.app_obj, - text, - ) - - else: - self.classic_stamp_list \ - = utils.extract_timestamps_from_descrip( - self.main_win_obj.app_obj, - text, - ) - - self.update_treeview() - - # ...before destroying the dialogue window - dialogue_win.destroy() - - - def on_delete_stamp_button_clicked(self, button, treeview): - - """Called from a callback in self.__init__(). - - Deletes the selected timestamp(s) from the video's timestamp list. - - Code adapted from config.VideoEditWin.on_delete_stamp_button_clicked(). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the timestamp list - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - return - - start_stamp = model[this_iter][1] - stop_stamp = model[this_iter][2] - clip_title = model[this_iter][3] - - # Timestamps stored in groups of three, in the form - # (start_stamp, stop_stamp, clip_title) - # Walk the list, and delete the first matching group - if self.video_obj is not None: - stamp_list = self.video_obj.stamp_list - else: - stamp_list = self.classic_stamp_list - - mod_list = [] - match_flag = False - - for mini_list in stamp_list: - - if not match_flag \ - and mini_list[0] == start_stamp \ - and (mini_list[1] is None or mini_list[1] == stop_stamp) \ - and (mini_list[2] is None or mini_list[2] == clip_title): - match_flag = True # Delete this one - else: - mod_list.append(mini_list) - - if self.video_obj is not None: - # (The called function will sort the list) - self.video_obj.set_timestamps(mod_list) - else: - self.classic_stamp_list = mod_list - self.classic_stamp_list.sort() - - # (Show changes) - self.update_treeview() - - - def on_dl_all_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Downloads/creates all clips. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.dl_mode = 'multiple' - - if self.video_obj is not None: - self.stamp_list = self.video_obj.stamp_list - else: - self.stamp_list = self.classic_stamp_list - - if self.stamp_list: - self.start_dl_flag = True - self.close() - - - def on_dl_chapters_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Downloads clips as chapters using yt-dlp directly. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.dl_mode = 'chapters' - self.start_dl_flag = True - self.close() - - - def on_dl_marked_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Downloads/creates marked clips. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.dl_mode = 'multiple' - self.stamp_list = [] - - if self.video_obj is not None: - this_stamp_list = self.video_obj.stamp_list - else: - this_stamp_list = self.classic_stamp_list - - count = -1 - for path in range(0, len(self.timestamp_liststore)): - - count += 1 - if self.timestamp_liststore[path][0] is True: - - start_stamp, stop_stamp, clip_title \ - = utils.clip_extract_data(this_stamp_list, count) - - self.stamp_list.append([start_stamp, stop_stamp, clip_title]) - - if self.stamp_list: - self.start_dl_flag = True - self.close() - - - def on_dl_single_button_clicked(self, button, entry, entry2, entry3): - - """Called from a callback in self.__init__(). - - Downloads/creates a single clip. - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2, entry3 (Gtk.Entry): Widgets specifying timestamps - and clip titles - - """ - - self.dl_mode = 'single' - - # (First entry is compulsory, others are optional; but timestamps must - # be in a valid format where specified) - start_stamp = entry.get_text() - if start_stamp != '': - self.start_stamp = utils.strip_whitespace(start_stamp) - - stop_stamp = entry2.get_text() - if stop_stamp != '': - self.stop_stamp = utils.strip_whitespace(stop_stamp) - - clip_title = entry3.get_text() - if clip_title != '': - self.clip_title = utils.strip_whitespace(clip_title) - - if self.start_stamp is not None: - self.start_dl_flag = True - self.close() - - - def on_extract_stamp_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Updates the video's timestamp list from its description, then displays - that list in the treeview. - - Code adapted from - config.VideoEditWin.on_extract_stamp_button_clicked(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.video_obj.extract_timestamps_from_descrip( - self.main_win_obj.app_obj, - ) - - self.update_treeview() - - - def on_optional_entry_changed(self, entry): - - """Called from callback in self.__init__(). - - Updates IVs. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - text = entry.get_text() - if text == '': - self.classic_video_name = None - else: - self.classic_video_name = text - - - def on_prefs_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Opens the preferences window to show clip settings. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # N.B. Destroy this window before opening the preferences window, or - # the latter will be hidden behind the main window - # N.B. If we don't destroy this window first, the preferences window is - # non-responsive - self.destroy() - config.SystemPrefWin(self.main_win_obj.app_obj, 'clips') - - - def on_radiobutton_toggled(self, radiobutton): - - """Called from a callback in self.__init__(). - - Updates widgets in the window, according to the current mode. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.clip_mode = 'downloader' - - self.button12.set_sensitive(True) - - self.label5.set_markup('' + _('Download one clip') + ':') - self.set_bold_button_text(self.button, _('Download clip')) - - self.label9.set_markup( - '' + _('Download multiple clips') + ':', - ) - self.set_bold_button_text( - self.button10, - _('Download marked clips'), - ) - self.set_bold_button_text(self.button11, _('Download all clips')) - - - def on_radiobutton2_toggled(self, radiobutton): - - """Called from a callback in self.__init__(). - - Updates widgets in the window, according to the current mode. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.clip_mode = 'ffmpeg' - - self.button12.set_sensitive(False) - - self.label5.set_markup('' + _('Download one clip') + ':') - self.set_bold_button_text(self.button, _('Download clip')) - self.label9.set_markup( - '' + _('Download multiple clips') + ':', - ) - self.set_bold_button_text( - self.button10, - _('Download marked clips'), - ) - self.set_bold_button_text(self.button11, _('Download all clips')) - - - def on_radiobutton3_toggled(self, radiobutton): - - """Called from a callback in self.__init__(). - - Updates widgets in the window, according to the current mode. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.clip_mode = 'create' - self.dl_mode = None - - self.button12.set_sensitive(False) - - self.label5.set_markup('' + _('Create one clip') + ':') - self.set_bold_button_text(self.button, _('Create clip')) - self.label9.set_markup( - '' + _('Create multiple clips') + ':', - ) - self.set_bold_button_text(self.button10, _('Create marked clips')) - self.set_bold_button_text(self.button11, _('Create all clips')) - - - def on_select_all_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Selects the checkbutton in every line in the treeview. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.timestamp_liststore)): - self.timestamp_liststore[path][0] = True - - - def on_select_none_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - De-selects the checkbutton in every line in the treeview. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.timestamp_liststore)): - self.timestamp_liststore[path][0] = False - - - def on_start_entry_changed(self, entry): - - """Called from callback in self.__init__(). - - Sensitises the download button, depending on whether the entry contains - text, or not. - - Note that the validity of any timestamps the user specifies is checked - by the calling mainwin.MainWin code. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - text = entry.get_text() - if text == '': - self.button.set_sensitive(False) - else: - self.button.set_sensitive(True) - - - def on_treeview_button_toggled(self, renderer_toggle, tree_path): - - """Called from a callback in self.__init__(). - - Toggles one of the check buttons in the treeview. - - Args: - - renderer_toggle (Gtk.CellRendererToggle): The widget clicked - - tree_path (Gtk.TreePath): Path to the clicked row - - """ - - self.timestamp_liststore[tree_path][0] \ - = not self.timestamp_liststore[tree_path][0] - - - def on_url_entry_changed(self, entry): - - """Called from callback in self.__init__(). - - Updates IVs. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - text = entry.get_text() - if text == '': - self.classic_url = None - else: - self.classic_url = text - - -class PrepareSliceDialogue(Gtk.Dialog): - - """Called by mainwin.MainWin.on_video_catalogue_process_clip(). - - Prompt the user to remove video slices. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - video_obj (media.Video): The video from which video slices will be - removed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, video_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Remove video slices\' dialogue starts' \ - + ' here. In the Videos tab, right-click a video and select' \ - + ' Special > Remove video slices...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - # The media.Video to be used - self.video_obj = video_obj - - - # IV list - Gtk widgets - # --------------------- - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.label3 = None # Gtk.Label - self.button = None # Gtk.Button - self.label6 = None # Gtk.Label - self.slice_liststore = None # Gtk.ListStore - self.button9 = None # Gtk.Button - self.button10 = None # Gtk.Button - - - # IV list - other - # --------------- - # Store the user's choice as an IV, so the calling function can - # retrieve it - # Slice mode: 'ffmpeg' to remove slices using FFmpeg, or 'create' to - # remove slices using the already-downloaded video - # (These values match those used in mainwin.PrepareClipDialogue) - self.slice_mode = 'ffmpeg' - # Download mode: 'single' to remove a single slice using specified - # timestamps/seconds, 'multiple' to remove multiple slices. Set to - # None when self.clip_mode is 'create' - # (These values match those used in mainwin.PrepareClipDialogue) - self.dl_mode = None - # When self.dl_mode is 'single', the start/stop times (timestamps or - # seconds). The start time is compulsory, the stop time is optional, - # but timestamps must be in a valid format where specified) - self.start_time = None - self.stop_time = None - # When self.dl_mode is 'multiple', a list of dictionaries in the form - # described by self.on_add_slice_button_clicked(), each representing - # a single slice. The list is used to set - # mainapp.TartubeApp.temp_slice_buffer_dict - self.slice_list = [] - - # Flag set to true when one of the 'Download' buttons is clicked, - # which closes the window (in the absence of an OK button) - self.start_dl_flag = False - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Remove video slices'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - ) - ) - - self.set_modal(True) - app_obj = self.main_win_obj.app_obj - - # Set up the dialogue window - self.set_default_size( - app_obj.config_win_width, - -1, - ) - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - grid_width = 2 - - label = Gtk.Label() - grid.attach(label, 0, 0, grid_width, 1) - label.set_markup('' + _('Select a method:') + '') - label.set_alignment(0, 0.5) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Download new sliced video'), - ) - grid.attach(self.radiobutton, 0, 1, 1, 1) - - self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) - grid.attach(self.radiobutton2, 1, 1, 1, 1) - # (Signal connect appears below) - self.radiobutton2.set_label( - _('Remove slices from the downloaded video'), - ) - if not self.video_obj.dl_flag: - self.radiobutton2.set_sensitive(False) - - else: - label2 = Gtk.Label() - grid.attach(label2, 0, 2, grid_width, 1) - label2.set_markup( - '' + _( - 'Warning: Downloading new sliced videos is usually MUCH' \ - + ' quicker!', - ) + '', - ) - label2.set_alignment(0, 0.5) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 3, grid_width, 1) - - # (Additional grid, to avoid messing up the format of the previous one) - grid2 = Gtk.Grid() - grid.attach(grid2, 0, 4, grid_width, 1) - grid2.set_column_spacing(main_win_obj.spacing_size) - grid2.set_row_spacing(main_win_obj.spacing_size) - - grid2_width = 4 - - self.label3 = Gtk.Label() - grid2.attach(self.label3, 0, 0, grid2_width, 1) - if self.slice_mode == 'create': - self.label3.set_markup( - '' + _('Create video with one slice') + ':(', - ) - else: - self.label3.set_markup( - '' + _('Download video with one slice') + ':', - ) - self.label3.set_alignment(0, 0.5) - - label4 = Gtk.Label() - grid2.attach(label4, 0, 1, 1, 1) - label4.set_markup(_('Start (timestamp or seconds)')) - label4.set_alignment(0, 0.5) - - entry_width = 12 - - entry = Gtk.Entry() - grid2.attach(entry, 1, 1, 1, 1) - # (Signal connect appears below) - entry.set_width_chars(entry_width) - - label5 = Gtk.Label() - grid2.attach(label5, 2, 1, 1, 1) - label5.set_markup(_('Stop (optional)')) - label5.set_alignment(0, 0.5) - - entry2 = Gtk.Entry() - grid2.attach(entry2, 3, 1, 1, 1) - entry2.set_width_chars(entry_width) - - self.button = Gtk.Button.new_with_label('') - grid2.attach(self.button, 2, 2, 2, 1) - # (Signal connect appears below) - if self.slice_mode == 'create': - self.set_bold_button_text(self.button, _('Create sliced video')) - else: - self.set_bold_button_text(self.button, _('Download sliced video')) - self.button.set_hexpand(True) - self.button.set_sensitive(False) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 5, grid_width, 1) - - self.label6 = Gtk.Label() - grid.attach(self.label6, 0, 6, grid_width, 1) - if self.slice_mode == 'create': - self.label6.set_markup( - '' + _('Create video with multiple slices') + ':', - ) - else: - self.label6.set_markup( - '' + _('Download video with multiple slices') + ':', - ) - self.label6.set_alignment(0, 0.5) - - frame = Gtk.Frame() - grid.attach(frame, 0, 7, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_hexpand(True) - scrolled.set_min_content_height(150) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(True) - - for i, column_title in enumerate([ - _('Marked'), - _('Category'), - _('Action type'), - _('Start'), - _('Stop'), - ]): - if i == 0: - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - renderer_toggle.set_sensitive(True) - renderer_toggle.set_activatable(True) - renderer_toggle.connect( - 'toggled', - self.on_treeview_button_toggled, - ) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - column_text.set_resizable(True) - - self.slice_liststore = Gtk.ListStore(bool, str, str, str, str) - treeview.set_model(self.slice_liststore) - - # Initialise the list - self.update_treeview() - - # (Additional grid, to avoid messing up the format of the previous one) - grid3 = Gtk.Grid() - grid.attach(grid3, 0, 8, grid_width, 1) - grid3.set_column_spacing(main_win_obj.spacing_size) - grid3.set_row_spacing(main_win_obj.spacing_size) - - grid3_width = 4 - - label7 = Gtk.Label() - grid3.attach(label7, 0, 0, 1, 1) - label7.set_markup(_('Category')) - label7.set_alignment(0, 0.5) - - combostore = Gtk.ListStore(str) - for category in formats.SPONSORBLOCK_CATEGORY_LIST: - combostore.append( [category] ) - - combo = Gtk.ComboBox.new_with_model(combostore) - grid3.attach(combo, 1, 0, 1, 1) - combo.set_hexpand(True) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_active(0) - - label8 = Gtk.Label() - grid3.attach(label8, 2, 0, 1, 1) - label8.set_markup(_('Action type')) - label8.set_alignment(0, 0.5) - - combostore2 = Gtk.ListStore(str) - for action in formats.SPONSORBLOCK_ACTION_LIST: - combostore2.append( [action] ) - - combo2 = Gtk.ComboBox.new_with_model(combostore2) - grid3.attach(combo2, 3, 0, 1, 1) - combo2.set_hexpand(True) - - renderer_text = Gtk.CellRendererText() - combo2.pack_start(renderer_text, True) - combo2.add_attribute(renderer_text, 'text', 0) - combo2.set_active(0) - - label9 = Gtk.Label() - grid3.attach(label9, 0, 1, 1, 1) - label9.set_markup(_('Start (timestamp or seconds)')) - label9.set_alignment(0, 0.5) - - entry3 = Gtk.Entry() - grid3.attach(entry3, 1, 1, 1, 1) - # (Signal connect appears below) - entry3.set_width_chars(entry_width) - - label10 = Gtk.Label() - grid3.attach(label10, 2, 1, 1, 1) - label10.set_markup(_('Stop (optional)')) - label10.set_alignment(0, 0.5) - - entry4 = Gtk.Entry() - grid3.attach(entry4, 3, 1, 1, 1) - # (Signal connect appears below) - entry4.set_width_chars(entry_width) - - # (Additional grid, to avoid messing up the format of the previous one) - grid4 = Gtk.Grid() - grid.attach(grid4, 0, 9, grid_width, 1) - grid4.set_column_spacing(main_win_obj.spacing_size) - grid4.set_row_spacing(main_win_obj.spacing_size) - - grid4_width = 4 - - button2 = Gtk.Button.new_with_label(_('Add slice')) - grid4.attach(button2, 0, 0, 1, 1) - # (Signal connect appears below) - button2.set_hexpand(True) - - button3 = Gtk.Button.new_with_label(_('Delete slice')) - grid4.attach(button3, 1, 0, 1, 1) - # (Signal connect appears below) - button3.set_hexpand(True) - - button4 = Gtk.Button.new_with_label(_('SponsorBlock settings')) - grid4.attach(button4, 2, 0, 1, 1) - # (Signal connect appears below) - button4.set_hexpand(True) - - button5 = Gtk.Button.new_with_label(_('Clear list')) - grid4.attach(button5, 3, 0, 1, 1) - # (Signal connect appears below) - button5.set_hexpand(True) - - # (Additional grid, to avoid messing up the format of the previous one) - grid5 = Gtk.Grid() - grid.attach(grid5, 0, 10, grid_width, 1) - grid5.set_column_spacing(main_win_obj.spacing_size) - grid5.set_row_spacing(main_win_obj.spacing_size) - - grid5_width = 4 - - button6 = Gtk.Button.new_with_label(_('Select all')) - grid5.attach(button6, 0, 0, 1, 1) - # (Signal connect appears below) - button6.set_hexpand(True) - - button7 = Gtk.Button.new_with_label(_('Select none')) - grid5.attach(button7, 1, 0, 1, 1) - # (Signal connect appears below) - button7.set_hexpand(True) - - button8 = Gtk.Button.new_with_label( - _('Contact SponsorBlock to reset list'), - ) - grid5.attach(button8, 2, 0, 2, 1) - # (Signal connect appears below) - button8.set_hexpand(True) - if app_obj.custom_sblock_mirror == '' or self.video_obj.vid is None: - button8.set_sensitive(False) - - self.button9 = Gtk.Button.new_with_label('') - grid5.attach(self.button9, 0, 1, 2, 1) - # (Signal connect appears below) - if self.slice_mode == 'create': - self.set_bold_button_text( - self.button9, - _('Create video, removing marked slices'), - ) - else: - self.set_bold_button_text( - self.button9, - _('Download video, removing marked slices'), - ) - self.button9.set_hexpand(True) - - self.button10 = Gtk.Button.new_with_label('') - grid5.attach(self.button10, 2, 1, 1, 1) - # (Signal connect appears below) - if self.slice_mode == 'create': - self.set_bold_button_text( - self.button10, - _('Create video, removing all slices'), - ) - else: - self.set_bold_button_text( - self.button10, - _('Download video, removing all slices'), - ) - self.button10.set_hexpand(True) - - # (Signal connects from above) - entry.connect('changed', self.on_start_entry_changed) - self.button.connect( - 'clicked', - self.on_dl_single_button_clicked, - entry, - entry2, - ) - - self.radiobutton.connect('toggled', self.on_radiobutton_toggled) - self.radiobutton2.connect('toggled', self.on_radiobutton2_toggled) - - button2.connect( - 'clicked', - self.on_add_slice_button_clicked, - combo, - combo2, - entry3, - entry4, - ) - button3.connect( - 'clicked', - self.on_delete_slice_button_clicked, - treeview, - ) - button4.connect('clicked', self.on_prefs_button_clicked) - button5.connect('clicked', self.on_clear_slice_button_clicked) - button6.connect('clicked', self.on_select_all_button_clicked) - button7.connect('clicked', self.on_select_none_button_clicked) - button8.connect('clicked', self.on_contact_sblock_clicked) - - self.button9.connect('clicked', self.on_dl_marked_button_clicked) - self.button10.connect('clicked', self.on_dl_all_button_clicked) - - # Display the dialogue window - self.show_all() - - - # Support functions - - - def set_bold_button_text(self, button, text): - - """Can be called by anything. - - Sets bold label text for a Gtk.Button. - - Args: - - button (Gtk.Button): The button to update - - text (str): The (translated) text, which does not need to contain - ... tags - - """ - - for child in button.get_children(): - child.set_label('' + text + '') - child.set_use_markup(True) - - - def update_treeview(self): - - """Called by self.__init__(). - - Fills or updates the treeview. Based on - config.VideoEditWin.setup_slices_tab_update_treeview(). - """ - - self.slice_liststore.clear() - - # Add each timestamp/title to the treeview, one row at a time - for mini_dict in self.video_obj.slice_list: - - if 'category' in mini_dict: - category = mini_dict['category'] - else: - category = 'n/a' - - if 'action' in mini_dict: - action = mini_dict['action'] - else: - action = 'n/a' - - if 'start_time' in mini_dict: - start_time = mini_dict['start_time'] - else: - start_time = 'n/a' - - if 'stop_time' in mini_dict \ - and mini_dict['stop_time'] is not None: - stop_time = mini_dict['stop_time'] - else: - stop_time = 'n/a' - - self.slice_liststore.append( - [ False, category, action, str(start_time), str(stop_time) ], - ) - - - # Callback class methods - - - def on_add_slice_button_clicked(self, button, combo, combo2, entry, - entry2): - - """Called from a callback in self.__init__(). - - Adds a new slice to the treeview. - - Code adapted from config.VideoEditWin.on_add_slice_button_clicked(). - - Args: - - button (Gtk.Button): The widget clicked - - combo, combo2 (Gtk.Entry): Other widgets to modify - - entry, entry2 (Gtk.Entry): Other widgets to modify - - """ - - app_obj = self.main_win_obj.app_obj - - tree_iter = combo.get_active_iter() - model = combo.get_model() - category = model[tree_iter][0] - - tree_iter2 = combo2.get_active_iter() - model2 = combo2.get_model() - action_type = model2[tree_iter2][0] - - start_time = utils.strip_whitespace(entry.get_text()) - stop_time = utils.strip_whitespace(entry2.get_text()) - - start_time = float( - utils.timestamp_convert_to_seconds(app_obj, start_time), - ) - - if stop_time == '': - stop_time = None - else: - stop_time = float( - utils.timestamp_convert_to_seconds(app_obj, stop_time), - ) - - # Do nothing if specified timestamps aren't valid - try: - ignore = float(start_time) - if stop_time is not None: - ignore = float(stop_time) - - except: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid start/stop times'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - if stop_time is not None and stop_time <= start_time: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Invalid start/stop times'), - 'error', - 'ok', - self, # Parent window is this window - ) - - return - - # Compile the mini-dictionary in the format returned by SponsorBlock - mini_dict = { - 'category': category, - 'action': action_type, - 'start_time': start_time, - 'stop_time': stop_time, - 'duration': 0, - } - - # Add it to the list - slice_list = self.video_obj.slice_list - slice_list.append(mini_dict) - - # (The called function will sort the list) - self.video_obj.set_slices(slice_list) - - # Show changes, and empty entry boxes - self.update_treeview() - entry.set_text('') - entry.set_text('') - - - def on_clear_slice_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Empties the video's slice list. - - Code adapted from config.VideoEditWin.on_clear_slice_button_clicked(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.video_obj.reset_slices() - self.update_treeview() - - - def on_contact_sblock_clicked(self, button): - - """Called from a callback in self.__init__(). - - Contacts SponsorBlock to reset the video's slice list. - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2, (Gtk.Entry): Widgets specifying timestamps/seconds - - """ - - utils.fetch_slice_data( - self.main_win_obj.app_obj, - self.video_obj, - ) - - self.update_treeview() - - - def on_delete_slice_button_clicked(self, button, treeview): - - """Called from a callback in self.__init__(). - - Deletes the selected slice(s) from the video's slice list. - - Code adapted from config.VideoEditWin.on_delete_slice_button_clicked(). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeVies): The treeview displaying the timestamp list - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - return - - # (Multiple selection is not enabled) - this_iter = model.get_iter(path_list[0]) - if this_iter is None: - return - - category = model[this_iter][1] - action_type = model[this_iter][2] - start_time = float(model[this_iter][3]) - stop_time = float(model[this_iter][4]) - - # Slices are stored as a list of mini-dictionaries, in the form - # described by self.on_add_slice_button_clicked() - # Walk the list, and delete the first matching mini-dictionary - slice_list = self.video_obj.slice_list - mod_list = [] - match_flag = False - - for mini_dict in slice_list: - - if not match_flag \ - and mini_dict['category'] == category \ - and mini_dict['action'] == action_type \ - and mini_dict['start_time'] == start_time \ - and mini_dict['stop_time'] == stop_time: - match_flag = True # Delete this one - else: - mod_list.append(mini_dict) - - # (The called function will sort the list) - self.video_obj.set_slices(mod_list) - - # (Show changes) - self.update_treeview() - - - def on_dl_all_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Downloads/creates all clips. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.dl_mode = 'multiple' - self.slice_list = self.video_obj.slice_list.copy() - - if self.slice_list: - self.start_dl_flag = True - self.close() - - - def on_dl_marked_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Downloads/creates video with marked slice(s) removed. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.dl_mode = 'multiple' - self.slice_list = [] - - count = -1 - for path in range(0, len(self.slice_liststore)): - - count += 1 - if self.slice_liststore[path][0] is True: - - self.slice_list.append(self.video_obj.slice_list[count]) - - if self.slice_list: - self.start_dl_flag = True - self.close() - - - def on_dl_single_button_clicked(self, button, entry, entry2): - - """Called from a callback in self.__init__(). - - Downloads/creates a sliced video. - - Args: - - button (Gtk.Button): The widget clicked - - entry, entry2, (Gtk.Entry): Widgets specifying timestamps/seconds - - """ - - self.dl_mode = 'single' - - # First entry is compulsory, second is optional - # Values, if specified, can be either a timestamp, or a value in - # seconds - start_time = entry.get_text() - if start_time != '': - self.start_time = utils.strip_whitespace(start_time) - - stop_time = entry2.get_text() - if stop_time != '': - self.stop_time = utils.strip_whitespace(stop_time) - - if self.start_time is not None: - self.start_dl_flag = True - self.close() - - - def on_prefs_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Opens the preferences window to show SponsorBlock settings. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # N.B. Destroy this window before opening the preferences window, or - # the latter will be hidden behind the main window - # N.B. If we don't destroy this window first, the preferences window is - # non-responsive - self.destroy() - config.SystemPrefWin(self.main_win_obj.app_obj, 'slices') - - - def on_radiobutton_toggled(self, radiobutton): - - """Called from a callback in self.__init__(). - - Updates widgets in the window, according to the current mode. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.slice_mode = 'ffmpeg' - - self.label3.set_markup( - '' + _('Download video with one slice') + ':' - ) - self.set_bold_button_text(self.button, _('Download sliced video')) - self.label6.set_markup( - '' + _('Download video with multiple slices') + ':', - ) - self.set_bold_button_text( - self.button9, - _('Download video, removing marked slices'), - ) - self.set_bold_button_text( - self.button10, - _('Download video, removing all slices'), - ) - - - def on_radiobutton2_toggled(self, radiobutton): - - """Called from a callback in self.__init__(). - - Updates widgets in the window, according to the current mode. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - if radiobutton.get_active(): - self.slice_mode = 'create' - self.dl_mode = None - - self.label3.set_markup( - '' + _('Create video with one slice') + ':' - ) - self.set_bold_button_text(self.button, _('Create sliced video')) - self.label6.set_markup( - '' + _('Create video with multiple slices') + ':', - ) - self.set_bold_button_text( - self.button9, - _('Create video, removing marked slices'), - ) - self.set_bold_button_text( - self.button10, - _('Create video, removing all slices'), - ) - - - def on_select_all_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - Selects the checkbutton in every line in the treeview. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.slice_liststore)): - self.slice_liststore[path][0] = True - - - def on_select_none_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - De-selects the checkbutton in every line in the treeview. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.slice_liststore)): - self.slice_liststore[path][0] = False - - - def on_start_entry_changed(self, entry): - - """Called from callback in self.__init__(). - - Sensitises the download button, depending on whether the entry contains - text, or not. - - Note that the validity of any timestamps the user specifies is checked - by the calling mainwin.MainWin code. - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - text = entry.get_text() - if text == '': - self.button.set_sensitive(False) - else: - self.button.set_sensitive(True) - - - def on_treeview_button_toggled(self, renderer_toggle, tree_path): - - """Called from a callback in self.__init__(). - - Toggles one of the check buttons in the treeview. - - Args: - - renderer_toggle (Gtk.CellRendererToggle): The widget clicked - - tree_path (Gtk.TreePath): Path to the clicked row - - """ - - self.slice_liststore[tree_path][0] \ - = not self.slice_liststore[tree_path][0] - - -class RecentVideosDialogue(Gtk.Dialog): - - """Called by mainwin.MainWin.on_video_index_recent_videos_time(). - - Python class handling a dialogue window that prompts the user to set the - time after which videos are removed from the fixed 'Recent Videos' folder. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Folder): The 'Recent Videos' folder - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Set removal time\' dialogue starts here.' \ - + ' In the Videos tab, right-click the \'Recent Videos\'' \ - + ' folder and select Downloads > Set removal time...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.spinbutton = None # Gtk.SpinButton - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Set removal time'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label() - grid.attach(label, 0, 0, 2, 1) - label.set_markup( - _( - 'When videos are checked/downloaded, older videos\nare removed' \ - + ' from the Recent Videos folder.', - ), - ) - label.set_alignment(0, 0.5) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 1, 2, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Empty the whole folder'), - ) - grid.attach(self.radiobutton, 0, 2, 2, 1) - # (Signal connect appears below) - - self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton, - _('Remove videos after days'), - ) - grid.attach(self.radiobutton2, 0, 3, 1, 1) - - self.spinbutton = Gtk.SpinButton.new_with_range(1, 14, 1) - grid.attach(self.spinbutton, 1, 3, 1, 1) - self.spinbutton.set_hexpand(False) - - if not main_win_obj.app_obj.fixed_recent_folder_days: - self.spinbutton.set_sensitive(False) - else: - self.radiobutton2.set_active(True) - self.spinbutton.set_value( - main_win_obj.app_obj.fixed_recent_folder_days, - ) - - # (Signal connects from above) - self.radiobutton.connect( - 'toggled', - self.on_radiobutton_toggled, - ) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_radiobutton_toggled(self, radiobutton): - - """Called from a callback in self.__init__(). - - (De)sensitises the spinbutton, depending on which radiobutton is - selected. - - Args: - - radiobutton (Gtk.RadioButton): The clicked widget - - """ - - if radiobutton.get_active(): - self.spinbutton.set_sensitive(False) - else: - self.spinbutton.set_sensitive(True) - - -class RemoveLockFileDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.load_db(). - - Python class handling a dialogue window that asks the user what to do, - if the database file can't be loaded because it's protected by a lockfile. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - switch_flag (bool): False when Tartube starts; True when a database - had already been loaded, and the user is trying to switch to a - different one - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, switch_flag): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Stale lockfile\' dialogue starts here.' \ - + ' Visible on startup if the Tartube database file is protected' \ - + ' bu a lockfile' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - other - # --------------- - # Flag set to True if the lockfile should be removed - self.remove_flag = False - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Stale lockfile'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ) - - self.set_modal(True) - - # Set up the dialogue window - spacing_size = self.main_win_obj.spacing_size - label_length = self.main_win_obj.long_string_max_len - - box = self.get_content_area() - - # Tartube logo on the left, widgets on the right - hbox = Gtk.HBox() - box.add(hbox) - - # Logo in the top corner - vbox = Gtk.VBox() - hbox.pack_start(vbox, False, False, spacing_size) - - image = Gtk.Image.new_from_pixbuf( - main_win_obj.pixbuf_dict['system_icon'], - ) - vbox.pack_start(image, False, False, spacing_size) - - grid = Gtk.Grid() - hbox.pack_start(grid, False, False, spacing_size) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - # (Actually, the grid width of the area to the right of the Tartube - # logo) - grid_width = 2 - - label = Gtk.Label( - utils.tidy_up_long_string( - _( - 'Failed to load the Tartube database file, because another' \ - + ' copy of Tartube seems to be using it', - ), - label_length, - ) + '\n\n' \ - + utils.tidy_up_long_string( - _( - 'Do you want to load it anyway?', - ), - label_length, - ) + '\n\n' \ - + utils.tidy_up_long_string( - _( - '(Only click \'Yes\' if you are sure that other copies of' \ - + ' Tartube are not using the database right now)', - ), - label_length, - ) - ) - grid.attach(label, 1, 0, grid_width, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 1, 1, grid_width, 1) - - button = Gtk.Button.new_with_label( - _('Yes, load the file'), - ) - grid.attach(button, 1, 2, 1, 1) - button.set_hexpand(True) - button.connect('clicked', self.on_yes_button_clicked) - - if not switch_flag: - msg = _('No, just shut down Tartube') - else: - msg = _('No, don\'t load the file') - - button2 = Gtk.Button.new_with_label(msg) - grid.attach(button2, 1, 3, 1, 1) - button2.set_hexpand(True) - button2.connect('clicked', self.on_no_button_clicked) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_yes_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the Yes button is clicked, set a flag for the calling function to - check, the close the window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.remove_flag = True - self.destroy() - - - def on_no_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the No button is clicked, set a flag for the calling function to - check, the close the window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.remove_flag = False - self.destroy() - - -class RenameContainerDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.rename_container(). - - Python class handling a dialogue window that prompts the user to rename - a channel, playlist or folder. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist, media.Folder): The media - data object whose name is to be changed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Rename container\' dialogue starts here.' \ - + ' In the Videos tab, right-click a channel and select' \ - + ' Channel actions > Rename channel...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - - - # Code - # ---- - - media_type = media_data_obj.get_type() - if media_type == 'channel': - string = _('Rename channel') - elif media_type == 'playlist': - string = _('Rename playlist') - else: - string = _('Rename folder') - - Gtk.Dialog.__init__( - self, - string, - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - if media_type == 'channel': - string = _('Set the new name for the channel:') - elif media_type == 'playlist': - string = _('Set the new name for the playlist:') - else: - string = _('Set the new name for the folder:') - - label = Gtk.Label() - grid.attach(label, 0, 0, 1, 1) - label.set_markup( - string + '\n\n' + media_data_obj.name + '\n\n' + _( - 'N.B. This procedure will modify your filesystem!\n', - ) - ) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 1, 1) - self.entry.set_text(media_data_obj.name) - - # Display the dialogue window - self.show_all() - - -class ResetContainerDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_reset_container(). - - Python class handling a dialogue window to allow the user to reset channel/ - playlist names in Tartube's database, replacing them with names gathered - from their child video's metadata (i.e. the original channel/playlist names - used on the site from which the videos were downloaded). - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Create profile\' dialogue starts here.' \ - + ' In the main window menu, click Media > Reset channel/' \ - + '/playlist names...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.treeview = None # Gtk.TreeView - self.liststore = None # Gtk.TreeView - - - # IV list - other - # --------------- - # Dictionary of media.Channel and media.Playlist objects whose names - # should be reset - # Dictionary in the form - # key: .dbid - # value: True to reset, False to not reset - self.reset_dict = {} - - # Dictionary of media.Channel and media.Playlist objects whose names - # should be reset, but not to the original website name, but to a - # name the user has typed - # Dictionary in the form - # key: .dbid (a subset of those in self.reset_dict) - # value: The new typed name - self.custom_dict = {} - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Reset channel/playlist names'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - self.set_default_size( - main_win_obj.app_obj.config_win_width, - main_win_obj.app_obj.config_win_height, - ) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid_width = 3 - - label = Gtk.Label( - _( - 'This list is updated whenever channels/playlists are' \ - + ' checked/downloaded', - ), - ) - grid.attach(label, 0, 0, grid_width, 1) - - label2 = Gtk.Label( - _( - 'Select which names should be reset to the names on the' \ - + ' original website', - ), - ) - grid.attach(label2, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - grid.attach(scrolled, 0, 2, grid_width, 1) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_hexpand(True) - scrolled.set_vexpand(True) - - frame = Gtk.Frame() - scrolled.add_with_viewport(frame) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.treeview = Gtk.TreeView() - frame.add(self.treeview) - self.treeview.set_can_focus(False) - - renderer_toggle = Gtk.CellRendererToggle() - renderer_toggle.connect('toggled', self.on_checkbutton_toggled) - column_toggle = Gtk.TreeViewColumn( - _('Reset'), - renderer_toggle, - active=0, - ) - self.treeview.append_column(column_toggle) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - _('Type'), - renderer_pixbuf, - pixbuf=1, - ) - self.treeview.append_column(column_pixbuf) - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - _('Database name'), - renderer_text, - text=2, - ) - self.treeview.append_column(column_text) - - renderer_text2 = Gtk.CellRendererText() - column_text2 = Gtk.TreeViewColumn( - _('Original name'), - renderer_text2, - text=3, - ) - self.treeview.append_column(column_text2) - renderer_text2.set_property('editable', True) - renderer_text2.connect( - 'edited', - self.on_original_name_edited, - ) - - renderer_text3 = Gtk.CellRendererText() - column_text3 = Gtk.TreeViewColumn( - 'hide', - renderer_text3, - text=4, - ) - column_text3.set_visible(False) - self.treeview.append_column(column_text3) - - self.liststore = Gtk.ListStore(bool, GdkPixbuf.Pixbuf, str, str, int) - self.treeview.set_model(self.liststore) - - # Get a sorted list of resettable channel/playlist names... - app_obj = self.main_win_obj.app_obj - sorted_list = [] - for dbid in app_obj.media_reset_container_dict.keys(): - sorted_list.append(app_obj.media_reg_dict[dbid]) - - sorted_list.sort(key=lambda x: x.name) - - # ...and populate the treeview - for media_data_obj in sorted_list: - - if isinstance(media_data_obj, media.Channel): - pixbuf = main_win_obj.pixbuf_dict['channel_small'] - else: - pixbuf = main_win_obj.pixbuf_dict['playlist_small'] - - self.liststore.append([ - True, - pixbuf, - media_data_obj.name, - app_obj.media_reset_container_dict[media_data_obj.dbid], - media_data_obj.dbid, - ]) - - self.reset_dict[media_data_obj.dbid] = True - - # Strip of widgets at the bottom - button = Gtk.Button.new_with_label(_('Select all')) - grid.attach(button, 1, 3, 1, 1) - button.set_hexpand(False) - button.connect('clicked', self.on_select_all_clicked) - - button2 = Gtk.Button.new_with_label(_('Unselect all')) - grid.attach(button2, 2, 3, 1, 1) - button2.set_hexpand(False) - button2.connect('clicked', self.on_deselect_all_clicked) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_checkbutton_toggled(self, checkbutton, path): - - """Called from a callback in self.__init__(). - - Respond when the user selects/deselects an item in the treeview. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - path (int): A number representing the widget's row - - """ - - # The user has clicked on the checkbutton widget, so toggle the widget - # itself - self.liststore[path][0] = not self.liststore[path][0] - - # Update the data to be returned (eventually) to the calling - # mainapp.TartubeApp.import_into_db() function - if not self.liststore[path][0]: - self.reset_dict[self.liststore[path][4]] = False - else: - self.reset_dict[self.liststore[path][4]] = True - - - def on_original_name_edited(self, widget, path, text): - - """Called from a callback in self.__init__(). - - Replace the channel/playlist's name on the original website with a - name that the user types. - - Args: - - widget (Gtk.CellRendererText): The widget clicked - - path (int): Path to the treeview line that was edited - - text (str): The new contents of the cell - - """ - - app_obj = self.main_win_obj.app_obj - - # Check the entered text is a valid name - if text == '' \ - or re.search(r'^\s*$', text) \ - or not app_obj.check_container_name_is_legal(text): - return - - # Check the container still exists - container_dbid = self.liststore[path][4] - if not container_dbid in app_obj.container_reg_dict: - return - - # Check that the parent folder doesn't already have a container - # with the same name - container_obj = app_obj.container_reg_dict[container_dbid] - if app_obj.find_duplicate_name_in_container( - container_obj.parent_obj, - text, - ): - return - - # Check that the name is unique within this dialogue window - for other_text in self.custom_dict.values(): - if other_text == text: - return - - # Update the column text - self.liststore[path][3] = text - - # Update the data to be returned (eventually) to the calling - # mainapp.TartubeApp.on_menu_reset_container() function - self.custom_dict[container_dbid] = text - - - def on_select_all_clicked(self, button): - - """Called from a callback in self.__init__(). - - Mark all channels/playlists/folders to be reset. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.liststore)): - self.liststore[path][0] = True - self.reset_dict[self.liststore[path][4]] = True - - - def on_deselect_all_clicked(self, button): - - """Called from a callback in self.__init__(). - - Mark all channels/playlists/folders to be not reset. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.liststore)): - self.liststore[path][0] = False - self.reset_dict[self.liststore[path][4]] = False - - -class ScheduledDialogue(Gtk.Dialog): - - """Called by MainWin.on_video_index_add_to_scheduled(). - - Python class handling a dialogue window that prompts the user to choose a - scheduled download. The specified channel/playlist/folder is added to the - scheduled download selected by the user (if any). - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist, media.Folder): The media - data object to be added to a scheduled download - - available_list (list): List of names of media.Scheduled objects that - don't already contain the specified media data object - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj, available_list): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Add to scheduled download\' dialogue' \ - + ' starts here. In the Videos tab, right-click a channel and' \ - + ' select Downloads > Add to scheduled download...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - other - # --------------- - # Store the user's choice as an IV, so the calling function can - # retrieve it - self.choice = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Add to scheduled download'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - spacing_size = self.main_win_obj.spacing_size - - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - - media_type = media_data_obj.get_type() - if media_type == 'channel': - string = _('Add the channel to this scheduled download:') - elif media_type == 'playlist': - string = _('Add the playlist to this scheduled download:') - else: - string = _('Add the folder to this scheduled download:') - - label = Gtk.Label(string) - grid.attach(label, 0, 0, 1, 1) - - # Add a combo - store = Gtk.ListStore(str) - for name in available_list: - store.append( [name] ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 0, 1, 1, 1) - combo.set_hexpand(True) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - - combo.connect('changed', self.on_combo_changed) - combo.set_active(0) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - radiobutton2 (Gtk.RadioButton): Another widget to check - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.choice = model[tree_iter][0] - - -class SetDestinationDialogue(Gtk.Dialog): - - """Called by MainWin.on_video_index_set_destination(). - - Python class handling a dialogue window that prompts the user to set the - alternative download destination for a channel, playlist or folder. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist, media.Folder): The media - data object whose download destination is to be changed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Set download destination\' dialogue' \ - + ' starts here. In the Videos tab, right-click a channel and' \ - + ' select Downloads > Set download destination...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - other - # --------------- - # Store function arguments as IVs, so callback functions can retrieve - # them - self.media_data_obj = media_data_obj - # Store the user's choice as an IV, so the calling function can - # retrieve it - # The two values can be distinguished, because .external_dir is always - # a string, and .master_dbid is always an integer - if media_data_obj.external_dir: - self.choice = media_data_obj.external_dir - else: - self.choice = media_data_obj.master_dbid - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Set download destination'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - spacing_size = self.main_win_obj.spacing_size - label_length = self.main_win_obj.very_long_string_max_len - - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - - grid_width = 3 - - # If the alternative download destination selected by this window, the - # last time it was opened, has since been deleted, then reset the IV - # that stores it - app_obj = main_win_obj.app_obj - prev_dbid = main_win_obj.previous_alt_dest_dbid - if prev_dbid is not None and not prev_dbid in app_obj.media_reg_dict: - prev_dbid = None - main_win_obj.set_previous_alt_dest_dbid(None) - - # Likewise, if the previous external directory no longer exists, then - # reset the IV that stores it - prev_external_dir = main_win_obj.previous_external_dir - if prev_external_dir is not None \ - and not os.path.isdir(prev_external_dir): - prev_external_dir = None - main_win_obj.set_previous_external_dir(None) - - # Add widgets - media_type = media_data_obj.get_type() - if os.name == 'nt': - if media_type == 'channel': - msg = _( - 'This channel normally downloads videos into its own' \ - + ' folder', - ) - elif media_type == 'playlist': - msg = _( - 'This playlist normally downloads videos into its own' \ - + ' folder', - ) - else: - msg = _( - 'This folder normally downloads videos into itself', - ) - - else: - if media_type == 'channel': - msg = _( - 'This channel normally downloads videos into its own' \ - + ' directory', - ) - elif media_type == 'playlist': - msg = _( - 'This playlist normally downloads videos into its own' \ - + ' directory', - ) - else: - msg = _( - 'This folder normally downloads videos into its own' \ - + ' directory', - ) - - label = Gtk.Label(utils.tidy_up_long_string(msg, label_length)) - grid.attach(label, 0, 0, grid_width, 1) - label.set_xalign(0) - - radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - _('Use this location'), - ) - grid.attach(radiobutton, 0, 1, grid_width, 1) - # (Signal connect appears below) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 2, grid_width, 1) - - msg = _('Choose a different location if:') \ - + '\n\n • ' + utils.tidy_up_long_string( - _( - 'You want to add a channel and its playlists, without' \ - + ' downloading the same video twice', - ), - label_length, - ) + '\n\n • ' + utils.tidy_up_long_string( - _( - 'A video creator has channels on both YouTube and' \ - + ' BitChute, and you want to add both without' \ - + ' downloading the same video twice', - ), - label_length, - ) - - label2 = Gtk.Label(msg) - grid.attach(label2, 0, 3, grid_width, 1) - label2.set_xalign(0) - - radiobutton2 = Gtk.RadioButton.new_from_widget(radiobutton) - grid.attach(radiobutton2, 0, 4, 1, 1) - radiobutton2.set_label('Use a different location:') - radiobutton2.set_hexpand(False) - # (Signal connect appears below) - - # Get a list of channels/playlists/folders - dbid_list = list(app_obj.container_reg_dict.keys()) - - # From this list, filter out: - # - Any channel/playlist/folder which has an alternative download - # destination set (a media data object can't have an alternative - # destination, and be an alternative destination at the same - # time) - # - media_data_obj's alternative download destination, if any - # - The most recently-selected alternative download destination, if - # any - # - media_data_obj itself - obj_list = [] - for this_dbid in dbid_list: - - this_obj = app_obj.media_reg_dict[this_dbid] - - if this_dbid != media_data_obj.dbid \ - and ( - media_data_obj.master_dbid == media_data_obj.dbid \ - or media_data_obj.master_dbid != this_dbid - ) and (prev_dbid is None or prev_dbid != this_dbid) \ - and this_obj.dbid == this_obj.master_dbid: - obj_list.append(this_obj) - - # Sort the modified list... - obj_list.sort(key=lambda x: x.name.lower()) - - # ...and then add, at the top of the list, possible destinations that - # were filtered out - obj_list.insert(0, media_data_obj) - - if media_data_obj.master_dbid != media_data_obj.dbid: - obj_list.insert( - 0, - app_obj.media_reg_dict[media_data_obj.master_dbid], - ) - - elif prev_dbid is not None: - prev_obj = app_obj.media_reg_dict[prev_dbid] - obj_list.insert(0, prev_obj) - - # Add a combo - store = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str) - count = -1 - - for this_obj in obj_list: - if isinstance(this_obj, media.Channel): - icon_name = 'channel_small' - elif isinstance(this_obj, media.Playlist): - icon_name = 'playlist_small' - else: - icon_name = 'folder_small' - - store.append([ - main_win_obj.pixbuf_dict[icon_name], - this_obj.dbid, - this_obj.name, - ]) - - count += 1 - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 1, 4, 2, 1) - combo.set_hexpand(True) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 2) - combo.set_active(0) - # (Signal connect appears below) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 5, grid_width, 1) - - if os.name == 'nt': - string = _( - 'Using an external folder is not recommended, in general.' \ - + ' Choose an external folder if:', - ) - else: - string = _( - 'Using an external directory is not recommended, in general.' \ - + ' Choose an external directory if:', - ) - - if os.name == 'nt': - string2 = _( - 'You want a different application to process the' \ - + ' downloaded videos (other applications should not modify' \ - + ' Tartube\'s main data folder)', - ) - else: - string2 = _( - 'You want a different application to process the' \ - + ' downloaded videos (other applications should not modify' \ - + ' Tartube\'s main data directory)', - ) - - label3 = Gtk.Label( - utils.tidy_up_long_string(string, label_length) \ - + '\n\n • ' + utils.tidy_up_long_string(string2, label_length), - ) - grid.attach(label3, 0, 6, grid_width, 1) - label3.set_xalign(0) - - radiobutton3 = Gtk.RadioButton.new_from_widget(radiobutton2) - radiobutton3.set_label('Use an external location:') - grid.attach(radiobutton3, 0, 7, 2, 1) - # (Signal connect appears below) - - button = Gtk.Button.new_with_label(_('Set')) - grid.attach(button, 2, 7, 1, 1) - button.set_hexpand(False) - # (Signal connect appears below) - - entry = Gtk.Entry() - grid.attach(entry, 0, 8, grid_width, 1) - entry.set_editable(False) - entry.set_can_focus(False) - if media_data_obj.external_dir is not None: - entry.set_text(media_data_obj.external_dir) - elif prev_external_dir is not None: - entry.set_text(prev_external_dir) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 9, grid_width, 1) - - # Set widget initial states - if type(self.choice) == str: - radiobutton3.set_active(True) - combo.set_sensitive(False) - elif self.choice != media_data_obj.dbid: - radiobutton2.set_active(True) - button.set_sensitive(False) - else: - radiobutton.set_active(True) - combo.set_sensitive(False) - button.set_sensitive(False) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_radiobutton_toggled, - combo, - button, - entry, - ) - radiobutton2.connect( - 'toggled', - self.on_radiobutton2_toggled, - combo, - button, - entry, - ) - radiobutton3.connect( - 'toggled', - self.on_radiobutton3_toggled, - combo, - button, - entry, - ) - combo.connect('changed', self.on_combo_changed) - button.connect('clicked', self.on_button_clicked, entry) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_button_clicked(self, button, entry): - - """Called from a callback in self.__init__(). - - Prompts the user for a directory path, then stores it, so the calling - function can retriveve it. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to update - - """ - - # Import the main application (for convenience) - app_obj = self.main_win_obj.app_obj - - # Show a file chooser - if os.name == 'nt': - msg = _('Select an external folder') - else: - msg = _('Select an external directory') - - dialogue_win = app_obj.dialogue_manager_obj.show_file_chooser( - msg, - self.main_win_obj, - 'folder', - ) - - response = dialogue_win.run() - dest_dir = dialogue_win.get_filename() - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # An external directory is not allowed inside Tartube's data - # directory - if dest_dir[:len(app_obj.data_dir)] == app_obj.data_dir: - - if os.name == 'nt': - msg = _( - 'An external folder must not be inside Tartube\'s' \ - + ' own data folder', - ) - - else: - msg = _( - 'An external directory must not be inside Tartube\'s' \ - + ' own data directory', - ) - - # (Unfortunately, the new dialogue window is not closeable - # unless this dialogue window is closed first) - self.destroy() - - app_obj.dialogue_manager_obj.show_msg_dialogue( - msg, - 'error', - 'ok', - None, # Parent window is main window - ) - - else: - self.choice = dest_dir - entry.set_text(dest_dir) - self.main_win_obj.set_previous_external_dir(dest_dir) - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - radiobutton2 (Gtk.RadioButton): Another widget to check - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - dbid = model[tree_iter][1] - - # (Allow for the possibility that the media data object might have - # been deleted, since the dialogue window opened) - if dbid in self.main_win_obj.app_obj.container_reg_dict: - self.choice = dbid - self.main_win_obj.set_previous_alt_dest_dbid(dbid) - - - def on_radiobutton_toggled(self, radiobutton, combo, button, entry): - - """Called from callback in self.__init__(). - - When the specified radiobutton is toggled, modify other widgets in the - dialogue window, and set self.choice (the value to be retrieved by the - calling function) - - Args: - - radiobutton (Gtk.RadioButton): The clicked widget - - combo (Gtk.ComboBox): Another widget to modify - - button (Gtk.Button): Another widget to modify - - entry (Gtk.Entry): Another widget to modify - - """ - - if radiobutton.get_active(): - combo.set_sensitive(False) - button.set_sensitive(False) - entry.set_sensitive(False) - self.choice = self.media_data_obj.dbid - - - def on_radiobutton2_toggled(self, radiobutton2, combo, button, entry): - - """Called from callback in self.__init__(). - - When the specified radiobutton is toggled, modify other widgets in the - dialogue window, and set self.choice (the value to be retrieved by the - calling function) - - Args: - - radiobutton2 (Gtk.RadioButton): The clicked widget - - combo (Gtk.ComboBox): The widget containing the user's choice - - button (Gtk.Button): Another widget to modify - - entry (Gtk.Entry): Another widget to modify - - """ - - if radiobutton2.get_active(): - combo.set_sensitive(True) - button.set_sensitive(False) - entry.set_sensitive(False) - - tree_iter = combo.get_active_iter() - model = combo.get_model() - dbid = model[tree_iter][1] - - # (Allow for the possibility that the media data object might have - # been deleted, since the dialogue window opened) - if dbid in self.main_win_obj.app_obj.container_reg_dict: - self.choice = dbid - self.main_win_obj.set_previous_alt_dest_dbid(dbid) - - - def on_radiobutton3_toggled(self, radiobutton2, combo, button, entry): - - """Called from callback in self.__init__(). - - When the specified radiobutton is toggled, modify other widgets in the - dialogue window, and set self.choice (the value to be retrieved by the - calling function) - - Args: - - radiobutton2 (Gtk.RadioButton): The clicked widget - - combo (Gtk.ComboBox): Another widget to modify - - button (Gtk.Button): Another widget to modify - - entry (Gtk.Entry): The widget containing the user's choice - - """ - - if radiobutton2.get_active(): - combo.set_sensitive(False) - button.set_sensitive(True) - entry.set_sensitive(True) - - # self.choice set to its default value, until the user actually - # specifies an external directory - dest_dir = entry.get_text() - if dest_dir == '': - self.choice = self.media_data_obj.dbid - else: - self.choice = dest_dir - - self.main_win_obj.set_previous_external_dir(dest_dir) - - -class SetNicknameDialogue(Gtk.Dialog): - - """Called by MainWin.on_video_index_set_nickname(). - - Python class handling a dialogue window that prompts the user to set the - nickname of a channel, playlist or folder. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist, media.Folder): The media - data object whose nickname is to be changed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Set nickname\' dialogue starts here.' \ - + ' In the Videos tab, right-click a channel and select' \ - + ' Channel actions > Set nickname...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Set nickname'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - spacing_size = self.main_win_obj.spacing_size - label_length = self.main_win_obj.long_string_max_len - - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - - media_type = media_data_obj.get_type() - if media_type == 'channel': - msg = _( - 'Set a nickname for the channel \'{0}\' (or leave it blank' \ - + ' to reset the nickname)', - ).format(media_data_obj.name) - elif media_type == 'playlist': - msg = _( - 'Set a nickname for the playlist \'{0}\' (or leave it blank' \ - + ' to reset the nickname)', - ).format(media_data_obj.name) - else: - msg = _( - 'Set a nickname for the folder \'{0}\' (or leave it blank' \ - + ' to reset the nickname)', - ).format(media_data_obj.name) - - label = Gtk.Label( - utils.tidy_up_long_string( - msg, - label_length, - ), - ) - grid.attach(label, 0, 0, 1, 1) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 1, 1) - self.entry.set_text(media_data_obj.nickname) - - # Display the dialogue window - self.show_all() - - -class SetURLDialogue(Gtk.Dialog): - - """Called by MainWin.on_video_index_set_url(). - - Python class handling a dialogue window that prompts the user to set the - source URL of a channel or playlist. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist): The media data object - whose source URL is to be changed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Set URL\' dialogue starts here.' \ - + ' In the Videos tab, right-click a channel and select' \ - + ' Channel actions > Set URL...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Set URL'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - spacing_size = self.main_win_obj.spacing_size - label_length = self.main_win_obj.long_string_max_len - - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - - media_type = media_data_obj.get_type() - if media_type == 'channel': - msg = _( - 'Update the URL for the channel \'{0}\'', - ).format(media_data_obj.name) - else: - msg = _( - 'Update the URL for the playlist \'{0}\'', - ).format(media_data_obj.name) - - label = Gtk.Label( - utils.tidy_up_long_string( - msg, - label_length, - ), - ) - grid.attach(label, 0, 0, 1, 1) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 1, 1) - self.entry.set_text(media_data_obj.source) - - # Display the dialogue window - self.show_all() - - -class SystemCmdDialogue(Gtk.Dialog): - - """Called by mainwin.MainWin.on_video_index_show_system_cmd() and - .on_video_catalogue_show_system_cmd(). - - Python class handling a dialogue window that shows the user the system - command that would be used in a download operation for a particular - media.Video, media.Channel, media.Playlist or media.Folder object. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object in question - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Show system command\' dialogue starts' \ - + ' here. In the Videos tab, right-click a channel and select' \ - + ' Downloads > Show system command...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.textbuffer = None # Gtk.TextBuffer - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Show system command'), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - (Gtk.STOCK_OK, Gtk.ResponseType.OK), - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - grid_width = 3 - - media_type = media_data_obj.get_type() - label = Gtk.Label( - utils.shorten_string( - utils.upper_case_first(media_type) + ': ' \ - + media_data_obj.name, - 50, - ), - ) - grid.attach(label, 0, 0, grid_width, 1) - - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_size_request(400, 150) - - textview = Gtk.TextView() - scrolled.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_hexpand(False) - textview.set_editable(False) - - self.textbuffer = textview.get_buffer() - # Initialise the textbuffer's contents - self.update_textbuffer(media_data_obj) - - button = Gtk.Button(_('Update')) - grid.attach(button, 0, 2, 1, 1) - button.set_hexpand(True) - button.connect( - 'clicked', - self.on_update_clicked, - media_data_obj, - ) - - button2 = Gtk.Button(_('Copy to clipboard')) - grid.attach(button2, 1, 2, 1, 1) - button2.set_hexpand(True) - button2.connect( - 'clicked', - self.on_copy_clicked, - media_data_obj, - ) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 3, 2, 1) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def update_textbuffer(self, media_data_obj): - - """Called from self.__init__(). - - Initialises the specified textbuffer. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object whose system command is - displayed in this dialogue window - - Return values: - - A string containing the system command displayed, or an empty - string if the system command could not be generated - - """ - - # Get the options.OptionsManager object that applies to this media - # data object - # (The manager might be specified by obj itself, or it might be - # specified by obj's parent, or we might use the default - # options.OptionsManager) - options_obj = utils.get_options_manager( - self.main_win_obj.app_obj, - media_data_obj, - ) - - # Generate the list of download options for this media data object - options_parser_obj = options.OptionsParser(self.main_win_obj.app_obj) - options_list = options_parser_obj.parse(media_data_obj, options_obj) - - # Obtain the system command used to download this media data object - cmd_list = utils.generate_ytdl_system_cmd( - self.main_win_obj.app_obj, - media_data_obj, - options_list, - ) - - # Display it in the textbuffer - if cmd_list: - char = ' ' - system_cmd = char.join(cmd_list) - - else: - system_cmd = '' - - self.textbuffer.set_text(system_cmd) - return system_cmd - - - # Callback class methods - - - def on_copy_clicked(self, button, media_data_obj): - - """Called from a callback in self.__init__(). - - Updates the contents of the textview, and copies the system command to - the clipboard. - - Args: - - button (Gtk.Button): The widget clicked - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object whose system command is - displayed in this dialogue window - - """ - - # Obtain the system command used to download this media data object, - # and display it in the textbuffer - system_cmd = self.update_textbuffer(media_data_obj) - - # Copy the system command to the clipboard - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clipboard.set_text(system_cmd, -1) - - - def on_update_clicked(self, button, media_data_obj): - - """Called from a callback in self.__init__(). - - Updates the contents of the textview. - - Args: - - button (Gtk.Button): The widget clicked - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object whose system command is - displayed in this dialogue window - - """ - - # Obtain the system command used to download this media data object, - # and display it in the textbuffer - self.update_textbuffer(media_data_obj) - - -class TestCmdDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_test_ytdl() and - MainWin.on_video_catalogue_test_dl() - - Python class handling a dialogue window that prompts the user for a - URL and youtube-dl options. If the user specifies one or both, they are - used in an info operation. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - source_url (str): If specified, this URL is added to the Gtk.Entry - automatically - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, source_url=None): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Test youtube-dl\' dialogue starts here.' \ - + ' In the main window menu, click Operations > Test youtube-dl' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - self.textbuffer = None # Gtk.TextBuffer - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - _('Test') + ' ' + main_win_obj.app_obj.get_downloader(), - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label( - _('URL of the video to download (optional)'), - ) - grid.attach(label, 0, 0, 1, 1) - - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 1, 1) - self.entry.set_hexpand(True) - if source_url is not None: - self.entry.set_text(source_url) - - label2 = Gtk.Label( - _('Command line options (optional)'), - ) - grid.attach(label2, 0, 2, 1, 1) - - frame = Gtk.Frame() - grid.attach(frame, 0, 3, 1, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_size_request(400, 150) - - textview = Gtk.TextView() - scrolled.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_hexpand(False) - if source_url is not None: - # The calling function has already specified a URL, so move the - # cursor straight into the textview - textview.grab_focus() - - self.textbuffer = textview.get_buffer() - - # Display the dialogue window - self.show_all() - - -class TidyDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_tidy_up() and - MainWin.on_video_index_tidy(). - - Python class handling a dialogue window that prompts the user for which - actions to perform during a tidy operation. If the user selects at least - one action, the calling function starts a tidy operation to apply them. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist or media.Folder): If - specified, only this media data object (and its children) are - tidied up - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj=None): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: \'Tidy up\' dialogue starts here.' \ - + ' In the main window menu, click Operations > Tidy up files...' - ) - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.checkbutton = None # Gtk.CheckButton - self.checkbutton2 = None # Gtk.CheckButton - self.checkbutton3 = None # Gtk.CheckButton - self.checkbutton4 = None # Gtk.CheckButton - self.checkbutton5 = None # Gtk.CheckButton - self.checkbutton6 = None # Gtk.CheckButton - self.checkbutton7 = None # Gtk.CheckButton - self.checkbutton8 = None # Gtk.CheckButton - self.checkbutton9 = None # Gtk.CheckButton - self.checkbutton10 = None # Gtk.CheckButton - self.checkbutton11 = None # Gtk.CheckButton - self.checkbutton12 = None # Gtk.CheckButton - self.checkbutton13 = None # Gtk.CheckButton - self.checkbutton14 = None # Gtk.CheckButton - self.checkbutton15 = None # Gtk.CheckButton - self.checkbutton16 = None # Gtk.CheckButton - self.checkbutton17 = None # Gtk.CheckButton - - - # Code - # ---- - - if media_data_obj is None: - title = _('Tidy up files') - elif isinstance(media_data_obj, media.Channel): - title = _('Tidy up channel') - elif isinstance(media_data_obj, media.Channel): - title = _('Tidy up playlist') - else: - title = _('Tidy up folder') - - Gtk.Dialog.__init__( - self, - title, - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - spacing_size = self.main_win_obj.spacing_size - label_length = self.main_win_obj.quite_long_string_max_len - - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(spacing_size) - grid.set_row_spacing(spacing_size) - - # Left column - self.checkbutton = Gtk.CheckButton() - grid.attach(self.checkbutton, 0, 0, 1, 1) - self.checkbutton.set_label(_('Check that videos are not corrupted')) - # (Signal connect appears below) - - self.checkbutton2 = Gtk.CheckButton() - grid.attach(self.checkbutton2, 0, 1, 1, 1) - self.checkbutton2.set_label(_('Delete corrupted video files')) - self.checkbutton2.set_sensitive(False) - - if not mainapp.HAVE_MOVIEPY_FLAG \ - or self.main_win_obj.app_obj.refresh_moviepy_timeout == 0: - self.checkbutton.set_sensitive(False) - self.checkbutton2.set_sensitive(False) - - self.checkbutton3 = Gtk.CheckButton() - grid.attach(self.checkbutton3, 0, 2, 1, 1) - self.checkbutton3.set_label(_('Check that videos do/don\'t exist')) - - self.checkbutton4 = Gtk.CheckButton() - grid.attach(self.checkbutton4, 0, 3, 1, 1) - self.checkbutton4.set_label( - utils.tidy_up_long_string( - _( - 'Delete downloaded video files (doesn\'t remove videos from' \ - + ' Tartube\'s database)', - ), - label_length, - ), - ) - # (Signal connect appears below) - - self.checkbutton5 = Gtk.CheckButton() - grid.attach(self.checkbutton5, 0, 4, 1, 1) - self.checkbutton5.set_label( - utils.tidy_up_long_string( - _('Also delete all video/audio files with the same name'), - label_length, - ), - ) - self.checkbutton5.set_sensitive(False) - - self.checkbutton6 = Gtk.CheckButton() - grid.attach(self.checkbutton6, 0, 5, 1, 1) - self.checkbutton6.set_label(_('Remove no-URL videos from database')) - - self.checkbutton7 = Gtk.CheckButton() - grid.attach(self.checkbutton7, 0, 6, 1, 1) - self.checkbutton7.set_label(_('Remove duplicate videos from database')) - - # Right column - self.checkbutton8 = Gtk.CheckButton() - grid.attach(self.checkbutton8, 0, 7, 1, 1) - self.checkbutton8.set_label(_('Delete all archive files')) - - self.checkbutton9 = Gtk.CheckButton() - grid.attach(self.checkbutton9, 1, 0, 1, 1) - self.checkbutton9.set_label(_('Move thumbnails into own folder')) - # (Signal connect appears below) - - self.checkbutton10 = Gtk.CheckButton() - grid.attach(self.checkbutton10, 1, 1, 1, 1) - self.checkbutton10.set_label(_('Delete all thumbnail files')) - # (Signal connect appears below) - - self.checkbutton11 = Gtk.CheckButton() - grid.attach(self.checkbutton11, 1, 2, 1, 1) - self.checkbutton11.set_label(_('Delete all .webp thumbnails')) - # (Signal connect appears below) - - self.checkbutton12 = Gtk.CheckButton() - grid.attach(self.checkbutton12, 1, 3, 1, 1) - self.checkbutton12.set_label( - utils.tidy_up_long_string( - _('Convert .webp thumbnails to .jpg using FFmpeg'), - label_length, - ), - ) - - self.checkbutton13 = Gtk.CheckButton() - grid.attach(self.checkbutton13, 1, 4, 1, 1) - self.checkbutton13.set_label( - utils.tidy_up_long_string( - _('Move other metadata files into own folder'), - label_length, - ), - ) - # (Signal connect appears below) - - self.checkbutton14 = Gtk.CheckButton() - grid.attach(self.checkbutton14, 1, 5, 1, 1) - self.checkbutton14.set_label(_('Delete all description files')) - - self.checkbutton15 = Gtk.CheckButton() - grid.attach(self.checkbutton15, 1, 6, 1, 1) - self.checkbutton15.set_label(_('Delete all metadata (JSON) files')) - - self.checkbutton16 = Gtk.CheckButton() - grid.attach(self.checkbutton16, 1, 7, 1, 1) - self.checkbutton16.set_label(_('Delete all annotation files')) - - # !!! DEBUG Git #472 - self.checkbutton17 = Gtk.CheckButton() - grid.attach(self.checkbutton17, 0, 8, 2, 1) - self.checkbutton17.set_label( - _( - 'EXPERIMENTAL: convert \'.unknown_video\' file extensions to .mp4' - ), - ) - - # Bottom strip - - button = Gtk.Button.new_with_label(_('Select all')) - grid.attach(button, 0, 9, 1, 1) - button.set_hexpand(False) - # (Signal connect appears below) - - button2 = Gtk.Button.new_with_label(_('Select none')) - grid.attach(button2, 1, 9, 1, 1) - button2.set_hexpand(False) - # (Signal connect appears below) - - # (Signal connects from above) - self.checkbutton.connect('toggled', self.on_checkbutton_toggled) - self.checkbutton4.connect('toggled', self.on_checkbutton4_toggled) - self.checkbutton9.connect('toggled', self.on_checkbutton9_toggled) - self.checkbutton10.connect('toggled', self.on_checkbutton10_toggled) - self.checkbutton11.connect('toggled', self.on_checkbutton11_toggled) - self.checkbutton13.connect('toggled', self.on_checkbutton13_toggled) - button.connect('clicked', self.on_select_all_clicked) - button2.connect('clicked', self.on_select_none_clicked) - - # Display the dialogue window - self.show_all() - - - # Callback class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - When the 'Check that videos are not corrupted' button is toggled, - update the 'Delete corrupted videos...' button. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active(): - self.checkbutton2.set_active(False) - self.checkbutton2.set_sensitive(False) - - else: - self.checkbutton2.set_sensitive(True) - - - def on_checkbutton4_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - When the 'Delete downloaded video files' button is toggled, update the - 'Also delete...' button. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active(): - self.checkbutton5.set_active(False) - self.checkbutton5.set_sensitive(False) - - else: - self.checkbutton5.set_sensitive(True) - - - def on_checkbutton9_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - When the 'Move thumbnails into to own folder' button is toggled, update - other widgets. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active(): - self.checkbutton10.set_sensitive(True) - self.checkbutton11.set_sensitive(True) - self.checkbutton12.set_sensitive(True) - - else: - self.checkbutton10.set_active(False) - self.checkbutton10.set_sensitive(False) - self.checkbutton11.set_active(False) - self.checkbutton11.set_sensitive(False) - self.checkbutton12.set_active(False) - self.checkbutton12.set_sensitive(False) - - - def on_checkbutton10_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - When the 'Delete all thumbnail files' button is toggled, update other - widgets. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active(): - self.checkbutton11.set_sensitive(True) - self.checkbutton12.set_sensitive(True) - - else: - self.checkbutton11.set_active(False) - self.checkbutton11.set_sensitive(False) - self.checkbutton12.set_active(False) - self.checkbutton12.set_sensitive(False) - - - def on_checkbutton11_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - When the 'Delete all .webp thumbnails' button is toggled, update other - widgets. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active(): - self.checkbutton12.set_sensitive(True) - - else: - self.checkbutton12.set_active(False) - self.checkbutton12.set_sensitive(False) - - - def on_checkbutton13_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - When the 'Move other metadata files into own folder' button is toggled, - update other widgets. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if not checkbutton.get_active(): - - self.checkbutton14.set_sensitive(True) - self.checkbutton15.set_sensitive(True) - self.checkbutton16.set_sensitive(True) - - else: - - self.checkbutton14.set_active(False) - self.checkbutton15.set_active(False) - self.checkbutton16.set_active(False) - - self.checkbutton14.set_sensitive(False) - self.checkbutton15.set_sensitive(False) - self.checkbutton16.set_sensitive(False) - - - def on_select_all_clicked(self, button): - - """Called from a callback in self.__init__(). - - Select all checkbuttons. - - Args: - - button (Gtk.Button): The clicked widget - - """ - - self.checkbutton.set_active(True) - self.checkbutton2.set_active(True) - self.checkbutton3.set_active(True) - self.checkbutton4.set_active(True) - self.checkbutton5.set_active(True) - self.checkbutton6.set_active(True) - self.checkbutton7.set_active(True) - self.checkbutton8.set_active(True) - self.checkbutton9.set_active(True) -# self.checkbutton10.set_active(False) -# self.checkbutton11.set_active(False) -# self.checkbutton12.set_active(False) - self.checkbutton13.set_active(True) -# self.checkbutton14.set_active(False) -# self.checkbutton15.set_active(False) -# self.checkbutton16.set_active(False) - self.checkbutton17.set_active(True) - - - def on_select_none_clicked(self, button): - - """Called from a callback in self.__init__(). - - Unselect all checkbuttons. - - Args: - - button (Gtk.Button): The clicked widget - - """ - - self.checkbutton.set_active(False) - self.checkbutton2.set_active(False) - self.checkbutton3.set_active(False) - self.checkbutton4.set_active(False) - self.checkbutton5.set_active(False) - self.checkbutton6.set_active(False) - self.checkbutton7.set_active(False) - self.checkbutton8.set_active(False) - self.checkbutton9.set_active(False) - self.checkbutton10.set_active(False) - self.checkbutton12.set_active(False) - self.checkbutton13.set_active(False) - self.checkbutton14.set_active(False) - self.checkbutton15.set_active(False) - self.checkbutton16.set_active(False) - self.checkbutton17.set_active(False) - - if not mainapp.HAVE_MOVIEPY_FLAG \ - or self.main_win_obj.app_obj.refresh_moviepy_timeout == 0: - self.checkbutton.set_sensitive(False) - self.checkbutton2.set_sensitive(False) diff --git a/build/lib/tartube/media.py b/build/lib/tartube/media.py deleted file mode 100644 index bc48f4a6..00000000 --- a/build/lib/tartube/media.py +++ /dev/null @@ -1,4652 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Media data classes.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import datetime -import functools -import os -import re -import time - - -# Import our modules -import formats -import mainapp -import utils -# Use same gettext translations -from mainapp import _ - - -# Classes - - -class GenericMedia(object): - - """Base python class inherited by media.Video, media.Channel, - media.Playlist and media.Folder.""" - - - # Public class methods - - - def get_natural_name(self, name): - - """Converts the specified name so it is suitable for so-called - 'natural' sorting, adding leading zeroes and reducing to all-lower - case. - - Based on the algorithm by Stephen Quan: - https://stackoverflow.com/questions/4836710/ - is-there-a-built-in-function-for-string-natural-sort - - Args: - name (str): The name to process - - """ - - return re.sub( - r'\d+', - lambda m: m.group(0).rjust(10, '0'), name - ).lower() - - - def get_type(self): - - if isinstance(self, Channel): - return 'channel' - elif isinstance(self, Playlist): - return 'playlist' - elif isinstance(self, Folder): - return 'folder' - else: - return 'video' - - - def get_translated_type(self, upper_flag=False): - - if not upper_flag: - - if isinstance(self, Channel): - return _('channel') - elif isinstance(self, Playlist): - return _('playlist') - elif isinstance(self, Folder): - return _('folder') - else: - return _('video') - - else: - - if isinstance(self, Channel): - return _('Channel') - elif isinstance(self, Playlist): - return _('Playlist') - elif isinstance(self, Folder): - return _('Folder') - else: - return _('Video') - - - # Set accessors - - - def set_error(self, msg): - - # The media.Folder object has no error/warning IVs (and shouldn't - # receive any error/warning messages) - if not isinstance(self, Folder): - self.error_list.append(msg) - - - def reset_error_warning(self): - - # The media.Folder object has no error/warning IVs (and shouldn't - # receive any error/warning messages) - if not isinstance(self, Folder): - self.error_list = [] - self.warning_list = [] - - - def set_fav_flag(self, flag): - - if flag: - self.fav_flag = True - else: - self.fav_flag = False - - - def set_nickname(self, nickname): - - if nickname is None or nickname == '': - self.nickname = self.name - else: - self.nickname = nickname - - self.natname = self.get_natural_name(self.nickname) - - - def set_options_obj(self, options_obj): - - self.options_obj = options_obj - - - def reset_options_obj(self): - - self.options_obj = None - - - def set_parent_obj(self, parent_obj): - - self.parent_obj = parent_obj - - - def set_warning(self, msg): - - # The media.Folder object has no error/warning IVs (and shouldn't - # receive any error/warning messages) - if not isinstance(self, Folder): - self.warning_list.append(msg) - - -class GenericContainer(GenericMedia): - - """Base python class inherited by media.Channel, media.Playlist and - media.Folder.""" - - - # Public class methods - - - def check_duplicate_video(self, source): - - """Can be called by anything. - - Before adding a video to a parent channel/playlist/folder, this - function can be called to check for videos with a duplicate URL. - - Args: - - source (str): The video URL to check - - Return values: - - True if any of the child media.Video objects in this folder have - the same source URL; False otherwise - - """ - - for child_obj in self.child_list: - - if isinstance(child_obj, Video) \ - and child_obj.source is not None \ - and child_obj.source == source: - # Duplicate found - return True - - # No duplicate found - return False - - - def check_duplicate_video_by_path(self, app_obj, path): - - """Can be called by anything. - - A modified version of self.check_duplicate_video(), which checks for - media.Video objects with duplicate paths, instead of dupliate URLs. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - path (str): The full file path to check - - Return values: - - True if any of the child media.Video objects in this folder have - the same source URL; False otherwise - - """ - - for child_obj in self.child_list: - - if isinstance(child_obj, Video) \ - and child_obj.file_name is not None: - - child_path = child_obj.get_actual_path(app_obj) - if child_path is not None and child_path == path: - - # Duplicate found - return True - - # No duplicate found - return False - - - def compile_all_containers(self, container_list): - - """Can be called by anything. Subsequently called by this function - recursively. - - Appends to the specified list this container, then calls all this - function recursively for all media.Channel, media.Playlist and - media.Folder objects, so they too can be added to the list. - - Args: - - container_list (list): A list of media.Channel, media.Playlist and - media.Folder objects - - Return values: - - The modified container_list - - """ - - container_list.append(self) - for child_obj in self.child_list: - - if not isinstance(child_obj, Video): - child_obj.compile_all_containers(container_list) - - return container_list - - - def compile_all_videos(self, video_list): - - """Can be called by anything. Subsequently called by this function - recursively. - - Appends to the specified list all child objects that are media.Video - objects, then calls this function recursively for all other child - objects, so they can add their children too. - - Args: - - video_list (list): A list of media.Video objects - - Return values: - - The modified video_list - - """ - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - video_list.append(child_obj) - else: - child_obj.compile_all_videos(video_list) - - return video_list - - - def compile_all_videos_by_frequency(self, data_type, period, - frequency_dict): - - """Can be called by anything, but mostly called by - config.GenericConfigWin.on_button_draw_graph_clicked(). - - Compile a dictionary of download times for each video in the container - (including those in sub-folders, channels and playlists). - - The dictionary shows the frequency of downloads for each specified - time period (for example, a day, a month, etc). - - Args: - - data_type (str): 'receive' to compile video frequencies by - receive (download) time, 'download' to compile by download time - - period (int): A time period, in seconds (e.g. 86400 for a day) - - frequency_dict (dict): The dictionary compiled so far (empty - unless multiple containers are being combined into a single - dictionary) - - Return values: - - Dictionary in the form - frequency_dict[time_period_number] = number_of_videos - - ...for example, if 10 videos were downloaded today, and 20 were - downloaded yesterday, and the period is 86400 (representing one - day), then - frequency_dict[0] = 10 - frequency_dict[1] = 20 - - """ - - now = time.time() - video_list = self.compile_all_videos( [] ) - - if data_type == 'receive': - - for video_obj in video_list: - - if video_obj.receive_time: - - time_units = int((now - video_obj.receive_time) / period) - - if time_units in frequency_dict: - frequency_dict[time_units] += 1 - else: - frequency_dict[time_units] = 1 - - else: - - for video_obj in video_list: - - if video_obj.upload_time: - - time_units = int((now - video_obj.upload_time) / period) - - if time_units in frequency_dict: - frequency_dict[time_units] += 1 - else: - frequency_dict[time_units] = 1 - - return frequency_dict - - - def compile_all_videos_by_size(self, frequency_dict): - - """Can be called by anything, but mostly called by - config.GenericConfigWin.on_button_draw_graph_clicked(). - - This functions specifies a limited set of file sizes. The set has been - chosen to produce aesthetic graphs. - - Compile a dictionary containing the number of videos for each size - range. Videos whose file size is not known are ignored. - - Args: - - frequency_dict (dict): The dictionary compiled so far (empty - unless multiple containers are being combined into a single - dictionary) - - Return values: - - Dictionary in the form - frequency_dict[range_label] = number_of_videos - - """ - - for video_obj in self.compile_all_videos( [] ): - - if video_obj.file_size is not None: - - # NB If these labels are changed, when the corresponding - # literal values in - # config.GenericConfigWin.on_button_draw_graph_clicked() must - # be changed too - if video_obj.file_size < 10_000_000: - label = '10MB' - elif video_obj.file_size < 25_000_000: - label = '25MB' - elif video_obj.file_size < 50_000_000: - label = '50MB' - elif video_obj.file_size < 100_000_000: - label = '100MB' - elif video_obj.file_size < 250_000_000: - label = '250MB' - elif video_obj.file_size < 500_000_000: - label = '500MB' - elif video_obj.file_size < 1000_000_000: - label = '1GB' - elif video_obj.file_size < 2000_000_000: - label = '2GB' - elif video_obj.file_size < 5000_000_000: - label = '5GB' - else: - label = '5GB+' - - if label in frequency_dict: - frequency_dict[label] += 1 - else: - frequency_dict[label] = 1 - - return frequency_dict - - - def compile_all_videos_by_duration(self, frequency_dict): - - """Can be called by anything, but mostly called by - config.GenericConfigWin.on_button_draw_graph_clicked(). - - This functions specifies a limited set of video durations (in seconds). - The set has been chosen to produce aesthetic graphs. - - Compile a dictionary containing the number of videos for each duration - range. Videos whose duration is not known are ignored. - - Args: - - frequency_dict (dict): The dictionary compiled so far (empty - unless multiple containers are being combined into a single - dictionary) - - Return values: - - Dictionary in the form - frequency_dict[range_label] = number_of_videos - - """ - - for video_obj in self.compile_all_videos( [] ): - - if video_obj.duration is not None: - - # NB If these labels are changed, when the corresponding - # literal values in - # config.GenericConfigWin.on_button_draw_graph_clicked() must - # be changed too - if video_obj.duration < 10: - label = '10s' - elif video_obj.duration < 60: - label = '1m' - elif video_obj.duration < 300: - label = '5m' - elif video_obj.duration < 600: - label = '10m' - elif video_obj.duration < 1200: - label = '20m' - elif video_obj.duration < 1800: - label = '30m' - elif video_obj.duration < 3600: - label = '1h' - elif video_obj.duration < 7200: - label = '2h' - elif video_obj.duration < 18000: - label = '5h' - else: - label = '5h+' - - if label in frequency_dict: - frequency_dict[label] += 1 - else: - frequency_dict[label] = 1 - - return frequency_dict - - - def count_descendants(self, count_list): - - """Can be called by anything. Subsequently called by this function - recursively. - - Counts the number of child objects, and then calls this function - recursively in those child objects to count their child objects. - - Args: - - count_list (list): A list representing the child objects counted - so far. List in the form - ( - total_count, video_count, channel_count, - playlist_count, folder_count, - ) - - Return values: - - The modified count_list - - """ - - for child_obj in self.child_list: - - count_list[0] += 1 - - if isinstance(child_obj, Video): - count_list[1] += 1 - else: - count_list = child_obj.count_descendants(count_list) - if isinstance(child_obj, Channel): - count_list[2] += 1 - elif isinstance(child_obj, Playlist): - count_list[3] += 1 - else: - count_list[4] += 1 - - return count_list - - - def del_child(self, child_obj): - - """Can be called by anything. - - Deletes a child object from self.child_list, first checking that it's - actually a child of this object. - - Args: - - child_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The child object to delete - - Return values: - - True if the child object was deleted, False if the specified object - was not a child of this object - - """ - - # Check this is really one of our children - if not child_obj in self.child_list: - return False - - else: - self.child_list.remove(child_obj) - - # Git #169, v2.2.026. A user reports that the counts can fall below - # 0. The authors can't reproduce the problem, but we can still - # prevent negative values - if isinstance(child_obj, Video): - self.vid_count -= 1 - if self.vid_count < 0: - self.vid_count = 0 - - if child_obj.bookmark_flag: - self.bookmark_count -= 1 - if self.bookmark_count < 0: - self.bookmark_count = 0 - - if child_obj.dl_flag: - self.dl_count -= 1 - if self.dl_count < 0: - self.dl_count = 0 - - if child_obj.fav_flag: - self.fav_count -= 1 - if self.fav_count < 0: - self.fav_count = 0 - - if child_obj.live_mode: - self.live_count -= 1 - if self.live_count < 0: - self.live_count = 0 - - if child_obj.missing_flag: - self.missing_count -= 1 - if self.missing_count < 0: - self.missing_count = 0 - - if child_obj.new_flag: - self.new_count -= 1 - if self.new_count < 0: - self.new_count = 0 - - if child_obj.waiting_flag: - self.waiting_count -= 1 - if self.waiting_count < 0: - self.waiting_count = 0 - - return True - - - def fetch_tooltip_text(self, app_obj, max_length=None, - show_error_flag=False): - - """Can be called by anything. - - Returns a string to be used as a tooltip for this channel, playlist or - folder. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - max_length (int or None): If specified, the maximum line length, in - characters - - show_error_flag (bool): If True, show the first error/warning - generated by the channel/playlist/folder - - Return values: - - Text containing the channel/playlist/folder directory path and - the source (except for folders), ready for display in a tooltip - - """ - - text = '#' + str(self.dbid) + ': ' + self.name + '\n\n' - - if not isinstance(self, Folder): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Source = video/channel/playlist URL', - ) - - text += _('Source:') + '\n' - if self.source is None: - text += '<' + _('unknown') + '>' - else: - text += self.source - - text += '\n\n' - - text += _('Location:') + '\n' - - location = self.get_default_dir(app_obj) - if location is None: - text += '<' + _('unknown') + '>' - else: - text += location - - if self.external_dir is not None: - - text += '\n\n' + _('Download destination:') + ' ' - if self.dbid in app_obj.container_unavailable_dict: - text += '(' + _('unavailable') + ')' - else: - text += self.external_dir - - elif self.master_dbid != self.dbid: - - dest_obj = app_obj.media_reg_dict[self.master_dbid] - text += '\n\n' + _('Download destination:') + ' ' + dest_obj.name - - # Show the first error/warning - if show_error_flag \ - and app_obj.show_tooltips_extra_flag \ - and not isinstance(self, Folder): - - error_warning_list = self.error_list + self.warning_list - length = len (error_warning_list) - if error_warning_list: - - text += '\n\n' + _('Errors\Warnings') + ' (' \ - + str(length) + '):\n' + str(error_warning_list.pop(0)) - - if length > 1: - text += '\n...' - - # Need to escape question marks or we'll get a markup error - text = re.sub('&', '&', text) - - # Apply a maximum line length, if required - if max_length is not None: - text = utils.tidy_up_long_descrip(text, max_length) - - return text - - - def find_matching_video(self, app_obj, name): - - """Can be called by anything. - - Checks all of this object's child objects, looking for a media.Video - object with a matching name. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - name (str): The name of the media.Video object to find - - Return values: - - The first matching media.Video object found, or None if no matching - videos are found. - - """ - - method = app_obj.match_method - first = app_obj.match_first_chars - ignore = app_obj.match_ignore_chars * -1 - - # Defend against two different version of a name from the same video, - # one with punctuation marks stripped away, and double quotes - # converted to single quotes (thanks, YouTube!) by replacing those - # characters with whitespace. Also strip leading/trailing whitespace, - # etc - test_name = self.strip_video_name(name) - - # Match the name against child media.Video objects - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - - child_name = self.strip_video_name(child_obj.name) - child_nickname = self.strip_video_name(child_obj.nickname) - - if ( - method == 'exact_match' \ - and ( - child_name == test_name \ - or ( - app_obj.match_nickname_flag \ - and child_nickname == test_name - ) - ) - ) or ( - method == 'match_first' \ - and ( - child_name[:first] == test_name[:first] \ - or ( - app_obj.match_nickname_flag \ - and child_nickname[:first] == test_name[:first] - ) - ) - ) or ( - method == 'ignore_last' \ - and ( - child_name[:ignore] == test_name[:ignore] \ - or ( - app_obj.match_nickname_flag \ - and child_name[:ignore] == test_name[:ignore] - ) - ) - ): - return child_obj - - - # No matches found - return None - - - def get_depth(self): - - """Can be called by anything. - - There is a limit to the depth of the media registry (a maximum number - of levels). - - This function finds the level occupied by this container object and - returns it. - - If this object has no parent, it is at level 1. If it has a parent - object, and the parent itself has no parent, this object is at level 2. - - Return values: - - The container object's level - - """ - - if self.parent_obj is None: - return 1 - - else: - level = 1 - parent_obj = self.parent_obj - - while parent_obj is not None: - level += 1 - parent_obj = parent_obj.parent_obj - - return level - - - def get_visible_videos(self, app_obj): - - """Can be called by anything. - - Returns a list of everything in self.child_list() which is a - media.Video object that should be visible in the Video Catalogue, - according to current settings. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - Return values: - - A filtered list of videos - - """ - - return_list = [] - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - if ( - app_obj.catalogue_draw_downloaded_flag \ - and child_obj.dl_flag - ) or ( - app_obj.catalogue_draw_undownloaded_flag \ - and not child_obj.dl_flag - ) or ( - app_obj.catalogue_draw_blocked_flag \ - and child_obj.block_flag - ): - return_list.append(child_obj) - - return return_list - - - def is_hidden(self): - - """Called by mainwin.MainWin.video_index_add_row() and - .video_index_select_row(). - - If this is a hidden media.Folder object, return True. - - If the parent media.Folder (or the parent's parent, and so on) is - hidden, return True. - - Otherwise, return False. (media.Channel and media.Playlist objects - can't be hidden directly.) - - Return values: - - True or False - - """ - - if isinstance(self, Folder) and self.hidden_flag: - return True - - parent_obj = self.parent_obj - - while parent_obj: - if isinstance(parent_obj, Folder) and parent_obj.hidden_flag: - return True - else: - parent_obj = parent_obj.parent_obj - - return False - - - def prepare_export(self, app_obj, include_video_flag, include_channel_flag, - include_playlist_flag): - - """Called by mainapp.TartubeApp.export_from_db(). Subsequently called - by this function recursively. - - Creates the dictionary, to be saved as a JSON file, described in the - comments to that function. This function is called when we want to - preserve the folder structure of the Tartube database. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - include_video_flag (bool): If True, include videos. If False, don't - include them - - include_channel_flag (bool): If True, include channels (and their - videos, if allowed). If False, ignore them - - include_playlist_flag (bool): If True, include playlists (and their - videos, if allowed). If False, ignore them - - Return values: - - return_dict (dict): A dictionary described in the comments in the - calling function - - """ - - # Ignore the types of media data object that we don't require (and all - # of their children) - # This function should not be called for media.Video objects - # This function can be called for fixed folders, but apart from the - # 'Unsorted Videos' and 'Video Clips' folders, we ignore them - media_type = self.get_type() - - if media_type == 'video' \ - or (media_type == 'channel' and not include_channel_flag) \ - or (media_type == 'playlist' and not include_playlist_flag) \ - or ( - media_type == 'folder' - and self.fixed_flag - and self != app_obj.fixed_misc_folder - and self != app_obj.fixed_clips_folder - ): - return {} - - # This dictionary contains values for the children of this object - db_dict = {} - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - - # (Don't bother exporting a video whose source URL/file is not - # known) - if include_video_flag \ - and child_obj.source is not None \ - and child_obj.file_name is not None \ - and child_obj.file_ext is not None: - - mini_dict = { - 'type': 'video', - 'dbid': child_obj.dbid, - 'vid': child_obj.vid, - 'name': child_obj.name, - 'nickname': None, - 'file': child_obj.file_name + child_obj.file_ext, - 'source': child_obj.source, - 'db_dict': {}, - } - - db_dict[child_obj.dbid] = mini_dict - - else: - - mini_dict = child_obj.prepare_export( - app_obj, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - if mini_dict: - db_dict[child_obj.dbid] = mini_dict - - # This dictionary contains values for this object, and for the children - # of this object - return_dict = { - 'type': media_type, - 'dbid': self.dbid, - 'vid': None, - 'name': self.name, - 'nickname': self.nickname, - 'file': None, - 'source': None, - 'db_dict': db_dict, - } - - if media_type != 'folder': - return_dict['source'] = self.source - - # Procedure complete - return return_dict - - - def prepare_flat_export(self, app_obj, db_dict, include_video_flag, - include_channel_flag, include_playlist_flag): - - """Called by mainapp.TartubeApp.export_from_db(). Subsequently called - by this function recursively. - - Creates the dictionary, to be saved as a JSON file, described in the - comments to that function. This function is called when we don't want - to preserve the folder structure of the Tartube database. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - db_dict (dict): The dictionary described in the comments in the - calling function - - include_video_flag (bool): If True, include videos. If False, don't - include them - - include_channel_flag (bool): If True, include channels (and their - videos, if allowed). If False, ignore them - - include_playlist_flag (bool): If True, include playlists (and their - videos, if allowed). If False, ignore them - - Return values: - - db_dict (dict): The modified dictionary - - """ - - # Ignore the types of media data object that we don't require (and all - # of their children) - # This function should not be called for media.Video objects - # This function can be called for fixed folders, but apart from the - # 'Unsorted Videos' and 'Video Clips' folders, we ignore them - media_type = self.get_type() - - if media_type == 'video' \ - or (media_type == 'channel' and not include_channel_flag) \ - or (media_type == 'playlist' and not include_playlist_flag) \ - or ( - media_type == 'folder' - and self.fixed_flag - and self != app_obj.fixed_misc_folder - and self != app_obj.fixed_clips_folder - ): - return {} - - # Add values to the dictionary - if media_type == 'channel' or media_type == 'playlist': - - child_dict = {} - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - - # (Don't bother exporting a video whose source URL/file is - # not known) - if include_video_flag \ - and child_obj.source is not None \ - and child_obj.file_name is not None \ - and child_obj.file_ext is not None: - - child_mini_dict = { - 'type': 'video', - 'dbid': child_obj.dbid, - 'vid': child_obj.vid, - 'name': child_obj.name, - 'nickname': None, - 'file': child_obj.file_name + child_obj.file_ext, - 'source': child_obj.source, - 'db_dict': {}, - } - - child_dict[child_obj.dbid] = child_mini_dict - - else: - - db_dict = child_obj.prepare_flat_export( - app_obj, - db_dict, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - mini_dict = { - 'type': media_type, - 'dbid': self.dbid, - 'vid': None, - 'name': self.name, - 'nickname': self.nickname, - 'file': None, - 'source': self.source, - 'db_dict': child_dict, - } - - db_dict[self.dbid] = mini_dict - - elif media_type == 'folder': - - for child_obj in self.child_list: - - if not isinstance(child_obj, Video): - - db_dict = child_obj.prepare_flat_export( - app_obj, - db_dict, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - # Procedure complete - return db_dict - - - def recalculate_counts(self): - - """Can be called by anything. - - Recalculates all count IVs. - """ - - self.vid_count = 0 - self.bookmark_count = 0 - self.dl_count = 0 - self.fav_count = 0 - self.live_count = 0 - self.missing_count = 0 - self.new_count = 0 - self.waiting_count = 0 - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - self.vid_count += 1 - - if child_obj.bookmark_flag: - self.bookmark_count += 1 - - if child_obj.dl_flag: - self.dl_count += 1 - - if child_obj.fav_flag: - self.fav_count += 1 - - if child_obj.live_mode: - self.live_count += 1 - - if child_obj.missing_flag: - self.missing_count += 1 - - if child_obj.new_flag: - self.new_count += 1 - - if child_obj.waiting_flag: - self.waiting_count += 1 - - - def strip_video_name(self, name): - - """Called by self.find_matching_video(). - - The specified name is the .name or .nickname of a child media.Video - object; or otherwise a string to be matched against the .name or - .nickname of a child media.Video object. - - Defend against two different versions of a name from the same video, - one with punctuation marks stripped away, and double quotes converted - to single quotes (thanks, YouTube!) by replacing those characters with - whitespace. - - Also remove punctuation, underlines and leading/trailing whitespace. - - Args: - - name (str): The name to strip - - """ - - # (After extensive testing, this is the only regex sequence I could - # find that worked as inteded) - test_name = name[:] - - # Remove punctuation - test_name = re.sub(r'\W+', ' ', test_name, flags=re.UNICODE) - # Also need to replace underline characters - test_name = re.sub(r'[\_\s]+', ' ', test_name) - # Also need to remove leading/trailing whitespace, in case the original - # video name started/ended with a question mark or something like - # that - test_name = re.sub(r'^\s+', '', test_name) - test_name = re.sub(r'\s+$', '', test_name) - - return test_name - - - def test_counts(self): - - """Can be called by anything. - - Recalculates all count IVs, and compares them against the container's - actual counts. - - Return values: - - True if there is a discrepancy; False if the container's actual - counts seem to be correct - - """ - - vid_count = 0 - bookmark_count = 0 - dl_count = 0 - fav_count = 0 - live_count = 0 - missing_count = 0 - new_count = 0 - waiting_count = 0 - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - vid_count += 1 - - if child_obj.bookmark_flag: - bookmark_count += 1 - - if child_obj.dl_flag: - dl_count += 1 - - if child_obj.fav_flag: - fav_count += 1 - - if child_obj.live_mode: - live_count += 1 - - if child_obj.missing_flag: - missing_count += 1 - - if child_obj.new_flag: - new_count += 1 - - if child_obj.waiting_flag: - waiting_count += 1 - - if vid_count != self.vid_count \ - or bookmark_count != self.bookmark_count \ - or dl_count != self.dl_count \ - or fav_count != self.fav_count \ - or live_count != self.live_count \ - or missing_count != self.missing_count \ - or new_count != self.new_count \ - or waiting_count != self.waiting_count: - return True - - else: - return False - - - # Set accessors - - - def reset_counts(self, vid_count, bookmark_count, dl_count, fav_count, - live_count, missing_count, new_count, waiting_count): - - """Called by mainapp.TartubeApp.update_db(). - - When a database created by an earlier version of Tartube is loaded, - the calling function updates IVs as required. - - This function is called if this object's video counts need to be - changed. - """ - - self.vid_count = vid_count - self.bookmark_count = bookmark_count - self.dl_count = dl_count - self.fav_count = fav_count - self.live_count = live_count - self.missing_count = missing_count - self.new_count = new_count - self.waiting_count = waiting_count - - - def inc_bookmark_count(self): - - self.bookmark_count += 1 - - - def dec_bookmark_count(self): - - self.bookmark_count -= 1 - if self.bookmark_count < 0: - self.bookmark_count = 0 - - - def inc_dl_count(self): - - self.dl_count += 1 - - - def dec_dl_count(self): - - self.dl_count -= 1 - if self.dl_count < 0: - self.dl_count = 0 - - - def set_dl_disable_flag(self, flag): - - if flag: - self.dl_disable_flag = True - # This group of flags are mutually exclusive - self.dl_no_db_flag = False - self.dl_sim_flag = False - - else: - self.dl_disable_flag = False - - - def set_dl_no_db_flag(self, flag): - - if flag: - self.dl_no_db_flag = True - # This group of flags are mutually exclusive - self.dl_disable_flag = False - self.dl_sim_flag = False - - else: - self.dl_no_db_flag = False - - - def set_dl_sim_flag(self, flag): - - if flag: - self.dl_sim_flag = True - # This group of flags are mutually exclusive - self.dl_no_db_flag = False - self.dl_disable_flag = False - - else: - self.dl_sim_flag = False - - - def set_external_dir(self, app_obj, external_dir): - - self.external_dir = external_dir - if external_dir is not None: - - # If the directory does not exist, try to create it - if not os.path.isdir(external_dir) \ - and not app_obj.make_directory(external_dir): - - # Failed - return False - - # If a semaphore file does not exist in the external directory, - # create one - if not app_obj.make_semaphore_file(external_dir): - - # Failed - return False - - return True - - - def inc_fav_count(self): - - self.fav_count += 1 - - - def dec_fav_count(self): - - self.fav_count -= 1 - if self.fav_count < 0: - self.fav_count = 0 - - - def inc_live_count(self): - - self.live_count += 1 - - - def dec_live_count(self): - - self.live_count -= 1 - if self.live_count < 0: - self.live_count = 0 - - - def set_master_dbid(self, app_obj, dbid): - - if dbid == self.master_dbid: - # No change to the current value - return - - else: - - # Update the old alternative download destination - if self.master_dbid != self.dbid: - - # (If mainapp.TartubeApp.fix_integrity_db() is fixing an - # error, the old destination object might not exist) - if self.master_dbid in app_obj.media_reg_dict: - old_dest_obj = app_obj.media_reg_dict[self.master_dbid] - old_dest_obj.del_slave_dbid(self.dbid) - - # Update this object's IV - self.master_dbid = dbid - - if self.master_dbid != self.dbid: - - # Update the new alternative download destination - new_dest_obj = app_obj.media_reg_dict[self.master_dbid] - new_dest_obj.add_slave_dbid(self.dbid) - - - def reset_master_dbid(self): - - self.master_dbid = self.dbid - - - def inc_missing_count(self): - - self.missing_count += 1 - - - def dec_missing_count(self): - - self.missing_count -= 1 - if self.missing_count < 0: - self.missing_count = 0 - - - def inc_new_count(self): - - self.new_count += 1 - - - def dec_new_count(self): - - self.new_count -= 1 - if self.new_count < 0: - self.new_count = 0 - - - def inc_waiting_count(self): - - self.waiting_count += 1 - - - def dec_waiting_count(self): - - self.waiting_count -= 1 - if self.waiting_count < 0: - self.waiting_count = 0 - - - def add_slave_dbid(self, dbid): - - """Called by self.set_master_dbid() only.""" - - # (Failsafe: don't add the same value to self.slave_dbid_list) - match_flag = False - for slave_dbid in self.slave_dbid_list: - if slave_dbid == dbid: - match_flag = True - break - - if not match_flag: - self.slave_dbid_list.append(dbid) - - - def del_slave_dbid(self, dbid): - - """Called by mainapp.TartubeApp.fix_integrity_db() or by - self.set_master_dbid() only.""" - - new_list = [] - - for slave_dbid in self.slave_dbid_list: - if slave_dbid != dbid: - new_list.append(slave_dbid) - - self.slave_dbid_list = new_list.copy() - - - def set_name(self, name): - - # Update the nickname at the same time, if it has the same value as - # this object's name - if self.nickname == self.name: - self.nickname = name - - self.name = name - - - # Get accessors - - - def get_actual_dir(self, app_obj, new_name=None): - - """Can be called by anything. - - Fetches the full path to the sub-directory actually used by this - channel, playlist or folder. - - If self.external_dir is set, returns it. - - Otherwise, if self.dbid and self.master_dbid are the same, then files - are downloaded to the default location; the sub-directory belonging to - the channel/playlist/folder. In that case, this function returns the - same value as self.get_default_dir(). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. This function returns that sub-directory. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - Optional args: - - new_name (str): If specified, fetches the full path to the - sub-directory that would be used by this channel, playlist or - folder, if it were renamed to 'new_name'. If not specified, the - channel/playlist/folder's actual name is used - - Return values: - - The full path to the sub-directory - - """ - - if self.external_dir is not None: - - return self.external_dir - - elif self.master_dbid != self.dbid: - - master_obj = app_obj.media_reg_dict[self.master_dbid] - return master_obj.get_default_dir(app_obj, new_name) - - else: - - return self.get_default_dir(app_obj, new_name) - - - def get_default_dir(self, app_obj, new_name=None): - - """Can be called by anything. - - Fetches the full path to the sub-directory used by this channel, - playlist or folder by default. - - If self.external_dir is set, or if self.master_dbid is not the same as - self.dbid, then files are actually downloaded to a non-default - location. To get the actual download location, call - self.get_actual_dir(). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - Optional args: - - new_name (str): If specified, fetches the full path to the - sub-directory that would be used by this channel, playlist or - folder, if it were renamed to 'new_name'. If not specified, the - channel/playlist/folder's actual name is used - - Return values: - - The full path to the sub-directory - - """ - - if new_name is not None: - dir_list = [new_name] - else: - dir_list = [self.name] - - obj = self - while obj.parent_obj: - - obj = obj.parent_obj - dir_list.insert(0, obj.name) - - return os.path.abspath(os.path.join(app_obj.downloads_dir, *dir_list)) - - - def get_relative_actual_dir(self, app_obj, new_name=None): - - """Can be called by anything. - - Fetches the path to the sub-directory used by this channel, playlist or - folder, relative to mainapp.TartubeApp.downloads_dir. - - If self.dbid and self.master_dbid are the same, then files are - downloaded to the default location; the sub-directory belonging to the - channel/playlist/folder. In that case, this function returns the same - value as self.get_default_dir(). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. This function returns that sub-directory. - - Exception: if an external directory is set, returns None (as the - external directory is always outside Tartube's data directory). THe - calling code must check for that - - Args: - - app_obj (mainapp.TartubeApp): The main application - - new_name (str): If specified, fetches the relative path to the - sub-directory that would be used by this channel, playlist or - folder, if it were renamed to 'new_name'. If not specified, the - channel/playlist/folder's actual name is used - - Return values: - - The path to the sub-directory relative to - mainapp.TartubeApp.downloads_dir, or None if an external - directory has been set - - """ - - if self.master_dbid != self.dbid: - - master_obj = app_obj.media_reg_dict[self.master_dbid] - return master_obj.get_relative_default_dir(app_obj, new_name) - - else: - - return self.get_relative_default_dir(app_obj, new_name) - - - def get_relative_default_dir(self, new_name=None): - - """Can be called by anything. - - Fetches the path to the sub-directory used by this channel, playlist or - folder by default, relative to mainapp.TartubeApp.downloads_dir. - - If self.external_dir is set, or if self.master_dbid is not the same as - self.dbid, then files are actually downloaded to a non-default - location. To get the actual relative location, call - self.get_relative_actual_dir(). - - Args: - - new_name (str): If specified, fetches the relative path to the - sub-directory that would be used by this channel, playlist or - folder, if it were renamed to 'new_name'. If not specified, the - channel/playlist/folder's actual name is used - - Return values: - - The path to the sub-directory relative to - mainapp.TartubeApp.downloads_dir - - """ - - if new_name is not None: - dir_list = [new_name] - else: - dir_list = [self.name] - - obj = self - while obj.parent_obj: - - obj = obj.parent_obj - dir_list.insert(0, obj.name) - - return os.path.join(*dir_list) - - -class GenericRemoteContainer(GenericContainer): - - """Base python class inherited by media.Channel and media.Playlist.""" - - - # Public class methods - - - def add_child(self, app_obj, child_obj, no_sort_flag=False): - - """Can be called by anything. - - Adds a child media data object, which must be a media.Video object. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - child_obj (media.Video): The child object - - no_sort_flag (bool): True when the calling code wants to delay - sorting the parent container object, for some reason; False if - not - - """ - - # Only media.Video objects can be added to a channel or playlist as a - # child object. Also, check this is not already a child object - if isinstance(child_obj, Video) or child_obj in self.child_list: - - self.child_list.append(child_obj) - if not no_sort_flag: - self.sort_children(app_obj) - - if isinstance(child_obj, Video): - self.vid_count += 1 - - - def sort_children(self, app_obj): - - """Can be called by anything. For example, called by self.add_child(). - - Sorts the child media.Video objects using the standard video-sorting - function. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - # Sort a copy of the list to prevent 'list modified during sort' - # errors - while True: - - copy_list = self.child_list.copy() - copy_list.sort(key=functools.cmp_to_key(app_obj.video_compare)) - - if len(copy_list) == len(self.child_list): - self.child_list = copy_list.copy() - break - - - # Set accessors - - - def clone_properties(self, other_obj): - - """Called by mainapp.TartubeApp.convert_remote_container() only. - - Copies properties from a media data object (about to be deleted) to - this media data object. - - Some properties are handled by the calling function; this function - handles the rest of them. - - Args: - - other_obj (media.Channel, media.Playlist): The object whose - properties should be copied - - """ - - self.options_obj = other_obj.options_obj - self.nickname = other_obj.nickname - self.source = other_obj.source - self.dl_no_db_flag = other_obj.dl_no_db_flag - self.dl_disable_flag = other_obj.dl_disable_flag - self.dl_sim_flag = other_obj.dl_sim_flag - self.fav_flag = other_obj.fav_flag - - self.bookmark_count = other_obj.bookmark_count - self.dl_count = other_obj.dl_count - self.fav_count = other_obj.fav_count - self.live_count = other_obj.live_count - self.missing_count = other_obj.missing_count - self.new_count = other_obj.new_count - self.waiting_count = other_obj.waiting_count - - self.error_list = other_obj.error_list.copy() - self.warning_list = other_obj.warning_list.copy() - - - def reset_rss(self): - - self.rss = None - - - def set_playlist_id(self, playlist_id, playlist_title): - - # (Don't overwrite an existing entry unless the existing name is blank) - if not playlist_id in self.playlist_id_dict \ - or self.playlist_id_dict[playlist_id] is None: - self.playlist_id_dict[playlist_id] = playlist_title - - - def reset_playlist_id(self): - - self.playlist_id_dict = {} - - - def extract_playlist_id(self, app_obj): - - """Can be called by anything, but mostly called by - config.ChannelPlaylistEditWin.on_reset_assoc_playlist_button_clicked(). - - Reads the .info.json file for every child media.Video object, extracts - the 'playlist_id' and 'playlist_title' fields, and uses them to update - self.playlist_id_dict. - - Does not check mainapp.TartubeApp.store_playlist_id_flag; the calling - code should do that, if necessary. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - self.playlist_id_dict = {} - - for child_obj in self.child_list: - - json_path = child_obj.check_actual_path_by_ext( - app_obj, - '.info.json', - ) - if json_path is not None: - - json_dict = app_obj.file_manager_obj.load_json(json_path) - if 'playlist_id' in json_dict: - - if 'playlist_title' in json_dict: - self.playlist_id_dict[json_dict['playlist_id']] \ - = json_dict['playlist_title'] - - else: - self.playlist_id_dict[json_dict['playlist_id']] = None - - - def set_source(self, source): - - self.source = source - self.enhanced = utils.is_enhanced(source) - self.update_rss_from_url(source) - - -class Video(GenericMedia): - - """Python class that handles an individual video. - - Args: - - app_obj (mainapp.TartubeApp): The main application (not stored as an - IV) - - dbid (int): A unique ID for this media data object - - name (str): The video name - - parent_obj (media.Channel, media.Playlist, media.Folder): The parent - media data object, if any - - options_obj (options.OptionsManager): The object specifying download - options for this video, if any - - no_sort_flag (bool): True when the calling code wants to delay sorting - the parent container object, for some reason; False if not - - """ - - - # Standard class methods - - - def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None, - no_sort_flag=False, dummy_flag=False): - - # IV list - class objects - # ----------------------- - # The parent object (a media.Channel, media.Playlist or media.Folder - # object. All media.Video objects have a parent) - self.parent_obj = parent_obj - # The options.OptionsManager object that specifies how this video is - # downloaded (or None, if the parent's options.OptionsManager object - # should be used instead) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Unique media data object ID (an integer) - # When a download operation is launched from the Classic Mode tab, the - # code creates a series of dummy media.Video objects that aren't - # added to the media data registry. Those dummy objects have negative - # dbids - self.dbid = dbid - - # Video name - self.name = name - # Video nickname (displayed in the Video Catalogue) - # If the video's JSON data has been fetched, self.name matches the - # actual filename of the video, and self.nickname is the video's - # title - # (In practical terms, if the user has specified that the video - # filename should be in the format NAME + ID, then self.name will be - # in the format 'NAME + ID', and self.nickname will be in the format - # 'NAME') - # If the video's JSON data has not been fetched, self.name and - # self.nickname are the same - self.nickname = name - # Modified version of self.nickname, padded with leading zeroes and - # reduced to lower case; used in so-called 'natural' sorting of names - self.natname = name - # Download source (a URL) - self.source = None - # The website's video ID, if known (e.g. on YouTube, everything after - # https://www.youtube.com/watch?v=) - self.vid = None - - # Flag set to True once the file has been downloaded, and is confirmed - # to exist in Tartube's data directory - self.dl_flag = False - # Flag set to True if Tartube should always simulate the download of - # video, or False if the downloads.DownloadManager object should - # decide whether to simulate, or not - self.dl_sim_flag = False - # Flag set to True if a this video is a video clip has been split off - # from another video in Tartube's database (which may or may not - # still exist) - self.split_flag = False - # Flag set to True if the video is marked as being censored, age- - # restricted or otherwise unavailable for download - self.block_flag = False - - # The size of the video (in bytes) - self.file_size = None - # The video's upload time (in Unix time) - # YouTube (etc) only supplies a date, which Tartube then converts into - # seconds, so videos uploaded on the same day will have the same - # value for self.upload_time) - self.upload_time = None - # The time at which Tartube downloaded this video (in Unix time) - # When downloading a channel or playlist, we assume that YouTube (etc) - # supplies us with the most recent upload first - # Therefore, when sorting videos by time, if self.upload_time is the - # same (multiple videos were uploaded on the same day), then those - # videos are sorted with the lowest value of self.receive_time first - self.receive_time = None - # The video's duration (in integer seconds) - self.duration = None - # For videos in a channel or playlist (i.e. a media.Video object whose - # parent is a media.Channel or media.Playlist object), the video's - # index in the channel/playlist. (The server supplies an index even - # for a channel, and the user might want to convert a channel to a - # playlist) - # For videos whose parent is a media.Folder, the value remains as None - self.index = None - - # The video's filename and extension - self.file_name = None - self.file_ext = None - - # Video description. A string of any length, containing newline - # characters if necessary. (Set to None if the video description is - # not known) - self.descrip = None - # Video short description - the first line in self.descrip, limited to - # a certain number of characters (specifically, - # mainwin.MainWin.very_long_string_max_len) - self.short = None - - # Livestream mode: 0 if the video is not a livestream (or if it was a - # livestream which has now finished, and behaves like a normal - # uploaded video), 1 if the livestream has not started, 2 if the - # livestream is currently being broadcast - # (Using a numerical mode makes the sorting algorithms more efficient) - self.live_mode = 0 - # YouTube 'premiere' videos and actual livestreams are treated in the - # same way, except that for the former, this flag is set to True - # (and a different background colour is used in the Video Catalogue) - self.live_debut_flag = False - # Flag set to True for a video which was a livestream, but is now not. - # Once a livestream video has been marked as a normal video, it can't - # be marked as a livestream again. (This prevents any problems in - # reading the RSS feeds from continually re-marking an old video as a - # livestream) - self.was_live_flag = False - # The time (matches time.time()) at which a livestream is due to start. - # YouTube supplies an approximate time (perhaps in hours or days), - # but it's still useful for sorting - # (For the benefit of the sorting functions, the default value is 0) - self.live_time = 0 - # When YouTube provides a livestream message, - # utils.extract_livestream_data() converts it into some text that can - # be displayed in the Video Catalogue - # The text to display. If the video is not a livestream, or has already - # started, or has already finished, or no recognised message was - # provided, an empty string - self.live_msg = '' - - # Flag set to True if the video is archived, meaning that it can't be - # auto-deleted (but it can still be deleted manually by the user) - self.archive_flag = False - # Flag set to True if the video is marked as bookmarked, so that it - # appears in the 'Bookmarks' system folder - self.bookmark_flag = False - # Flag set to True if the video is marked a favourite. Upon download, - # it's marked as a favourite if the same IV in the parent channel, - # playlist or folder (also in the parent's parent, and so on) is True - self.fav_flag = False - # Flag set to True if the video is marked as missing (the user has - # downloaded it from a channel/playlist, but the video has since - # been removed from that channel/playlist by its creator) - # Videos are only marked missing when - # mainapp.TartubeApp.track_missing_videos_flag is set - self.missing_flag = False - # Flag set to True at the same time self.dl_sim_flag is set to True, - # showing that the video has been downloaded and not watched - self.new_flag = False - # Flag set to True if the video is marked add as added to the - # 'Waiting Videos' system folder - self.waiting_flag = False - - # When a video is marked to be downloaded in the fixed 'Temporary - # Videos' folder, we store the name of the original parent channel/ - # playlist/folder here, for display in the Video Catalogue - self.orig_parent = None - - # List of subtitles available for this video. Items in the list are - # language codes gathered from the video's metadata file (e.g. - # 'en_US', 'live_chat') - self.subs_list = [] - - # List of timestamps, extracted from the video's description and/or - # metadata, or added manually by the user - # List in groups of three, in the form - # [start_stamp, stop_stamp, clip_title] - # The timestamps are strings in the form n+:n+[:n+], e.g. '15:52', - # '01:15:52' - # When the list of timestamps is extracted from a video's description/ - # metadata, 'stop_stamp' is None. It is usually set when extracted - # from the metadata file - # When 'stop_stamp' is None, the clip is presumed to be the same as the - # next 'start_stamp', or (if there are no more timestamps) the end of - # the video - # The user can insert their own groups of timestamps, in which case - # 'start_stamp' is compulsory, and 'stop_stamp' is optional (None if - # not specified) - # 'clip_title' is always optional, and is None if not specified - self.stamp_list = [] - # List containing data retrieved from SponsorBlock, or added manually - # by the user - # Every item in the list is a dictionary containing data for a single - # video slice, in the form: - # mini_dict['category'] = One of the values in - # formats.SPONSORBLOCK_CATEGORY_LIST (e.g. 'sponsor') - # mini_dict['action'] = One of the values in - # formats.SPONSORBLOCK_ACTION_LIST (e.g. 'skip') - # mini_dict['start_time'] - # mini_dict['stop_time'] = Floating point values in seconds, - # the beginning and end of the slice. If 'stop_time' is None, - # the end of the video is used - # mini_dict['duration'] = The video duration, as reported by - # SponsorBlock. This valus is not required by Tartube code, - # and its default value is 0 - self.slice_list = [] - # List containing video comments, extracted from the video's metadata. - # Only popuplated when downloading the video with yt-dlp - # List of dictionaries, sorted by timestamp (most recent first). Each - # dictionary contains a reduced set of the keys extracted from yt-dl - # data, including these compulsory items: - # ['id']: (int) Simple seequential integer ID, the first omment - # added to the list is 1 - # ['text']: (str) Text of the comment itself - # ['parent']: (int): ID of the parent comment, or None if no - # parent - # These items are optional: - # ['timestamp']: (int) Epoch timestamp of the comment. As of - # v2.3.318, all comments in a YouTube video share the same - # timestamp - # ['time']: (str) String describing the comment age, e.g. '3 days - # ago' - # ['author']: (str) Name of comment author - # ['likes']: (int) Number of likes - # ['fav_flag']: (bool) True if comment favourited, False if not - # ['ul_flag']: (bool) True if commenter is uploader, False if not - self.comment_list = [] - - # List of error/warning messages generated the last time the video was - # checked or downloaded. Both set to empty lists if the video has - # never been checked or downloaded, or if there was no error/warning - # on the last check/download attempt - # NB If an error/warning message is generated when downloading a - # channel or playlist, the message is stored in the media.Channel - # or media.Playlist object instead - self.error_list = [] - self.warning_list = [] - - # IVs used only when the download operation is launched from the - # Classic Mode tab - # Flag set to True if this is a dummy media.Video object - self.dummy_flag = False - # The destination directory for the download - self.dummy_dir = None - # The full path to a downloaded file, if available - self.dummy_path = None - # A string specifying the media format to download, or None if the user - # didn't specify one - # The string is made up of three optional components in a fixed order - # and separated by underlines: 'convert', the video/audio format, and - # the video resolution, for example 'mp4', 'mp4_720p', - # 'convert_mp4_720p' - # Valid values are those specified by formats.VIDEO_FORMAT_LIST, - # formats.AUDIO_FORMAT_LIST and formats.VIDEO_RESOLUTION_LIST - self.dummy_format = None - # Flag set to True to enable yt-dlp to remove all SponsorBlock - # categories - self.dummy_sblock_flag = False - # Flag set to True if the download was completed, in which case - # self.source is not added to mainapp.TartubeApp.classic_pending_list - # (remembering it for the next session) - # Specifically, it remains False when the download is waiting to start, - # or if the VideoDownloader returns a return value of STOPPED, or - # if (during a download) self.dummy_path is still None, meaning no - # videos have been downloaded - # Once set to True, it is never set back to False. So, if the user - # tries to re-download a channel/playlist and no new videos are - # found, the flag remains set to True - self.dummy_dl_flag = False - - - # Code - # ---- - - # Update the parent - if parent_obj: - self.parent_obj.add_child(app_obj, self, no_sort_flag) - - - def compile_updated_ivs(self): - - """Called by mainapp.TartubeApp.check_broken_objs() and - .fix_broken_objs(). - - Returns a dictionary of IVs that have been added since the first - public release of Tartube (v.1.0), and their default values - - Return values: - - The dictionary described above - - """ - - return { - 'nickname': self.name, - 'vid': None, - 'live_mode': 0, - 'live_debut_flag': False, - 'was_live_flag': False, - 'live_time': 0, - 'live_msg': '', - 'archive_flag': False, - 'bookmark_flag': False, - 'missing_flag': False, - 'waiting_flag': False, - 'orig_parent': None, - 'split_flag': False, - 'stamp_list': [], - 'slice_list': [], - 'comment_list': [], - 'dummy_flag': False, - 'dummy_dir': None, - 'dummy_path': None, - 'dummy_format': None, - } - - - # Public class methods - - - def ancestor_is_favourite(self): - - """Called by mainapp.TartubeApp.mark_video_downloaded(). - - Checks whether any ancestor channel, playlist or folder is marked as - favourite. - - Return values: - - True if the parent (or the parent's parent, and so on) is marked - favourite, False otherwise - - """ - - parent_obj = self.parent_obj - - while parent_obj: - if parent_obj.fav_flag: - return True - else: - parent_obj = parent_obj.parent_obj - - return False - - - def fetch_tooltip_text(self, app_obj, max_length=None, - show_error_flag=False): - - """Can be called by anything. - - Returns a string to be used as a tooltip for this video. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - max_length (int or None): If specified, the maximum line length, in - characters - - show_error_flag (bool): If True, show the first error/warning - generated by the channel/playlist/folder - - Return values: - - Text containing the video's file path and source, ready for display - in a tooltip - - """ - - if not self.dummy_flag: - - ignore_me = _( - 'TRANSLATOR\'S NOTE: WAITING = livestream not started,' \ - + ' LIVE = livestream started', - ) - - if self.live_mode == 1: - live_str = ' <' + _('WAITING') + '>' - elif self.live_mode == 2: - live_str = ' <' + _('LIVE') + '>' - else: - live_str = '' - - if self.block_flag: - block_str = ' <' + _('BLOCKED') + '>' - else: - block_str = '' - - text = ' #' + str(self.dbid) + live_str + block_str + ': ' \ - + self.name + '\n\n' - - if self.parent_obj: - - if isinstance(self.parent_obj, Channel): - text += _('Channel:') + ' ' - elif isinstance(self.parent_obj, Playlist): - text += _('Playlist:') + ' ' - else: - text += _('Folder:') + ' ' - - text += self.parent_obj.name + '\n\n' - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Source = video/channel/playlist URL', - ) - - text += _('Source:') + '\n' - if self.source is None: - text += '<' + _('unknown') + '>' - else: - text += self.source - - text += '\n\n' + _('File:') + '\n' - if self.file_name is None: - text += '<' + _('unknown') + '>' - else: - text += self.get_actual_path(app_obj) - - else: - - # When the download operation is launched from the Classic Mode - # tab, there is less to display - text = _('Source:') + '\n' - if self.source is None: - text += '<' + _('unknown') + '>' - else: - text += self.source - - if self.dummy_path is not None: - text += '\n\n' + _('File:') + '\n' + self.dummy_path - - # Show the first error/warning - if show_error_flag and app_obj.show_tooltips_extra_flag: - - error_warning_list = self.error_list + self.warning_list - length = len (error_warning_list) - if error_warning_list: - - text += '\n\n' + _('Errors\Warnings') + ' (' \ - + str(length) + '):\n' + str(error_warning_list.pop(0)) - - if length > 1: - text += '\n...' - - # Apply a maximum line length, if required - if max_length is not None: - text = utils.tidy_up_long_descrip(text, max_length) - - return text - - - def read_video_descrip(self, app_obj, max_length): - - """Can be called by anything. - - Reads the .description file, if it exists, and updates IVs. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - max_length (int): When storing the description in this object's - IVs, the maximum line length to use - - """ - - descrip_path = self.check_actual_path_by_ext(app_obj, '.description') - if descrip_path: - - text = app_obj.file_manager_obj.load_text(descrip_path) - if text is not None: - self.set_video_descrip(app_obj, text, max_length) - - - def extract_timestamps_from_descrip(self, app_obj, override_descrip=None): - - """Can be called by anything. Often called by - self.set_video_descrip(). - - From the video description stored as self.descrip, attempt to extract - the video's timestamps. - - Compiles a list in groups of three, in the form - [start_stamp, stop_stamp, clip_title] - 'start_stamp' is a string in the form h+:m+[:s+], e.g. '15:52', - '01:15:52' - 'stop_stamp' is always None, so that if 'start_stamp' is used to split - a video clip, the clip ends at the next 'start_stamp' (or at the - end of the video). It's up to the user to specify their own - 'stop_stamp' values explicitly, if they need them. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - override_descrip (str or None): If specified, extract timestamps - from this string, rather than from self.descrip - - """ - - if (self.descrip is None or self.descrip == '') \ - and (override_descrip is None or override_descrip == ''): - return - - if override_descrip is not None and override_descrip != '': - self.stamp_list = utils.extract_timestamps_from_descrip( - app_obj, - override_descrip, - ) - - else: - self.stamp_list = utils.extract_timestamps_from_descrip( - app_obj, - self.descrip, - ) - - - def extract_timestamps_from_chapters(self, app_obj, chapter_list): - - """Called by downloads.VideoDownloader.confirm_sim_video() and - mainapp.TartubeApp.update_video_from_json(). - - When supplied with a list of chapters from the video's metadata, - convert that data and store it as a list of timestamps. - - For the sake of simplicity, the calling function doesn't check whether - we're allowed to do that, so this function does the checking. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - chapter_list (list): An ordered list containing a series of python - dictionaries. Each dictionary corresponds to the start of a - single chapter, and is expected to contain the keys - 'start_time', 'end_time' and 'title'. YouTube always supplies - all three, but in common with other parts of the code, we will - still accept the chapter if 'end_time' and/or 'title' are - missing - - """ - - # Do the checking, as promised above - if not app_obj.video_timestamps_extract_json_flag \ - or (self.stamp_list and not app_obj.video_timestamps_replace_flag): - return - - # Extract each chapter in turn - self.stamp_list = utils.extract_timestamps_from_chapters( - app_obj, - chapter_list, - ) - - - def set_slices(self, slice_list): - - """Can be called by anything. - - Sets the video's slice list, first sorting it. - """ - - self.slice_list \ - = list(sorted(slice_list, key=lambda x:x['start_time'])) - - - def convert_slices(self, slice_data_list): - - """Can be called by anything, but principally called by - utils.fetch_slice_data(). - - A modified form of self.set_slices(). - - From SponsorBlock we retrieve a slice data for a video. Convert the - data from used by SponsorBlock into the form used by Tartube, before - saving it in self.slice_list. - """ - - new_list = [] - for old_mini_dict in slice_data_list: - - # (Filter out invalid data. Don't worry about the 'videoDuration' - # field, as Tartube doesn't need it; and ignore the 'UUID' field - # completely) - if 'category' in old_mini_dict \ - and 'actionType' in old_mini_dict \ - and 'segment' in old_mini_dict: - - new_mini_dict = {} - new_mini_dict['category'] = old_mini_dict['category'] - new_mini_dict['action'] = old_mini_dict['actionType'] - new_mini_dict['start_time'] = old_mini_dict['segment'][0] - new_mini_dict['stop_time'] = old_mini_dict['segment'][1] - new_mini_dict['duration'] = old_mini_dict['videoDuration'] - - new_list.append(new_mini_dict) - - self.slice_list = list(sorted(new_list, key=lambda x:x['start_time'])) - - - def reset_slices(self): - - """Can be called by anything. - - Empties the video's slice list. - """ - - self.slice_list = [] - - - def set_timestamps(self, stamp_list): - - """Can be called by anything. - - Sets the video's timestamp list, first sorting it. - """ - - stamp_list.sort() - self.stamp_list = stamp_list.copy() - - - def reset_timestamps(self): - - """Can be called by anything. - - Empties the video's timestamp list. - """ - - self.stamp_list = [] - - - def set_comments(self, comment_list): - - """Can be called by anything. - - Sets the video's comments list, after sorting it. - """ - - # 'comment_list' contains a sequence of dictionaries. Some of the keys - # in the dictionaries are not required, and must be removed - # The key is the original field provided by yt-dlp, the corresponding - # value is the field used by media.Video - check_dict = { - 'id': 'id', - 'text': 'text', - 'timestamp': 'timestamp', - 'time_text': 'time', - 'like_count': 'likes', - 'is_favorited': 'fav_flag', - 'author': 'author', - 'author_is_uploader': 'ul_flag', - 'parent': 'parent', - } - - # Use simple sequential integers for the 'id' and 'parent' fields - id_count = 1 - parent_dict = {} - - # Process each comment - new_list = [] - - for mini_dict in comment_list: - - new_dict = {} - - for key in mini_dict.keys(): - if key in check_dict and mini_dict[key] is not None: - - if key == 'id': - this_id = id_count - parent_dict[mini_dict[key]] = this_id - new_dict['id'] = this_id - - id_count += 1 - - elif key == 'parent': - if not mini_dict[key] in parent_dict: - new_dict['parent'] = None - else: - new_dict['parent'] = parent_dict[mini_dict[key]] - - else: - new_dict[check_dict[key]] = mini_dict[key] - - # This key is also compulosry; add a null parent, if not found - if not 'parent' in new_dict: - new_dict['parent'] = None - - # These keys are compulsory, ignore the comment if they're not - # found - if 'id' in new_dict and 'text' in new_dict: - new_list.append(new_dict) - - # Sort comments by timestamp - # v2.3.317 disabled, since all timestamps are the same for each video - # at the moment -# new_list = list(sorted(new_list, key=lambda x:x['time'])) - - # Update the IV - self.comment_list = new_list - - - def reset_comments(self): - - """Can be called by anything. - - Empties the video's comment list. - """ - - self.comment_list = [] - - - def contains_comment(self, search_text, regex_flag): - - """Called by mainwin.MainWin.video_catalogue_apply_filter(). - - Returns True if any comment contains the specified 'search_text', or - False if none of them do (of if there are no comments to search). - - Args: - - search_text(str): The text/pattern for which to search - - regex_flag (bool): True if 'search_text' is a regex, False - otherwise - - """ - - if not regex_flag: - - lower_text = search_text.lower() - for mini_dict in self.comment_list: - if mini_dict['text'].find(lower_text) > -1: - return True - - else: - - for mini_dict in self.comment_list: - if re.search(search_text, mini_dict['text'], re.IGNORECASE): - return True - - # No matching comments - return False - - - def extract_subs_list(self, subs_dict): - - """Called by mainapp.TartubeApp.update_video_from_json() and - downloads.VideoDownloader.confirm_sim_video(). - - The calling code has extracted a dictionary of subtitles from this - video's metadata. Each key in the dictionary should be a language - code, e.g. 'en_us', 'live_chat' - - Use this dictionary to update the IV. - - Args: - - subs_dict (dict): The dictionary described above - - """ - - self.subs_list = [] - for key in subs_dict.keys(): - self.subs_list.append(key) - - - # Set accessors - - - def set_archive_flag(self, flag): - - if flag: - self.archive_flag = True - else: - self.archive_flag = False - - - def set_block_flag(self, flag): - - if flag: - self.block_flag = True - else: - self.block_flag = False - - - def set_bookmark_flag(self, flag): - - if flag: - self.bookmark_flag = True - else: - self.bookmark_flag = False - - - def set_cloned_name(self, orig_obj): - - """Called by mainwin.MainWin.on_video_catalogue_mark_temp_dl(), etc. - - When a copy of a video is marked to be downloaded in the fixed - 'Temporary Videos' folder, we can copy across the original video's - name and description. - - Args: - - orig_obj (media.Video): The original video - - """ - - self.name = orig_obj.name - self.nickname = orig_obj.nickname - self.descrip = orig_obj.descrip - self.short = orig_obj.short - - - def set_dl_flag(self, flag=False): - - self.dl_flag = flag - - if self.receive_time is None: - self.receive_time = int(time.time()) - - - def set_dl_sim_flag(self, flag): - - if flag: - self.dl_sim_flag = True - else: - self.dl_sim_flag = False - - - def set_dummy(self, url, dir_str, format_str): - - """Called by mainwin.MainWin.classic_mode_tab_add_urls(), immediately - after the call to self.new(). - - Sets up this media.Video object as a dummy object, not added to the - media data registry. - - Args: - - url (str): The URL to download (which might reperesent a video, - channel or playlist; the dummy media.Video object represents - all of them) - - dir_str (str): The destination directory for the download, chosen - by the user - - format_str (str): One of the video/audio formats specified by - formats.VIDEO_FORMAT_LIST and formats.AUDIO_FORMAT_LIST - - """ - - self.dummy_flag = True - self.dummy_dir = dir_str - self.dummy_path = None - self.dummy_format = format_str - - self.source = url - - - def set_dummy_dl_flag(self, flag): - - if flag: - self.dummy_dl_flag = True - else: - self.dummy_dl_flag = False - - - def set_dummy_path(self, path): - - self.dummy_path = path - - - def set_dummy_sblock_flag(self, flag): - - if flag: - self.dummy_sblock_flag = True - else: - self.dummy_sblock_flag = False - - - def set_duration(self, duration=None): - - if duration is not None: - if duration != int(duration): - self.duration = int(duration) + 1 - else: - self.duration = duration - - else: - self.duration = None - - - def set_file(self, filename, extension): - - self.file_name = filename - self.file_ext = extension - - - def set_file_ext(self, extension): - - self.file_ext = extension - - - def set_file_from_path(self, path): - - directory, this_file = os.path.split(path) - filename, extension = os.path.splitext(this_file) - self.file_name = filename - self.file_ext = extension - - - def set_file_size(self, size=None): - - self.file_size = size - - - def set_index(self, index): - - if index is None: - self.index = None - else: - self.index = int(index) - - - def set_live_mode(self, mode): - - self.live_mode = mode - - - def set_live_data(self, live_data_dict): - - """Interprets the dictionary returned by - utils.extract_livestream_data(). - """ - - if 'live_msg' in live_data_dict: - self.live_msg = live_data_dict['live_msg'] - else: - self.live_msg = '' - - if 'live_time' in live_data_dict: - self.live_time = live_data_dict['live_time'] - else: - self.live_time = 0 - - if 'live_debut_flag' in live_data_dict: - self.live_debut_flag = live_data_dict['live_debut_flag'] - - - def set_missing_flag(self, flag): - - if flag: - self.missing_flag = True - else: - self.missing_flag = False - - - def set_mkv(self): - - """Called by mainapp.TartubeApp.update_video_when_file_found() and - refresh.RefreshManager.refresh_from_default_destination(). - - When the warning 'Requested formats are incompatible for merge and will - be merged into mkv' has been seen, the calling function has found an - .mkv file rather than the .mp4 file it was expecting. - - Update the IV. - """ - - self.file_ext = '.mkv' - - - def set_name(self, name): - - self.name = name - - - def set_new_flag(self, flag): - - if flag: - self.new_flag = True - else: - self.new_flag = False - - -# def set_options_obj(): # Inherited from GenericMedia - - - def set_orig_parent(self, parent_obj): - - self.orig_parent = parent_obj.name - - - def set_parent_obj(self, parent_obj): - - self.parent_obj = parent_obj - - - def set_receive_time(self, other_video_obj=None): - - """Can be called by anything. - - Usually, the video's receive time is set to the moment the media.Video - object is created; but in case we need to clone another video's - receive time, the other video can be specified as an argument. - - Args: - - other_vide_obj (media.Video): The video whose .receive_time should - be clone - - """ - - if other_video_obj is None: - self.receive_time = int(time.time()) - else: - self.receive_time = other_video_obj.receive_time - - - def set_source(self, source): - - self.source = source - - - def set_split_flag(self, flag): - - if flag: - self.split_flag = True - else: - self.split_flag = False - - - def reset_subs_list(self): - - self.subs_list = [] - - - def set_upload_time(self, unix_time=None): - - self.upload_time = int(unix_time) - - - def set_vid(self, vid): - - self.vid = vid - - - def set_video_descrip(self, app_obj, descrip, max_length): - - """Can be called by anything. - - Converts the video description into a list of lines, max_length - characters long (longer lines are split into shorter ones). - - Then uses the first line to set the short description, and uses all - lines to set the full description. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - descrip (str): The video description - - max_length (int): A maximum line size - - """ - - if descrip is not None and descrip != '': - - self.descrip = utils.tidy_up_long_descrip(descrip, max_length) - self.short = utils.shorten_string(descrip, max_length) - - # Extract timestamps from the description, if allowed - if app_obj.video_timestamps_extract_descrip_flag \ - and ( - app_obj.video_timestamps_replace_flag \ - or not self.stamp_list - ): - self.extract_timestamps_from_descrip(app_obj) - - else: - self.descrip = None - self.short = None - - - def reset_video_descrip(self): - - self.descrip = None - self.short = None - - - def set_waiting_flag(self, flag): - - if flag: - self.waiting_flag = True - else: - self.waiting_flag = False - - - def set_was_live_flag(self, flag): - - if flag: - self.was_live_flag = True - else: - self.was_live_flag = False - - - # Get accessors - - - def get_actual_path(self, app_obj): - - """Can be called by anything. - - Returns the full path to the video file in its actual location. - - If self.dbid and self.master_dbid are the same, then files are - downloaded to the default location; the sub-directory belonging to the - channel/playlist/folder. In that case, this function returns the same - value as self.get_default_path(). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. This function returns a path to the file in that - sub-directory. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - Return values: - - The path described above - - """ - - return os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - self.file_name + self.file_ext, - ), - ) - - - def get_actual_path_by_ext(self, app_obj, ext): - - """Can be called by anything. - - Returns the full path to a file associated with the video; specifically - one with the same file name, but a different extension (for example, - the video's thumbnail file). - - If self.dbid and self.master_dbid are the same, then files are - downloaded to the default location; the sub-directory belonging to the - channel/playlist/folder. In that case, this function returns the same - value as self.get_default_path_by_ext(). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. This function returns a path to the file in that - sub-directory. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - ext (str): The extension, e.g. 'png' or '.png' - - Return values: - - The full file path (the file may or may not exist) - - """ - - # Add the full stop, if not supplied by the calling function - if not ext.find('.') == 0: - ext = '.' + ext - - return os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - self.file_name + ext, - ), - ) - - - def get_actual_path_in_subdirectory_by_ext(self, app_obj, ext): - - """Can be called by anything. - - Modified version of self.get_actual_path_by_ext(). - - The file might be stored in the same directory as its video, or in the - sub-directory '.thumbs' (for thumbnails) or '.data' (for everything - else). - - self.get_actual_path_by_ext() returns the former; this function returns - the latter. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - ext (str): The extension, e.g. 'png' or '.png' - - Return values: - - The full file path (the file may or may not exist) - - """ - - # Add the full stop, if not supplied by the calling function - if not ext.find('.') == 0: - ext = '.' + ext - - # There are two sub-directories, one for thumbnails, one for metadata - if ext in formats.IMAGE_FORMAT_EXT_LIST: - - return os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - app_obj.thumbs_sub_dir, - self.file_name + ext, - ), - ) - - else: - - return os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - app_obj.metadata_sub_dir, - self.file_name + ext, - ), - ) - - - def check_actual_path_by_ext(self, app_obj, ext): - - """Can be called by anything. - - Modified version of self.get_actual_path_by_ext(). - - The file has the same name as its video, but with a different extension - (for example, the video's thumbnail file). - - The file might be stored in the same directory as its video, or in the - sub-directory '.thumbs' (for thumbnails) or '.data' (for everything - else). - - This function checks to see whether the file exists in the same - directory as its folder and, if so, returns the file path. If not, it - checks to see whether the file exists in the '.thumbs' or '.data' - sub-directory and, if so, returns the file path. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - ext (str): The extension, e.g. 'png' or '.png' - - Return values: - - The full path to the file if it exists, or None if not - - """ - - # Add the full stop, if not supplied by the calling function - if not ext.find('.') == 0: - ext = '.' + ext - - # Check the normal location - main_path = os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - self.file_name + ext, - ), - ) - - if os.path.isfile(main_path): - return main_path - - # Check the sub-directory location - if ext in formats.IMAGE_FORMAT_EXT_LIST: - - subdir_path = os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - app_obj.thumbs_sub_dir, - self.file_name + ext, - ), - ) - - else: - - subdir_path = os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - app_obj.metadata_sub_dir, - self.file_name + ext, - ), - ) - - if os.path.isfile(subdir_path): - return subdir_path - else: - return None - - - def get_default_path(self, app_obj): - - """Can be called by anything. - - Returns the full path to the video file in its default location. - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. To get the actual path to the video file, call - self.get_actual_path(). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - Return values: - - The full file path (the file may or may not exist) - - """ - - return os.path.abspath( - os.path.join( - self.parent_obj.get_default_dir(app_obj), - self.file_name + self.file_ext, - ), - ) - - - def get_default_path_by_ext(self, app_obj, ext): - - """Can be called by anything. - - Returns the full path to a file associated with the video; specifically - one with the same file name, but a different extension (for example, - the video's thumbnail file). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. To get the actual path to the associated file, call - self.get_actual_path_by_ext(). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - ext (str): The extension, e.g. 'png' or '.png' - - Return values: - - The full file path (the file may or may not exist) - - """ - - # Add the full stop, if not supplied by the calling function - if not ext.find('.') == 0: - ext = '.' + ext - - return os.path.abspath( - os.path.join( - self.parent_obj.get_default_dir(app_obj), - self.file_name + ext, - ), - ) - - - def get_default_path_in_subdirectory_by_ext(self, app_obj, ext): - - """Can be called by anything. - - Modified version of self.get_default_path_by_ext(). - - The file might be stored in the same directory as its video, or in the - sub-directory '.thumbs' (for thumbnails) or '.data' (for everything - else). - - self.get_default_path_by_ext() returns the former; this function - returns the latter. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - ext (str): The extension, e.g. 'png' or '.png' - - Return values: - - The full file path (the file may or may not exist) - - """ - - # Add the full stop, if not supplied by the calling function - if not ext.find('.') == 0: - ext = '.' + ext - - # There are two sub-directories, one for thumbnails, one for metadata - if ext in formats.IMAGE_FORMAT_EXT_LIST: - - return os.path.abspath( - os.path.join( - self.parent_obj.get_default_dir(app_obj), - app_obj.thumbs_sub_dir, - self.file_name + ext, - ), - ) - - else: - - return os.path.abspath( - os.path.join( - self.parent_obj.get_default_dir(app_obj), - app_obj.metadata_sub_dir, - self.file_name + ext, - ), - ) - - - def get_file_size_string(self): - - """Can be called by anything. - - Converts self.file_size, in bytes, into a formatted string. - - Return values: - - The converted string, or None if self.file_size is not set - - """ - - if self.file_size: - return utils.convert_bytes_to_string(self.file_size) - else: - return "" - - - def get_receive_date_string(self, pretty_flag=False): - - """Can be called by anything. - - A modified version of self.get_receive_time_string(), returning just - the date, not the date and the time. - - Args: - - pretty_flag (bool): If True, the strings 'Today' and 'Yesterday' - are returned, when possible - - Return values: - - The formatted string, or None if self.receive_time is not set - - """ - - if not self.receive_time: - return None - - elif not pretty_flag: - timestamp = datetime.datetime.fromtimestamp(self.receive_time) - return timestamp.strftime('%Y-%m-%d') - - else: - today = datetime.date.today() - today_str = today.strftime('%y%m%d') - - yesterday = datetime.date.today() - datetime.timedelta(days=1) - yesterday_str = yesterday.strftime('%y%m%d') - - testday = datetime.datetime.fromtimestamp(self.receive_time) - testday_str = testday.strftime('%y%m%d') - - if testday_str == today_str: - return _('Today') - elif testday_str == yesterday_str: - return _('Yesterday') - else: - return testday.strftime('%Y-%m-%d') - - - def get_receive_time_string(self): - - """Can be called by anything. - - Converts self.upload_time, in Unix time, into a formatted string. - - Return values: - - The formatted string, or None if self.receive_time is not set - - """ - - if self.receive_time: - return str(datetime.datetime.fromtimestamp(self.receive_time)) - else: - return None - - - def get_upload_date_string(self, pretty_flag=False): - - """Can be called by anything. - - A modified version of self.get_upload_time_string(), returning just the - date, not the date and the time. - - Args: - - pretty_flag (bool): If True, the strings 'Today' and 'Yesterday' - are returned, when possible - - Return values: - - The formatted string, or None if self.upload_time is not set - - """ - - if not self.upload_time: - return None - - elif not pretty_flag: - timestamp = datetime.datetime.fromtimestamp(self.upload_time) - return timestamp.strftime('%Y-%m-%d') - - else: - today = datetime.date.today() - today_str = today.strftime('%y%m%d') - - yesterday = datetime.date.today() - datetime.timedelta(days=1) - yesterday_str = yesterday.strftime('%y%m%d') - - testday = datetime.datetime.fromtimestamp(self.upload_time) - testday_str = testday.strftime('%y%m%d') - - if testday_str == today_str: - return _('Today') - elif testday_str == yesterday_str: - return _('Yesterday') - else: - return testday.strftime('%Y-%m-%d') - - - def get_upload_time_string(self): - - """Can be called by anything. - - Converts self.upload_time, in Unix time, into a formatted string. - - Return values: - - The formatted string, or None if self.upload_time is not set - - """ - - if self.upload_time: - return str(datetime.datetime.fromtimestamp(self.upload_time)) - else: - return None - - -class Channel(GenericRemoteContainer): - - """Python class that handles a channel (e.g. on YouTube). - - Args: - - app_obj (mainapp.TartubeApp): The main application (not stored as an - IV) - - dbid (int): A unique ID for this media data object - - name (str): The channel name - - parent_obj (media.Folder): The parent media data object, if any - - options_obj (options.OptionsManager): The object specifying download - options for this channel, if any - - """ - - - # Standard class methods - - - def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None): - - # IV list - class objects - # ----------------------- - # The parent object (a media.Folder object if this channel is - # downloaded into a particular sub-directory, or None otherwise) - self.parent_obj = parent_obj - # List of media.Video objects for this channel - self.child_list = [] - # The options.OptionsManager object that specifies how this channel is - # downloaded (or None, if the parent's options.OptionsManager object - # should be used instead) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Unique media data object ID (an integer) - self.dbid = dbid - - # Channel name - self.name = name - # Channel nickname (displayed in the Video Index; the same as .name, - # unless the user changes it) - self.nickname = name - # Modified version of self.nickname, padded with leading zeroes and - # reduced to lower case; used in so-called 'natural' sorting of names - self.natname = name - # Download source (a URL) - self.source = None - # If this channel belongs to an 'enhanced' website, the key in - # formats.ENHANCED_SITE_DICT for that website; otherwise None - self.enhanced = None - # RSS feed source (a URL), used by livestream operations on compatible - # websites. For YouTube channels, set automatically during a download - # operation. For channels on other websites, can be set manually - self.rss = None - - # External download destination - a directory at a fixed position in - # the filesystem, outside Tartube's data directory. Use is not - # recommended because of potential file read/write problems, but is - # available to users who need it - # If specified, the full path to the external directory - self.external_dir = None - # Alternative download destination - the dbid of a channel, playlist or - # folder in whose directory videos, thumbnails (etc) are downloaded. - # By default, set to the dbid of this channel; but can be set to the - # dbid of any other channel/playlist/folder - # Used for: (1) adding a channel and its playlists to the Tartube - # database, so that duplicate videos don't exist on the user's - # filesystem, (2) tying together, for example, a YouTube and a - # BitChute account, so that duplicate videos don't exist on the - # user's filesystem - # NB A media data object can't have an alternative download destination - # and itself be the alternative download destination for another - # media data object; it must be one or the other (or neither) - # NB Ignored if self.external_dir is specified - self.master_dbid = dbid - # A list of dbids for any channel, playlist or folder that uses this - # channel as its alternative destination - self.slave_dbid_list = [] - - # The flags in this group are mutually exclusive; only one flag (or - # none of them) should be True - # Flag set to True if videos in this channel should be downloaded, but - # not added to the database. (If True, the channel and its videos - # are never checked) - self.dl_no_db_flag = False - # Flag set to True if this channel should never be checked or - # downloaded - self.dl_disable_flag = False - # Flag set to True if Tartube should always simulate the download of - # videos in this channel, or False if the downloads.DownloadManager - # object should decide whether to simulate, or not - self.dl_sim_flag = False - - # Flag set to True if this channel is marked as favourite, meaning - # that all child video objects are automatically marked as - # favourites - # (Child video objects will also be marked as favourite if one of this - # channel's ancestors are marked as favourite) - self.fav_flag = False - - # The total number of child video objects - self.vid_count = 0 - # The number of child video objects that are marked as bookmarked, - # downloaded, favourite, livestreams, missing, new and in the - # 'Waiting Videos' system folders - self.bookmark_count = 0 - self.dl_count = 0 - self.fav_count = 0 - self.live_count = 0 - self.missing_count = 0 - self.new_count = 0 - self.waiting_count = 0 - - # Dictionary of playlist IDs extracted from every video in this channel - # (can be used to compile a list of playlists associated with a - # channel) - # Dictionary in the form - # playlist_id_dict[playlist_id] = playlist_title - # ...where 'playlist_title' is None if the title is not known - self.playlist_id_dict = {} - - # List of error/warning messages generated the last time the channel - # was checked or downloaded. Both set to empty lists if the channel - # has never been checked or downloaded, or if there was no error/ - # warning on the last check/download attempt - # NB If an error/warning message is generated when downloading an - # individual video (not in a channel or playlist), the message is - # stored in the media.Video object - self.error_list = [] - self.warning_list = [] - - - # Code - # ---- - - # Update the parent (if any) - if self.parent_obj: - self.parent_obj.add_child(app_obj, self) - - - def compile_updated_ivs(self): - - """Called by mainapp.TartubeApp.check_broken_objs() and - .fix_broken_objs(). - - Returns a dictionary of IVs that have been added since the first - public release of Tartube (v0.1.0), and their default values - - Return values: - - The dictionary described above - - """ - - return { - 'nickname': self.name, - 'rss': None, - 'external_dir': None, - 'master_dbid': self.dbid, - 'slave_dbid_list': [], - 'dl_no_db_flag': False, - 'dl_disable_flag': False, - 'bookmark_count': 0, - 'live_count': 0, - 'missing_count': 0, - 'waiting_count': 0, - } - - - # Public class methods - - -# def add_child(): # Inherited from GenericRemoteContainer - - -# def check_duplicate_video(): # Inherited from GenericContainer - - -# def check_duplicate_video_by_path(): # Inherited from GenericContainer - - -# def del_child(): # Inherited from GenericContainer - - -# def sort_children(): # Inherited from GenericRemoteContainer - - - def update_rss_from_id(self, channel_id): - - """Can be called by anything, for example from - downloads.VideoDownloader.extract_stdout_data(). - - Updates the value of the RSS feed for this channel, self.rss (unless - it has already been set). - - Updates are only possible for channels belong to one of the 'enhanced' - websites specified by formats.ENHANCED_SITE_DICT. - - Args: - - channel_id (str): The channel ID - - """ - - if self.rss: - return - - self.rss = utils.convert_enhanced_template_from_json( - 'rss_channel_list', - self.enhanced, - # Use a fake dictionary of JSON data, as if it had been extracted - # from the video's metadata - { 'channel_id': channel_id }, - ) - - - def update_rss_from_json(self, json_dict): - - """Can be called by anything, for example from - downloads.VideoDownloader.extract_stdout_data(). - - Updates the value of the RSS feed for this channel, self.rss (unless - it has already been set). - - Updates are only possible for channels belong to one of the 'enhanced' - websites specified by formats.ENHANCED_SITE_DICT. - - Args: - - json_dict (dict): Dictionary of JSON data supplied by youtube-dl - for a video, hopefully containing items such as the channel ID - - """ - - if self.rss or not self.source or not self.enhanced: - return - - self.rss = utils.convert_enhanced_template_from_json( - 'rss_channel_list', - self.enhanced, - json_dict, - ) - - - def update_rss_from_name(self, name): - - """Can be called by anything (currently not called by anything). - - Updates the value of the RSS feed for this channel, self.rss (unless - it has already been set). - - Updates are only possible for channels belong to one of the 'enhanced' - websites specified by formats.ENHANCED_SITE_DICT. - - Args: - - name (str): The channel name as it would appear in youtube-dl - output (e.g. 'youtube') - - """ - - if self.rss: - return - - self.rss = utils.convert_enhanced_template_from_json( - 'rss_channel_list', - self.enhanced, - # Use a fake dictionary of JSON data, as if it had been extracted - # from the video's metadata - { 'channel': name }, - ) - - - def update_rss_from_url(self, url): - - """Can be called by anything, for example by self.set_source(). - - Updates the value of the RSS feed for this channel, self.rss (unless - it has already been set). - - Updates are only possible for channels belong to one of the 'enhanced' - websites specified by formats.ENHANCED_SITE_DICT. - - Args: - - url (str): URL which hopefully matches one of the regexes - specified by formats.ENHANCED_SITE_DICT - - """ - - if self.rss or not url or not self.enhanced: - return - - self.rss = utils.convert_enhanced_template_from_url( - 'rss_channel_list', - self.enhanced, - url, - ) - - - # Set accessors - - -# def reset_counts(): # Inherited from GenericContainer - - -# def set_dl_sim_flag(): # Inherited from GenericMedia - - -# def set_options_obj(): # Inherited from GenericMedia - - -# def set_source(): # Inherited from GenericRemoteContainer - - - # Get accessors - - -# def get_actual_dir(): # Inherited from GenericContainer - - -# def get_default_dir(): # Inherited from GenericContainer - - -# def get_relative_actual_dir(): # Inherited from GenericContainer - - -# def get_relative_default_dir(): # Inherited from GenericContainer - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass - - -class Playlist(GenericRemoteContainer): - - """Python class that handles a playlist (e.g. on YouTube). - - Args: - - app_obj (mainapp.TartubeApp): The main application (not stored as an - IV) - - dbid (int): A unique ID for this media data object - - name (str): The playlist name - - parent_obj (media.Folder): The parent media data object, if any - - options_obj (options.OptionsManager): The object specifying download - options for this channel, if any - - """ - - - # Standard class methods - - - def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None): - - # IV list - class objects - # ----------------------- - # The parent object (a media.Folder object if this playlist is - # downloaded into a particular sub-directory, or None otherwise) - self.parent_obj = parent_obj - # List of media.Video objects for this playlist - self.child_list = [] - # The options.OptionsManager object that specifies how this playlist - # is downloaded (or None, if the parent's options.OptionsManager - # object should be used instead) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Unique media data object ID (an integer) - self.dbid = dbid - - # Playlist name - self.name = name - # Playlist nickname (displayed in the Video Index; the same as .name, - # unless the user changes it) - self.nickname = name - # Modified version of self.nickname, padded with leading zeroes and - # reduced to lower case; used in so-called 'natural' sorting of names - self.natname = name - # Download source (a URL) - self.source = None - # If this channel belongs to an 'enhanced' website, the key in - # formats.ENHANCED_SITE_DICT for that website; otherwise None - self.enhanced = None - # RSS feed source (a URL), used by livestream operations on compatible - # websites. Set automatically for YouTube videos, and can be set - # manually by the user for other websites - self.rss = None - - # External download destination - a directory at a fixed position in - # the filesystem, outside Tartube's data directory. Use is not - # recommended because of potential file read/write problems, but is - # available to users who need it - # If specified, the full path to the external directory - self.external_dir = None - # Alternative download destination - the dbid of a channel, playlist or - # folder in whose directory videos, thumbnails (etc) are downloaded. - # By default, set to the dbid of this playlist; but can be set to the - # dbid of any other channel/playlist/folder - # Used for: (1) adding a channel and its playlists to the Tartube - # database, so that duplicate videos don't exist on the user's - # filesystem, (2) tying together, for example, a YouTube and a - # BitChute account, so that duplicate videos don't exist on the - # user's filesystem - # NB A media data object can't have an alternative download destination - # and itself be the alternative download destination for another - # media data object; it must be one or the other (or neither) - # NB Ignored if self.external_dir is specified - self.master_dbid = dbid - # A list of dbids for any channel, playlist or folder that uses this - # playlist as its alternative destination - self.slave_dbid_list = [] - - # The flags in this group are mutually exclusive; only one flag (or - # none of them) should be True - # Flag set to True if videos in this playlist should be downloaded, but - # not added to the database. (If True, the playlist and its videos - # are never checked) - self.dl_no_db_flag = False - # Flag set to True if this playlist should never be checked or - # downloaded - self.dl_disable_flag = False - # Flag set to True if Tartube should always simulate the download of - # videos in this playlist, or False if the downloads.DownloadManager - # object should decide whether to simulate, or not - self.dl_sim_flag = False - - # Flag set to True if this playlist is marked as favourite, meaning - # that all child video objects are automatically marked as - # favourites - # (Child video objects will also be marked as favourite if one of this - # playlist's ancestors are marked as favourite) - self.fav_flag = False - - # The total number of child video objects - self.vid_count = 0 - # The number of child video objects that are marked as bookmarked, - # downloaded, favourite, livestreams, missing, new and in the - # 'Waiting Videos' system folders - self.bookmark_count = 0 - self.dl_count = 0 - self.fav_count = 0 - self.live_count = 0 - self.missing_count = 0 - self.new_count = 0 - self.waiting_count = 0 - - # Dictionary of playlist IDs extracted from every video in this - # playlist (will usually contain just one key-value pair; however, if - # the URL self.source is actually a channel, not a playlist, it may - # contain many key-value pairs; presumably the user will want to - # convert the media.Playlist to a media.Channel at some point) - # Dictionary in the form - # playlist_id_dict[playlist_id] = playlist_title - # ...where 'playlist_title' is None if the title is not known - self.playlist_id_dict = {} - - # List of error/warning messages generated the last time the channel - # was checked or downloaded. Both set to empty lists if the channel - # has never been checked or downloaded, or if there was no error/ - # warning on the last check/download attempt - # NB If an error/warning message is generated when downloading an - # individual video (not in a channel or playlist), the message is - # stored in the media.Video object - self.error_list = [] - self.warning_list = [] - - - # Code - # ---- - - # Update the parent (if any) - if self.parent_obj: - self.parent_obj.add_child(app_obj, self) - - - def compile_updated_ivs(self): - - """Called by mainapp.TartubeApp.check_broken_objs() and - .fix_broken_objs(). - - Returns a dictionary of IVs that have been added since the first - public release of Tartube (v0.1.0), and their default values - - Return values: - - The dictionary described above - - """ - - return { - 'nickname': self.name, - 'rss': None, - 'external_dir': None, - 'master_dbid': self.dbid, - 'slave_dbid_list': [], - 'dl_no_db_flag': False, - 'dl_disable_flag': False, - 'bookmark_count': 0, - 'live_count': 0, - 'missing_count': 0, - 'waiting_count': 0, - } - - - # Public class methods - - -# def add_child(): # Inherited from GenericRemoteContainer - - -# def check_duplicate_video(): # Inherited from GenericContainer - - -# def check_duplicate_video_by_path(): # Inherited from GenericContainer - - -# def del_child(): # Inherited from GenericContainer - - -# def sort_children(): # Inherited from GenericRemoteContainer - - - def update_rss_from_id(self, playlist_id): - - """Can be called by anything, for example from - downloads.VideoDownloader.extract_stdout_data(). - - Updates the value of the RSS feed for this playlist, self.rss (unless - it has already been set). - - Updates are only possible for playlists belong to one of the 'enhanced' - websites specified by formats.ENHANCED_SITE_DICT. - - Args: - - playlist_id (str): The playlist ID - - """ - - if self.rss: - return - - self.rss = utils.convert_enhanced_template_from_json( - 'rss_playlist_list', - self.enhanced, - # Use a fake dictionary of JSON data, as if it had been extracted - # from the video's metadata - { 'playlist_id': playlist_id }, - ) - - - def update_rss_from_json(self, json_dict): - - """Can be called by anything, for example from - downloads.VideoDownloader.extract_stdout_data(). - - Updates the value of the RSS feed for this playlist, self.rss (unless - it has already been set). - - Updates are only possible for playlists belong to one of the 'enhanced' - websites specified by formats.ENHANCED_SITE_DICT. - - Args: - - json_dict (dict): Dictionary of JSON data supplied by youtube-dl - for a video, hopefully containing items such as the playlist ID - - """ - - if self.rss or not self.source or not self.enhanced: - return - - self.rss = utils.convert_enhanced_template_from_json( - 'rss_playlist_list', - self.enhanced, - json_dict, - ) - - - def update_rss_from_name(self, name): - - """Can be called by anything (currently not called by anything). - - Updates the value of the RSS feed for this playlist, self.rss (unless - it has already been set). - - Updates are only possible for playlists belong to one of the 'enhanced' - websites specified by formats.ENHANCED_SITE_DICT. - - Args: - - name (str): The playlist name as it would appear in youtube-dl - output (e.g. 'youtube') - - """ - - if self.rss: - return - - self.rss = utils.convert_enhanced_template_from_json( - 'rss_playlist_list', - self.enhanced, - # Use a fake dictionary of JSON data, as if it had been extracted - # from the video's metadata - { 'playlist_title': name }, - ) - - - def update_rss_from_url(self, url): - - """Can be called by anything, for example by self.set_source(). - - Updates the value of the RSS feed for this channel, self.rss (unless - it has already been set). - - Updates are only possible for channels belong to one of the 'enhanced' - websites specified by formats.ENHANCED_SITE_DICT. - - Args: - - url (str): URL which hopefully matches one of the regexes - specified by formats.ENHANCED_SITE_DICT - - """ - - if self.rss or not url or not self.enhanced: - return - - self.rss = utils.convert_enhanced_template_from_url( - 'rss_playlist_list', - self.enhanced, - url, - ) - - - # Set accessors - - -# def reset_counts(): # Inherited from GenericContainer - - -# def set_dl_sim_flag(): # Inherited from GenericMedia - - -# def set_options_obj(): # Inherited from GenericMedia - - -# def set_source(): # Inherited from GenericRemoteContainer - - - # Get accessors - - -# def get_actual_dir(): # Inherited from GenericContainer - - -# def get_default_dir(): # Inherited from GenericContainer - - -# def get_relative_actual_dir(): # Inherited from GenericContainer - - -# def get_relative_default_dir(): # Inherited from GenericContainer - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass - - -class Folder(GenericContainer): - - """Python class that handles a sub-directory inside Tartube's data folder, - into which other media data objects (media.Video, media.Channel, - media.Playlist and other media.Folder objects) can be downloaded. - - Args: - - app_obj (mainapp.TartubeApp): The main application (not stored as an - IV) - - dbid (int): A unique ID for this media data object - - name (str): The folder name - - parent_obj (media.Folder): The parent media data object, if any - - options_obj (options.OptionsManager): The object specifying download - options for this channel, if any - - fixed_flag (bool): If True, this folder can't be deleted by the user - - priv_flag (bool): If True, the user can't add anything to this folder, - because Tartube uses it for special purposes - - restrict_mode (str): 'full' if this folder can contain videos, but not - channels/playlists/folders, 'partial' if this folder can contain - videos and folders, but not channels and playlists, 'open' if this - folder can contain any combination of videos, channels, playlists - and folders - - temp_flag (bool): If True, the folder's contents should be deleted - when Tartube shuts down (but the folder itself remains) - - """ - - - # Standard class methods - - - def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None, - restrict_mode='open', fixed_flag=False, priv_flag=False, temp_flag=False): - - # IV list - class objects - # ----------------------- - # The parent object (another media.Folder object, or None if no parent) - self.parent_obj = parent_obj - # List of media.Video, media.Channel, media.Playlist and media.Folder - # objects for which this object is the parent - self.child_list = [] - # The options.OptionsManager object that specifies how this channel is - # downloaded (or None, if the parent's options.OptionsManager object - # should be used instead) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Unique media data object ID (an integer) - self.dbid = dbid - - # Folder name - self.name = name - # Folder nickname (displayed in the Video Index; the same as .name, - # unless the user changes it). Note that the nickname of a fixed - # folder can't be changed - self.nickname = name - # Modified version of self.nickname, padded with leading zeroes and - # reduced to lower case; used in so-called 'natural' sorting of names - self.natname = name - - # External download destination - a directory at a fixed position in - # the filesystem, outside Tartube's data directory. Use is not - # recommended because of potential file read/write problems, but is - # available to users who need it - # If specified, the full path to the external directory - # NB Fixed folders cannot have an external directory - self.external_dir = None - # Alternative download destination - the dbid of a channel, playlist or - # folder in whose directory videos, thumbnails (etc) are downloaded. - # By default, set to the dbid of this folder; but can be set to the - # dbid of any other channel/playlist/folder - # Used for: (1) adding a channel and its playlists to the Tartube - # database, so that duplicate videos don't exist on the user's - # filesystem, (2) tying together, for example, a YouTube and a - # BitChute account, so that duplicate videos don't exist on the - # user's filesystem - # NB A media data object can't have an alternative download destination - # and itself be the alternative download destination for another - # media data object; it must be one or the other (or neither) - # NB Fixed folders cannot have an alternative download destination - # NB Ignored if self.external_dir is specified - self.master_dbid = dbid - # A list of dbids for any channel, playlist or folder that uses this - # folder as its alternative destination - self.slave_dbid_list = [] - - # Contents restriction mode: 'full' if this folder can contain - # videos, but not channels/playlists/folders, 'partial' if this - # folder can contain videos and folders, but not channels and - # playlists, 'open' if this folder can contain any combination of - # videos, channels, playlists and folders - self.restrict_mode = restrict_mode - # Flag set to False if the folder can be deleted by the user, or True - # if it can't be deleted by the user - self.fixed_flag = fixed_flag - # Flag set to True to mark this as a private folder, meaning that the - # user can't add anything to it (because Tartube uses it for special - # purposes) - self.priv_flag = priv_flag - # Flag set to True for any folder whose contents should be deleted when - # Tartube shuts down (but the folder itself remains) - self.temp_flag = temp_flag - - # The flags in this group are mutually exclusive; only one flag (or - # none of them) should be True - # Flag set to True if videos in this folder should be downloaded, but - # not added to the database. (If True, the folder and its videos - # are never checked) - self.dl_no_db_flag = False - # Flag set to True if this folder should never be checked or - # downloaded. If True, the setting applies to any descendant - # channels, playlists and folders - self.dl_disable_flag = False - # Flag set to True if Tartube should always simulate the download of - # videos in this folder, or False if the downloads.DownloadManager - # object should decide whether to simulate, or not - self.dl_sim_flag = False - - # Flag set to True if this folder is hidden (not visible in the Video - # Index). Note that only folders can be hidden; channels and - # playlists cannot - self.hidden_flag = False - # Flag set to True if this folder is marked as favourite, meaning that - # any descendant video objects are automatically marked as favourites - # (but not descendant channels, playlists or folders) - # (Descendant video objects will also be marked as favourite if one of - # this folder's ancestors are marked as favourite) - self.fav_flag = False - - # The total number of child video objects - self.vid_count = 0 - # The number of child video objects that are marked as bookmarked, - # downloaded, favourite, livestreams, missing, new and in the - # 'Waiting Videos' system folders - self.bookmark_count = 0 - self.dl_count = 0 - self.fav_count = 0 - self.live_count = 0 - self.missing_count = 0 - self.new_count = 0 - self.waiting_count = 0 - - - # Code - # ---- - - # Update the parent (if any) - if self.parent_obj: - self.parent_obj.add_child(app_obj, self) - - - def compile_updated_ivs(self): - - """Called by mainapp.TartubeApp.check_broken_objs() and - .fix_broken_objs(). - - Returns a dictionary of IVs that have been added since the first - public release of Tartube (v0.1.0), and their default values - - Return values: - - The dictionary described above - - """ - - if hasattr(self, 'restrict_flag'): - if self.restrict_flag: - restrict_mode = 'full' - else: - restrict_mode = 'open' - else: - restrict_mode = self.restrict_mode - - return { - 'nickname': self.name, - 'external_dir': None, - 'master_dbid': self.dbid, - 'slave_dbid_list': [], - 'restrict_mode': restrict_mode, - 'dl_no_db_flag': False, - 'dl_disable_flag': False, - 'bookmark_count': 0, - 'live_count': 0, - 'missing_count': 0, - 'waiting_count': 0, - } - - - # Public class methods - - - def add_child(self, app_obj, child_obj, no_sort_flag=False): - - """Can be called by anything. - - Adds a child media data object, which can be any type of media data - object (including another media.Folder object). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - child_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The child object - - no_sort_flag (bool): If True, the child list is not sorted after - the new object has been added - - """ - - # Check this is not already a child object - if not child_obj in self.child_list: - - self.child_list.append(child_obj) - if not no_sort_flag: - self.sort_children(app_obj) - - if isinstance(child_obj, Video): - self.vid_count += 1 - - -# def check_duplicate_video(): # Inherited from GenericContainer - - -# def check_duplicate_video_by_path(): # Inherited from GenericContainer - - -# def del_child(): # Inherited from GenericContainer - - - def sort_children(self, app_obj): - - """Can be called by anything. For example, called by self.add_child(). - - Sorts the child media.Video, media.Channel, media.Playlist and - media.Folder objects. - """ - - # Sort a copy of the list to prevent 'list modified during sort' - # errors - while True: - - copy_list = self.child_list.copy() - copy_list.sort( - key=functools.cmp_to_key(app_obj.folder_child_compare), - ) - - if len(copy_list) == len(self.child_list): - self.child_list = copy_list.copy() - break - - - # Set accessors - - -# def reset_counts(): # Inherited from GenericContainer - - -# def set_dl_sim_flag(): # Inherited from GenericMedia - - - def set_hidden_flag(self, flag): - - if flag: - self.hidden_flag = True - else: - self.hidden_flag = False - - -# def set_options_obj(): # Inherited from GenericMedia - - - # Get accessors - - -# def get_actual_dir(): # Inherited from GenericContainer - - -# def get_default_dir(): # Inherited from GenericContainer - - -# def get_relative_actual_dir(): # Inherited from GenericContainer - - -# def get_relative_default_dir(): # Inherited from GenericContainer - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass - - -class Scheduled(object): - - """Python class that handles a scheduled download operation. - - Args: - - name (str): Unique name for the scheduled download (a string, minimum 1 - character) - - dl_mode (str): Download operation type: 'sim' (for simulated - downloads), 'real' (for real downloads) or 'custom_real' (for - custom downloads; the value is checked before being used, and - converted to 'custom_sim' where necessary) - - start_mode (str): 'disabled' to disable this scheduled download, - 'start' to perform the operation whenever Tartube starts, - 'start_after' to perform the operation some time after Tartube - starts, 'repeat' to perform the operation at regular intervals, - 'timtetable' to perform the operation at pre-determined intervals - - """ - - - # Standard class methods - - - def __init__(self, name, dl_mode, start_mode): - - # IV list - other - # --------------- - # Unique name for the scheduled download (a string, minimum 1 - # character) - self.name = name - - # Download operation type: 'sim' (for simulated downloads), 'real' (for - # real downloads) or 'custom_real' (for all custom downloads; the - # value is checked before being used, and converted to 'custom_sim' - # where necessary) - self.dl_mode = dl_mode - # The .uid of the custom download programme to use (a key in - # mainapp.TartubeApp.custom_dl_reg_dict). If None or an unrecognised - # value, a 'real' download takes place - # Ignored if self.dl_mode is not 'custom_real' - self.custom_dl_uid = None - - # Start mode - # 'disabled' - disable this scheduled download - # 'start' - perform the operation whenever Tartube starts - # 'start_after' - perform the operation some time after Tartube - # starts - # 'repeat' - perform the operation at regular intervals - # 'timetable' - perform the operation at pre-determined intervals - self.start_mode = start_mode - - # The time between scheduled downloads (minimum value 1) - # self.start_mode = 'start_after' - # The time after Tartube starts at which the scheduled download - # happens - # self.start_mode = 'repeat': - # The time between repeating scheduled downloads - self.wait_value = 2 - # self.wait_value uses this unit (any of the values in - # formats.TIME_METRIC_LIST; but the 'seconds' value is not available - # in the edit window's combobox) - self.wait_unit = 'hours' - - # Timetable of times at which the scheduled download happens. when - # self.start_mode = 'timetable' - # Each item in the list is a mini-list in the form - # [ day_string, time_string ] - # ... where 'day_string' is a kay in formats.SPECIFIED_DAYS_DICT (e.g. - # 'every_day', 'monday', and 'time_string' is a 24-hour time in - # the form 'hh:mm' - self.timetable_list = [] - # A window of 5 minutes during which scheduled downloads can start - # (i.e. if the timetable time is 14:00 and Tartube is started at - # 14:02, the scheduled download still starts) - # Absolute minimum value is 1, recommended minimum is 60 - self.timetable_window = 300 - - # The time (system time, in seconds) at which this scheduled download - # last started (regardless of whether it was scheduled to begin at - # that time, or not) - self.last_time = 0 - # When self.start_mode is 'start' or 'start_after', - # mainapp.TartubeApp.start sets this value to the time at which the - # scheduled download should start - # Once the scheduled download is started, the value is set back to 0 - self.only_time = 0 - - # When multiple scheduled downloads are due to start at the same time, - # a flag that marks this as an exclusive scheduled download - # Scheduled downloads are checked in the order specified by - # mainapp.TartubeApp.scheduled_list. If this is flag is True for any - # of them, only one of the flagged downloads starts - self.exclusive_flag = False - # When this scheduled download is due to start, what to do if another - # download operation is in progress: 'join' to add media data objects - # to the current download operation (at the end of the existing - # list), 'priority' to add media data objects to the current download - # operation (at the beginning of the existing list), 'skip' to wait - # until the next scheduled operation time instead - # Note that this affects a download operation already in progress, - # not one which is about to start (because multiple scheduled - # downloads are due to start at the same time) - self.join_mode = 'skip' - - # Flag set to True if Tartube should shut down after this scheduled - # download operation occurs, False if not - self.shutdown_flag = False - # Flag set to True if the whole channel/playlist/folder should be - # checked/downloaded, regardless of the value of - # mainapp.TartubeApp.operation_limit_flag, etc - self.ignore_limits_flag = False - - # Maximum simultaneous downloads. If the flag is True, the specified - # value overrides the equivalent mainapp.TartubeApp IV - - # Maximum download bandwidth. If the flag is True, the specified - # value overrides the equivalent mainapp.TartubeApp IV - self.scheduled_num_worker = 2 - self.scheduled_num_worker_apply_flag = False - self.scheduled_bandwidth = 500 - self.scheduled_bandwidth_apply_flag = False - - # Flag set to True if the download operation should encompass all media - # data objects - self.all_flag = True - # List of .dbid values for media.Channel, media.Playlist and - # media.Folder objects to add to each download operation (not the - # objects themselves. All of their children are also added). Ignored - # if self.all_flag is True - self.media_list = [] - - - # Public class methods - - - def check_start(self): - - """Called by mainapp.TartubeApp.script_slow_timer_callback(). - - Tests whether it is time to start this scheduled download, or not. - - Return values: - - True to start the scheduled download, False otherwise. - - """ - - wait_time = self.wait_value * formats.TIME_METRIC_DICT[self.wait_unit] - - if ( - ( - self.start_mode == 'repeat' \ - and self.last_time + wait_time < time.time() - ) or ( - ( - self.start_mode == 'start' \ - or self.start_mode == 'start_after' - ) and self.only_time > 0 \ - and self.only_time < time.time() - ) or ( - self.start_mode == 'timetable' \ - and self.check_timetable() - ) - ): - return True - else: - return False - - - def check_timetable(self): - - """Called by self.check_start() when self.start_mode is 'timetable'. - - Tests whether it is time to start this scheduled download, or not, - depending on dates/times specified in self.timetable_list(). - - Return values: - - True to start the scheduled download, False otherwise. - - """ - - local = utils.get_local_time() - current_day = local.today().weekday() - current_hours = int(local.strftime('%H')) - current_minutes = int(local.strftime('%M')) - - # Each 'mini_list' is in the form [ day_string, time_string ] - for mini_list in self.timetable_list: - - # Today? - if not utils.check_day(current_day, mini_list[0]): - continue - - # Between these two times (a window of 5 minutes, by default)? - early_time = datetime.datetime.now() - early_time = early_time.replace( - hour = int(mini_list[1][0:2]), - minute = int(mini_list[1][3:5]), - second = 0, - ) - - late_time = early_time \ - + datetime.timedelta(seconds=self.timetable_window) - - # Give each scheduled download a two minute window in which to - # start - if early_time > datetime.datetime.fromtimestamp( - self.last_time + self.timetable_window, - ) and datetime.datetime.fromtimestamp(time.time()) >= early_time \ - and datetime.datetime.fromtimestamp(time.time()) <= late_time: - return True - - # Try again later - return False - - - # Set accessors - - - def reset_custom_dl_uid(self): - - self.custom_dl_uid = None - if self.dl_mode == 'custom_real': - self.dl_mode = 'real' - - - def set_last_time(self, time): - - self.last_time = time - - - def add_media(self, dbid): - - self.media_list.append(dbid) - self.all_flag = False - - - def set_only_time(self, time): - - self.only_time = time diff --git a/build/lib/tartube/options.py b/build/lib/tartube/options.py deleted file mode 100644 index b3899eee..00000000 --- a/build/lib/tartube/options.py +++ /dev/null @@ -1,2160 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Module that contains a class storing download options.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import os -import re - - -# Import our modules -import formats -import mainapp -import media -import utils -# Use same gettext translations -from mainapp import _ - - -# Classes - - -class OptionsManager(object): - - """Partially based on the OptionsManager class in youtube-dl-gui. - - This class handles settings for downloading media. Unlike youtube-dl-gui, - which has one group of download options applied to all downloads, this - object can be applied to any of the media data classes in media.py (so it - can be applied to a single video, or a whole channel, or generally to all - downloads). - - Tartube's options.OptionsManager implements a subset of options implemented - by the equivalent class in youtube-dl-gui. - - Args: - - uid (int): Unique ID for this options manager - - name (str): A non-unique name for this options manager. Options - managers that, on creation, are attached to a particular media data - object have the same name as that object - - dbid (int or None): If this options manager, on create, is attached to - a particular media data object, the .dbid of that object; otherwise - None - - Canonical list of options: - - Options are listed here in the same order in which they appear in - youtube-dl's documentation. - - OPTIONS - - ignore_errors (bool): If True, youtube-dl will ignore the errors and - continue the download operation - - abort_on_error (bool): If True, youtube-dl will abord downloading - further playlist videos if an error occurs - - live_from_start (bool): If True, downloads livestreams from the start. - Currently only supported for YouTube (and marked experimental) - - wait_for_video_min (int): Minimum number of seconds to wait between - retries for scheduled livestreams to become available (in seconds) - - NETWORK OPTIONS - - proxy (str): Use the specified HTTP/HTTPS proxy. If none is specified, - then Tartube will cycle through the list of proxies specified in - mainapp.TartubeApp.dl_proxy_list (if any) - - socket_timeout (str): Time to wait before giving up, in seconds - - source_address (str): Client-side IP address to bind to - - force_ipv4 (str): Make all connections via IPv4 - - force_ipv6 (str): Make all connections via IPv6 - - GEO-RESTRICTION - - geo_verification_proxy (str): Use this proxy to verify the IP address - for some geo-restricted sites - - geo_bypass (bool): Bypass geographic restriction via faking - X-Forwarded-For HTTP header - - no_geo_bypass (bool): Do not bypass geographic restriction via faking - X-Forwarded-For HTTP header - - geo_bypass_country (str): Force bypass geographic restriction with - explicitly provided two-letter ISO 3166-2 country code - - geo_bypass_ip_block (str): Force bypass geographic restriction with - explicitly provided IP block in CIDR notation - - VIDEO SELECTION - - playlist_start (int): Playlist index to start downloading - - playlist_end (int): Playlist index to stop downloading - - playlist_items (str): Comma-separated playlist index of the videos to - download, in the form '[START]:[STOP][:STEP]' or 'START-STOP'. Use - negative indices to count from the right and negative STEP to - download in reverse order, e.g. on a playhlist of 15 videos, - '1:3,7,-5::2' downloads the videos at index '1,2,3,7,11,13,15' - - max_downloads (int): Maximum number of video files to download from the - given playlist - - min_filesize (float): Minimum file size of the video file. If the video - file is smaller than the given size then youtube-dl will abort the - download operation - - max_filesize (float): Maximum file size of the video file. If the video - file is larger than the given size then youtube-dl will abort the - download operation - - date (str): Download only videos uploaded on this date (YYYYMMDD) - - date_before (str): Download only videos uploaded on or before this date - (YYYYMMDD) - - date_after (str): Download only videos uploaded on or after this date - (YYYYMMDD) - - min_views (int): Do not download any videos with fewer than this many - views - - max_views (int): Do not download any videos with more than this many - views - - match_filter (str): Generic video filter (see the comments in the - youtube-dl documentation). Tartube automatically adds quotes to - the beginning and end of the string - - age_limit (str): Download only videos suitable for the given age - - include_ads (bool): Download advertisements (experimental) - - DOWNLOAD OPTIONS - - limit_rate (str): Bandwidth limit, in bytes (example strings: 50K or - 4.2M) (not implemented by youtube-dl-gui) - NB: Can't be directly modified by user - - retries (int): Number of youtube-dl retries - - abort_on_unavailable_fragment (bool): Abort download if a fragment is - unavailable (default is to skip unavailable fragments) - - playlist_reverse (bool): When True, download playlist videos in reverse - order - - playlist_random (bool): When True, download playlist videos in random - order - - native_hls (bool): When True, youtube-dl will prefer the native HLS - (HTTP Live Streaming) implementation (rather than prefering FFmpeg, - which is at the current time the better downloader for general - compatibility) - - hls_prefer_ffmpeg (bool): When True, youtube-dl will prefer FFmpeg - (N.B. This should not be confused with the 'prefer_ffmpeg' option) - - external_downloader (str): Use the specified external downloaded. - youtube-dl currently supports the strings 'aria2c', 'avconv', - 'axel', 'curl', 'ffmpeg', 'httpie', 'wget' (use an empty string to - disable this option) - - external_arg_string (str): Arguments to pass to the external - downloader. Tartube automatically adds quotes to the beginning and - end of the string - - FILESYSTEM OPTIONS - - restrict_filenames (bool): If True, youtube-dl will restrict the - downloaded file's filename to ASCII characters only - - nomtime (bool): When True will not use the last-modified header to set - the file modification time (i.e., use the time at which the server - believes the resources was last modified) - - write_description (bool): If True, youtube-dl will write video - description to a .description file - - write_info (bool): If True, youtube-dl will write video metadata to an - .info.json file - - write_annotations (bool): If True, youtube-dl will write video - annotations to an .annotations.xml file - - cookies_path (str): Path to the cookie jar. If not specified, then - Tartube will use the cookie jar 'cookies.txt' in the main - data directory - - THUMBNAIL IMAGES - - write_thumbnail (bool): If True youtube-dl will write thumbnail image - to disc - - WORKAROUNDS - - force_encoding (str): Force the specified encoding - - no_check_certificate (bool): If True, suppress HTTPS certificate - validation - - prefer_insecure (bool): If True, use an unencrypted connection to - retrieve information about the video. (Currently supported only for - YouTube) - - user_agent (str): Specify a custom user agent for youtube-dl - - referer (str): Specify a custom referer to use if the video access is - restricted to one domain - - min_sleep_interval (int): Number of seconds to sleep before each - download when used alone, or a lower bound of a range for - randomised sleep before each download (minimum possible number of - seconds to sleep) when used along with 'max_sleep_interval' - - max_sleep_interval (int): Upper bound of a range for randomized sleep - before each download (maximum possible number of seconds to sleep). - Can only be used along with a non-zero value for min_sleep_interval - - VIDEO FORMAT OPTIONS - - video_format (str): Video format to download. When this option is set - to '0' youtube-dl will choose the best video format available for - the given URL. Otherwise, set in a call to - OptionsParser.build_video_format(), combining the contents of the - 'video_format_list' and 'video_format_mode' options. The combined - value is passed to youtube-dl with the -f switch - - all_formats (bool): If True, download all available video formats. - Also set in the call to OptionsParser.build_video_format() - - prefer_free_formats (bool): If True, prefer free video formats unless - one is specfied by video_format, etc - - yt_skip_dash (bool): If True, do not download DASH-related data with - YouTube videos - - merge_output_format (str): If a merge is required (e.g. - bestvideo+bestaudio), output to this container format. youtube-dl - supports the strings 'mkv', 'mp4', 'ogg', 'webm', 'flv' (or an - empty string to ignore this option) - - SUBTITLE OPTIONS - - write_subs (bool): If True, youtube-dl will try to download the - subtitles file for the given URL - - write_auto_subs (bool): If True, youtube-dl will try to download the - automatic subtitles file for the given URL - - write_all_subs (bool): If True, youtube-dl will try to download all the - the available subtitles files for the given URL - - subs_format (str): Subtitle format preference. youtube-dl supports - 'srt', 'ass', 'vtt', 'lrc' or combinations thereof, e.g. - 'ass/srt/best' - - subs_lang (str): Language of the subtitles file to download. Requires - the 'write_subs' option. Can not be set directly by the user; - instead, OptionsParser.parse() converts the option 'subs_lang_list' - to a string, and sets this option to that string - - AUTHENTIFICATION OPTIONS - - username (str): Username to login with - - password (str): Password to login with - - two_factor (str): Two-factor authentification code - - net_rc (bool): If True, use .netrc authentification data - - video_password (str): Video password for the given URL - - ADOBE PASS OPTIONS - - ap_mso (str): Adobe Pass multiple-system operator (TV provider) - identifier - - ap_username (str): Multiple-system operator account login - - ap_password (str): Multiple-system operator account password. If this - option is left out, yt-dlp will ask interactively - - POST-PROCESSING OPTIONS - - extract_audio (bool): If True, youtube-dl will post-process the video - file - - audio_format (str): Audio format of the post-processed file. Available - values are 'mp3', 'wav', 'aac', 'm4a', 'vorbis', 'opus' & 'flac' - - audio_quality (str): Audio quality of the post-processed file. - Available values for VBR are '9' ('high'), '5' (medium) or '0' - ('low'). Other values are '320k', '256k', '192k', '128k', '96k'. - The default value is 5 - - recode_video (str): Encode the video to another format if necessary. - One of the strings 'avi', 'flv', 'mkv', 'mp4', 'ogg', 'webm', or an - empty string if disabled - - pp_args (str): Give these arguments to the postprocessor. Tartube - automatically adds quotes to the beginning and end of the string - - keep_video (bool): If True, youtube-dl will keep the video file after - post-processing it - - embed_subs (bool): If True, youtube-dl will merge the subtitles file - with the video (only for .mp4 files) - - embed_thumbnail (bool): When True will embed the thumbnail in the audio - file as cover art - - add_metadata (bool): When True will write metadata to the video file - - fixup_policy (str): Automatically correct known faults of the file. - The string can be 'never', 'warn', 'detect_or_worn' or an empty - string if disabled - - prefer_avconv (bool): Prefer AVConv over FFmpeg for running the - postprocessors - - prefer_ffmpeg (bool): Prefer FFmpeg over AVConv for running the - postprocessors - - YT-DLP OPTIONS (will not work with other downloaders) - - [not passed to yt-dlp directly] - - output_format_list (list): List of arguments used with the '--output' - parameter, in the form TYPES:TEMPLATE. Each argument should have - a unique TYPES component. If this option is specified, - 'output_format' and 'output_template' are ignored - - output_path_list (list): List of arguments used with the '--paths' - paremeter, in the form TYPES:PATH. Each argument should have a - unique TYPES component. If 'output_path_list' contains any values, - then 'output_format_list' and/or 'output_format' are used without a - preceding absolute path - - extractor_args_list (list): Pass these arguments to the extractor. You - can use this option multiple times to give different arguments to - different extractors. Each item in the list is in the form - KEY:ARGS - - [Video Selection Options] - - break_on_existing (bool): If True, stops the download process when - encountering a file that is in the archive - - break_on_reject (bool): If True, stops the download process when - encountering a file that has been filtered out - - skip_playlist_after_errors (int): Number of allowed failures until the - rest of the playlist is skipped - - [Download Options] - - concurrent_fragments (int): Number of fragments of a DASH/hlsnative - video that should be download concurrently (default is 1) - - throttled_rate (int): Minimum download rate in bytes per second below - which throttling is assumed and the video data is re-extracted - (e.g. 100K) - - [Filesystem Options] - - windows_filenames (bool): If True, forces filenames to be MS Windows - compatible - - trim_filenames (int): Limit the filename length (excluding extension) - to the specified number of characters - - no_overwrites (bool): If True, does not overwrite any files - - force_overwrites (bool): If True, overwrites all video and metadata - files. This option includes '--no-continue' (for which there is no - Tartube download option) - - write_playlist_metafiles (bool): If True, writes playlist metadata in - addition to the video metadata when using --write-info-json, - --write-description etc. (default) - - no_clean_info_json (bool): If True, writes all fields to the infojson - (default is to remove some private fields) - - no_cookies (bool): If True, does not read/dump cookies from/to file - (default) - - cookies_from_browser (str): The name of the browser and (optionally) - the name/path of the profile to load cookies from; a string in the - from BROWSER[+KEYRING][:PROFILE] - - no_cookies_from_browser (bool): If True, does not load cookies from the - browser (default) - - [Internet Shortcut Options] - - write_link (bool): If True, writes an internet shortcut file, depending - on the current platform (.url, .webloc or .desktop). The URL may be - cached by the OS - - write_url_link (bool): If True, writes a .url Windows internet - shortcut. The OS caches the URL based on the file path - - write_webloc_link (bool): If True, writes a .webloc macOS internet - shortcut - - write_desktop_link (bool): If True, writes a .desktop Linux internet - shortcut - - [Verbosity and Simulation Options] - - ignore_no_formats_error (bool): If True, ignore "No video formats" - error. Useful for extracting metadata even if the video is not - actually available for download (experimental) - - force_write_archive (bool): If True, forces download archive entries to - be written as far as no errors occur, even if -s or another - simulation option is used - - [Workaround Options] - - sleep_requests (int): Number of seconds to sleep between requests - during data extraction - - sleep_subtitles (int): Number of seconds to sleep before each download. - This is the minimum time to sleep when used along with - 'max_sleep_interval' - - [Video Format Options] - - video_multistreams (bool): If True, allows multiple video streams to be - merged into a single file - - audio_multistreams (bool): If True, allows multiple audio streams to be - merged into a single file - - check_formats (bool): If True, checks that the formats selected are - actually downloadable (Experimental) - - allow_unplayable_formats (bool): If True, allows unplayable formats to - be listed and downloaded. All video post-processing will also be - turned off - - [Post-Processing Options] - - remux_video (str): Remux the video into another container if necessary - (currently supported: mp4|mkv|flv|webm|mov|avi|mp3|mka|m4a|ogg - |opus). If target container does not support the video/audio codec, - remuxing will fail. You can specify multiple rules; Eg. - "aac>m4a/mov>mp4/mkv" will remux aac to m4a, mov to mp4 and - anything else to mkv. - - embed_metadata (bool): Embed metadata including chapter markers (if - supported by the format) to the video file - - convert_thumbnails (str): Convert the thumbnails to another format - (currently supported: jpg|png) - - split_chapters (bool): Split video into multiple files based on - internal chapters. The "chapter:" prefix can be used with - 'output_path_list' and 'output_template_list' to set the output - filename for the split files - - [Extractor Options] - - extractor_retries (str): Number of retries for known extractor errors - (default is '3'), or 'infinite' - - no_allow_dynamic_mpd (bool): If True, do not process dynamic DASH - manifests - - hls_split_discontinuity (bool): If True, splits HLS playlists to - different formats at discontinuities such as ad breaks - - YOUTUBE-DL-GUI OPTIONS (not passed to youtube-dl directly) - - [used to build parameters of --output and --paths] - - output_format (int): Option in the range 0-9, which is converted into - a youtube-dl output template using - formats.FILE_OUTPUT_CONVERT_DICT. If the value is 0, then the - custom 'output_template' is used instead - - output_template (str): Can be any output template supported by - youtube-dl. Ignored if 'output_format' is not 0 - - [used in conjunction with the 'min_filesize' & 'max_filesize' options] - - max_filesize_unit (str): Maximum file size unit. Available values: - '' (for bytes), 'k' (for kilobytes, etc), 'm', 'g', 't', 'p', - 'e', 'z', 'y' - - min_filesize_unit (str): Minimum file size unit. Available values - as above - - [in youtube-dl-gui, this was named 'cmd_args'] - - extra_cmd_string (str): String that contains extra youtube-dl options - separated by spaces. Components containing whitespace can be - enclosed within double quotes "..." - - direct_cmd_flag (str): If True, only those command options specified by - extra_cmd_string (including the source URL) are used; Tartube - merely adds the downloader (e.g. 'youtube-dl') and the output - directory switch (i.e. -o) - - direct_url_flag (str): If True, Tartube assumes that - 'extra_cmd_string' contains the URL to check/download. Otherwise, - the source URL of each affected media data object(s) is used, as - normal. Ignored if 'direct_cmd_flag' is False - - TARTUBE OPTIONS (not passed to youtube-dl directly) - - move_description (bool): - move_info (bool): - move_annotations (bool): - move_thumbnail (bool): - During a download operation (real or simulated), if these values - are True, the video description/JSON/annotations files are moved to - a '.data' sub-directory, and the thumbnails are moved to a - '.thumbs' sub-directory, inside the directory containing the videos - - keep_description (bool): - keep_info (bool): - keep_annotations (bool): - keep_thumbnail (bool): - During a download operation (not simulated, e.g. when the user - clicks the 'Download all' button), the video description/JSON/ - annotations/thumbnail files are downloaded only if - 'write_description', 'write_info', 'write_annotations' and/or - 'write_thumbnail' are True - - They are initially stored in the same sub-directory in which - Tartube will store the video - - If these options are True, they stay there; otherwise, they are - copied into the equivalent location in Tartube's temporary - directories. - - sim_keep_description (bool): - sim_keep_info (bool): - sim_keep_annotations (bool): - sim_keep_thumbnail (bool): - During a download operation (simulated, e.g. when the user clicks - the 'Check all' button), the video's JSON file is always loaded - into memory - - If 'write_description' and 'sim_keep_description' are both True, - the description file is written directly to the sub-directory in - which Tartube would store the video - - If 'write_description' is True but 'sim_keep_description' not, the - description file is written to the equivalent location in Tartube's - temporary directories. - - The same applies to the JSON, annotations and thumbnail files. - - use_fixed_folder (str or None): If not None, then all videos are - downloaded to one of Tartube's fixed folders (not including private - folders) - currently, that group consists of only 'Temporary - Videos', 'Unsorted Videos' and 'Video Clips' (or their translated - equivalents). The value, if not None, should be 'temp' (matches - mainapp.TartubeApp.fixed_temp_folder), 'misc' - (matches mainapp.TartubeApp.fixed_misc_folder) or 'clips' - (matches mainapp.TartubeApp.fixed_clips_folder) - - match_title_list (list): Download only matching titles (regex or - caseless sub-string). Each item in the list is passed to youtube-dl - as a separate --match-title argument - - reject_title_list (list): Skip download for any matching titles (regex - or caseless sub-string). Each item in the list is passed to - youtube-dl as a separate --reject-title argument - - video_format_list (list): List of video formats to download, in order - of preference. If an empty list, youtube-dl will choose the best - video format available for the given URL. Otherwise, the items in - this list are keys in formats.VIDEO_FORMAT_DICT. The corresponding - values are combined and stored as the 'video_format' option, first - being rearrnaged to put video formats before audio formats - (otherwise youtube-dl won't download the video formats) - - video_format_mode (str): 'all' to download all available formats, - ignoring the preference list (sets the option 'all_formats'). - 'single' to download the first available format in - 'video_format_list'. 'single_agree' to download the first format in - 'video_format_list' that's available for all videos. 'multiple' to - download all available formats in 'video_format_list' - - subs_lang_list (list): List of language tags which are used to set - the 'subs_lang' option - - downloader_config (bool): If True, a youtube-dl configuration is - specified with the '--config-location' option. On Linux/MacOS, the - user-wide configuration file is used - ('~/.config/youtube-dl/config'). On MS Windows, a file in the - Tartube directory is used (youtube-dl.conf) - - """ - - - # Standard class methods - - - def __init__(self, uid, name, dbid=None): - - # IV list - other - # --------------- - # Unique ID for this options manager - self.uid = uid - # A non-unique name for this options manager. Managers that are - # attached to a media data object have the same name as that object - # (The name is not unique because, for example, videos could have the - # same name as a channel; it's up to the user to avoid duplicate - # names) - # Empty strings are not valid as names - self.name = name - # A short description, intended for use in the Drag and Drop tab - # Empty strings are valid as descriptions - self.descrip = name - # A list of .dbid value for any media data objects to which this - # options manager is attached. (When attached to a media.Folder, - # this options manager applies to any child video, channel, playlist - # or sub-folder) - # An empty list if not attached to a media data object - self.dbid_list = [] - - # Dictionary of download options for youtube-dl, set by a call to - # self.reset_options - self.options_dict = {} - - - # Code - # ---- - - if dbid is not None: - self.dbid_list = [dbid] - - # Initialise youtube-dl options - self.reset_options() - - - # Public class methods - - - def clone_options(self, other_options_manager_obj): - - """Called by mainapp.TartubeApp.apply_download_options(), - .clone_download_options() and .clone_download_options_from_window(). - - Clones download options from the specified object into this object, - completely replacing this object's download options. - - Args: - - other_options_manager_obj (options.OptionsManager): The download - options object (usually the General Options Manager), from - which options will be cloned - - """ - - self.options_dict = other_options_manager_obj.options_dict.copy() - - # In the dictionary's key-value pairs, some values are themselves lists - # that must be copied directly - for key in [ - 'match_title_list', 'reject_title_list', 'video_format_list', - 'subs_lang_list', - ]: - self.options_dict[key] \ - = other_options_manager_obj.options_dict[key].copy() - - - def rearrange_formats(self): - - """Called by config.OptionsEditWin.apply_changes(). - - The option 'video_format_list' specifies video formats, audio formats - or a mixture of both. - - youtube-dl won't download the specified formats properly, if audio - formats appear before video formats. Therefore, this function is called - to rearrange the list, putting all video formats above all audio - formats. - """ - - format_list = self.options_dict['video_format_list'] - video_list = [] - audio_list = [] - comb_list = [] - - for code in format_list: - - if code != '0': - - if formats.VIDEO_OPTION_TYPE_DICT[code] is False: - video_list.append(code) - else: - audio_list.append(code) - - comb_list.extend(video_list) - comb_list.extend(audio_list) - - self.options_dict['video_format_list'] = format_list - - - def reset_options(self): - - """Called by self.__init__(). - - Resets (or initialises) self.options_dict to its default state. - """ - - if os.name == 'nt': - windows_filenames_flag = True - else: - windows_filenames_flag = False - - self.options_dict = { - # OPTIONS - 'ignore_errors': True, - 'abort_on_error': False, - 'live_from_start': False, - 'wait_for_video_min': 0, - # NETWORK OPTIONS - 'proxy': '', - 'socket_timeout': '', - 'source_address': '', - 'force_ipv4': False, - 'force_ipv6': False, - # GEO-RESTRICTION - 'geo_verification_proxy': '', - 'geo_bypass': False, - 'no_geo_bypass': False, - 'geo_bypass_country': '', - 'geo_bypass_ip_block': '', - # VIDEO SELECTION - 'playlist_start': 1, - 'playlist_end': 0, - 'playlist_items': '', - 'max_downloads': 0, - 'min_filesize': 0, - 'max_filesize': 0, - 'date': '', - 'date_before': '', - 'date_after': '', - 'min_views': 0, - 'max_views': 0, - 'match_filter': '', - 'age_limit': '', - 'include_ads': False, - # DOWNLOAD OPTIONS - 'limit_rate': '', # Can't be directly modified by user - 'retries': 10, - 'abort_on_unavailable_fragment': False, - 'playlist_reverse': False, - 'playlist_random': False, - 'native_hls': True, - 'hls_prefer_ffmpeg': False, - 'external_downloader': '', - 'external_arg_string': '', - # FILESYSTEM OPTIONS - 'restrict_filenames': False, - 'nomtime': False, - 'write_description': True, - 'write_info': True, - 'write_annotations': False, - 'cookies_path': '', - # THUMBNAIL IMAGES - 'write_thumbnail': True, - # VERBOSITY / SIMULATION OPTIONS - # (none implemented) - # WORKAROUNDS - 'force_encoding': '', - 'no_check_certificate': False, - 'prefer_insecure': False, - 'user_agent': '', - 'referer': '', - 'min_sleep_interval': 0, - 'max_sleep_interval': 0, - # VIDEO FORMAT OPTIONS - 'video_format': '0', - 'all_formats': False, - 'prefer_free_formats': False, - 'yt_skip_dash': False, - 'merge_output_format': '', - # SUBTITLE OPTIONS - 'write_subs': False, - 'write_auto_subs': False, - 'write_all_subs': False, - 'subs_format': '', - 'subs_lang': '', - # AUTHENTIFICATION OPTIONS - 'username': '', - 'password': '', - 'two_factor': '', - 'net_rc': False, - 'video_password': '', - # ADOBE PASS OPTIONS - 'ap_mso': '', - 'ap_username': '', - 'ap_password': '', - # POST-PROCESSING OPTIONS - 'extract_audio': False, - 'audio_format': '', - 'audio_quality': '5', - 'recode_video': '', - 'pp_args': '', - 'keep_video': False, - 'embed_subs': False, - 'embed_thumbnail': False, - 'add_metadata': False, - 'fixup_policy': '', - 'prefer_avconv': False, - 'prefer_ffmpeg': False, - # YT-DLP OPTIONS - # (not passed to yt-dlp directly) - 'output_format_list': [], - 'output_path_list': [], - 'extractor_args_list': [], - # (Video Selection Options) - 'break_on_existing': False, - 'break_on_reject': False, - 'skip_playlist_after_errors': 0, - # (Download Options) - 'concurrent_fragments': 1, - 'throttled_rate': 0, - # (Filesystem Options) - 'windows_filenames': windows_filenames_flag, - 'trim_filenames': 0, - 'no_overwrites': False, - 'force_overwrites': False, - 'write_playlist_metafiles': False, - 'no_clean_info_json': False, - 'no_cookies': False, - 'cookies_from_browser': '', - 'no_cookies_from_browser': True, - # (Internet Shortcut Options) - 'write_link': False, - 'write_url_link': False, - 'write_webloc_link': False, - 'write_desktop_link': False, - # (Verbosity and Simulation Options) - 'ignore_no_formats_error': False, - 'force_write_archive': False, - # (Workaround Options) - 'sleep_requests': 0, - 'sleep_subtitles': 0, - # (Video Format Options) - 'video_multistreams': False, - 'audio_multistreams': False, - 'check_formats': False, - 'allow_unplayable_formats': False, - # (Post-Processing Options) - 'remux_video': '', - 'embed_metadata': False, - 'convert_thumbnails': '', - 'split_chapters': False, - # (Extractor Options) - 'extractor_retries': '3', - 'no_allow_dynamic_mpd': False, - 'hls_split_discontinuity': False, - # YOUTUBE-DL-GUI OPTIONS - 'output_format': 2, - 'output_template': '%(title)s.%(ext)s', - 'max_filesize_unit': '', - 'min_filesize_unit': '', - 'extra_cmd_string': '', - 'direct_cmd_flag': False, - 'direct_url_flag': False, - # TARTUBE OPTIONS - 'move_description': False, - 'move_info': False, - 'move_annotations': False, - 'move_thumbnail': False, - 'keep_description': False, - 'keep_info': False, - 'keep_annotations': False, - 'keep_thumbnail': True, - 'sim_keep_description': False, - 'sim_keep_info': False, - 'sim_keep_annotations': False, - 'sim_keep_thumbnail': True, - 'use_fixed_folder': None, - 'match_title_list': [], - 'reject_title_list': [], - 'video_format_list': [], - 'video_format_mode': 'single', - 'subs_lang_list': [ 'en' ], - 'downloader_config': False, - } - - - def set_general_options(self): - - """Called by mainapp.TartubeApp.start(). - - Configures this object with a suitable description. - """ - - self.descrip = _('General (default) download options') - - - def set_classic_mode_options(self, no_descrip_flag=False): - - """Called by mainapp.TartubeApp.start() and - .apply_classic_download_options(). - - Also called by self.set_mp3_options(). - - Configures this object for use in the Classic Mode tab. - - Args: - - no_descrip_flag (bool): Set to True when called from - self.set_mp3_options() - - """ - - if not no_descrip_flag: - self.descrip = _('Download options for the Classic Mode tab') - - self.options_dict['write_description'] = False - self.options_dict['write_info'] = False - self.options_dict['write_annotations'] = False - self.options_dict['write_thumbnail'] = False - - self.options_dict['move_description'] = False - self.options_dict['move_info'] = False - self.options_dict['move_annotations'] = False - self.options_dict['move_thumbnail'] = False - - self.options_dict['keep_description'] = False - self.options_dict['keep_info'] = False - self.options_dict['keep_annotations'] = False - self.options_dict['keep_thumbnail'] = False - - self.options_dict['sim_keep_description'] = False - self.options_dict['sim_keep_info'] = False - self.options_dict['sim_keep_annotations'] = False - self.options_dict['sim_keep_thumbnail'] = False - - - def set_mp3_options(self): - - """Called by mainapp.TartubeApp.start(). - - Configures this object to download MP3s. - """ - - self.descrip = _('Download and convert to MP3 (requires FFmpeg)') - - # (Actual downloads take place in the Classic Mode tab, so use the same - # file options) - self.set_classic_mode_options(True) - - # (Convert everything to MP3) - self.options_dict['extract_audio'] = True - self.options_dict['audio_format'] = 'mp3' - - - # Set accessors - - - def add_dbid(self, dbid): - - # (Don't add duplicates. This might be a concern, when called from - # mainapp.TartubeApp.fix_integrity_db() ) - if not dbid in self.dbid_list: - self.dbid_list.append(dbid) - - - def del_dbid(self, dbid): - - self.dbid_list.remove(dbid) - - - def reset_dbid(self): - - self.dbid_list = [] - - -class OptionsParser(object): - - """Called by downloads.DownloadManager.__init__() and by - mainwin.SystemCmdDialogue.update_textbuffer(). - - This object converts the download options specified by an - options.OptionsManager object into a list of youtube-dl command line - options, whenever required. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - # IV list - class objects - # ----------------------- - # The main application - self.app_obj = app_obj - - - # IV list - other - # --------------- - # List of options.OptionHolder objects, with their initial settings - # The options here are in the same order in which they appear in - # youtube-dl's options list - self.option_holder_list = [ - # OPTIONS - # -i, --ignore-errors - OptionHolder('ignore_errors', '-i', False), - # --abort-on-error - OptionHolder('abort_on_error', '--abort-on-error', False), - # --live-from-start - OptionHolder('live_from_start', '--live-from-start', False), - # --wait-for-video MIN (N.B. the MAX value is not supported) - OptionHolder('wait_for_video_min', '--wait-for-video', 0), - # NETWORK OPTIONS - # --proxy URL - OptionHolder('proxy', '--proxy', ''), - # --socket-timeout SECONDS - OptionHolder('socket_timeout', '--socket-timeout', ''), - # --source-address IP - OptionHolder('source_address', '--source-address', ''), - # -4, --force-ipv4 - OptionHolder('force_ipv4', '--force-ipv4', False), - # -6, --force-ipv6 - OptionHolder('force_ipv6', '--force-ipv6', False), - # GEO-RESTRICTION - # --geo-verification-proxy URL - OptionHolder( - 'geo_verification_proxy', - '--geo-verification-proxy', - '', - ), - # --geo-bypass - OptionHolder('geo_bypass', '--geo-bypass', False), - # --no-geo-bypass - OptionHolder('no_geo_bypass', '--no-geo-bypass', False), - # --geo-bypass-country CODE - OptionHolder('geo_bypass_country', '--geo-bypass-country', ''), - # --geo-bypass-ip-block IP_BLOCK - OptionHolder('geo_bypass_ip_block', '--geo-bypass-ip-block', ''), - # VIDEO SELECTION - # --playlist-start NUMBER - OptionHolder('playlist_start', '--playlist-start', 1), - # --playlist-end NUMBER - OptionHolder('playlist_end', '--playlist-end', 0), - # --playlist-items ITEM_SPEC - OptionHolder('playlist_items', '--playlist-items', ''), - # --max-downloads NUMBER - OptionHolder('max_downloads', '--max-downloads', 0), - # --min-filesize SIZE - OptionHolder('min_filesize', '--min-filesize', 0), - # --max-filesize SIZE - OptionHolder('max_filesize', '--max-filesize', 0), - # --date DATE - OptionHolder('date', '--date', ''), - # --datebefore DATE - OptionHolder('date_before', '--datebefore', ''), - # --dateafter DATE - OptionHolder('date_after', '--dateafter', ''), - # --min-views COUNT - OptionHolder('min_views', '--min-views', 0), - # --max-views COUNT - OptionHolder('max_views', '--max-views', 0), - # --match-filter FILTER - OptionHolder('match_filter', '--match-filter', ''), - # --age-limit YEARS - OptionHolder('age_limit', '--age-limit', ''), - # --include-ads - OptionHolder('include_ads', '--include-ads', False), - # DOWNLOAD OPTIONS - # -r, --limit-rate RATE - OptionHolder('limit_rate', '-r', ''), - # -R, --retries RETRIES - OptionHolder('retries', '-R', 10), - # --abort-on-unavailable-fragment - OptionHolder( - 'abort_on_unavailable_fragment', - '--abort-on-unavailable-fragment', - False - ), - # --playlist-reverse - OptionHolder('playlist_reverse', '--playlist-reverse', False), - # --playlist-random - OptionHolder('playlist_random', '--playlist-random', False), - # --hls-prefer-native - OptionHolder('native_hls', '--hls-prefer-native', False), - # --hls-prefer-ffmpeg - OptionHolder('hls_prefer_ffmpeg', '--hls-prefer-ffmpeg', False), - # --external-downloader COMMAND - OptionHolder('external_downloader', '--external-downloader', ''), - # --external-downloader-args ARGS - OptionHolder( - 'external_arg_string', - '--external-downloader-args', - '', - ), - # FILESYSTEM OPTIONS - # --restrict-filenames - OptionHolder('restrict_filenames', '--restrict-filenames', False), - # --no-mtime - OptionHolder('nomtime', '--no-mtime', False), - # --write-description - OptionHolder('write_description', '--write-description', False), - # --write-info-json - OptionHolder('write_info', '--write-info-json', False), - # --write-annotations - OptionHolder('write_annotations', '--write-annotations', False), - # --cookies FILE - OptionHolder('cookies_path', '--cookies', ''), - # THUMBNAIL IMAGES - # --write-thumbnail - OptionHolder('write_thumbnail', '--write-thumbnail', False), - # VERBOSITY / SIMULATION OPTIONS - # (none implemented) - # WORKAROUNDS - # --encoding ENCODING - OptionHolder('force_encoding', '--encoding', ''), - # --no-check-certificate - OptionHolder( - 'no_check_certificate', - '--no-check-certificate', - False, - ), - # --prefer-insecure - OptionHolder('prefer_insecure', '--prefer-insecure', False), - # --user-agent UA - OptionHolder('user_agent', '--user-agent', ''), - # --referer URL - OptionHolder('referer', '--referer', ''), - # --sleep-interval SECONDS - OptionHolder('min_sleep_interval', '--sleep-interval', 0), - # --max-sleep-interval SECONDS - OptionHolder('max_sleep_interval', '--max-sleep-interval', 0), - # VIDEO FORMAT OPTIONS - # -f, --format FORMAT - OptionHolder('video_format', '-f', '0'), - # --all-formats - OptionHolder('all_formats', '--all-formats', False), - # --prefer-free-formats - OptionHolder( - 'prefer_free_formats', - '--prefer-free-formats', - False, - ), - # --youtube-skip-dash-manifest - OptionHolder( - 'yt_skip_dash', - '--youtube-skip-dash-manifest', - False, - ), - # --merge-output-format FORMAT - OptionHolder('merge_output_format', '--merge-output-format', ''), - # SUBTITLE OPTIONS - # --write-sub - OptionHolder('write_subs', '--write-sub', False), - # --write-auto-sub - OptionHolder('write_auto_subs', '--write-auto-sub', False), - # --all-subs - OptionHolder('write_all_subs', '--all-subs', False), - # --sub-format FORMAT - OptionHolder('subs_format', '--sub-format', ''), - # --sub-lang LANGS - # NB This '--sub-lang' string is not the one used as a switch by - # self.parse() - OptionHolder('subs_lang', '--sub-lang', '', ['write_subs']), - # AUTHENTIFICATION OPTIONS - # -u, --username USERNAME - OptionHolder('username', '-u', ''), - # -p, --password PASSWORD - OptionHolder('password', '-p', ''), - # -2, --twofactor TWOFACTOR - OptionHolder('two_factor', '--twofactor', ''), - # -n, --netrc - OptionHolder('net_rc', '--netrc', False), - # --video-password PASSWORD - OptionHolder('video_password', '--video-password', ''), - # ADOBE PASS OPTIONS - # --ap-mso MSO - OptionHolder('ap_mso', '--ap-mso', ''), - # --ap-username USERNAME - OptionHolder('ap_username', '--ap-username', ''), - # --ap-password PASSWORD - OptionHolder('ap_password', '--ap-password', ''), - # POST-PROCESSING OPTIONS - # -x, --extract-audio - OptionHolder('extract_audio', '-x', False), - # --audio-format FORMAT - OptionHolder('audio_format', '--audio-format', ''), - # --audio-quality QUALITY - OptionHolder( - 'audio_quality', - '--audio-quality', - '5', - ['extract_audio'], - ), - # --recode-video FORMAT - OptionHolder('recode_video', '--recode-video', ''), - # --postprocessor-args ARGS - OptionHolder('pp_args', '--postprocessor-args', ''), - # -k, --keep-video - OptionHolder('keep_video', '-k', False), - # --embed-subs - OptionHolder( - 'embed_subs', - '--embed-subs', - False, - ['write_auto_subs', 'write_subs'], - ), - # --embed-thumbnail - OptionHolder('embed_thumbnail', '--embed-thumbnail', False), - # --add-metadata - OptionHolder('add_metadata', '--add-metadata', False), - # --fixup POLICY - OptionHolder('fixup_policy', '--fixup', ''), - # --prefer-avconv - OptionHolder('prefer_avconv', '--prefer-avconv', False), - # --prefer-ffmpeg - OptionHolder('prefer_ffmpeg', '--prefer-ffmpeg', False), - # YT-DLP OPTIONS - # (not given an options.OptionHolder object) -# OptionHolder('output_format_list', '', []), -# OptionHolder('output_path_list', '', []), - # (Video Selection Options) - # --break-on-existing - OptionHolder('break_on_existing', '--break-on-existing', False), - # --break-on-reject - OptionHolder('break_on_reject', '--break-on-reject', False), - # --skip-playlist-after-errors N - OptionHolder( - 'skip_playlist_after_errors', - '--skip-playlist-after-errors', - 0, - ), - # (Download Options) - # --concurrent-fragments N - OptionHolder('concurrent_fragments', '--concurrent-fragments', 1), - # --throttled-rate RATE - OptionHolder('throttled_rate', '--throttled-rate', 0), - # (Filesystem Options) - # --windows-filenames - OptionHolder('windows_filenames', '--windows-filenames', False), - # --trim-filenames LENGTH - OptionHolder('trim_filenames', '--trim-filenames', 0), - # --no-overwrites - OptionHolder('no_overwrites', '--no-overwrites', False), - # --force-overwrites - OptionHolder('force_overwrites', '--force-overwrites', False), - # --write-playlist-metafiles - OptionHolder( - 'write_playlist_metafiles', - '--write-playlist-metafiles', - False, - ), - # --no-clean-infojson - OptionHolder('no_clean_info_json', '--no-clean-infojson', False), - # --no-cookies - OptionHolder('no_cookies', '--no-cookies', False), - # --cookies-from-browser BROWSER[+KEYRING][:PROFILE] - OptionHolder('cookies_from_browser', '--cookies-from-browser', ''), - # --no-cookies-from-browser - OptionHolder( - 'no_cookies_from_browser', - '--no-cookies-from-browser', - True, - ), - # (Internet Shortcut Options) - # --write-link - OptionHolder('write_link', '--write-link', False), - # --write-url-link - OptionHolder('write_url_link', '--write-url-link', False), - # --write-webloc-link - OptionHolder('write_webloc_link', '--write-webloc-link', False), - # --write-desktop-link - OptionHolder('write_desktop_link', '--write-desktop-link', False), - # (Verbosity and Simulation Options) - # --ignore-no-formats-error - OptionHolder( - 'ignore_no_formats_error', - '--ignore-no-formats-error', - False, - ), - # --force-write-archive - OptionHolder( - 'force_write_archive', - '--force-write-archive', - False, - ), - # (Workaround Options) - # --sleep-requests SECONDS - OptionHolder('sleep_requests', '--sleep-requests', 0), - # --sleep-subtitles SECONDS - OptionHolder('sleep_subtitles', '--sleep-subtitles', 0), - # (Video Format Options) - # --video-multistreams - OptionHolder('video_multistreams', '--video-multistreams', False), - # --audio-multistreams - OptionHolder('audio_multistreams', '--audio-multistreams', False), - # --check-formats - OptionHolder('check_formats', '--check-formats', False), - # --allow-unplayable-formats - OptionHolder( - 'allow_unplayable_formats', - '--allow-unplayable-formats', - False, - ), - # (Post-Processing Options) - # --remux-video FORMAT - OptionHolder('remux_video', '--remux-video', ''), - # --embed-metadata - OptionHolder('embed_metadata', '--embed-metadata', False), - # --convert-thumbnails FORMAT - OptionHolder('convert_thumbnails', '--convert-thumbnails', ''), - # --split-chapters - OptionHolder('split_chapters', '--split-chapters', False), - # (Extractor Options) - # --extractor-retries RETRIES - OptionHolder('extractor_retries', '--extractor-retries', '3'), - # --no-allow-dynamic-mpd - OptionHolder( - 'no_allow_dynamic_mpd', - '--no-allow-dynamic-mpd', - False, - ), - # --hls-split-discontinuity - OptionHolder( - 'hls_split_discontinuity', - '--hls-split-discontinuity', - False, - ), - # YOUTUBE-DL-GUI OPTIONS (not given an options.OptionHolder object) -# OptionHolder('output_format', '', 2), -# OptionHolder('output_template', '', ''), -# OptionHolder('max_filesize_unit', '', ''), -# OptionHolder('min_filesize_unit', '', ''), -# OptionHolder('extra_cmd_string', '', ''), -# OptionHolder('direct_cmd_flag', '', False), -# OptionHolder('direct_url_flag', '', False), - # TARTUBE OPTIONS (not given an options.OptionHolder object) -# OptionHolder('move_description', '', False), -# OptionHolder('move_info', '', False), -# OptionHolder('move_annotations', '', False), -# OptionHolder('move_thumbnail', '', False), -# OptionHolder('keep_description', '', False), -# OptionHolder('keep_info', '', False), -# OptionHolder('keep_annotations', '', False), -# OptionHolder('keep_thumbnail', '', False), -# OptionHolder('sim_keep_description', '', False), -# OptionHolder('sim_keep_info', '', False), -# OptionHolder('sim_keep_annotations', '', False), -# OptionHolder('sim_keep_thumbnail', '', False), -# OptionHolder('use_fixed_folder', '', None), -# OptionHolder('match_title_list', '', []), -# OptionHolder('reject_title_list', '', []), -# OptionHolder('video_format_list', '', []), -# OptionHolder('video_format_mode', '', 'single'), -# OptionHolder('subs_lang_list', '', []), -# OptionHolder('downloader_config', '', False), - ] - - - # Public class methods - - - def parse(self, media_data_obj, options_manager_obj, - operation_type='real', scheduled_obj=None): - - """Called by downloads.DownloadWorker.prepare_download() and - mainwin.MainWin.update_textbuffer() and several other functions. - - Converts the download options stored in the specified - options.OptionsManager object into a list of youtube-dl command line - options. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object being downloaded - - options_manager_obj (options.OptionsManager): The object containing - the download options for this media data object - - operation_type (str): 'sim', 'real', 'custom_sim', 'custom_real', - 'classic_sim', 'classic_real', 'classic_custom' (matching - possible values of downloads.DownloadManager.operation_type) - - scheduled_obj (media.Scheduled): If a scheduled download is - involved, the corresponding object (so bandwidth limits can be - extracted) - - Return values: - - List of strings with all the youtube-dl command line options - - """ - - # Force youtube-dl's progress bar to be outputted as separate lines - options_list = ['--newline'] - - # Create a copy of the dictionary - copy_dict = options_manager_obj.options_dict.copy() - - # Get the directory into which files are downloaded - dir_path = self.build_dir( - media_data_obj, - copy_dict, - operation_type, - ) - - # Modify various values in the copy. Set the 'video_format' and - # 'all_formats' options - self.build_video_format(media_data_obj, copy_dict, operation_type) - # Set the 'min_filesize' and 'max_filesize' options - self.build_file_sizes(copy_dict) - # Set the 'limit_rate' option - self.build_limit_rate(copy_dict, scheduled_obj) - # Set the 'proxy' option - self.build_proxy(copy_dict) - - # Parse basic youtube-dl command line options - for option_holder_obj in self.option_holder_list: - - # First deal with special cases... - if option_holder_obj.name == 'extract_audio': - if copy_dict['audio_format'] == '': - value = copy_dict[option_holder_obj.name] - - if value != option_holder_obj.default_value: - options_list.append(option_holder_obj.switch) - - elif option_holder_obj.name == 'audio_format': - value = copy_dict[option_holder_obj.name] - - if copy_dict['extract_audio'] \ - and value != option_holder_obj.default_value: - options_list.append('-x') - options_list.append(option_holder_obj.switch) - options_list.append(utils.to_string(value)) - - # The '-x' / '--audio-quality' switch must precede the - # '--audio-quality' switch, if both are used - # Therefore, if the current value of the 'audio_quality' - # option is not the default value ('5'), then insert the - # '--audio-quality' switch into the options list right - # now - if copy_dict['audio_quality'] != '5': - options_list.append('--audio-quality') - options_list.append( - utils.to_string(copy_dict['audio_quality']), - ) - - elif option_holder_obj.name == 'audio_quality': - # If the '--audio-quality' switch was not added by the code - # block just above, then follow the standard procedure - if option_holder_obj.switch not in options_list: - if option_holder_obj.check_requirements(copy_dict): - value = copy_dict[option_holder_obj.name] - - if value != option_holder_obj.default_value: - options_list.append(option_holder_obj.switch) - options_list.append(utils.to_string(value)) - - elif option_holder_obj.name == 'match_filter': - value = utils.to_string(copy_dict[option_holder_obj.name]) - if self.app_obj.block_livestreams_flag: - - if value == '': - value = '!is_live' - else: - value += ' \& !is_live' - - if value != '': - options_list.append(option_holder_obj.switch) - options_list.append(value) - - elif option_holder_obj.name == 'external_arg_string' \ - or option_holder_obj.name == 'pp_args': - value = copy_dict[option_holder_obj.name] - if value != '': - options_list.append(option_holder_obj.switch) - options_list.append('"' + utils.to_string(value) + '"') - - elif option_holder_obj.name == 'cookies_path': - cookies_path = copy_dict[option_holder_obj.name] - options_list.append('--cookies') - # If no path is specified, use a standard location for the - # cookie jar (otherwise youtube-dl will write it to - # ../tartube/tartube) - if cookies_path == '': - options_list.append( - os.path.abspath( - os.path.join( - self.app_obj.data_dir, - self.app_obj.cookie_file_name, - ), - ), - ) - else: - options_list.append(cookies_path) - - elif option_holder_obj.name == 'trim_filenames': - length = copy_dict[option_holder_obj.name] - # The --trim-filenames option specifies a length that includes - # the directory name. If the user has specified a length that - # is shorter than the directory name, then ignore this - # option - if length >= (len(dir_path) + 2): - options_list.append('--trim-filenames') - options_list.append(str(length)) - - # For all other options, just check the value is valid - elif option_holder_obj.check_requirements(copy_dict): - value = copy_dict[option_holder_obj.name] - - if value != option_holder_obj.default_value: - options_list.append(option_holder_obj.switch) - - if not option_holder_obj.is_boolean(): - options_list.append(utils.to_string(value)) - - # Parse the 'match_title_list' and 'reject_title_list' - for item in copy_dict['match_title_list']: - options_list.append('--match-title') - options_list.append(item) - - for item in copy_dict['reject_title_list']: - options_list.append('--reject-title') - options_list.append(item) - - # Parse the 'subs_lang_list' option - if copy_dict['write_subs'] \ - and not copy_dict['write_auto_subs'] \ - and not copy_dict['write_all_subs'] \ - and copy_dict['subs_lang_list']: - - options_list.append('--sub-lang') - options_list.append(','.join(copy_dict['subs_lang_list'])) - - # Parse the 'extractor_args_list' option - for item in copy_dict['extractor_args_list']: - options_list.append('--extractor-args') - options_list.append(item) - - # Add the SponsorBlock option used in Classic Mode downloads - if ( - operation_type == 'classic_sim' \ - or operation_type == 'classic_real' \ - or operation_type == 'classic_custom' - ) and media_data_obj.dummy_sblock_flag: - options_list.append('--sponsorblock-remove') - options_list.append('default') - - # Build the --output and --paths options - options_list = self.build_paths( - media_data_obj, - dir_path, - copy_dict, - options_list, - ) - - # Parse the 'extra_cmd_string' option, so it overrules everything else. - # The option can contain arguments inside double quotes "..." - # (arguments that can therefore contain whitespace) - parsed_list = utils.parse_options(copy_dict['extra_cmd_string']) - for item in parsed_list: - options_list.append(item) - - # Parse the 'downloader_config' option, so it overrules everything - # else - if copy_dict['downloader_config']: - - options_list.append('--config-location') - options_list.append(utils.get_dl_config_path(self.app_obj)) - - # Filter out yt-dlp options, if required. A list of them is specified - # in mainapp.TartubeApp.ytdlp_exclusive_options_dict - if self.app_obj.ytdlp_filter_options_flag \ - and ( - self.app_obj.ytdl_fork is None \ - or self.app_obj.ytdl_fork != 'yt-dlp' - ): - filter_list = options_list.copy() - options_list = [] - - while filter_list: - - item = filter_list.pop(0) - if item in self.app_obj.ytdlp_exclusive_options_dict: - - if self.app_obj.ytdlp_exclusive_options_dict[item]: - # This option takes an argument - filter_list.pop(0) - - else: - options_list.append(item) - - # Parsing complete - return options_list - - - def build_file_sizes(self, copy_dict): - - """Called by self.parse(). - - Build the value of the 'min_filesize' and 'max_filesize' options and - store them in the options dictionary. - - Args: - - copy_dict (dict): Copy of the original options dictionary. - - """ - - if copy_dict['min_filesize']: - copy_dict['min_filesize'] = \ - utils.to_string(copy_dict['min_filesize']) + \ - copy_dict['min_filesize_unit'] - - if copy_dict['max_filesize']: - copy_dict['max_filesize'] = \ - utils.to_string(copy_dict['max_filesize']) + \ - copy_dict['max_filesize_unit'] - - - def build_limit_rate(self, copy_dict, scheduled_obj): - - """Called by self.parse(). - - Build the value of the 'limit_rate' option and store it in the options - dictionary. - - Args: - - copy_dict (dict): Copy of the original options dictionary - - scheduled_obj (media.Scheduled): If a scheduled download is - involved, the corresponding object (so bandwidth limits can be - extracted) - - """ - - # Set the bandwidth limit (e.g. '50K'). If alternative performance - # limits currently apply, use that limit instead - if self.app_obj.download_manager_obj \ - and scheduled_obj \ - and scheduled_obj.scheduled_bandwidth_apply_flag: - - # The bandwidth limit is divided equally between the workers - limit = int( - scheduled_obj.scheduled_bandwidth - / len(self.app_obj.download_manager_obj.worker_list) - ) - - copy_dict['limit_rate'] = str(limit) + 'K' - - elif self.app_obj.download_manager_obj \ - and self.app_obj.download_manager_obj.alt_limits_flag \ - and self.app_obj.alt_bandwidth_apply_flag: - - limit = int( - self.app_obj.alt_bandwidth - / self.app_obj.alt_num_worker - ) - - copy_dict['limit_rate'] = str(limit) + 'K' - - elif self.app_obj.bandwidth_apply_flag: - - limit = int( - self.app_obj.bandwidth_default - / self.app_obj.num_worker_default - ) - - copy_dict['limit_rate'] = str(limit) + 'K' - - - def build_proxy(self, copy_dict): - - """Called by self.parse(). - - Build the value of the 'proxy' option and store it in the options - dictionary. - - Args: - - copy_dict (dict): Copy of the original options dictionary. - - """ - - # If the option is already specified, we use it. Otherwise, cycle - # through the proxies in the main appliation's list - if not copy_dict['proxy']: - - proxy = self.app_obj.get_proxy() - if proxy is not None: - - copy_dict['proxy'] = proxy - - - def build_dir(self, media_data_obj, copy_dict, operation_type): - - """Called by self.parse(). - - Finds the directory in which downloaded files are saved. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object being downloaded - - copy_dict (dict): Copy of the original options dictionary - - operation_type (str): 'sim', 'real', 'custom_sim', 'custom_real', - 'classic_sim', 'classic_real', 'classic_custom' (matching - possible values of downloads.DownloadManager.operation_type) - - Return values: - - The output directory, as a string - - """ - - # Fetch the equivalent fixed folder 'Temporary Videos', 'Unsorted - # Videos' or 'Video Clips', if specified - override_obj = self.app_obj.get_fixed_folder( - copy_dict['use_fixed_folder'] - ) - - if operation_type == 'classic_sim' \ - or operation_type == 'classic_real' \ - or operation_type == 'classic_custom': - - # Special case: if a download operation was launched from the - # Classic Mode tab, the directory is specified in that tab - dir_path = media_data_obj.dummy_dir - - elif not isinstance(media_data_obj, media.Video) \ - and override_obj is not None: - - # Because of the override, save all videos to a system folder - override_obj = other_obj.get_default_dir(self.app_obj) - - elif isinstance(media_data_obj, media.Video): - dir_path = media_data_obj.parent_obj.get_actual_dir(self.app_obj) - - else: - dir_path = media_data_obj.get_actual_dir(self.app_obj) - - return dir_path - - - def build_paths(self, media_data_obj, dir_path, copy_dict, options_list): - - """Called by self.parse(). - - Build the --output and --paths options, adding them directly to the - options list. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object being downloaded - - dir_path (str): The directory into which files are to be downloaded - - copy_dict (dict): Copy of the original options dictionary - - options_list (list): List of download options compiled so far; this - function adds options directly to the list - - Return values: - - The modified options_list - - """ - - # (When 'output_format_list' is specified, then 'output_format' and - # 'output_template' are ignored. However, 'output_format_list' is - # normally only used with yt-dlp) - if not copy_dict['output_format_list'] \ - or self.app_obj.ytdl_fork is None \ - or self.app_obj.ytdl_fork != 'yt-dlp' \ - or not self.app_obj.ytdlp_filter_options_flag: - # Set the youtube-dl output template for the video's file - template \ - = formats.FILE_OUTPUT_CONVERT_DICT[copy_dict['output_format']] - # In the case of copy_dict['output_format'] = 0 - if template is None: - template = copy_dict['output_template'] - - options_list.append('--output') - - # (When 'output_path_list' is specified, the template is used - # without a preceding directory path) - if copy_dict['output_path_list']: - options_list.append(template) - else: - options_list.append( - os.path.abspath(os.path.join(dir_path, template)), - ) - - else: - - for item in copy_dict['output_format_list']: - options_list.append('--output') - options_list.append(item) - - # Thirdly, set the yt-dlp option 'output_path_list' - if copy_dict['output_path_list']: - - for item in copy_dict['output_path_list']: - options_list.append('--paths') - options_list.append(item) - - # Lastly, apply the output override for a media.Video object, if one - # is specified - if media_data_obj.dbid in self.app_obj.temp_output_override_dict: - - # Add it to the end of 'options_list', so anyone examining the - # yellow text in the Output Tab can clearly see that this is - # an override - name = self.app_obj.temp_output_override_dict[media_data_obj.dbid] - options_list.append('--output') - options_list.append( - os.path.abspath(os.path.join(dir_path, name + '.%(ext)s')), - ) - - return options_list - - - def build_video_format(self, media_data_obj, copy_dict, operation_type): - - """Called by self.parse(). - - Build the value of the 'video_format' and 'all_formats' options and - store them in the options dictionary. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object being downloaded - - copy_dict (dict): Copy of the original options dictionary - - operation_type (str): 'sim', 'real', 'custom_sim', 'custom_real', - 'classic_sim', 'classic_real', 'classic_custom' (matching - possible values of downloads.DownloadManager.operation_type) - - """ - - if isinstance(media_data_obj, media.Video): - - # Special case: if a download operation was launched from the - # Classic Mode tab, the video format may be specified by that tab - if ( - operation_type == 'classic_sim' \ - or operation_type == 'classic_real' \ - or operation_type == 'classic_custom' - ) and media_data_obj.dummy_format is not None: - - # A string specifying the media format to download, or None if - # the user didn't specify one - # The string is made up of three optional components in a fixed - # order and separated by underlines: 'convert', the video/ - # audio format, and the video resolution, for example 'mp4', - # 'mp4_720p', 'convert_mp4_720p' - # Valid values are those specified by formats.VIDEO_FORMAT_LIST - # formats.AUDIO_FORMAT_LIST and formats.VIDEO_RESOLUTION_LIST - convert_flag, this_format, this_res \ - = utils.extract_dummy_format(media_data_obj.dummy_format) - - if not convert_flag and this_res is None: - - # Download the video in the specified format and - # resolution, if available - - # Ignore all video/audio formats except the one specified - # by the user in the Classic Mode tab - copy_dict['video_format'] = this_format - copy_dict['all_formats'] = False - copy_dict['video_format_list'] = [] - copy_dict['video_format_mode'] = '' - copy_dict['recode_video'] = '' - - # v2.1.009: Since the user doesn't have the possibility of - # setting the -f and --merge-output-format options to the - # same value (e.g. 'mp4'), we must do so artificially - # yt-dlp supports: avi, flv, mkv, mov, mp4, webm - if this_format in formats.VIDEO_FORMAT_DICT \ - and this_format != 'ogg': - copy_dict['merge_output_format'] = this_format - - return - - elif this_format is not None \ - and this_format in formats.VIDEO_FORMAT_DICT: - - # Converting video formats requires post-processing - # Ignore all video/audio formats except the one specified - # by the user in the Classic Mode tab - if this_res is None: - copy_dict['video_format'] = '0' - else: - copy_dict['video_format'] \ - = 'bestvideo[height<=?' + this_res + ']' \ - + '+bestaudio/best[height<=?' + this_res + ']' - - copy_dict['all_formats'] = False - copy_dict['video_format_list'] = [] - copy_dict['video_format_mode'] = '' - copy_dict['recode_video'] = this_format - - return - - elif this_format is not None \ - and this_format in formats.AUDIO_FORMAT_DICT: - - # Converting audio formats requires post-processing - copy_dict['video_format'] = '0' - copy_dict['all_formats'] = False - copy_dict['video_format_list'] = [] - copy_dict['video_format_mode'] = '' - copy_dict['extract_audio'] = True - copy_dict['audio_format'] = this_format - copy_dict['recode_video'] = '' - - return - - elif this_format is None and this_res is not None: - - copy_dict['video_format'] = \ - 'bestvideo[height<=?' + this_res + ']' \ - + '+bestaudio/best[height<=?' + this_res + ']' - copy_dict['all_formats'] = False - copy_dict['video_format_list'] = [] - copy_dict['video_format_mode'] = '' - copy_dict['recode_video'] = '' - - # Special case: for simulated downloads, don't specify any video - # formats; if the format isn't available for some videos, we'll get - # an error for each of them (rather than the simulated download we - # were hoping for) - if operation_type == 'sim' \ - or operation_type == 'classic_sim' \ - or operation_type == 'custom_sim': - - copy_dict['video_format'] = '0' - copy_dict['all_formats'] = False - copy_dict['video_format_list'] = [] - copy_dict['video_format_mode'] = '' - - return - - # The 'video_format_list' options contains values corresponding to the - # keys in formats.VIDEO_OPTION_DICT, which are either real extractor - # codes (e.g. '35' representing 'flv [480p]') or dummy extractor - # codes (e.g. 'mp4') - # Some dummy extractor codes are in the form '720p', '1080p60' etc, - # representing progressive scan resolutions. If the user specifies - # at least one of those codes, the first one is used, and all other - # extractor codes are ignored - video_format_list = copy_dict['video_format_list'] - resolution_dict = formats.VIDEO_RESOLUTION_DICT.copy() - fps_dict = formats.VIDEO_FPS_DICT.copy() - - # If the progressive scan resolution is specified, it overrides all - # other video format options - height = None - fps = None - - if self.app_obj.video_res_apply_flag: - height = resolution_dict[self.app_obj.video_res_default] - # (Currently, formats.VIDEO_FPS_DICT only lists formats with 60fps) - if self.app_obj.video_res_default in fps_dict: - fps = fps_dict[self.app_obj.video_res_default] - - else: - - for item in video_format_list: - - if item in resolution_dict: - height = resolution_dict[item] - if item in fps_dict: - fps = fps_dict[item] - - break - - if height is not None: - - # (Currently, formats.VIDEO_FPS_DICT only lists formats with 60fps) - if fps is None: - - # Use a youtube-dl argument in the form - # 'bestvideo[height<=?height]+bestaudio/best[height<=height]' - copy_dict['video_format'] = 'bestvideo[height<=?' \ - + str(height) + ']+bestaudio/best[height<=?' + str(height) \ - + ']' - - else: - - copy_dict['video_format'] = 'bestvideo[height<=?' \ - + str(height) + '][fps<=?' + str(fps) \ - + ']+bestaudio/best[height<=?' + str(height) + ']' - - copy_dict['all_formats'] = False - copy_dict['video_format_list'] = [] - copy_dict['video_format_mode'] = '' - - # Not using a progressive scan resolution - elif video_format_list: - - video_format_mode = copy_dict['video_format_mode'] - - if video_format_mode == 'all': - copy_dict['video_format'] = 0 - copy_dict['all_formats'] = True - - else: - - copy_dict['all_formats'] = False - - if video_format_mode == 'single_agree': - char = '/' - elif video_format_mode == 'multiple': - char = ',' - else: - # mode is 'single' - char = '+' - - copy_dict['video_format'] = char.join(video_format_list) - - copy_dict['video_format_list'] = [] - copy_dict['video_format_mode'] = '' - - -class OptionHolder(object): - - """Called from options.OptionsParser.__init__(). - - The options parser object converts the download options specified by an - options.OptionsManager object into a list of youtube-dl command line - options, whenever required. - - Each option has a name, a command line switch, a default value and an - optional list of requirements; they are stored together in an instance of - this object. - - Args: - - name (str): Option name. Must be a valid option name from the - optionsmanager.OptionsManager class (see the list in at the - beginning of the options.OptionsManager class) - - switch (str): The option command line switch. See - https://github.com/rg3/youtube-dl/#options - - default_value (any): The option default value. Must be the same type - as the corresponding option from the optionsmanager.OptionsManager - class. - - requirement_list (list): The requirements for the given option. This - argument is a list of strings with the name of all the options - that this specific option needs. If there are no requirements, the - IV is set to None. (For example 'subs_lang' needs the 'write_subs' - option to be enabled.) - - """ - - - # Standard class methods - - - def __init__(self, name, switch, default_value, requirement_list=None): - - # IV list - other - # --------------- - self.name = name - self.switch = switch - self.default_value = default_value - self.requirement_list = requirement_list - - - # Public class methods - - - def check_requirements(self, copy_dict): - - """Called by options.OptionsParser.parse(). - - Check if options required by another option are enabled, or not. - - Args: - - copy_dict (dict): Copy of the original options dictionary. - - Return values: - - True if any of the required options is enabled, otherwise returns - False. - - """ - - if not self.requirement_list: - return True - - return any([copy_dict[req] for req in self.requirement_list]) - - - def is_boolean(self): - - """Called by options.OptionsParser.parse(). - - Return values: - - True if the option is a boolean switch, otherwise returns False - - """ - - return type(self.default_value) is bool diff --git a/build/lib/tartube/process.py b/build/lib/tartube/process.py deleted file mode 100644 index 3dfff024..00000000 --- a/build/lib/tartube/process.py +++ /dev/null @@ -1,1012 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Process operation classes.""" - - -# Import Gtk modules -import gi -from gi.repository import GObject - - -# Import other modules -import os -import re -import shutil -import subprocess -import threading -import time - - -# Import our modules -import media -import utils -# Use same gettext translations -from mainapp import _ - - -# Classes - - -class ProcessManager(threading.Thread): - - """Called by mainapp.TartubeApp.process_manager_start(). - - Python class to manage the process operation, in which media.Video objects - are processed with FFmpeg, using the options specified by a - ffmpeg_tartube.FFmpegOptionsManager object. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - options_obj (ffmpeg_tartube.FFmpegOptionsManager): Object specifying - the FFmpeg options to apply - - video_list (list): A list of media.Video objects - - """ - - - # Standard class methods - - - def __init__(self, app_obj, options_obj, video_list): - - super(ProcessManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # ffmpeg_tartube.FFmpegOptionsManager object specifying the FFmpeg - # options to apply - self.options_obj = options_obj - # A list of media.Video objects to be processed with FFmpeg - self.video_list = video_list - - # The child process created by self.create_child_process(). Used only - # by self.slice_video() to concatenate video clips; all other child - # processes are handled by ffmpeg_tartube.FFmpegManager - self.child_process = None - - # IV list - other - # --------------- - # Flag set to False if self.stop_process_operation() is called, which - # halts the operation immediately - self.running_flag = True - - # The time at which the process operation began (in seconds since - # epoch) - self.start_time = int(time.time()) - # The time at which the process operation completed (in seconds since - # epoch) - self.stop_time = None - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # The number of media.Video objects processed so far... - self.job_count = 0 - # ...and the total number to process (these numbers are displayed in - # the progress bar in the Videos tab) - self.job_total = len(video_list) - # The total number of successful and failed FFmpeg procedures - self.success_count = 0 - self.fail_count = 0 - # Flag set to True if any video file is successfully split (which - # may require mainapp.TartubeApp.update_manager_finished to redraw - # the Video Index and Video Catalogue) - self.split_success_flag = False - # Flag set to True if a fatal error occurs - self.fatal_error_flag = False - - # Dictionary of clip tiles used during this operation (i.e. when - # splitting a video into clips), used to re-name duplicates - self.clip_title_dict = {} - - # List of new media.Video objects added to the database. At the end - # of the operation, we try to detect their video length/file size in - # the usual way - self.new_video_list = [] - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Calls FFmpegManager.run_ffmpeg for every media.Video object in the - list. - - Then informs the main application that the process operation is - complete. - """ - - # Show information about the process operation in the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Starting process operation'), - ) - - # Process each video in turn - dest_dir_list = [] - check_dict = {} - while self.running_flag and self.video_list: - - video_obj = self.video_list.pop(0) - self.job_count += 1 - - # Update our progress in the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Video') + ' ' + str(self.job_count) + '/' \ - + str(self.job_total) + ': ' + video_obj.name, - ) - - default_flag = False - if video_obj.dbid in self.app_obj.temp_stamp_buffer_dict: - - # Split the video into video clips, using the timestamps - # specified directly by the user (instead of the one - # specified by the media.Video object) - dest_dir = self.split_video(video_obj) - - elif video_obj.dbid in self.app_obj.temp_slice_buffer_dict: - - # Produce a single output video with slices removed, using the - # slices specified directly by the user (instead of the one - # specified by the media.Video object) - dest_dir = self.slice_video(video_obj) - - elif self.options_obj.options_dict['output_mode'] == 'split': - - # Split the video into video clips, using the .stamp_list - # specified by the media.Video object - dest_dir = self.split_video(video_obj) - - elif self.options_obj.options_dict['output_mode'] == 'slice': - - # Produce a single output video with slices removed, using the - # .slice_list specified by the media.Video object - dest_dir = self.slice_video(video_obj) - - else: - - # Process the video with FFmpeg. One source video produces one - # output video - self.process_video(video_obj) - default_flag = True - - if not default_flag: - - if self.fatal_error_flag: - # This is a fatal error - break - - else: - # Add the returned destination directory to a list, - # first checking for duplicates - if not dest_dir in check_dict: - dest_dir_list.append(dest_dir) - check_dict[dest_dir] = None - - # Pause a moment, before the next iteration of the loop (don't want - # to hog resources) - time.sleep(self.sleep_time) - - # Operation complete. Set the stop time - self.stop_time = int(time.time()) - - # Show a confirmation in the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Process operation finished'), - ) - - # Let the timer run for a few more seconds to prevent Gtk errors - GObject.timeout_add( - 0, - self.app_obj.process_manager_halt_timer, - ) - - # Open the destination directories, if required - if self.options_obj.options_dict['output_mode'] == 'split' \ - and self.app_obj.split_video_auto_open_flag: - - for dest_dir in dest_dir_list: - utils.open_file(self.app_obj, dest_dir) - - - def create_child_process(self, cmd_list): - - """Called by self.slice_video() only, in order to concatenate video - clips into a single video file. All other child processes are handled - by ffmpeg_tartube.FFmpegManager. - - Based on YoutubeDLDownloader._create_process(). - - Executes the system command, creating a new child process which - concatenates files. - - Args: - - cmd_list (list): Python list that contains the command to execute. - - """ - - # Strip double quotes from arguments - # (Since we're sending the system command one argument at a time, we - # don't need to retain the double quotes around any single argument - # and, in fact, doing so would cause an error) - cmd_list = utils.strip_double_quotes(cmd_list) - - # Create the child process - info = preexec = None - - if os.name == 'nt': - # Hide the child process window that MS Windows helpfully creates - # for us - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - # Make this child process the process group leader, so that we can - # later kill the whole process group with os.killpg - preexec = os.setsid - - try: - self.child_process = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=preexec, - startupinfo=info, - ) - - except (ValueError, OSError) as error: - pass - - - def create_temp_dir(self, orig_video_obj, parent_dir): - - """Called by self.slice_video(). - - Before splitting a video into clips, and then concatenating the clips, - create a temporary directory for the clips so we don't accidentally - overwrite anything. - - Args: - - orig_video_obj (media.Video): The video to be split - - parent_dir (str): Full path to the parent container's directory - - Return values: - - The temporary directory created on success, None on failure - - """ - - # Work out where the temporary directory should be... - temp_dir = os.path.abspath( - os.path.join( - parent_dir, - '.clips_' + str(orig_video_obj.dbid) - ), - ) - - # ...then create it - try: - if os.path.isdir(temp_dir): - self.app_obj.remove_directory(temp_dir) - - self.app_obj.make_directory(temp_dir) - - return temp_dir - - except: - return None - - - def is_child_process_alive(self): - - """Called by self.split_video(). - - Based on YoutubeDLDownloader._proc_is_alive(). - - Called continuously during concatenation of video clips to check - whether the child process has finished or not. - - Return values: - - True if the child process is alive, otherwise returns False. - - """ - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def process_video(self, orig_video_obj, dest_dir=None, start_point=None, \ - stop_point=None, clip_title=None, override_output_mode=None): - - """Called by self.run(), .slice_video() and .split_video(). - - Sends a single video to FFmpeg for post-processing. - - Args: - - orig_video_obj (media.Video): The video to be sent to FFmpeg - - dest_dir (str): When splitting a video, the directory into which - the video clips are saved (which may or may not be the same as - the directory of the original file). Depending on settings, it - may be the directory for a media.Folder object, or not. Not - specified when not splitting a video - - start_point, stop_point (str): When splitting a video, the - timestamps at which to start/stop (e.g. '15:29'). If - 'stop_point' is not specified, the clip ends at the end of - the video. When removing video slices, the time (in seconds) - at the beginning/end of each slice. If 'stop_point' is not - specified, the slice ends at the end of the video - - clip_title (str): When splitting a video, the title of this video - clip (if specified) - - override_output_mode (str): When splitting/slicing a video, and the - user has specified their own .stamp_list or .slice_list, then - this value is set to 'split' or 'slice', overriding the - 'output_mode' of the FFmpegOptionsManager. Otherwise set to - None - - Return values: - - True of success, False on failure - - """ - - # mainwin.MainWin.on_video_catalogue_process_ffmpeg_multi() should have - # filtered any media.Video objects whose .file_name is unknown, but - # just in case, check again - # (Special case: 'dummy' video objects (those downloaded in the Classic - # Mode tab) use different IVs) - if orig_video_obj.file_name is None \ - and ( - not orig_video_obj.dummy_flag - or orig_video_obj.dummy_path is None - ): - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('FAILED: File name is not known'), - ) - - self.fail_count += 1 - - return False - - # Get the source/output files, ahd the full FFmpeg system command (as a - # list, and including the source/output files) - if override_output_mode is None: - - source_path, output_path, cmd_list \ - = self.options_obj.get_system_cmd( - self.app_obj, - orig_video_obj, - start_point, - stop_point, - clip_title, - dest_dir, - ) - - else: - - source_path, output_path, cmd_list \ - = self.options_obj.get_system_cmd( - self.app_obj, - orig_video_obj, - start_point, - stop_point, - clip_title, - dest_dir, - { 'output_mode': override_output_mode }, - ) - - if source_path is None: - - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('FAILED: File not found'), - ) - - self.fail_count += 1 - - return False - - # Update the main window's progress bar - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - orig_video_obj.name, - self.job_count, - self.job_total, - ) - - # Update the Output tab again - self.app_obj.main_win_obj.output_tab_write_system_cmd( - 1, - ' '.join(cmd_list), - ) - - # Process the video - success_flag, msg \ - = self.app_obj.ffmpeg_manager_obj.run_ffmpeg_directly( - orig_video_obj, - source_path, - cmd_list, - ) - - if not success_flag: - - self.fail_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('FAILED:') + ' ' + msg, - ) - - return False - - else: - - self.success_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Output file:') + ' ' + output_path, - ) - - # (If splitting files, there is nothing more to do) - if start_point is not None: - return True - - # Otherwise, delete the original video file, if required - if self.options_obj.options_dict['delete_original_flag'] \ - and os.path.isfile(source_path) \ - and os.path.isfile(output_path) \ - and source_path != output_path: - - if not self.app_obj.remove_file(source_path): - self.fail_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('Could not delete the original file:') + ' ' \ - + source_path, - ) - - # Ignoring changes to the extension, has the video/audio filename - # changed? - new_dir, new_file = os.path.split(output_path) - new_name, new_ext = os.path.splitext(new_file) - old_name = orig_video_obj.name - - rename_flag = False - if ( - self.options_obj.options_dict['add_end_filename'] != '' \ - or self.options_obj.options_dict['regex_match_filename'] \ - != '' \ - ) and old_name != new_name: - rename_flag = True - - # If the flag is set, rename a thumbnail file to match the - # video file - if rename_flag \ - and self.options_obj.options_dict['rename_both_flag']: - - thumb_path = utils.find_thumbnail( - self.app_obj, - orig_video_obj, - True, # Rename a temporary thumbnail too - ) - - if thumb_path: - - thumb_name, thumb_ext = os.path.splitext(thumb_path) - new_thumb_path = os.path.abspath( - os.path.join( - new_dir, - new_name + thumb_ext, - ), - ) - - # (On MSWin, can't do os.rename if the destination file - # already exists) - if os.path.isfile(new_thumb_path): - self.app_obj.remove_file(new_thumb_path) - - # (os.rename sometimes fails on external hard drives; this - # is safer) - if not self.app_obj.move_file_or_directory( - thumb_path, - new_thumb_path, - ): - self.fail_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('Could not rename the thumbnail:') + ' ' \ - + thumb_path, - ) - - # If a video/audio file was processed, update its filename - if self.options_obj.options_dict['input_mode'] != 'thumb': - - if not orig_video_obj.dummy_flag: - orig_video_obj.set_file_from_path(output_path) - else: - orig_video_obj.set_dummy_path(output_path) - - # Also update its .name IV (but its .nickname) - if rename_flag: - orig_video_obj.set_name(new_name) - - return True - - - def slice_video(self, orig_video_obj): - - """Called by self.run(). - - Removes slices from a video using FFmpeg. - - Args: - - orig_video_obj (media.Video): The video to be sent to FFmpeg - - Return values: - - The video's parent directory on success, None on failure - - """ - - # Contact the SponsorBlock server to update the video's slice data, if - # allowed - # (No point doing it, if the temporary buffer is set) - if not orig_video_obj.dbid in self.app_obj.temp_slice_buffer_dict: - - if self.app_obj.sblock_re_extract_flag \ - and not orig_video_obj.slice_list: - utils.fetch_slice_data( - app_obj, - orig_video_obj, - self.download_worker_obj.worker_id, - ) - - # Import the correct slice list - override_output_mode = None - if orig_video_obj.dbid in self.app_obj.temp_slice_buffer_dict: - - override_output_mode = 'slice' - temp_flag = True - - # Use the temporary buffer - slice_list \ - = self.app_obj.temp_slice_buffer_dict[orig_video_obj.dbid] - # The first entry in 'slice_list' is the value 'create'; remove it - slice_list.pop(0) - # (The temporary buffer, once used, must be emptied immediately) - self.app_obj.del_temp_slice_buffer_dict(orig_video_obj.dbid) - - elif self.options_obj.options_dict['slice_mode'] == 'video' \ - and orig_video_obj.slice_list: - - # Use the video's own slice list - slice_list = orig_video_obj.slice_list.copy() - temp_flag = False - - elif self.options_obj.options_dict['slice_mode'] == 'custom' \ - and self.options_obj.options_dict['slice_list']: - - # Use the slice list specified by the FFmpeg options object - slice_list = self.options_obj.options_dict['slice_list'] - temp_flag = False - - # Convert this list from a list of video slices to be removed, to a - # list of video clips to be retained - # The returned list is in groups of two, in the form - # [start_time, stop_time] - # ...where 'start_time' and 'stop_time' are floating-point values in - # seconds. 'stop_time' can be None to signify the end of the video, - # but 'start_time' is 0 to signify the start of the video - clip_list = utils.convert_slices_to_clips( - self.app_obj, - self.app_obj.general_custom_dl_obj, - slice_list, - temp_flag, - ) - if not clip_list: - - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('FAILED: No slices associated with video'), - ) - - self.fail_count += 1 - return None - - # Create a temporary directory for this video so we don't accidentally - # overwrite anything - parent_dir = orig_video_obj.parent_obj.get_actual_dir(self.app_obj) - orig_video_path = orig_video_obj.get_actual_path(self.app_obj) - temp_dir = self.create_temp_dir(orig_video_obj, parent_dir) - if temp_dir is None: - - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('FAILED: Can\'t create temporary directory'), - ) - - self.fail_count += 1 - self.fatal_error_flag = True - return None - - # Extract the clips, one at a time. For each video clip, we use a - # separate FFmpeg command - list_size = len(clip_list) - for i in range(list_size): - - mini_list = clip_list[i] - start_time = mini_list[0] - stop_time = mini_list[1] - - # Update the Output tab - if not stop_time: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Video clip') + ' ' + str(i + 1) + '/' + str(list_size) \ - + ': ' + str(start_time) + ' - ' + _('End of video') - ) - - else: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Video clip') + ' ' + str(i + 1) + '/' + str(list_size) \ - + ': ' + str(start_time) + ' - ' + str(stop_time) - ) - - # Extract the clip - if not self.process_video( - orig_video_obj, - temp_dir, - start_time, - stop_time, - 'clip_' + str(i + 1), # Clip title - override_output_mode, - ): - # Don't continue creating more clips after an error - self.fatal_error_flag = True - # (Delete the temporary directory after failure) - self.app_obj.remove_directory(temp_dir) - return None - - # If there is more than one clip, they must be concatenated to produce - # a single video (like the original video, from which the video - # slices have been removed) - if list_size == 1: - output_path = os.path.abspath( - os.path.join(temp_dir, 'clip_1' + orig_video_obj.file_ext), - ) - - else: - # For FFmpeg's benefit, write a text file listing every clip - line_list = [] - clips_file = os.path.abspath( - os.path.join(temp_dir, 'clips.txt'), - ) - - for i in range(list_size): - line_list.append( - 'file \'' + os.path.abspath( - os.path.join( - temp_dir, - 'clip_' + str(i + 1) + orig_video_obj.file_ext, - ), - ), - ) - - with open(clips_file, 'w') as fh: - fh.write('\n'.join(line_list)) - - # Prepare the FFmpeg command to concatenate the clips together - output_path = os.path.abspath( - os.path.join( - temp_dir, - orig_video_obj.file_name + orig_video_obj.file_ext, - ), - ) - - cmd_list = [ - self.app_obj.ffmpeg_manager_obj.get_executable(), - '-safe', - '0', - '-f', - 'concat', - '-i', - clips_file, - '-c', - 'copy', - output_path, - ] - - # Update the Output tab again - self.app_obj.main_win_obj.output_tab_write_system_cmd( - 1, - ' '.join(cmd_list), - ) - - # Create a new child process using the command - self.create_child_process(cmd_list) - - # Wait for the concatenation to finish. We are not bothered - # about reading the child process STDOUT/STDERR, since we can - # just test for the existence of the output file - while self.is_child_process_alive(): - time.sleep(self.sleep_time) - - if not os.path.isfile(output_path): - - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('FAILED: Can\'t concatenate clips'), - ) - - # (Delete the temporary directory after failure) - self.fail_count += 1 - self.app_obj.remove_directory(temp_dir) - return None - - # Move the single video file back into the parent directory, replacing - # any file of the same name that's already there - if os.path.isfile(orig_video_path): - self.app_obj.remove_file(orig_video_path) - - if not self.app_obj.move_file_or_directory( - output_path, - orig_video_path, - ): - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _( - 'FAILED: Clips were concatenated, but could not move' \ - + ' the output file out of the temporary directory', - ), - ) - - # (Delete the temporary directory after failure) - self.fail_count += 1 - self.app_obj.remove_directory(temp_dir) - return None - - # Delete the temporary directory - self.app_obj.remove_directory(temp_dir) - - # Procedure successful - return parent_dir - - - def split_video(self, orig_video_obj): - - """Called by self.run(). - - Splits a video into video clips using FFmpeg. - - Args: - - orig_video_obj (media.Video): The video to be sent to FFmpeg - - Return values: - - The destination directory of the clips on success, None on failure - - """ - - # Re-extract timestamps from the video's .info.json or .description - # file, if allowed - # (No point doing it, if the temporary buffer is set) - if not orig_video_obj.dbid in self.app_obj.temp_stamp_buffer_dict: - - if self.app_obj.video_timestamps_re_extract_flag \ - and not orig_video_obj.stamp_list: - self.app_obj.update_video_from_json(orig_video_obj, 'chapters') - - if self.app_obj.video_timestamps_re_extract_flag \ - and not orig_video_obj.stamp_list: - orig_video_obj.extract_timestamps_from_descrip(self.app_obj) - - # Set the containing folder, creating a media.Folder object and/or a - # sub-directory for the video clips, if required - parent_obj, parent_dir, dest_obj, dest_dir \ - = utils.clip_set_destination(self.app_obj, orig_video_obj) - - if parent_obj is None: - - # There is already a media.Folder with the same name, somewhere - # else in the database - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _( - 'FAILED: Can\'t create the destination folder either because' \ - + ' a folder with the same name already exists, or because' \ - + ' new folders can\'t be added to the parent folder', - ), - ) - - self.fail_count += 1 - self.fatal_error_flag = True - return None - - # Import the correct timestamp list - override_output_mode = None - if orig_video_obj.dbid in self.app_obj.temp_stamp_buffer_dict: - - override_output_mode = 'split' - - # Use the temporary buffer - stamp_list \ - = self.app_obj.temp_stamp_buffer_dict[orig_video_obj.dbid] - # The first entry in 'stamp_list' is the value 'create'; remove it - stamp_list.pop(0) - # The temporary buffer, once used, must be emptied immediately - self.app_obj.del_temp_stamp_buffer_dict(orig_video_obj.dbid) - - elif self.options_obj.options_dict['split_mode'] == 'video' \ - and orig_video_obj.stamp_list: - - # Use the video's own timestamp list - stamp_list = orig_video_obj.stamp_list.copy() - - elif self.options_obj.options_dict['split_mode'] == 'custom' \ - and self.options_obj.options_dict['split_list']: - - # Use the timestamp list specified by the FFmpeg options object - stamp_list = self.options_obj.options_dict['split_list'] - - # Split the video - if stamp_list: - - # One source video is split into one or more video clips, using - # timestamps provided by the media.Video object itself - # Each video clip uses a separate FFmpeg command - list_size = len(stamp_list) - for i in range(list_size): - - # List in the form - # [start_stamp, stop_stamp, clip_title] - # If 'stop_stamp' is not specified, then the 'start_stamp' of - # the next clip is used. If there are no more clips, then - # this clip will end at the end of the video - start_stamp, stop_stamp, clip_title \ - = utils.clip_extract_data(stamp_list, i) - - # Set a (hopefully unique) clip title - clip_title = utils.clip_prepare_title( - self.app_obj, - orig_video_obj, - self.clip_title_dict, - clip_title, - i + 1, - list_size, - ) - - self.clip_title_dict[clip_title] = None - - # Update the Output tab - if not stop_stamp: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Video clip') + ' ' + str(i + 1) + '/' \ - + str(list_size) + ': ' + start_stamp + ' - ' \ - + _('End of video') + ': ' + clip_title - ) - - else: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Video clip') + ' ' + str(i + 1) + '/' \ - + str(list_size) + ': ' + start_stamp + ' - ' \ - + stop_stamp + ': ' + clip_title - ) - - # Extract the clip - if not self.process_video( - orig_video_obj, - dest_dir, - start_stamp, - stop_stamp, - clip_title, - override_output_mode, - ): - # Don't continue creating more clips after an error - self.fatal_error_flag = True - return None - - elif dest_obj \ - and self.app_obj.split_video_add_db_flag: - - new_video_obj = utils.clip_add_to_db( - self.app_obj, - dest_obj, - orig_video_obj, - clip_title, - ) - - if new_video_obj: - - # All done - self.new_video_list.append(new_video_obj) - self.split_success_flag = True - - else: - - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - _('FAILED: No timestamps associated with video'), - ) - - self.fail_count += 1 - return None - - # Splitting of this video is complete. Delete the original video, if - # required - if self.app_obj.split_video_auto_delete_flag \ - and isinstance(orig_video_obj.parent_obj, media.Folder): - self.app_obj.delete_video( - orig_video_obj, - True, # Delete all files - True, # Don't update Video Index yet - True, # Or Video Catalogue - ) - - # Procedure successful - return dest_dir - - - def stop_process_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Stops the process operation. - """ - - self.running_flag = False diff --git a/build/lib/tartube/refresh.py b/build/lib/tartube/refresh.py deleted file mode 100644 index 65a29a11..00000000 --- a/build/lib/tartube/refresh.py +++ /dev/null @@ -1,618 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Refresh operation classes.""" - - -# Import Gtk modules -import gi -from gi.repository import GObject - - -# Import other modules -import os -import threading -import time - - -# Import our modules -import formats -import media -import utils -# Use same gettext translations -from mainapp import _ - - -# Classes - - -class RefreshManager(threading.Thread): - - """Called by mainapp.TartubeApp.refresh_manager_continue(). - - Python class to manage the refresh operation, in which the media registry - is checked against Tartube's data directory and updated as appropriate. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - init_obj (media.Channel, media.Playlist, media.Folder or None): If - specified, only this media data object is refreshed. If not - specified, the whole media data registry is refreshed. - - """ - - - # Standard class methods - - - def __init__(self, app_obj, init_obj=None): - - super(RefreshManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media data object (channel, playlist or folder) to refresh, or - # None if the whole media data registry is to be refreshed - self.init_obj = init_obj - - - # IV list - other - # --------------- - # Flag set to False if self.stop_refresh_operation() is called, which - # halts the operation immediately - self.running_flag = True - - # The time at which the refresh operation began (in seconds since - # epoch) - self.start_time = int(time.time()) - # The time at which the refresh operation completed (in seconds since - # epoch) - self.stop_time = None - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # The number of media data objects refreshed so far... - self.job_count = 0 - # ...and the total number to refresh (these numbers are displayed in - # the progress bar in the Videos tab) - self.job_total = 0 - - # Total number of videos analysed - self.video_total_count = 0 - # Number of videos matched with a media.Video object in the database - self.video_match_count = 0 - # Number of videos not matched, and therefore given a new media.Video - # object - self.video_new_count = 0 - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Compiles a list of media data objects (channels, playlists and folders) - to refresh. If self.init_obj is not set, only that channel/playlist/ - folder (and its child channels/playlists/folders) are refreshed; - otherwise the whole media registry is refreshed. - - Then calls self.refresh_from_default_destination() for each item in the - list. - - Finally informs the main application that the refresh operation is - complete. - """ - - # Show information about the refresh operation in the Output tab - if not self.init_obj: - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Starting refresh operation, analysing whole database'), - ) - - else: - - media_type = self.init_obj.get_type() - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Starting refresh operation, analysing \'{}\'').format( - self.init_obj.name, - ), - ) - - # Compile a list of channels, playlists and folders to refresh (each - # one has their own sub-directory inside Tartube's data directory) - obj_list = [] - if self.init_obj: - # Add this channel/playlist/folder, and any child channels/ - # playlists/folders (but not videos, obviously) - obj_list = self.init_obj.compile_all_containers(obj_list) - else: - # Add all channels/playlists/folders in the database - for dbid in self.app_obj.container_reg_dict.keys(): - - obj = self.app_obj.media_reg_dict[dbid] - # Don't add private folders - if not isinstance(obj, media.Folder) or not obj.priv_flag: - obj_list.append(obj) - - self.job_total = len(obj_list) - - # Check each sub-directory in turn, updating the media data registry - # as we go - while self.running_flag and obj_list: - - obj = obj_list.pop(0) - - if obj.external_dir is not None or obj.dbid != obj.master_dbid: - self.refresh_from_actual_destination(obj) - else: - self.refresh_from_default_destination(obj) - - # Pause a moment, before the next iteration of the loop (don't want - # to hog resources) - time.sleep(self.sleep_time) - - # Operation complete. Set the stop time - self.stop_time = int(time.time()) - - # Show a confirmation in the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Refresh operation finished'), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Number of video files analysed:') + ' ' \ - + str(self.video_total_count), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Video files already in the database:') + ' ' \ - + str(self.video_match_count), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('New videos found and added to the database:') + ' ' \ - + str(self.video_new_count), - ) - - # Let the timer run for a few more seconds to prevent Gtk errors - GObject.timeout_add( - 0, - self.app_obj.refresh_manager_halt_timer, - ) - - - def refresh_from_default_destination(self, media_data_obj): - - """Called by self.run(). - - Refreshes a single channel, playlist or folder, for which an - alternative download destination has not been set. - - If a file is missing in the channel/playlist/folder's sub-directory, - mark the video object as not downloaded. - - If unexpected video files exist in the sub-directory, create a new - media.Video object for them. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object to refresh - - """ - - # Update the main window's progress bar - self.job_count += 1 - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - media_data_obj.name, - self.job_count, - self.job_total, - ) - - # Keep a running total of matched/new videos for this channel, playlist - # or folder - local_total_count = 0 - local_match_count = 0 - local_new_count = 0 - - # Update our progress in the Output tab - if isinstance(media_data_obj, media.Channel): - string = _('Channel:') + ' ' - elif isinstance(media_data_obj, media.Playlist): - string = _('Playlist:') + ' ' - else: - string = _('Folder:') + ' ' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - string + media_data_obj.name, - ) - - # Get the sub-directory for this media data object - dir_path = media_data_obj.get_default_dir(self.app_obj) - - # Get a list of video files in the sub-directory - try: - init_list = os.listdir(dir_path) - except: - # Can't read the directory - return - - # From this list, filter out files without a recognised video/audio - # file extension (.mp4, .webm, etc) - mod_list = [] - for relative_path in init_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - # (Don't handle unwisely-named directories...) - if os.path.isfile( - os.path.abspath( - os.path.join(dir_path, relative_path), - ), - ): - - filename, ext = os.path.splitext(relative_path) - # (Remove the initial .) - ext = ext[1:] - if ext in formats.VIDEO_FORMAT_DICT \ - or ext in formats.AUDIO_FORMAT_DICT: - - mod_list.append(relative_path) - - # From the new list, filter out duplicate filenames (e.g. if the list - # contains both 'my_video.mp4' and 'my_video.webm', filter out the - # second one, adding to a list of alternative files) - filter_list = [] - filter_dict = {} - alt_list = [] - for relative_path in mod_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - filename, ext = os.path.splitext(relative_path) - - if not filename in filter_dict: - filter_list.append(relative_path) - filter_dict[filename] = relative_path - - else: - alt_list.append(relative_path) - - # Now compile a dictionary of media.Video objects in this channel/ - # playlist/folder, so we can eliminate them one by one - check_dict = {} - for child_obj in media_data_obj.child_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - if isinstance(child_obj, media.Video) and child_obj.file_name: - - # Does the video file still exist? - this_file = child_obj.file_name + child_obj.file_ext - if child_obj.dl_flag and not this_file in init_list: - self.app_obj.mark_video_downloaded(child_obj, False) - else: - check_dict[child_obj.file_name] = child_obj - - # If this channel/playlist/folder is the alternative download - # destination for other channels/playlists/folders, compile a - # dicationary of their media.Video objects - # (If we find a video we weren't expecting, before creating a new - # media.Video object, we must first check it isn't one of them) - slave_dict = {} - for slave_dbid in media_data_obj.slave_dbid_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - slave_obj = self.app_obj.media_reg_dict[slave_dbid] - for child_obj in slave_obj.child_list: - - if isinstance(child_obj, media.Video) and child_obj.file_name: - slave_dict[child_obj.file_name] = child_obj - - # Now try to match each video file (in filter_list) with an existing - # media.Video object (in check_dict) - # If there is no match, and if the video file doesn't match a video - # in another channel/playlist/folder (for which this is the - # alternative download destination), then we can create a new - # media.Video object - for relative_path in filter_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - filename, ext = os.path.splitext(relative_path) - - if self.app_obj.refresh_output_videos_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Checking:') + ' ' + filename, - ) - - if filename in check_dict: - - # File matched - self.video_total_count += 1 - local_total_count += 1 - self.video_match_count += 1 - local_match_count += 1 - - # If it is not marked as downloaded, we can mark it so now - child_obj = check_dict[filename] - if not child_obj.dl_flag: - self.app_obj.mark_video_downloaded(child_obj, True) - - # Make sure the stored extension is correct (e.g. if we've - # matched an existing .webm video file, with an expected - # .mp4 video file) - if child_obj.file_ext != ext: - child_relative_path \ - = child_obj.file_name + child_obj.file_ext - - if not child_relative_path in alt_list: - child_obj.set_file(filename, ext) - - # Take this opportunity to update the video file size (etc), - # in case they weren't set during the original download - self.app_obj.update_video_from_filesystem( - child_obj, - child_obj.get_actual_path(self.app_obj), - ) - - # Eliminate this media.Video object; no other video file should - # match it - del check_dict[filename] - - # Update our progress in the Output tab (if required) - if self.app_obj.refresh_output_videos_flag: - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Match:') + ' ' + filename, - ) - - elif filename not in slave_dict: - - # File didn't match a media.Video object - self.video_total_count += 1 - local_total_count += 1 - self.video_new_count += 1 - local_new_count += 1 - - # Display the list of non-matching videos, if required - if self.app_obj.refresh_output_videos_flag \ - and self.app_obj.refresh_output_verbose_flag: - - for failed_path in check_dict.keys(): - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Non-match:') + ' ' + filename, - ) - - # Create a new media.Video object - video_obj = self.app_obj.add_video(media_data_obj, None) - video_path = os.path.abspath( - os.path.join( - dir_path, - filter_dict[filename], - ) - ) - - # Set the new video object's IVs - filename, ext = os.path.splitext(filter_dict[filename]) - video_obj.set_name(filename) - video_obj.set_nickname(filename) - video_obj.set_file(filename, ext) - - if ext == '.mkv': - video_obj.set_mkv() - - video_obj.set_file_size( - os.path.getsize( - os.path.abspath( - os.path.join(dir_path, filter_dict[filename]), - ), - ), - ) - - # If the video's JSON file has been downloaded, we can extract - # video statistics from it - self.app_obj.update_video_from_json(video_obj) - - # For any of those statistics that haven't been set (because - # the JSON file was missing or didn't contain the right - # statistics), set them directly - self.app_obj.update_video_from_filesystem( - video_obj, - video_path, - ) - - # This call marks the video as downloaded, and also updates the - # Video Index and Video Catalogue (if required) - self.app_obj.mark_video_downloaded(video_obj, True) - - if self.app_obj.refresh_output_videos_flag: - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('New video:') + ' ' + filename, - ) - - # Check complete, display totals - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Total videos:') + ' ' + str(local_total_count) \ - + ', ' + _('matched:') + ' ' + str(local_match_count) \ - + ', ' + _('new:') + ' ' + str(local_new_count), - ) - - - def refresh_from_actual_destination(self, media_data_obj): - - """Called by self.run(). - - A modified version of self.refresh_from_default_destination(). - Refreshes a single channel, playlist or folder, for which an - alternative download destination has been set. - - If a file is missing in the alternative download destination, mark the - video object as not downloaded. - - Don't check for unexpected video files in the alternative download - destination - we expect that they exist. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object to refresh - - """ - - # Update the main window's progress bar - self.job_count += 1 - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - media_data_obj.name, - self.job_count, - self.job_total, - ) - - # Keep a running total of matched videos for this channel, playlist or - # folder - local_total_count = 0 - local_match_count = 0 - # (No new media.Video objects are created) - local_missing_count = 0 - - # Update our progress in the Output tab - if isinstance(media_data_obj, media.Channel): - string = _('Channel:') + ' ' - elif isinstance(media_data_obj, media.Playlist): - string = _('Playlist:') + ' ' - else: - string = _('Folder:') + ' ' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - string + media_data_obj.name, - ) - - # Get the alternative download destination - dir_path = media_data_obj.get_actual_dir(self.app_obj) - - # Get a list of video files in that sub-directory - try: - init_list = os.listdir(dir_path) - except: - # Can't read the directory - return - - # Now check each media.Video object, to see if the video file still - # exists (or not) - for child_obj in media_data_obj.child_list: - - if isinstance(child_obj, media.Video) and child_obj.file_name: - - this_file = child_obj.file_name + child_obj.file_ext - if child_obj.dl_flag and not this_file in init_list: - - local_missing_count += 1 - - # Video doesn't exist, so mark it as not downloaded - self.app_obj.mark_video_downloaded(child_obj, False) - - # Update our progress in the Output tab (if required) - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Missing:') + ' ' + child_obj.name, - ) - - elif not child_obj.dl_flag and this_file in init_list: - - self.video_total_count += 1 - local_total_count += 1 - self.video_match_count += 1 - local_match_count += 1 - - # Video exists, so mark it as downloaded (but don't mark it - # as new) - self.app_obj.mark_video_downloaded(child_obj, True, True) - - # Update our progress in the Output tab (if required) - if self.app_obj.refresh_output_videos_flag: - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Match:') + ' ' + child_obj.name, - ) - - # Check complete, display totals - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Total videos:') + ' ' + str(local_total_count) \ - + ', ' + _('matched:') + ' ' + str(local_match_count) \ - + ', ' + _('missing:') + ' ' + str(local_missing_count), - ) - - - def stop_refresh_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Stops the refresh operation. - """ - - self.running_flag = False diff --git a/build/lib/tartube/tartube b/build/lib/tartube/tartube deleted file mode 100644 index 4674533a..00000000 --- a/build/lib/tartube/tartube +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Tartube main file.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import os -import sys -import threading -import traceback -import importlib.util - - -# Add module directory to path to prevent import issues -spec = importlib.util.find_spec('tartube') -if spec is not None: - sys.path.append(os.path.abspath(os.path.dirname(spec.origin))) - - -# Import our modules -import mainapp - - -# 'Global' variables -__packagename__ = 'tartube' -__version__ = '2.4.370' -__date__ = '13 Apr 2023' -__copyright__ = 'Copyright \xa9 2019-2023 A S Lewis' -__license__ = """ -Copyright \xa9 2019-2023 A S Lewis. - -This program is free software; you can redistribute it and/or modify it under -the terms of the GNU Lesser General Public License as published by the Free -Software Foundation; either version 2.1 of the License, or (at your option) any -later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -details. - -You should have received a copy of the GNU Lesser General Public License along -with this program. If not, see . -""" -__author_list__ = [ - 'A S Lewis', -] -__credit_list__ = [ - # (This list is formatted to suit Gtk.AboutDialog) - 'Partially based on youtube-dl-gui by MrS0m30n3', - 'https://github.com/MrS0m30n3/youtube-dl-gui', - 'FFmpeg thumbnail code adapted from youtube-dl', - 'http://youtube-dl.org/', - 'FFmpeg options adapted from FFmpeg Command', - 'Line Wizard by AndreKR', - 'https://github.com/AndreKR/ffmpeg-command-line-wizard', - 'Upgraded Textview by Kevin Mehall', - 'https://kevinmehall.net/2010/pygtk_multi_select_drag_drop', - 'XDG support by Scott Stevenson', - 'https://pypi.org/project/xdg/', -] -__description__ = 'GUI front-end for youtube-dl, yt-dlp and\nother' \ -+ ' compatible video downloaders' -__website__ = 'http://tartube.sourceforge.io' -__app_id__ = 'io.sourceforge.tartube' -__website_bugs__ = 'https://github.com/axcore/tartube' -__website_dev__ = 'http://raw.githubusercontent.com/axcore/tartube/master' -# Flag set to True if multiple instances of Tartube are allowed; False if -# only a single instance is allowed -__multiple_instance_flag__ = True -# There are four executables; this default one, and three others used in -# packaging. The others are identical, except for the values of these -# variables -__pkg_install_flag__ = False -__pkg_strict_install_flag__ = False -__pkg_no_download_flag__ = False - - -# Uncaught exception handling -def setup_thread_excepthook(): - - """Workaround for 'sys.excepthook' thread bug from: - http://bugs.python.org/issue1230540 - - Adapted from - https://stackoverflow.com/questions/1643327/sys-excepthook-and-threading - - Call once from the main thread before creating any threads. - """ - - init_original = threading.Thread.__init__ - - def init(self, *args, **kwargs): - - init_original(self, *args, **kwargs) - run_original = self.run - - def run_with_except_hook(*args2, **kwargs2): - try: - run_original(*args2, **kwargs2) - except Exception: - sys.excepthook(*sys.exc_info()) - - self.run = run_with_except_hook - - threading.Thread.__init__ = init - - -app = None -def handle_uncaught_exception(except_type, value, tb): - - """Intercepts uncaught exceptions, and diverts the message to the main - window's Errors/Warnings tab, so ordinary users can actually see them and - (hopefully) report them. - - Then raises the exception as normal. - - Adapted from - https://dev.to/joshuaschlichting/ - catching-every-single-exception-with-python-40o3 - - Args: - except_type (type): Exception type - value (TypeError): Exception value - tb (traceback): Exception traceback - - """ - - if app is not None: - - app.system_exception( - except_type, - value, - '\n'.join(traceback.extract_tb(tb).format()) - ) - - raise type(value) - - -setup_thread_excepthook() -sys.excepthook = handle_uncaught_exception - - -# Start Tartube -app = mainapp.TartubeApp() -app.run(sys.argv) diff --git a/build/lib/tartube/tidy.py b/build/lib/tartube/tidy.py deleted file mode 100644 index 89938508..00000000 --- a/build/lib/tartube/tidy.py +++ /dev/null @@ -1,1636 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Tidy operation classes.""" - - -# Import Gtk modules -import gi -from gi.repository import GObject - - -# Import other modules -try: - import moviepy.editor -except: - pass - -import os -import re -import shutil -import threading -import time - - -# Import our modules -import formats -import media -import utils -# Use same gettext translations -from mainapp import _ - - -# Classes - - -class TidyManager(threading.Thread): - - """Called by mainapp.TartubeApp.tidy_manager_start(). - - Python class to manage the tidy operation, in which videos can be checked - for corruption and actually existing (or not), and various file types can - be deleted collectively. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - choices_dict (dict): A dictionary specifying the choices made by the - user in mainwin.TidyDialogue. The dictionary is in the following - format: - - media_data_obj: A media.Channel, media.Playlist or media.Folder - object, or None if all channels/playlists/folders are to be - tidied up. If specified, the cahnnel/playlist/folder and all of - its descendants are checked - - corrupt_flag: True if video files should be checked for corruption - - del_corrupt_flag: True if corrupted video files should be deleted - - exist_Flag: True if video files that should exist should be - checked, in case they don't (and vice-versa) - - del_video_flag: True if downloaded video files should be deleted - - del_others_flag: True if all video/audio files with the same name - should be deleted (as artefacts of post-processing with FFmpeg - or AVConv) - - remove_no_url_flag: True if any media.Video objects whose URL is - not set should be removed from the database (no files are - deleted) - - remove_duplicate_flag: True if any media.Video objects, which are - not marked as downloaded and which share a URL with another - media.Video object with the same parent and which is marked as - downloaded, should be removed from the database (no files are - deleted) - - del_archive_flag: True if all youtube-dl archive files should be - deleted - - move_thumb_flag: True if all thumbnail files should be moved into a - subdirectory - - del_thumb_flag: True if all thumbnail files should be deleted - - del_webp_flag: True if all .webp thumbnail files should be deleted - - convert_webp_flag: True if all .webp thumbnail files should be - converted to .jpg - - move_data_flag: True if description, metadata (JSON) and annotation - files should be moved into a subdirectory - - del_descrip_flag: True if all description files should be deleted - - del_json_flag: True if all metadata (JSON) files should be deleted - - del_xml_flag: True if all annotation files should be deleted - - convert_ext_flag: True if .unknown_video file extensions should be - converted to .mp4 (experimental; see Git #472) - - """ - - - # Standard class methods - - - def __init__(self, app_obj, choices_dict): - - super(TidyManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media data object (channel, playlist or folder) to be tidied up, - # or None if the whole data directory is to be tidied up - # If specified, the channel/playlist/folder and all of its descendants - # are checked - self.init_obj = choices_dict['media_data_obj'] - - - # IV list - other - # --------------- - # Flag set to False if self.stop_tidy_operation() is called, which - # halts the operation immediately - self.running_flag = True - - # The time at which the tidy operation began (in seconds since epoch) - self.start_time = int(time.time()) - # The time at which the tidy operation completed (in seconds since - # epoch) - self.stop_time = None - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # Flags specifying which actions should be applied - # True if video files should be checked for corruption - self.corrupt_flag = choices_dict['corrupt_flag'] - # True if corrupted video files should be deleted - self.del_corrupt_flag = choices_dict['del_corrupt_flag'] - # True if video files that should exist should be checked, in case they - # don't (and vice-versa) - self.exist_flag = choices_dict['exist_flag'] - # True if downloaded video files should be deleted - self.del_video_flag = choices_dict['del_video_flag'] - # True if all video/audio files with the same name should be deleted - # (as artefacts of post-processing with FFmpeg or AVConv) - self.del_others_flag = choices_dict['del_others_flag'] - # True if any media.Video objects whose URL is not set should be - # removed from the database (no files are deleted) - self.remove_no_url_flag = choices_dict['remove_no_url_flag'] - # True if any media.Video objects, which are not marked as downloaded - # and which share a URL with another media.Video object with the same - # parent and which is marked as downloaded, should be removed from - # the database (no files are deleted) - self.remove_duplicate_flag = choices_dict['remove_duplicate_flag'] - # True if all youtube-dl archive files should be deleted - self.del_archive_flag = choices_dict['del_archive_flag'] - # True if all thumbnail files should be moved into a subdirectory - self.move_thumb_flag = choices_dict['move_thumb_flag'] - # True if all thumbnail files should be deleted - self.del_thumb_flag = choices_dict['del_thumb_flag'] - # True if all .webp thumbnail files should be deleted - self.del_webp_flag = choices_dict['del_webp_flag'] - # True if all .webp thumbnail files should be converted to .jpg. - # Requires mainapp.TartubeApp.ffmpeg_fail_flag set to False - self.convert_webp_flag = choices_dict['convert_webp_flag'] - # True if description, metadata (JSON) and annotation files should be - # moved into a subdirectory - self.move_data_flag = choices_dict['move_data_flag'] - # True if all description files should be deleted - self.del_descrip_flag = choices_dict['del_descrip_flag'] - # True if all metadata (JSON) files should be deleted - self.del_json_flag = choices_dict['del_json_flag'] - # True if all annotation files should be deleted - self.del_xml_flag = choices_dict['del_xml_flag'] - # True if .unknown_video file extensions should be converted to .mp4 - # (experimental; see Git #472) - self.convert_ext_flag = choices_dict['convert_ext_flag'] - - # The number of media data objects whose directories have been tidied - # so far... - self.job_count = 0 - # ...and the total number to tidy (these numbers are displayed in the - # progress bar in the Videos tab) - self.job_total = 0 - - # Individual counts, updated as we go - self.video_corrupt_count = 0 - self.video_corrupt_deleted_count = 0 - self.video_exist_count = 0 - self.video_no_exist_count = 0 - self.video_deleted_count = 0 - self.other_deleted_count = 0 - self.remove_no_url_count = 0 - self.remove_duplicate_count = 0 - self.archive_deleted_count = 0 - self.thumb_moved_count = 0 - self.thumb_deleted_count = 0 - self.webp_deleted_count = 0 - self.webp_converted_count = 0 - self.data_moved_count = 0 - self.descrip_deleted_count = 0 - self.json_deleted_count = 0 - self.xml_deleted_count = 0 - self.ext_converted_count = 0 - - - # Code - # ---- - - # Do not convert .webp thumbnails, if not allowed - if self.app_obj.ffmpeg_fail_flag: - self.convert_webp_flag = False - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Compiles a list of media data objects (channels, playlists and folders) - to tidy up. If self.init_obj is not set, only that channel/playlist/ - folder (and its child channels/playlists/folders) are tidied up; - otherwise the whole data directory is tidied up. - - Then calls self.tidy_directory() for each item in the list. - - Finally informs the main application that the tidy operation is - complete. - """ - - # Show information about the tidy operation in the Output tab - if not self.init_obj: - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Starting tidy operation, tidying up whole data directory'), - ) - - else: - - media_type = self.init_obj.get_type() - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Starting tidy operation, tidying up \'{0}\'').format( - self.init_obj.name, - ) - ) - - if self.corrupt_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Check videos are not corrupted:') + ' ' + text, - ) - - if self.corrupt_flag: - - if self.del_corrupt_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete corrupted videos:') + ' ' + text, - ) - - if self.exist_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Check videos do/don\'t exist:') + ' ' + text, - ) - - if self.del_video_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete all video files:') + ' ' + text, - ) - - if self.del_video_flag: - - if self.del_others_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete other video/audio files:') + ' ' + text, - ) - - if self.remove_no_url_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Remove no_URL videos from database:') + ' ' + text, - ) - - if self.remove_duplicate_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Remove undownloaded duplicate videos from database:') \ - + ' ' + text, - ) - - if self.del_archive_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete downloader archive files:') + ' ' + text, - ) - - if self.move_thumb_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Move thumbnails into own folder:') + ' ' + text, - ) - - if self.del_thumb_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete all thumbnail files:') + ' ' + text, - ) - - if self.del_webp_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete all .webp thumbnail files:') + ' ' + text, - ) - - if self.convert_webp_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Convert .webp thumbnails to .jpg:') + ' ' + text, - ) - - if self.move_data_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Move other metadata files into own folder:') \ - + ' ' + text, - ) - - if self.del_descrip_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete all description files:') + ' ' + text, - ) - - if self.del_json_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete all metadata (JSON) files:') + ' ' + text, - ) - - if self.del_xml_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Delete all annotation files:') + ' ' + text, - ) - - if self.convert_ext_flag: - text = _('YES') - else: - text = _('NO') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Convert .unknown_video file extensions to .mp4:') \ - + ' ' + text, - ) - - # Compile a list of channels, playlists and folders to tidy up (each - # one has their own sub-directory inside Tartube's data directory) - obj_list = [] - if self.init_obj: - # Add this channel/playlist/folder, and any child channels/ - # playlists/folders (but not videos, obviously) - obj_list = self.init_obj.compile_all_containers(obj_list) - - else: - # Add all channels/playlists/folders in the database - for dbid in self.app_obj.container_reg_dict.keys(): - - obj = self.app_obj.media_reg_dict[dbid] - # Don't add private folders - if not isinstance(obj, media.Folder) or not obj.priv_flag: - obj_list.append(obj) - - self.job_total = len(obj_list) - - # Check each sub-directory in turn, updating the media data registry - # as we go - while self.running_flag and obj_list: - self.tidy_directory(obj_list.pop(0)) - - # Pause a moment, before the next iteration of the loop (don't want - # to hog resources) - time.sleep(self.sleep_time) - - # Operation complete. Set the stop time - self.stop_time = int(time.time()) - - # Show a confirmation in the Output tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Tidy operation finished'), - ) - - if self.corrupt_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Corrupted videos found:') + ' ' \ - + str(self.video_corrupt_count), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Corrupted videos deleted:') + ' ' \ - + str(self.video_corrupt_deleted_count), - ) - - if self.exist_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('New video files detected:') + ' ' \ - + str(self.video_exist_count), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Missing video files detected:') + ' ' \ - + str(self.video_no_exist_count), - ) - - if self.del_video_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Non-corrupted video files deleted:') + ' ' \ - + str(self.video_deleted_count), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Other video/audio files deleted:') + ' ' \ - + str(self.other_deleted_count), - ) - - if self.remove_no_url_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('No-URL videos removed from database:') + ' ' \ - + str(self.remove_no_url_count), - ) - - if self.remove_duplicate_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' \ - + _('Undownloaded duplicate videos removed from database:') \ - + ' ' + str(self.remove_duplicate_count), - ) - - if self.del_archive_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Downloader archive files deleted:') + ' ' \ - + str(self.archive_deleted_count), - ) - - if self.move_thumb_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Thumbnail files moved:') + ' ' \ - + str(self.thumb_moved_count), - ) - - if self.del_thumb_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Thumbnail files deleted:') + ' ' \ - + str(self.thumb_deleted_count), - ) - - if self.del_webp_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('.webp thumbnail files deleted:') + ' ' \ - + str(self.webp_deleted_count), - ) - - if self.convert_webp_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('.webp thumbnails converted to .jpg:') + ' ' \ - + str(self.webp_converted_count), - ) - - if self.move_data_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Other metadata files moved:') + ' ' \ - + str(self.data_moved_count), - ) - - if self.del_descrip_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Description files deleted:') + ' ' \ - + str(self.descrip_deleted_count), - ) - - if self.del_json_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Metadata (JSON) files deleted:') + ' ' \ - + str(self.json_deleted_count), - ) - - if self.del_xml_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Annotation files deleted:') + ' ' \ - + str(self.xml_deleted_count), - ) - - if self.convert_ext_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('File extensions converted:') + ' ' \ - + str(self.ext_converted_count), - ) - - # Let the timer run for a few more seconds to prevent Gtk errors - GObject.timeout_add( - 0, - self.app_obj.tidy_manager_halt_timer, - ) - - - def tidy_directory(self, media_data_obj): - - """Called by self.run(). - - Tidy up the directory of a single channel, playlist or folder. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - # Update the main window's progress bar - self.job_count += 1 - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - media_data_obj.name, - self.job_count, - self.job_total, - ) - - media_type = media_data_obj.get_type() - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - _('Checking:') + ' \'' + media_data_obj.name + '\'', - ) - - if self.convert_ext_flag: - self.convert_file_ext(media_data_obj) - - if self.corrupt_flag: - self.check_video_corrupt(media_data_obj) - - if self.exist_flag: - self.check_videos_exist(media_data_obj) - - if self.del_video_flag: - self.delete_video(media_data_obj) - - if self.remove_no_url_flag: - self.remove_no_url(media_data_obj) - - if self.remove_duplicate_flag: - self.remove_duplicate(media_data_obj) - - if self.del_archive_flag: - self.delete_archive(media_data_obj) - - if self.move_thumb_flag: - self.move_thumb(media_data_obj) - - if self.del_thumb_flag: - self.delete_thumb(media_data_obj) - - if self.del_webp_flag: - self.delete_webp(media_data_obj) - - if self.convert_webp_flag: - self.convert_webp(media_data_obj) - - if self.move_data_flag: - self.move_data(media_data_obj) - - if self.del_descrip_flag: - self.delete_descrip(media_data_obj) - - if self.del_json_flag: - self.delete_json(media_data_obj) - - if self.del_xml_flag: - self.delete_xml(media_data_obj) - - - def convert_file_ext(self, media_data_obj): - - """Called by self.tidy_directory(). - - Git #472: yt-dlp occasionally downloads .mp4 videos from VK (and - possibly other websites) whose file extension is mistakenly set to - .mp4. Convert any such files to .mp4. - - This is an experimental/temporary feature. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - # Import the main window (for convenience) - main_win_obj = self.app_obj.main_win_obj - - # Get a list of video files in the directory - container_path = media_data_obj.get_actual_dir(self.app_obj) - try: - init_list = os.listdir(container_path) - except: - # Can't read the directory - return - - # Find all .unknown_video files - for relative_path in init_list: - - # (If self.stop_tidy_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - file_path = os.path.abspath( - os.path.join(container_path, relative_path), - ) - - if os.path.isfile(file_path): - - filename, ext = os.path.splitext(relative_path) - if ext == '.unknown_video': - mod_file_path = re.sub( - r'\.unknown_video$', - '.mp4', - file_path, - ) - self.app_obj.move_file_or_directory( - file_path, - mod_file_path, - ) - self.ext_converted_count += 1 - - - def check_video_corrupt(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - video are corrupted, don't delete them (let the user do that manually). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - # Import the main window (for convenience) - main_win_obj = self.app_obj.main_win_obj - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None \ - and video_obj.dl_flag: - - video_path = video_obj.get_actual_path(self.app_obj) - - if os.path.isfile(video_path): - - # Code copied from - # mainapp.TartubeApp.update_video_from_filesystem() - # When the video file is corrupted, moviepy freezes - # indefinitely - # Instead, let's try placing the procedure inside a thread - # (unlike the original function, this one is never called - # if .refresh_moviepy_timeout is 0) - this_thread = threading.Thread( - target=self.call_moviepy, - args=(video_obj, video_path,), - ) - - this_thread.daemon = True - this_thread.start() - this_thread.join(self.app_obj.refresh_moviepy_timeout) - if this_thread.is_alive(): - - # moviepy timed out, so assume the video is corrupted - self.video_corrupt_count += 1 - - if self.del_corrupt_flag \ - and os.path.isfile(video_path): - - # Delete the corrupted file - if self.app_obj.remove_file(video_path): - self.video_corrupt_deleted_count += 1 - - main_win_obj.output_tab_write_stdout( - 1, - ' ' + _( - 'Deleted (possibly) corrupted video file:', - ) + ' \'' + video_obj.name + '\'', - ) - - self.app_obj.mark_video_downloaded( - video_obj, - False, - ) - - else: - main_win_obj.output_tab_write_stderr( - 1, - ' ' + _( - 'Failed to delete (possibly)' \ - + ' corrupted video file:', - ) + ' \'' + video_obj.name + '\'', - ) - - else: - - # Don't delete it - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _( - 'Video file might be corrupt:', - ) + ' \'' + video_obj.name + '\'', - ) - - - def check_videos_exist(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - video should exist, but doesn't (or vice-versa), modify the media.Video - object's IVs accordingly. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - video_path = video_obj.get_actual_path(self.app_obj) - - if not video_obj.dl_flag \ - and os.path.isfile(video_path): - - # File exists, but is marked as not downloaded - self.app_obj.mark_video_downloaded( - video_obj, - True, # Video is downloaded - True, # ...but don't mark it as new - ) - - self.video_exist_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _( - 'Video file exists:', - ) + ' \'' + video_obj.name + '\'', - ) - - elif video_obj.dl_flag \ - and not os.path.isfile(video_path): - - # File doesn't exist, but is marked as downloaded - self.app_obj.mark_video_downloaded( - video_obj, - False, # Video is not downloaded - ) - - self.video_no_exist_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _( - 'Video file doesn\'t exist:', - ) + ' \'' + video_obj.name + '\'', - ) - - - def delete_video(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - video exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - ext_list = formats.VIDEO_FORMAT_LIST.copy() - ext_list.extend(formats.AUDIO_FORMAT_LIST) - - for video_obj in media_data_obj.compile_all_videos( [] ): - - video_path = None - if video_obj.file_name is not None: - - video_path = video_obj.get_actual_path(self.app_obj) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - video_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - video_path, - ) - - if video_path is not None: - - if video_obj.dl_flag \ - and os.path.isfile(video_path): - - # Delete the downloaded video file - if self.app_obj.remove_file(video_path): - - # Mark the video as not downloaded - self.app_obj.mark_video_downloaded(video_obj, False) - self.video_deleted_count += 1 - - if self.del_others_flag: - - # Also delete all video/audio files with the same name - # There might be thousands of files in the directory, so - # using os.walk() or something like that might be too - # expensive - # Also, post-processing might create various artefacts, all - # of which must be deleted - for ext in ext_list: - - other_path = video_obj.get_actual_path_by_ext( - self.app_obj, - ext, - ) - - if os.path.isfile(other_path) \ - and self.app_obj.remove_file(other_path): - self.other_deleted_count += 1 - - # For an encore, delete all post-processing artefacts in the form - # VIDEO_NAME.fNNN.ext, where NNN is an integer and .ext is one of - # the video extensions specified by formats.VIDEO_FORMAT_LIST - # (.mkv, etc) - # (The previous code won't pick them up, but we can delete them all - # now.) - # (The alternative download destination, if set, is not affected.) - check_list = [] - search_path = media_data_obj.get_default_dir(self.app_obj) - - for (dir_path, dir_name_list, file_name_list) in os.walk(search_path): - check_list.extend(file_name_list) - - char = '|' - regex = r'\.f\d+\.(' + char.join(formats.VIDEO_FORMAT_LIST) + ')$' - for check_path in check_list: - if re.search(regex, check_path): - - full_path = os.path.abspath( - os.path.join(search_path, check_path), - ) - - if os.path.isfile(full_path) \ - and self.app_obj.remove_file(full_path): - self.other_deleted_count += 1 - - - def remove_no_url(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - video has no URL, remove it from the database (but don't delete any - files). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.source is None: - GObject.timeout_add( - 0, - self.app_obj.delete_video, - video_obj, - ) - - self.remove_no_url_count += 1 - - - def remove_duplicate(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - video is not marked as downloaded, and has the same URL as another - child video (of the same specified media data object) which IS marked - as downloaded, remove the undownloaded one from the database (but - don't delete any files). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - # Compile dictionaries of downloaded and undownloaded URLs - dl_dict = {} - not_dl_dict = {} - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.source is not None: - if video_obj.dl_flag: - dl_dict[video_obj.source] = video_obj.dbid - else: - not_dl_dict[video_obj.source] = video_obj.dbid - - # Check undownloaded videos, looking for a matching downloaded video - for url in not_dl_dict.keys(): - - if url in dl_dict: - - # Duplicate found - dbid = not_dl_dict[url] - if dbid in self.app_obj.media_reg_dict: - - duplicate_obj = self.app_obj.media_reg_dict[dbid] - GObject.timeout_add( - 0, - self.app_obj.delete_video, - duplicate_obj, - ) - - self.remove_duplicate_count += 1 - - - def delete_archive(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks the specified media data object's directory. If a youtube-dl - archive file is found there, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - archive_path = os.path.abspath( - os.path.join( - media_data_obj.get_default_dir(self.app_obj), - self.app_obj.ytdl_archive_name, - ), - ) - - # Delete the archive file - if os.path.isfile(archive_path) \ - and self.app_obj.remove_file(archive_path): - self.archive_deleted_count += 1 - - - def move_thumb(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated thumbnail file exists, moves it into its own sub-directory. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - # Thumbnails might be in one of four locations. If the - # thumbnail has already been moved into /.thumbs, then of - # course we don't move it again (and this function returns an - # empty list) - path_list = utils.find_thumbnail_restricted( - self.app_obj, - video_obj, - ) - - if path_list: - - main_path = os.path.abspath( - os.path.join( - path_list[0], path_list[1], - ), - ) - - subdir = os.path.abspath( - os.path.join( - path_list[0], self.app_obj.thumbs_sub_dir, - ), - ) - - subdir_path = os.path.abspath( - os.path.join( - path_list[0], - self.app_obj.thumbs_sub_dir, - path_list[1], - ), - ) - - if os.path.isfile(main_path) \ - and not os.path.isfile(subdir_path): - - if not os.path.isdir(subdir): - self.app_obj.make_directory(subdir) - - if self.app_obj.move_file_or_directory( - main_path, - subdir_path, - ): - self.thumb_moved_count += 1 - - - def delete_thumb(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated thumbnail file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - # Thumbnails might be in one of four locations - thumb_path = utils.find_thumbnail(self.app_obj, video_obj) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - if thumb_path is not None: - - thumb_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - thumb_path, - ) - - # Delete the thumbnail file - if thumb_path is not None \ - and os.path.isfile(thumb_path) \ - and self.app_obj.remove_file(thumb_path): - self.thumb_deleted_count += 1 - - - def delete_webp(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated .webp thumbnail file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - # Thumbnails might be in one of two locations - webp_path = utils.find_thumbnail_webp_strict( - self.app_obj, - video_obj - ) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - if webp_path is not None: - - webp_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - webp_path, - ) - - # Delete the thumbnail file - if webp_path is not None \ - and os.path.isfile(webp_path) \ - and self.app_obj.remove_file(webp_path): - self.webp_deleted_count += 1 - - - def convert_webp(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated thumbnail file in a .webp or malformed .jpg format exists, - convert it to .jpg. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - # Thumbnails might be in one of four locations - thumb_path = utils.find_thumbnail_webp_intact_or_broken( - self.app_obj, - video_obj, - ) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - if thumb_path is not None: - - thumb_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - thumb_path, - ) - - if thumb_path is not None \ - and os.path.isfile(thumb_path): - - # Convert to .jpg - if not self.app_obj.ffmpeg_manager_obj.convert_webp( - thumb_path - ): - # FFmpeg is probably not installed; don't try any more - # conversions - self.convert_webp_flag = False - self.app_obj.set_ffmpeg_fail_flag(True) - - else: - - self.webp_converted_count += 1 - - - def move_data(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated thumbnail file exists, moves it into its own sub-directory. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - # Description/JSON/annotations files might be in one of four - # locations. If the file has already been moved into /.data, - # then of course we don't move it again - for ext in ['.description', '.info.json', '.annotations.xml']: - - main_path = video_obj.get_actual_path_by_ext( - self.app_obj, - ext, - ) - - subdir = os.path.abspath( - os.path.join( - video_obj.parent_obj.get_actual_dir(self.app_obj), - self.app_obj.metadata_sub_dir, - ), - ) - - subdir_path \ - = video_obj.get_actual_path_in_subdirectory_by_ext( - self.app_obj, - ext, - ) - - if os.path.isfile(main_path) \ - and not os.path.isfile(subdir_path): - - if not os.path.isdir(subdir): - self.app_obj.make_directory(subdir) - - # (os.rename sometimes fails on external hard drives; - # this is safer) - if self.app_obj.move_file_or_directory( - main_path, - subdir_path, - ): - self.data_moved_count += 1 - - - def delete_descrip(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated description file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - main_path = video_obj.get_actual_path_by_ext( - self.app_obj, - '.description', - ) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - main_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - main_path, - ) - - # Delete the description file - if main_path is not None \ - and os.path.isfile(main_path) \ - and self.app_obj.remove_file(main_path): - self.descrip_deleted_count += 1 - - # (Repeat for a file that might be in the sub-directory - # '.data') - subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext( - self.app_obj, - '.description', - ) - - subdir_path = self.check_video_in_actual_dir( - subdir_path, - video_obj, - subdir_path, - ) - - if subdir_path is not None \ - and os.path.isfile(subdir_path) \ - and self.app_obj.remove_file(subdir_path): - self.descrip_deleted_count += 1 - - - def delete_json(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated metadata (JSON) file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - main_path = video_obj.get_actual_path_by_ext( - self.app_obj, - '.info.json', - ) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - main_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - main_path, - ) - - # Delete the metadata file - if main_path is not None \ - and os.path.isfile(main_path) \ - and self.app_obj.remove_file(main_path): - self.json_deleted_count += 1 - - # (Repeat for a file that might be in the sub-directory - # '.data') - subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext( - self.app_obj, - '.info.json', - ) - - subdir_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - subdir_path, - ) - - if subdir_path is not None \ - and os.path.isfile(subdir_path) \ - and self.app_obj.remove_file(subdir_path): - self.json_deleted_count += 1 - - - def delete_xml(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated annotation file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - main_path = video_obj.get_actual_path_by_ext( - self.app_obj, - '.annotations.xml', - ) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - main_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - main_path, - ) - - # Delete the annotation file - if main_path is not None \ - and os.path.isfile(main_path) \ - and self.app_obj.remove_file(main_path): - self.xml_deleted_count += 1 - - # (Repeat for a file that might be in the sub-directory - # '.data') - subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext( - self.app_obj, - '.annotations.xml', - ) - - subdir_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - subdir_path, - ) - - if subdir_path is not None \ - and os.path.isfile(subdir_path) \ - and self.app_obj.remove_file(subdir_path): - self.xml_deleted_count += 1 - - - def call_moviepy(self, video_obj, video_path): - - """Called by thread inside self.check_video_corrupt(). - - When we call moviepy.editor.VideoFileClip() on a corrupted video file, - moviepy freezes indefinitely. - - This function is called inside a thread, so a timeout of (by default) - ten seconds can be applied. - - Args: - - video_obj (media.Video): The video object being updated - - video_path (str): The path to the video file itself - - """ - - try: - clip = moviepy.editor.VideoFileClip(video_path) - - except: - self.video_corrupt_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' ' + _('Video file might be corrupt:') + ' \'' \ - + video_obj.name + '\'', - ) - - - def check_video_in_actual_dir(self, container_obj, video_obj, delete_path): - - """Called by self.delete_video(), .delete_descrip(), .delete_json(), - .delete_xml() and .delete_thumb(). - - If the video's parent container has an alternative download destination - set, we must check the corresponding media data object. If the latter - also has a media.Video object matching this video, then this function - returns None and nothing is deleted. Otherwise, the specified - delete_path is returned, so it can be deleted. - - Args: - - container_obj (media.Channel, media.Playlist, media.Folder): A - channel, playlist or folder - - video_obj (media.Video): A video contained in that channel, - playlist or folder - - delete_path (str): The path to a file which the calling function - wants to delete - - Return values: - - The specified delete_path if it can be deleted, or None if it - should not be deleted - - """ - - if container_obj.external_dir is not None \ - or container_obj.dbid == container_obj.master_dbid: - - # No alternative download destination to check - return delete_path - - else: - - # Get the channel/playlist/folder acting as container_obj's - # alternative download destination - master_obj = self.app_obj.media_reg_dict[container_obj.master_dbid] - - # Check its videos. Are there any videos with the same name? - for child_obj in master_obj.child_list: - - if child_obj.file_name is not None \ - and child_obj.file_name == video_obj.file_name: - - # Don't delete the file associated with this video - return None - - # There are no videos with the same name, so the file can be - # deleted - return delete_path - - - def stop_tidy_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Stops the tidy operation. - """ - - self.running_flag = False diff --git a/build/lib/tartube/updates.py b/build/lib/tartube/updates.py deleted file mode 100644 index 0f828d56..00000000 --- a/build/lib/tartube/updates.py +++ /dev/null @@ -1,1079 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Update operation classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import GObject - - -# Import other modules -import os -import queue -import re -import requests -import signal -import subprocess -import sys -import threading -import time - - -# Import our modules -import downloads -import utils -# Use same gettext translations -from mainapp import _ - - -# Classes - - -class UpdateManager(threading.Thread): - - """Called by mainapp.TartubeApp.update_manager_start() or - .update_manager_start_from_wizwin(). - - Python class to create a system child process, to do one of two jobs: - - 1. Install FFmpeg, matplotlib or streamlink (on MS Windows only) - - 2. Install youtube-dl, or update it to its most recent version. - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - update_type (str): 'ffmpeg' to install FFmpeg (on MS Windows only), - 'matplotlib' to install matplotlib (on MS Windows only), - 'streamlink' to install streamlink (on MS Windows onlY), or 'ytdl' - to install/update youtube-dl (or a fork of it) - - wiz_win_obj (wizwin.SetupWizWin or None): The calling setup wizard - window (if set, the main window doesn't exist yet) - - """ - - - # Standard class methods - - - def __init__(self, app_obj, update_type, wiz_win_obj=None): - - super(UpdateManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The calling setup wizard window (if set, the main window doesn't - # exist yet) - self.wiz_win_obj = wiz_win_obj - - # The child process created by self.create_child_process() - self.child_process = None - - # Read from the child process STDOUT (i.e. self.child_process.stdout) - # and STDERR (i.e. self.child_process.stderr) in an asynchronous way - # by polling this queue.PriorityQueue object - self.queue = queue.PriorityQueue() - self.stdout_reader = downloads.PipeReader(self.queue, 'stdout') - self.stderr_reader = downloads.PipeReader(self.queue, 'stderr') - - - # IV list - other - # --------------- - # The time (in seconds) between iterations of the loop in - # self.install_ffmpeg(), .install_matplotlib(), .install_streamlink() - # and .install_ytdl() - self.sleep_time = 0.1 - - # 'ffmpeg' to install FFmpeg (on MS Windows only), 'matplotlib' to - # install matplotlib (on MS Windows only), 'streamlink' to install - # streamlink (on MS Windows only) or 'ytdl' to install/update - # youtube-dl (or a fork of it) - self.update_type = update_type - # Flag set to True if the update operation succeeds, False if it fails - self.success_flag = False - - # The youtube-dl version number as a string, if captured from the child - # process (e.g. '2019.07.02') - self.ytdl_version = None - - # (For debugging purposes, store any STDOUT/STDERR messages received; - # otherwise we would just set a flag if a STDERR message was - # received) - self.stdout_list = [] - self.stderr_list = [] - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Initiates the download. - """ - - if self.update_type == 'ffmpeg': - self.install_ffmpeg() - elif self.update_type == 'matplotlib': - self.install_matplotlib() - elif self.update_type == 'streamlink': - self.install_streamlink() - else: - self.install_ytdl() - - - def create_child_process(self, cmd_list): - - """Called by self.install_ffmpeg(), .install_matplotlib(), - .install_streamlink() or .install_ytdl(). - - Based on code from downloads.VideoDownloader.create_child_process(). - - Executes the system command, creating a new child process which - executes youtube-dl. - - Updates self.stderr_list in the event of an error. - - Args: - - cmd_list (list): Python list that contains the command to execute - - """ - - # Strip double quotes from arguments - # (Since we're sending the system command one argument at a time, we - # don't need to retain the double quotes around any single argument - # and, in fact, doing so would cause an error) - cmd_list = utils.strip_double_quotes(cmd_list) - - # Create the child process - info = preexec = None - - if os.name == 'nt': - # Hide the child process window that MS Windows helpfully creates - # for us - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - else: - # Make this child process the process group leader, so that we can - # later kill the whole process group with os.killpg - preexec = os.setsid - - try: - self.child_process = subprocess.Popen( - cmd_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=preexec, - startupinfo=info, - ) - - except (ValueError, OSError) as error: - # (The code in self.run() will spot that the child process did not - # start) - self.stderr_list.append(_('Child process did not start')) - - - def install_ffmpeg(self): - - """Called by self.run(). - - A modified version of self.install_ytdl, that installs FFmpeg on an - MS Windows system. - - Creates a child process to run the installation process. - - Reads from the child process STDOUT and STDERR, and calls the main - application with the result of the update (success or failure). - """ - - # Show information about the update operation in the Output tab - self.install_ffmpeg_write_output( - _('Starting update operation, installing FFmpeg'), - ) - - # Create a new child process to install either the 64-bit or 32-bit - # version of FFmpeg, as appropriate - if sys.maxsize <= 2147483647: - binary = 'mingw-w64-i686-ffmpeg' - else: - binary = 'mingw-w64-x86_64-ffmpeg' - - # Prepare a system command... - cmd_list = ['pacman', '-S', binary, '--noconfirm'] - # ...and display it in the Output tab (if required) - self.install_ffmpeg_write_output( - ' '.join(cmd_list), - True, # A system command, not a message - ) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_ffmpeg_child_process(): - pass - - # (Generate our own error messages for debugging purposes, in certain - # situations) - if self.child_process is None: - self.stderr_list.append(_('FFmpeg installation did not start')) - - elif self.child_process.returncode > 0: - self.stderr_list.append( - _('Child process exited with non-zero code: {}').format( - self.child_process.returncode, - ) - ) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.update_manager_finished() - if not self.stderr_list: - self.success_flag = True - - # Show a confirmation in the the Output tab (or wizard window textview) - self.install_ffmpeg_write_output(_('Update operation finished')) - - # Let the timer run for a few more seconds to prevent Gtk errors - GObject.timeout_add( - 0, - self.app_obj.update_manager_halt_timer, - ) - - - def install_ffmpeg_write_output(self, msg, system_cmd_flag=False): - - """Called by self.install_ffmpeg(). - - Writes a message to the Output tab (or to the setup wizard window, if - called from there). - - Args: - - msg (str): The message to display - - system_cmd_flag (bool): If True, display system commands in a - different colour in the Output tab (ignored when writing in - the setup wizard window) - - """ - - if not self.wiz_win_obj: - - if not system_cmd_flag: - self.app_obj.main_win_obj.output_tab_write_stdout(1, msg) - else: - self.app_obj.main_win_obj.output_tab_write_system_cmd(1, msg) - - else: - - GObject.timeout_add( - 0, - self.wiz_win_obj.ffmpeg_page_write, - msg, - ) - - - def install_matplotlib(self): - - """Called by self.run(). - - A modified version of self.install_ytdl, that installs matplotlib on an - MS Windows system. - - Creates a child process to run the installation process. - - Reads from the child process STDOUT and STDERR, and calls the main - application with the result of the update (success or failure). - """ - - # Show information about the update operation in the Output tab - self.install_matplotlib_write_output( - _('Starting update operation, installing matplotlib'), - ) - - # Create a new child process to install either the 64-bit or 32-bit - # version of matplotlib, as appropriate - if sys.maxsize <= 2147483647: - binary = 'mingw-w64-i686-python-matplotlib' - else: - binary = 'mingw-w64-x86_64-python-matplotlib' - - # Prepare a system command... - cmd_list = ['pacman', '-S', binary, '--noconfirm'] - # ...and display it in the Output tab (if required) - self.install_matplotlib_write_output( - ' '.join(cmd_list), - True, # A system command, not a message - ) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_matplotlib_child_process(): - pass - - # (Generate our own error messages for debugging purposes, in certain - # situations) - if self.child_process is None: - self.stderr_list.append(_('matplotlib installation did not start')) - - elif self.child_process.returncode > 0: - self.stderr_list.append( - _('Child process exited with non-zero code: {}').format( - self.child_process.returncode, - ) - ) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.update_manager_finished() - if not self.stderr_list: - self.success_flag = True - - # Show a confirmation in the the Output tab (or wizard window textview) - self.install_matplotlib_write_output(_('Update operation finished')) - - # Let the timer run for a few more seconds to prevent Gtk errors - GObject.timeout_add( - 0, - self.app_obj.update_manager_halt_timer, - ) - - - def install_matplotlib_write_output(self, msg, system_cmd_flag=False): - - """Called by self.install_matplotlib(). - - Writes a message to the Output tab (or to the setup wizard window, if - called from there). - - Args: - - msg (str): The message to display - - system_cmd_flag (bool): If True, display system commands in a - different colour in the Output tab (ignored when writing in - the setup wizard window) - - """ - - if not system_cmd_flag: - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.output_tab_write_stdout, - 1, - msg, - ) - - else: - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.output_tab_write_system_cmd, - 1, - msg, - ) - - - def install_streamlink(self): - - """Called by self.run(). - - A modified version of self.install_ytdl, that installs streamlink on an - MS Windows system. - - Creates a child process to run the installation process. - - Reads from the child process STDOUT and STDERR, and calls the main - application with the result of the update (success or failure). - """ - - # Show information about the update operation in the Output tab - self.install_streamlink_write_output( - _('Starting update operation, installing streamlink'), - ) - - # Create a new child process to install either the 64-bit or 32-bit - # version of streamlink, as appropriate - if sys.maxsize <= 2147483647: - binary = 'mingw-w64-i686-streamlink' - else: - binary = 'mingw-w64-x86_64-streamlink' - - # Prepare a system command... - cmd_list = ['pacman', '-S', binary, '--noconfirm'] - # ...and display it in the Output tab (if required) - self.install_streamlink_write_output( - ' '.join(cmd_list), - True, # A system command, not a message - ) - - # Create a new child process using that command... - self.create_child_process(cmd_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_streamlink_child_process(): - pass - - # (Generate our own error messages for debugging purposes, in certain - # situations) - if self.child_process is None: - self.stderr_list.append(_('streamlink installation did not start')) - - elif self.child_process.returncode > 0: - self.stderr_list.append( - _('Child process exited with non-zero code: {}').format( - self.child_process.returncode, - ) - ) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.update_manager_finished() - if not self.stderr_list: - self.success_flag = True - - # Show a confirmation in the the Output tab (or wizard window textview) - self.install_streamlink_write_output(_('Update operation finished')) - - # Let the timer run for a few more seconds to prevent Gtk errors - GObject.timeout_add( - 0, - self.app_obj.update_manager_halt_timer, - ) - - - def install_streamlink_write_output(self, msg, system_cmd_flag=False): - - """Called by self.install_streamlink(). - - Writes a message to the Output tab (or to the setup wizard window, if - called from there). - - Args: - - msg (str): The message to display - - system_cmd_flag (bool): If True, display system commands in a - different colour in the Output tab (ignored when writing in - the setup wizard window) - - """ - - if not system_cmd_flag: - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.output_tab_write_stdout, - 1, - msg, - ) - - else: - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.output_tab_write_system_cmd, - 1, - msg, - ) - - - def install_ytdl(self): - - """Called by self.run(). - - Based on code from downloads.VideoDownloader.do_download(). - - Creates a child process to run the youtube-dl update. - - Reads from the child process STDOUT and STDERR, and calls the main - application with the result of the update (success or failure). - """ - - # Show information about the update operation in the Output tab (or in - # the setup wizard window, if called from there) - downloader = self.app_obj.get_downloader(self.wiz_win_obj) - self.install_ytdl_write_output( - _('Starting update operation, installing/updating ' + downloader), - ) - - # Prepare the system command - - # The user can change the system command for updating youtube-dl, - # depending on how it was installed - # (For example, if youtube-dl was installed via pip, then it must be - # updated via pip) - if self.wiz_win_obj \ - and self.wiz_win_obj.ytdl_update_current is not None: - ytdl_update_current = self.wiz_win_obj.ytdl_update_current - else: - ytdl_update_current = self.app_obj.ytdl_update_current - - # Special case: install yt-dlp with no dependencies, if required - if ( - ( - not self.wiz_win_obj \ - and self.app_obj.ytdl_fork == 'yt-dlp' \ - and self.app_obj.ytdl_fork_no_dependency_flag - ) or ( - self.wiz_win_obj \ - and self.wiz_win_obj.ytdl_fork == 'yt-dlp' \ - and self.wiz_win_obj.ytdl_fork_no_dependency_flag - ) - ): - if ytdl_update_current == 'ytdl_update_pip': - ytdl_update_current = 'ytdl_update_pip_no_dependencies' - - elif ytdl_update_current == 'ytdl_update_pip3' \ - or ytdl_update_current == 'ytdl_update_pip3_recommend': - ytdl_update_current = 'ytdl_update_pip3_no_dependencies' - - elif ytdl_update_current == 'ytdl_update_win_64': - ytdl_update_current = 'ytdl_update_win_64_no_dependencies' - - elif ytdl_update_current == 'ytdl_update_win_32': - ytdl_update_current = 'ytdl_update_win_32_no_dependencies' - - # Prepare a system command... - if os.name == 'nt' \ - and ytdl_update_current == 'ytdl_update_custom_path' \ - and re.search(r'\.exe$', self.app_obj.ytdl_path): - # Special case: on MS Windows, a custom path may point at an .exe, - # therefore 'python3' must be removed from the system command - # (we can't run 'python3.exe youtube-dl.exe' or anything like - # that) - cmd_list = [self.app_obj.ytdl_path, '-U'] - - else: - cmd_list = self.app_obj.ytdl_update_dict[ytdl_update_current] - - mod_list = [] - for arg in cmd_list: - - # Substitute in the fork, if one is specified - arg = self.app_obj.check_downloader(arg, self.wiz_win_obj) - # Convert a path beginning with ~ (not on MS Windows) - if os.name != 'nt': - arg = re.sub(r'^\~', os.path.expanduser('~'), arg) - - mod_list.append(arg) - - # ...and display it in the Output tab (if required) - self.install_ytdl_write_output( - ' '.join(mod_list), - True, # A system command, not a message - ) - - # Create a new child process using that command... - self.create_child_process(mod_list) - # ...and set up the PipeReader objects to read from the child process - # STDOUT and STDERR - if self.child_process is not None: - self.stdout_reader.attach_fh(self.child_process.stdout) - self.stderr_reader.attach_fh(self.child_process.stderr) - - while self.is_child_process_alive(): - - # Pause a moment between each iteration of the loop (we don't want - # to hog system resources) - time.sleep(self.sleep_time) - - # Read from the child process STDOUT and STDERR, in the correct - # order, until there is nothing left to read - while self.read_ytdl_child_process(downloader): - pass - - # (Generate our own error messages for debugging purposes, in certain - # situations) - if self.child_process is None: - - msg = _('Update did not start') - - self.stderr_list.append(msg) - self.install_ytdl_write_output(msg) - - elif self.child_process.returncode > 0: - - msg = _('Child process exited with non-zero code: {}').format( - self.child_process.returncode, - ) - - self.stderr_list.append(msg) - self.install_ytdl_write_output(msg) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.update_manager_finished - if not self.stderr_list: - self.success_flag = True - - # Show a confirmation in the the Output tab (or wizard window textview) - self.install_ytdl_write_output(_('Update operation finished')) - - # Let the timer run for a few more seconds to prevent Gtk errors (for - # systems with Gtk < 3.24) - GObject.timeout_add( - 0, - self.app_obj.update_manager_halt_timer, - ) - - - def install_ytdl_write_output(self, msg, system_cmd_flag=False): - - """Called by self.install_ytdl(). - - Writes a message to the Output tab (or to the setup wizard window, if - called from there). - - Args: - - msg (str): The message to display - - system_cmd_flag (bool): If True, display system commands in a - different colour in the Output tab (ignored when writing in - the setup wizard window) - - """ - - if not self.wiz_win_obj: - - if not system_cmd_flag: - self.app_obj.main_win_obj.output_tab_write_stdout(1, msg) - else: - self.app_obj.main_win_obj.output_tab_write_system_cmd(1, msg) - - else: - - GObject.timeout_add( - 0, - self.wiz_win_obj.downloader_page_write, - msg, - ) - - - def intercept_version_from_stdout(self, stdout, downloader): - - """Called by self.install_yt_dl() only. - - Check a STDOUT message, hoping to intercept the new youtube-dl version - number. - - Args: - - stdout (str): The STDOUT message - - downloader (str): The name of the downloader, e.g. 'yt-dlp' - - """ - - regex_list = [ - r'Requirement already up\-to\-date\: ' + downloader \ - + r' in .*\(([^\(\)]+)\)\s*$', - r'Requirement already satisfied\: ' + downloader \ - + r' in .*\(([^\(\)]+)\)\s*$', - r'yt-dlp is up to date \(([^\(\)]+)\)\s*$', - r'Successfully installed ' + downloader + r'\-([^\(\)]+)\s*$', - ] - - for regex in regex_list: - substring = re.search(regex, stdout) - if substring: - self.ytdl_version = substring.group(1) - return - - - def is_child_process_alive(self): - - """Called by self.install_ffmpeg(), .install_matplotlib(), - .install_streamlink(), .install_ytdl() and .stop_update_operation(). - - Based on code from downloads.VideoDownloader.is_child_process_alive(). - - Called continuously during the self.run() loop to check whether the - child process has finished or not. - - Return values: - - True if the child process is alive, otherwise returns False. - - """ - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def read_ffmpeg_child_process(self): - - """Called by self.install_ffmpeg(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Return values: - - True if either STDOUT or STDERR were read, None if both queues were - empty - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.app_obj.system_error, - 701, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # On MS Windows we use cp1252, so that Tartube can communicate with the - # Windows console - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - # Show command line output in the Output tab (or wizard window - # textview) - self.install_ffmpeg_write_output(data) - - # STDERR - else: - - # Ignore pacman warning messages, e.g. 'warning: dependency cycle - # detected:' - if data and not re.search(r'^warning\:', data): - - self.stderr_list.append(data) - - # Show command line output in the Output tab (or wizard window - # textview) - self.install_ffmpeg_write_output(data) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def read_matplotlib_child_process(self): - - """Called by self.install_matplotlib(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Return values: - - True if either STDOUT or STDERR were read, None if both queues were - empty - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.app_obj.system_error, - 702, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # On MS Windows we use cp1252, so that Tartube can communicate with the - # Windows console - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - # Show command line output in the Output tab (or wizard window - # textview) - self.install_matplotlib_write_output(data) - - # STDERR - else: - - # Ignore pacman warning messages, e.g. 'warning: dependency cycle - # detected:' - if data and not re.search(r'^warning\:', data): - - self.stderr_list.append(data) - - # Show command line output in the Output tab (or wizard window - # textview) - self.install_matplotlib_write_output(data) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def read_streamlink_child_process(self): - - """Called by self.install_matplotlib(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Return values: - - True if either STDOUT or STDERR were read, None if both queues were - empty - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.app_obj.system_error, - 703, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # On MS Windows we use cp1252, so that Tartube can communicate with the - # Windows console - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - # Show command line output in the Output tab (or wizard window - # textview) - self.install_streamlink_write_output(data) - - # STDERR - else: - - # Ignore pacman warning messages, e.g. 'warning: dependency cycle - # detected:' - if data and not re.search(r'^warning\:', data): - - self.stderr_list.append(data) - - # Show command line output in the Output tab (or wizard window - # textview) - self.install_streamlink_write_output(data) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def read_ytdl_child_process(self, downloader): - - """Called by self.install_ytdl(). - - Reads from the child process STDOUT and STDERR, in the correct order. - - Args: - - downloader (str): e.g. 'youtube-dl' - - Return values: - - True if either STDOUT or STDERR were read, None if both queues were - empty - - """ - - # mini_list is in the form [time, pipe_type, data] - try: - mini_list = self.queue.get_nowait() - - except: - # Nothing left to read - return None - - # Failsafe check - if not mini_list \ - or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'): - - # Just in case... - GObject.timeout_add( - 0, - self.app_obj.system_error, - 704, - 'Malformed STDOUT or STDERR data', - ) - - # STDOUT or STDERR has been read - data = mini_list[2].rstrip() - # On MS Windows we use cp1252, so that Tartube can communicate with the - # Windows console - data = data.decode(utils.get_encoding(), 'replace') - - # STDOUT - if mini_list[1] == 'stdout': - - # "It looks like you installed youtube-dl with a package manager, - # pip, setup.py or a tarball. Please use that to update." - # "The script youtube-dl is installed in '...' which is not on - # PATH. Consider adding this directory to PATH..." - if re.search('It looks like you installed', data) \ - or re.search( - 'The script ' + downloader + ' is installed', - data, - ): - self.stderr_list.append(data) - - else: - - # Try to intercept the new version number for youtube-dl - self.intercept_version_from_stdout(data, downloader) - self.stdout_list.append(data) - - # Show command line output in the Output tab (or wizard window - # textview) - self.install_ytdl_write_output(data) - - # STDERR - else: - - # If the user has pip installed, rather than pip3, they will by now - # (mid-2019) be seeing a Python 2.7 deprecation warning. Ignore - # that message, if received - # If a newer version of pip is available, the user will see a - # 'You should consider upgrading' warning. Ignore that too, if - # received - if not re.search('DEPRECATION', data) \ - and not re.search('You are using pip version', data) \ - and not re.search('You should consider upgrading', data): - self.stderr_list.append(data) - - # Show command line output in the Output tab (or wizard window - # textview) - self.install_ytdl_write_output(data) - - # Either (or both) of STDOUT and STDERR were non-empty - self.queue.task_done() - return True - - - def stop_update_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Based on code from downloads.VideoDownloader.stop(). - - Terminates the child process. - """ - - if self.is_child_process_alive(): - - if os.name == 'nt': - # os.killpg is not available on MS Windows (see - # https://bugs.python.org/issue5115 ) - self.child_process.kill() - - # When we kill the child process on MS Windows the return code - # gets set to 1, so we want to reset the return code back to - # 0 - self.child_process.returncode = 0 - - else: - os.killpg(self.child_process.pid, signal.SIGKILL) diff --git a/build/lib/tartube/utils.py b/build/lib/tartube/utils.py deleted file mode 100644 index a98954b1..00000000 --- a/build/lib/tartube/utils.py +++ /dev/null @@ -1,4787 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Utility functions used by code copied from youtube-dl-gui.""" - - -# Import Gtk modules -from gi.repository import Gtk, Gdk, GObject - - -# Import other modules -import datetime -import glob -import hashlib -import locale -import math -import os -import re -import requests -import shutil -import subprocess -import sys -import time -from urllib.parse import urlparse, urljoin - - -# Import our modules -import classes -import formats -import mainapp -import media -# Use same gettext translations -from mainapp import _ - - -# Functions - - -def add_links_to_entry_from_clipboard(app_obj, entry, duplicate_text=None, -drag_drop_text=None, no_modify_flag=None): - - """Called by various functions in mainWin.AddChannelDialogue and - mainwin.AddPlaylistDialogue. - - Function to add valid URLs from the clipboard to a Gtk.Entry, ignoring - anything that is not a valid URL. - - A duplicate URL can be specified, when the dialogue window's clipboard - monitoring is turned on; it prevents this function adding the same URL - that was added the previous time. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - entry (Gtk.Entry): The entry to which valis URLs should be added. - Only the first valid URL is added, replacing any previous contents - (unless the URL matches the specified duplicate - - duplicate_text (str): If specified, ignore the clipboard contents, if - it matches this URL - - drag_drop_text (str): If specified, use this text and ignore the - clipboard - - no_modify_flag (bool): If True, the entry is not updated, instead, - the URL that would have been added to it is merely returned - - Return values: - - The URL added to the entry (or that would have been added to the entry) - or None if no valid and non-duplicate URL was found in the - clipboard - - """ - - if drag_drop_text is None: - - # Get text from the system clipboard - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - cliptext = clipboard.wait_for_text() - - else: - - # Ignore the clipboard, and use the specified text - cliptext = drag_drop_text - - # Eliminate empty lines and any lines that are not valid URLs (we assume - # that it's one URL per line) - # Use the first valid line that doesn't match the duplicate (if specified) - if cliptext is not None and cliptext != Gdk.SELECTION_CLIPBOARD: - - for line in cliptext.splitlines(): - - # (Cope with multiple valid URLs on the same line, or a single - # valid URL with arbitrary text before and afterwards) - for item in line.split(): - - if check_url(item): - - item = strip_whitespace(item) - if re.search(r'\S', item) \ - and (duplicate_text is None or item != duplicate_text): - - if not no_modify_flag: - entry.set_text(item) - - return item - - # No valid and non-duplicate URL found - return None - - -def add_links_to_textview(app_obj, link_list, textbuffer, mark_start=None, -mark_end=None, drag_drop_text=None): - - """Called by mainwin.AddVideoDialogue.__init__(), - .on_window_drag_data_received() and .clipboard_timer_callback(). - - Also called by utils.add_links_to_textview_from_clipboard(). - - Function to add valid URLs from the clipboard to a Gtk.TextView, ignoring - anything that is not a valid URL, and ignoring duplicate URLs. - - If some text is supplied as an argument, uses that text rather than the - clipboard text - - Args: - - app_obj (mainapp.TartubeApp): The main application - - link_list (list): List of URLs to add to the textview - - textbuffer (Gtk.TextBuffer): The textbuffer to which valis URLs should - be added (unless they are duplicates) - - mark_start, mark_end (Gtk.TextMark): The marks at the start/end of the - buffer (using marks rather than iters prevents Gtk errors) - - drag_drop_text (str): If specified, use this text and ignore the - clipboard - - """ - - # Split each line by whitespace. This allows us to recognise multiple valid - # URLs on the same line, and also to interpret a line containing a URL - # and miscellaneous text - mod_link_list = [] - for line in link_list: - - for item in line.split(): - mod_link_list.append(item) - - # Eliminate empty lines and any lines that are not valid URLs (we assume - # that it's one URL per line) - # At the same time, trim initial/final whitespace - valid_list = [] - for line in mod_link_list: - if check_url(line): - - line = strip_whitespace(line) - if re.search(r'\S', line): - valid_list.append(line) - - if valid_list: - - # Some URLs survived the cull - - # Get the contents of the buffer - if mark_start is None or mark_end is None: - - # No Gtk.TextMarks supplied, we're forced to use iters - buffer_text = textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - else: - - buffer_text = textbuffer.get_text( - textbuffer.get_iter_at_mark(mark_start), - textbuffer.get_iter_at_mark(mark_end), - False, - ) - - # Remove any URLs that already exist in the buffer - line_list = buffer_text.splitlines() - mod_list = [] - for line in valid_list: - if not line in line_list: - mod_list.append(line) - - # Add any surviving URLs to the buffer, first adding a newline - # character, if the buffer doesn't end in one - if mod_list: - - if not re.search(r'\n\s*$', buffer_text) and buffer_text != '': - mod_list[0] = '\n' + mod_list[0] - - textbuffer.insert( - textbuffer.get_end_iter(), - str.join('\n', mod_list) + '\n', - ) - - -def add_links_to_textview_from_clipboard(app_obj, textbuffer, mark_start=None, -mark_end=None, drag_drop_text=None): - - """Called by mainwin.AddVideoDialogue.__init__(), - .on_window_drag_data_received() and .clipboard_timer_callback(). - - Function to add valid URLs from the clipboard to a Gtk.TextView, ignoring - anything that is not a valid URL, and ignoring duplicate URLs. - - If some text is supplied as an argument, uses that text rather than the - clipboard text - - Args: - - app_obj (mainapp.TartubeApp): The main application - - textbuffer (Gtk.TextBuffer): The textbuffer to which valis URLs should - be added (unless they are duplicates) - - mark_start, mark_end (Gtk.TextMark): The marks at the start/end of the - buffer (using marks rather than iters prevents Gtk errors) - - drag_drop_text (str): If specified, use this text and ignore the - clipboard - - """ - - if drag_drop_text is None: - - # Get text from the system clipboard - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - cliptext = clipboard.wait_for_text() - - else: - - # Ignore the clipboard, and use the specified text - cliptext = drag_drop_text - - # Pass the text on to the next function, first converting it into a list - if cliptext is not None: - add_links_to_textview( - app_obj, - cliptext.splitlines(), - textbuffer, - mark_start, - mark_end, - ) - - -def check_day(this_day_num, target_day_str): - - """Can be called by anything. - - formats.SPECIFIED_DAYS_DICT contains a set of strings representing one or - more days, e.g. 'every_day', 'monday'. - - Check whether one of those strings matches a particular day. - - Args: - - this_day_num (int): Number in the range 0 (Monday) to 6 (Sunday), - usually representing today - - target_day_str (str): One of the strings in formats.SPECIFIED_DAYS_DICT - - Return values: - - True if 'this_day_num' matches 'target_day_str', False otherwise - - """ - - if target_day_str != 'every_day': - - if (target_day_str == 'weekdays' and this_day_num > 4) \ - or (target_day_str == 'weekends' and this_day_num < 5) \ - or (target_day_str == 'monday' and this_day_num != 0) \ - or (target_day_str == 'tuesday' and this_day_num != 1) \ - or (target_day_str == 'wednesday' and this_day_num != 2) \ - or (target_day_str == 'thursday' and this_day_num != 3) \ - or (target_day_str == 'friday' and this_day_num != 4) \ - or (target_day_str == 'saturday' and this_day_num != 5) \ - or (target_day_str == 'sunday' and this_day_num != 6): - return False - - return True - - -def check_url(url): - - """Can be called by anything. - - Checks for valid URLs. - - Args: - - url (str): The URL to check - - Return values: - - True if the URL is valid, False if invalid - - """ - - url = strip_whitespace(url) - - # Based on various methods suggested by - # https://stackoverflow.com/questions/25259134/ - # how-can-i-check-whether-a-url-is-valid-using-urlparse - - try: - # Add a scheme, if the specified URL doesn't provide one - if not re.search(r'^[a-zA-Z]+://', url): - url = 'http://' + url - - final_url = urlparse(urljoin(url, '/')) - is_valid = ( - all([final_url.scheme, final_url.netloc, final_url.path]) - and len(final_url.netloc.split('.')) > 1 - and not re.search(r'\s', url) - ) - - return is_valid - - except: - return False - - -def clip_add_to_db(app_obj, dest_obj, orig_video_obj, clip_title, \ -clip_path=None): - - """Called by downloads.ClipDownloader.extract_stdout_data() and - process.ProcessManager.run(). - - Add the video clip to the Tartube database. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - dest_obj (media.Folder): The folder object into which the new video - object is to be created - - orig_video_obj (media.Video): The original video, from which the video - clip has been split - - clip_title (str): The clip title for the new video, matching its - filename - - clip_path (str): Full path to the clip, if known (if not known, we - guess the path) - - Return values: - - The new media.Video object on success, or None of failure - - """ - - new_video_obj = app_obj.add_video( - dest_obj, - None, # No source - False, # Not a simulated download - True, # Don't sort the container's child list yet - ) - - if not new_video_obj: - - return None - - else: - - source = orig_video_obj.source - if source is None or source == '': - source = _('No link') - - new_video_obj.set_name(clip_title) - new_video_obj.set_nickname(clip_title) - new_video_obj.set_video_descrip( - app_obj, - _('Split from original video') + ' (' \ - + str(orig_video_obj.dbid) + ')\n' + orig_video_obj.name \ - + '\n' + orig_video_obj.source, - app_obj.main_win_obj.descrip_line_max_len, - ) - - if clip_path is not None: - - # Use the supplied path (when called by downloads.ClipDownloader) - new_video_obj.set_file_from_path(clip_path) - - else: - - # Guess the path (when called by process.ProcessManager, it's safe - # to do that) - new_video_obj.set_file(clip_title, orig_video_obj.file_ext) - - # Specifying the original video clones its .receive_time - new_video_obj.set_receive_time(orig_video_obj) - new_video_obj.set_upload_time(orig_video_obj.upload_time) - - # (The video length and file size is set elsewhere) - - # The video exists, so mark it as downloaded (even if only the original - # video was downloaded) - app_obj.mark_video_downloaded(new_video_obj, True) - - # Copy the original video's thumbnail, if required - if app_obj.split_video_copy_thumb_flag: - - thumb_path = find_thumbnail(app_obj, orig_video_obj) - if thumb_path: - - new_video_path = new_video_obj.get_actual_path(app_obj) - thumb_name, thumb_ext = os.path.splitext(thumb_path) - video_name, video_ext = os.path.splitext(new_video_path) - new_thumb_path = video_name + thumb_ext - - if not os.path.isfile(new_thumb_path): - try: - shutil.copyfile(thumb_path, new_thumb_path) - except: - pass - - # Clips split off from an original use a different icon - new_video_obj.set_split_flag(True) - - # Now the video's properties are fully updated, the parent containers - # can be sorted - dest_obj.sort_children(app_obj) - app_obj.fixed_all_folder.sort_children(app_obj) - - # If the clips' parent media data object (a channel, playlist or - # folder) is selected in the Video Index, update the Video Catalogue - # for the clip - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_video, - new_video_obj, - ) - - return new_video_obj - - -def clip_extract_data(stamp_list, clip_num): - - """Can be called by anything. - - media.Video.stamp_list stores details for video clips in groups of three, - in the form - [start_stamp, stop_stamp, clip_title] - - This function is called with a copy of media.Video.stamp_list (or some data - in the same format), and the index of one of those groups, corresponding to - a single video clip. - - If 'stop_stamp' is not specified, then 'start_stamp' of the following - clip is used (unless that clip also starts at the same time, in which - case we use the next clip that does not start at the same time). - - If there are no more clips, then this clip will end at the end of the - video. - - Args: - - stamp_list (list): The copy of a media.Video's .stamp_list IV - - clip_num (int): The index of a group in stamp_list, the first clip is - #0. It is the calling function's responsibility to ensure that - clip_num is not outside the bounds of stamp_list - - Return values: - - Returns a list in the form - - start_stamp, stop_stamp, clip_title - - ...in which 'stop_stamp' might have been modified, as described - above - - """ - - list_size = len(stamp_list) - mini_list = stamp_list[clip_num] - - start_stamp = mini_list[0] - stop_stamp = mini_list[1] - clip_title = mini_list[2] - - if stop_stamp is None and clip_num < (list_size - 1): - - for i in range((clip_num + 1), list_size): - - next_list = stamp_list[i] - - if next_list[0] != start_stamp: - stop_stamp = next_list[0] - break - - return start_stamp, stop_stamp, clip_title - - -def clip_prepare_title(app_obj, video_obj, clip_title_dict, clip_title, -clip_num, clip_max): - - """Called by downloads.ClipDownloader.do_download_clips_with_ffmpeg(), etc, - and by process.ProcessManager.run(). - - Before creating a video clip, decide what its clip title should be. - The title depends on various settings. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video to be sent to FFmpeg - - clip_title_dict (dict): Dictionary of clip titles used when splitting a - video into clips), used to re-name duplicates - - clip_title (str): When splitting a video, the title of this video clip - (if specified) - - clip_num (int): When splitting a video, the number of video clips split - so far (including this one, so the first video clip is #1) - - clip_max (int): The number of clips to be split from this video in - total - - Return values: - - The clip title - - """ - - # If 'clip_title' is not specified, use a generic clip title - # (The value is None only when not splitting a video) - if clip_title is None or clip_title == '': - clip_title = app_obj.split_video_custom_title - - # All clips from the same video should be formatted with a fixed number of - # digits (so any list of files will appear in the correct order) - if clip_max > 99: - clip_str = "{:03d}".format(clip_num) - elif clip_max > 9: - clip_str = "{:02d}".format(clip_num) - else: - clip_str = str(clip_num) - - # Set the video clip's filename, using the specified format - # Note that dummy media.Video objects might not have a .file_name set. In - # that case, we can use the .nickname (which is optionally set by the - # user, but which might also be None) - orig_name = video_obj.file_name - if orig_name is None \ - and video_obj.dummy_flag \ - and video_obj.nickname != app_obj.default_video_name: - orig_name = video_obj.nickname - - if app_obj.split_video_name_mode == 'num': - mod_title = clip_str - elif app_obj.split_video_name_mode == 'clip': - mod_title = clip_title - elif app_obj.split_video_name_mode == 'num_clip': - mod_title = clip_str + ' ' + clip_title - elif app_obj.split_video_name_mode == 'clip_num': - mod_title = clip_title + ' ' + clip_str - - elif app_obj.split_video_name_mode == 'orig': - - if orig_name is None: - mod_title = app_obj.split_video_custom_title - else: - mod_title = orig_name - - elif app_obj.split_video_name_mode == 'orig_num': - - if orig_name is None: - mod_title = clip_str - else: - mod_title = orig_name + ' ' + clip_str - - elif app_obj.split_video_name_mode == 'orig_clip': - - if orig_name is None: - mod_title = clip_title - else: - mod_title = orig_name + ' ' + clip_title - - elif app_obj.split_video_name_mode == 'orig_num_clip': - - if orig_name is None: - mod_title = clip_str + ' ' + clip_title - else: - mod_title = orig_name + ' ' + clip_str + ' ' + clip_title - - elif app_obj.split_video_name_mode == 'orig_clip_num': - - if orig_name is None: - mod_title = clip_title + ' ' + clip_str - else: - mod_title = orig_name + ' ' + clip_title + ' ' + clip_str - - # Failsafe - if mod_title is None: - mod_title = clip_title - - # Ensure that we don't write multiple video clips with the same clip - # title (i.e. the same filename) - count = 0 - this_title = mod_title - - if video_obj.dummy_flag: - parent_dir = video_obj.dummy_dir - else: - parent_dir = video_obj.parent_obj.get_actual_dir(app_obj) - - if video_obj.file_ext is None: - - # (When launched from Classic Mode, we can't rely on the file name/ - # extension being available; see the comments in - # mainapp.TartubeApp.download_manager_finished() - this_path = os.path.abspath( - os.path.join( - parent_dir, - this_title, - ), - ) - - elif not app_obj.split_video_subdir_flag: - - this_path = os.path.abspath( - os.path.join( - parent_dir, - this_title + video_obj.file_ext, - ), - ) - - else: - - this_path = os.path.abspath( - os.path.join( - parent_dir, - video_obj.file_name, - this_title + video_obj.file_ext, - ), - ) - - while 1: - - if not this_title in clip_title_dict and not os.path.isfile(this_path): - - return this_title - - else: - - # (Proceed to the next iteration of the loop, adding a number - # to the end of the clip title until we get a file path that - # hasn't already been written) - count += 1 - this_title = mod_title + '_' + str(count) - - if video_obj.file_ext is None: - - this_path = os.path.abspath( - os.path.join( - parent_dir, - this_title, - ), - ) - - elif not app_obj.split_video_subdir_flag: - - this_path = os.path.abspath( - os.path.join( - parent_dir, - this_title + video_obj.file_ext, - ), - ) - - else: - - this_path = os.path.abspath( - os.path.join( - parent_dir, - video_obj.file_name, - this_title + video_obj.file_ext, - ), - ) - - -def clip_prepare_chapter_output_template(app_obj, video_obj, dest_dir): - - """Called by downloads.ClipDownloader.do_download_clips_with_chapters(). - - A slimmed-down version of utils.clip_prepare_title(), used when downloading - all clips from a video at the same time (in which case, each clip is named - using a youtube-dl output template). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video to be sent to FFmpeg - - dest_dir (str): Full path to the download destination directory - - Return values: - - The output template (including a full file path, unlike the return - value of utils.clip_prepare_title() ) - - """ - - # Set the video clip's filename, using the specified format - # Note that dummy media.Video objects might not have a .file_name set - # (especially in an operation started in Classic Mode), so we have to - # take account of that - if app_obj.split_video_name_mode == 'num': - mod_title = '%(section_number)s' - elif app_obj.split_video_name_mode == 'clip': - mod_title = '%(section_title)s' - elif app_obj.split_video_name_mode == 'num_clip': - mod_title = '%(section_number)s %(section_title)s' - elif app_obj.split_video_name_mode == 'clip_num': - mod_title = '%(section_title)s %(section_number)s' - - elif app_obj.split_video_name_mode == 'orig' \ - or app_obj.split_video_name_mode == 'orig_num': - - # N.B. We must have a unique clip name, so these two settings are - # combined - if video_obj.file_name is None: - mod_title = '%(section_number)s' - else: - mod_title = video_obj.file_name + ' %(section_number)s' - - elif app_obj.split_video_name_mode == 'orig_clip': - - if video_obj.file_name is None: - mod_title = '%(section_title)s' - else: - mod_title = video_obj.file_name + ' %(section_title)s' - - elif app_obj.split_video_name_mode == 'orig_num_clip': - - if video_obj.file_name is None: - mod_title = '%(section_number)s %(section_title)s' - else: - mod_title = video_obj.file_name \ - + ' %(section_number)s %(section_title)s' - - elif app_obj.split_video_name_mode == 'orig_clip_num': - - if video_obj.file_name is None: - mod_title = '%(section_title)s %(section_number)s' - else: - mod_title = video_obj.file_name \ - + ' %(section_title)s %(section_number)s' - - # Failsafe - if mod_title is None: - mod_title = '%(section_number)s' - - # Set the output template with its correct file path - if video_obj.file_ext is None: - - return os.path.abspath( - os.path.join( - dest_dir, - mod_title + '.%(ext)s', - ), - ) - - elif not app_obj.split_video_subdir_flag: - - return os.path.abspath( - os.path.join( - dest_dir, - mod_title + video_obj.file_ext, - ), - ) - - else: - - return os.path.abspath( - os.path.join( - dest_dir, - video_obj.file_name, - mod_title + video_obj.file_ext, - ), - ) - - -def clip_set_destination(app_obj, video_obj): - - """Called by downloads.ClipDownloader.do_download() and - process.ProcessManager.run(). - - Sets the media.Folder and/or the filestem folder in which the video clips - will be created/downloaded. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video object whose video files is to be - split into clips - - Return values: - - A list in the form: - - ( - parent_folder_object, parent_directory, - destination_folder_object, destination_directory - ) - - ...with all values set to None if there was an error - - """ - - # parent_obj is either the Video Clips folder, or the original video's - # container (which might be a channel, playlist or folder) - if app_obj.split_video_clips_dir_flag: - parent_obj = app_obj.fixed_clips_folder - else: - parent_obj = video_obj.parent_obj - - parent_dir = parent_obj.get_actual_dir(app_obj) - - # Now set the actual directory into which the video clips are to be - # created/downloaded - # Original and clip files in the same directory - if not app_obj.split_video_subdir_flag: - return parent_obj, parent_dir, parent_obj, parent_dir - - # Otherwise, video clips are in a sub-directory of 'parent_dir' - if not isinstance(parent_obj, media.Folder) \ - or not app_obj.split_video_add_db_flag: - - # Cannot create a media.Folder inside a channel/playlist, so simply - # create a sub-directory in the filesystem. The video clips can be - # created there, but can't be added to the Tartube database - dest_dir = os.path.abspath( - os.path.join( - parent_obj.get_actual_dir(app_obj), - # Sub-directory is named after the video - video_obj.file_name, - ), - ) - - if not os.path.isdir(dest_dir): - app_obj.make_directory(dest_dir) - - return parent_obj, parent_dir, None, dest_dir - - # Otherwise, we can create a media.Folder inside another media.Folder - if video_obj.dbid in app_obj.container_reg_dict: - - # A media.Folder with the right name already exists - dest_obj = app_obj.media_reg_dict[video_obj.dbid] - - if dest_obj.parent_obj != parent_obj: - - # There is already a media.Folder with the same name, somewhere - # else in the database. This is a fatal error - return None, None, None, None - - else: - - # Use the existing media.Folder - dest_dir = dest_obj.get_actual_dir(app_obj) - - return parent_obj, parent_dir, dest_obj, dest_dir - - # The media.Folder corresponding to the directory doesn't exist yet - dest_obj = app_obj.add_folder( - video_obj.name, # The folder name - parent_obj, - False, # No simulated downloads - parent_obj.restrict_mode, - ) - - if not dest_obj: - - # Some folders (e.g. Unsorted Videos) can't contain other folders - return None, None, None, None - - else: - - dest_dir = dest_obj.get_actual_dir(app_obj) - - return parent_obj, parent_dir, dest_obj, dest_dir - - -def compile_mini_options_dict(options_manager_obj): - - """Called by downloads.VideoDownloader.confirm_new_video() and - .confirm_old_video(). - - Also called by downloads.StreamDownloader.do_download(). - - Compiles a dictionary containing a subset of download options from the - specified options.OptionsManager object, to be passed on to - mainapp.TartubeApp.announce_video_download(). - - Args: - - options_manager_obj (options.OptionsManager): The options manager - for this download - - """ - - mini_options_dict = { - 'keep_description': \ - options_manager_obj.options_dict['keep_description'], - 'keep_info': \ - options_manager_obj.options_dict['keep_info'], - 'keep_annotations': \ - options_manager_obj.options_dict['keep_annotations'], - 'keep_thumbnail': \ - options_manager_obj.options_dict['keep_thumbnail'], - 'move_description': \ - options_manager_obj.options_dict['move_description'], - 'move_info': \ - options_manager_obj.options_dict['move_info'], - 'move_annotations': \ - options_manager_obj.options_dict['move_annotations'], - 'move_thumbnail': \ - options_manager_obj.options_dict['move_thumbnail'], - } - - return mini_options_dict - - -def convert_bytes_to_string(num_bytes): - - """Can be called by anything. - - Uses formats.FILESIZE_METRIC_DICT to convert an arbitrary integer, in - bytes, into a readable string like '27.5 MiB'. - - Based on code from https://stackoverflow.com/questions/12523586/ - python-format-size-application-converting-b-to-kb-mb-gb-tb - - Args: - - num_bytes (int): An integer, 0 or above - - Return values: - - A string formatted to 1dp - - """ - - # Don't want to return '0.0 B', this string looks a lot nicer - if num_bytes == 0: - return '0 KiB' - - unit_step = 1024 - unit_step_thresh = unit_step - 0.05 - last_label = formats.FILESIZE_METRIC_LIST[-1] - - for unit in formats.FILESIZE_METRIC_LIST: - if num_bytes < unit_step_thresh: - break - if unit != last_label: - num_bytes /= unit_step - - return '{:.1f} {}'.format(num_bytes, unit) - - -def convert_enhanced_template_from_json(convert_type, enhanced_name, \ -json_dict): - - """Can be called by anything. - - Typically called by media.Channel.update_rss_from_json() and - media.Playlist.update_rss_from_json(). - - 'convert_type' is one of the keys in formats.ENHANCED_SITE_DICT. Its - corresponding value is a list of templates for a URL for a video, channel, - playlist or RSS feed. - - The values in the list typically contain any of a set of four-letter - strings (e.g. ' ci '), which can be substituted for values provided by a - JSON dictionary, which was itself obtained from a video's metadata. See the - comments in formats.py for the full set of four-letter strings. - - Args: - - convert_type (str): 'convert_video_list', 'convert_channel_list', - 'convert_playlist_list', 'rss_channel_list' or 'rss_playlist_list' - - enhanced_name (str): A key in formats.ENHANCED_SITE_DICT, representing - a single website - - url (str): A URL from which video/channel/playlist names/IDs can be - extracted - - Return values: - - Returns the first template whose four-letter strings have all been - substituted out, or None if no such template is found - - """ - - if not enhanced_name in formats.ENHANCED_SITE_DICT: - return None - - mini_dict = formats.ENHANCED_SITE_DICT[enhanced_name] - if not convert_type in mini_dict or not mini_dict[convert_type]: - return None - - for template in mini_dict[convert_type]: - - if 'id' in json_dict \ - and json_dict['id'] \ - and template.find(' vi '): - template = re.sub(' vi ', json_dict['id'], template) - - if 'title' in json_dict \ - and json_dict['title'] \ - and template.find(' vn '): - template = re.sub(' vn ', json_dict['title'], template) - - if 'channel_id' in json_dict \ - and json_dict['channel_id'] \ - and template.find(' ci '): - template = re.sub(' ci ', json_dict['channel_id'], template) - - if 'channel' in json_dict \ - and json_dict['channel'] \ - and template.find(' cn '): - template = re.sub(' cn ', json_dict['channel'], template) - # (BitChute doesn't provide a 'channel' in its JSON) - elif enhanced_name == 'bitchute' \ - and 'uploader' in json_dict \ - and json_dict['uploader'] \ - and template.find(' cn '): - template = re.sub(' cn ', json_dict['uploader'], template) - - if 'playlist_id' in json_dict \ - and json_dict['playlist_id'] \ - and template.find(' pi '): - template = re.sub(' pi ', json_dict['playlist_id'], template) - - if 'playlist_title' in json_dict \ - and json_dict['playlist_title'] \ - and template.find(' pn '): - template = re.sub(' pn ', json_dict['playlist_title'], template) - - if template.find(' ') == -1: - return template - - return None - - -def convert_enhanced_template_from_url(convert_type, enhanced_name, url): - - """Can be called by anything. - - Typically called by media.Channel.update_rss_from_url() and - media.Playlist.update_rss_from_url(). - - 'convert_type' is one of the keys in formats.ENHANCED_SITE_DICT. Its - corresponding value is a list of templates for a URL for a video, channel, - playlist or RSS feed. - - The values in the list typically contain any of a set of four-letter - strings (e.g. ' ci '), which can be substituted for values extracted from - the 'url' argument. See the comments in formats.py for the full set of - four-letter strings. - - Args: - - convert_type (str): 'convert_video_list', 'convert_channel_list', - 'convert_playlist_list', 'rss_channel_list' or 'rss_playlist_list' - - enhanced_name (str): A key in formats.ENHANCED_SITE_DICT, representing - a single website - - url (str): A URL from which video/channel/playlist names/IDs can be - extracted - - Return values: - - Returns the first template whose four-letter strings have all been - substituted out, or None if no such template is found - - """ - - if not enhanced_name in formats.ENHANCED_SITE_DICT: - return None - - mini_dict = formats.ENHANCED_SITE_DICT[enhanced_name] - if not convert_type in mini_dict or not mini_dict[convert_type]: - return None - - vid, vname, cid, cname, pid, pname = extract_enhanced_template_components( - enhanced_name, - url, - ) - - for template in mini_dict[convert_type]: - - if vid is not None and template.find(' vi '): - template = re.sub(' vi ', vid, template) - if vname is not None and template.find(' vn '): - template = re.sub(' vn ', vname, template) - if cid is not None and template.find(' ci '): - template = re.sub(' ci ', cid, template) - if cname is not None and template.find(' cn '): - template = re.sub(' cn ', cname, template) - if pid is not None and template.find(' pi '): - template = re.sub(' pi ', pid, template) - if pname is not None and template.find(' pn '): - template = re.sub(' pn ', pname, template) - - if template.find(' ') == -1: - return template - - return None - - -def convert_path_to_temp(app_obj, old_path, move_flag=False): - - """Can be called by anything. - - Converts a full path to a file that would be stored in Tartube's data - directory (mainapp.TartubeApp.downloads_dir) into the equivalent path in - Tartube's temporary directory (mainapp.TartubeApp.temp_dl_dir). - - Optionally moves a file from one location to the other. - - Regardless of whether the file is moved or not, creates the destination - sub-directory if it doesn't already exist, and deletes the destination file - if it already exists (both of which prevent exceptions being raised). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - old_path (str): Full path to the existing file - - move_flag (bool): If True, the file is actually moved to the new - location - - Return values: - - Returns the converted full file path, or None if a filesystem error - occurs - - """ - - if old_path[0:len(app_obj.data_dir)] != app_obj.data_dir: - - # Special case: if 'old_path' is outside Tartube's data directory, then - # there is no equivalent path in the temporary directory - # Instead. dump everything into ../tartube-data/.temp/.dump - # There is a small risk of duplicates, but that shouldn't be anything - # more than an inconvenience - old_dir, old_file = os.path.split(old_path) - new_path = os.path.abspath( - os.path.join( - app_obj.temp_dir, - '.dump', - old_file, - ), - ) - - new_dir, new_filename = os.path.split(new_path) - - else: - - # Normal conversion within Tartube's data directory - data_dir_len = len(app_obj.downloads_dir) - - new_path = app_obj.temp_dl_dir + old_path[data_dir_len:] - new_dir, new_filename = os.path.split(new_path.strip("\"")) - - # The destination folder must exist, before moving files into it - if not os.path.exists(new_dir): - if not app_obj.make_directory(new_dir): - return None - - # On MS Windows, a file name new_path must not exist, or an exception will - # be raised - if os.path.isfile(new_path) \ - and not app_obj.remove_file(new_path): - return None - - # Move the file now, if the calling code requires that - if move_flag: - - rename_file(app_obj, old_path, new_path) - - # Return the converted file path - return new_path - - -def convert_seconds_to_string(seconds, short_flag=False): - - """Can be called by anything. - - Converts a time in seconds into a formatted string. - - Args: - - seconds (int or float): The time to convert - - short_flag (bool): If True, show '05:15' rather than '0:05:15' - - Return values: - - The converted string, e.g. '05:12' or '16:05:12' - - """ - - # Round up fractional seconds - if seconds is not None: - if seconds != int(seconds): - seconds = int(seconds) + 1 - else: - seconds = 1 - - if short_flag and seconds < 3600: - - # When required, show 05:15 rather than 0:05:15 - minutes = int(seconds / 60) - seconds = int(seconds % 60) - - return '{:02d}:{:02d}'.format(minutes, seconds) - - else: - return str(datetime.timedelta(seconds=seconds)) - - -def convert_string_to_bytes(text): - - """Can be called by anything. - - The opposite of utils.convert_bytes_to_string(), converting a stringified - value (e.g. '25.2KiB/s' or '25.2 KiB/s') into a value in bytes. - - Because of rounding errors (e.g. the value above has been rounded to 1 - decimal place), the value in bytes will only approximate the - original argument passed to utils.convert_bytes_to_string(). - - Args: - - text (str): The text to convert, the output of - utils.convert_bytes_to_string() - - Return values: - - A value in bytes (or 0 on error) - - """ - - # (From '25.2KiB/s', extract '25.2' and 'KiB') - match = re.search(r'^([\d\.]+)\s*([\w]+)', text) - if match: - value = match.groups()[0] - unit = match.groups()[1] - else: - return 0 - - if unit in formats.FILESIZE_METRIC_DICT: - return int(float(value) * formats.FILESIZE_METRIC_DICT[unit]) - else: - return 0 - - -def convert_slices_to_clips(app_obj, custom_dl_obj, slice_list, temp_flag): - - """Called by downloads.ClipDownloader.do_download_remove_slices() and - process.ProcessManager.slice_video(). - - Convert a list of video slices to be removed from a video into a list of - video clips to be retained. - - 'slice_list' is a list of dictionaries, one per slice, in the form - mini_dict['category'] = One of the values in - formats.SPONSORBLOCK_CATEGORY_LIST (e.g. 'sponsor') - mini_dict['action'] = One of the values in - formats.SPONSORBLOCK_ACTION_LIST (e.g. 'skip') - mini_dict['start_time'] - mini_dict['stop_time'] = Floating point values in seconds, the - beginning and end of the slice - mini_dict['duration'] = The video duration, as reported by - SponsorBlock. This valus is not required by Tartube code, and its - default value is 0 - - The returned list is in groups of two, in the form - [start_time, stop_time] - ...where 'start_time' and 'stop_time' are floating-point values in - seconds. 'stop_time' can be None to signify the end of the video, but - 'start_time' is 0 to signify the start of the video. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that applies. If not specified, all slices must be removed - - slice_list (list): The list of slices to be removed, in the form - described above - - temp_flag (bool): If True, the user specified slices in - mainwin.on_video_catalogue_process_slice(), so regardless of - the contents of downloads.CustomDLManager.slice_dict, all slices - must be removed - - Return values: - - The converted list described above - - """ - - clip_list = [] - count = 0 - # (The first clip starts at the beginning of the video) - previous_time = 0 - - for mini_dict in slice_list: - - # (Only remove video slices which the user has opted to remove) - if temp_flag \ - or custom_dl_obj is None \ - or custom_dl_obj.slice_dict[mini_dict['category']]: - - # (If the video starts with a removable slice, then the first - # clip will start after the slice) - if mini_dict['start_time'] != 0 \ - and mini_dict['start_time'] != '0': - - # Remove this slice - count += 1 - - # This clip start at the end of the previous slice. and - # ends at the start of this slice - clip_list.append([ - previous_time, - mini_dict['start_time'], - ]) - - # Next clip starts at the end of this slice - if mini_dict['stop_time'] is not None: - previous_time = mini_dict['stop_time'] - else: - # This clip ends at the end of the video; ignore any additional - # data in slice_list - return clip_list - - if previous_time != 0 and previous_time != '0': - - # (The last clip starts at the end of the last removable slice, and - # ends at the end of the video) - clip_list.append([ previous_time, None ]) - - return clip_list - - -def convert_youtube_to_hooktube(url): - - """Can be called by anything. - - Converts a YouTube weblink to a HookTube weblink (but doesn't modify links - to other sites. - - Args: - - url (str): The weblink to convert - - Return values: - - The converted string - - """ - - if re.search(r'^https?:\/\/(www\.)+youtube\.com', url): - - url = re.sub( - r'(www\.)+youtube\.com', - 'hooktube.com', - url, - # Substitute first occurrence only - 1, - ) - - return url - - -def convert_youtube_to_invidious(app_obj, url): - - """Can be called by anything. - - Converts a YouTube weblink to an Invidious weblink (but doesn't modify - links to other sites). - - Args: - - url (str): The weblink to convert - - Return values: - - The converted string - - """ - - if re.search(r'^https?:\/\/(www\.)+youtube\.com', url) \ - and re.search(r'\w+\.\w+', app_obj.custom_invidious_mirror): - - url = re.sub( - r'(www\.)+youtube\.com', - app_obj.custom_invidious_mirror, - url, - # Substitute first occurrence only - 1, - ) - - return url - - -def convert_youtube_to_other(app_obj, url, custom_dl_obj=None): - - """Can be called by anything. - - Converts a YouTube weblink to a weblink pointing at an alternative - YouTube front-end (such as Hooktube or Invidious). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - url (str): The weblink to convert - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that provides the alternative front-end. If not specified, - the General Custom Download Manager is used - - Return values: - - The converted string - - """ - - if custom_dl_obj is None: - custom_dl_obj = app_obj.general_custom_dl_obj - - if re.search(r'^https?:\/\/(www\.)+youtube\.com', url): - - url = re.sub( - r'(www\.)+youtube\.com', - custom_dl_obj.divert_website, - url, - # Substitute first occurrence only - 1, - ) - - return url - - -def disk_get_free_space(path, bytes_flag=False): - - """Can be called by anything. - - Returns the size of the disk on which a specified file/directory exists, - minus the used space on that disk. - - Args: - - path (str): Path to a file/directory on the disk, typically Tartube's - data directory - - bytes_flag (bool): False to return an integer value in GB (reduced to - 3 decimal places), True to return a value in bytes - - Return values: - - The free space in GB (or in bytes, if the flag is specified), or 0 if - the size can't be calculated for any reason - - """ - - try: - total_bytes, used_bytes, free_bytes = shutil.disk_usage( - os.path.realpath(path), - ) - - if not bytes_flag: - return round( (free_bytes / 1000000000), 3) - else: - return free_bytes - - except: - return 0 - - -def disk_get_total_space(path=None, bytes_flag=False): - - """Can be called by anything. - - Returns the size of the disk on which a specified file/directory exists. - - Args: - - path (str): Path to a file/directory on the disk, typically Tartube's - data directory - - bytes_flag (bool): False to return an integer value in GB (reduced to - 3 decimal places), True to return a value in bytes - - Return values: - - The total size in GB (or in bytes, if the flag is specified). If no - path or an invalid path is specified, returns 0 - - """ - - if path is None \ - or ( - not os.path.isdir(path) and not os.path.isfile(path) - ): - return 0 - - else: - - total_bytes, used_bytes, free_bytes = shutil.disk_usage( - os.path.realpath(path), - ) - - if not bytes_flag: - return round( (total_bytes / 1000000000), 3) - else: - return total_bytes - - -def disk_get_used_space(path=None, bytes_flag=False): - - """Can be called by anything. - - Returns the size of the disk on which a specified file/directory exists, - minus the free space on that disk. - - Args: - - path (str): Path to a file/directory on the disk, typically Tartube's - data directory - - bytes_flag (bool): False to return an integer value in GB (reduced to - 3 decimal places), True to return a value in bytes - - Return values: - - The used space in GB (or in bytes, if the flag is specified). If no - path or an invalid path is specified, returns 0 - - """ - - if path is None \ - or ( - not os.path.isdir(path) and not os.path.isfile(path) - ): - return 0 - - else: - - total_bytes, used_bytes, free_bytes = shutil.disk_usage( - os.path.realpath(path), - ) - - if not bytes_flag: - return round( (used_bytes / 1000000000), 3) - else: - return used_bytes - - -def extract_dummy_format(format_str): - - """Called by options.OptionsParser.build_video_format() and - downloads.StreamDownloader.choose_path(). - - A media.Video's .dummy_format IV is made up of three optional components - in a fixed order. - - Extract those components, and return them as a list. - - Args: - - format_str (str): The value of media.Video.dummy_format. The calling - code should have checked that the value is not None - - Return values: - - A list in the form (convert_flag, format, resolution), with any - unspecified values returned as None - - """ - - convert_flag = False - this_format = None - this_res = None - - split_list = format_str.split('_') - if split_list and split_list[0] == 'convert': - split_list.pop(0) - convert_flag = True - - if split_list \ - and ( - split_list[0] in formats.VIDEO_FORMAT_LIST \ - or split_list[0] in formats.AUDIO_FORMAT_LIST - ): - this_format = split_list.pop(0) - - if split_list \ - and split_list[0] in formats.VIDEO_RESOLUTION_LIST: - this_res = formats.VIDEO_RESOLUTION_DICT[split_list.pop(0)] - - return [ convert_flag, this_format, this_res ] - - -def extract_enhanced_template_components(enhanced_name, url): - - """Can be called by anything. - - Typically called by utils.convert_enhanced_template_from_url(). - - formats.ENHANCED_SITE_DICT provides a set of regexes which can be used to - extract video/channel/playlist names and IDs from a recognised URL. - - Extracts the names and IDs, returning them as a list. - - Args: - - enhanced_name (str): A key in formats.ENHANCED_SITE_DICT, representing - a single website - - url (str): A URL from which video/channel/playlist names/IDs can be - extracted - - Return values: - - A list in the form (vid, vname, cid, cname, pid, pname), any or all of - which may be None - - """ - - if not enhanced_name in formats.ENHANCED_SITE_DICT: - return None - - mini_dict = formats.ENHANCED_SITE_DICT[enhanced_name] - - vid = None - for regex in mini_dict['extract_vid_list']: - match = re.search(regex, url) - if match: - # The first group is the optional 'www.' component; we are looking - # for the second group - vid = match.groups()[1] - - vname = None - for regex in mini_dict['extract_vname_list']: - match = re.search(regex, url) - if match: - vname = match.groups()[1] - - cid = None - for regex in mini_dict['extract_cid_list']: - match = re.search(regex, url) - if match: - cid = match.groups()[1] - - cname = None - for regex in mini_dict['extract_cname_list']: - match = re.search(regex, url) - if match: - cname = match.groups()[1] - - pid = None - for regex in mini_dict['extract_pid_list']: - match = re.search(regex, url) - if match: - pid = match.groups()[1] - - pname = None - for regex in mini_dict['extract_pname_list']: - match = re.search(regex, url) - if match: - pname = match.groups()[1] - - return vid, vname, cid, cname, pid, pname - - -def extract_livestream_data(stderr): - - """Called by downloads.JSONFetcher.do_fetch() and - MiniJSONFetcher.do_fetch(). - - Also called by downloads.VideoDownloader.register_error_warning(). - - For some reason, YouTube messages giving the (approximate) start time of a - livestream are written to STDERR. - - Extracts various data and returns it. - - Args: - - stderr (text): A standard YouTube message - - Return values: - - If extraction is successful, returns a dictionary of three values: - - live_msg (str): Text that can be displayed in the Video Catalogue - live_time (int): Approximate time (matching time.time()) at which - the livestream is due to start - live_debut_flag (bool): True for a YouTube 'premiere' video, False - for an ordinary livestream - - If extraction fails, returns an empty list - - """ - - # This live event will begin in a few moments. - match_list = re.search( - r'This live event will begin in a few moments', - stderr, - ) - - if match_list: - - return { - 'live_msg': _('Live soon'), - 'live_time': int(time.time()), - 'live_debut_flag': False, - } - - # Premiere will begin shortly - match_list = re.search( - r'Premiere will begin shortly', - stderr, - ) - - if match_list: - - return { - 'live_msg': _('Debut soon'), - 'live_time': int(time.time()), - 'live_debut_flag': True, - } - - # This live event will begin in N minutes. - match_list = re.search( - r'This live event will begin in (\d+) minute', - stderr, - ) - - if match_list: - - group_list = match_list.groups() - number = int(group_list[0]) - - return { - 'live_msg': _('Live in {0} minutes').format(group_list[0]), - 'live_time': int(time.time()) + (number * 60), - 'live_debut_flag': False, - } - - # Premieres in N minutes - match_list = re.search( - r'Premieres in (\d+) minute', - stderr, - ) - - if match_list: - - group_list = match_list.groups() - number = int(group_list[0]) - - return { - 'live_msg': _('Debut in {0} minutes').format(group_list[0]), - 'live_time': int(time.time()) + (number * 60), - 'live_debut_flag': True, - } - - # This live event will begin in N hours. - match_list = re.search( - r'This live event will begin in (\d+) hour', - stderr, - ) - - if match_list: - - group_list = match_list.groups() - number = int(group_list[0]) - - return { - 'live_msg': _('Live in {0} hours').format(group_list[0]), - 'live_time': int(time.time()) + (number * 3600), - 'live_debut_flag': False, - } - - # Premieres in N hours - match_list = re.search( - r'Premieres in (\d+) hour', - stderr, - ) - - if match_list: - - group_list = match_list.groups() - number = int(group_list[0]) - - return { - 'live_msg': _('Debut in {0} hours').format(group_list[0]), - 'live_time': int(time.time()) + (number * 3600), - 'live_debut_flag': True, - } - - # This live event will begin in N days. - match_list = re.search( - r'This live event will begin in (\d+) day', - stderr, - ) - - if match_list: - - group_list = match_list.groups() - number = int(group_list[0]) - - return { - 'live_msg': _('Live in {0} days').format(group_list[0]), - 'live_time': int(time.time()) + (number * 86400), - 'live_debut_flag': False, - } - - # Premieres in N days - match_list = re.search( - r'Premieres in (\d+) day', - stderr, - ) - - if match_list: - - group_list = match_list.groups() - number = int(group_list[0]) - - return { - 'live_msg': _('Debut in {0} days').format(group_list[0]), - 'live_time': int(time.time()) + (number * 86400), - 'live_debut_flag': True, - } - - # Not a livestream, return an empty dictionary - return {} - - -def extract_path_components(path): - - """Can be called by anything. - - Based on the extract_data() function in youtube-dl-gui's downloaders.py. - - Given a full path to a file, extracts the directory, filename and file - extension, returning them as a list. - - Args: - - path (str): Full path to a file - - Return values: - - A list if the form (directory, filename, extension) - - """ - - directory, fullname = os.path.split(path) - filename, extension = os.path.splitext(fullname) - - return directory, filename, extension - - -def extract_timestamps_from_descrip(app_obj, descrip): - - """Can be called by anything. If setting a media.Video object's timestamp - list, call media.Video.extract_timestamps_from_descrip() instead. - - From some arbitrary text, attempt to extract a video's timestamps. - - Compiles a list in groups of three, in the form - [start_stamp, stop_stamp, clip_title] - 'start_stamp' is a string in the form h+:m+[:s+], e.g. '15:52', - '01:15:52' - 'stop_stamp' is always None, so that if 'start_stamp' is used to split - a video clip, the clip ends at the next 'start_stamp' (or at the - end of the video). It's up to the user to specify their own - 'stop_stamp' values explicitly, if they need them. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - descrip (str): The text from which timestamps are extracted. This - function assumes it is not an empty string - - """ - - regex = r'^\s*(' + app_obj.timestamp_regex + r')(\s.*)' - rev_regex = r'^(.*\s)(' + app_obj.timestamp_regex + r')\s*$' - digit_count = 0 - line_list = descrip.splitlines() - - temp_list = [] - stamp_list = [] - - for line in line_list: - - # (To improve detection, remove initial/final non-alphanumeric - # characters) - line = re.sub(r'^[\W\s]+', '', line) - line = re.sub(r'[\W\s]+$', '', line) - - # We would like every timestamp to be in the same format, i.e. - # either none of them have an h+ component, or all of them - # have an h+ component, with exactly the same number of digits - # (with any necessary leading zeroes) - # Extract the timestamps into a temporary list. For each timestamp, - # count the number of digits for the h+ component, and store the - # highest number of digits found - - # 15:52 Title - result = re.search(regex, line) - - if result: - - title = result.groups()[5] - hours = result.groups()[2] - minutes = result.groups()[3] - seconds = result.groups()[4] - - else: - - # Title 15:52 - result = re.search(rev_regex, line) - if result: - - title = result.groups()[0] - hours = result.groups()[3] - minutes = result.groups()[4] - seconds = result.groups()[5] - - if result: - - # Remove punctuation in the title, such as the hyphen in a line - # like 'Intro - 15.52', and strip leading/trailing whitespace - if title != '': - # !!! DEBUG This is not yet tested on other alphabets - title = re.sub(r'\s\W+\s', ' ', title) - title = strip_whitespace(title) - - # Use None as the title, rather than an empty string - if title == '': - title = None - - # Count the number of digits in the h+ component, having - # removed any leading zeroes - if hours is not None: - this_len = len(str(int(hours))) - if this_len > digit_count: - digit_count = this_len - - # Temporarily store the components - temp_list.append( [title, hours, minutes, seconds] ) - - # Now compile the a list of timestamps, formatted as strings in the - # form h:mm:ss or mm:ss, and with the correct number of leading - # zeroes applied - for mini_list in temp_list: - - stamp_list.append( - [ - timestamp_quick_format( # 'start_stamp' - app_obj, - mini_list[1], # Hours (optional) - mini_list[2], # Minutes - mini_list[3], # Seconds - digit_count, # Number of digits in h+ - ), - None, # 'stop_stamp' - mini_list[0], # 'clip_title' - - ] - ) - - # Sort by timestamp (since we can't assume the description does that) - stamp_list.sort() - - # Procedure complete - return stamp_list - - -def extract_timestamps_from_chapters(app_obj, chapter_list): - - """Can be called by anything. If setting a media.Video object's timestamp - list, call media.Video.extract_timestamps_from_chapters() instead. - - When supplied with a list of chapters from the video's metadata, - convert that data and store it as a list of timestamps. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - chapter_list (list): An ordered list containing a series of python - dictionaries. Each dictionary corresponds to the start of a - single chapter, and is expected to contain the keys - 'start_time', 'end_time' and 'title'. YouTube always supplies - all three, but in common with other parts of the code, we will - still accept the chapter if 'end_time' and/or 'title' are - missing - - """ - - # Extract each chapter in turn - stamp_list = [] - while chapter_list: - - chapter_dict = chapter_list.pop(0) - - if not 'start_time' in chapter_dict: - # Ignore this chapter - continue - else: - # Tartube timestamps use whole seconds, so round up any - # fractional values - start = int(chapter_dict['start_time']) - - stop = None - if 'end_time' in chapter_dict: - stop = int(chapter_dict['end_time']) - # If a chapter stops at second #10, the next chapter starts at - # second #10 - # But FFmpeg expects the chapter to stop at second #9, so take - # account of that - if chapter_list: - stop -= 1 - - clip_title = None - if 'title' in chapter_dict and chapter_dict['title'] != '': - clip_title = chapter_dict['title'] - - # 'start' and 'stop' are in seconds. Convert them to a string in - # the usual format, 'mm:ss' or 'h:mm:ss', where the 'h' component - # can contain any number of digits - # The True flag tells the function not to include the 'h' component - # if it's zero - start_stamp = convert_seconds_to_string(start, True) - if stop is not None: - stop_stamp = convert_seconds_to_string(stop, True) - else: - stop_stamp = None - - stamp_list.append( [start_stamp, stop_stamp, clip_title] ) - - # Procedure complete - return stamp_list - - -def find_available_name(app_obj, old_name, min_value=2, max_value=9999): - - """Can be called by anything. - - Finds a new name for a media.Channel, media.Playlist or media.Folder object - which is currently named 'old_name'. - - This function slightly modifies the name, converting 'my_name' into - 'my_name_N', where N is the smallest positive integer for which the name is - available. - - For this function, 'available' means that the name is not illegal (see the - comments in mainapp.TartubeApp.__init__() for a definition of illegal), - and is not in use by any other channel, playlist or folder (anywhere in - the database). - - If the specified 'old_name' is already in a format like 'Channel_4', then - the old number is stripped away, and this function starts looking from the - first integer after that (for example, 'Channel_5'). - - To preclude any possibility of infinite loops, the function will give up - after 'max_value' attempts. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - old_name (str): The name which is already in use by a media data object - - min_value (str): The first name to try. 2 by default, so the first - name checked will be 'my_name_2' - - max_value (int): When to give up. 9999 by default, meaning that this - function will try everything up to 'my_name_9999' before giving up. - If set to -1, this function never gives up - - Return values: - - None on failure, the new name on success - - """ - - # If old_name is already in the format 'my_name_N', where N is an integer - # in the range min_value < N < max_value, then strip it away - if re.search(r'\_\d+$', old_name): - - number = int(re.sub(r'^.*\_(\d+)$', r'\1', old_name)) - mod_name = re.sub(r'^(.*)\_\d+$', r'\1', old_name) - - if number >= 2 and number < max_value: - - old_name = mod_name - min_value = number + 1 - - # Compile a dictionary of unavailable names - check_dict = {} - for this_obj in app_obj.container_reg_dict.values(): - check_dict[this_obj.name] = None - - # Find an available name - if max_value != -1: - - for n in range (min_value, max_value): - - new_name = old_name + '_' + str(n) - if not new_name in check_dict: - return new_name - - # Failure - return None - - else: - - # Renaming is essential, for example, in calls from - # mainapp.TartubeApp.load_db(). Keep going indefinitely until an - # available name is found - n = 1 - while 1: - n += 1 - - new_name = old_name + '_' + str(n) - if not new_name in check_dict: - return new_name - - -def fetch_slice_data(app_obj, video_obj, page_num=None, terminal_flag=False): - - """Called by functions in downloads.VideoDownloader, - downloads.ClipDownloader and process.ProcessManager. - - Contacts the SponsorBlock API server to retrieve video slice data for the - speciffied video. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video for which SponsorBlock data should - should be retrieved. The calling code must check that its - .vid is set - - page_num (int or None): The page number of the Output tab where output - can be displayed. If None, then no output is displayed in the - Output tab at all; otherwise, output is displayed (or not) - depending on the usual Tartube settings - - terminal_flag (bool): If False, then no output is displayed in the - terminal at all, and no text is written to the downloader log at - all; otherwise, output is displayed/written (or not) depending - on the usual Tartube settings - - """ - - if not app_obj.sblock_obfuscate_flag: - - # Don't hash the video's ID - url = 'https://' + app_obj.custom_sblock_mirror + '/skipSegments/' - payload = { 'videoID': video_obj.vid } - - else: - - # Hash the video's ID - vid = video_obj.vid - hashed = hashlib.sha256(vid.encode()) - hex_str = hashed.hexdigest() - - short_str = hex_str[0:4] - - url = 'https://' + app_obj.custom_sblock_mirror + '/skipSegments/' \ - + short_str - payload = {} - - # Write to the Output tab and/or terminal, if required - msg = '[SponsorBlock] Contacting ' + url + '...' - if page_num is not None and app_obj.ytdl_output_stdout_flag: - app_obj.main_win_obj.output_tab_write_stdout(page_num, msg) - if terminal_flag: - if app_obj.ytdl_write_stdout_flag: - print(msg) - if app_obj.ytdl_log_stdout_flag: - app_obj.write_downloader_log(msg) - - # Contact the server - try: - request_obj = requests.get( - url, - params = payload, - timeout = app_obj.request_get_timeout, - ) - - except: - - msg = '[SponsorBlock] Could not contact server' - if page_num is not None and app_obj.ytdl_output_stderr_flag: - app_obj.main_win_obj.output_tab_write_stderr(page_num, msg) - if terminal_flag: - if app_obj.ytdl_write_stderr_flag: - print(msg) - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(msg) - - return - - # 400 = bad request, 404 = not found - if request_obj.status_code == 400: - - msg = '[SponsorBlock] Server returned error 400: bad request' - if page_num is not None and app_obj.ytdl_output_stderr_flag: - app_obj.main_win_obj.output_tab_write_stderr(page_num, msg) - if terminal_flag: - if app_obj.ytdl_write_stderr_flag: - print(msg) - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(msg) - - return - - elif request_obj.status_code == 404: - - msg = '[SponsorBlock] Server returned error 404: video ID not found' - if page_num is not None and app_obj.ytdl_output_stderr_flag: - app_obj.main_win_obj.output_tab_write_stderr(page_num, msg) - if terminal_flag: - if app_obj.ytdl_write_stderr_flag: - print(msg) - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(msg) - - return - - # (Conversion to JSON might produce an exception) - try: - json_table = request_obj.json() - - except: - - msg = '[SponsorBlock] Server returned invalid data' - if page_num is not None and app_obj.ytdl_output_stderr_flag: - app_obj.main_win_obj.output_tab_write_stderr(page_num, msg) - if terminal_flag: - if app_obj.ytdl_write_stderr_flag: - print(msg) - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(msg) - - return - - # Only use the data matching the video (since the video ID may have - # been obfuscated just above) - for mini_dict in json_table: - - if not 'videoID' in mini_dict or not 'segments' in mini_dict: - - msg = '[SponsorBlock] Server returned invalid data' - if page_num is not None and app_obj.ytdl_output_stderr_flag: - app_obj.main_win_obj.output_tab_write_stderr(page_num, msg) - if terminal_flag: - if app_obj.ytdl_write_stderr_flag: - print(msg) - if app_obj.ytdl_log_stderr_flag: - app_obj.write_downloader_log(msg) - - return - - elif mini_dict['videoID'] == video_obj.vid: - - video_obj.convert_slices(mini_dict['segments']) - - if page_num is not None: - - msg = '[SponsorBlock] Video slices retrieved: ' \ - + str(len(video_obj.slice_list)) - - if page_num is not None and app_obj.ytdl_output_stdout_flag: - app_obj.main_win_obj.output_tab_write_stdout( - page_num, - msg, - ) - - if terminal_flag: - if app_obj.ytdl_write_stdout_flag: - print(msg) - if app_obj.ytdl_log_stdout_flag: - app_obj.write_downloader_log(msg) - - return - - -def find_thumbnail(app_obj, video_obj, temp_dir_flag=False): - - """Can be called by anything. - - No way to know which image format is used by all websites for their video - thumbnails, so look for the most common ones, and return the path to the - thumbnail file if one is found. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video object handling the downloaded video - - temp_dir_flag (bool): If True, this function will look in Tartube's - temporary data directory, if the thumbnail isn't found in the main - data directory - - Return values: - - The full path to the thumbnail file, or None - - """ - - if video_obj.dummy_flag: - - # Special case: 'dummy' video objects (those downloaded in the Classic - # Mode tab) use different IVs - if video_obj.dummy_path is None: - return None - - file_name, file_ext = os.path.splitext(video_obj.dummy_path) - for this_ext in formats.IMAGE_FORMAT_EXT_LIST: - - thumb_path = file_name + this_ext - if os.path.isfile(thumb_path): - return thumb_path - - # No matching thumbnail found - return None - - else: - - # All other media.Video objects - for ext in formats.IMAGE_FORMAT_LIST: - - # Look in Tartube's permanent data directory - normal_path = video_obj.check_actual_path_by_ext(app_obj, ext) - if normal_path is not None: - return normal_path - - elif temp_dir_flag: - - # Look in temporary data directory - data_dir_len = len(app_obj.downloads_dir) - - temp_path = video_obj.get_actual_path_by_ext(app_obj, ext) - temp_path = app_obj.temp_dl_dir + temp_path[data_dir_len:] - if os.path.isfile(temp_path): - return temp_path - - # Catch YouTube .jpg thumbnails, in the form .jpg?... - # v2.2.005 The glob.glob() call crashes on certain videos. I'm not sure - # why, but we can circumvent the crash with try...except - normal_path = video_obj.get_actual_path_by_ext(app_obj, '.jpg*') - try: - for glob_path in glob.glob(normal_path): - if os.path.isfile(glob_path): - return glob_path - except: - pass - - if temp_dir_flag: - - temp_path = video_obj.get_actual_path_by_ext(app_obj, '.jpg*') - temp_path = app_obj.temp_dl_dir + temp_path[data_dir_len:] - - try: - for glob_path in glob.glob(temp_path): - if os.path.isfile(glob_path): - return glob_path - except: - pass - - # No matching thumbnail found - return None - - -def find_thumbnail_from_filename(app_obj, dir_path, filename): - - """Can be called by anything. - - A modified version of utils.find_thumbnail(), used when there is no - media.Video object, but instead the directory and filename for a video - (and its thumbnail). - - No way to know which image format is used by all websites for their video - thumbnails, so look for the most common ones, and return the path to the - thumbnail file if one is found. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - dir_path (str): The full path to the directory in which the video is - saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - Return values: - - The full path to the thumbnail file, or None - - """ - - for this_ext in formats.IMAGE_FORMAT_EXT_LIST: - - thumb_path = os.path.abspath( - os.path.join(dir_path, filename + this_ext), - ) - - if os.path.isfile(thumb_path): - return thumb_path - - # No matching thumbnail found - return None - - -def find_thumbnail_restricted(app_obj, video_obj): - - """Called by mainapp.TartubeApp.update_video_when_file_found(). - - Modified version of utils.find_thumbnail(). - - Returns the path of the thumbnail in the same directory as its video. The - path is returned as a list, so the calling code can convert it into the - equivalent path in the '.thumbs' subdirectory. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video object handling the downloaded video - - Return values: - - A list whose items, when combined, will be the full path to the - thumbnail file. If no thumbnail file was found, an empty list is - returned - - """ - - for ext in formats.IMAGE_FORMAT_LIST: - - actual_dir = video_obj.parent_obj.get_actual_dir(app_obj) - test_path = os.path.abspath( - os.path.join( - actual_dir, - video_obj.file_name + ext, - ), - ) - - if os.path.isfile(test_path): - return [ actual_dir, video_obj.file_name + ext ] - - # No matching thumbnail found - return [] - - -def find_thumbnail_webp_intact_or_broken(app_obj, video_obj): - - """Can be called by anything. - - In June 2020, YouTube started serving .webp thumbnails. Gtk cannot display - them, so Tartube typically converts themto .jpg. - - This is a modified version of utils.find_thumbnail(), which looks for - thumbnails in the .webp or malformed .jpg format, and return the path to - the thumbnail file if one is found. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video object handling the downloaded video - - Return values: - - The full path to the thumbnail file, or None - - """ - - for ext in ('.webp', '.jpg'): - - main_path = video_obj.get_actual_path_by_ext(app_obj, ext) - if os.path.isfile(main_path) \ - and ( - app_obj.ffmpeg_manager_obj.is_webp(main_path) \ - or app_obj.ffmpeg_manager_obj.is_mislabelled_webp(main_path) - ): - return main_path - - # The extension may be followed by additional characters, e.g. - # .jpg?sqp=-XXX (as well as several other patterns) - # v2.2.005 The glob.glob() call crashes on certain videos. I'm not - # sure why, but we can circumvent the crash with try...except - try: - for actual_path in glob.glob(main_path + '*'): - if os.path.isfile(actual_path) \ - and app_obj.ffmpeg_manager_obj.is_webp(actual_path): - return actual_path - except: - pass - - subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext( - app_obj, - ext, - ) - - if os.path.isfile(subdir_path) \ - and ( - app_obj.ffmpeg_manager_obj.is_webp(subdir_path) \ - or app_obj.ffmpeg_manager_obj.is_mislabelled_webp(subdir_path) - ): - return subdir_path - - try: - for actual_path in glob.glob(subdir_path + '*'): - if os.path.isfile(actual_path) \ - and app_obj.ffmpeg_manager_obj.is_webp(actual_path): - return actual_path - except: - pass - - # No webp thumbnail found - return None - - -def find_thumbnail_webp_strict(app_obj, video_obj): - - """Can be called by anything. - - A modified version of utils.find_thumbnail_webp_intact_or_broken(), to be - called by any code which wants a path to a .webp thumbnail, not caring - whether it is a valid .webp image or not. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video object handling the downloaded video - - Return values: - - The full path to the thumbnail file, or None - - """ - - ext = '.webp' - - main_path = video_obj.get_actual_path_by_ext(app_obj, ext) - if os.path.isfile(main_path): - return main_path - - subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext( - app_obj, - ext, - ) - if os.path.isfile(subdir_path): - return subdir_path - - # No webp thumbnail found - return None - - -def generate_ytdl_system_cmd(app_obj, media_data_obj, options_list, -dl_sim_flag=False, dl_classic_flag=False, missing_video_check_flag=None, -custom_dl_obj=None, divert_mode=None): - - """Called by downloads.VideoDownloader.do_download() and - mainwin.SystemCmdDialogue.update_textbuffer(). - - Based on YoutubeDLDownloader._get_cmd(). - - Prepare the system command that instructs youtube-dl to download the - specified media data object. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object to be downloaded - - options_list (list): A list of download options generated by a call to - options.OptionsParser.parse() - - dl_sim_flag (bool): True if a simulated download is to take place, - False if a real download is to take place - - dl_classic_flag (bool): True if the download operation was launched - from the Classic Mode tab, False otherwise - - missing_video_check_flag (bool): True if the download operation is - trying to detect missing videos (downloaded by user, but since - removed by the creator), False otherwise - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that applies, if any - - divert_mode (str): If not None, should be one of the values of - downloads.CustomDLManager.divert_mode: 'default', 'hooktube', - 'invidious' or 'other'. If not 'default', a media.Video object - whose source URL points to YouTube should be converted to the - specified alternative YouTube front-end (no conversion takes place - for channels/playlists/folders) - - Return values: - - A list that contains the system command to execute and its arguments - - """ - - # Simulate the download, rather than actually downloading videos, if - # required - if dl_sim_flag: - options_list.append('--dump-json') - - # If actually downloading videos, use (or create) an archive file so that, - # if the user deletes the videos, youtube-dl won't try to download them - # again - # We don't use an archive file when downloading into a system folder, - # unless a non-default location for the file has been specified - if ( - (not dl_classic_flag and app_obj.allow_ytdl_archive_flag) \ - or (dl_classic_flag and app_obj.classic_ytdl_archive_flag) - ): - if not dl_classic_flag \ - and ( - not isinstance(media_data_obj, media.Folder) - or not media_data_obj.fixed_flag - or app_obj.allow_ytdl_archive_mode != 'default' - ) and ( - not isinstance(media_data_obj, media.Video) - or not isinstance(media_data_obj.parent_obj, media.Folder) - or not media_data_obj.parent_obj.fixed_flag - or app_obj.allow_ytdl_archive_mode != 'default' - ): - # (Create the archive file in the media data object's default - # sub-directory, not the alternative download destination, as - # this helps youtube-dl to work the way we want it to work) - if isinstance(media_data_obj, media.Video): - dl_path = media_data_obj.parent_obj.get_default_dir(app_obj) - else: - dl_path = media_data_obj.get_default_dir(app_obj) - - if app_obj.allow_ytdl_archive_mode == 'top': - archive_dir = app_obj.data_dir - elif app_obj.allow_ytdl_archive_mode == 'custom': - if app_obj.allow_ytdl_archive_path is not None \ - and app_obj.allow_ytdl_archive_path != '': - archive_dir = app_obj.allow_ytdl_archive_path - else: - # Failsafe - archive_dir = dl_path - else: - # app_obj.allow_ytdl_archive_mode == 'default' - archive_dir = dl_path - - options_list.append('--download-archive') - options_list.append( - os.path.abspath( - os.path.join(archive_dir, app_obj.ytdl_archive_name), - ), - ) - - elif dl_classic_flag: - - # Create the archive file in destination directory - dl_path = media_data_obj.dummy_dir - - options_list.append('--download-archive') - options_list.append( - os.path.abspath( - os.path.join(dl_path, app_obj.ytdl_archive_name), - ), - ) - - # yt-dlp options - if app_obj.ytdl_fork is not None and app_obj.ytdl_fork == 'yt-dlp': - - # Fetch video comments, if required - if (dl_sim_flag and app_obj.check_comment_fetch_flag) \ - or (not dl_sim_flag and app_obj.dl_comment_fetch_flag): - options_list.append('--write-comments') - - # Show verbose output (youtube-dl debugging mode), if required - if app_obj.ytdl_write_verbose_flag: - options_list.append('--verbose') - - # Supply youtube-dl with the path to the ffmpeg/avconv binary, if the - # user has provided one - # If both paths have been set, prefer ffmpeg, unless the 'prefer_avconv' - # download option had been specified - if '--prefer-avconv' in options_list and app_obj.avconv_path is not None: - options_list.append('--ffmpeg-location') - options_list.append(app_obj.avconv_path) - elif app_obj.ffmpeg_path is not None: - options_list.append('--ffmpeg-location') - options_list.append(app_obj.ffmpeg_path) - elif app_obj.avconv_path is not None: - options_list.append('--ffmpeg-location') - options_list.append(app_obj.avconv_path) - - # Convert a YouTube URL to an alternative YouTube front-end, if required - source = media_data_obj.source - if isinstance(media_data_obj, media.Video) and divert_mode: - if divert_mode == 'hooktube': - source = convert_youtube_to_hooktube(source) - elif divert_mode == 'invidious': - source = convert_youtube_to_invidious(app_obj, source) - elif divert_mode == 'custom' \ - and custom_dl_obj.divert_website is not None \ - and len(custom_dl_obj.divert_website) > 2: - source = convert_youtube_to_other(app_obj, source, custom_dl_obj) - - # Convert a downloader path beginning with ~ (not on MS Windows) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Set the list. At the moment, a custom path must be preceded by 'python3' - # (Git #243), except on MS Windows when the custom path points at an .exe - # (Git #299) - if app_obj.ytdl_path_custom_flag \ - and (os.name != 'nt' or not re.search(r'\.exe$', ytdl_path)): - cmd_list = ['python3'] + [ytdl_path] + options_list + [source] - else: - cmd_list = [ytdl_path] + options_list + [source] - - return cmd_list - - -def generate_direct_system_cmd(app_obj, media_data_obj, options_obj): - - """Called by downloads.VideoDownloader.do_download() (only). - - A simplified version of utils.generate_ytdl_system_cmd(). - - Prepare the system command that instructs youtube-dl to download the - specified media data object, when the options.OptionsManager object wants - to set most command line options directly (i.e. when the 'direct_cmd_flag' - option is True). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object to be downloaded. Note that - its source URL might be overriden by the command line options - specified by the options.OptionsManager object - - options_obj (options.OptionsManager): The options manager object itself - - Return values: - - A list that contains the system command to execute and its arguments - - """ - - # Convert a downloader path beginning with ~ (not on MS Windows) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Parse the command line options specified by the 'extra_cmd_string' option - # (converting a string into a list of elements separated by whitespace) - options_list = parse_options(options_obj.options_dict['extra_cmd_string']) - - # Set the list. At the moment, a custom path must be preceded by 'python3' - # (Git #243), except on MS Windows when the custom path points at an .exe - # (Git #299) - if app_obj.ytdl_path_custom_flag \ - and (os.name != 'nt' or not re.search(r'\.exe$', ytdl_path)): - cmd_list = ['python3'] + [ytdl_path] + options_list - else: - cmd_list = [ytdl_path] + options_list - - # Add the source URL, if allowed - if not options_obj.options_dict['direct_url_flag'] \ - and media_data_obj.source is not None: - cmd_list.append( [media_data_obj.source] ) - - return cmd_list - - -def generate_slice_system_cmd(app_obj, orig_video_obj, options_list, \ -temp_dir, clip_count, start_time, stop_time, custom_dl_obj, divert_mode, \ -classic_flag): - - """Called by downloads.ClipDownloader.do_download_remove_slices() (only). - - A modified version of utils.generate_ffmpeg_split_system_cmd(). - - Prepares the system command that instructs youtube-dl to download a video - clip (instead of downloading the whole video). The downloaded clips are - expected to be concatenated together to make a single video. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - orig_video_obj (media.Video): The video object for the video from which - the clip is downloaded - - options_list (list): A list of download options generated by a call to - options.OptionsParser.parse() - - temp_dir (str): The directory into which the clip will be downloaded; - should be a temporary directory, which will be removed as soon as - the clips have been concatenated together - - clip_count (int): The nnumber of clips created so far, including this - one - - start_time (float): Time in seconds at which the clip starts - - stop_time (float): Time in seconds at which the clip stops. If not - specified, then the stop point is the end of the video - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that applies, if any - - divert_mode (str): If not None, should be one of the values of - downloads.CustomDLManager.divert_mode: 'default', 'hooktube', - 'invidious' or 'other'. If not 'default', a media.Video object - whose source URL points to YouTube should be converted to the - specified alternative YouTube front-end (no conversion takes place - for channels/playlists/folders) - - classic_flag (bool): Specifies the (standard) operation type. True for - 'classic_custom', False for 'custom_real' - - """ - - # Filter out download options that get in the way. A list of them is - # specified in mainapp.TartubeApp.split_ignore_option_dict - # Unlike the corresponding code in - # utils.generate_ffmpeg_split_system_cmd() (etc), we do write the - # metadata files (if required), but only for the first clip - # Exception: if the parent container's .dl_no_db_flag is set, don't write - # the metadata files at all - mod_options_list = [] - while options_list: - - item = options_list.pop(0) - if item in app_obj.split_ignore_option_dict \ - and ( - orig_video_obj.parent_obj.dl_no_db_flag \ - or clip_count > 1 \ - or ( - item != '--write-description' \ - and item != '--write-info-json' \ - and item != '--write-annotations' \ - and item != '--write-thumbnail' - ) - ): - if app_obj.split_ignore_option_dict[item]: - # This option takes an argument - options_list.pop(0) - - else: - mod_options_list.append(item) - - # Set the file format. If none is specified by download options, then use - # a default one - # FFmpeg cannot split media in DASH formats, so we need to check for that - # If --format is not specified but --extract_audio is specified, then don't - # insert a --format option at all - format_match_flag = False - audio_match_flag = False - for i in range(0, len(mod_options_list)): - - if mod_options_list[i] == '-f' or mod_options_list[i] == '--format': - - mod_options_list[i+1] = '(' + mod_options_list[i + 1] \ - + ')[protocol!*=dash]' - - format_match_flag = True - - if mod_options_list[i] == '-x' \ - or mod_options_list[i] == '--extract-audio': - - audio_match_flag = True - - if not format_match_flag and not audio_match_flag: - - mod_options_list.append('-f') - mod_options_list.append('(bestvideo+bestaudio/best)[protocol!*=dash]') - - # On MS Windows and yt-dlp, if --restrict-filenames is not specified, then - # insert --windows-filenames. (I can't be sure that yt-dlp knows it is - # running on MS Windows, when running inside MSYS2) - if app_obj.ytdl_fork is not None \ - and app_obj.ytdl_fork == 'yt-dlp' \ - and os.name == 'nt' \ - and not '--restrict-filenames' in mod_options_list: - mod_options_list.append('--windows-filenames') - - # Supply youtube-dl with the path to the ffmpeg binary, if the user has - # provided one - if app_obj.ffmpeg_path is not None: - mod_options_list.append('--ffmpeg-location') - mod_options_list.append(app_obj.ffmpeg_path) - - # Specify the external downloader, and the timestamps for the clip - slice_arg = '-ss ' + str(start_time) - if stop_time is not None: - slice_arg += ' -to ' + str(stop_time) - - # Force keyframes at cuts, if required, by re-encoding the clips - # This is a very inefficient way of doing things (for example, compared to - # yt-dlp's method), but at least it requires only one FFmpeg command - if app_obj.slice_video_force_keyframe_flag: - slice_arg += ' -c:v libx264' - - mod_options_list.append('--external-downloader') - mod_options_list.append('ffmpeg') - mod_options_list.append('--external-downloader-args') - mod_options_list.append(slice_arg) - - # Set the output template - mod_options_list.append('-o') - mod_options_list.append( - os.path.abspath( - os.path.join(temp_dir, 'clip_' + str(clip_count) + '.%(ext)s'), - ) - ) - - # Convert a YouTube URL to an alternative YouTube front-end, if required - source = orig_video_obj.source - if divert_mode is not None: - if divert_mode == 'hooktube': - source = convert_youtube_to_hooktube(source) - elif divert_mode == 'invidious': - source = convert_youtube_to_invidious(app_obj, source) - elif divert_mode == 'custom' \ - and custom_dl_obj.divert_website is not None \ - and len(custom_dl_obj.divert_website) > 2: - source = convert_youtube_to_other(app_obj, source, custom_dl_obj) - - # Convert a downloader path beginning with ~ (not on MS Windows) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Set the list. At the moment, a custom path must be preceded by 'python3' - # (Git #243), except on MS Windows when the custom path points at an .exe - # (Git #299) - if app_obj.ytdl_path_custom_flag \ - and (os.name != 'nt' or not re.search(r'\.exe$', ytdl_path)): - cmd_list = ['python3'] + [ytdl_path] + mod_options_list + [source] - else: - cmd_list = [ytdl_path] + mod_options_list + [source] - - return cmd_list - - -def generate_chapters_split_system_cmd(app_obj, orig_video_obj, options_list, -dest_dir, temp_dir, output_template, custom_dl_obj, divert_mode, classic_flag): - - """Called by downloads.ClipDownloader.do_download_clips_with_chapters() - (only). - - A simplified version of utils.generate_ytdl_system_cmd(). - - Prepares the system command that instructs yt-dlp to download chapters as - video clips (instead of downloading the whole video). - - Note that, at the time of writing (v2.4.297), only yt-dlp supports the - --split-chapters download option. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - orig_video_obj (media.Video): The video object for the video from which - the clip is downloaded - - options_list (list): A list of download options generated by a call to - options.OptionsParser.parse() - - dest_dir (str): The directory into which the clip will be downloaded - - temp_dir (str): A temporary directory used during the download - - output_template (str): Output template, including the full file path - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that applies, if any - - divert_mode (str): If not None, should be one of the values of - downloads.CustomDLManager.divert_mode: 'default', 'hooktube', - 'invidious' or 'other'. If not 'default', a media.Video object - whose source URL points to YouTube should be converted to the - specified alternative YouTube front-end (no conversion takes place - for channels/playlists/folders) - - classic_flag (bool): Specifies the (standard) operation type. True for - 'classic_custom', False for 'custom_real' - - """ - - # Filter out download options that get in the way. A list of them is - # specified in mainapp.TartubeApp.split_ignore_option_dict - mod_options_list = [] - while options_list: - - item = options_list.pop(0) - if item in app_obj.split_ignore_option_dict: - - if app_obj.split_ignore_option_dict[item]: - # This option takes an argument - options_list.pop(0) - - elif item == '-o' \ - or item == '--output' \ - or item == '-P' \ - or item == '--paths': - - # The output template is set below - # This option takes an argument - options_list.pop(0) - - else: - mod_options_list.append(item) - - # On MS Windows and yt-dlp, if --restrict-filenames is not specified, then - # insert --windows-filenames. (I can't be sure that yt-dlp knows it is - # running on MS Windows, when running inside MSYS2) - if app_obj.ytdl_fork is not None \ - and app_obj.ytdl_fork == 'yt-dlp' \ - and os.name == 'nt' \ - and not '--restrict-filenames' in mod_options_list: - mod_options_list.append('--windows-filenames') - - # Supply youtube-dl with the path to the ffmpeg binary, if the user has - # provided one - if app_obj.ffmpeg_path is not None: - mod_options_list.append('--ffmpeg-location') - mod_options_list.append(app_obj.ffmpeg_path) - - # Set up chapters, the output template, and the temporary directory - mod_options_list.append('--split-chapters') - mod_options_list.append('--output') - mod_options_list.append('chapter:' + output_template) - mod_options_list.append('--paths') - mod_options_list.append(temp_dir) - - # Show verbose output (youtube-dl debugging mode), if required - if app_obj.ytdl_write_verbose_flag: - options_list.append('--verbose') - - # Convert a YouTube URL to an alternative YouTube front-end, if required - source = orig_video_obj.source - if divert_mode is not None: - if divert_mode == 'hooktube': - source = convert_youtube_to_hooktube(source) - elif divert_mode == 'invidious': - source = convert_youtube_to_invidious(app_obj, source) - elif divert_mode == 'custom' \ - and custom_dl_obj.divert_website is not None \ - and len(custom_dl_obj.divert_website) > 2: - source = convert_youtube_to_other(app_obj, source, custom_dl_obj) - - # Convert a downloader path beginning with ~ (not on MS Windows) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Set the list. At the moment, a custom path must be preceded by 'python3' - # (Git #243), except on MS Windows when the custom path points at an .exe - # (Git #299) - if app_obj.ytdl_path_custom_flag \ - and (os.name != 'nt' or not re.search(r'\.exe$', ytdl_path)): - cmd_list = ['python3'] + [ytdl_path] + mod_options_list + [source] - else: - cmd_list = [ytdl_path] + mod_options_list + [source] - - return cmd_list - - -def generate_downloader_split_system_cmd(app_obj, orig_video_obj, options_list, -dest_dir, temp_dir, stamp_list, custom_dl_obj, divert_mode, classic_flag): - - """Called by downloads.ClipDownloader.do_download_clips_with_downloader() - (only). - - A simplified version of utils.generate_ytdl_system_cmd(). - - Prepares the system command that instructs yt-dlp to download sections as - video clips (instead of downloading the whole video). - - Note that, at the time of writing (v2.4.297), only yt-dlp supports the - --download-sections download option. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - orig_video_obj (media.Video): The video object for the video from which - the clip is downloaded - - options_list (list): A list of download options generated by a call to - options.OptionsParser.parse() - - dest_dir (str): The directory into which the clip will be downloaded - - temp_dir (str): A temporary directory used during the download - - stamp_list (list): List in groups of three, in the form - [start_timestamp, stop_timestamp, clip_title] - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that applies, if any - - divert_mode (str): If not None, should be one of the values of - downloads.CustomDLManager.divert_mode: 'default', 'hooktube', - 'invidious' or 'other'. If not 'default', a media.Video object - whose source URL points to YouTube should be converted to the - specified alternative YouTube front-end (no conversion takes place - for channels/playlists/folders) - - classic_flag (bool): Specifies the (standard) operation type. True for - 'classic_custom', False for 'custom_real' - - """ - - # Filter out download options that get in the way. A list of them is - # specified in mainapp.TartubeApp.split_ignore_option_dict - mod_options_list = [] - while options_list: - - item = options_list.pop(0) - if item in app_obj.split_ignore_option_dict: - - if app_obj.split_ignore_option_dict[item]: - # This option takes an argument - options_list.pop(0) - - elif item == '-o' \ - or item == '--output' \ - or item == '-P' \ - or item == '--paths': - - # The output template is set below - # This option takes an argument - options_list.pop(0) - - else: - mod_options_list.append(item) - - # On MS Windows and yt-dlp, if --restrict-filenames is not specified, then - # insert --windows-filenames. (I can't be sure that yt-dlp knows it is - # running on MS Windows, when running inside MSYS2) - if app_obj.ytdl_fork is not None \ - and app_obj.ytdl_fork == 'yt-dlp' \ - and os.name == 'nt' \ - and not '--restrict-filenames' in mod_options_list: - mod_options_list.append('--windows-filenames') - - # Supply youtube-dl with the path to the ffmpeg binary, if the user has - # provided one - if app_obj.ffmpeg_path is not None: - mod_options_list.append('--ffmpeg-location') - mod_options_list.append(app_obj.ffmpeg_path) - - # Set up sections, the output template, and the temporary directory - for i in range(len(stamp_list)): - - # List in the form [start_stamp, stop_stamp, clip_title] - # If 'stop_stamp' is not specified, then 'start_stamp' of the next clip - # is used. If there are no more clips, then this clip will end at the - # end of the video - start_stamp, stop_stamp, clip_title = clip_extract_data(stamp_list, i) - - mod_options_list.append('--download-sections') - # Use the clip title, if available; otherwise use timestamps - if stop_stamp is None or stop_stamp == '': - mod_options_list.append('*' + start_stamp + '-inf') - else: - mod_options_list.append('*' + start_stamp + '-' + stop_stamp) - - mod_options_list.append('--output') - mod_options_list.append('Video %(section_start)s %(section_end)s') - mod_options_list.append('--paths') - mod_options_list.append(temp_dir) - - # Force keyframes at cuts, if required - if app_obj.split_video_force_keyframe_flag: - mod_options_list.append('--force-keyframes-at-cuts') - - # Show verbose output (youtube-dl debugging mode), if required - if app_obj.ytdl_write_verbose_flag: - options_list.append('--verbose') - - # Convert a YouTube URL to an alternative YouTube front-end, if required - source = orig_video_obj.source - if divert_mode is not None: - if divert_mode == 'hooktube': - source = convert_youtube_to_hooktube(source) - elif divert_mode == 'invidious': - source = convert_youtube_to_invidious(app_obj, source) - elif divert_mode == 'custom' \ - and custom_dl_obj.divert_website is not None \ - and len(custom_dl_obj.divert_website) > 2: - source = convert_youtube_to_other(app_obj, source, custom_dl_obj) - - # Convert a downloader path beginning with ~ (not on MS Windows) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Set the list. At the moment, a custom path must be preceded by 'python3' - # (Git #243), except on MS Windows when the custom path points at an .exe - # (Git #299) - if app_obj.ytdl_path_custom_flag \ - and (os.name != 'nt' or not re.search(r'\.exe$', ytdl_path)): - cmd_list = ['python3'] + [ytdl_path] + mod_options_list + [source] - else: - cmd_list = [ytdl_path] + mod_options_list + [source] - - return cmd_list - - -def generate_ffmpeg_split_system_cmd(app_obj, orig_video_obj, options_list, -dest_dir, clip_title, start_stamp, stop_stamp, custom_dl_obj, divert_mode, -classic_flag): - - """Called by downloads.ClipDownloader.do_download_clips_with_ffmpeg() - (only). - - A simplified version of utils.generate_ytdl_system_cmd(). - - Prepares the system command that instructs youtube-dl to download a video - clip (instead of downloading the whole video). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - orig_video_obj (media.Video): The video object for the video from which - the clip is downloaded - - options_list (list): A list of download options generated by a call to - options.OptionsParser.parse() - - dest_dir (str): The directory into which the clip will be downloaded - - clip_title (str): The title of the clip (used as the clip's filename, - so must not be None) - - start_stamp (str): Timestamp at which the clip starts (in a format - recognised by FFmpeg) - - stop_stamp (str or None): Timestamp at which the clip stops. If not - specified, then the stop point is the end of the video - - custom_dl_obj (downloads.CustomDLManager or None): The custom download - manager that applies, if any - - divert_mode (str): If not None, should be one of the values of - downloads.CustomDLManager.divert_mode: 'default', 'hooktube', - 'invidious' or 'other'. If not 'default', a media.Video object - whose source URL points to YouTube should be converted to the - specified alternative YouTube front-end (no conversion takes place - for channels/playlists/folders) - - classic_flag (bool): Specifies the (standard) operation type. True for - 'classic_custom', False for 'custom_real' - - """ - - # Filter out download options that get in the way. A list of them is - # specified in mainapp.TartubeApp.split_ignore_option_dict - mod_options_list = [] - while options_list: - - item = options_list.pop(0) - if item in app_obj.split_ignore_option_dict: - - if app_obj.split_ignore_option_dict[item]: - # This option takes an argument - options_list.pop(0) - - else: - mod_options_list.append(item) - - # Set the file format. If none is specified by download options, then use - # a default one - # FFmpeg cannot split media in DASH formats, so we need to check for that - # If --format is not specified but --extract_audio is specified, then don't - # insert a --format option at all - format_match_flag = False - audio_match_flag = False - for i in range(0, len(mod_options_list)): - - if mod_options_list[i] == '-f' or mod_options_list[i] == '--format': - - mod_options_list[i+1] = '(' + mod_options_list[i + 1] \ - + ')[protocol!*=dash]' - - format_match_flag = True - - if mod_options_list[i] == '-x' \ - or mod_options_list[i] == '--extract-audio': - - audio_match_flag = True - - if not format_match_flag and not audio_match_flag: - - mod_options_list.append('-f') - mod_options_list.append('(bestvideo+bestaudio/best)[protocol!*=dash]') - - # On MS Windows and yt-dlp, if --restrict-filenames is not specified, then - # insert --windows-filenames. (I can't be sure that yt-dlp knows it is - # running on MS Windows, when running inside MSYS2) - if app_obj.ytdl_fork is not None \ - and app_obj.ytdl_fork == 'yt-dlp' \ - and os.name == 'nt' \ - and not '--restrict-filenames' in mod_options_list: - mod_options_list.append('--windows-filenames') - - # Supply youtube-dl with the path to the ffmpeg binary, if the user has - # provided one - if app_obj.ffmpeg_path is not None: - mod_options_list.append('--ffmpeg-location') - mod_options_list.append(app_obj.ffmpeg_path) - - # Specify the external downloader, and the timestamps for the clip - stamp_arg = '-ss ' + start_stamp - if stop_stamp is not None: - stamp_arg += ' -to ' + stop_stamp - - # Force keyframes at cuts, if required, by re-encoding the clips - # This is a very inefficient way of doing things (for example, compared to - # yt-dlp's method), but at least it requires only one FFmpeg command - if app_obj.split_video_force_keyframe_flag: - stamp_arg += ' -c:v libx264' - - mod_options_list.append('--external-downloader') - mod_options_list.append('ffmpeg') - mod_options_list.append('--external-downloader-args') - mod_options_list.append('ffmpeg:' + stamp_arg) - - # Set the output template - if not classic_flag: - - template = os.path.abspath( - os.path.join(dest_dir, clip_title + '.%(ext)s'), - ) - - else: - - template = os.path.abspath( - os.path.join(orig_video_obj.dummy_dir, clip_title + '.%(ext)s'), - ) - - mod_options_list.append('-o') - mod_options_list.append(template) - - # Show verbose output (youtube-dl debugging mode), if required - if app_obj.ytdl_write_verbose_flag: - options_list.append('--verbose') - - # Convert a YouTube URL to an alternative YouTube front-end, if required - source = orig_video_obj.source - if divert_mode is not None: - if divert_mode == 'hooktube': - source = convert_youtube_to_hooktube(source) - elif divert_mode == 'invidious': - source = convert_youtube_to_invidious(app_obj, source) - elif divert_mode == 'custom' \ - and custom_dl_obj.divert_website is not None \ - and len(custom_dl_obj.divert_website) > 2: - source = convert_youtube_to_other(app_obj, source, custom_dl_obj) - - # Convert a downloader path beginning with ~ (not on MS Windows) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Set the list. At the moment, a custom path must be preceded by 'python3' - # (Git #243), except on MS Windows when the custom path points at an .exe - # (Git #299) - if app_obj.ytdl_path_custom_flag \ - and (os.name != 'nt' or not re.search(r'\.exe$', ytdl_path)): - cmd_list = ['python3'] + [ytdl_path] + mod_options_list + [source] - else: - cmd_list = [ytdl_path] + mod_options_list + [source] - - return cmd_list - - -def generate_m3u_system_cmd(app_obj, media_data_obj): - - """Called by downloads.StreamDownloader.do_download_m3u(). - - Prepare the system command that instructs youtube-dl to download the - .m3u manifest for the URL associated with the media data object. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - media_data_obj (media.Video): The media data object whose .m3u manifest - is to be downloaded - - Return values: - - A list that contains the system command to execute and its arguments - - """ - - # Convert a downloader path beginning with ~ (not on MS Windows) - ytdl_path = app_obj.check_downloader(app_obj.ytdl_path) - if os.name != 'nt': - ytdl_path = re.sub(r'^\~', os.path.expanduser('~'), ytdl_path) - - # Set the list. At the moment, a custom path must be preceded by 'python3' - # (Git #243), except on MS Windows when the custom path points at an .exe - # (Git #299) - if app_obj.ytdl_path_custom_flag \ - and (os.name != 'nt' or not re.search(r'\.exe$', ytdl_path)): - cmd_list = ['python3'] + [ytdl_path] + ['-g'] + [media_data_obj.source] - else: - cmd_list = [ytdl_path] + ['-g'] + [media_data_obj.source] - - return cmd_list - - -def generate_streamlink_system_cmd(app_obj, media_data_obj, path): - - """Called by downloads.StreamDownloader.do_download_streamlink(). - - Prepare the system command that instructs streamlink to download the - media.Video object. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - media_data_obj (media.Video): The media data object whose URL is to be - downloaded - - path (str): The full path to the output video - - Return values: - - A list that contains the system command to execute and its arguments - - """ - - if app_obj.streamlink_path is None: - # (Assume the binary is in the user's PATH) - streamlink_path = 'streamlink' - else: - streamlink_path = app_obj.streamlink_path - - return [ - streamlink_path, - '--hls-live-restart', - '--force', # Streamlink has no option to resume old stream - '--stream-timeout', - str(app_obj.livestream_dl_timeout * 60), - '-o', - path, - media_data_obj.source, - 'best', - ] - - -def get_encoding(): - - """Called by utils.convert_item(). - - Based on the get_encoding() function in youtube-dl-gui. - - Return values: - - The system encoding - - """ - - try: - encoding = locale.getpreferredencoding() - 'TEST'.encode(encoding) - except: - encoding = 'UTF-8' - - return encoding - - -def get_local_time(): - - """Can be called by anything. - - Returns a datetime object that has been converted from UTC to the local - time zone. - - Return values: - - A datetime.datetime object, configured to the local time zone - - """ - - utc = datetime.datetime.utcfromtimestamp(time.time()) - return utc.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None) - - -def get_dl_config_path(app_obj): - - """Can be called by anything. - - Returns the full path to the youtube-dl configuration file, in the location - Tartube assuems it to be. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - Return values: - - The full path - - """ - - if app_obj.ytdl_fork is None: - ytdl_fork = 'youtube-dl' - else: - ytdl_fork = app_obj.ytdl_fork - - if os.name != 'nt': - - return os.path.abspath( - os.path.join( - os.path.expanduser('~'), - '.config', - ytdl_fork, - 'config', - ), - ) - - else: - - return os.path.abspath( - os.path.join( - app_obj.script_parent_dir, - ytdl_fork + '.conf', - ), - ) - - -def get_options_manager(app_obj, media_data_obj): - - """Can be called by anything. Subsequently called by this function - recursively. - - Fetches the options.OptionsManager which applies to the specified media - data object. - - The media data object might specify its own options.OptionsManager, or - we might have to use the parent's, or the parent's parent's (and so - on). As a last resort, use General Options Manager. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): A media data object - - Return values: - - The options.OptionsManager object that applies to the specified media - data object - - """ - - if media_data_obj.options_obj: - return media_data_obj.options_obj - elif media_data_obj.parent_obj: - return get_options_manager(app_obj, media_data_obj.parent_obj) - else: - return app_obj.general_options_obj - - -def handle_files_after_download(app_obj, options_obj, dir_path, filename, -dummy_obj=None): - - """Called by various functions in downloads.py, after a video is checked/ - downloaded but not added to Tartube's database. - - Handles the removal of the description, JSON and thumbnail files, according - to the settings in the options.OptionsManager object. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - options_obj (options.OptionsManager object): Specifies the download - options for this download - - dir_path (str): The full path to the directory in which the video is - saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - dummy_obj (media.Video or None): If specified, a 'dummy' video used by - the Classic Mode tab (and not added to Tartube's database) - - """ - - # Description file - descrip_path = os.path.abspath( - os.path.join(dir_path, filename + '.description'), - ) - - if descrip_path and not options_obj.options_dict['keep_description']: - - new_path = convert_path_to_temp( - app_obj, - descrip_path, - ) - - if os.path.isfile(descrip_path): - if not os.path.isfile(new_path): - app_obj.move_file_or_directory(descrip_path, new_path) - else: - app_obj.remove_file(descrip_path) - - # (Don't replace a file that already exists) - elif descrip_path \ - and not os.path.isfile(descrip_path) \ - and options_obj.options_dict['move_description'] \ - and dummy_obj: - - move_metadata_to_subdir(app_obj, dummy_obj, '.description') - - # JSON data file - json_path = os.path.abspath( - os.path.join(dir_path, filename + '.info.json'), - ) - - if json_path and not options_obj.options_dict['keep_info']: - - new_path = convert_path_to_temp(app_obj, json_path) - - if os.path.isfile(json_path): - if not os.path.isfile(new_path): - app_obj.move_file_or_directory(json_path, new_path) - else: - app_obj.remove_file(json_path) - - elif json_path \ - and not os.path.isfile(json_path) \ - and options_obj.options_dict['move_info'] \ - and dummy_obj: - - move_metadata_to_subdir(app_obj, dummy_obj, '.info.json') - - # (Annotations removed by YouTube in 2019 - see comments elsewhere) - - # Thumbnail file - if dummy_obj: - thumb_path = find_thumbnail(app_obj, dummy_obj) - else: - thumb_path = find_thumbnail_from_filename(app_obj, dir_path, filename) - - if thumb_path and not options_obj.options_dict['keep_thumbnail']: - - new_path = convert_path_to_temp(app_obj, thumb_path) - - if os.path.isfile(thumb_path): - if not os.path.isfile(new_path): - app_obj.move_file_or_directory(thumb_path, new_path) - else: - app_obj.remove_file(thumb_path) - - elif thumb_path \ - and not os.path.isfile(thumb_path) \ - and options_obj.options_dict['move_thumbnail'] \ - and dummy_obj: - - move_thumbnail_to_subdir(app_obj, dummy_obj) - - -def is_enhanced(url): - - """Can be called by anything, usually called by - media.GenericRemoteContainer.set_source(). - - Checks whether a URL matches one of the 'enhanced' websites specified by - formats.ENHANCED_SITE_DICT. - - Args: - - url (str or None): The URL to check - - Return values: - - Returns a key in formats.ENHANCED_SITE_DICT or, if the URL does not - match an 'enhanced' website, returns None. If no URL is specified, - returns None - - """ - - if url is None: - return None - - for key in formats.ENHANCED_SITE_LIST: - - mini_dict = formats.ENHANCED_SITE_DICT[key] - for regex in mini_dict['detect_list']: - if re.search(regex, url): - return mini_dict['name'] - - return None - - -def is_video_enhanced(video_obj): - - """Can be called by anything. - - Checks whether a video's parent channel or playlist matches one of the - 'enhanced' websites specified by formats.ENHANCED_SITE_DICT. - - If the video's parent is a media.Folder, do the check on the video's own - .source. - - Args: - - video_obj (media.Video): The media data object to check - - Return values: - - Returns a key in formats.ENHANCED_SITE_DICT or, if the video does not - originate from an 'enhanced' website, returns None. If no URL is - specified, returns None - - """ - - if isinstance(video_obj.parent_obj, media.Folder): - return is_enhanced(video_obj.source) - else: - return is_enhanced(video_obj.parent_obj.source) - - -def match_subs(custom_dl_obj, subs_list): - - """Called by downloads.DownloadList.create_item() and - downloads.VideoDownloader.confirm_sim_video(). - - The CustomDLManager object may specify one or more languages; compare - that list to a video's list of available subtitles, to see if there are any - matches. - - Args: - - custom_dl_obj (downloads.CustomDLManager): The custom download - mager which specifies a list of languages - - subs_list (list): A list of language codes extracted from the video's - metadata, one for each set of subtitles - - Return values: - - True if any language matches an available subtitle, False if none - of them match - - """ - - # 'short_code' is a value in formats.LANGUAGE_CODE_DICT, e.g. 'en', - # 'live_chat' - for short_code in custom_dl_obj.dl_if_subs_list: - - # media.VideoObj.subs_list contains the language code specified by - # the metadata file, e.g. 'en_US'. Ignore everything but the - # first two letters - for long_code in subs_list: - if long_code.lower().startswith(short_code): - return True - - return False - - -def move_metadata_to_subdir(app_obj, video_obj, ext): - - """Can be called by anything. - - Moves a description, JSON or annotations file from the same directory as - its video, into the subdirectory '.data'. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The file's parent video - - ext (str): The file extension, which will be one of '.description', - '.info.json' or '.annotations.xml' - - """ - - main_path = video_obj.get_actual_path_by_ext(app_obj, ext) - subdir = os.path.abspath( - os.path.join( - video_obj.parent_obj.get_actual_dir(app_obj), - app_obj.metadata_sub_dir, - ), - ) - - subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext( - app_obj, - ext, - ) - - if os.path.isfile(main_path) and not os.path.isfile(subdir_path): - - if not os.path.isdir(subdir): - app_obj.make_directory(subdir) - - app_obj.move_file_or_directory(main_path, subdir_path) - - -def move_thumbnail_to_subdir(app_obj, video_obj): - - """Can be called by anything. - - Moves a thumbnail file from the same directory as its video, into the - subdirectory '.thumbs'. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The file's parent video - - """ - - path_list = find_thumbnail_restricted(app_obj, video_obj) - if path_list: - - main_path = os.path.abspath( - os.path.join( - path_list[0], path_list[1], - ), - ) - - subdir = os.path.abspath( - os.path.join( - path_list[0], app_obj.thumbs_sub_dir, - ), - ) - - subdir_path = os.path.abspath( - os.path.join( - path_list[0], app_obj.thumbs_sub_dir, path_list[1], - ), - ) - - if os.path.isfile(main_path) \ - and not os.path.isfile(subdir_path): - - if not os.path.isdir(subdir): - app_obj.make_directory(subdir) - - app_obj.move_file_or_directory(main_path, subdir_path) - - -def open_file(app_obj, uri): - - """Can be called by anything. - - Opens a file using the system's default software (e.g. open a media file in - the default media player; open a weblink in the default browser). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - uri (str): The URI to open - - """ - - if sys.platform == "win32": - - try: - os.startfile(uri) - except: - app_obj.system_error( - 501, - 'Could not open \'' + str(uri) + '\'', - ) - - else: - - opener = "open" if sys.platform == "darwin" else "xdg-open" - # (Assume a return code of 0 on success) - if subprocess.call([opener, uri]): - app_obj.system_error( - 502, - 'Could not open \'' + str(uri) + '\'', - ) - - -def parse_options(text): - - """Called by options.OptionsParser.parse() or info.InfoManager.run(). - - Also called by ffmpeg_tartube.FFmpegOptionsManager.get_system_cmd() to - parse FFmpeg command-line options on the same basis. - - Parses a string containing one or more youtube-dl download options and - their arguments (or the FFmpeg equivalent). - - Anything inside double quotes constitutes a single argument (which can - therefore contain whitespace characters). - - If the string contains any newline characters, that characters terminates - the argument, closing newline character or not. - - Args: - - text (str): The string to parse, e.g. '--do-something "foo bar baz"' - - Return values: - - A separated list of youtube-dl download options (or the FFmpeg - equivalent) - - """ - - # Add options, one at a time, to a list - return_list = [] - - for line in text.splitlines(): - - # Set a flag for an item beginning with double quotes, and reset it for - # an item ending in double quotes - quote_flag = False - # Temporary list to hold such quoted arguments - quote_list = [] - - for item in line.split(): - - quote_flag = (quote_flag or item[0] == "\"") - - if quote_flag: - quote_list.append(item) - else: - return_list.append(item) - - if quote_flag and item[-1] == "\"": - - # Special case mode is over - return_list.append(' '.join(quote_list)) - - quote_flag = False - quote_list = [] - - return return_list - - -def rename_file(app_obj, old_path, new_path): - - """Can be called by anything. - - Renames (mmoves) a file. (Is not usually called for directories, but could - be.) - - Args: - - app_obj (mainapp.TartubeApp): The main application - - old_path (str): Full path to the file to be renamed - - new_path (str): Full path to the renamed file - - """ - - try: - - # (On MSWin, can't do os.rename if the destination file already exists) - if os.path.isfile(new_path): - app_obj.remove_file(new_path) - - # (os.rename sometimes fails on external hard drives; this is safer) - shutil.move(old_path, new_path) - - except: - - app_obj.system_error( - 503, - 'Could not rename \'' + str(old_path) + '\'', - ) - - -def shorten_string(string, num_chars): - - """Can be called by anything. - - If string is longer than num_chars, truncates it and adds an ellipsis. - - Args: - - string (string): The string to convert - - num_chars (int): The maximum length of the desired string - - Return values: - - The converted string - - """ - - if string and len(string) > num_chars: - num_chars -= 3 - string = string[:num_chars] + '...' - - return string - - -def shorten_string_two_lines(string, num_chars): - - """Can be called by anything. - - Modified version of shorten_string(). Reduces the string to two lines - (separated by a newline character). Each line has the specified maximum - length. If there is too much text to fit on the second line, truncates it - and adds an ellipsis. - - Args: - - string (string): The string to convert - - num_chars (int): The maximum length of the desired string - - Return values: - - The converted string - - """ - - if string and len(string) > num_chars: - - line_list = [] - current_line = '' - - for word in string.split(): - - if len(word) > num_chars: - - # To keep the code simple, the algorithm ends here, with this - # word on a separate line. This may produce a return string - # containing only line - # Exception: try to split URLs on the '/' character neareest - # to the end of the first 'num_chars' characters of 'word' - shortended_word = word[0:num_chars] - pos = shortended_word.rfind('/') - if pos > -1: - pos += 1 - line_list.append(word[0:pos]) - line_list.append(word[pos:]) - - else: - - if current_line != '': - line_list.append(current_line) - - line_list.append(word[0:num_chars] + '...') - - break - - else: - - if current_line != '': - mod_line = current_line + ' ' + word - else: - mod_line = word - - if len(mod_line) > num_chars: - - if current_line != '': - line_list.append(current_line) - - current_line = word - - else: - - current_line = mod_line - - line_list.append(current_line) - - # Dispense with everything but the first two lines - line_count = len(line_list) - if line_count > 2: - - return line_list[0] + '\n' + line_list[1] + '...' - - elif line_count == 2: - - return line_list[0] + '\n' + line_list[1] - - else: - - return line_list[0] - - else: - - # 'string' is empty or short, so there's no need to split it up at all - return string - - -def stream_output_is_ignorable(stderr): - - """Called by downloads.StreamDownloader.read_child_process() and - downloads.VideoDownloader.read_child_process() - - Special handling of messages received from STDERR during direct livestream - downloads (i.e. youtube-dl without .m3u). - - From the text received, we strip out anything we don't want. The calling - code will use the modified text (if any remains) in its STDOUT stream. - - Args: - - stderr (str): The text received from STDERR - - Return values: - - A modified stderr, or None if the whole text should be ignored - - """ - - match = re.search(r'^(frame.*speed\=\s*[\S]+x)', stderr) - if match: - return match.groups()[0] - - # (All but the first two lines occur only near the beginning of the - # output) - for regex in [ - r'^\[hls\s\@\s\w+\]\s', - r'^\[https\s\@\s\w+\]\s', - r'^Input\s\#\d+,\s', - r'^Output\s\#\d+,\s', - r'^Stream mapping\:', - r'^Press \[q\] to stop,', - r'^[\s]{2}', - ]: - if re.search(regex, stderr): - return None - - return stderr - - -def strip_double_quotes(input_list): - - """Can be called by anything. Mostly called by code that creates a child - process to run a system command. - - Strips leading and trailing double quotes from every string in a list. - - Args: - - string_list (list): A list of strings to modify - - Return values: - - The modified list - - """ - - return_list = [] - for item in input_list: - return_list.append(item.strip('"')) - - return return_list - - -def strip_whitespace(string): - - """Can be called by anything. - - Removes any leading/trailing whitespace from a string. - - Args: - - string (str): The string to convert - - Return values: - - The converted string - - """ - - if string is not None: - string = string.strip() - - return string - - -def strip_whitespace_multiline(string): - - """Can be called by anything. - - An extended version of utils.strip_whitespace(). - - Divides a string into lines, removes empty lines, removes any leading/ - trailing whitespace from each line, then combines the lines back into a - single string (with lines separated by newline characters). - - Args: - - string (str): The string to convert - - Return values: - - The converted string - - """ - - line_list = string.splitlines() - mod_list = [] - - for line in line_list: - line = line.strip() - - if re.search(r'\S', line): - mod_list.append(line) - - return "\n".join(mod_list) - - -def tidy_up_container_name(app_obj, string, max_length): - - """Called by mainapp.TartubeApp.on_menu_add_channel(), - .on_menu_add_playlist() and .on_menu_add_folder(). - - Before creating a channel, playlist or folder, tidies up the name. - - Removes any leading/trailing whitespace. Reduces multiple whitespace - characters to a single space character. Applies a maximum length. - - Also replaces any forward/backward slashes with hyphens (if the user - specifies a name like 'Foo / Bar', that would create a directory on the - filesystem called .../Foo/Bar, which is definitely not what we want). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - string (str): The string to convert - - max_length (int): The maximum length of the converted string (should be - mainapp.TartubeApp.container_name_max_len) - - Return values: - - The converted string, or an empty string for an irretrievable name - - """ - - if string: - - string = string.strip() - string = re.sub(r'\s+', ' ', string) - - # Get rid of ASCII control characters (illegal on Windows, a pain in - # the behind on POSIX) - string = re.sub(r'[\x00-\x1F]', '', string) - - if os.name != 'nt': - # Forbidden characters on POSIX: / - # Forbidden on MacOS, depending on context: : - string = re.sub(r'[\/\:]', '-', string) - - else: - # Illegal filenames - if string in app_obj.illegal_name_mswin_list: - return '' - - for illegal in app_obj.illegal_name_mswin_list: - if re.search('^' + illegal + r'\.', string): - return '' - - # Forbidden characters on MS Windows: < > : " / \ | ? * - string = re.sub(r'[\<\>\:\/\\\|\?\*]', '-', string) - string = re.sub(r'[\"]', '\'', string) - - # Cannot end with a dot - string = re.sub(r'\.+$', '', string) - - return string[0:max_length] - - else: - - # Empty string - return string - - -def tidy_up_long_descrip(string, max_length=80): - - """Can be called by anything. - - A modified version of utils.tidy_up_long_string. In this case, the - specified string can contain any number of newline characters. We begin - by splitting that string into a list of lines. - - Then we split any line which is longer than the specified maximum length, - which gives us a (possibly longer) list of lines. - - Finally we recombine those lines into a single string, with lines joined by - newline characters. - - Args: - - string (str): The string to convert - - max_length (int): The maximum length of lines, before they are - recombined into a single string - - Return values: - - The converted string - - """ - - if string: - - line_list = [] - - for line in string.splitlines(): - - if line == '': - # Preserve empty lines - line_list.append('') - - else: - - w = classes.ModTextWrapper( - width=max_length, - break_long_words=False, - # Split up URLs on the forward slash character, as well as - # on hyphen(s) - break_on_hyphens=True, - ) - - new_list = w.wrap(line) - - for mini_line in new_list: - line_list.append(mini_line) - - return '\n'.join(line_list) - - else: - - # Empty string - return string - - -def tidy_up_long_string(string, max_length=80, reduce_flag=True, -split_words_flag=False): - - """Can be called by anything. - - The specified string can contain any number of newline characters. - - Replaces newline characters with a single space character. - - Optionally reduces multiple whitespace characters and removes initial/ - final whitespace character(s). - - Then splits the string into a list of lines, each with the specified - maximum length. - - Finally recombines those lines into a single string, with lines joined by - newline characters. - - Args: - - string (str): The string to convert - - max_length (int): The maximum length of lines, before they are - recombined into a single string - - reduce_flag (bool): If True, initial and final whitespace is removed, - and multiple successive whitespace characters are reduced to a - single space character - - split_words_flag(bool): If True, the function will break words - (including hyphenated words) into smaller pieces, if necessary - - Return values: - - The converted string - - """ - - if string: - - string = re.sub(r'\r\n', ' ', string) - - if reduce_flag: - string = re.sub(r'^\s+', '', string) - string = re.sub(r'\s+$', '', string) - string = re.sub(r'\s+', ' ', string) - - line_list = [] - for line in string.splitlines(): - - if line == '': - # Preserve empty lines - line_list.append('') - - else: - - w = classes.ModTextWrapper( - width=max_length, - break_long_words=split_words_flag, - # Split up URLs on the forward slash character, as well as - # on hyphen(s) - break_on_hyphens=split_words_flag, - ) - - new_list = w.wrap(line) - - for mini_line in new_list: - line_list.append(mini_line) - - return '\n'.join(line_list) - - else: - - # Empty string - return string - - -def timestamp_add_second(app_obj, stamp=None): - - """Can be called by anything. - - Adds a second to a timestamp, converting a value like '1:59' to '2:00', or - a value like '1:59:59' to '2:00:00'. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - stamp (str): A timestamp in the form 'mm:ss' or 'h:mm:ss'. Leading - zeroes are optional for all components, and the 'h' component can - contain any number of digits - - Return values: - - The converted string. If the supplied timestamp is invalid or not - specified, its value is returned unmodified - - """ - - if stamp is not None: - - regex = '^' + app_obj.timestamp_regex + '$' - match = re.search(regex, stamp) - if match: - - hours = match.groups()[1] - if hours is not None: - hours = int(hours) - - minutes = int(match.groups()[2]) - seconds = int(match.groups()[3]) - - seconds += 1 - if seconds >= 60: - seconds = 0 - minutes += 1 - - if minutes >= 60: - minutes = 0 - if hours is not None: - hours += 1 - else: - hours = 1 - - stamp = '{:02d}'.format(minutes) + ':{:02d}'.format(seconds) - if hours: - stamp = str(hours) + ':' + stamp - - return stamp - - -def timestamp_compare(app_obj, start_stamp, stop_stamp): - - """Can be called by anything, after the user has manually entered a start - and stop timestamp. - - Checks that either of the following is True: - - 1. 'stop_stamp' is None - 2. 'start_stamp' occurs earlier than 'stop_stamp' - - Args: - - start_stamp (str): A timestamp in the form 'mm:ss' or 'h:mm:ss'. - Leading zeroes are optional for all components, and the 'h' - component can contain any number of digits - - stop_stamp (str or None): If specified, another timestamp in the same - format - - Return values: - - False if 'stop_stamp' is earlier than 'start_stamp', if 'start_stamp' - is invalid, or 'stop_stamp' is specified and invalid; True - otherwise - - """ - - if stop_stamp is None: - return True - - regex = '^' + app_obj.timestamp_regex + '$' - match = re.search(regex, start_stamp) - if not match: - return False - - else: - if match.groups()[1] is not None: - start_hours = int(match.groups()[1]) - else: - start_hours = 0 - - start_seconds = int(match.groups()[3]) \ - + (int(match.groups()[2]) * 60) \ - + (start_hours * 3600) - - match = re.search(regex, stop_stamp) - if not match: - return False - - else: - if match.groups()[1] is not None: - stop_hours = int(match.groups()[1]) - else: - stop_hours = 0 - - stop_seconds = int(match.groups()[3]) \ - + (int(match.groups()[2]) * 60) \ - + (stop_hours * 3600) - - if stop_seconds < start_seconds: - return False - else: - return True - - -def timestamp_convert_to_seconds(app_obj, stamp): - - """Can be called by anything. - - Converts a timestamp to a value in seconds. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - stamp (str): A timestamp in the form 'mm:ss' or 'h:mm:ss'. Leading - zeroes are optional for all components, and the 'h' component can - contain any number of digits - - Return values: - - The converted value, or the original value if 'stamp' is not a valid - timestamp - - """ - - regex = '^' + app_obj.timestamp_regex + '$' - match = re.search(regex, stamp) - if match: - hours = match.groups()[1] - if hours is not None: - hours = int(hours) - - minutes = int(match.groups()[2]) - seconds = int(match.groups()[3]) - - if hours: - return seconds + minutes*60 + hours*60*60 - else: - return seconds + minutes*60 - - else: - return stamp - - -def timestamp_format(app_obj, stamp): - - """Can be called by anything. - - The user can specify timestamps without leading zeroes, for example '1:59' - for '01:59', or even '1:5' or '01:05'. - - Add leading zeroes for the minutes and seconds components. If the hours - component is specified, removes any leading zeroes. If it is not specified, - adds it. This ensures that any list of timestamps is sorted correctly. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - stamp (str): A timestamp in the form 'mm:ss' or 'h:mm:ss'. Leading - zeroes are optional for all components, and the 'h' component can - contain any number of digits - - Return values: - - The converted string. If the supplied timestamp is invalid, it is - returned unmodified - - """ - - regex = '^' + app_obj.timestamp_regex + '$' - match = re.search(regex, stamp) - if match: - - hours = match.groups()[1] - if hours is not None: - hours = int(hours) - else: - hours = 0 - - minutes = int(match.groups()[2]) - seconds = int(match.groups()[3]) - - stamp = '{:02d}'.format(minutes) + ':{:02d}'.format(seconds) - stamp = str(int(hours)) + ':' + stamp - - return stamp - - -def timestamp_quick_format(app_obj, hours, minutes, seconds, - hour_digit_count=None): - - """Can be called by anything. - - A shorter version of utils.timestamp_format(), used when the hours, minutes - and seconds components have already been extracted. (It would be wasteful - to use the same regex to extract them again). - - The original timestamp was in the form 'mm:ss' or 'h:mm:ss'. Leading zeroes - are optional for all components, and the 'h' component can contain any - number of digits - - Add leading zeroes for the minutes and seconds components. - - Removes leading zeroes for the hours component, if specified. However, if - 'hour_digit_count' is specified, adds leading zeroes to make the correct - number of digits. - - If the hours component is not specified, adds one. - - As a result of calling this function, any list of timestamps processed with - this function can be sorted in the correct order. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - hours (str or None): Optional string with the "h" component - - minutes, seconds (str): Non-optional strings with the 'mm' and 'ss' - components - - hour_digit_count (int): The number of digits to use in the hours - component, if specified - - """ - - stamp = '{:02d}'.format(int(minutes)) + ':{:02d}'.format(int(seconds)) - - if hours is None: - hours = 0 - - # Remove leading zeroes - hours = str(int(hours)) - - if hour_digit_count is None or len(hours) >= hour_digit_count: - - stamp = hours + ':' + stamp - - elif len(hours) < hour_digit_count: - - # Add leading zeroes - stamp = hours.rjust(hour_digit_count, '0') + ':' + stamp - - return stamp - - -def to_string(data): - - """Can be called by anything. - - Convert any data type to a string. - - Args: - - data (-): The data type - - Return values: - - The converted string - - """ - - return '%s' % data - - -def upper_case_first(string): - - """Can be called by anything. - - Args: - - string (str): The string to capitalise - - Return values: - - The converted string - - """ - - return string[0].upper() + string[1:] diff --git a/build/lib/tartube/wizwin.py b/build/lib/tartube/wizwin.py deleted file mode 100644 index 45a09088..00000000 --- a/build/lib/tartube/wizwin.py +++ /dev/null @@ -1,4123 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Wizard window classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, Gdk, GdkPixbuf - - -# Import other modules -import os -import re -import zipfile - - -# Import our modules -import __main__ -import formats -import mainapp -import media -import utils -# Use same gettext translations -from mainapp import _ - - -# Classes - - -class GenericWizWin(Gtk.Window): - - """Generic Python class for windows in which the user can modify various - system settings. - - Unlike classes inheriting from GenericEditWin, widgets are arranged in - pages, with only page visible at a time. The user can cycle through pages - (when allowed) by clicking the 'Next' or 'Previous' buttons. - - Modifications are usually applied immediately, but the code provides an - .apply_changes() function for anything that should be applied, when the - user has cycled through all the pages. - """ - - - # Standard class methods - - -# def __init__(): # Provided by child object - - - # Public class methods - - - def is_duplicate(self, app_obj): - - """Called by self.__init__. - - Don't open this wizard window, if another wizard window (if any class) - is already open. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - Return values: - - True if a duplicate is found, False if not - - """ - - if app_obj.main_win_obj.wiz_win_obj: - - # Duplicate found - app_obj.main_win_obj.wiz_win_obj.present() - return True - - else: - - # Not a duplicate - return False - - - def setup(self): - - """Called by self.__init__(). - - Sets up the wizard window when it opens. - """ - - # Set the default window size - self.set_default_size( - self.app_obj.config_win_width, - self.app_obj.config_win_height, - ) - - # Set the window's Gtk icon list - self.set_icon_list(self.app_obj.main_win_obj.win_pixbuf_list) - - # Set up main widgets - self.setup_grid() - self.setup_button_strip() - - # Set up the first page (a widget layout on self.inner_grid) - self.setup_page() - - # Procedure complete - self.show_all() - - # Inform the main window of this window's birth (so that Tartube - # doesn't allow an operation to start until all configuration windows - # have closed) - self.app_obj.main_win_obj.add_child_window(self) - - # Add a callback so we can inform the main window of this window's - # destruction - self.connect('destroy', self.close) - - - def setup_grid(self): - - """Called by self.setup(). - - Sets up two Gtk.Grids. The first one contains an inner grid, and a - button strip. - """ - - box = Gtk.Box() - self.add(box) - box.set_border_width(self.spacing_size) - - self.grid = Gtk.Grid() - box.add(self.grid) - self.grid.set_row_spacing(self.spacing_size) - self.grid.set_column_spacing(self.spacing_size) - - frame = Gtk.Frame() - self.grid.attach(frame, 0, 0, 1, 1) - frame.set_hexpand(True) - frame.set_vexpand(True) - - self.vbox = Gtk.VBox() - frame.add(self.vbox) - self.vbox.set_border_width(self.spacing_size * 2) - - self.inner_grid = Gtk.Grid() - self.vbox.pack_start(self.inner_grid, True, False, 0) - self.inner_grid.set_row_spacing(self.spacing_size) - self.inner_grid.set_column_spacing(self.spacing_size) - - - def setup_button_strip(self): - - """Called by self.setup(). - - Creates a strip of buttons at the bottom of the window: a 'cancel' - button on the left, and 'next'/'previous' buttons on the right. - - The window is closed by using the 'cancel' button, or by clicking the - 'next' button on the last page. - """ - - hbox = Gtk.HBox() - self.grid.attach(hbox, 0, 1, 1, 1) - - # 'Cancel' button - self.cancel_button = Gtk.Button(_('Cancel')) - hbox.pack_start(self.cancel_button, False, False, 0) - self.cancel_button.get_child().set_width_chars(10) - self.cancel_button.set_tooltip_text( - _('Close this window without completing it'), - ); - self.cancel_button.connect('clicked', self.on_button_cancel_clicked) - - # 'Next' button - self.next_button = Gtk.Button(_('Next')) - hbox.pack_end(self.next_button, False, False, 0) - self.next_button.get_child().set_width_chars(10) - self.next_button.set_tooltip_text(_('Go to the next page')); - self.next_button.connect('clicked', self.on_button_next_clicked) - - # 'Previous' button - self.prev_button = Gtk.Button(_('Previous')) - hbox.pack_end(self.prev_button, False, False, self.spacing_size) - self.prev_button.get_child().set_width_chars(10) - self.prev_button.set_tooltip_text(_('Go to the previous page')); - self.prev_button.connect('clicked', self.on_button_prev_clicked) - - - def setup_page(self): - - """Called initially by self.setup(), then by .on_button_next_clicked() - or .on_button_prev_clicked(). - - Sets up the page specified by self.current_page. - """ - - index = self.current_page - page_func = self.page_list[self.current_page] - if page_func is None: - - # Emergency fallback - index = 0 - page_func = self.page_list[0] - - if len(self.page_list) <= 1: - self.next_button.set_sensitive(False) - self.prev_button.set_sensitive(False) - elif index == 0: - self.next_button.set_sensitive(True) - self.prev_button.set_sensitive(False) - else: - self.next_button.set_sensitive(True) - self.prev_button.set_sensitive(True) - - if index >= len(self.page_list) - 1: - self.next_button.set_label(_('OK')) - else: - self.next_button.set_label(_('Next')) - - self.next_button.get_child().set_width_chars(10) - - # Replace the inner grid... - self.vbox.remove(self.inner_grid) - - self.inner_grid = Gtk.Grid() - self.vbox.pack_start(self.inner_grid, True, False, 0) - self.inner_grid.set_row_spacing(self.spacing_size) - self.inner_grid.set_column_spacing(self.spacing_size) - - # ...and then refill it, with the widget layout for the new page - method = getattr(self, page_func) - method() - - self.show_all() - - - def convert_next_button(self): - - """Can be called by anything. - - Converts the 'Next' to an 'OK' button, and sensitises it. - - Should usually be called from the last page, when the code is ready to - let the window finish the wizard. - """ - - self.next_button.set_label(_('Finish')) - self.next_button.get_child().set_width_chars(10) - self.next_button.set_sensitive(True) - - - def apply_changes(self): - - """Called by self.on_button_next_clicked(). - - The default function is empty. Any changes that need to be applied, - when the wizard window closes, can be applied in a function with this - name. - """ - - pass - - - def cancel_changes(self): - - """Called by self.on_button_cancel_clicked(). - - The default function is empty. Any changes that need to be applied, - when the wizard window is closed by clicking the 'Cancel' button, can - be applied in a function with this name. - """ - - pass - - - def close(self, widget): - - """Called from callback in self.setup(). - - Inform the main application that this window is closing. - - Args: - - widget (GenericWizWin): This window - - """ - - self.app_obj.main_win_obj.del_child_window(self) - - - # (Add widgets) - - - def add_image(self, image_path, x, y, wid, hei): - - """Called by various functions in the child wizard window. - - Adds a Gtk.Image to self.inner_grid, with more than the usual padding. - - Args: - - image_path (str): Full path to the image file to load - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The Gtk.Box containing the image - - """ - - box = Gtk.Box() - self.inner_grid.attach(box, x, y, wid, hei) - box.set_border_width(self.spacing_size * 2) - - image = Gtk.Image() - box.add(image) - image.set_from_pixbuf( - self.app_obj.file_manager_obj.load_to_pixbuf(image_path), - ) - image.set_hexpand(True) - - return box - - - def add_framed_image(self, image_path, x, y, wid, hei): - - """Called by various functions in the child wizard window. - - Adds a Gtk.Image to self.inner_grid, inside a Gtk.Frame, with more than - the usual padding. - - Args: - - image_path (str): Full path to the image file to load - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The Gtk.Box containing the image - - """ - - box = Gtk.Box() - self.inner_grid.attach(box, x, y, wid, hei) - box.set_border_width(self.spacing_size * 2) - - frame = Gtk.Frame() - box.set_center_widget(frame) - - image = Gtk.Image() - frame.add(image) - image.set_from_pixbuf( - self.app_obj.file_manager_obj.load_to_pixbuf(image_path), - ) - - return box - - - def add_label(self, text, x, y, wid, hei): - - """Called by various functions in the child wizard window. - - Adds a Gtk.Label to self.inner_grid. - - Args: - - text (str): Pango markup displayed in the label - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The label widget created - - """ - - label = Gtk.Label() - self.inner_grid.attach(label, x, y, wid, hei) - label.set_markup(text) - label.set_hexpand(True) - label.set_alignment(0.5, 0.5) - - return label - - - def add_empty_label(self, x, y, wid, hei): - - """Called by various functions in the child wizard window. - - Adds an empty Gtk.Label (for spacing) to self.inner_grid. - - Args: - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The label widget created - - """ - - label = Gtk.Label() - self.inner_grid.attach(label, x, y, wid, hei) - # (Using a space, rather than an empty string, better preserves the - # intended layout) - label.set_text(' ') - label.set_hexpand(True) - label.set_alignment(0.5, 0.5) - - return label - - - def add_checkbutton(self, text, x, y, wid, hei): - - """Called by various functions in the child wizard window. - - Adds a Gtk.CheckButton to self.inner_grid. - - Args: - - text (string or None): The text to display in the checkbutton's - label. No label is used if 'text' is an empty string or None - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The checkbutton widget created - - """ - - checkbutton = Gtk.CheckButton() - self.inner_grid.attach(checkbutton, x, y, wid, hei) - checkbutton.set_hexpand(True) - if text is not None and text != '': - checkbutton.set_label(text) - - return checkbutton - - - def add_radiobutton(self, prev_button, text, x, y, wid, hei): - - """Called by various functions in the child wizard window. - - Adds a Gtk.RadioButton to self.inner_grid. - - Args: - - prev_button (Gtk.RadioButton or None): When this is the first - radio button in the group, None. Otherwise, the previous - radio button in the group. Use of this argument links the radio - buttons together, ensuring that only one of them can be active - at any time - - text (string or None): The text to display in the radiobutton's - label. No label is used if 'text' is an empty string or None - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The radiobutton widget created - - """ - - radiobutton = Gtk.RadioButton.new_from_widget(prev_button) - self.inner_grid.attach(radiobutton, x, y, wid, hei) - radiobutton.set_hexpand(True) - if text is not None and text != '': - radiobutton.set_label(text) - - return radiobutton - - - def add_textview(self, x, y, wid, hei): - - """Called by various functions in the child wizard window. - - Adds a Gtk.TextView to self.inner_grid. - - Args: - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Return values: - - The scroller, textview and textbuffer widgets created - - """ - - frame = Gtk.Frame() - self.inner_grid.attach(frame, x, y, wid, hei) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_size_request(-1, 125) - - textview = Gtk.TextView() - scrolled.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_editable(False) - textview.set_cursor_visible(False) - - return scrolled, textview, textview.get_buffer() - - - # Callback class methods - - - def on_button_cancel_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Closes the wizard window without applying any changes. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.cancel_changes() - self.destroy() - - - def on_button_next_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Goes to the the next page, or (if already on the last page) applies - any changes waiting to be applied, then closes the window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if self.current_page >= (len(self.page_list) - 1): - self.apply_changes() - self.destroy() - - else: - - self.current_page += 1 - self.setup_page() - - - def on_button_prev_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Goes to the previous page. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if self.current_page > 0: - self.current_page -= 1 - self.setup_page() - - -class SetupWizWin(GenericWizWin): - - """Python class for a 'wizard window' displayed when Tartube starts and - no config file can be loaded (meaning that this is a new installation). - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Setup wizard window starts here.' \ - + ' This window is visible on startup, if there is no config' \ - + ' file (settings.json)' - ) - - Gtk.Window.__init__(self, title=_('Tartube setup')) - - if self.is_duplicate(app_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.vbox = None # Gtk.VBox - self.inner_grid = None # Gtk.Grid - self.cancel_button = None # Gtk.Button - self.next_button = None # Gtk.Button - self.prev_button = None # Gtk.Button - # (Textbuffers used to display output of an update operation, and the - # buttons used to initiate that operation) - self.downloader_button = None # Gtk.Button - self.update_button = None # Gtk.Button - self.update_combo = None # Gtk.ComboBox - self.update_liststore = None # Gtk.ListStore - self.downloader_scrolled = None # Gtk.ScrolledWindow - self.downloader_textview = None # Gtk.TextView - self.downloader_textbuffer = None # Gtk.TextBuffer - self.ffmpeg_button = None # Gtk.Button - self.ffmpeg_scrolled = None # Gtk.ScrolledWindow - self.ffmpeg_textview = None # Gtk.TextView - self.ffmpeg_textbuffer = None # Gtk.TextBuffer - self.tutorial_button = None # Gtk.Button - self.auto_open_button = None # Gtk.Button - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between preference window widgets - self.spacing_size = self.app_obj.default_spacing_size - - # List of 'pages' (widget layouts on self.inner_grid). Each item in the - # list is the function to call - self.page_list = [] # Set below - # The number of the current page (the first is 0), matching an index in - # self.page_list - self.current_page = 0 - - # Make the code a little simpler, by checking for MS Windows just once - # (set below) - self.mswin_flag = False - - # User choices; they are applied when the window is closed (and - # self.apply_changes() is called) - # Path to Tartube's data directory - self.data_dir = None - # The new value of mainapp.TartubeApp.db_backup_mode, if any - self.db_backup_mode = None - # The name of the youtube-dl fork to use, by default ('None' when - # youtube-dl itself should be used) - self.ytdl_fork = 'yt-dlp' - # Flag set to True if yt-dlp (only), when installed via pip, should be - # installed without dependencies - if os.name == 'nt': - self.ytdl_fork_no_dependency_flag = True - else: - self.ytdl_fork_no_dependency_flag = False - # The new value of mainapp.TartubeApp.ytdl_update_current, if any - self.ytdl_update_current = None - # The new value of - # mainapp.TartubeApp.show_classic_tab_on_startup_flag, if any - self.show_classic_tab_on_startup_flag = None - - # Flag set to True, once the 'More options' button has been clicked, - # so that it is never visible again - self.more_options_flag = False - # Flag set to True after the user has tried install FFmpeg at least - # once (even if the attempt failed) - self.try_install_ffmpeg_flag = False - - # Flag set to True if the tutorial wizard window should be opened, - # after this one closes - self.open_tutorial_flag = False - - # Standard length of text in the wizard window - self.text_len = 60 - - # Code - # ---- - - # Check for MS Windows - if os.name == 'nt': - - self.mswin_flag = True - - # Set the page list, which depends on operating system and packaging - self.page_list = [ - 'setup_start_page', - 'setup_db_page', - 'setup_backup_page', - 'setup_set_downloader_page', - ] - - if self.mswin_flag: - self.page_list.append('setup_fetch_downloader_page') - self.page_list.append('setup_fetch_ffmpeg_page') - self.page_list.append('setup_classic_mode_page') - self.page_list.append('setup_finish_page_mswin') - - elif __main__.__pkg_strict_install_flag__: - self.page_list.append('setup_classic_mode_page') - self.page_list.append('setup_finish_page_strict') - - else: - self.page_list.append('setup_fetch_downloader_page') - self.page_list.append('setup_classic_mode_page') - self.page_list.append('setup_finish_page_default') - - # Set up the wizard window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericWizWin - - -# def setup(): # Inherited from GenericWizWin - - -# def setup_grid(): # Inherited from GenericWizWin - - -# def setup_button_strip(): # Inherited from GenericWizWin - - -# def setup_page(): # Inherited from GenericWizWin - - -# def convert_next_button(): # Inherited from GenericWizWin - - - def apply_changes(self): - - """Called by self.on_button_next_clicked(). - - Apply the settings the user has specified. - """ - - if self.data_dir is not None: - self.app_obj.set_data_dir(self.data_dir) - self.app_obj.set_data_dir_alt_list( [ self.data_dir ] ) - self.app_obj.update_data_dirs() - - # (None values are acceptable) - self.app_obj.set_ytdl_fork(self.ytdl_fork) - - self.app_obj.set_ytdl_fork_no_dependency_flag( - self.ytdl_fork_no_dependency_flag, - ) - - # (A None value, only if they haven't been changed) - if self.db_backup_mode is not None: - self.app_obj.set_db_backup_mode(self.db_backup_mode) - - if self.ytdl_update_current is not None: - self.app_obj.set_ytdl_update_current(self.ytdl_update_current) - - if self.show_classic_tab_on_startup_flag is not None: - self.app_obj.set_show_classic_tab_on_startup_flag( - self.show_classic_tab_on_startup_flag, - ) - - # Continue with general initialisation - self.app_obj.open_wiz_win_continue(self) - - - def cancel_changes(self): - - """Called by self.on_button_cancel_clicked(). - - Tartube needs to be shut down (unless an update operation is running, - in which case we stop it.) - """ - - if self.app_obj.update_manager_obj: - - self.app_obj.update_manager_obj.stop_update_operation() - - else: - - # (Prevent the shutdown code from saving the config file and/or - # database) - self.app_obj.disable_load_save() - - # (Delete the config file, so that this window will appear again, - # the next time Tartube runs) - config_path = self.app_obj.get_config_path() - if os.path.isfile(config_path): - os.remove(config_path) - - # Shut down Tartube - self.app_obj.stop() - - -# def close(): # Inherited from GenericWizWin - - - # (Setup pages) - - - def setup_start_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_image( - self.app_obj.main_win_obj.icon_dict['system_icon'], - 0, 0, 1, 1, - ) - - self.add_label( - '' \ - + _('Welcome to Tartube!') + '', - 0, 1, 1, 1, - ) - - if __main__.__pkg_no_download_flag__: - - edition = _('Video downloads are disabled in this package') - - elif __main__.__pkg_strict_install_flag__: - - edition = _( - 'For this package, youtube-dl and FFmpeg must be' \ - + ' installed separately', - ) - - elif __main__.__pkg_install_flag__: - - edition = _('Package edition') - - elif os.name == 'nt': - - edition = _('MS Windows edition') - - else: - - edition = _('Standard edition') - - self.add_label( - '' \ - + utils.tidy_up_long_string(edition, self.text_len) + '', - 0, 2, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 3, 1, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('Please take a few moments to set up the application.'), - self.text_len, - ) + '', - 0, 4, 1, 1, - ) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('Click the Next button to get started.'), - self.text_len, - ) + '', - 0, 5, 1, 1, - ) - - - def setup_db_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - grid_width = 3 - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('Tartube stores all of its downloads in one place.'), - self.text_len, - ) + '', - 0, 0, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, grid_width, 1) - - if not self.mswin_flag: - - msg = utils.tidy_up_long_string( - _( - 'If you don\'t want to use the default location, then' \ - + ' click Choose to select a different one.', - ), - self.text_len, - ) - - msg2 = utils.tidy_up_long_string( - _( - 'If you have used Tartube before, you can select an' \ - + ' existing directory, instead of creating a new one.', - ), - self.text_len, - ) - - else: - - msg = _('Click Choose to create a new folder.') - msg2 = utils.tidy_up_long_string( - _( - 'If you have used Tartube before, you can select an' \ - + ' existing folder, instead of creating a new one.', - ), - self.text_len, - ) - - self.add_label( - '' + msg + '', - 0, 2, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 3, grid_width, 1) - - self.add_label( - '' + msg2 + '', - 0, 4, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 5, grid_width, 1) - - button = Gtk.Button(_('Choose')) - self.inner_grid.attach(button, 1, 6, 1, 1) - # (Signal connect appears below) - - if not self.mswin_flag: - button2 = Gtk.Button(_('Use default location')) - self.inner_grid.attach(button2, 1, 7, 1, 1) - # (Signal connect appears below) - - # (Empty label for spacing) - self.add_empty_label(0, 8, grid_width, 1) - - # The specified path appears here, after it has been selected - if self.data_dir is None: - - label = self.add_label( - '', - 0, 9, grid_width, 1, - ) - - else: - - label = self.add_label( - '' \ - + self.data_dir + '', - 0, 9, grid_width, 1, - ) - - # (Signal connects from above) - button.connect( - 'clicked', - self.on_button_choose_folder_clicked, - label, - ) - - if not self.mswin_flag: - - button2.connect( - 'clicked', - self.on_button_default_folder_clicked, - label, - ) - - # Disable the Next button until a folder has been created/selected - if self.data_dir is None: - self.next_button.set_sensitive(False) - - - def setup_backup_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - grid_width = 3 - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'When saving the database, Tartube makes a backup' \ - + ' copy of its database file (in case something goes' \ - + ' wrong).', - ), - self.text_len, - ) + '', - 0, 0, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, grid_width, 1) - - radiobutton = self.add_radiobutton( - None, - _('Delete the backup as soon as the database has been saved'), - 1, 2, 1, 1, - ) - radiobutton.set_hexpand(False) - # (Signal connect appears below) - - radiobutton2 = self.add_radiobutton( - radiobutton, - _('Keep the backup file, replacing any previous backup file'), - 1, 3, 1, 1, - ) - # (Signal connect appears below) - radiobutton2.set_hexpand(False) - - radiobutton3 = self.add_radiobutton( - radiobutton2, - _('Make a new backup file once per day'), - 1, 4, 1, 1, - ) - radiobutton3.set_hexpand(False) - # (Signal connect appears below) - - radiobutton4 = self.add_radiobutton( - radiobutton3, - _('Make a new backup file every time the database is saved'), - 1, 5, 1, 1, - ) - radiobutton4.set_active(True) - radiobutton4.set_hexpand(False) - # (Signal connect appears below) - - # (Signal connects from above) - radiobutton.connect( - 'toggled', - self.on_button_backup_mode_toggled, - 'default', - ) - radiobutton2.connect( - 'toggled', - self.on_button_backup_mode_toggled, - 'single', - ) - radiobutton3.connect( - 'toggled', - self.on_button_backup_mode_toggled, - 'daily', - ) - radiobutton4.connect( - 'toggled', - self.on_button_backup_mode_toggled, - 'always', - ) - - # (Empty labels either side of the radio buttons, so they appear in - # the middle) - self.add_empty_label(0, 2, 1, 1) - self.add_empty_label(2, 2, 1, 1) - - - def setup_set_downloader_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - grid_width = 3 - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('Choose which downloader to use.'), - self.text_len, - ) + '', - 0, 0, grid_width, 1, - ) - - # yt-dlp - radiobutton, checkbutton = self.setup_set_downloader_page_add_button( - 1, # Row number - 'yt-dlp: ' \ - + self.app_obj.ytdl_fork_descrip_dict['yt-dlp'] \ - + '', - _('Use yt-dlp'), - None, # No radiobutton group yet - 'button' # Show a checkbutton - ) - - # youtube-dl - radiobutton2 = self.setup_set_downloader_page_add_button( - 2, # Row number - 'youtube-dl: ' \ - + self.app_obj.ytdl_fork_descrip_dict['youtube-dl'] \ - + '', - _('Use youtube-dl'), - radiobutton, - ) - - # Any other fork - radiobutton3, entry = self.setup_set_downloader_page_add_button( - 3, # Row number - '' + _('Other forks') + ': ' \ - + self.app_obj.ytdl_fork_descrip_dict['custom'] \ - + '', - _('Use this fork:'), - radiobutton2, - 'entry', # Show an entry - ) - - # # Set widgets' initial states - if self.ytdl_fork is None or self.ytdl_fork == 'youtube-dl': - radiobutton2.set_active(True) - checkbutton.set_sensitive(False) - entry.set_sensitive(False) - elif self.ytdl_fork == 'yt-dlp': - radiobutton.set_active(True) - checkbutton.set_sensitive(True) - entry.set_sensitive(False) - else: - radiobutton3.set_active(True) - if self.ytdl_fork is not None: - entry.set_text(self.ytdl_fork) - else: - entry.set_text('') - checkbutton.set_sensitive(False) - entry.set_sensitive(True) - - # (Signal connects from the call to - # self.setup_set_downloader_page_add_button() ) - radiobutton.connect( - 'toggled', - self.on_button_ytdl_fork_toggled, - checkbutton, - entry, - 'yt-dlp', - ) - checkbutton.connect('toggled', self.on_button_ytdlp_install_toggled) - radiobutton2.connect( - 'toggled', - self.on_button_ytdl_fork_toggled, - checkbutton, - entry, - 'youtube-dl', - ) - radiobutton3.connect( - 'toggled', - self.on_button_ytdl_fork_toggled, - checkbutton, - entry, - ) - entry.connect( - 'changed', - self.on_entry_ytdl_fork_changed, - radiobutton3, - ) - - - def setup_set_downloader_page_add_button(self, row, label_text, radio_text, - radiobutton=None, extra_mode=None): - - """Called by self.setup_set_downloader_page(). - - Adds widgets for a single downloader option. - - Args: - - row (int): Row number in self.inner_grid - - label_text (str): Text to use in a Gtk.Label - - radio_text (str): Text to use in a Gtk.RadioButton - - radiobutton (Gtk.RadioButton): The previous radiobutton in the same - group - - extra_mode (str or None): 'entry' to show an extra Gtk.Entry, - 'button' to show an extra Gtk.CheckButton, or None for no - extra widget - - Return values: - - If 'extra_mode' is None, returns the radiobutton. If 'entry' or - 'button', returns the radiobutton and the extra widget as a - list - - """ - - if not extra_mode: - grid_width = 1 - else: - grid_width = 2 - - # (Use an event box so the downloader can be selected by clicking - # anywhere in the frame) - event_box = Gtk.EventBox() - self.inner_grid.attach(event_box, 1, row, 1, 1) - # (Signal connect appears below) - - frame = Gtk.Frame() - event_box.add(frame) - frame.set_border_width(self.spacing_size) - frame.set_hexpand(False) - - grid = Gtk.Grid() - frame.add(grid) - grid.set_border_width(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - label = Gtk.Label() - grid.attach(label, 0, 0, grid_width, 1) - label.set_markup(utils.tidy_up_long_string(label_text)) - label.set_hexpand(False) - label.set_alignment(0, 0.5) - - radiobutton2 = Gtk.RadioButton.new_from_widget(radiobutton) - grid.attach(radiobutton2, 0, 1, 1, 1) - radiobutton2.set_hexpand(False) - radiobutton2.set_label(' ' + radio_text) - # (Signal connect appears in the calling function) - - # (Signal connect from above) - event_box.connect( - 'button-press-event', - self.on_frame_downloader_clicked, - radiobutton2, - ) - - if extra_mode == 'button': - - # For yt-dlp, add a checkbutton, and return it with the radiobutton - checkbutton = Gtk.CheckButton.new_with_label( - _( - _('Install without dependencies') + '\n' \ - + _('(recommended on MS Windows)'), - ), - ) - grid.attach(checkbutton, 1, 1, 1, 1) - if self.ytdl_fork_no_dependency_flag: - checkbutton.set_active(True) - # (Signal connect appears in the calling function) - - return radiobutton2, checkbutton - - elif extra_mode == 'entry': - - # For other forks, add an entry, and return it with the radiobutton - entry = Gtk.Entry() - grid.attach(entry, 1, 1, 1, 1) - entry.set_hexpand(True) - entry.set_editable(True) - # (Signal connect appears in the calling function) - radiobutton2.set_hexpand(False) - - return radiobutton2, entry - - else: - return radiobutton2 - - - def setup_fetch_downloader_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - grid_width = 3 - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('Click the button to install or update the downloader.'), - self.text_len, - ) + '', - 0, 0, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'You should do this, even if you think it is already' \ - + ' installed.', - ), - self.text_len, - ) + '', - 0, 2, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 3, grid_width, 1) - - if os.name != 'nt': - extra_rows = 0 - - else: - extra_rows = 1 - self.add_label( - '' \ - + _('Estimated install size') + ': 15 MB', - 0, (3 + extra_rows), grid_width, 1, - ) - - self.downloader_button = Gtk.Button(_('Install and update downloader')) - self.inner_grid.attach( - self.downloader_button, - 1, (4 + extra_rows), 1, 1, - ) - self.downloader_button.set_hexpand(False) - # (Signal connect appears below) - - self.update_button = Gtk.Button(_('More options')) - # (Making the button invisible doesn't work, so instead don't add it - # to the grid at all) - if os.name != 'nt' and not self.more_options_flag: - self.inner_grid.attach( - self.update_button, - 1, (5 + extra_rows), 1, 1, - ) - self.update_button.set_hexpand(False) - # (Signal connect appears below) - - # (When the update button is clicked, it is made invisible, and this - # widget is made visible instead) - self.update_liststore = Gtk.ListStore(str, str) - for item in self.app_obj.ytdl_update_list: - self.update_liststore.append( - [item, formats.YTDL_UPDATE_DICT[item]], - ) - - self.update_combo = Gtk.ComboBox.new_with_model(self.update_liststore) - if os.name != 'nt': - self.inner_grid.attach( - self.update_combo, - 1, (5 + extra_rows), 1, 1, - ) - if not self.more_options_flag: - self.update_combo.set_visible(False) - - renderer_text = Gtk.CellRendererText() - self.update_combo.pack_start(renderer_text, True) - self.update_combo.add_attribute(renderer_text, 'text', 1) - self.update_combo.set_entry_text_column(1) - - if self.ytdl_update_current is not None: - ytdl_update_current = self.ytdl_update_current - else: - ytdl_update_current = self.app_obj.ytdl_update_current - - self.update_combo.set_active( - self.app_obj.ytdl_update_list.index(ytdl_update_current), - ) - # (Signal connect appears below) - - # Update the combo, so that the youtube-dl fork, rather than - # youtube-dl itself, is visible (if applicable) - self.refresh_update_combo() - - self.downloader_scrolled, self.downloader_textview, \ - self.downloader_textbuffer = self.add_textview( - 0, (7 + extra_rows), grid_width, 1, - ) - - # (Signal connects from above) - self.downloader_button.connect( - 'clicked', - self.on_button_fetch_downloader_clicked, - ) - - self.update_button.connect( - 'clicked', - self.on_button_update_path_clicked, - ) - - # (Signal connects from above) - self.update_combo.connect('changed', self.on_combo_update_changed) - - - def setup_fetch_ffmpeg_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - grid_width = 3 - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('Click the button to install FFmpeg.'), - self.text_len, - ) + '', - 0, 0, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'Without FFmpeg, Tartube cannot download high-resolution' \ - + ' videos, and cannot display video thumbnails from' \ - + ' YouTube.', - ), - self.text_len, - ) + '', - 0, 2, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 3, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'The operation might take several minutes. If it fails,' \ - + ' try clicking the Install FFmpeg button again.', - ), - self.text_len, - ) + '', - 0, 4, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 5, grid_width, 1) - - if os.name != 'nt': - extra_rows = 0 - - else: - extra_rows = 1 - - self.add_label( - '' \ - + _('Download size') + ': 0.3 GB - ' \ - + _('Install size') + ': 1.5 GB', - 0, (5 + extra_rows), grid_width, 1, - ) - - if not self.try_install_ffmpeg_flag: - msg = _('Install FFmpeg') - else: - msg = _('Reinstall FFmpeg') - self.ffmpeg_button = Gtk.Button(msg) - - self.inner_grid.attach(self.ffmpeg_button, 1, (6 + extra_rows), 1, 1) - self.ffmpeg_button.set_hexpand(False) - # (Signal connect appears below) - - self.ffmpeg_scrolled, self.ffmpeg_textview, self.ffmpeg_textbuffer \ - = self.add_textview( - 0, (7 + extra_rows), grid_width, 1, - ) - - # (Signal connects from above) - self.ffmpeg_button.connect( - 'clicked', - self.on_button_fetch_ffmpeg_clicked, - ) - - - def setup_classic_mode_page(self): - - """Called by self.setup_page(). - - Invites the user to open Tartube at the Classic Mode tab. - """ - - grid_width = 3 - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('Tartube adds videos to a database.'), - self.text_len, - ) + '', - 0, 0, grid_width, 1, - ) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('If you don\'t need a database, you can use Classic Mode.'), - self.text_len, - ) + '', - 0, 1, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 2, grid_width, 1) - - self.add_image( - self.app_obj.main_win_obj.icon_dict['setup_classic_icon'], - 0, 3, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 4, grid_width, 1) - - if not self.show_classic_tab_on_startup_flag: - msg = _('Always open Tartube at this tab') - else: - msg = _('Don\'t open Tartube at this tab') - - self.auto_open_button = Gtk.Button(msg) - self.inner_grid.attach(self.auto_open_button, 1, 5, 1, 1) - self.auto_open_button.set_hexpand(False) - self.auto_open_button.connect( - 'clicked', - self.on_button_auto_open_clicked, - ) - - - def setup_finish_page_mswin(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page, shown only on MS Windows. - """ - - grid_width = 3 - - self.add_image( - self.app_obj.main_win_obj.icon_dict['ready_icon'], - 0, 0, grid_width, 1, - ) - - self.add_label( - '' \ - + _('All done!') + '', - 0, 1, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 2, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'If you need to re-install or update the downloader or' \ - + ' FFmpeg, you can do it from the main window\'s menu.', - ), - self.text_len, - ) + '', - 0, 3, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 4, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'Click this button below to see a short tutorial, or' \ - + ' click the OK button to start Tartube!', - ), - self.text_len, - ) + '', - 0, 5, grid_width, 1, - ) - - self.add_tutorial_button(6) - - - def setup_finish_page_strict(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page, shown only after a STRICT install - from a DEB/RPM package. - """ - - grid_width = 3 - - self.add_image( - self.app_obj.main_win_obj.icon_dict['ready_icon'], - 0, 0, grid_width, 1, - ) - - self.add_label( - '' \ - + _('All done!') + '', - 0, 1, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 2, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'You must install the downloader on your system, before' \ - + ' you can use Tartube.' - ), - self.text_len, - ) + '', - 0, 3, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 4, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'It is strongly recommended that you install FFmpeg.' \ - + ' Without it, Tartube cannot download video clips or' \ - + ' high-resolution videos, and cannot display many' \ - + ' thumbnails.', - ), - self.text_len, - ) + '', - 0, 5, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 6, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'Click this button below to see a short tutorial, or' \ - + ' click the OK button to start Tartube!', - ), - self.text_len, - ) + '', - 0, 7, grid_width, 1, - ) - - self.add_tutorial_button(8) - - - def setup_finish_page_default(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page, for all operating systems except - MS Windows. - """ - - grid_width = 3 - - self.add_image( - self.app_obj.main_win_obj.icon_dict['ready_icon'], - 0, 0, grid_width, 1, - ) - - self.add_label( - '' \ - + _('All done!') + '', - 0, 1, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 2, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('It is strongly recommended that you install FFmpeg.'), - self.text_len, - ) + '', - 0, 3, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 4, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'Without FFmpeg, Tartube cannot download video clips or' \ - + ' high-resolution videos, and cannot display many' \ - + ' video thumbnails.', - ), - self.text_len, - ) + '', - 0, 5, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 6, grid_width, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'Click this button below to see a short tutorial, or' \ - + ' click the OK button to start Tartube!', - ), - self.text_len, - ) + '', - 0, 7, grid_width, 1, - ) - - self.add_tutorial_button(8) - - - # (Support functions) - - - def add_tutorial_button(self, row): - - """Called by several (final) pages to show a button that opens the - tutorial wizard window. - - Args: - - row (int): The row on which the button is placed - - """ - - grid2 = Gtk.Grid() - self.inner_grid.attach( - grid2, - 1, row, 1, 1, - ) - grid2.set_hexpand(False) - - box = Gtk.Box() - grid2.attach(box, 0, 0, 1, 3) - box.set_border_width(self.spacing_size * 2) - - image = Gtk.Image() - box.add(image) - image.set_from_pixbuf( - self.app_obj.main_win_obj.pixbuf_dict['learn_left_large'], - ) - image.set_hexpand(False) - - self.tutorial_button = Gtk.Button(_('Read the tutorial')) - grid2.attach( - self.tutorial_button, - 1, 1, 1, 1 - ) - self.tutorial_button.set_hexpand(True) - self.tutorial_button.connect( - 'clicked', - self.on_button_tutorial_clicked, - ) - - box2 = Gtk.Box() - grid2.attach(box2, 2, 0, 1, 3) - box2.set_border_width(self.spacing_size * 2) - - image2 = Gtk.Image() - box2.add(image2) - image2.set_from_pixbuf( - self.app_obj.main_win_obj.pixbuf_dict['learn_right_large'], - ) - image2.set_hexpand(False) - - - def downloader_page_write(self, msg): - - """Called by updates.UpdateManager.install_ytdl_write_output() or - self.downloader_fetch_finished(). - - When installing/updating youtube-dl (or a fork), write a message in the - textview. - - N.B. Because Gtk is not thread safe, this function must always be - called from within GObject.timeout_add(). - - Args: - - msg (str): The message to display - - """ - - self.downloader_textbuffer.insert( - self.downloader_textbuffer.get_end_iter(), - utils.tidy_up_long_string(msg) + '\n', - ) - - adjust = self.downloader_scrolled.get_vadjustment() - adjust.set_value(adjust.get_upper()) - - self.downloader_textview.queue_draw() - - - def downloader_fetch_finished(self, msg): - - """Called by mainapp.TartubeApp.update_manager_finished(). - - Display the success/failure message in the textview, and re-sensitise - buttons. - - Args: - - msg (str): The success/failure message to display - - """ - - GObject.timeout_add( - 0, - self.downloader_page_write, - msg, - ) - - self.downloader_button.set_sensitive(True) - self.next_button.set_sensitive(True) - self.prev_button.set_sensitive(True) - - - def ffmpeg_page_write(self, msg): - - """Called by updates.UpdateManager.install_ytdl_write_output() or - self.ffmpeg_fetch_finished(). - - When installing FFmpeg, write a message in the textview. - - N.B. Because Gtk is not thread safe, this function must always be - called from within GObject.timeout_add(). - - Args: - - msg (str): The message to display - - """ - - self.ffmpeg_textbuffer.insert( - self.ffmpeg_textbuffer.get_end_iter(), - utils.tidy_up_long_string(msg) + '\n', - ) - - adjust = self.ffmpeg_scrolled.get_vadjustment() - adjust.set_value(adjust.get_upper()) - - self.downloader_textview.queue_draw() - - - def ffmpeg_fetch_finished(self, msg): - - """Called by mainapp.TartubeApp.update_manager_finished(). - - Display the success/failure message in the textview, and re-sensitise - buttons. - - Args: - - msg (str): The success/failure message to display - - """ - - GObject.timeout_add( - 0, - self.ffmpeg_page_write, - msg, - ) - - self.ffmpeg_button.set_label(_('Reinstall FFmpeg')) - self.ffmpeg_button.set_sensitive(True) - self.next_button.set_sensitive(True) - self.prev_button.set_sensitive(True) - - self.try_install_ffmpeg_flag = True - - - def refresh_update_combo(self): - - """Called by self.setup_fetch_downloader_page(). - - When the youtube-dl fork is changed, updates the contents of the - combo created by self.setup_fetch_downloader_page(). - """ - - fork = standard = 'youtube-dl' - if self.ytdl_fork is not None: - fork = self.ytdl_fork - - count = -1 - for item in self.app_obj.ytdl_update_list: - - count += 1 - descrip = re.sub(standard, fork, formats.YTDL_UPDATE_DICT[item]) - self.update_liststore.set( - self.update_liststore.get_iter(Gtk.TreePath(count)), - 1, - descrip, - ) - - - # (Callbacks) - - - def on_button_auto_open_clicked(self, button): - - """Called from a callback in self.setup_classic_mode_page(). - - Sets whether the main window should open at the Classic Mode tab, or - not. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if not self.show_classic_tab_on_startup_flag: - self.show_classic_tab_on_startup_flag = True - button.set_label(_('Don\'t open Tartube at this tab')) - else: - self.show_classic_tab_on_startup_flag = False - button.set_label(_('Always open Tartube at this tab')) - - - def on_button_backup_mode_toggled(self, radiobutton, mode): - - """Called from callback in self.setup_backup_page(). - - Sets the database file's backup mode. - - Args: - - radiobutton (Gtk.Radiobutton): The widget clicked - - mode (str): The new value of mainapp.TartubeApp.db_backup_mode - - """ - - if radiobutton.get_active(): - self.db_backup_mode = mode - - - def on_button_cancel_clicked(self, button): - - """Modified version of the standard function, called from a callback in - self.setup_button_strip(). - - Closes the wizard window without applying any changes, unless an - update operation is in progress. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.cancel_changes() - if not self.app_obj.update_manager_obj: - self.destroy() - - - def on_button_choose_folder_clicked(self, button, label): - - """Called from a callback in self.setup_db_page(). - - Opens a file chooser dialogue, so the user can set the location of - Tartube's data directory. - - Args: - - button (Gtk.Button): The widget clicked - - label (Gtk.Label): Once set, the path to the directory is displayed - in this label - - """ - - if not self.mswin_flag: - title = _('Select Tartube\'s data directory') - else: - title = _('Select Tartube\'s data folder') - - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - title, - self, - 'folder', - ) - - # Get the user's response - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - - self.data_dir = dialogue_win.get_filename() - label.set_markup( - '' + self.data_dir \ - + '', - ) - - # Data directory set, so re-enable the Next button - self.next_button.set_sensitive(True) - - dialogue_win.destroy() - - - def on_button_default_folder_clicked(self, button, label): - - """Called from a callback in self.setup_db_page(). - - Sets the default location for Tartube's data directory (not on MS - Windows). - - Args: - - button (Gtk.Button): The widget clicked - - label (Gtk.Label): Once set, the path to the directory is displayed - in this label - - """ - - self.data_dir = self.app_obj.data_dir - label.set_markup( - '' \ - + self.app_obj.data_dir + '', - ) - - # Data directory set, so re-enable the Next button - self.next_button.set_sensitive(True) - - - def on_button_fetch_downloader_clicked(self, button): - - """Called from a callback in self.setup_fetch_page(). - - Starts an update operation to download and install the selected fork of - youtube-dl. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Desensitise buttons until the operation is complete - button.set_sensitive(False) - self.next_button.set_sensitive(False) - self.prev_button.set_sensitive(False) - - # Start the update operation - if not self.app_obj.update_manager_start_from_wizwin(self, 'ytdl'): - - # Operation did not start - button.set_sensitive(True) - self.next_button.set_sensitive(True) - self.prev_button.set_sensitive(True) - - - def on_button_fetch_ffmpeg_clicked(self, button): - - """Called from a callback in self.setup_fetch_page(). - - Starts an update operation to download and install FFmpeg. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Desensitise buttons until the operation is complete - button.set_sensitive(False) - self.next_button.set_sensitive(False) - self.prev_button.set_sensitive(False) - - # Start the update operation - if not self.app_obj.update_manager_start_from_wizwin(self, 'ffmpeg'): - - # Operation did not start - button.set_sensitive(True) - self.next_button.set_sensitive(True) - self.prev_button.set_sensitive(True) - - - def on_button_tutorial_clicked(self, button): - - """Called from a callback in self.setup_fetch_page(). - - Closes this window, and marks the tutorial wizard window to be opened. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.open_tutorial_flag = True - self.apply_changes() - self.destroy() - - - def on_button_update_path_clicked(self, button): - - """Called from a callback in self.setup_fetch_page(). - - Makes the 'More options' button invisible, and the update path combo - visible. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - button.set_visible(False) - self.update_combo.set_visible(True) - - # Flag set to True, once the 'More options' button has been clicked, - # so that it is never visible again - self.more_options_flag = True - - - def on_button_ytdl_fork_toggled(self, radiobutton, checkbutton, entry, \ - fork_type=None): - - """Called from callback in self.setup_set_downloader_page(). - - Sets the youtube-dl fork to be used. See also - self.on_entry_ytdl_fork_changed(). - - Args: - - radiobutton (Gtk.Radiobutton): The widget clicked - - checkbutton (Gtk.CheckButton): Another widget to be updated - - entry (Gtk.Entry): Another widget to be updated - - fork_type (str): 'yt-dlp', 'youtube-dl', or None for any other fork - - """ - - if radiobutton.get_active(): - - if fork_type is None: - - fork_name = entry.get_text() - # (As in the preference window, if the 'other fork' option is - # selected, but nothing is entered in the entry box, use - # youtube-dl as the downloader) - if fork_name == '': - self.ytdl_fork = None - else: - self.ytdl_fork = fork_name - - checkbutton.set_sensitive(False) - entry.set_sensitive(True) - - elif fork_type == 'youtube-dl': - - self.ytdl_fork = None - checkbutton.set_sensitive(False) - entry.set_text('') - entry.set_sensitive(False) - - elif fork_type == 'yt-dlp': - - self.ytdl_fork = fork_type - checkbutton.set_sensitive(True) - entry.set_text('') - entry.set_sensitive(False) - - - def on_button_ytdlp_install_toggled(self, checkbutton): - - """Called from callback in self.setup_set_downloader_page(). - - Sets the flag to install yt-dlp with or without dependencies. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active(): - self.ytdl_fork_no_dependency_flag = True - else: - self.ytdl_fork_no_dependency_flag = False - - - def on_combo_update_changed(self, combo): - - """Called from callback in self.setup_fetch_downloader_page(). - - Sets the youtube-dl install/update method. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.ytdl_update_current = model[tree_iter][0] - - - def on_entry_ytdl_fork_changed(self, entry, radiobutton): - - """Called from callback in self.setup_set_downloader_page(). - - Sets the youtube-dl fork to be used. See also - self.on_button_ytdl_fork_toggled(). - - Args: - - entry (Gtk.Entry): The widget changed - - """ - - if radiobutton.get_active(): - - entry_text = utils.strip_whitespace(entry.get_text()) - # (As in the preference window, if the 'other fork' option is - # selected, but nothing is entered in the entry box, use - # youtube-dl as the system default) - if entry_text == '': - self.ytdl_fork = None - else: - self.ytdl_fork = entry_text - - - def on_frame_downloader_clicked(self, event_box, event_button, - radiobutton): - - """Called from callback in self.setup_set_downloader_page(). - - Enables/disables selecting a downloader by clicking anywhere in its - containing frame. - - Args: - - event_box (Gtk.EventBox): Ignored - - event_button (Gdk.EventButton): Ignored - - radiobutton (Gtk.RadioButton): The radiobutton inside the clicked - frame, which should be made active - - """ - - if not radiobutton.get_active(): - radiobutton.set_active(True) - - -class ImportYTWizWin(GenericWizWin): - - """Python class for a 'wizard window' used to import YouTube subscriptions. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Subscriptions wizard window starts here.' \ - + ' In the menu, click Media > Export/Import > Import' \ - + ' YouTube subscriptions...' - ) - - Gtk.Window.__init__(self, title=_('Import YouTube subscriptions')) - - if self.is_duplicate(app_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.vbox = None # Gtk.VBox - self.inner_grid = None # Gtk.Grid - self.cancel_button = None # Gtk.Button - self.next_button = None # Gtk.Button - self.prev_button = None # Gtk.Button - # (From self.setup_finish_page) - self.liststore = None # Gtk.ListStore - self.checkbutton = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between preference window widgets - self.spacing_size = self.app_obj.default_spacing_size - - # List of 'pages' (widget layouts on self.inner_grid). Each item in the - # list is the function to call - self.page_list = [] # Set below - # The number of the current page (the first is 0), matching an index in - # self.page_list - self.current_page = 0 - - # User choices - # Path to the YouTube export file - self.import_path = None - # Dictionary of importable channels/playlists, in the form described in - # mainapp.TartubeApp.export_from_db() - self.db_dict = {} - - # Standard length of text in the wizard window - self.text_len = 60 - - - # Code - # ---- - - # Set the page list - self.page_list = [ - 'setup_start_page', - 'setup_load_page', - 'setup_finish_page', - ] - - # Set up the wizard window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericWizWin - - -# def setup(): # Inherited from GenericWizWin - - -# def setup_grid(): # Inherited from GenericWizWin - - -# def setup_button_strip(): # Inherited from GenericWizWin - - -# def setup_page(): # Inherited from GenericWizWin - - -# def convert_next_button(): # Inherited from GenericWizWin - - - def apply_changes(self): - - """Called by self.on_button_next_clicked(). - - Import the specified channels/playlists into the Tartube database. - """ - - (video_count, channel_count, playlist_count, folder_count) \ - = self.app_obj.process_import( - self.db_dict, # The imported data... - self.db_dict, # ...already exists in 'flattened' form - None, # No parent 'mini_dict' yet - False, # No videos to import - self.checkbutton.get_active(), - True, # Imported into selected folder, if any - 0, # video_count - 0, # channel_count - 0, # playlist count - 0, # folder_count - ) - - if not channel_count and not playlist_count: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - _('Nothing has been imported'), - 'error', - 'ok', - ) - - else: - - # Show a confirmation - msg = _('Imported into database') \ - + ':\n\n' + _('Videos') + ': ' + str(video_count) \ - + '\n' + _('Channels') + ': ' + str(channel_count) \ - + '\n' + _('Playlists') + ': ' + str(playlist_count) \ - + '\n' + _('Folders') + ': ' + str(folder_count) - - self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue( - msg, - 'info', - 'ok', - ) - - -# def cancel_changes(): # Inherited from GenericWizWin - - -# def close(): # Inherited from GenericWizWin - - - # (Setup pages) - - - def setup_start_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_image( - self.app_obj.main_win_obj.icon_dict['yt_icon'], - 0, 0, 1, 1, - ) - - self.add_label( - '' \ - + _('Import YouTube Subscriptions') + '', - 0, 1, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 2, 1, 1) - - row = 2 - for msg in [ - _('Open YouTube in your browser, and sign in'), - _('Click the channel icon in the top-right corner'), - _('Select Your data in YouTube'), - _('Under Your YouTube dashboard, click More'), - _('Click Download YouTube data'), - _('Click All YouTube data included'), - _('Deselect everything except Subscriptions'), - _('Click Next step, then Create export'), - _('Wait a few moments, then click Download'), - - ]: - row += 1 - self.add_label( - '* ' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, row, 1, 1, - ) - - - def setup_load_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - grid_width = 3 - - self.add_image( - self.app_obj.main_win_obj.icon_dict['yt_icon'], - 0, 0, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, grid_width, 1) - - self.add_label( - '' \ - + _('Select the YouTube export file') \ - + '', - 0, 2, grid_width, 1, - ) - - button = Gtk.Button(_('Select file')) - self.inner_grid.attach(button, 1, 3, 1, 1) - # (Signal connect appears below) - - # (Empty label for spacing) - self.add_empty_label(0, 4, grid_width, 1) - - # The specified path appears here, after it has been selected - if self.import_path is None: - - label = self.add_label( - '', - 0, 5, grid_width, 1, - ) - - else: - - label = self.add_label( - '' \ - + self.import_path + '', - 0, 5, grid_width, 1, - ) - - # (Signal connects from above) - button.connect( - 'clicked', - self.on_button_choose_import_clicked, - label, - ) - - # Disable the Next button until a valid import has been selected - if self.import_path is None: - self.next_button.set_sensitive(False) - - - def setup_finish_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - grid_width = 5 - - self.add_image( - self.app_obj.main_win_obj.icon_dict['yt_icon'], - 0, 0, grid_width, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - self.inner_grid.attach(scrolled, 0, 2, grid_width, 1) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_size_request(-1, 250) - - frame = Gtk.Frame() - scrolled.add_with_viewport(frame) - - treeview = Gtk.TreeView() - frame.add(treeview) - treeview.set_headers_visible(True) - treeview.set_can_focus(False) - - for i, column_title in enumerate( - [ _('Import'), _('Type'), _('Name'), _('URL'), 'hide' ], - ): - if i == 0: - renderer_toggle = Gtk.CellRendererToggle() - renderer_toggle.connect('toggled', self.on_checkbutton_toggled) - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - treeview.append_column(column_toggle) - elif i == 1: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - column_title, - renderer_pixbuf, - pixbuf=i, - ) - treeview.append_column(column_pixbuf) - column_pixbuf.set_resizable(False) - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - treeview.append_column(column_text) - if column_title == 'hide': - column_text.set_visible(False) - else: - column_text.set_resizable(True) - if i == 2: - renderer_text.connect( - 'edited', - self.on_container_name_edited, - ) - elif i == 3: - renderer_text.connect( - 'edited', - self.on_container_url_edited, - ) - renderer_text.set_property('editable', True) - - self.liststore = Gtk.ListStore( - bool, GdkPixbuf.Pixbuf, str, str, int, - ) - treeview.set_model(self.liststore) - - # Populate the treeview - - # (Sorting function for the code immediately below) - def sort_dict_by_name(this_dict): - return this_dict['name'] - - # Deal with importable channels/playlists in alphabetical order - for mini_dict in sorted(self.db_dict.values(), key=sort_dict_by_name): - - self.liststore.append([ - mini_dict['import_flag'], - self.app_obj.main_win_obj.pixbuf_dict[ - mini_dict['type'] + '_small' - ], - mini_dict['name'], - mini_dict['source'], - mini_dict['dbid'], - ]) - - # Strip of widgets at the bottom - self.checkbutton = self.add_checkbutton( - _('Merge channels/playlists'), - 0, 3, 1, 1, - ) - - button = Gtk.Button(_('Toggle channel/playlist')) - self.inner_grid.attach(button, 1, 3, 1, 1) - button.connect( - 'clicked', - self.on_button_toggle_container_clicked, - treeview, - ) - - button2 = Gtk.Button(_('Select all')) - self.inner_grid.attach(button2, 2, 3, 1, 1) - button2.connect('clicked', self.on_button_select_all_clicked) - - button3 = Gtk.Button(_('Unselect all')) - self.inner_grid.attach(button3, 3, 3, 1, 1) - button3.connect('clicked', self.on_button_unselect_all_clicked) - - - # (Support functions) - - - def validate_import(self, label): - - """Called by self.on_button_choose_import_clicked(). - - The path to the export file has been stored in self.import_path. - - Check the file exists, is valid, and contains some YouTube - subscriptions. Update IVs and the text of 'label' accordingly - - Args: - - label (Gtk.Label): A widget to update - - Return values: - - True on success, False of failure - - """ - - fail_msg = None - - # Check a .zip file really exists - if self.import_path is None \ - or not os.path.isfile(self.import_path) \ - or not zipfile.is_zipfile(self.import_path): - fail_msg = _('Missing of invalid export file') - - # Prepare to extract the archive in a temporary directory, deleting any - # existing directory first - temp_dir = None - if not fail_msg: - - temp_dir = os.path.abspath( - os.path.join( - self.app_obj.temp_dir, - 'zip', - ), - ) - - if os.path.isdir(temp_dir) \ - and not self.app_obj.remove_directory(temp_dir): - fail_msg = _('Unable to prepare export file for extraction') - - if not fail_msg: - - if not self.app_obj.make_directory(temp_dir): - fail_msg = _('Unable to prepare export file for extraction') - - # Extract the subscriptions.csv file from the archive - if not fail_msg: - - rel_path = os.path.join( - 'Takeout', - 'YouTube and YouTube Music', - 'subscriptions', - 'subscriptions.csv', - ) - - try: - with(zipfile.ZipFile(self.import_path)) as zip_file: - zip_file.extract(member=rel_path, path=temp_dir) - - except: - fail_msg = _('Unable to extract export file') - - # Check that a subscriptions.csv exists - if not fail_msg: - - csv_path = os.path.abspath( - os.path.join( - temp_dir, - 'Takeout', - 'YouTube and YouTube Music', - 'subscriptions', - 'subscriptions.csv', - ), - ) - - if not os.path.isfile(csv_path): - fail_msg = _('Missing subscriptions in export file') - - # Extract a list of channels/playlists from the export file - subscription_list = [] - if not fail_msg: - - # List of lists; each minilist represents one channel/playlist, - # and is in the form [name, URL] - try: - - fh = open(csv_path, 'r') - count = 0 - for line in fh.readlines(): - - # Ignore the first line, which should be - # 'Channel ID,Channel URL,Channel title' - count += 1 - if count > 1: - - match = re.search( - r'^([^,]+),([^,]+),([^,\n]+)\n*$', - line, - ) - if match: - subscription_list.append([ - match.groups()[2], # Channel title - match.groups()[1], # URL - ]) - - except: - fail_msg = _('Cannot read subscriptions in export file') - - if not fail_msg and not subscription_list: - fail_msg = _('No subscriptions in export file') - - # Delete the temporary directory and update the text of the label - if temp_dir is not None: - self.app_obj.remove_directory(temp_dir) - - if fail_msg: - - # (No channels/playlists are currently importable) - self.db_dict = {} - - label.set_markup( - '' + fail_msg \ - + '', - ) - - return False - - else: - - if len(subscription_list) == 1: - - label.set_markup( - '' \ - + _('Export file is valid, extracted 1 subscription') \ - + '', - ) - - else: - - label.set_markup( - '' \ - + _( - 'Export file is valid, extracted {0} subscriptions', - ).format(len(subscription_list)) \ - + '', - ) - - # Convert 'subscription_list' into the standard export/import - # format described in mainapp.TartubeApp.export_from_db() - self.db_dict = {} - count = 0 - - for mini_list in subscription_list: - - count += 1 - - mini_dict = { - # (Assume everything is a channel, initially) - 'type': 'channel', - # (Use a fake .dbid; code in mainapp.TartubeApp creates - # new media.Channel and media.Playlist objects with - # real .dbids) - 'dbid': count, - 'vid': None, - 'name': mini_list[0], - 'nickname': mini_list[0], - 'file': None, - 'source': mini_list[1], - # (This procedure does not import videos) - 'db_dict': {}, - # (Add one of the extra key-value pairs also added by - # mainwin.ImportDialogue) - 'import_flag': True, - } - - self.db_dict[count] = mini_dict - - # Procedure complete - return True - - - # (Callbacks) - - - def on_button_choose_import_clicked(self, button, label): - - """Called from a callback in self.setup_load_page(). - - Opens a file chooser dialogue, so the user can set the location of the - YouTube export. - - Args: - - button (Gtk.Button): The widget clicked - - label (Gtk.Label): Once set, the path to the directory is displayed - in this label - - """ - - dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser( - 'Select the YouTube export', - self, - 'open', - ) - - # Get the user's response - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - - self.import_path = dialogue_win.get_filename() - # Check the export file, storing the results in various IVs, and - # setting label accordingly - # Then enable/disable the Next button - if not self.validate_import(label): - self.next_button.set_sensitive(False) - else: - self.next_button.set_sensitive(True) - - dialogue_win.destroy() - - - def on_button_select_all_clicked(self, button): - - """Called from a callback in self.setup_finish_page(). - - Mark all channels/playlists to be imported. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.liststore)): - self.liststore[path][0] = True - - for mini_dict in self.db_dict.values(): - mini_dict['import_flag'] = True - - - def on_button_toggle_container_clicked(self, button, treeview): - - """Called from a callback in self.setup_finish_page(). - - Switch the selected channel to a playlist, or vice-versa. - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview to be modified - - """ - - selection = treeview.get_selection() - (model, tree_iter) = selection.get_selected() - if tree_iter is None: - - # Nothing selected - return - - # Update the dictionary of data to be imported - fake_dbid = model[tree_iter][4] - for mini_dict in self.db_dict.values(): - - if mini_dict['dbid'] == fake_dbid: - - if mini_dict['type'] == 'channel': - mini_dict['type'] = 'playlist' - else: - mini_dict['type'] = 'channel' - - # Update the treeview's row - model[tree_iter] = [ - mini_dict['import_flag'], - self.app_obj.main_win_obj.pixbuf_dict[ - mini_dict['type'] + '_small' - ], - mini_dict['name'], - mini_dict['source'], - mini_dict['dbid'], - ] - - return - - - def on_button_unselect_all_clicked(self, button): - - """Called from a callback in self.setup_finish_page(). - - Mark all channels/playlists to be not imported. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - for path in range(0, len(self.liststore)): - self.liststore[path][0] = False - - for mini_dict in self.db_dict.values(): - mini_dict['import_flag'] = False - - - def on_checkbutton_toggled(self, checkbutton, path): - - """Called from a callback in self.__init__(). - - Respond when the user selects/deselects an item in the treeview. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - path (int): A number representing the widget's row - - """ - - # The user has clicked on the checkbutton widget, so toggle the widget - # itself - self.liststore[path][0] = not self.liststore[path][0] - - # Update IVs - mini_dict = self.db_dict[self.liststore[path][4]] - mini_dict['import_flag'] = self.liststore[path][0] - - - def on_container_name_edited(self, widget, path, text): - - """Called from callback in self.setup_finish_page(). - - Updates the name of a channel/playlist. - - Args: - - widget (Gtk.CellRendererText): The widget clicked - - path (int): Path to the treeview line that was edited - - text (str): The new contents of the cell - - """ - - # Check the entered text is a valid name - if text == '' \ - or re.search(r'^\s*$', text) \ - or not self.app_obj.check_container_name_is_legal(text): - return - - # Update the column text - self.liststore[path][2] = text - - # Update IVs - mini_dict = self.db_dict[self.liststore[path][4]] - mini_dict['name'] = text - mini_dict['nickname'] = text - - - def on_container_url_edited(self, widget, path, text): - - """Called from callback in self.setup_finish_page(). - - Updates the URL of a channel/playlist. - - Args: - - widget (Gtk.CellRendererText): The widget clicked - - path (int): Path to the treeview line that was edited - - text (str): The new contents of the cell - - """ - - # Check the entered text is a valid name - if not utils.check_url(text): - return - - # Update the column text - self.liststore[path][3] = text - - # Update IVs - mini_dict = self.db_dict[self.liststore[path][4]] - mini_dict['source'] = text - - -class TutorialWizWin(GenericWizWin): - - """Python class for a 'wizard window' used to show a short tutorial. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - ignore_me = _( - 'TRANSLATOR\'S NOTE: Tutorial wizard window starts here.' \ - + ' In the menu, click Help > Show tutorial...' - ) - - Gtk.Window.__init__(self, title=_('Tartube Tutorial')) - - if self.is_duplicate(app_obj): - return - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.vbox = None # Gtk.VBox - self.inner_grid = None # Gtk.Grid - self.cancel_button = None # Gtk.Button - self.next_button = None # Gtk.Button - self.prev_button = None # Gtk.Button - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between preference window widgets - self.spacing_size = self.app_obj.default_spacing_size - - # List of 'pages' (widget layouts on self.inner_grid). Each item in the - # list is the function to call - self.page_list = [] # Set below - # The number of the current page (the first is 0), matching an index in - # self.page_list - self.current_page = 0 - - # Standard length of text in the wizard window - self.text_len = 70 - - - # Code - # ---- - - # Set the page list - # N.B. If more pages are added, mainwin.MainWin.tutorial_page_count - # must be updated with the new page count! - self.page_list = ['setup_start_page'] - for i in range(1, self.app_obj.main_win_obj.tutorial_page_count): - self.page_list.append('setup_page_' + str(i)) - - self.page_list.append('setup_finish_page') - - # Set up the wizard window - self.setup() - - - # Public class methods - - -# def is_duplicate(): # Inherited from GenericWizWin - - - def setup(self): - - """Called by self.__init__(). - - Sets up the wizard window when it opens. - """ - - # Set the non-default size for this wizard window - self.set_default_size(750, 575) - - # Set the window's Gtk icon list - self.set_icon_list(self.app_obj.main_win_obj.win_pixbuf_list) - - # Set up main widgets - self.setup_grid() - self.setup_button_strip() - - # Set up the first page (a widget layout on self.inner_grid) - self.setup_page() - - # Procedure complete - self.show_all() - - # Inform the main window of this window's birth (so that Tartube - # doesn't allow an operation to start until all configuration windows - # have closed) - self.app_obj.main_win_obj.add_child_window(self) - - # Add a callback so we can inform the main window of this window's - # destruction - self.connect('destroy', self.close) - - -# def setup_grid(): # Inherited from GenericWizWin - - -# def setup_button_strip(): # Inherited from GenericWizWin - - -# def setup_page(): # Inherited from GenericWizWin - - -# def convert_next_button(): # Inherited from GenericWizWin - - -# def apply_changes(): # Inherited from GenericWizWin - - -# def cancel_changes(): # Inherited from GenericWizWin - - -# def close(): # Inherited from GenericWizWin - - - # (Setup pages) - - - def setup_start_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_image( - self.app_obj.main_win_obj.icon_dict['system_icon'], - 0, 0, 1, 1, - ) - - self.add_label( - '' \ - + _('Tartube Tutorial') + '', - 0, 1, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 2, 1, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('This tutorial will take about five to ten minutes.'), - self.text_len, - ) + '', - 0, 4, 1, 1, - ) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('Click the Next button to get started.'), - self.text_len, - ) + '', - 0, 5, 1, 1, - ) - - - def setup_page_1(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial1'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _('Click the \'Add a new channel\' button.') - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_2(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial2'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'In the first box, give the channel a name. In the second box,' \ - + ' paste the channel\'s URL.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_3(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial3'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _('Your new channel is added to Tartube\'s database.') - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_4(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial4'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'Click \'Check all\' to fetch a list of videos. Click \'Download' \ - + ' all\' to download the videos.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_5(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial5'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _('As well as adding channels, you can add playlists.') - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_6(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial6'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'If you want to add videos individually, click the \'Add new' \ - + ' videos\' button. They will be downloaded to the' \ - + ' \'Unsorted Videos\' folder.' \ - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_7(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial7'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'You can organise your videos into folders. Click the \'Add a' \ - + ' new folder\' button.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_8(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial8'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'You can drag-and-drop channels and playlists into the right' \ - + ' folder.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_9(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial9'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'You can check the progress of your downloads in the' \ - + ' \'Progress\' tab.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_10(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial10'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _('Advanced users can see more detail in the \'Output\' tab.') - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_11(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial11'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'A summary of any problems will appear in the' \ - + ' \'Errors / Warnings\' tab.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_12(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial12'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'If you don\'t want to build a database of videos, then you' \ - + ' can use the \'Classic Mode\' tab.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_13(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial13'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'In the box at the top, paste the URLs of videos, channels' \ - + ' and playlists.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_14(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial14'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _('Set the download folder.') - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_15(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial15'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _('Choose the video or audio format.') - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_16(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial16'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _('Click the \'Add URLs\' button.') - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_17(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial17'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'If you like, you can now set up more downloads using' \ - + ' a different download folder and/or format.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_18(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial18'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _('When you\'re ready, click the \'Download all\' button.') - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_19(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial19'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'Download options change the way your videos are downloaded.' \ - + ' To see them, click \'Edit > General download options...\'', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_20(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial20'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'A completely different set of download options is used in the ' \ - + ' the \'Classic Mode\' tab. Click the menu button in the' \ - + ' top-right corner.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_21(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial21'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'The new window has many options you can change. For example, to' \ - + ' download videos as .mp3 files, click the \'Convert\' tab.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_22(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial22'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'Advanced users can specify download options directly,' \ - + ' overriding settings anywhere else in this window.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_23(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial23'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'Advanced users can also reveal extra download options by' \ - + ' clicking this button.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_24(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial24'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'You can apply a separate set of download options to a channel.' \ - + ' Right-click it, and select \'Downloads > Apply download' \ - + ' options...\'', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_25(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial25'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'The icon changes to remind you that the channel has its own' \ - + ' set of download options.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_26(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial26'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'If you add download options to a folder, they apply to all' \ - + ' videos, channels and playlists inside that folder.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_27(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial27'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'You can change Tartube\'s own settings by clicking \'Edit' \ - + ' > System preferences...\'', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_28(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial28'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'The new window has many settings you can change. For example,' \ - + ' to create new databases, click the \'Files > Database\' tab.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_29(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial29'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'Once again, advanced users can reveal extra settings by' \ - + ' clicking this button.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_30(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial30'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'You can download video clips by right-clicking a video, then' \ - + ' selecting \'Special > Create video clips...\'', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_31(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial31'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'Of course, if you\'re using the Classic Mode tab, clips can' \ - + ' be downloaded without first adding the video to Tartube\'s' \ - + ' database.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_32(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial32'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'yt-dlp is updated frequently, so don\'t forget to download' \ - + ' the updates. Click \'Operations > Update yt-dlp\'.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_page_33(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - self.add_framed_image( - self.app_obj.main_win_obj.icon_dict['tutorial33'], - 0, 0, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 1, 1, 1) - - msg = _( - 'More help is available on our website. Click \'Help >' \ - + ' Go to website\'.', - ) - self.add_label( - '' \ - + utils.tidy_up_long_string(msg, self.text_len) + '', - 0, 2, 1, 1, - ) - - - def setup_finish_page(self): - - """Called by self.setup_page(). - - Sets up the widget layout for a page. - """ - - - self.add_image( - self.app_obj.main_win_obj.icon_dict['system_icon'], - 0, 0, 1, 1, - ) - - self.add_label( - '' \ - + _('Tartube Tutorial') + '', - 0, 1, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 2, 1, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _('That\'s the end of the tutorial.'), - self.text_len, - ) + '', - 0, 3, 1, 1, - ) - - # (Empty label for spacing) - self.add_empty_label(0, 4, 1, 1) - - self.add_label( - '' \ - + utils.tidy_up_long_string( - _( - 'You can read it again at any time by clicking' \ - + ' \'Help > Show tutorial...\' in Tartube\'s menu.', - ), - self.text_len, - ) + '', - 0, 5, 1, 1, - ) diff --git a/build/lib/tartube/xdg_tartube.py b/build/lib/tartube/xdg_tartube.py deleted file mode 100644 index 653b4127..00000000 --- a/build/lib/tartube/xdg_tartube.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright © 2016-2021 Scott Stevenson -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all -# copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL -# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE -# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL -# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR -# PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -# -# Tartube v2.0.0 -# Imported into Tartube from https://pypi.org/project/xdg/ and renamed, so -# that the Debian/RPM packagers don't confuse it with the standard xdg -# module -# Tartube v2.3.0 -# Updated imported code to v5.0.1 (Nov 13 2020) -# Tartube v2.4.204 -# Updated imported code to v5.1.1 (Jul 24 2021) - -"""XDG Base Directory Specification variables. - -xdg_cache_home(), xdg_config_home(), xdg_data_home(), and xdg_state_home() -return pathlib.Path objects containing the value of the environment variable -named XDG_CACHE_HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, and XDG_STATE_HOME -respectively, or the default defined in the specification if the environment -variable is unset, empty, or contains a relative path rather than absolute -path. - -xdg_config_dirs() and xdg_data_dirs() return a list of pathlib.Path -objects containing the value, split on colons, of the environment -variable named XDG_CONFIG_DIRS and XDG_DATA_DIRS respectively, or the -default defined in the specification if the environment variable is -unset or empty. Relative paths are ignored, as per the specification. - -xdg_runtime_dir() returns a pathlib.Path object containing the value of -the XDG_RUNTIME_DIR environment variable, or None if the environment -variable is not set, or contains a relative path rather than absolute path. - -""" - -# pylint: disable=fixme - -import os -from pathlib import Path -from typing import List, Optional - -__all__ = [ - "xdg_cache_home", - "xdg_config_dirs", - "xdg_config_home", - "xdg_data_dirs", - "xdg_data_home", - "xdg_runtime_dir", - "xdg_state_home", - "XDG_CACHE_HOME", - "XDG_CONFIG_DIRS", - "XDG_CONFIG_HOME", - "XDG_DATA_DIRS", - "XDG_DATA_HOME", - "XDG_RUNTIME_DIR", -] - - -def _path_from_env(variable: str, default: Path) -> Path: - """Read an environment variable as a path. - - The environment variable with the specified name is read, and its - value returned as a path. If the environment variable is not set, is - set to the empty string, or is set to a relative rather than - absolute path, the default value is returned. - - Parameters - ---------- - variable : str - Name of the environment variable. - default : Path - Default value. - - Returns - ------- - Path - Value from environment or default. - - """ - # TODO(srstevenson): Use assignment expression in Python 3.8. - value = os.environ.get(variable) - if value and os.path.isabs(value): - return Path(value) - return default - - -def _paths_from_env(variable: str, default: List[Path]) -> List[Path]: - """Read an environment variable as a list of paths. - - The environment variable with the specified name is read, and its - value split on colons and returned as a list of paths. If the - environment variable is not set, or set to the empty string, the - default value is returned. Relative paths are ignored, as per the - specification. - - Parameters - ---------- - variable : str - Name of the environment variable. - default : List[Path] - Default value. - - Returns - ------- - List[Path] - Value from environment or default. - - """ - # TODO(srstevenson): Use assignment expression in Python 3.8. - value = os.environ.get(variable) - if value: - paths = [ - Path(path) for path in value.split(":") if os.path.isabs(path) - ] - if paths: - return paths - return default - - -def xdg_cache_home() -> Path: - """Return a Path corresponding to XDG_CACHE_HOME.""" - return _path_from_env("XDG_CACHE_HOME", Path.home() / ".cache") - - -def xdg_config_dirs() -> List[Path]: - """Return a list of Paths corresponding to XDG_CONFIG_DIRS.""" - return _paths_from_env("XDG_CONFIG_DIRS", [Path("/etc/xdg")]) - - -def xdg_config_home() -> Path: - """Return a Path corresponding to XDG_CONFIG_HOME.""" - return _path_from_env("XDG_CONFIG_HOME", Path.home() / ".config") - - -def xdg_data_dirs() -> List[Path]: - """Return a list of Paths corresponding to XDG_DATA_DIRS.""" - return _paths_from_env( - "XDG_DATA_DIRS", - [Path(path) for path in "/usr/local/share/:/usr/share/".split(":")], - ) - - -def xdg_data_home() -> Path: - """Return a Path corresponding to XDG_DATA_HOME.""" - return _path_from_env("XDG_DATA_HOME", Path.home() / ".local" / "share") - - -def xdg_runtime_dir() -> Optional[Path]: - """Return a Path corresponding to XDG_RUNTIME_DIR. - - If the XDG_RUNTIME_DIR environment variable is not set, None will be - returned as per the specification. - - """ - value = os.getenv("XDG_RUNTIME_DIR") - if value and os.path.isabs(value): - return Path(value) - return None - - -def xdg_state_home() -> Path: - """Return a Path corresponding to XDG_STATE_HOME.""" - return _path_from_env("XDG_STATE_HOME", Path.home() / ".local" / "state") - - -# The following variables are deprecated, but remain for backward compatibility -XDG_CACHE_HOME = xdg_cache_home() -XDG_CONFIG_DIRS = xdg_config_dirs() -XDG_CONFIG_HOME = xdg_config_home() -XDG_DATA_DIRS = xdg_data_dirs() -XDG_DATA_HOME = xdg_data_home() -XDG_RUNTIME_DIR = xdg_runtime_dir() diff --git a/build/scripts-3.10/tartube b/build/scripts-3.10/tartube deleted file mode 100755 index bc08b54c..00000000 --- a/build/scripts-3.10/tartube +++ /dev/null @@ -1,167 +0,0 @@ -#!python -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2023 A S Lewis -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - - -"""Tartube main file.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import os -import sys -import threading -import traceback -import importlib.util - - -# Add module directory to path to prevent import issues -spec = importlib.util.find_spec('tartube') -if spec is not None: - sys.path.append(os.path.abspath(os.path.dirname(spec.origin))) - - -# Import our modules -import mainapp - - -# 'Global' variables -__packagename__ = 'tartube' -__version__ = '2.4.370' -__date__ = '13 Apr 2023' -__copyright__ = 'Copyright \xa9 2019-2023 A S Lewis' -__license__ = """ -Copyright \xa9 2019-2023 A S Lewis. - -This program is free software; you can redistribute it and/or modify it under -the terms of the GNU Lesser General Public License as published by the Free -Software Foundation; either version 2.1 of the License, or (at your option) any -later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -details. - -You should have received a copy of the GNU Lesser General Public License along -with this program. If not, see . -""" -__author_list__ = [ - 'A S Lewis', -] -__credit_list__ = [ - # (This list is formatted to suit Gtk.AboutDialog) - 'Partially based on youtube-dl-gui by MrS0m30n3', - 'https://github.com/MrS0m30n3/youtube-dl-gui', - 'FFmpeg thumbnail code adapted from youtube-dl', - 'http://youtube-dl.org/', - 'FFmpeg options adapted from FFmpeg Command', - 'Line Wizard by AndreKR', - 'https://github.com/AndreKR/ffmpeg-command-line-wizard', - 'Upgraded Textview by Kevin Mehall', - 'https://kevinmehall.net/2010/pygtk_multi_select_drag_drop', - 'XDG support by Scott Stevenson', - 'https://pypi.org/project/xdg/', -] -__description__ = 'GUI front-end for youtube-dl, yt-dlp and\nother' \ -+ ' compatible video downloaders' -__website__ = 'http://tartube.sourceforge.io' -__app_id__ = 'io.sourceforge.tartube' -__website_bugs__ = 'https://github.com/axcore/tartube' -__website_dev__ = 'http://raw.githubusercontent.com/axcore/tartube/master' -# Flag set to True if multiple instances of Tartube are allowed; False if -# only a single instance is allowed -__multiple_instance_flag__ = True -# There are four executables; this default one, and three others used in -# packaging. The others are identical, except for the values of these -# variables -__pkg_install_flag__ = False -__pkg_strict_install_flag__ = False -__pkg_no_download_flag__ = False - - -# Uncaught exception handling -def setup_thread_excepthook(): - - """Workaround for 'sys.excepthook' thread bug from: - http://bugs.python.org/issue1230540 - - Adapted from - https://stackoverflow.com/questions/1643327/sys-excepthook-and-threading - - Call once from the main thread before creating any threads. - """ - - init_original = threading.Thread.__init__ - - def init(self, *args, **kwargs): - - init_original(self, *args, **kwargs) - run_original = self.run - - def run_with_except_hook(*args2, **kwargs2): - try: - run_original(*args2, **kwargs2) - except Exception: - sys.excepthook(*sys.exc_info()) - - self.run = run_with_except_hook - - threading.Thread.__init__ = init - - -app = None -def handle_uncaught_exception(except_type, value, tb): - - """Intercepts uncaught exceptions, and diverts the message to the main - window's Errors/Warnings tab, so ordinary users can actually see them and - (hopefully) report them. - - Then raises the exception as normal. - - Adapted from - https://dev.to/joshuaschlichting/ - catching-every-single-exception-with-python-40o3 - - Args: - except_type (type): Exception type - value (TypeError): Exception value - tb (traceback): Exception traceback - - """ - - if app is not None: - - app.system_exception( - except_type, - value, - '\n'.join(traceback.extract_tb(tb).format()) - ) - - raise type(value) - - -setup_thread_excepthook() -sys.excepthook = handle_uncaught_exception - - -# Start Tartube -app = mainapp.TartubeApp() -app.run(sys.argv)