34132 lines
1.1 MiB
34132 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
|
|
# 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
|
|
# 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,
|
|
)
|
|
|
|
# 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(
|
|
203,
|
|
'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(
|
|
205,
|
|
'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(
|
|
201,
|
|
'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(
|
|
202,
|
|
'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(
|
|
999,
|
|
'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(
|
|
204,
|
|
'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 \
|
|
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 \
|
|
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())
|
|
|
|
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)
|
|
|
|
# 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':
|
|
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)
|
|
|
|
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)
|
|
|
|
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,
|
|
dl_flag,
|
|
not_dl_flag,
|
|
source_flag,
|
|
temp_folder_flag,
|
|
live_flag,
|
|
unavailable_flag,
|
|
video_list,
|
|
)
|
|
|
|
# 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,
|
|
dl_flag,
|
|
not_dl_flag,
|
|
source_flag,
|
|
temp_folder_flag,
|
|
live_flag,
|
|
False, # unavailable_flag does not apply here
|
|
video_list,
|
|
)
|
|
|
|
# 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)
|
|
|
|
# Remember undownloaded URLs
|
|
remember_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
|
|
_('_Remember 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, dl_flag, \
|
|
not_dl_flag, source_flag, temp_folder_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
|
|
|
|
dl_flag (bool): Flag set to True if any of the media.Video objects
|
|
have their .dl_flag IV set
|
|
|
|
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
|
|
|
|
temp_folder_flag (bool): Flag set to True if any of the media.Video
|
|
objects' parent objects are media.Folder objects, and if those
|
|
media.Folder objects have their .temp_flag 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)
|
|
|
|
# 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 \
|
|
or unavailable_flag:
|
|
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)
|
|
|
|
# Mark as not livestreams
|
|
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,
|
|
)
|
|
popup_menu.append(not_live_menu_item)
|
|
|
|
# Finalise livestreams
|
|
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,
|
|
)
|
|
popup_menu.append(finalise_live_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 \
|
|
or unavailable_flag:
|
|
temp_menu_item.set_sensitive(False)
|
|
|
|
|
|
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(
|
|
206,
|
|
'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(
|
|
207,
|
|
'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(
|
|
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
|
|
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(
|
|
209,
|
|
'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(
|
|
210,
|
|
'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(
|
|
211,
|
|
'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(
|
|
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, 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(
|
|
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,
|
|
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(
|
|
214,
|
|
'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(
|
|
215,
|
|
'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(
|
|
216,
|
|
'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(
|
|
217,
|
|
'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(
|
|
218,
|
|
'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(
|
|
219,
|
|
'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(
|
|
220,
|
|
'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(
|
|
221,
|
|
'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]
|
|
|
|
# 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
|
|
|
|
# Remove the row
|
|
path = Gtk.TreePath(row_num)
|
|
tree_iter = self.progress_list_liststore.get_iter(path)
|
|
self.progress_list_liststore.remove(tree_iter)
|
|
|
|
# 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]
|
|
|
|
|
|
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(
|
|
222,
|
|
'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().
|
|
|
|
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.
|
|
"""
|
|
|
|
# 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_start_iter(),
|
|
self.classic_textbuffer.get_end_iter(),
|
|
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)
|
|
if not self.app_obj.classic_duplicate_remove_flag:
|
|
self.classic_textbuffer.set_text(invalid_url_string)
|
|
else:
|
|
self.classic_textbuffer.set_text('')
|
|
|
|
|
|
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(
|
|
223,
|
|
'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_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_start_iter(),
|
|
self.classic_textbuffer.get_end_iter(),
|
|
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
|
|
|
|
|
|
# (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(
|
|
224,
|
|
'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(
|
|
225,
|
|
'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(
|
|
226,
|
|
'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
|
|
|
|
"""
|
|
|
|
if self.app_obj.current_manager_obj:
|
|
return self.app_obj.system_error(
|
|
227,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Start a download operation
|
|
self.app_obj.download_manager_start('sim', False, [media_data_obj] )
|
|
|
|
|
|
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(
|
|
228,
|
|
'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(
|
|
229,
|
|
'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(
|
|
230,
|
|
'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(
|
|
231,
|
|
'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(
|
|
232,
|
|
'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
|
|
|
|
"""
|
|
|
|
if self.app_obj.current_manager_obj:
|
|
return self.app_obj.system_error(
|
|
233,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Start a download operation
|
|
self.app_obj.download_manager_start('real', False, [media_data_obj] )
|
|
|
|
|
|
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(
|
|
234,
|
|
'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_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(
|
|
235,
|
|
'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(
|
|
236,
|
|
'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(
|
|
237,
|
|
'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(
|
|
238,
|
|
'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(
|
|
239,
|
|
'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(
|
|
240,
|
|
'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(
|
|
241,
|
|
'Cannot modify the URL of a video',
|
|
)
|
|
|
|
elif isinstance(media_data_obj, media.Folder):
|
|
return self.app_obj.system_error(
|
|
242,
|
|
'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(
|
|
243,
|
|
'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(
|
|
244,
|
|
'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(
|
|
245,
|
|
'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(
|
|
246,
|
|
'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(
|
|
247,
|
|
'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(
|
|
248,
|
|
'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(
|
|
249,
|
|
'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
|
|
|
|
"""
|
|
|
|
self.app_obj.delete_video(media_data_obj, True)
|
|
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
for media_data_obj in media_data_list:
|
|
self.app_obj.delete_video(media_data_obj, True)
|
|
|
|
# Standard de-selection of everything in the Video Catalogue
|
|
self.video_catalogue_unselect_all()
|
|
|
|
|
|
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(
|
|
250,
|
|
'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(
|
|
251,
|
|
'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(
|
|
252,
|
|
'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(
|
|
253,
|
|
'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_obj (media.Video): The clicked video object
|
|
|
|
"""
|
|
|
|
# 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(
|
|
254,
|
|
'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_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(
|
|
255,
|
|
'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(
|
|
256,
|
|
'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(
|
|
257,
|
|
'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_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
|
|
|
|
"""
|
|
|
|
text = self.classic_textbuffer.get_text(
|
|
self.classic_textbuffer.get_start_iter(),
|
|
self.classic_textbuffer.get_end_iter(),
|
|
# Don't include hidden characters
|
|
False,
|
|
)
|
|
|
|
if not (re.search('^\S*$', text)) \
|
|
and not (re.search('\n+\s*$', text)):
|
|
self.classic_textbuffer.set_text(text + '\n')
|
|
|
|
|
|
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(
|
|
258,
|
|
'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(
|
|
259,
|
|
'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(
|
|
260,
|
|
'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(
|
|
261,
|
|
'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(
|
|
262,
|
|
'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_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(
|
|
263,
|
|
'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_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(
|
|
264,
|
|
'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
|
|
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
|
|
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.very_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 &' \
|
|
+ ' - 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 & 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 & 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(
|
|
265,
|
|
'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
|
|
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.very_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 &' \
|
|
+ ' - 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 & 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(
|
|
266,
|
|
'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)
|
|
self.button2.set_label(_('Delete files on your computer'))
|
|
grid.attach(self.button2, 0, 10, 1, 1)
|
|
|
|
# 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 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 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)
|