tartube/tartube/mainwin.py

35310 lines
1.1 MiB

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019-2022 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""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.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.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_box = None # Gkt.HBox
self.custom_dl_media_button = None # Gtk.Button
self.custom_dl_select_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_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
# (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_format_radiobutton = None # Gtk.RadioButton
self.classic_format_radiobutton2 = None # Gtk.RadioButton
self.classic_livestream_checkbutton = None
# Gtk.CheckButton
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_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
# 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 = name of the media data object (stored in its .name IV)
# 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 name 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 = 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
# 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
# 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 = {}
# 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
# 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.
"""
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':
# Separator
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')
# 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')
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())
self.switch_view_menu_item = \
Gtk.MenuItem.new_with_mnemonic(_('_Switch between views'))
media_sub_menu.append(self.switch_view_menu_item)
self.switch_view_menu_item.set_action_name('app.switch_view_menu')
# 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())
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.
"""
# 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 video(s)'))
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:
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.
"""
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.
"""
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_box = Gtk.HBox.new(False, self.spacing_size)
self.button_box.pack_start(self.custom_dl_box, False, False, 0)
self.custom_dl_media_button = Gtk.Button()
self.custom_dl_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',
)
self.custom_dl_select_button = Gtk.Button()
self.custom_dl_box.pack_start(
self.custom_dl_select_button,
False,
True,
0
)
self.custom_dl_select_button.set_label('+')
self.custom_dl_select_button.set_tooltip_text(
_('Select the custom download to use'),
)
self.custom_dl_select_button.set_action_name(
'app.custom_dl_select_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 up'))
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 down'))
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.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(' '))
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(' '))
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.catologue_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.
"""
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,
)
translate_note = _(
'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(20)
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)
# Limit the size of the 'Source' and 'Incoming file' columns. The
# others always contain few characters, so let them expand as they
# please
for column in [4, 7]:
column_obj = self.progress_list_treeview.get_column(column)
column_obj.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(20)
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)
# Limit the size of the 'New videos' column (the 'Downloaded to'
# column)
column_obj = self.results_list_treeview.get_column(3)
column_obj.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,
)
self.alt_limits_frame = Gtk.Frame()
grid.attach(self.alt_limits_frame, 4, 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()
grid.attach(self.video_res_checkbutton, 5, 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)
grid.attach(self.video_res_combobox, 6, 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 of the list'))
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,
)
def setup_classic_mode_tab(self):
"""Called by self.setup_win().
Creates widgets for the 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 = 8
# 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'))
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)
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, 5, 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, 6, 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, 7, 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)
# Video/audio format
label5 = Gtk.Label(_('Format:'))
grid.attach(label5, 0, 4, 1, 1)
label5.set_xalign(0)
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)
# (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)
# (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
self.classic_format_radiobutton = Gtk.RadioButton.new_with_label(
None,
_('Convert to this format'),
)
grid.attach(self.classic_format_radiobutton, 3, 4, 1, 1)
self.classic_format_radiobutton.set_hexpand(False)
# (Signal connect appears below)
self.classic_format_radiobutton2 \
= Gtk.RadioButton.new_with_label_from_widget(
self.classic_format_radiobutton,
_('Download in this format'),
)
grid.attach(self.classic_format_radiobutton2, 4, 4, 1, 1)
self.classic_format_radiobutton2.set_hexpand(True)
if not self.app_obj.classic_format_convert_flag:
self.classic_format_radiobutton2.set_active(True)
if not self.app_obj.classic_format_selection:
self.classic_format_radiobutton.set_sensitive(False)
self.classic_format_radiobutton2.set_sensitive(False)
self.classic_livestream_checkbutton = Gtk.CheckButton()
grid.attach(self.classic_livestream_checkbutton, 5, 4, 1, 1)
self.classic_livestream_checkbutton.set_label(_('Is a livestream'))
self.classic_livestream_checkbutton.set_hexpand(True)
if self.app_obj.classic_livestream_flag:
self.classic_livestream_checkbutton.set_active(True)
# (Signal connect appears below)
# (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_format_radiobutton.connect(
'toggled',
self.on_classic_format_radiobutton_toggled,
)
self.classic_livestream_checkbutton.connect(
'toggled',
self.on_classic_livestream_checkbutton_toggled,
)
# Add URLs button
self.classic_add_urls_button = Gtk.Button(
' ' + _('Add URLs') + ' ',
)
grid.attach(self.classic_add_urls_button, 6, 4, 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'))
# 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)
# Fourth 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(20)
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,
)
# Limit the size of the 'Source' and 'Incoming file' columns. The
# others always contain few characters, so let them expand as they
# please
for column in [2, 5]:
column_obj = self.classic_progress_treeview.get_column(column)
column_obj.set_fixed_width(200)
# Fifth 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_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,
self.spacing_size,
)
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, 0)
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,
self.spacing_size,
)
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, 0)
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.
"""
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(
'<b>' + _(
'When you drag a video here, it is added to the Classic Mode' \
+ ' tab',
) + '</b>',
)
label2 = Gtk.Label()
vbox2.pack_start(label2, True, True, 0)
label2.set_markup(
'<b>' + _(
'Each zone represents a set of download options',
) + '</b>',
)
if os.name == 'nt':
label3 = Gtk.Label()
vbox2.pack_start(label3, True, True, 0)
label3.set_markup(
'<b><i>' + _(
'Warning: Drag and drop does not work well on MS Windows',
) + '</i></b>',
)
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.
"""
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.
"""
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_box:
self.custom_dl_media_button.set_sensitive(False)
self.custom_dl_select_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:
self.video_catalogue_redraw_all(
self.video_index_current,
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_box:
self.custom_dl_media_button.set_sensitive(True)
self.custom_dl_select_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:
self.video_catalogue_redraw_all(
self.video_index_current,
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
"""
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_box:
self.button_box.remove(self.custom_dl_box)
self.custom_dl_box = None
self.custom_dl_media_button = None
self.custom_dl_select_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_box = Gtk.HBox.new(False, self.spacing_size)
self.button_box.pack_start(self.custom_dl_box, False, False, 0)
self.custom_dl_media_button = Gtk.Button()
self.custom_dl_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',
)
self.custom_dl_select_button = Gtk.Button()
self.custom_dl_box.pack_start(
self.custom_dl_select_button,
False,
True,
0
)
self.custom_dl_select_button.set_label('+')
self.custom_dl_select_button.set_tooltip_text(
_('Select the custom download to use'),
)
self.custom_dl_select_button.set_action_name(
'app.custom_dl_select_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
"""
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_box:
self.button_box.remove(self.custom_dl_box)
self.custom_dl_box = None
self.custom_dl_media_button = None
self.custom_dl_select_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_box = Gtk.HBox.new(False, self.spacing_size)
self.button_box.pack_start(self.custom_dl_box, False, False, 0)
self.custom_dl_media_button = Gtk.Button()
self.custom_dl_box.pack_start(
self.custom_dl_media_button,
True,
True,
0
)
self.custom_dl_media_button.set_sensitive(False)
self.custom_dl_select_button = Gtk.Button()
# (Aesthetics are better, when the '+' button is not visible at
# all)
if finish_flag:
self.custom_dl_box.pack_start(
self.custom_dl_select_button,
False,
True,
0
)
self.custom_dl_select_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_box:
self.custom_dl_media_button.set_label(msg)
self.custom_dl_select_button.set_label('+')
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_box:
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_select_button.set_label('+')
self.custom_dl_select_button.set_tooltip_text(
_('Select the custom download to use'),
)
if __main__.__pkg_no_download_flag__ \
or self.app_obj.disable_dl_all_flag:
self.custom_dl_media_button.set_sensitive(False)
self.custom_dl_select_button.set_sensitive(False)
else:
self.custom_dl_media_button.set_sensitive(True)
self.custom_dl_select_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 corresponding buttons in the Classic Mode tab must also be
# updated
self.classic_stop_button.set_sensitive(not 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_box:
self.custom_dl_media_button.set_sensitive(False)
self.custom_dl_select_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_box:
self.custom_dl_media_button.set_sensitive(False)
self.custom_dl_select_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_box:
self.custom_dl_media_button.set_sensitive(sens_flag)
self.custom_dl_select_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)
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_box:
if __main__.__pkg_no_download_flag__ \
or self.app_obj.disable_dl_all_flag:
self.custom_dl_media_button.set_sensitive(False)
self.custom_dl_select_button.set_sensitive(False)
else:
self.custom_dl_media_button.set_sensitive(sens_flag)
self.custom_dl_select_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_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
"""
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_box:
self.button_box.remove(self.custom_dl_box)
self.custom_dl_box = None
self.custom_dl_media_button = None
self.custom_dl_select_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_box = Gtk.HBox.new(False, self.spacing_size)
self.button_box.pack_start(self.custom_dl_box, False, False, 0)
self.custom_dl_media_button = Gtk.Button()
self.custom_dl_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)
self.custom_dl_select_button = Gtk.Button()
# (Aesthetics are better, when the '+' button is not visible at
# all)
# self.custom_dl_box.pack_start(
# self.custom_dl_select_button,
# False,
# True,
# 0
# )
self.custom_dl_select_button.set_label('+')
self.custom_dl_select_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 media_name in self.app_obj.media_name_dict.keys():
dbid = self.app_obj.media_name_dict[media_name]
if dbid in this_dict:
self.video_index_set_marker(media_name)
else:
self.video_index_reset_marker(media_name)
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
"""
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.
"""
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_SORT_ASCENDING,
)
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 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_SORT_DESCENDING,
)
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 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 by upload time (default),
or alphabetically. On startup, set the correct value of the combobox.
"""
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_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.
"""
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(
'<b>' + _(
'This tab emulates the classic youtube-dl-gui interface',
) + '</b>',
)
self.classic_banner_label2.set_markup(
'<b>' + _(
'Videos downloaded here are not added to Tartube\'s' \
+ ' database',
) + '</b>',
)
else:
self.classic_banner_img.set_from_pixbuf(
self.pixbuf_dict['warning_large'],
)
self.classic_banner_label.set_markup(
'<b>' + _(
'If your preferred formats are not available, the' \
+ ' download will fail!',
) + '</b>',
)
self.classic_banner_label2.set_markup(
'<b>' + _(
'If you want a specific format, install FFMpeg and' \
+ ' select \'Convert to this format\'!',
) + '</b>',
)
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().
"""
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.
"""
# 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 (temporarily)
selection = self.video_index_treeview.get_selection()
selection.set_mode(Gtk.SelectionMode.NONE)
# Show/hide system folders
for name in self.app_obj.media_name_dict:
dbid = self.app_obj.media_name_dict[name]
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 is not None:
dbid = self.app_obj.media_name_dict[self.video_index_current]
media_data_obj = self.app_obj.media_reg_dict[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
"""
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.
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
Returns:
-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 names of the media data objects on each row
sort_column, sort_type \
= self.video_index_sortmodel.get_sort_column_id()
name1 = treestore.get_value(row_iter1, sort_column)
name2 = treestore.get_value(row_iter2, sort_column)
# Get corresponding media data objects
id1 = self.app_obj.media_name_dict[name1]
obj1 = self.app_obj.media_reg_dict[id1]
id2 = self.app_obj.media_name_dict[name2]
obj2 = self.app_obj.media_reg_dict[id2]
# Do 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
Returns:
-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
Returns:
-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, name):
"""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
name (str): The name of the clicked media data object
"""
# Find the right-clicked media data object (and a string to describe
# its type)
dbid = self.app_obj.media_name_dict[name]
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.name in self.app_obj.media_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_submenu = self.custom_dl_popup_submenu([ media_data_obj ])
custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
custom_dl_menu_item.set_submenu(custom_dl_submenu)
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)
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,
)
actions_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)
# 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)
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.name 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)
# Separator
downloads_submenu.append(Gtk.SeparatorMenuItem())
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
"""
# 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.name in self.app_obj.media_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_submenu = self.custom_dl_popup_submenu([ video_obj ])
custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('C_ustom download video')
)
custom_dl_menu_item.set_submenu(custom_dl_submenu)
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)
translate_note = _(
'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()
if video_obj.dl_flag:
clip_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('_Create video clip...'),
)
else:
clip_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('Download video clip...'),
)
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())
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)
"""
# 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.name \
in self.app_obj.media_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
"""
# 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
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 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')
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:
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(
_('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
"""
# 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,
)
# 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.
"""
# 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
"""
# 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)
# 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_path_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('_Copy 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)
popup_menu.append(copy_path_menu_item)
# Copy URL
copy_url_menu_item = Gtk.MenuItem.new_with_mnemonic(_('Copy UR_L'))
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)
popup_menu.append(copy_url_menu_item)
# Copy system command
copy_cmd_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('Copy s_ystem 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)
popup_menu.append(copy_cmd_menu_item)
# Separator
popup_menu.append(Gtk.SeparatorMenuItem())
# Move up
move_up_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('Move _up'),
)
move_up_menu_item.connect(
'activate',
self.on_classic_progress_list_from_popup,
'move_up',
video_list,
)
popup_menu.append(move_up_menu_item)
# Move down
move_down_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('Move _down'),
)
move_down_menu_item.connect(
'activate',
self.on_classic_progress_list_from_popup,
'move_down',
video_list,
)
popup_menu.append(move_down_menu_item)
# Separator
popup_menu.append(Gtk.SeparatorMenuItem())
# 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
"""
# 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)
translate_note = _(
'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):
"""Called by mainapp.TartubeApp.on_menu_custom_dl_select().
When the user right-clicks the custom download manager selection button
in the Videos tab, shows a context-sensitive popup menu.
"""
# Set up the popup menu
popup_menu = self.custom_dl_popup_submenu()
# 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
"""
# 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
"""
# 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
"""
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
"""
video_index_current = self.video_index_current
# 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 video_index_current is not None:
dbid = self.app_obj.media_name_dict[video_index_current]
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 = 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)
# (Tooltips are initially enabled, and if necessary are disabled by a
# call to self.disable_tooltips() shortly afterwards)
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)
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.media_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.name in self.video_index_marker_dict \
and not media_data_obj.name 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.name] = tree_ref
if media_data_obj.name in self.video_index_marker_dict:
self.video_index_marker_dict[media_data_obj.name] = tree_ref
if media_data_obj.name in self.video_index_old_marker_dict:
del self.video_index_old_marker_dict[media_data_obj.name]
# 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.name in self.video_index_marker_dict \
and not media_data_obj.name 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.name]
# ...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.name] = tree_ref
if media_data_obj.name in self.video_index_marker_dict:
self.video_index_marker_dict[media_data_obj.name] = tree_ref
if media_data_obj.name in self.video_index_old_marker_dict:
del self.video_index_old_marker_dict[media_data_obj.name]
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.name]
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 is not None \
and self.video_index_current == media_data_obj.name:
selection = self.video_index_treeview.get_selection()
selection.unselect_all()
self.video_index_current = None
self.video_catalogue_reset()
# Update IVs
if media_data_obj.name in self.video_index_marker_dict:
del self.video_index_marker_dict[media_data_obj.name]
# 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.name]
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.name]
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
# Update the treeview row
tree_ref = self.video_index_row_dict[media_data_obj.name]
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()
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
# Update the treeview row
tree_ref = self.video_index_row_dict[media_data_obj.name]
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()
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
# Update the treeview row
tree_ref = self.video_index_row_dict[media_data_obj.name]
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()
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
Returns:
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.name in self.app_obj.media_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
Returns:
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:
translate_note = _(
'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',
)
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.name in self.app_obj.media_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, name=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:
name (str): The name of a media.Channel, media.Playlist or
media.Folder; a key in mainapp.TartubeApp.media_name_dict
"""
old_size = len(self.video_index_marker_dict)
container_list = []
if name is None:
# Set all markers in the Video Index
container_list = self.video_index_row_dict.keys()
else:
# Set the marker on the row for the specified channel/playlist/
# folder
container_list = [ name ]
for this_name in container_list:
# System folders cannot be marked
# Channels/playlists/folders for which checking and downloading is
# disabled can't be marked
dbid = self.app_obj.media_name_dict[this_name]
media_data_obj = self.app_obj.media_reg_dict[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_name]
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_name] = 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, name=None):
"""Can be called by anything.
Resets the marker on a specified row of the Video Index (or on all of
them).
Args:
name (str): The name of a media.Channel, media.Playlist or
media.Folder; a key in mainapp.TartubeApp.media_name_dict
"""
old_size = len(self.video_index_marker_dict)
if name 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 name in self.app_obj.media_name_dict:
# Reset the marker on the row for the specified channel/playlist/
# folder
tree_ref = self.video_index_row_dict[name]
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 name in self.video_index_marker_dict:
del self.video_index_marker_dict[name]
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)
def video_index_update_marker(self, old_name, new_name):
"""Called my mainapp.TartubeApp.rename_container() and
.rename_container_silently().
When a container is renamed, update the IV which keeps track of Video
Index markers, as it stores the container's name as a key.
Args:
old_name, new_name (str): The names of the container
"""
if old_name in self.video_index_marker_dict:
old_size = len(self.video_index_marker_dict)
self.video_index_marker_dict[new_name] = \
self.video_index_marker_dict[old_name]
del self.video_index_marker_dict[old_name]
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, name, 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:
name (str): The selected media data object's name; one of the keys
in self.media_name_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 is None \
or self.video_index_current != name \
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 name in self.app_obj.media_name_dict:
return self.app_obj.system_error(
215,
'Cannot redraw Video Catalogue because container is missing' \
+ ' from database',
)
dbid = self.app_obj.media_name_dict[name]
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',
)
# The Video Catalogue can be sorted in one of four modes. If the
# selected container was last sorted when the mode was different,
# then trigger a re-sort
# (Sorting only when we need to prevents the need to resort every
# container in the database, whenever the user changes the mode)
if self.app_obj.catalogue_sort_mode != container_obj.last_sort_mode:
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 is None \
or not (
self.video_index_current == video_obj.parent_obj.name
or self.video_index_current == app_obj.fixed_all_folder.name
or (
self.video_index_current \
== app_obj.fixed_bookmark_folder.name \
and video_obj.bookmark_flag
) or (
self.video_index_current == app_obj.fixed_fav_folder.name \
and video_obj.fav_flag
) or (
self.video_index_current == app_obj.fixed_live_folder.name \
and video_obj.live_mode
) or (
self.video_index_current == app_obj.fixed_missing_folder.name
and video_obj.missing_flag
) or (
self.video_index_current == app_obj.fixed_new_folder.name
and video_obj.new_flag
) or (
self.video_index_current == app_obj.fixed_recent_folder.name
and video_obj in app_obj.fixed_recent_folder.child_list
) or (
self.video_index_current == app_obj.fixed_waiting_folder.name \
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_dbid = app_obj.media_name_dict[self.video_index_current]
container_obj = app_obj.media_reg_dict[container_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 is None:
return
elif self.video_index_current != video_obj.parent_obj.name \
and self.video_index_current != app_obj.fixed_all_folder.name \
and (
self.video_index_current != app_obj.fixed_bookmark_folder.name \
or not video_obj.bookmark_flag
) and (
self.video_index_current != app_obj.fixed_fav_folder.name \
or not video_obj.fav_flag
) and (
self.video_index_current != app_obj.fixed_live_folder.name \
or not video_obj.live_mode
) and (
self.video_index_current != app_obj.fixed_missing_folder.name \
or not video_obj.missing_flag
) and (
self.video_index_current != app_obj.fixed_new_folder.name \
or not video_obj.new_flag
) and (
self.video_index_current != app_obj.fixed_recent_folder.name \
or video_obj in app_obj.fixed_recent_folder.child_list
) and (
self.video_index_current != app_obj.fixed_waiting_folder.name \
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
dbid = app_obj.media_name_dict[self.video_index_current]
container_obj = app_obj.media_reg_dict[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 is None:
return
else:
dbid = self.app_obj.media_name_dict[self.video_index_current]
container_obj = self.app_obj.media_reg_dict[dbid]
# Force the resort
container_obj.sort_children(self.app_obj)
# Redraw the Video Catalogue, switching to the first page
self.video_catalogue_redraw_all(
self.video_index_current,
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 fouind 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_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_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_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 is not None:
dbid = self.app_obj.media_name_dict[self.video_index_current]
parent_obj = self.app_obj.media_reg_dict[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.catologue_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,
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)
# 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 is not None:
dbid = self.app_obj.media_name_dict[self.video_index_current]
parent_obj = self.app_obj.media_reg_dict[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,
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 is not None:
dbid = self.app_obj.media_name_dict[self.video_index_current]
parent_obj = self.app_obj.media_reg_dict[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,
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 = {}
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:
return self.app_obj.system_error(
222,
'Missing row in Progress List',
)
# 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 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
# Instead of overwriting the filename, when the download concludes,
# 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_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:
string = string + '/' \
+ str(dl_stat_dict['playlist_size'])
else:
string = string + '/1'
else:
string = dl_stat_dict[key]
try:
tree_iter = self.progress_list_liststore.get_iter(
tree_path
)
self.progress_list_liststore.set(
tree_iter,
column,
string,
)
except:
return
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:
return self.app_obj.system_error(
999,
'Cannot remove row in Progress List (row does not exist)',
)
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,
)
# (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
),
),
)
# (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('^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('^\s', format_str):
format_str = None
else:
format_str = re.sub('^\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('^\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 radiobutton 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
if format_str is not None \
and self.classic_format_radiobutton.get_active():
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,
)
url_list = url_string.splitlines()
# 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
)
def classic_mode_tab_create_dummy_video(self, url, dest_dir, \
format_str=None):
"""Called by self.classic_mode_tab_add_urls() 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
Returns:
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 self.app_obj.classic_livestream_flag:
new_obj.set_live_mode(2)
# 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,
)
url_list = url_string.splitlines()
# 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_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:
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,
)
# (Drag and Drop tab)
def drag_drop_grid_reset(self):
"""Can be called by anything.
Draws a grid of mainwin.DropZoneBox 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()
translate_note = _(
'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
Returns:
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 = '<span color="{:s}">' \
+ GObject.markup_escape_text(msg) + '</span>\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.
"""
# 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
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
"""
# 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
"""
# 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.name 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 not 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 dialouge window
# opened
if choice is not None \
and media_data_obj.name in self.app_obj.media_name_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.name)
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 that are not already
# attached to a media data object, 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 options_obj.dbid is None:
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 (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_name = model[drop_iter][1]
if dest_name is None or dest_name == '':
return
else:
dest_id = self.app_obj.media_name_dict[dest_name]
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)
dest_id = self.app_obj.media_name_dict[dest_name]
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_name = model[start_iter][1]
if drag_name is None or drag_name == '':
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
drag_id = self.app_obj.media_name_dict[drag_name]
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.split('\n'):
# 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
# with the same parent folder
for line in video_list:
if media_data_obj.check_duplicate_video(line):
duplicate_list.append(line)
else:
self.app_obj.add_video(media_data_obj, line)
# 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_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_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_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_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:
self.video_index_popup_menu(
event,
self.video_index_sortmodel[tree_iter][1],
)
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:
name = model[tree_iter][1]
# 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 = None
self.video_catalogue_reset()
else:
# Update IVs
self.video_index_current = name
dbid = self.app_obj.media_name_dict[name]
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(
name,
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.name]
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.name in self.video_index_marker_dict:
del self.video_index_marker_dict[media_data_obj.name]
else:
self.video_index_marker_dict[media_data_obj.name] \
= self.video_index_row_dict[media_data_obj.name]
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.name]
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.name in self.video_index_marker_dict:
del self.video_index_marker_dict[media_data_obj.name]
else:
self.video_index_marker_dict[media_data_obj.name] \
= self.video_index_row_dict[media_data_obj.name]
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
"""
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.name \
in self.app_obj.media_unavailable_dict:
self.app_obj.del_media_unavailable_dict(
media_data_obj.name,
)
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.name \
in self.app_obj.media_unavailable_dict:
self.app_obj.del_media_unavailable_dict(
media_data_obj.name,
)
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(),
'del_archive_flag': dialogue_win.checkbutton6.get_active(),
'move_thumb_flag': dialogue_win.checkbutton7.get_active(),
'del_thumb_flag': dialogue_win.checkbutton8.get_active(),
'convert_webp_flag': dialogue_win.checkbutton9.get_active(),
'move_data_flag': dialogue_win.checkbutton10.get_active(),
'del_descrip_flag': dialogue_win.checkbutton11.get_active(),
'del_json_flag': dialogue_win.checkbutton12.get_active(),
'del_xml_flag': dialogue_win.checkbutton13.get_active(),
}
# Now destroy the window
dialogue_win.destroy()
if response == Gtk.ResponseType.OK:
# If nothing was selected, then there is nothing to do
# (Don't need to check 'del_others_flag' here)
if not choices_dict['corrupt_flag'] \
and not choices_dict['exist_flag'] \
and not choices_dict['del_video_flag'] \
and not choices_dict['del_thumb_flag'] \
and not choices_dict['convert_webp_flag'] \
and not choices_dict['del_descrip_flag'] \
and not choices_dict['del_json_flag'] \
and not choices_dict['del_xml_flag'] \
and not choices_dict['del_archive_flag'] \
and not choices_dict['move_thumb_flag'] \
and not choices_dict['move_data_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_thumb_flag'] \
or choices_dict['del_descrip_flag'] \
or choices_dict['del_json_flag'] \
or choices_dict['del_xml_flag'] \
or choices_dict['del_archive_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 media_data_obj.source:
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 that are not already
# attached to a media data object, 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 options_obj.dbid is None:
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
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_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 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,
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.
Args:
menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video): The clicked video object
"""
# Prompt the user for start/stop timestamps and a clip title
dialogue_win = PrepareClipDialogue(self, media_data_obj)
response = dialogue_win.run()
# Get the specified timestamps/clip title, before destroying the window
start_stamp = utils.strip_whitespace(dialogue_win.start_stamp)
stop_stamp = utils.strip_whitespace(dialogue_win.stop_stamp)
clip_title = utils.strip_whitespace(dialogue_win.clip_title)
all_flag = dialogue_win.all_flag
dialogue_win.destroy()
if response != Gtk.ResponseType.CANCEL \
and response != Gtk.ResponseType.DELETE_EVENT:
if not all_flag:
# Check timestamps are valid. 'stop_stamp' and 'clip_title' are
# optional, and default to None
if stop_stamp == '':
stop_stamp = None
if clip_title == '':
clip_title = None
regex = r'^' + self.app_obj.timestamp_regex + r'$'
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
# Store the values in a temporary buffer, so that download/
# process operations can retrieve them
self.app_obj.set_temp_stamp_list([
[ start_stamp, stop_stamp, clip_title ],
])
else:
# Download clips for all timestamps in the media.Video's
# .stamp_list, ignoring any timestamps/clip titles the user
# just entered in the dialogue window
self.app_obj.set_temp_stamp_list(media_data_obj.stamp_list)
if not media_data_obj.dl_flag:
# 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 ],
)
else:
# 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 ],
)
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
"""
# 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.
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
start_time = utils.strip_whitespace(dialogue_win.start_time)
stop_time = utils.strip_whitespace(dialogue_win.stop_time)
all_flag = dialogue_win.all_flag
all_but_flag = dialogue_win.all_but_flag
dialogue_win.destroy()
if response != Gtk.ResponseType.CANCEL \
and response != Gtk.ResponseType.DELETE_EVENT:
if not all_flag:
# 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__()
# Then store the values in a temporary buffer, so that
# download/process operations can retrieve them
if not all_but_flag:
mini_dict = {
'category': 'sponsor',
'action': 'skip',
'start_time': start_time,
'stop_time': stop_time,
'duration': 0,
}
self.app_obj.set_temp_slice_list([ mini_dict ])
elif start_time > 0 and stop_time is not None:
mini_dict = {
'category': 'sponsor',
'action': 'skip',
'start_time': 0,
'stop_time': start_time,
'duration': 0,
}
mini_dict2 = {
'category': 'sponsor',
'action': 'skip',
'start_time': stop_time,
'stop_time': None,
'duration': 0,
}
self.app_obj.set_temp_slice_list([ mini_dict, mini_dict2 ])
elif start_time > 0 and stop_time is None:
mini_dict = {
'category': 'sponsor',
'action': 'skip',
'start_time': 0,
'stop_time': start_time,
'duration': 0,
}
self.app_obj.set_temp_slice_list([ mini_dict ])
else:
mini_dict = {
'category': 'sponsor',
'action': 'skip',
'start_time': stop_time,
'stop_time': None,
'duration': 0,
}
self.app_obj.set_temp_slice_list([ mini_dict ])
else:
# Use all slices in the media.Video's .slice_list, ignoring any
# times the user just entered in the dialogue window
self.app_obj.set_temp_slice_list(media_data_obj.slice_list)
if not media_data_obj.dl_flag:
# 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 ],
)
else:
# 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 ],
)
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
"""
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.set_options_obj(None)
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():
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 is not None:
self.video_catalogue_redraw_all(self.video_index_current, 1)
else:
# Invalid page size, so reinsert the size that's already visible
entry.set_text(str(self.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_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 is not None:
self.video_catalogue_redraw_all(
self.video_index_current,
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 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,
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('\S', source) or re.search('\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),
2,
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),
2,
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): 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
# Tell the video downloader to stop after the current video check/
# download has finished
downloader_obj.stop_soon()
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
string = ''
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]
# If all three are 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 != '':
string += file_path + '\n' + source + '\n' + name + '\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(string, -1)
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
"""
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_format_radiobutton.set_sensitive(False)
self.classic_format_radiobutton2.set_sensitive(False)
else:
self.classic_format_radiobutton.set_sensitive(True)
self.classic_format_radiobutton2.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_format_radiobutton_toggled(self, radiobutton):
"""Called from callback in self.setup_classic_mode_tab().
(De)sensitises radiobuttons, and updates IVs.
Args:
radiobutton (Gtk.RadioButton): The widget clicked
"""
if radiobutton.get_active():
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_livestream_checkbutton_toggled(self, checkbutton):
"""Called from callback in self.setup_classic_mode_tab().
Updates IVs.
Args:
radiobutton (Gtk.RadioButton): 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.disapply_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
"""
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', '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 == '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_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_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('^\S*$', text)) \
and not (re.search('\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_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)
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_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).
"""
# 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,
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,
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,
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 \
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_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
# 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.split('\n'):
# Remove leading/trailing whitespace
line = utils.strip_whitespace(line)
match = re.search('^file\:\/\/(.*)', line)
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 line in url_list:
if utils.check_url(line):
url_list.append(line)
else:
duplicate_list.append(line)
# 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 visible, 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 visible. 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 visible
parent_obj = None
if self.video_index_current is not None:
dbid \
= self.app_obj.media_name_dict[self.video_index_current]
parent_obj = self.app_obj.media_reg_dict[dbid]
if 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:
msg = _('The following items are duplicates:')
for line in duplicate_list:
msg += '\n\n' + line
self.app_obj.dialogue_manager_obj.show_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 \
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_path_flag:
if not dummy_flag and video_obj.file_name is not None:
text += video_obj.get_actual_path(self.app_obj)
elif dummy_flag and video_obj.dummy_path is not None:
text += video_obj.dummy_path
text += '\n'
if self.app_obj.drag_video_source_flag:
if video_obj.source is not None:
text += video_obj.source
text += '\n'
if self.app_obj.drag_video_name_flag:
if video_obj.name is not None:
text += video_obj.name
text += '\n'
if self.app_obj.drag_thumb_path_flag:
thumb_path = utils.find_thumbnail(
self.app_obj,
video_obj,
True,
)
if thumb_path is not None:
text += thumb_path
text += '\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):
# 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(
'<span font_size="large"' + string + '>' + \
html.escape(
utils.shorten_string(
name,
self.main_win_obj.very_long_string_max_len,
),
quote=True,
) + '</span>'
)
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('<i>' + string + '</i>: ' + 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:') + ' <i>' + _('unknown') + '</i>'
size = self.video_obj.get_file_size_string()
if size != "":
msg += ' - ' + _('Size:') + ' ' + size
else:
msg += ' - ' + _('Size:') + ' <i>' + _('unknown') + '</i>'
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:') + ' <i>' + _('unknown') + '</i>'
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(
'<span font_size="large"' + string + '>' + \
html.escape(
utils.shorten_string(
name,
self.main_win_obj.quite_long_string_max_len,
),
quote=True,
) + '</span>'
)
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.split('\n')
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(
'<a href="more" title="' \
+ _('Show the full description') \
+ '">' + _('More') + '</a> ' + 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(
'<a href="less" title="' \
+ _('Show the short description') \
+ '">' + _('Less') + '</a> ' + descrip + '\n',
)
else:
self.descrip_label.set_text(descrip)
else:
self.descrip_label.set_markup('<i>No description set</i>')
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 = '<i>'
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 += '</i>: ' + 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_text(string)
elif not self.expand_descrip_flag:
self.descrip_label.set_markup(
'<a href="more" title="' \
+ _('Show the full description') \
+ '">' + _('More') + '</a> ' + string,
)
else:
descrip = html.escape(self.video_obj.descrip, quote=True)
self.descrip_label.set_markup(
'<a href="less" title="' \
+ _('Show the short description') \
+ '">' + _('Less') + '</a> ' + 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:') + ' <i>' + _('unknown') + '</i>'
size = self.video_obj.get_file_size_string()
if size != "":
string = string + ' - ' + _('Size:') + ' ' + size
else:
string = string + ' - ' + _('Size:') + ' <i>' \
+ _('unknown') + '</i>'
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 + ' <i>' + _('unknown') \
+ '</i>'
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 = '<s>' + _('Notify') + '</s>'
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(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, notify the user') \
+ '">' + label + '</a>',
)
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 = '<s>' + _('Alarm') + '</s>'
else:
label = _('Alarm')
self.live_auto_alarm_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, sound an alarm') \
+ '">' + label + '</a>',
)
if dbid in app_obj.media_reg_auto_open_dict:
label = '<s>' + _('Open') + '</s>'
else:
label = _('Open')
self.live_auto_open_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, open it') \
+ '">' + label + '</a>',
)
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 = '<s>' + _('D/L on start') + '</s>'
else:
label = _('D/L on start')
self.live_auto_dl_start_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, download it') \
+ '">' + label + '</a>',
)
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 = '<s>' + _('D/L on stop') + '</s>'
else:
label = _('D/L on stop')
self.live_auto_dl_stop_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream stops, download it') \
+ '">' + label + '</a>',
)
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 = '<a href="' + html.escape(
self.video_obj.get_actual_path(self.main_win_obj.app_obj),
) + '" title="' + _('Watch in your media player') + '">' \
+ _('Player') + '</a>'
# (Many labels are not clickable when a channel/playlist/folder's
# external directory is marked disabled)
if self.video_obj.parent_obj.name \
in self.main_win_obj.app_obj.media_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(
'<a href="' + html.escape(self.video_obj.source) \
+ '" title="' + _('Download this video') + '">' \
+ _('Download') + '</a>',
)
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:
translate_note = _(
'TRANSLATOR\'S NOTE: If you want to use &, use &amp;' \
+ ' - 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(
'<a href="' + html.escape(self.video_obj.source) \
+ '" title="' + _('Download and watch in your media player') \
+ '">' + _('Download &amp; watch') + '</a>',
)
else:
# Link not clickable
self.watch_player_label.set_markup(
'<i>' + _('Not downloaded') + '</i>',
)
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(
'<a href="' + html.escape(source, quote=True) \
+ '" title="' + _('Watch on website') + '">' \
+ _('Website') + '</a>',
)
# 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(
'<a href="' + html.escape(source, quote=True) \
+ '" title="' + _('Watch on {0}').format(pretty) + '">' \
+ pretty + '</a>',
)
# 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(
'<a href="' + html.escape(source, quote=True) \
+ '" title="' + _('Watch on YouTube') + '">' \
+ _('YouTube') + '</a>',
)
if not self.video_obj.live_mode:
# Links clickable
self.watch_hooktube_label.set_markup(
'<a href="' \
+ html.escape(
utils.convert_youtube_to_hooktube(source),
quote=True,
) \
+ '" title="' + _('Watch on HookTube') + '">' \
+ _('HookTube') + '</a>',
)
self.watch_invidious_label.set_markup(
'<a href="' \
+ html.escape(
utils.convert_youtube_to_invidious(
app_obj,
source,
),
quote=True,
) \
+ '" title="' + _('Watch on Invidious') + '">' \
+ _('Invidious') + '</a>',
)
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(
'<a href="' \
+ html.escape(
utils.convert_youtube_to_other(
app_obj,
source,
),
quote=True,
) \
+ '" title="' \
+ app_obj.custom_dl_obj.divert_website \
+ '">' + _('Other') + '</a>',
)
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('<i>' + _('No link') + '</i>')
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.name \
in self.main_win_obj.app_obj.media_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 = '<s>' + _('Notify') + '</s>'
# 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(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, notify the user') \
+ '">' + label + '</a>',
)
# Sound alarm/don't sound alarm
if not dbid in app_obj.media_reg_auto_alarm_dict:
label = _('Alarm')
else:
label = '<s>' + _('Alarm') + '</s>'
if unavailable_flag:
self.live_auto_alarm_label.set_markup(_('Alarm'))
else:
self.live_auto_alarm_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, sound an alarm') \
+ '">' + label + '</a>',
)
# Open/don't open
if not dbid in app_obj.media_reg_auto_open_dict:
label = _('Open')
else:
label = '<s>' + _('Open') + '</s>'
if unavailable_flag:
self.live_auto_open_label.set_markup(_('Open'))
else:
self.live_auto_open_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, open it') \
+ '">' + label + '</a>',
)
# 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 = '<s>' + _('D/L on start') + '</s>'
if unavailable_flag:
self.live_auto_dl_start_label.set_markup(_('D/L on start'))
else:
self.live_auto_dl_start_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, download it') \
+ '">' + label + '</a>',
)
# 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 = '<s>' + _('D/L on stop') + '</s>'
if unavailable_flag:
self.live_auto_dl_stop_label.set_markup(_('D/L on stop'))
else:
self.live_auto_dl_stop_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream stops, download it') \
+ '">' + label + '</a>',
)
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.name \
in self.main_win_obj.app_obj.media_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(
'<a href="' + html.escape(link_text) + '" title="' \
+ _('Download to a temporary folder later') \
+ '">' + _('Mark for download') + '</a>',
)
self.temp_dl_label.set_markup(
'<a href="' + html.escape(link_text) + '" title="' \
+ _('Download to a temporary folder') \
+ '">' + _('Download') + '</a>',
)
self.temp_dl_watch_label.set_markup(
'<a href="' + html.escape(link_text) + '" title="' \
+ _('Download to a temporary folder, then watch') \
+ '">' + _('D/L &amp; watch') + '</a>',
)
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 = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Prevent automatic deletion of the video') + '">'
if not self.video_obj.archive_flag:
self.marked_archive_label.set_markup(
text + _('Archived') + '</a>',
)
else:
self.marked_archive_label.set_markup(
text + '<s>' + _('Archived') + '</s></a>',
)
# Bookmarked/not bookmarked
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Show video in Bookmarks folder') + '">'
if not self.video_obj.bookmark_flag:
self.marked_bookmark_label.set_markup(
# text + _('Bookmarked') + '</a>',
text + _('B/mark') + '</a>',
)
else:
self.marked_bookmark_label.set_markup(
# text + '<s>' + _('Bookmarked') + '</s></a>',
text + '<s>' + _('B/mark') + '</s></a>',
)
# Favourite/not favourite
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Show in Favourite Videos folder') + '">'
if not self.video_obj.fav_flag:
self.marked_fav_label.set_markup(
text + _('Favourite') + '</a>',
)
else:
self.marked_fav_label.set_markup(
text + '<s>' + _('Favourite') + '</s></a>')
# Missing/not missing
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Mark video as removed by creator') + '">'
if not self.video_obj.missing_flag:
self.marked_missing_label.set_markup(
text + _('Missing') + '</a>',
)
else:
self.marked_missing_label.set_markup(
text + '<s>' + _('Missing') + '</s></a>',
)
# New/not new
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Mark video as never watched') + '">'
if not self.video_obj.new_flag:
self.marked_new_label.set_markup(
text + _('New') + '</a>',
)
else:
self.marked_new_label.set_markup(
text + '<s>' + _('New') + '</s></a>',
)
# In waiting list/not in waiting list
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Show in Waiting Videos folder') + '">'
if not self.video_obj.waiting_flag:
self.marked_waiting_label.set_markup(
text + _('Waiting') + '</a>',
)
else:
self.marked_waiting_label.set_markup(
text + '<s>' + _('Waiting') + '</s></a>',
)
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.
Returns:
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.
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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 2020, the youtube-dl --yes-overwrites option is still
# not available, so as a temporary measure 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
Returns:
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(
'<span font_size="large"' + string + '>' + \
html.escape(
utils.shorten_string_two_lines(
name,
max_line_length,
),
quote=True,
) + '</span>'
)
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.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(
'<i>' + string + '</i> ' + string2,
)
else:
self.container_label.set_markup(
'<i>' + string + '</i> <a href="' \
+ html.escape(parent_obj.source) + '" title="' \
+ _('Click to open') + '">' + string2 + '</a>',
)
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 = '<s>' + _('Notify') + '</s>'
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(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, notify the user') \
+ '">' + label + '</a>',
)
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 = '<s>' + _('Alarm') + '</s>'
else:
label = _('Alarm')
self.live_auto_alarm_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, sound an alarm') \
+ '">' + label + '</a>',
)
if dbid in app_obj.media_reg_auto_open_dict:
label = '<s>' + _('Open') + '</s>'
else:
label = _('Open')
self.live_auto_open_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, open it') \
+ '">' + label + '</a>',
)
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 = '<s>' + _('D/L on start') + '</s>'
else:
label = _('D/L on start')
self.live_auto_dl_start_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream starts, download it') \
+ '">' + label + '</a>',
)
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 = '<s>' + _('D/L on stop') + '</s>'
else:
label = _('D/L on stop')
self.live_auto_dl_stop_label.set_markup(
'<a href="' + name + '" title="' \
+ _('When the livestream stops, download it') \
+ '">' + label + '</a>',
)
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 = '<a href="' + html.escape(
self.video_obj.get_actual_path(self.main_win_obj.app_obj),
) + '" title="' + _('Watch in your media player') + '">' \
+ _('Player') + '</a>'
# (Many labels are not clickable when a channel/playlist/folder's
# external directory is marked disabled)
if self.video_obj.parent_obj.name \
in self.main_win_obj.app_obj.media_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:
translate_note = _('TRANSLATOR\'S NOTE: D/L means download')
# Link clickable
self.watch_player_label.set_markup(
'<a href="' + html.escape(self.video_obj.source) \
+ '" title="' + _('Download this video') + '">' \
+ _('Download') + '</a>',
)
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:
translate_note = _(
'TRANSLATOR\'S NOTE: If you want to use &, use &amp;' \
+ ' - 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(
'<a href="' + html.escape(self.video_obj.source) \
+ '" title="' + _('Download and watch in your media player') \
+ '">' + _('D/L &amp; watch') + '</a>',
)
else:
# Link not clickable
self.watch_player_label.set_markup(
'<i>' + _('Can\'t D/L') + '</i>',
)
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 = ''
translate_note = _(
'TRANSLATOR\'S NOTE: This section contains shortened' \
+ ' labels: Archive = Archived, B/Mark = Bookmarked,' \
+ ' Waiting: In waiting list',
)
# Archived/not archived
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Prevent automatic deletion of the video') + '">'
if not self.video_obj.archive_flag:
self.marked_archive_label.set_markup(
text + _('Archived') + '</a>',
)
else:
self.marked_archive_label.set_markup(
text + '<s>' + _('Archived') + '</s></a>',
)
# Bookmarked/not bookmarked
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Show video in Bookmarks folder') + '">'
if not self.video_obj.bookmark_flag:
self.marked_bookmark_label.set_markup(
text + _('B/mark') + '</a>',
)
else:
self.marked_bookmark_label.set_markup(
text + '<s>' + _('B/mark') + '</s></a>',
)
# Favourite/not favourite
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Show in Favourite Videos folder') + '">'
if not self.video_obj.fav_flag:
self.marked_fav_label.set_markup(
text + _('Favourite') + '</a>',
)
else:
self.marked_fav_label.set_markup(
text + '<s>' + _('Favourite') + '</s></a>')
# Missing/not missing
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Mark video as removed by creator') + '">'
if not self.video_obj.missing_flag:
self.marked_missing_label.set_markup(
text + _('Missing') + '</a>',
)
else:
self.marked_missing_label.set_markup(
text + '<s>' + _('Missing') + '</s></a>',
)
# New/not new
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Mark video as never watched') + '">'
if not self.video_obj.new_flag:
self.marked_new_label.set_markup(
text + _('New') + '</a>',
)
else:
self.marked_new_label.set_markup(
text + '<s>' + _('New') + '</s></a>',
)
# In waiting list/not in waiting list
text = '<a href="' + html.escape(link_text) + '" title="' \
+ _('Show in Waiting Videos folder') + '">'
if not self.video_obj.waiting_flag:
self.marked_waiting_label.set_markup(
text + _('Waiting') + '</a>',
)
else:
self.marked_waiting_label.set_markup(
text + '<s>' + _('Waiting') + '</s></a>',
)
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.
Returns:
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.
Returns:
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
Returns:
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
Returns:
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
Returns:
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
Returns:
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(
'<b><span font_size="large">' + self.options_obj.name \
+ '</span></b>',
)
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(
'<i>' + html.escape(
utils.shorten_string_two_lines(
self.update_text,
length,
),
) + '</i>',
)
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_name (str): The name of the parent folder, or None if
the channels/playlists shouldn't be added inside a media.Folder
"""
# Standard class methods
def __init__(self, main_win_obj, suggest_parent_name=None):
# 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/playlsits 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.Folders to display in the Gtk.ComboBox
self.folder_list = []
# The media.Folder selected in the combobox
self.parent_name = 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('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(
_(
'<b>HINT</b>: You can also click <b>Media > Reset channel/' \
+ 'playlist names...</b>',
),
)
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)
renderer_text.set_property("editable", True)
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 one
# in the list. If not, 'Temporary Videos' is the first one in the
# list
for name, dbid in main_win_obj.app_obj.media_name_dict.items():
media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]
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.media_max_level \
and (
suggest_parent_name is None
or suggest_parent_name != media_data_obj.name
):
self.folder_list.append(media_data_obj.name)
self.folder_list.sort()
self.folder_list.insert(0, '')
self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
# Store the combobox's selected item, so the calling function can
# retrieve it.
self.parent_name = 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)
box2 = Gtk.Box()
self.grid2.attach(box2, 1, 0, 1, 1)
box2.set_border_width(main_win_obj.spacing_size)
image = Gtk.Image()
box2.add(image)
image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small'])
self.liststore2 = Gtk.ListStore(str)
for item in self.folder_list:
self.liststore2.append([item])
combo = Gtk.ComboBox.new_with_model(self.liststore2)
self.grid2.attach(combo, 2, 0, 1, 1)
combo.set_hexpand(True)
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)
# Display the dialogue window
self.show_all()
# 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 name in self.main_win_obj.app_obj.media_name_dict:
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 name in self.main_win_obj.app_obj.media_name_dict:
return name
# 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_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.split('\n'):
# 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) \
and not line in self.url_list:
mod_line = utils.strip_whitespace(line)
new_list.append(mod_line)
self.url_list.append(mod_line)
# 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_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_name = self.folder_list[combo.get_active()]
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_name (str): The name 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_name=None,
dl_sim_flag=False, monitor_flag=False):
# 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 to display in the Gtk.ComboBox
self.folder_list = []
# The media.Folder selected in the combobox (if any)
self.parent_name = 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(
'<i>' + _('(Use the channel\'s real name or a customised name)') \
+ '</i>',
)
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.custom_locale == 'ko_KR':
image.set_from_pixbuf(
main_win_obj.pixbuf_dict['yt_remind_icon_kr'],
)
elif main_win_obj.app_obj.custom_locale == 'nl_NL':
image.set_from_pixbuf(
main_win_obj.pixbuf_dict['yt_remind_icon_nl'],
)
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 one
# in the list. If not, 'No parent videos' is the first one in the
# list
for name, dbid in main_win_obj.app_obj.media_name_dict.items():
media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]
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.media_max_level \
and (
suggest_parent_name is None
or suggest_parent_name != media_data_obj.name
):
self.folder_list.append(media_data_obj.name)
self.folder_list.sort()
self.folder_list.insert(0, '')
self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
if suggest_parent_name is not None:
self.folder_list.insert(0, suggest_parent_name)
# Store the combobox's selected item, so the calling function can
# retrieve it.
self.parent_name = 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, str)
for item in self.folder_list:
if item == '':
pixbuf = main_win_obj.pixbuf_dict['slice_small']
listmodel.append( [pixbuf, ' ' + _('No parent folder')] )
elif item == main_win_obj.app_obj.fixed_temp_folder.name:
pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
listmodel.append( [pixbuf, ' ' + item] )
else:
pixbuf = main_win_obj.pixbuf_dict['folder_small']
listmodel.append( [pixbuf, ' ' + item] )
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', 1)
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_name = 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):
# 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_name (str): The name 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_name=None):
# 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 to display in the Gtk.ComboBox
self.folder_list = []
# The media.Folder selected in the combobox (if any)
self.parent_name = 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 name, dbid in main_win_obj.app_obj.media_name_dict.items():
media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]
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.media_max_level \
and (
suggest_parent_name is None
or suggest_parent_name != media_data_obj.name
):
self.folder_list.append(media_data_obj.name)
self.folder_list.sort()
self.folder_list.insert(0, '')
self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
if suggest_parent_name is not None:
self.folder_list.insert(0, suggest_parent_name)
# Store the combobox's selected item, so the calling function can
# retrieve it.
self.parent_name = 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, str)
for item in self.folder_list:
if item == '':
pixbuf = main_win_obj.pixbuf_dict['slice_small']
listmodel.append( [pixbuf, ' ' + _('No parent folder')] )
elif item == main_win_obj.app_obj.fixed_temp_folder.name:
pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
listmodel.append( [pixbuf, ' ' + item] )
else:
pixbuf = main_win_obj.pixbuf_dict['folder_small']
listmodel.append( [pixbuf, ' ' + item] )
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', 1)
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_name = 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_name (str): The name 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_name=None,
dl_sim_flag=False, monitor_flag=False):
# 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 selected in the combobox (if any)
self.parent_name = 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(
'<i>' + _('(Use the playlist\'s real name or a customised name)') \
+ '</i>',
)
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 one
# in the list. If not, 'No parent videos' is the first one in the
# list
for name, dbid in main_win_obj.app_obj.media_name_dict.items():
media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]
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.media_max_level \
and (
suggest_parent_name is None
or suggest_parent_name != media_data_obj.name
):
self.folder_list.append(media_data_obj.name)
self.folder_list.sort()
self.folder_list.insert(0, '')
self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
if suggest_parent_name is not None:
self.folder_list.insert(0, suggest_parent_name)
# Store the combobox's selected item, so the calling function can
# retrieve it.
self.parent_name = 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, str)
for item in self.folder_list:
if item == '':
pixbuf = main_win_obj.pixbuf_dict['slice_small']
listmodel.append( [pixbuf, ' ' + _('No parent folder')] )
elif item == main_win_obj.app_obj.fixed_temp_folder.name:
pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
listmodel.append( [pixbuf, ' ' + item] )
else:
pixbuf = main_win_obj.pixbuf_dict['folder_small']
listmodel.append( [pixbuf, ' ' + item] )
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', 1)
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_name = 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):
# 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.') \
+ ' <b>0:30 ' + _('Introduction') + '</b>',
)
label.set_xalign(0)
label = Gtk.Label()
# 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('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()
# 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):
# 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 to display in the Gtk.ComboBox
self.folder_list = []
# The media.Folder selected in the combobox
self.parent_name = 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('<i>' + text + '</i>')
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('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' 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 = main_win_obj.video_index_current
if selected:
dbid = main_win_obj.app_obj.media_name_dict[selected]
container_obj = main_win_obj.app_obj.media_reg_dict[dbid]
if isinstance(container_obj, media.Folder) \
and not container_obj.priv_flag:
folder_obj = container_obj
for name, dbid in main_win_obj.app_obj.media_name_dict.items():
media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]
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.name)
self.folder_list.sort()
self.folder_list.insert(0, main_win_obj.app_obj.fixed_misc_folder.name)
self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
self.folder_list.insert(
1,
main_win_obj.app_obj.fixed_clips_folder.name,
)
if folder_obj:
self.folder_list.insert(0, folder_obj.name)
# Store the combobox's selected item, so the calling function can
# retrieve it.
self.parent_name = 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, str)
for item in self.folder_list:
if item == '':
pixbuf = main_win_obj.pixbuf_dict['slice_small']
listmodel.append( [pixbuf, ' ' + _('No parent folder')] )
elif item == main_win_obj.app_obj.fixed_misc_folder.name:
pixbuf = main_win_obj.pixbuf_dict['folder_green_small']
listmodel.append( [pixbuf, ' ' + item] )
elif item == main_win_obj.app_obj.fixed_temp_folder.name:
pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
listmodel.append( [pixbuf, ' ' + item] )
else:
pixbuf = main_win_obj.pixbuf_dict['folder_small']
listmodel.append( [pixbuf, ' ' + item] )
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.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()
# 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_name = 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):
# 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 any options.OptionsManager objects (besides
# the General Options Manager) that are not already attached to a
# media data object
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 \
and options_obj.dbid is None:
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):
# 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 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):
# 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 name in sorted(main_win_obj.video_index_marker_dict):
dbid = main_win_obj.app_obj.media_name_dict[name]
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):
# 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('<b>' + media_data_obj.name + '</b>')
# 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):
# 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
if self.main_win_obj.app_obj.current_manager_obj \
or options_obj.dbid is not None:
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):
# 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 = main_win_obj.video_index_current
parent_obj = None
try:
parent_dbid = main_win_obj.app_obj.media_name_dict[current]
parent_obj = main_win_obj.app_obj.media_reg_dict[parent_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('<b>' + parent_obj.name + '</b>')
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):
# 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):
# 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):
# 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):
# 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):
# 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):
# 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('<i>' + text + '</i>')
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('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()
# 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):
# 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(
'<b>' \
+ utils.shorten_string(main_win_obj.app_obj.data_dir, 50) \
+ '</b>',
)
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):
# 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'),
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)
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):
# 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(
'<b>' + _('Nothing happened?') + '</b>',
)
# 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 <b>Output</b> tab and the\n' \
'<b>Errors/Warnings</b> 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 PrepareClipDialogue(Gtk.Dialog):
"""Called by mainwin.MainWin.on_video_catalogue_process_clip().
Prompt the user for a start/stop timestamp, and a clip title.
Args:
main_win_obj (mainwin.MainWin): The parent main window
video_obj (media.Video): The video from which a clip will be downloaded
or extracted
"""
# Standard class methods
def __init__(self, main_win_obj, video_obj):
# 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
# ---------------------
# (none)
# IV list - other
# ---------------
# Store the user's choice as an IV, so the calling function can
# retrieve it
self.start_stamp = None
self.stop_stamp = None
self.clip_title = None
self.all_flag = False
# Code
# ----
if not video_obj.dl_flag:
local_title = _('Download video clip')
else:
local_title = _('Create video clip')
Gtk.Dialog.__init__(
self,
local_title,
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
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)
# (Artificially widen the window a little to make it look better)
label.set_markup(
_('Start timestamp (e.g. 15:29)') + (24 * ' '),
)
label.set_alignment(0, 0.5)
entry = Gtk.Entry()
grid.attach(entry, 0, 1, 1, 1)
# (Signal connect appears below)
label2 = Gtk.Label()
grid.attach(label2, 0, 2, 1, 1)
label2.set_markup(_('Stop timestamp (optional)'))
label2.set_alignment(0, 0.5)
entry2 = Gtk.Entry()
grid.attach(entry2, 0, 3, 1, 1)
entry2.connect('changed', self.on_stop_entry_changed)
label3 = Gtk.Label()
grid.attach(label3, 0, 4, 1, 1)
label3.set_markup(_('Clip title (optional)'))
label3.set_alignment(0, 0.5)
entry3 = Gtk.Entry()
grid.attach(entry3, 0, 5, 1, 1)
entry3.connect('changed', self.on_title_entry_changed)
if not video_obj.dl_flag:
msg = _('Download this clip')
else:
msg = _('Create this clip')
button = Gtk.Button.new_with_label(msg)
grid.attach(button, 0, 6, 1, 1)
button.set_hexpand(False)
button.connect('clicked', self.on_one_button_clicked)
button.set_sensitive(False)
if not video_obj.dl_flag:
msg = _('Download all clips')
else:
msg = _('Create all clips')
msg += ' (' + str(len(video_obj.stamp_list)) + ')'
button2 = Gtk.Button.new_with_label(msg)
grid.attach(button2, 0, 7, 1, 1)
button2.set_hexpand(False)
button2.connect('clicked', self.on_all_button_clicked)
if not video_obj.stamp_list:
button2.set_sensitive(False)
# (Signal connect from above)
entry.connect('changed', self.on_start_entry_changed, button)
# Display the dialogue window
self.show_all()
# Callback class methods
def on_all_button_clicked(self, button):
"""Called from a callback in self.__init__().
Marks all clips to be created/downloaded, and closes the dialogue
window.
Args:
button (Gtk.Button): The widget clicked
"""
self.all_flag = True
self.destroy()
def on_one_button_clicked(self, button):
"""Called from a callback in self.__init__().
Marks one clip to be created/downloaded, using the specified
timestamps and/or clip title.
Args:
button (Gtk.Button): The widget clicked
"""
self.all_flag = False
self.destroy()
def on_start_entry_changed(self, entry, button):
"""Called from callback in self.__init__().
Sets the start timestamp.
Args:
entry (Gtk.Entry): The clicked widget
button (Gtk.Button): Another widget to be modified
"""
value = entry.get_text()
# (Unspecified timestamps/titles are stored as None, not empty strings)
if value == '':
self.start_stamp = None
button.set_sensitive(False)
else:
self.start_stamp = value
button.set_sensitive(True)
def on_stop_entry_changed(self, entry):
"""Called from callback in self.__init__().
Sets the stop timestamp.
Args:
entry (Gtk.Entry): The clicked widget
"""
value = entry.get_text()
if value == '':
self.stop_stamp = None
else:
self.stop_stamp = value
def on_title_entry_changed(self, entry):
"""Called from callback in self.__init__().
Sets the clip title.
Args:
entry (Gtk.Entry): The clicked widget
"""
value = entry.get_text()
if value == '':
self.clip_title = None
else:
self.clip_title = value
class PrepareSliceDialogue(Gtk.Dialog):
"""Called by mainwin.MainWin.on_video_catalogue_process_clip().
Prompt the user for a video slice, to be removed from the clicked video.
Args:
main_win_obj (mainwin.MainWin): The parent main window
video_obj (media.Video): The video from which a slice will be removed.
"""
# Standard class methods
def __init__(self, main_win_obj, video_obj):
# 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
# ---------------------
# (none)
# IV list - other
# ---------------
# Store the user's choice as an IV, so the calling function can
# retrieve it
self.start_time = None
self.stop_time = None
self.all_flag = False
self.all_but_flag = False
# Code
# ----
if not video_obj.dl_flag:
local_title = _('Download sliced video')
else:
local_title = _('Create sliced video')
Gtk.Dialog.__init__(
self,
local_title,
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
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)
# (Artificially widen the window a little to make it look better)
label.set_markup(
_('Start (timestamp or seconds)'),
)
label.set_alignment(0, 0.5)
entry = Gtk.Entry()
grid.attach(entry, 0, 1, 1, 1)
# (Signal connect appears below)
label2 = Gtk.Label()
grid.attach(label2, 0, 2, 1, 1)
label2.set_markup(_('Stop (optional)'))
label2.set_alignment(0, 0.5)
entry2 = Gtk.Entry()
grid.attach(entry2, 0, 3, 1, 1)
entry2.connect('changed', self.on_stop_entry_changed)
if not video_obj.dl_flag:
msg = _('Download and remove this slice')
else:
msg = _('Create this sliced video')
button = Gtk.Button.new_with_label(msg)
grid.attach(button, 0, 4, 1, 1)
button.set_hexpand(False)
button.connect('clicked', self.on_one_button_clicked)
button.set_sensitive(False)
if video_obj.dl_flag:
extra_rows = 0
button2 = Gtk.Button()
else:
extra_rows = 1
button2 = Gtk.Button.new_with_label(
_('Download and remove everything but this slice'),
)
grid.attach(button2, 0, 5, 1, 1)
button2.set_hexpand(False)
button2.connect('clicked', self.on_all_but_button_clicked)
button2.set_sensitive(False)
if not video_obj.dl_flag:
msg = _('Download video with all slices removed')
else:
msg = _('Create video with all slices removed')
msg += ' (' + str(len(video_obj.slice_list)) + ')'
button3 = Gtk.Button.new_with_label(msg)
grid.attach(button3, 0, (5 + extra_rows), 1, 1)
button3.set_hexpand(False)
button3.connect('clicked', self.on_all_button_clicked)
if not video_obj.slice_list:
button3.set_sensitive(False)
# (Signal connect from above)
entry.connect('changed', self.on_start_entry_changed, button, button2)
# Display the dialogue window
self.show_all()
# Callback class methods
def on_all_button_clicked(self, button):
"""Called from a callback in self.__init__().
Marks all slices to be removed, and closes the dialogue window.
Args:
button (Gtk.Button): The widget clicked
"""
self.all_flag = True
self.destroy()
def on_all_but_button_clicked(self, button):
"""Called from a callback in self.__init__().
Marks everything but the specified slice to be removed, using the
specified times.
Args:
button (Gtk.Button): The widget clicked
"""
self.all_but_flag = True
self.destroy()
def on_one_button_clicked(self, button):
"""Called from a callback in self.__init__().
Marks one slice to be removed, using the specified times.
Args:
button (Gtk.Button): The widget clicked
"""
self.all_flag = False
self.destroy()
def on_start_entry_changed (self, entry, button, button2):
"""Called from callback in self.__init__().
Sets the start time.
Args:
entry (Gtk.Entry): The clicked widget
button, button2 (Gtk.Button): Other widgets to be modified
"""
value = entry.get_text()
# (Unspecified times are stored as None, not empty strings)
if value == '':
self.start_time = None
button.set_sensitive(False)
button2.set_sensitive(False)
else:
self.start_time = value
button.set_sensitive(True)
button2.set_sensitive(True)
def on_stop_entry_changed (self, entry):
"""Called from callback in self.__init__().
Sets the stop time.
Args:
entry (Gtk.Entry): The clicked widget
"""
value = entry.get_text()
if value == '':
self.stop_time = None
else:
self.stop_time = value
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):
# 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):
# 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):
# 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<b>' + media_data_obj.name + '</b>\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):
# 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
"""
# Check the entered text is a valid name
if text == '' \
or re.search('^\s*$', text) \
or not self.main_win_obj.app_obj.check_container_name_is_legal(text):
return
# Check the entered text is not a duplicate
if text in self.main_win_obj.app_obj.media_name_dict:
return
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[self.liststore[path][4]] = 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):
# 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):
# 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
# forget 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':
string = _(
'This channel normally downloads videos into its own' \
+ ' folder',
)
elif media_type == 'playlist':
string = _(
'This playlist normally downloads videos into its own' \
+ ' folder',
)
else:
string = _(
'This folder normally downloads videos into itself',
)
else:
if media_type == 'channel':
string = _(
'This channel normally downloads videos into its own' \
+ ' directory',
)
elif media_type == 'playlist':
string = _(
'This playlist normally downloads videos into its own' \
+ ' directory',
)
else:
string = _(
'This folder normally downloads videos into its own' \
+ ' directory',
)
label = Gtk.Label(utils.tidy_up_long_string(string, 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)
string = _('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(string)
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.media_name_dict.values())
# 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
mod_dbid_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:
mod_dbid_list.append(this_dbid)
# Sort the modified list...
name_list = []
for this_dbid in mod_dbid_list:
this_obj = app_obj.media_reg_dict[this_dbid]
name_list.append(this_obj.name)
name_list.sort(key=lambda x: x.lower())
# ...and then add, at the top of the list, possible destinations that
# were filtered out
name_list.insert(0, media_data_obj.name)
if media_data_obj.master_dbid != media_data_obj.dbid:
current_obj = app_obj.media_reg_dict[media_data_obj.master_dbid]
name_list.insert(0, current_obj.name)
elif prev_dbid is not None:
prev_obj = app_obj.media_reg_dict[prev_dbid]
name_list.insert(0, prev_obj.name)
# Add a combo
store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
count = -1
for name in name_list:
dbid = app_obj.media_name_dict[name]
obj = app_obj.media_reg_dict[dbid]
if isinstance(obj, media.Channel):
icon_name = 'channel_small'
elif isinstance(obj, media.Playlist):
icon_name = 'playlist_small'
else:
icon_name = 'folder_small'
store.append( [main_win_obj.pixbuf_dict[icon_name], 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', 1)
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()
pixbuf, name = model[tree_iter][:2]
# (Allow for the possibility that the media data object might have
# been deleted, since the dialogue window opened)
if name in self.main_win_obj.app_obj.media_name_dict:
dbid = self.main_win_obj.app_obj.media_name_dict[name]
obj = self.main_win_obj.app_obj.media_reg_dict[dbid]
self.choice = obj.dbid
self.main_win_obj.set_previous_alt_dest_dbid(obj.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()
pixbuf, name = model[tree_iter][:2]
# (Allow for the possibility that the media data object might have
# been deleted, since the dialogue window opened)
if name in self.main_win_obj.app_obj.media_name_dict:
dbid = self.main_win_obj.app_obj.media_name_dict[name]
obj = self.main_win_obj.app_obj.media_reg_dict[dbid]
self.choice = obj.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):
# 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):
# 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):
# 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
Returns:
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):
# 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):
# 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
# 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, 1, 0, 1, 1)
self.checkbutton8.set_label(_('Delete all archive files'))
self.checkbutton9 = Gtk.CheckButton()
grid.attach(self.checkbutton9, 1, 1, 1, 1)
self.checkbutton9.set_label(_('Move thumbnails into own folder'))
# (Signal connect appears below)
self.checkbutton10 = Gtk.CheckButton()
grid.attach(self.checkbutton10, 1, 2, 1, 1)
self.checkbutton10.set_label(_('Delete all thumbnail files'))
self.checkbutton11 = Gtk.CheckButton()
grid.attach(self.checkbutton11, 1, 3, 1, 1)
self.checkbutton11.set_label(
utils.tidy_up_long_string(
_('Convert .webp thumbnails to .jpg using FFmpeg'),
label_length,
),
)
self.checkbutton12 = Gtk.CheckButton()
grid.attach(self.checkbutton12, 1, 4, 1, 1)
self.checkbutton12.set_label(
utils.tidy_up_long_string(
_('Move other metadata files into own folder'),
label_length,
),
)
# (Signal connect appears below)
self.checkbutton13 = Gtk.CheckButton()
grid.attach(self.checkbutton13, 1, 5, 1, 1)
self.checkbutton13.set_label(_('Delete all description files'))
self.checkbutton14 = Gtk.CheckButton()
grid.attach(self.checkbutton14, 1, 6, 1, 1)
self.checkbutton14.set_label(_('Delete all metadata (JSON) files'))
self.checkbutton15 = Gtk.CheckButton()
grid.attach(self.checkbutton15, 1, 7, 1, 1)
self.checkbutton15.set_label(_('Delete all annotation files'))
# Bottom strip
button = Gtk.Button.new_with_label(_('Select all'))
grid.attach(button, 0, 8, 1, 1)
button.set_hexpand(False)
# (Signal connect appears below)
button2 = Gtk.Button.new_with_label(_('Select none'))
grid.attach(button2, 1, 8, 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_checkbutton14_toggled)
self.checkbutton12.connect('toggled', self.on_checkbutton15_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_checkbutton14_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)
else:
self.checkbutton10.set_active(False)
self.checkbutton10.set_sensitive(False)
def on_checkbutton15_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.checkbutton13.set_sensitive(True)
self.checkbutton14.set_sensitive(True)
self.checkbutton15.set_sensitive(True)
else:
self.checkbutton13.set_active(False)
self.checkbutton14.set_active(False)
self.checkbutton15.set_active(False)
self.checkbutton13.set_sensitive(False)
self.checkbutton14.set_sensitive(False)
self.checkbutton15.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(True)
self.checkbutton12.set_active(True)
self.checkbutton13.set_active(False)
self.checkbutton14.set_active(False)
self.checkbutton15.set_active(False)
self.checkbutton10.set_sensitive(False)
self.checkbutton13.set_sensitive(False)
self.checkbutton14.set_sensitive(False)
self.checkbutton15.set_sensitive(False)
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.checkbutton11.set_active(False)
self.checkbutton12.set_active(False)
self.checkbutton13.set_active(False)
self.checkbutton14.set_active(False)
self.checkbutton15.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)
self.checkbutton10.set_sensitive(True)
self.checkbutton13.set_sensitive(True)
self.checkbutton14.set_sensitive(True)
self.checkbutton15.set_sensitive(True)