6186 lines
200 KiB
Python
6186 lines
200 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (C) 2019 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 classes."""
|
|
|
|
|
|
# Import Gtk modules
|
|
import gi
|
|
gi.require_version('Gtk', '3.0')
|
|
from gi.repository import Gtk, Gdk
|
|
from gi.repository import GdkPixbuf
|
|
|
|
|
|
# Import other modules
|
|
import cgi
|
|
import datetime
|
|
from gi.repository import Gio
|
|
import os
|
|
from gi.repository import Pango
|
|
import re
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
|
|
# Import our modules
|
|
from . import config
|
|
from . import constants
|
|
#from . import __main__
|
|
import __main__
|
|
from . import mainapp
|
|
from . import media
|
|
from . import options
|
|
from . import utils
|
|
|
|
|
|
# Classes
|
|
class MainWin(Gtk.ApplicationWindow):
|
|
|
|
"""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() + ' v' \
|
|
+ __main__.__version__ + ' UNSTABLE',
|
|
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_menubar)
|
|
self.menubar = None # Gtk.MenuBar
|
|
# (from self.setup_toolbar)
|
|
self.toolbar = None # Gtk.Toolbar
|
|
# (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 # Gkt.Box
|
|
self.errors_tab = None # Gkt.Box
|
|
self.errors_label = None # Gkt.Label
|
|
# (from self.setup_videos_tab)
|
|
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.check_button = None # Gtk.Button
|
|
self.download_button = None # Gtk.Button
|
|
self.video_catalogue_scrolled = None # Gtk.ScrolledWindow
|
|
self.video_catalogue_frame = None # Gtk.Frame
|
|
self.video_catalogue_listbox = None # Gtk.ListBox
|
|
# (from self.setup_progress_tab)
|
|
self.progress_paned = None # Gtk.VPaned
|
|
self.progress_list_scrolled = None # Gtk.ScrolledWindow
|
|
self.progress_list_framed = None # Gtk.Frame
|
|
self.progress_list_treeview = None # Gtk.TreeView
|
|
self.progress_list_liststore = None # Gtk.ListStore
|
|
self.results_list_scrolled = None # Gtk.Frame
|
|
self.results_list_frame = None # Gtk.Frame
|
|
self.results_list_treeview = None # Gtk.TreeView
|
|
self.results_list_liststore = None # Gtk.ListStore
|
|
self.button_box = None # Gtk.VBox
|
|
self.checkbutton = None # Gtk.CheckButton
|
|
self.spinbutton = None # Gtk.SpinButton
|
|
self.progress_bar = None # Gtk.ProgressBar
|
|
self.progress_label = None # Gtk.Label
|
|
# (from self.setup_errors_tab)
|
|
self.errors_list_scrolled = None # Gtk.ScrolledWindow
|
|
self.errors_list_framed = None # Gtk.Frame
|
|
self.errors_list_treeview = None # Gtk.TreeView
|
|
self.errors_list_liststore = None # Gtk.ListStore
|
|
self.error_list_button = None # Gtk.Button
|
|
|
|
# (Widgets which must be (de)sensitised during download/update/refresh
|
|
# operations, in addition to self.check_button and
|
|
# self.download_button)
|
|
self.save_db_menu_item = None # Gtk.MenuItem
|
|
self.system_prefs_menu_item = None # Gtk.MenuItem
|
|
self.gen_options_menu_item = None # Gtk.MenuItem
|
|
self.check_all_menu_item = None # Gtk.MenuItem
|
|
self.download_all_menu_item = None # Gtk.MenuItem
|
|
self.stop_download_menu_item = None # Gtk.MenuItem
|
|
self.update_ytdl_menu_item = None # Gtk.MenuItem
|
|
self.refresh_db_menu_item = None # Gtk.MenuItem
|
|
self.check_all_toolbutton = None # Gtk.ToolButton
|
|
self.download_all_toolbutton = None # Gtk.ToolButton
|
|
self.stop_download_toolbutton = None # Gtk.ToolButton
|
|
# (Other widgets that might be modified, depending on current
|
|
# conditions)
|
|
self.test_menu_item = None # Gtk.MenuItem
|
|
self.test_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
|
|
# In the Videos Tab, the width (in pixels) of the Video Index
|
|
self.videos_paned_posn = 200
|
|
# In the Progress Tab, the height (in pixels) of the Progress List
|
|
self.progress_paned_posn = 200
|
|
# Standard size of video thumbnails in the Video Catalogue, in pixels,
|
|
# assuming that the actual thumbnail file is 1280x720
|
|
self.thumb_width = 128
|
|
self.thumb_height = 76
|
|
|
|
# Paths to Tartube standard icon files. Dictionary in the form
|
|
# key - a string like 'video_both_large'
|
|
# value - full filepath to the icon file
|
|
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 each window's icon list
|
|
self.win_pixbuf_list = []
|
|
|
|
# Standard limits for the length of strings displayed in various
|
|
# widgets
|
|
self.long_string_max_len = 64
|
|
self.medium_string_max_len = 45
|
|
self.string_max_len = 32
|
|
self.short_string_max_len = 24
|
|
self.tiny_string_max_len = 16
|
|
|
|
# List of configuration windows (anything inheriting from
|
|
# config.GenericConfigWin) that are currently open. A download/
|
|
# update/refresh operation cannot start when one of these windows are
|
|
# open (and the windows cannot be opened during such an operation)
|
|
self.config_win_list = []
|
|
|
|
# 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 = {}
|
|
# 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 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 or
|
|
# mainwin.ComplexCatalogueItem objects (depending on the current
|
|
# value of self.complex_catalogue_flag)
|
|
# 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 or
|
|
# mainWin.ComplexCatalogueItem, which matches the dbid of its
|
|
# media.Video object)
|
|
# value = the catalogue item itself
|
|
self.video_catalogue_dict = {}
|
|
# An ordered list of .dbid IVs for all mainwin.SimpleCatalogueItem or
|
|
# mainwin.ComplexCatalogueItem objects (in the order they're
|
|
# displayed)
|
|
self.video_catalogue_list = []
|
|
|
|
# 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.dbid 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
|
|
# media.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.dbid 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 = {}
|
|
|
|
# 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_thumbnail': flags from
|
|
# the options.OptionsManager object used for to download the
|
|
# video (not added to the dictionary at all for simulated
|
|
# downloads)
|
|
self.results_list_temp_list = []
|
|
|
|
# Errors / Warnings Tab IVs
|
|
# The number of errors added to the Error List, since this tab was the
|
|
# visible one (updated by self.errors_list_add_row() or
|
|
# self.errors_list_add_system_error(), and reset back to zero by
|
|
# self.on_notebook_switch_page() when the tab becomes the visible one
|
|
# again)
|
|
self.tab_error_count = 0
|
|
# The number of warnings added to the Error List, since this tab was
|
|
# the visible one
|
|
self.tab_warning_count = 0
|
|
# 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
|
|
|
|
# State variables
|
|
# 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
|
|
|
|
|
|
# Code
|
|
# ----
|
|
|
|
# Create GdkPixbuf.Pixbufs for all Tartube standard icons
|
|
self.setup_pixbufs()
|
|
# Set up the main window
|
|
self.setup_win()
|
|
|
|
|
|
# Public class methods
|
|
|
|
|
|
def setup_pixbufs(self):
|
|
|
|
"""Called by self.__init__().
|
|
|
|
Populates self.icon_dict and self.pixbuf.dict from the lists provided
|
|
by constants.py.
|
|
"""
|
|
|
|
for key in constants.DIALOGUE_ICON_DICT:
|
|
rel_path = constants.DIALOGUE_ICON_DICT[key]
|
|
full_path = os.path.join('icons', 'dialogue', rel_path)
|
|
self.icon_dict[key] = full_path
|
|
|
|
for key in constants.LARGE_ICON_DICT:
|
|
rel_path = constants.LARGE_ICON_DICT[key]
|
|
full_path = os.path.join('icons', 'large', rel_path)
|
|
self.icon_dict[key] = full_path
|
|
|
|
for key in constants.SMALL_ICON_DICT:
|
|
rel_path = constants.SMALL_ICON_DICT[key]
|
|
full_path = os.path.join('icons', 'small', rel_path)
|
|
self.icon_dict[key] = full_path
|
|
|
|
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 constants.WIN_ICON_LIST:
|
|
full_path = os.path.join('icons', 'win', rel_path)
|
|
self.win_pixbuf_list.append(
|
|
GdkPixbuf.Pixbuf.new_from_file(full_path),
|
|
)
|
|
|
|
|
|
def setup_win(self):
|
|
|
|
"""Called by self.__init__().
|
|
|
|
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)
|
|
|
|
# Create main window widgets
|
|
self.setup_grid()
|
|
self.setup_menubar()
|
|
self.setup_toolbar()
|
|
self.setup_notebook()
|
|
self.setup_videos_tab()
|
|
self.setup_progress_tab()
|
|
self.setup_errors_tab()
|
|
|
|
|
|
# (Create main window widgets)
|
|
|
|
|
|
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_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.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')
|
|
|
|
separator_item = Gtk.SeparatorMenuItem()
|
|
file_sub_menu.append(separator_item)
|
|
|
|
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('E_dit')
|
|
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')
|
|
|
|
# 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)
|
|
|
|
add_video_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'Add _videos...',
|
|
)
|
|
media_sub_menu.append(add_video_menu_item)
|
|
add_video_menu_item.set_action_name('app.add_video_menu')
|
|
|
|
add_channel_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'Add _channel...',
|
|
)
|
|
media_sub_menu.append(add_channel_menu_item)
|
|
add_channel_menu_item.set_action_name('app.add_channel_menu')
|
|
|
|
add_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'Add _playlist...',
|
|
)
|
|
media_sub_menu.append(add_playlist_menu_item)
|
|
add_playlist_menu_item.set_action_name('app.add_playlist_menu')
|
|
|
|
add_folder_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'Add _folder...',
|
|
)
|
|
media_sub_menu.append(add_folder_menu_item)
|
|
add_folder_menu_item.set_action_name('app.add_folder_menu')
|
|
|
|
separator_item2 = Gtk.SeparatorMenuItem()
|
|
media_sub_menu.append(separator_item2)
|
|
|
|
switch_view_menu_item = \
|
|
Gtk.MenuItem.new_with_mnemonic('_Switch between views')
|
|
media_sub_menu.append(switch_view_menu_item)
|
|
switch_view_menu_item.set_action_name('app.switch_view_menu')
|
|
|
|
self.show_hidden_menu_item = \
|
|
Gtk.MenuItem.new_with_mnemonic('Show _hidden folders')
|
|
media_sub_menu.append(self.show_hidden_menu_item)
|
|
self.show_hidden_menu_item.set_action_name('app.show_hidden_menu')
|
|
|
|
if self.app_obj.debug_test_media_menu_flag:
|
|
|
|
separator_item3 = Gtk.SeparatorMenuItem()
|
|
media_sub_menu.append(separator_item3)
|
|
|
|
self.test_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'_Add test media',
|
|
)
|
|
media_sub_menu.append(self.test_menu_item)
|
|
self.test_menu_item.set_action_name('app.test_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.stop_download_menu_item = \
|
|
Gtk.MenuItem.new_with_mnemonic('_Stop downloads')
|
|
ops_sub_menu.append(self.stop_download_menu_item)
|
|
self.stop_download_menu_item.set_sensitive(False)
|
|
self.stop_download_menu_item.set_action_name('app.stop_download_menu')
|
|
|
|
separator_item4 = Gtk.SeparatorMenuItem()
|
|
ops_sub_menu.append(separator_item4)
|
|
|
|
self.update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'_Update youtube-dl',
|
|
)
|
|
ops_sub_menu.append(self.update_ytdl_menu_item)
|
|
self.update_ytdl_menu_item.set_action_name('app.update_ytdl_menu')
|
|
|
|
separator_item5 = Gtk.SeparatorMenuItem()
|
|
ops_sub_menu.append(separator_item5)
|
|
|
|
self.refresh_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'_Refresh whole database',
|
|
)
|
|
ops_sub_menu.append(self.refresh_db_menu_item)
|
|
self.refresh_db_menu_item.set_action_name('app.refresh_db_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')
|
|
|
|
|
|
def setup_toolbar(self):
|
|
|
|
"""Called by self.setup_win().
|
|
|
|
Sets up a Gtk.Toolbar near the top of the main window, below the menu.
|
|
"""
|
|
|
|
self.toolbar = Gtk.Toolbar()
|
|
self.grid.attach(self.toolbar, 0, 1, 1, 1)
|
|
|
|
count = 0;
|
|
|
|
# Toolbar items
|
|
add_video_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ADD)
|
|
self.toolbar.insert(add_video_button, count)
|
|
add_video_button.set_label('Videos')
|
|
add_video_button.set_is_important(True)
|
|
add_video_button.set_action_name('app.add_video_toolbutton')
|
|
count += 1
|
|
|
|
add_channel_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ADD)
|
|
self.toolbar.insert(add_channel_button, count)
|
|
add_channel_button.set_label('Channel')
|
|
add_channel_button.set_is_important(True)
|
|
add_channel_button.set_action_name('app.add_channel_toolbutton')
|
|
count += 1
|
|
|
|
add_playlist_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ADD)
|
|
self.toolbar.insert(add_playlist_button, count)
|
|
add_playlist_button.set_label('Playlist')
|
|
add_playlist_button.set_is_important(True)
|
|
add_playlist_button.set_action_name('app.add_playlist_toolbutton')
|
|
count += 1
|
|
|
|
add_folder_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ADD)
|
|
self.toolbar.insert(add_folder_button, count)
|
|
add_folder_button.set_label('Folder')
|
|
add_folder_button.set_is_important(True)
|
|
add_folder_button.set_action_name('app.add_folder_toolbutton')
|
|
count += 1
|
|
|
|
self.toolbar.insert(Gtk.SeparatorToolItem(), count)
|
|
count += 1
|
|
|
|
self.check_all_toolbutton = Gtk.ToolButton.new_from_stock(
|
|
Gtk.STOCK_REFRESH,
|
|
)
|
|
self.toolbar.insert(self.check_all_toolbutton, count)
|
|
self.check_all_toolbutton.set_label('Check all')
|
|
self.check_all_toolbutton.set_is_important(True)
|
|
self.check_all_toolbutton.set_action_name('app.check_all_toolbutton')
|
|
count += 1
|
|
|
|
self.download_all_toolbutton = Gtk.ToolButton.new_from_stock(
|
|
Gtk.STOCK_EXECUTE,
|
|
)
|
|
self.toolbar.insert(self.download_all_toolbutton, count)
|
|
self.download_all_toolbutton.set_label('Download all')
|
|
self.download_all_toolbutton.set_is_important(True)
|
|
self.download_all_toolbutton.set_action_name(
|
|
'app.download_all_toolbutton',
|
|
)
|
|
count += 1
|
|
|
|
self.stop_download_toolbutton = Gtk.ToolButton.new_from_stock(
|
|
Gtk.STOCK_STOP,
|
|
)
|
|
self.toolbar.insert(self.stop_download_toolbutton, count)
|
|
self.stop_download_toolbutton.set_label('Stop')
|
|
self.stop_download_toolbutton.set_is_important(True)
|
|
self.stop_download_toolbutton.set_sensitive(False)
|
|
self.stop_download_toolbutton.set_action_name(
|
|
'app.stop_download_toolbutton',
|
|
)
|
|
count += 1
|
|
|
|
self.toolbar.insert(Gtk.SeparatorToolItem(), count)
|
|
count += 1
|
|
|
|
switch_view_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CONVERT)
|
|
self.toolbar.insert(switch_view_button, count)
|
|
switch_view_button.set_label('Switch')
|
|
switch_view_button.set_is_important(True)
|
|
switch_view_button.set_action_name('app.switch_view_toolbutton')
|
|
count += 1
|
|
|
|
if self.app_obj.debug_test_media_toolbar_flag:
|
|
self.test_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CDROM)
|
|
self.toolbar.insert(self.test_button, count)
|
|
self.test_button.set_label('Test')
|
|
self.test_button.set_is_important(True)
|
|
self.test_button.set_action_name('app.test_toolbutton')
|
|
count += 1
|
|
|
|
quit_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CLOSE)
|
|
self.toolbar.insert(quit_button, count)
|
|
quit_button.set_label('Quit')
|
|
quit_button.set_is_important(True)
|
|
quit_button.set_action_name('app.quit_toolbutton')
|
|
count += 1
|
|
|
|
|
|
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)
|
|
|
|
# Errors 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.videos_paned_posn)
|
|
self.videos_paned.set_wide_handle(True)
|
|
|
|
# Left-hand side
|
|
vbox = Gtk.VBox()
|
|
self.videos_paned.add1(vbox)
|
|
|
|
self.video_index_scrolled = Gtk.ScrolledWindow()
|
|
vbox.pack_start(self.video_index_scrolled, True, True, 0)
|
|
self.video_index_scrolled.set_policy(
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
)
|
|
|
|
self.video_index_frame = Gtk.Frame()
|
|
self.video_index_scrolled.add_with_viewport(self.video_index_frame)
|
|
|
|
# Video index
|
|
self.video_index_reset()
|
|
|
|
# 'Check all' and 'Download all' buttons
|
|
self.button_box = Gtk.VBox()
|
|
vbox.pack_start(self.button_box, False, False, 0)
|
|
|
|
self.check_button = Gtk.Button()
|
|
self.button_box.pack_start(
|
|
self.check_button,
|
|
True,
|
|
True,
|
|
self.spacing_size,
|
|
)
|
|
self.check_button.set_label('Check all')
|
|
self.check_button.set_action_name('app.check_all_button')
|
|
|
|
self.download_button = Gtk.Button()
|
|
self.button_box.pack_start(self.download_button, True, True, 0)
|
|
self.download_button.set_label('Download all')
|
|
self.download_button.set_action_name('app.download_all_button')
|
|
|
|
# Right-hand side
|
|
self.video_catalogue_scrolled = Gtk.ScrolledWindow()
|
|
self.videos_paned.add2(self.video_catalogue_scrolled)
|
|
self.video_catalogue_scrolled.set_policy(
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
)
|
|
|
|
self.video_catalogue_frame = Gtk.Frame()
|
|
self.video_catalogue_scrolled.add_with_viewport(
|
|
self.video_catalogue_frame,
|
|
)
|
|
|
|
# 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.progress_paned_posn)
|
|
self.progress_paned.set_wide_handle(True)
|
|
|
|
# Upper half
|
|
self.progress_list_scrolled = Gtk.ScrolledWindow()
|
|
self.progress_paned.add1(self.progress_list_scrolled)
|
|
self.progress_list_scrolled.set_policy(
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
)
|
|
|
|
self.progress_list_frame = Gtk.Frame()
|
|
self.progress_list_scrolled.add_with_viewport(self.progress_list_frame)
|
|
|
|
# Progress List
|
|
self.progress_list_treeview = Gtk.TreeView()
|
|
self.progress_list_frame.add(self.progress_list_treeview)
|
|
|
|
for i, column_title in enumerate(
|
|
[
|
|
'', 'Source', 'Videos', 'Status', 'Incoming file', 'Ext',
|
|
'Size', '%', 'ETA', 'Speed',
|
|
]
|
|
):
|
|
if not column_title:
|
|
renderer_pixbuf = Gtk.CellRendererPixbuf()
|
|
column_pixbuf = Gtk.TreeViewColumn(
|
|
'',
|
|
renderer_pixbuf,
|
|
pixbuf=i,
|
|
)
|
|
self.progress_list_treeview.append_column(column_pixbuf)
|
|
|
|
else:
|
|
renderer_text = Gtk.CellRendererText()
|
|
column_text = Gtk.TreeViewColumn(
|
|
column_title,
|
|
renderer_text,
|
|
text=i,
|
|
)
|
|
self.progress_list_treeview.append_column(column_text)
|
|
|
|
self.progress_list_liststore = Gtk.ListStore(
|
|
GdkPixbuf.Pixbuf,
|
|
str, str, str, str, str, str, str, str, str,
|
|
)
|
|
self.progress_list_treeview.set_model(self.progress_list_liststore)
|
|
|
|
# Lower half
|
|
self.results_list_scrolled = Gtk.ScrolledWindow()
|
|
self.progress_paned.add2(self.results_list_scrolled)
|
|
self.results_list_scrolled.set_policy(
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
)
|
|
|
|
self.results_list_frame = Gtk.Frame()
|
|
self.results_list_scrolled.add_with_viewport(self.results_list_frame)
|
|
|
|
# Results List
|
|
self.results_list_treeview = Gtk.TreeView()
|
|
self.results_list_frame.add(self.results_list_treeview)
|
|
|
|
for i, column_title in enumerate(
|
|
[
|
|
'', '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)
|
|
|
|
elif column_title == 'File':
|
|
renderer_toggle = Gtk.CellRendererToggle()
|
|
column_toggle = Gtk.TreeViewColumn(
|
|
column_title,
|
|
renderer_toggle,
|
|
active=i,
|
|
)
|
|
self.results_list_treeview.append_column(column_toggle)
|
|
|
|
else:
|
|
renderer_text = Gtk.CellRendererText()
|
|
column_text = Gtk.TreeViewColumn(
|
|
column_title,
|
|
renderer_text,
|
|
text=i,
|
|
)
|
|
self.results_list_treeview.append_column(column_text)
|
|
|
|
self.results_list_liststore = Gtk.ListStore(
|
|
GdkPixbuf.Pixbuf,
|
|
str, str, str, str,
|
|
bool,
|
|
GdkPixbuf.Pixbuf,
|
|
str,
|
|
)
|
|
self.results_list_treeview.set_model(self.results_list_liststore)
|
|
|
|
# Strip of widgets at the bottom
|
|
hbox = Gtk.HBox()
|
|
vbox.pack_start(hbox, False, False, self.spacing_size)
|
|
hbox.set_border_width(self.spacing_size)
|
|
|
|
self.checkbutton = Gtk.CheckButton()
|
|
hbox.pack_start(self.checkbutton, False, False, 0)
|
|
self.checkbutton.set_label('Limit simultaneous downloads to')
|
|
self.checkbutton.set_active(self.app_obj.num_worker_apply_flag)
|
|
self.checkbutton.connect('toggled', self.on_checkbutton_changed)
|
|
|
|
self.spinbutton = Gtk.SpinButton.new_with_range(
|
|
self.app_obj.num_worker_min,
|
|
self.app_obj.num_worker_max,
|
|
1,
|
|
)
|
|
hbox.pack_start(self.spinbutton, False, False, self.spacing_size)
|
|
self.spinbutton.set_value(self.app_obj.num_worker_default)
|
|
self.spinbutton.connect('value-changed', self.on_spinbutton_changed)
|
|
|
|
label = Gtk.Label('KiB/s')
|
|
hbox.pack_end(label, False, False, 0)
|
|
|
|
self.spinbutton2 = Gtk.SpinButton.new_with_range(
|
|
self.app_obj.bandwidth_min,
|
|
self.app_obj.bandwidth_max,
|
|
1,
|
|
)
|
|
hbox.pack_end(self.spinbutton2, False, False, self.spacing_size)
|
|
self.spinbutton2.set_value(self.app_obj.bandwidth_default)
|
|
self.spinbutton2.connect('value-changed', self.on_spinbutton2_changed)
|
|
|
|
self.checkbutton2 = Gtk.CheckButton()
|
|
hbox.pack_end(self.checkbutton2, False, False, 0)
|
|
self.checkbutton2.set_label('Limit download speed to')
|
|
self.checkbutton2.set_active(self.app_obj.bandwidth_apply_flag)
|
|
self.checkbutton2.connect('toggled', self.on_checkbutton2_changed)
|
|
|
|
|
|
def setup_errors_tab(self):
|
|
|
|
"""Called by self.setup_win().
|
|
|
|
Creates widgets for the Errors Tab.
|
|
"""
|
|
|
|
vbox = Gtk.VBox()
|
|
self.errors_tab.pack_start(vbox, True, True, 0)
|
|
|
|
# Errors List
|
|
self.errors_list_scrolled = Gtk.ScrolledWindow()
|
|
vbox.pack_start(self.errors_list_scrolled, True, True, 0)
|
|
self.errors_list_scrolled.set_policy(
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
)
|
|
|
|
self.errors_list_frame = Gtk.Frame()
|
|
self.errors_list_scrolled.add_with_viewport(self.errors_list_frame)
|
|
|
|
self.errors_list_treeview = Gtk.TreeView()
|
|
self.errors_list_frame.add(self.errors_list_treeview)
|
|
|
|
for i, column_title in enumerate(['', '', 'Time', 'Media', '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)
|
|
|
|
else:
|
|
renderer_text = Gtk.CellRendererText()
|
|
column_text = Gtk.TreeViewColumn(
|
|
column_title,
|
|
renderer_text,
|
|
text=i,
|
|
)
|
|
self.errors_list_treeview.append_column(column_text)
|
|
|
|
self.errors_list_liststore = Gtk.ListStore(
|
|
GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
|
|
str, str, str, str,
|
|
)
|
|
self.errors_list_treeview.set_model(self.errors_list_liststore)
|
|
|
|
# Strip of widgets at the bottom
|
|
|
|
hbox = Gtk.HBox()
|
|
vbox.pack_start(hbox, False, False, self.spacing_size)
|
|
hbox.set_border_width(self.spacing_size)
|
|
|
|
self.error_list_button = Gtk.Button()
|
|
hbox.pack_end(self.error_list_button, False, False, 0)
|
|
self.error_list_button.set_label('Clear the list')
|
|
self.error_list_button.connect(
|
|
'clicked',
|
|
self.on_errors_list_clear,
|
|
)
|
|
|
|
|
|
# (Moodify main window widgets)
|
|
|
|
|
|
def sensitise_operation_widgets(self, flag):
|
|
|
|
"""Called by mainapp.TartubeApp.download_manager_start(),
|
|
.download_manager_finished() and self.modify_operation_widgets().
|
|
|
|
(De)sensitises widgets that must not be sensitised during a download,
|
|
update or refresh operation.
|
|
|
|
Args:
|
|
|
|
flag (True or False): False to desensitise widget at the start of
|
|
an operation, True to re-sensitise widgets at the end of the
|
|
operation
|
|
|
|
"""
|
|
|
|
self.system_prefs_menu_item.set_sensitive(flag)
|
|
self.gen_options_menu_item.set_sensitive(flag)
|
|
self.check_all_menu_item.set_sensitive(flag)
|
|
self.download_all_menu_item.set_sensitive(flag)
|
|
self.update_ytdl_menu_item.set_sensitive(flag)
|
|
self.refresh_db_menu_item.set_sensitive(flag)
|
|
|
|
self.check_all_toolbutton.set_sensitive(flag)
|
|
self.download_all_toolbutton.set_sensitive(flag)
|
|
|
|
# (The 'Save database' menu item must remain desensitised if file load/
|
|
# save is disabled)
|
|
if not self.app_obj.disable_load_save_flag:
|
|
self.save_db_menu_item.set_sensitive(flag)
|
|
|
|
# (The 'Stop' button/menu item are only sensitised during a download/
|
|
# update/refresh operation)
|
|
if not flag:
|
|
self.stop_download_menu_item.set_sensitive(True)
|
|
self.stop_download_toolbutton.set_sensitive(True)
|
|
else:
|
|
self.stop_download_menu_item.set_sensitive(False)
|
|
self.stop_download_toolbutton.set_sensitive(False)
|
|
|
|
|
|
def modify_widgets_in_update_operation(self, finish_flag):
|
|
|
|
"""Called by mainapp.TartubeApp.update_manager_start() and
|
|
.update_manager_finished().
|
|
|
|
Modify and de(sensitise) widgets during an update operation.
|
|
|
|
Args:
|
|
|
|
finish_flag (True or False): False at the start of the update
|
|
operation, True at the end of it
|
|
|
|
"""
|
|
|
|
if not finish_flag:
|
|
|
|
self.check_button.set_label('Updating')
|
|
self.check_button.set_sensitive(False)
|
|
self.download_button.set_label('youtube-dl')
|
|
self.download_button.set_sensitive(False)
|
|
self.sensitise_operation_widgets(False)
|
|
|
|
else:
|
|
self.check_button.set_label('Check all')
|
|
self.check_button.set_sensitive(True)
|
|
self.download_button.set_label('Download all')
|
|
self.download_button.set_sensitive(True)
|
|
self.sensitise_operation_widgets(True)
|
|
|
|
# Make the widget changes visible
|
|
self.show_all()
|
|
|
|
|
|
def modify_widgets_in_refresh_operation(self, finish_flag):
|
|
|
|
"""Called by mainapp.TartubeApp.refresh_manager_start() and
|
|
.refresh_manager_finished().
|
|
|
|
Modify and de(sensitise) widgets during a refresh operation.
|
|
|
|
Args:
|
|
|
|
finish_flag (True or False): False at the start of the refresh
|
|
operation, True at the end of it
|
|
|
|
"""
|
|
|
|
if not finish_flag:
|
|
|
|
self.check_button.set_label('Refreshing')
|
|
self.check_button.set_sensitive(False)
|
|
self.download_button.set_label('database')
|
|
self.download_button.set_sensitive(False)
|
|
self.sensitise_operation_widgets(False)
|
|
|
|
else:
|
|
self.check_button.set_label('Check all')
|
|
self.check_button.set_sensitive(True)
|
|
self.download_button.set_label('Download all')
|
|
self.download_button.set_sensitive(True)
|
|
self.sensitise_operation_widgets(True)
|
|
|
|
# Make the widget changes visible
|
|
self.show_all()
|
|
|
|
|
|
def show_progress_bar(self, force_sim_flag=False):
|
|
|
|
"""Called by mainapp.TartubeApp.download_manager_start().
|
|
|
|
At the start of a download operation, replace self.download_button
|
|
with a progress bar (and a label just above it).
|
|
|
|
Args:
|
|
|
|
force_sim_flag (True/False): True if playlists/channels are to be
|
|
checked for new videos, without downloading anything. False if
|
|
videos are to be downloaded (or not) depending on each media
|
|
data object's .dl_sim_flag IV
|
|
|
|
"""
|
|
|
|
if self.progress_bar:
|
|
return self.app_obj.system_error(
|
|
201,
|
|
'Videos Tab progress bar is already visible',
|
|
)
|
|
|
|
# Remove some existing widgets
|
|
self.button_box.remove(self.download_button)
|
|
|
|
# Update some existing widgets
|
|
if force_sim_flag:
|
|
self.check_button.set_label('Checking...')
|
|
else:
|
|
self.check_button.set_label('Downloading...')
|
|
|
|
# Replace the removed widgets
|
|
self.progress_bar = Gtk.ProgressBar()
|
|
self.button_box.pack_start(self.progress_bar, True, True, 0)
|
|
self.progress_bar.set_fraction(0)
|
|
self.progress_bar.set_show_text(True)
|
|
if force_sim_flag:
|
|
self.progress_bar.set_text('Checking...')
|
|
else:
|
|
self.progress_bar.set_text('Downloading...')
|
|
|
|
# The 'Download all' button will be replaced in the call to
|
|
# self.hide_progress_bar()
|
|
self.download_button = None
|
|
|
|
# Make the changes visible
|
|
self.button_box.show_all()
|
|
|
|
|
|
def hide_progress_bar(self):
|
|
|
|
"""Called by mainapp.TartubeApp.download_manager_start().
|
|
|
|
At the end of a download operation, replace self.progress_list with the
|
|
original button.
|
|
"""
|
|
|
|
if not self.progress_bar:
|
|
return self.app_obj.system_error(
|
|
202,
|
|
'Videos Tab progress bar is not already visible',
|
|
)
|
|
|
|
# Remove existing widgets...
|
|
self.button_box.remove(self.progress_bar)
|
|
|
|
# Update some existing widgets
|
|
self.check_button.set_label('Check all')
|
|
|
|
# Replace the removed widgets
|
|
self.download_button = Gtk.Button()
|
|
self.button_box.pack_start(self.download_button, True, True, 0)
|
|
self.download_button.set_label('Download all')
|
|
self.download_button.set_action_name('app.download_all_button')
|
|
|
|
# The progress bar will be replaced in any subsequent calls to
|
|
# self.show_progress_bar()
|
|
self.progress_bar = None
|
|
|
|
# Make the changes visible
|
|
self.button_box.show_all()
|
|
|
|
|
|
def update_progress_bar(self, text, count, total):
|
|
|
|
"""Called by downloads.DownloadManager.run().
|
|
|
|
During a download operation, updates the progress bar just below the
|
|
Video Index.
|
|
|
|
Args:
|
|
|
|
text (string): 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(
|
|
203,
|
|
'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)
|
|
)
|
|
|
|
|
|
# (Auto-sort functions for main window widgets)
|
|
|
|
|
|
def video_index_auto_sort(self, treestore, row_iter1, row_iter2, data):
|
|
|
|
"""Sorting function created by self.videos_tab.
|
|
|
|
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)
|
|
if obj1.name.lower() < obj2.name.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 OLDvideo_catalogue_auto_sort(self, row1, row2, data, notify):
|
|
|
|
"""Sorting function created by self.videos_tab.
|
|
|
|
Automatically sorts rows in the Video Catalogue.
|
|
|
|
Args:
|
|
|
|
row1, row2 (mainwin.CatalogueRow): Two rows in the liststore, 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, 0 if
|
|
their order should not be changed
|
|
|
|
"""
|
|
|
|
# Get the media.Video objects displayed on each row
|
|
obj1 = row1.video_obj
|
|
obj2 = row2.video_obj
|
|
|
|
# Sort videos by playlist index (if set), then by upload time, and then
|
|
# by receive (download) time
|
|
if obj1.index is not None and obj2.index is not None:
|
|
if obj1.index < obj2.index:
|
|
return -1
|
|
else:
|
|
return 1
|
|
elif obj1.upload_time < obj2.upload_time:
|
|
return 1
|
|
elif obj1.upload_time == obj2.upload_time:
|
|
if obj1.receive_time < obj2.receive_time:
|
|
return -1
|
|
elif obj1.receive_time == obj2.receive_time:
|
|
return 0
|
|
else:
|
|
return 1
|
|
else:
|
|
return -1
|
|
|
|
def video_catalogue_auto_sort(self, row1, row2, data, notify):
|
|
|
|
"""Sorting function created by self.videos_tab.
|
|
|
|
Automatically sorts rows in the Video Catalogue.
|
|
|
|
Args:
|
|
|
|
row1, row2 (mainwin.CatalogueRow): Two rows in the liststore, 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, 0 if
|
|
their order should not be changed
|
|
|
|
"""
|
|
|
|
# Get the media.Video objects displayed on each row
|
|
obj1 = row1.video_obj
|
|
obj2 = row2.video_obj
|
|
|
|
# Sort videos by playlist index (if set), then by upload time, and then
|
|
# by receive (download) time
|
|
if obj1.index is not None and obj2.index is not None:
|
|
if obj1.index < obj2.index:
|
|
return -1
|
|
else:
|
|
return 1
|
|
# # Convert Python2 to Python3
|
|
# elif obj1.upload_time < obj2.upload_time:
|
|
# return 1
|
|
# elif obj1.upload_time == obj2.upload_time:
|
|
# if obj1.receive_time < obj2.receive_time:
|
|
# return -1
|
|
# elif obj1.receive_time == obj2.receive_time:
|
|
# return 0
|
|
# else:
|
|
# return 1
|
|
elif obj1.upload_time is not None and obj2.upload_time is not None:
|
|
if obj1.upload_time < obj2.upload_time:
|
|
return 1
|
|
elif obj1.upload_time == obj2.upload_time:
|
|
if obj1.receive_time < obj2.receive_time:
|
|
return -1
|
|
elif obj1.receive_time == obj2.receive_time:
|
|
return 0
|
|
else:
|
|
return 1
|
|
else:
|
|
return -1
|
|
|
|
else:
|
|
return 0
|
|
|
|
|
|
# (Video Index)
|
|
|
|
|
|
def video_index_reset(self):
|
|
|
|
"""Called by self.setup_videos_tab() and then by
|
|
mainapp.TartubeApp.load_db(), .switch_db() and
|
|
config.SystemPrefWin.on_complex_button_toggled().
|
|
|
|
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.
|
|
"""
|
|
|
|
# If not called by self.setup_videos_tab()...
|
|
if self.video_index_treeview:
|
|
|
|
# Reset Video Index IVs
|
|
self.video_index_row_dict = {}
|
|
|
|
# Remove the old widgets
|
|
self.video_index_frame.remove(
|
|
self.video_index_frame.get_child(),
|
|
)
|
|
|
|
# Set up the widgets
|
|
self.video_index_treeview = Gtk.TreeView()
|
|
self.video_index_frame.add(self.video_index_treeview)
|
|
self.video_index_treeview.set_headers_visible(False)
|
|
# (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_target_list = [('video index', 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,
|
|
GdkPixbuf.Pixbuf,
|
|
str,
|
|
)
|
|
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,
|
|
)
|
|
|
|
count = -1
|
|
for item in ['hide', 'hide', 'pixbuf', 'show']:
|
|
|
|
count += 1
|
|
|
|
if item is 'pixbuf':
|
|
|
|
renderer_pixbuf = Gtk.CellRendererPixbuf()
|
|
column_pixbuf = Gtk.TreeViewColumn(
|
|
None,
|
|
renderer_pixbuf,
|
|
pixbuf=count,
|
|
)
|
|
self.video_index_treeview.append_column(column_pixbuf)
|
|
|
|
else:
|
|
renderer_text = Gtk.CellRendererText()
|
|
column_text = Gtk.TreeViewColumn(
|
|
None,
|
|
renderer_text,
|
|
text=count,
|
|
)
|
|
self.video_index_treeview.append_column(column_text)
|
|
if item is 'hide':
|
|
column_text.set_visible(False)
|
|
else:
|
|
column_text.set_cell_data_func(
|
|
renderer_text,
|
|
self.video_index_render_text,
|
|
)
|
|
|
|
selection = self.video_index_treeview.get_selection()
|
|
selection.connect('changed', self.on_video_index_selection_changed)
|
|
|
|
# Make the changes visible
|
|
self.video_index_treeview.show_all()
|
|
|
|
|
|
def video_index_populate(self):
|
|
|
|
"""Called by mainapp.TartubeApp.start(), .load_db() and .switch_db().
|
|
|
|
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.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(
|
|
204,
|
|
'Video Index initialisation failure',
|
|
)
|
|
|
|
else:
|
|
self.video_index_setup_row(media_data_obj, None)
|
|
|
|
|
|
def video_index_setup_row(self, media_data_obj, parent_pointer=None):
|
|
|
|
"""Called by self.video_index_repopulate(), and then 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.videx_index_get_icon(media_data_obj)
|
|
if not pixbuf:
|
|
return self.app_obj.system_error(
|
|
205,
|
|
'Video index setup row request failed sanity check',
|
|
)
|
|
|
|
# Add a row to the treeview
|
|
new_pointer = self.video_index_treestore.append(
|
|
parent_pointer,
|
|
[
|
|
media_data_obj.dbid,
|
|
media_data_obj.name,
|
|
pixbuf,
|
|
self.video_index_get_text(media_data_obj),
|
|
],
|
|
)
|
|
|
|
# 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),
|
|
)
|
|
self.video_index_row_dict[media_data_obj.name] = tree_ref
|
|
|
|
# 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):
|
|
|
|
"""Called by mainapp.TartubeApp.move_container_to_top(),
|
|
.move_container(), .mark_folder_hidden().
|
|
|
|
Also called by callbacks in mainapp.TartubeApp.on_menu_add_channel(),
|
|
.cb on_menu_add_folder() and cb on_menu_add_playlist().
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
# Don't add a hidden folder, or any of its children
|
|
if media_data_obj.is_hidden():
|
|
return
|
|
|
|
# Prepare the icon
|
|
pixbuf = self.videx_index_get_icon(media_data_obj)
|
|
if not pixbuf:
|
|
return self.app_obj.system_error(
|
|
206,
|
|
'Video index setup row request failed sanity check',
|
|
)
|
|
|
|
# Temporarily disable auto-sorting, or the call to
|
|
# Gtk.TreeView.expand_to_path() will sometimes fail
|
|
# !!! TODO BUG: Why? Who knows, it's a Gtk thing. Unfortunately that
|
|
# means rows in the Video Index might be in the wrong order, but
|
|
# that's better than a failure of .expand_to_path() )
|
|
self.video_index_no_sort_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,
|
|
pixbuf,
|
|
self.video_index_get_text(media_data_obj),
|
|
],
|
|
)
|
|
|
|
# Expand rows to make the new media data object visible...
|
|
self.video_index_treeview.expand_to_path(parent_ref.get_path())
|
|
|
|
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,
|
|
pixbuf,
|
|
self.video_index_get_text(media_data_obj),
|
|
],
|
|
)
|
|
|
|
# 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),
|
|
)
|
|
self.video_index_row_dict[media_data_obj.name] = tree_ref
|
|
|
|
# Select the row (which clears the Video Catalogue)
|
|
selection = self.video_index_treeview.get_selection()
|
|
selection.select_path(tree_ref.get_path())
|
|
|
|
# Re-enable auto-sorting, if disabled
|
|
if self.video_index_no_sort_flag:
|
|
self.video_index_no_sort_flag = False
|
|
|
|
|
|
def video_index_delete_row(self, media_data_obj):
|
|
|
|
"""Called by mainapp.TartubeApp.move_container_to_top(),
|
|
.move_container(), .delete_container() and .mark_folder_hidden().
|
|
|
|
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(
|
|
207,
|
|
'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 (which resets 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()
|
|
|
|
# Procedure complete
|
|
self.show_all()
|
|
|
|
|
|
def video_index_select_row(self, media_data_obj):
|
|
|
|
"""Called by mainapp.TartubeApp.move_container_to_top(),
|
|
.move_container() and .on_menu_add_video().
|
|
|
|
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(
|
|
208,
|
|
'Video Index select row request failed sanity check',
|
|
)
|
|
|
|
# Select the row, expanding the treeview path to make it visible, if
|
|
# necessary
|
|
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_treeview.expand_to_path(tree_path)
|
|
|
|
selection = self.video_index_treeview.get_selection()
|
|
# !!! TODO BUG: This generates a Gtk error:
|
|
# Gtk-CRITICAL **: gtk_tree_model_sort_get_path: assertion
|
|
# 'priv->stamp == iter->stamp' failed
|
|
selection.select_iter(tree_iter)
|
|
|
|
self.show_all()
|
|
|
|
|
|
def video_index_update_row_icon(self, media_data_obj):
|
|
|
|
"""Called by mainapp.TartubeApp.mark_container_favourite(),
|
|
.apply_download_options() and .remove_download_options().
|
|
|
|
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.
|
|
|
|
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(
|
|
209,
|
|
'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, self.videx_index_get_icon(media_data_obj))
|
|
|
|
|
|
def video_index_update_row_text(self, media_data_obj):
|
|
|
|
"""Called by callback in self.on_video_index_enforce_check().
|
|
|
|
Also called by mainapp.TartubeApp.add_video(), .delete_video(),
|
|
.mark_video_new(), .mark_video_downloaded(),
|
|
.mark_video_favourite() and .mark_container_favourite().
|
|
|
|
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.
|
|
|
|
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(
|
|
210,
|
|
'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_text(media_data_obj))
|
|
|
|
|
|
def videx_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.
|
|
|
|
"""
|
|
|
|
if isinstance(media_data_obj, media.Channel):
|
|
|
|
if media_data_obj.fav_flag and media_data_obj.options_obj:
|
|
icon = 'channel_both_large'
|
|
elif media_data_obj.fav_flag:
|
|
icon = 'channel_left_large'
|
|
elif media_data_obj.options_obj:
|
|
icon = 'channel_right_large'
|
|
else:
|
|
icon = 'channel_none_large'
|
|
|
|
elif isinstance(media_data_obj, media.Playlist):
|
|
|
|
if media_data_obj.fav_flag and media_data_obj.options_obj:
|
|
icon = 'playlist_both_large'
|
|
elif media_data_obj.fav_flag:
|
|
icon = 'playlist_left_large'
|
|
elif media_data_obj.options_obj:
|
|
icon = 'playlist_right_large'
|
|
else:
|
|
icon = 'playlist_none_large'
|
|
|
|
elif isinstance(media_data_obj, media.Folder):
|
|
|
|
if media_data_obj.priv_flag:
|
|
if media_data_obj.fav_flag and media_data_obj.options_obj:
|
|
icon = 'folder_private_both_large'
|
|
elif media_data_obj.fav_flag:
|
|
icon = 'folder_private_left_large'
|
|
elif media_data_obj.options_obj:
|
|
icon = 'folder_private_right_large'
|
|
else:
|
|
icon = 'folder_private_none_large'
|
|
|
|
elif media_data_obj.temp_flag:
|
|
if media_data_obj.fav_flag and media_data_obj.options_obj:
|
|
icon = 'folder_temp_both_large'
|
|
elif media_data_obj.fav_flag:
|
|
icon = 'folder_temp_left_large'
|
|
elif media_data_obj.options_obj:
|
|
icon = 'folder_temp_right_large'
|
|
else:
|
|
icon = 'folder_temp_none_large'
|
|
|
|
elif media_data_obj.fixed_flag:
|
|
if media_data_obj.fav_flag and media_data_obj.options_obj:
|
|
icon = 'folder_fixed_both_large'
|
|
elif media_data_obj.fav_flag:
|
|
icon = 'folder_fixed_left_large'
|
|
elif media_data_obj.options_obj:
|
|
icon = 'folder_fixed_right_large'
|
|
else:
|
|
icon = 'folder_fixed_none_large'
|
|
|
|
else:
|
|
if media_data_obj.fav_flag and media_data_obj.options_obj:
|
|
icon = 'folder_both_large'
|
|
elif media_data_obj.fav_flag:
|
|
icon = 'folder_left_large'
|
|
elif media_data_obj.options_obj:
|
|
icon = 'folder_right_large'
|
|
else:
|
|
icon = 'folder_none_large'
|
|
|
|
else:
|
|
return
|
|
|
|
if icon in self.icon_dict:
|
|
return self.pixbuf_dict[icon]
|
|
|
|
# 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.name,
|
|
self.tiny_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:
|
|
|
|
if media_data_obj.vid_count:
|
|
text += '\nV:' + str(media_data_obj.vid_count) \
|
|
+ ' N:' + str(media_data_obj.new_count) \
|
|
+ ' F:' + str(media_data_obj.fav_count) \
|
|
+ ' D:' + str(media_data_obj.dl_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_render_text(self, col, renderer, model, tree_iter, data):
|
|
|
|
"""Called by self.video_index_reset().
|
|
|
|
Cell renderer function. When the text column of the Video Index is
|
|
about to be rendered, set the font to normal, bold or italic, depending
|
|
on the media data object's IVs.
|
|
|
|
Args:
|
|
col (Gtk.TreeViewColumn): The treeview column about to be rendered.
|
|
|
|
renderer (Gtk.CellRendererText): The Gtk object handling the
|
|
rendering.
|
|
|
|
model (Gtk.TreeModelSort): The treeview's row data is stored here.
|
|
|
|
tree_iter (Gtk.TreeIter): A pointer to the row containing the cell
|
|
to be rendered.
|
|
|
|
data (None): Ignored
|
|
|
|
"""
|
|
|
|
dbid = model.get_value(tree_iter, 0)
|
|
media_data_obj = self.app_obj.media_reg_dict[dbid]
|
|
|
|
# If marked new (unwatched), show as bold text
|
|
if media_data_obj.new_count:
|
|
renderer.set_property('weight', Pango.Weight.BOLD)
|
|
else:
|
|
renderer.set_property('weight', Pango.Weight.NORMAL)
|
|
|
|
# If downloads disabled, show as italic text
|
|
if media_data_obj.dl_sim_flag:
|
|
renderer.set_property('style', Pango.Style.ITALIC)
|
|
else:
|
|
renderer.set_property('style', Pango.Style.NORMAL)
|
|
|
|
|
|
def video_index_popup_menu(self, event, name):
|
|
|
|
"""Called by self.video_index_treeview_click_event().
|
|
|
|
When the user right-clicks on the Video Index, show a context-sensitive
|
|
popup menu.
|
|
|
|
Args:
|
|
|
|
event (Gdk.EventButton): The mouse click event
|
|
|
|
name (string): 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]
|
|
|
|
if isinstance(media_data_obj, media.Channel):
|
|
media_type = 'channel'
|
|
elif isinstance(media_data_obj, media.Playlist):
|
|
media_type = 'playlist'
|
|
else:
|
|
media_type = 'folder'
|
|
|
|
# Set up the popup menu
|
|
popup_menu = Gtk.Menu()
|
|
|
|
# Check/download/refresh items
|
|
check_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'_Check ' + media_type,
|
|
)
|
|
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
|
|
):
|
|
check_menu_item.set_sensitive(False)
|
|
popup_menu.append(check_menu_item)
|
|
|
|
download_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'_Download ' + media_type,
|
|
)
|
|
download_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_download,
|
|
media_data_obj,
|
|
)
|
|
if self.app_obj.current_manager_obj \
|
|
or (
|
|
isinstance(media_data_obj, media.Folder) \
|
|
and media_data_obj.priv_flag
|
|
):
|
|
download_menu_item.set_sensitive(False)
|
|
popup_menu.append(download_menu_item)
|
|
|
|
refresh_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'_Refresh ' + media_type,
|
|
)
|
|
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)
|
|
popup_menu.append(refresh_menu_item)
|
|
|
|
# Separator
|
|
separator_item = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item)
|
|
|
|
# Apply/remove/edit download options, disable downloads
|
|
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,
|
|
)
|
|
popup_menu.append(apply_options_menu_item)
|
|
if 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,
|
|
)
|
|
popup_menu.append(remove_options_menu_item)
|
|
if 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)
|
|
|
|
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,
|
|
)
|
|
popup_menu.append(edit_options_menu_item)
|
|
if self.app_obj.current_manager_obj or not media_data_obj.options_obj:
|
|
edit_options_menu_item.set_sensitive(False)
|
|
|
|
enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
|
|
'D_isable downloads',
|
|
)
|
|
enforce_check_menu_item.set_active(media_data_obj.dl_sim_flag)
|
|
enforce_check_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_enforce_check,
|
|
media_data_obj,
|
|
)
|
|
popup_menu.append(enforce_check_menu_item)
|
|
if self.app_obj.current_manager_obj:
|
|
enforce_check_menu_item.set_sensitive(False)
|
|
|
|
# Separator
|
|
separator_item2 = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item2)
|
|
|
|
# Show properties/downloads, hide folder
|
|
show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'Show _properties...',
|
|
)
|
|
show_properties_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_show_properties,
|
|
media_data_obj,
|
|
)
|
|
popup_menu.append(show_properties_menu_item)
|
|
if self.app_obj.current_manager_obj:
|
|
show_properties_menu_item.set_sensitive(False)
|
|
|
|
show_downloads_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'_Show destination',
|
|
)
|
|
show_downloads_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_show_downloads,
|
|
media_data_obj,
|
|
)
|
|
popup_menu.append(show_downloads_menu_item)
|
|
if isinstance(media_data_obj, media.Folder) \
|
|
and media_data_obj.priv_flag:
|
|
show_downloads_menu_item.set_sensitive(False)
|
|
|
|
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,
|
|
)
|
|
popup_menu.append(hide_folder_menu_item)
|
|
|
|
# Move to top level
|
|
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,
|
|
)
|
|
popup_menu.append(move_top_menu_item)
|
|
if not media_data_obj.parent_obj \
|
|
or self.app_obj.current_manager_obj:
|
|
move_top_menu_item.set_sensitive(False)
|
|
|
|
# Separator
|
|
separator_item3 = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item3)
|
|
|
|
# Mark videos as new/favourite
|
|
mark_videos_submenu = Gtk.Menu()
|
|
|
|
mark_new_menu_item = Gtk.MenuItem.new_with_mnemonic('_New')
|
|
mark_new_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_mark_new,
|
|
media_data_obj,
|
|
)
|
|
mark_videos_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('N_ot new')
|
|
mark_old_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_mark_not_new,
|
|
media_data_obj,
|
|
)
|
|
mark_videos_submenu.append(mark_old_menu_item)
|
|
|
|
mark_fav_menu_item = Gtk.MenuItem.new_with_mnemonic('_Favourite')
|
|
mark_fav_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_mark_favourite,
|
|
media_data_obj,
|
|
)
|
|
mark_videos_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(
|
|
'Not f_avourite',
|
|
)
|
|
mark_not_fav_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_mark_not_favourite,
|
|
media_data_obj,
|
|
)
|
|
mark_videos_submenu.append(mark_not_fav_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)
|
|
|
|
# Separator
|
|
separator_item4 = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item4)
|
|
|
|
# Delete items
|
|
delete_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'De_lete ' + media_type,
|
|
)
|
|
delete_menu_item.connect(
|
|
'activate',
|
|
self.on_video_index_delete_container,
|
|
media_data_obj,
|
|
)
|
|
if self.app_obj.current_manager_obj:
|
|
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)
|
|
|
|
|
|
# (Video Catalogue)
|
|
|
|
|
|
def video_catalogue_reset(self):
|
|
|
|
"""Called by self.setup_videos_tab() and then by
|
|
self.video_catalogue_redraw_all().
|
|
|
|
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.video_catalogue_listbox:
|
|
self.video_catalogue_frame.remove(
|
|
self.video_catalogue_frame.get_child(),
|
|
)
|
|
|
|
# Reset IVs (when called by anything)
|
|
self.video_catalogue_list = []
|
|
self.video_catalogue_dict = {}
|
|
|
|
# Set up the widgets
|
|
listbox = Gtk.ListBox()
|
|
self.video_catalogue_frame.add(listbox)
|
|
self.video_catalogue_listbox = listbox
|
|
|
|
self.video_catalogue_listbox.set_sort_func(
|
|
self.video_catalogue_auto_sort,
|
|
None,
|
|
False,
|
|
)
|
|
|
|
# Make the changes visible
|
|
self.video_catalogue_frame.show_all()
|
|
|
|
|
|
def video_catalogue_redraw_all(self, name):
|
|
|
|
"""Called from callbacks in self.on_video_index_selection_changed(),
|
|
mainapp.TartubeApp.on_button_switch_view(),
|
|
.on_menu_add_video() and on_menu_test().
|
|
|
|
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 all the video objects stored as
|
|
children in that channel, playlist or folder.
|
|
|
|
Depending on the value of self.complex_catalogue_flag, the Video
|
|
Catalogue consists of a list of mainwin.SimpleCatalogueItem or
|
|
mainwin.ComplexCatalogueItem objects, one for each row in the
|
|
Gtk.ListBox (corresponding to a single video).
|
|
|
|
This function clears the previous contents of the Gtk.ListBox and
|
|
resets IVs.
|
|
|
|
Then, it adds new rows to the Gtk.ListBox and creates a new
|
|
mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem object for
|
|
each video.
|
|
|
|
Args:
|
|
|
|
name (string): The selected media data object's name; one of the
|
|
keys in self.media_name_dict
|
|
|
|
"""
|
|
|
|
# The parent media data object is a media.Channel, media.playlist or
|
|
# media.Folder object
|
|
dbid = self.app_obj.media_name_dict[name]
|
|
parent_obj = self.app_obj.media_reg_dict[dbid]
|
|
|
|
# Sanity check - the selected item in the Video Index should not be a
|
|
# media.Video object
|
|
if not parent_obj or (isinstance(parent_obj, media.Video)):
|
|
return self.system_error(
|
|
211,
|
|
'Videos should not appear in the Video Index',
|
|
)
|
|
|
|
# Reset the previous contents of the Video Catalogue, if any, and reset
|
|
# IVs
|
|
self.video_catalogue_reset()
|
|
|
|
# The parent media data object has any number of child media data
|
|
# objects, but we only respond to those that are media.Video objects
|
|
for child_obj in parent_obj.child_list:
|
|
|
|
if isinstance(child_obj, media.Video):
|
|
|
|
# Create a new catalogue item object for each video
|
|
if not self.app_obj.complex_catalogue_flag:
|
|
catalogue_item_obj = SimpleCatalogueItem(
|
|
self,
|
|
child_obj,
|
|
)
|
|
|
|
else:
|
|
catalogue_item_obj = ComplexCatalogueItem(
|
|
self,
|
|
child_obj,
|
|
)
|
|
|
|
self.video_catalogue_list.append(catalogue_item_obj.dbid)
|
|
self.video_catalogue_dict[catalogue_item_obj.dbid] = \
|
|
catalogue_item_obj
|
|
|
|
# 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(child_obj)
|
|
self.video_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()
|
|
|
|
# Procedure complete
|
|
self.video_catalogue_listbox.show_all()
|
|
|
|
|
|
def video_catalogue_update_row(self, video_obj):
|
|
|
|
"""Called by self.results_list_update_row and a callback in
|
|
self.on_video_catalogue_enforce_check().
|
|
|
|
Also called by mainapp.TartubeApp.create_video_from_download(),
|
|
.announce_video_download(), .mark_video_new() and
|
|
.mark_video_favourite().
|
|
|
|
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).
|
|
|
|
If the video is now yet visible in the Video Catalogue, creates a new
|
|
mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem object and
|
|
adds a row to the Gtk.ListBox.
|
|
|
|
Args:
|
|
|
|
video_obj (media.Video) - The video to update
|
|
|
|
"""
|
|
|
|
# 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
|
|
selection = self.video_index_treeview.get_selection()
|
|
(model, iter) = selection.get_selected()
|
|
|
|
if iter is None \
|
|
or (
|
|
model[iter][1] != video_obj.parent_obj.name \
|
|
and model[iter][1] != self.app_obj.fixed_all_folder.name \
|
|
and (
|
|
model[iter][1] != self.app_obj.fixed_new_folder.name \
|
|
or not video_obj.new_flag
|
|
)
|
|
):
|
|
return
|
|
|
|
# Does a mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem
|
|
# object already exist for this video?
|
|
if video_obj.dbid in self.video_catalogue_dict:
|
|
|
|
# Update the catalogue item object, which updates the widgets in
|
|
# the Gtk.ListBox
|
|
catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid]
|
|
catalogue_item_obj.update_widgets()
|
|
|
|
else:
|
|
|
|
# Create a new catalogue item object
|
|
if not self.app_obj.complex_catalogue_flag:
|
|
|
|
catalogue_item_obj = SimpleCatalogueItem(
|
|
self,
|
|
video_obj,
|
|
)
|
|
|
|
else:
|
|
catalogue_item_obj = ComplexCatalogueItem(
|
|
self,
|
|
video_obj,
|
|
)
|
|
|
|
self.video_catalogue_list.append(catalogue_item_obj.dbid)
|
|
self.video_catalogue_dict[catalogue_item_obj.dbid] \
|
|
= catalogue_item_obj
|
|
|
|
# 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(video_obj)
|
|
self.video_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()
|
|
|
|
# Force the Gtk.ListBox to sort its rows, so that videos are displayed
|
|
# in the correct order
|
|
self.video_catalogue_listbox.invalidate_sort()
|
|
|
|
# Procedure complete
|
|
self.video_catalogue_listbox.show_all()
|
|
|
|
|
|
def video_catalogue_delete_row(self, video_obj):
|
|
|
|
"""Called by mainapp.TartubeApp.delete_video(),
|
|
.mark_video_new() and .mark_video_favourite().
|
|
|
|
This function is called with a media.Video object. If that video is
|
|
already visible in the Video Catalogue, removes the corresponding
|
|
mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem .
|
|
|
|
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
|
|
selection = self.video_index_treeview.get_selection()
|
|
(model, iter) = selection.get_selected()
|
|
|
|
if iter is None \
|
|
or (
|
|
model[iter][1] != video_obj.parent_obj.name \
|
|
and model[iter][1] != self.app_obj.fixed_all_folder.name \
|
|
and (
|
|
model[iter][1] != self.app_obj.fixed_new_folder.name \
|
|
or video_obj.new_flag
|
|
)
|
|
):
|
|
return
|
|
|
|
# Does a mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem
|
|
# object exist for this video?
|
|
if video_obj.dbid in self.video_catalogue_dict:
|
|
|
|
# Remove the catalogue item object and its mainwin.CatalogueRow
|
|
# object (the latter being a wrapper for Gtk.ListBoxRow)
|
|
catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid]
|
|
|
|
# Remove the row from the Gtk.ListBox
|
|
self.video_catalogue_listbox.remove(
|
|
catalogue_item_obj.catalogue_row,
|
|
)
|
|
|
|
# Update IVs
|
|
try:
|
|
index = self.video_catalogue_list.index(
|
|
catalogue_item_obj.dbid,
|
|
)
|
|
|
|
except:
|
|
return self.app_obj.system_error(
|
|
212,
|
|
'Could not find row to delete in Video Catalogue',
|
|
)
|
|
|
|
del self.video_catalogue_list[index]
|
|
del self.video_catalogue_dict[video_obj.dbid]
|
|
|
|
# Procedure complete
|
|
self.video_catalogue_listbox.show_all()
|
|
|
|
|
|
def video_catalogue_popup_menu(self, event, video_obj):
|
|
|
|
"""Called by mainwin.SimpleCatalogueItem.on_right_click() and
|
|
mainwin.ComplexCatalogueItem.on_right_click().
|
|
|
|
When the user right-clicks on the Video Catalogue, show 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
|
|
|
|
"""
|
|
|
|
# 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,
|
|
)
|
|
if self.app_obj.current_manager_obj:
|
|
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 self.app_obj.current_manager_obj:
|
|
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 self.app_obj.current_manager_obj:
|
|
download_menu_item.set_sensitive(False)
|
|
popup_menu.append(download_menu_item)
|
|
|
|
# Separator
|
|
separator_item = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item)
|
|
|
|
# Watch video in player
|
|
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,
|
|
)
|
|
if not video_obj.dl_flag:
|
|
watch_player_menu_item.set_sensitive(False)
|
|
popup_menu.append(watch_player_menu_item)
|
|
|
|
# Watch video online. For YouTube URLs, offer an alternative website
|
|
if not video_obj.source:
|
|
watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'W_atch on website',
|
|
)
|
|
else:
|
|
mod_source = utils.convert_youtube_to_hooktube(video_obj.source)
|
|
if video_obj.source != mod_source:
|
|
watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'W_atch on YouTube',
|
|
)
|
|
|
|
else:
|
|
watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'W_atch on website',
|
|
)
|
|
|
|
watch_website_menu_item.connect(
|
|
'activate',
|
|
self.on_video_catalogue_watch_website,
|
|
video_obj,
|
|
)
|
|
if not video_obj.source:
|
|
watch_website_menu_item.set_sensitive(False)
|
|
popup_menu.append(watch_website_menu_item)
|
|
|
|
if video_obj.source and video_obj.source != mod_source:
|
|
|
|
watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'Watch on _HookTube',
|
|
)
|
|
watch_hooktube_menu_item.connect(
|
|
'activate',
|
|
self.on_video_catalogue_watch_hooktube,
|
|
video_obj,
|
|
)
|
|
popup_menu.append(watch_hooktube_menu_item)
|
|
|
|
# Separator
|
|
separator_item2 = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item2)
|
|
|
|
# New/favourite videos
|
|
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,
|
|
)
|
|
popup_menu.append(new_video_menu_item)
|
|
if not video_obj.dl_flag:
|
|
new_video_menu_item.set_sensitive(False)
|
|
|
|
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,
|
|
)
|
|
popup_menu.append(fav_video_menu_item)
|
|
if not video_obj.dl_flag:
|
|
fav_video_menu_item.set_sensitive(False)
|
|
|
|
# Separator
|
|
separator_item3 = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item3)
|
|
|
|
# Apply/remove/edit download options, disable downloads
|
|
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,
|
|
)
|
|
popup_menu.append(apply_options_menu_item)
|
|
if 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,
|
|
)
|
|
popup_menu.append(remove_options_menu_item)
|
|
if 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,
|
|
)
|
|
popup_menu.append(edit_options_menu_item)
|
|
if self.app_obj.current_manager_obj or not video_obj.options_obj:
|
|
edit_options_menu_item.set_sensitive(False)
|
|
|
|
enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
|
|
'D_isable 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,
|
|
)
|
|
popup_menu.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)
|
|
|
|
# Separator
|
|
separator_item4 = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item4)
|
|
|
|
# Show properties
|
|
show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
|
'Show _properties...',
|
|
)
|
|
show_properties_menu_item.connect(
|
|
'activate',
|
|
self.on_video_catalogue_show_properties,
|
|
video_obj,
|
|
)
|
|
popup_menu.append(show_properties_menu_item)
|
|
if self.app_obj.current_manager_obj:
|
|
show_properties_menu_item.set_sensitive(False)
|
|
|
|
# Separator
|
|
separator_item5 = Gtk.SeparatorMenuItem()
|
|
popup_menu.append(separator_item5)
|
|
|
|
# Delete video
|
|
delete_menu_item = Gtk.MenuItem.new_with_mnemonic('De_lete video')
|
|
delete_menu_item.connect(
|
|
'activate',
|
|
self.on_video_catalogue_delete_video,
|
|
video_obj,
|
|
)
|
|
if not video_obj.dl_flag:
|
|
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)
|
|
|
|
|
|
# (Progress List)
|
|
|
|
|
|
def progress_list_reset(self):
|
|
|
|
"""Called by mainapp.TartubeApp.download_manager_start().
|
|
|
|
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(
|
|
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 = {}
|
|
|
|
|
|
def progress_list_init(self, download_list_obj):
|
|
|
|
"""Called by mainapp.TartubeApp.download_manager_start().
|
|
|
|
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 dbid in download_list_obj.download_item_list:
|
|
|
|
# Create a new row in the treeview
|
|
download_item_obj = download_list_obj.download_item_dict[dbid]
|
|
row_iter = self.progress_list_liststore.append([])
|
|
|
|
# Store the row's details so we can update it later
|
|
self.progress_list_row_dict[dbid] \
|
|
= self.progress_list_row_count
|
|
self.progress_list_row_count += 1
|
|
|
|
# Prepare the icon
|
|
if isinstance(download_item_obj.media_data_obj, media.Channel):
|
|
pixbuf = self.pixbuf_dict['channel_small']
|
|
elif isinstance(download_item_obj.media_data_obj, media.Playlist):
|
|
pixbuf = self.pixbuf_dict['playlist_small']
|
|
elif isinstance(download_item_obj.media_data_obj, media.Folder):
|
|
pixbuf = self.pixbuf_dict['folder_small']
|
|
else:
|
|
pixbuf = self.pixbuf_dict['video_small']
|
|
|
|
# Set the row's initial contents
|
|
self.progress_list_liststore.set(row_iter, 0, pixbuf)
|
|
self.progress_list_liststore.set(
|
|
row_iter,
|
|
1,
|
|
utils.shorten_string(
|
|
download_item_obj.media_data_obj.name,
|
|
self.string_max_len,
|
|
)
|
|
)
|
|
self.progress_list_liststore.set(row_iter, 3, 'Waiting')
|
|
|
|
|
|
def progress_list_receive_dl_stats(self, download_item_obj, dl_stat_dict):
|
|
|
|
"""Called by downloads.DownloadWorker.talk_to_mainwin().
|
|
|
|
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 media.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.
|
|
|
|
"""
|
|
|
|
# Check that the Progress List actually has a row for the specified
|
|
# downloads.DownloadItem object
|
|
if not download_item_obj.dbid in self.progress_list_row_dict:
|
|
return self.app_obj.system_error(
|
|
213,
|
|
'Missing row in Progress List',
|
|
)
|
|
|
|
# Temporarily store the dictionary of download statistics
|
|
if not download_item_obj.dbid in self.progress_list_temp_dict:
|
|
new_dl_stat_dict = {}
|
|
else:
|
|
new_dl_stat_dict \
|
|
= self.progress_list_temp_dict[download_item_obj.dbid]
|
|
|
|
for key in dl_stat_dict:
|
|
new_dl_stat_dict[key] = dl_stat_dict[key]
|
|
|
|
self.progress_list_temp_dict[download_item_obj.dbid] \
|
|
= new_dl_stat_dict
|
|
|
|
|
|
def progress_list_display_dl_stats(self):
|
|
|
|
"""Called by mainapp.TartubeApp.timer_callback(), which is itself
|
|
called periodically by a gobject timer.
|
|
|
|
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 dbid in temp_dict:
|
|
|
|
# Get a dictionary of download statistics for this media object
|
|
# The dictionary is in the standard format described in the
|
|
# comments to media.VideoDownloader.extract_stdout_data()
|
|
dl_stat_dict = temp_dict[dbid]
|
|
|
|
# Get the corresponding treeview row
|
|
tree_path = Gtk.TreePath(self.progress_list_row_dict[dbid])
|
|
|
|
# Update statistics displayed in that row
|
|
# (Columns 0 and 1 are not modified, once the row has been added to
|
|
# the treeview)
|
|
column = 1
|
|
|
|
for key in (
|
|
'playlist_index',
|
|
'status',
|
|
'filename',
|
|
'extension',
|
|
'filesize',
|
|
'percent',
|
|
'eta',
|
|
'speed',
|
|
):
|
|
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 = utils.shorten_string(
|
|
dl_stat_dict[key],
|
|
self.string_max_len,
|
|
)
|
|
|
|
self.progress_list_liststore.set(
|
|
self.progress_list_liststore.get_iter(tree_path),
|
|
column,
|
|
string,
|
|
)
|
|
|
|
|
|
# (Results List)
|
|
|
|
|
|
def results_list_reset(self):
|
|
|
|
"""Called by mainapp.TartubeApp.download_manager_start().
|
|
|
|
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(
|
|
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 = []
|
|
|
|
|
|
def results_list_add_row(self, download_item_obj, video_obj, \
|
|
keep_description=None, keep_info=None, keep_thumbnail=None):
|
|
|
|
"""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
|
|
|
|
keep_description (True, False):
|
|
keep_info (True, False):
|
|
keep_thumbnail (True, False): Settings from the
|
|
options.OptionsManager object used to download the video (all
|
|
of them set to 'None' for a simulated download)
|
|
|
|
"""
|
|
|
|
# Create a new row in the treeview
|
|
row_iter = self.results_list_liststore.append([])
|
|
|
|
# Prepare the icons
|
|
if self.app_obj.download_manager_obj.force_sim_flag \
|
|
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(
|
|
214,
|
|
'Results List add row request failed sanity check',
|
|
)
|
|
|
|
# Set the row's initial contents
|
|
self.results_list_liststore.set(row_iter, 0, pixbuf)
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
1,
|
|
utils.shorten_string(video_obj.name, self.string_max_len),
|
|
)
|
|
|
|
# (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:
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
2,
|
|
utils.convert_seconds_to_string(video_obj.duration),
|
|
)
|
|
|
|
if video_obj.file_size:
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
3,
|
|
video_obj.get_file_size_string(),
|
|
)
|
|
|
|
if video_obj.upload_time:
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
4,
|
|
video_obj.get_upload_date_string(),
|
|
)
|
|
|
|
self.results_list_liststore.set(row_iter, 5, video_obj.dl_flag)
|
|
self.results_list_liststore.set(row_iter, 6, pixbuf2)
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
7,
|
|
utils.shorten_string(
|
|
video_obj.parent_obj.name,
|
|
self.string_max_len,
|
|
),
|
|
)
|
|
|
|
# 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,
|
|
}
|
|
|
|
if keep_description is not None:
|
|
temp_dict['keep_description'] = keep_description
|
|
|
|
if keep_info is not None:
|
|
temp_dict['keep_info'] = keep_info
|
|
|
|
if keep_thumbnail is not None:
|
|
temp_dict['keep_thumbnail'] = keep_thumbnail
|
|
|
|
# Update IVs
|
|
self.results_list_temp_list.append(temp_dict)
|
|
# (The number of rows has just increased, so increment the IV for the
|
|
# next call to this function)
|
|
self.results_list_row_count += 1
|
|
|
|
|
|
def results_list_update_row(self):
|
|
|
|
"""Called by mainapp.TartubeApp.timer_callback(), which is itself
|
|
called periodically by a gobject timer.
|
|
|
|
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 = os.path.join(
|
|
video_obj.file_dir,
|
|
video_obj.file_name + video_obj.file_ext,
|
|
)
|
|
|
|
# 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 = os.path.join(
|
|
video_obj.file_dir,
|
|
video_obj.file_name + '.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,
|
|
)
|
|
|
|
# Update the video catalogue in the 'Videos' tab
|
|
self.video_catalogue_update_row(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 Progress List
|
|
tree_path = Gtk.TreePath(temp_dict['row_num'])
|
|
row_iter = self.results_list_liststore.get_iter(tree_path)
|
|
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
1,
|
|
utils.shorten_string(video_obj.name, self.string_max_len),
|
|
)
|
|
|
|
if video_obj.duration is not None:
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
2,
|
|
utils.convert_seconds_to_string(
|
|
video_obj.duration,
|
|
),
|
|
)
|
|
|
|
if video_obj.file_size:
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
3,
|
|
video_obj.get_file_size_string(),
|
|
)
|
|
|
|
if video_obj.upload_time:
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
4,
|
|
video_obj.get_upload_date_string(),
|
|
)
|
|
|
|
self.results_list_liststore.set(row_iter, 5, video_obj.dl_flag)
|
|
self.results_list_liststore.set(row_iter, 6, pixbuf)
|
|
|
|
self.results_list_liststore.set(
|
|
row_iter,
|
|
7,
|
|
utils.shorten_string(
|
|
video_obj.parent_obj.name,
|
|
self.string_max_len,
|
|
),
|
|
)
|
|
|
|
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
|
|
|
|
|
|
# (Errors List)
|
|
|
|
|
|
def errors_list_reset(self):
|
|
|
|
"""Called by self.on_errors_list_clear() and various functions in the
|
|
main application.
|
|
|
|
Empties the Gtk.TreeView in the Errors List, ready for it to be
|
|
refilled.
|
|
|
|
(There are no IVs to reset.)
|
|
"""
|
|
|
|
# Reset widgets
|
|
self.errors_list_liststore = Gtk.ListStore(
|
|
GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
|
|
str, str, str, str,
|
|
)
|
|
self.errors_list_treeview.set_model(self.errors_list_liststore)
|
|
|
|
|
|
def errors_list_add_row(self, media_data_obj):
|
|
|
|
"""Called by downloads.DownloadWorker.run().
|
|
|
|
When a download job generates error and/or warning messages, display
|
|
this function is called to display them in the Errors List.
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
# Create a new row for every error and warning message
|
|
# Use the same time on each
|
|
utc = datetime.datetime.utcfromtimestamp(time.time())
|
|
time_string = str(utc.strftime('%H:%M:%S'))
|
|
|
|
for msg in media_data_obj.error_list:
|
|
|
|
# Create a new row in the treeview
|
|
row_iter = self.errors_list_liststore.append([])
|
|
|
|
# Prepare the pixbufs
|
|
pixbuf = self.pixbuf_dict['error_small']
|
|
|
|
if isinstance(media_data_obj, media.Video):
|
|
pixbuf2 = self.pixbuf_dict['video_small']
|
|
elif isinstance(media_data_obj, media.Channel):
|
|
pixbuf2 = self.pixbuf_dict['channel_small']
|
|
elif isinstance(media_data_obj, media.Playlist):
|
|
pixbuf2 = self.pixbuf_dict['playlist_small']
|
|
else:
|
|
return self.app_obj.system_error(
|
|
215,
|
|
'Errors List add row request failed sanity check',
|
|
)
|
|
|
|
# Set the row's contents
|
|
self.errors_list_liststore.set(row_iter, 0, pixbuf)
|
|
self.errors_list_liststore.set(row_iter, 1, pixbuf2)
|
|
self.errors_list_liststore.set(row_iter, 2, time_string)
|
|
self.errors_list_liststore.set(
|
|
row_iter,
|
|
3,
|
|
utils.shorten_string(media_data_obj.name, self.string_max_len),
|
|
)
|
|
self.errors_list_liststore.set(
|
|
row_iter,
|
|
4,
|
|
utils.tidy_up_long_string(msg),
|
|
)
|
|
|
|
# (Don't update the Errors/Warnings tab label if it's the visible
|
|
# tab)
|
|
if self.visible_tab_num != 2:
|
|
self.tab_error_count += 1
|
|
|
|
for msg in media_data_obj.warning_list:
|
|
|
|
# Create a new row in the treeview
|
|
row_iter = self.errors_list_liststore.append([])
|
|
|
|
# Prepare the pixbuf
|
|
pixbuf = self.pixbuf_dict['warning_small']
|
|
|
|
if isinstance(media_data_obj, media.Video):
|
|
pixbuf2 = self.pixbuf_dict['video_small']
|
|
elif isinstance(media_data_obj, media.Channel):
|
|
pixbuf2 = self.pixbuf_dict['channel_small']
|
|
elif isinstance(media_data_obj, media.Playlist):
|
|
pixbuf2 = self.pixbuf_dict['playlist_small']
|
|
else:
|
|
return self.app_obj.system_error(
|
|
216,
|
|
'Errors List add row request failed sanity check',
|
|
)
|
|
|
|
# Set the row's contents
|
|
self.errors_list_liststore.set(row_iter, 0, pixbuf)
|
|
self.errors_list_liststore.set(row_iter, 1, pixbuf2)
|
|
self.errors_list_liststore.set(row_iter, 2, time_string)
|
|
self.errors_list_liststore.set(
|
|
row_iter,
|
|
3,
|
|
utils.shorten_string(media_data_obj.name, self.string_max_len),
|
|
)
|
|
self.errors_list_liststore.set(
|
|
row_iter,
|
|
4,
|
|
utils.tidy_up_long_string(msg),
|
|
)
|
|
|
|
# (Don't update the Errors/Warnings tab label if it's the visible
|
|
# tab)
|
|
if self.visible_tab_num != 2:
|
|
self.tab_warning_count += 1
|
|
|
|
# Update the tab's label to show the number of warnings/errors visible
|
|
if self.visible_tab_num != 2:
|
|
self.errors_list_refresh_label()
|
|
|
|
|
|
def errors_list_add_system_error(self, 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.
|
|
|
|
Args:
|
|
|
|
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 in the treeview
|
|
row_iter = self.errors_list_liststore.append([])
|
|
|
|
# Prepare the pixbufs
|
|
pixbuf = self.pixbuf_dict['error_small']
|
|
pixbuf2 = self.pixbuf_dict['system_error_small']
|
|
|
|
# Set the row's contents
|
|
utc = datetime.datetime.utcfromtimestamp(time.time())
|
|
time_string = str(utc.strftime('%H:%M:%S'))
|
|
|
|
self.errors_list_liststore.set(row_iter, 0, pixbuf)
|
|
self.errors_list_liststore.set(row_iter, 1, pixbuf2)
|
|
self.errors_list_liststore.set(row_iter, 2, time_string)
|
|
self.errors_list_liststore.set(
|
|
row_iter,
|
|
3,
|
|
utils.upper_case_first(__main__.__packagename__) + ' error',
|
|
)
|
|
self.errors_list_liststore.set(
|
|
row_iter,
|
|
4,
|
|
utils.tidy_up_long_string(str(error_code) + ': ' + msg),
|
|
)
|
|
|
|
# (Don't update the Errors/Warnings tab label if it's the visible
|
|
# tab)
|
|
if self.visible_tab_num != 2:
|
|
self.tab_error_count += 1
|
|
self.errors_list_refresh_label()
|
|
|
|
|
|
def errors_list_refresh_label(self):
|
|
|
|
"""Called by self.errors_list_add_row(),
|
|
.errors_list_add_system_error() and .on_notebook_switch_page().
|
|
|
|
When the Errors / Warnings tab becomes the visible one, reset the
|
|
tab's label (to show 'Errors / Warnings')
|
|
|
|
When an error or warning is added to the Error List, refresh the tab's
|
|
label (to show something like 'Errors (4) / Warnings (1)' )
|
|
"""
|
|
|
|
text = '_Errors'
|
|
if self.tab_error_count:
|
|
text += ' (' + str(self.tab_error_count) + ')'
|
|
|
|
text += ' / Warnings'
|
|
if self.tab_warning_count:
|
|
text += ' (' + str(self.tab_warning_count) + ')'
|
|
|
|
self.errors_label.set_text_with_mnemonic(text)
|
|
|
|
|
|
# Callback class methods
|
|
|
|
|
|
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 == 2:
|
|
self.tab_error_count = 0
|
|
self.tab_warning_count = 0
|
|
self.errors_list_refresh_label()
|
|
|
|
|
|
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(
|
|
217,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# 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,
|
|
media_data_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(
|
|
218,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Start a download operation
|
|
self.app_obj.download_manager_start(True, media_data_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.Channel):
|
|
The clicked media data object
|
|
|
|
"""
|
|
|
|
self.app_obj.delete_container(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(
|
|
219,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Start a download operation
|
|
self.app_obj.download_manager_start(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.setup_videos_tab().
|
|
|
|
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')
|
|
|
|
# Extract the drop destination
|
|
drop_info = treeview.get_dest_row_at_pos(x, y)
|
|
if drop_info is not None:
|
|
|
|
# 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]
|
|
|
|
# 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 drag_name and dest_name:
|
|
|
|
drag_id = self.app_obj.media_name_dict[drag_name]
|
|
dest_id = self.app_obj.media_name_dict[dest_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.setup_videos_tab().
|
|
|
|
Override the usual Gtk handler, and allow
|
|
self.on_video_index_drag_data_received() to collect the results of the
|
|
drag procedure.
|
|
|
|
Args:
|
|
|
|
treeview (Gtk.TreeView): 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(
|
|
220,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Open an edit window
|
|
config.OptionsEditWin(
|
|
self.app_obj,
|
|
media_data_obj.options_obj,
|
|
media_data_obj,
|
|
)
|
|
|
|
|
|
def on_video_index_enforce_check(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(
|
|
221,
|
|
'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)
|
|
|
|
self.video_index_update_row_text(media_data_obj)
|
|
|
|
|
|
def on_video_index_mark_favourite(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 chidlren, 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
|
|
|
|
"""
|
|
|
|
self.app_obj.mark_container_favourite(media_data_obj, True)
|
|
|
|
|
|
def on_video_index_mark_not_favourite(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 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
|
|
|
|
"""
|
|
|
|
self.app_obj.mark_container_favourite(media_data_obj, False)
|
|
|
|
|
|
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_new(self, menu_item, media_data_obj):
|
|
|
|
"""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
|
|
|
|
"""
|
|
|
|
# Special arrangements for private folders
|
|
# (Don't need to check the 'New Videos' folder, as the popup menu item
|
|
# is commented out for that)
|
|
if media_data_obj == self.app_obj.fixed_all_folder:
|
|
|
|
# Check every video
|
|
for other_obj in list(self.app_obj.media_reg_dict.values()):
|
|
|
|
if isinstance(other_obj, media.Video):
|
|
self.app_obj.mark_video_new(other_obj, True)
|
|
|
|
elif media_data_obj == self.app_obj.fixed_fav_folder:
|
|
|
|
# Check every favourite video
|
|
for other_obj in list(self.app_obj.media_reg_dict.values()):
|
|
|
|
if isinstance(other_obj, media.Video) and other_obj.fav_flag:
|
|
self.app_obj.mark_video_new(other_obj, True)
|
|
|
|
else:
|
|
|
|
# Check only videos that are descendants of the specified media
|
|
# data object
|
|
for other_obj in media_data_obj.compile_all_videos( [] ):
|
|
|
|
if other_obj.dl_flag:
|
|
self.app_obj.mark_video_new(other_obj, True)
|
|
|
|
|
|
def on_video_index_mark_not_new(self, menu_item, media_data_obj):
|
|
|
|
"""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
|
|
|
|
"""
|
|
|
|
# Special arrangements for private folders
|
|
if media_data_obj == self.app_obj.fixed_all_folder \
|
|
or media_data_obj == self.app_obj.fixed_new_folder:
|
|
|
|
# Check every video
|
|
for other_obj in list(self.app_obj.media_reg_dict.values()):
|
|
|
|
if isinstance(other_obj, media.Video):
|
|
self.app_obj.mark_video_new(other_obj, False)
|
|
|
|
elif media_data_obj == self.app_obj.fixed_fav_folder:
|
|
|
|
# Check every favourite video
|
|
for other_obj in list(self.app_obj.media_reg_dict.values()):
|
|
|
|
if isinstance(other_obj, media.Video) and other_obj.fav_flag:
|
|
self.app_obj.mark_video_new(other_obj, False)
|
|
|
|
else:
|
|
|
|
# Check only videos that are descendants of the specified media
|
|
# data object
|
|
for other_obj in media_data_obj.compile_all_videos( [] ):
|
|
self.app_obj.mark_video_new(other_obj, False)
|
|
|
|
|
|
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_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(
|
|
222,
|
|
'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(
|
|
223,
|
|
'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_right_click(self, treeview, event):
|
|
|
|
"""Called from callback in self.setup_videos_tab().
|
|
|
|
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),
|
|
)
|
|
|
|
iter = self.video_index_sortmodel.get_iter(path)
|
|
if iter is not None:
|
|
self.video_index_popup_menu(
|
|
event,
|
|
self.video_index_sortmodel[iter][1],
|
|
)
|
|
|
|
|
|
def on_video_index_selection_changed(self, selection):
|
|
|
|
"""Called from callback in self.on_video_index_selection_changed().
|
|
|
|
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, iter) = selection.get_selected()
|
|
|
|
# Don't update the Video Catalogue during certain proecudres, 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 iter is None:
|
|
self.video_index_current = None
|
|
self.video_catalogue_reset()
|
|
else:
|
|
self.video_index_current = model[iter][1]
|
|
self.video_catalogue_redraw_all(model[iter][1])
|
|
|
|
|
|
def on_video_index_show_downloads(self, menu_item, media_data_obj):
|
|
|
|
"""Called from a callback in self.video_index_popup_menu().
|
|
|
|
Opens the sub-directory folder in which downloads for the specified
|
|
media data object are stored.
|
|
|
|
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_dir(self.app_obj)
|
|
utils.open_file(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(
|
|
224,
|
|
'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_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(
|
|
225,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Apply download options to the media data object
|
|
media_data_obj.set_options_obj(options.OptionsManager())
|
|
# Update the video catalogue to show the right icon
|
|
self.video_catalogue_update_row(media_data_obj)
|
|
|
|
# Open an edit window to show the options immediately
|
|
config.OptionsEditWin(
|
|
self.app_obj,
|
|
media_data_obj.options_obj,
|
|
media_data_obj,
|
|
)
|
|
|
|
|
|
def on_video_catalogue_check(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
|
|
|
|
"""
|
|
|
|
if self.app_obj.current_manager_obj:
|
|
return self.app_obj.system_error(
|
|
226,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Start a download operation
|
|
self.app_obj.download_manager_start(True, media_data_obj)
|
|
|
|
|
|
def on_video_catalogue_delete_video(self, menu_item, media_data_obj):
|
|
|
|
"""Called from a callback in self.video_catalogue_popup_menu().
|
|
|
|
Deletes the video.
|
|
"""
|
|
|
|
self.app_obj.delete_video(media_data_obj)
|
|
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
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(False, media_data_obj)
|
|
|
|
|
|
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(
|
|
228,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Open an edit window
|
|
config.OptionsEditWin(
|
|
self.app_obj,
|
|
media_data_obj.options_obj,
|
|
media_data_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(
|
|
229,
|
|
'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)
|
|
|
|
self.video_catalogue_update_row(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(
|
|
230,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# If the file exists, delete it (otherwise youtube-dl won't download
|
|
# anything)
|
|
# Don't even check media.Video.dl_flag: the file might exist, even if
|
|
# the flag has not been set
|
|
if media_data_obj.file_dir:
|
|
|
|
path = os.path.join(
|
|
media_data_obj.file_dir,
|
|
media_data_obj.file_name + media_data_obj.file_ext,
|
|
)
|
|
|
|
if os.path.isfile(path):
|
|
os.remove(path)
|
|
|
|
# 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(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(
|
|
231,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Remove download options from the media data object
|
|
media_data_obj.set_options_obj(None)
|
|
# Update the video catalogue to show the right icon
|
|
self.video_catalogue_update_row(media_data_obj)
|
|
|
|
|
|
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(
|
|
232,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Open the edit window immediately
|
|
config.VideoEditWin(self.app_obj, media_data_obj)
|
|
|
|
|
|
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_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_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(
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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(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)
|
|
|
|
|
|
def on_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.checkbutton.get_active():
|
|
self.app_obj.set_num_worker_default(
|
|
int(self.spinbutton.get_value())
|
|
)
|
|
|
|
|
|
def on_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.checkbutton.get_active():
|
|
|
|
self.app_obj.set_num_worker_apply_flag(True)
|
|
self.app_obj.set_num_worker_default(
|
|
int(self.spinbutton.get_value())
|
|
)
|
|
|
|
else:
|
|
|
|
self.app_obj.set_num_worker_apply_flag(False)
|
|
|
|
|
|
def on_spinbutton2_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
|
|
|
|
"""
|
|
|
|
self.app_obj.set_bandwidth_default(
|
|
int(self.spinbutton2.get_value())
|
|
)
|
|
|
|
|
|
def on_checkbutton2_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
|
|
|
|
"""
|
|
|
|
self.app_obj.set_bandwidth_apply_flag(self.checkbutton2.get_active())
|
|
|
|
|
|
def on_errors_list_clear(self, button):
|
|
|
|
"""Called from callback in self.setup_errors_tab().
|
|
|
|
In the Errors Tab, when the user clicks the 'Clear the list' button,
|
|
clear the Errors List.
|
|
|
|
Args:
|
|
|
|
button (Gtk.Button): The clicked widget
|
|
|
|
"""
|
|
|
|
self.errors_list_reset()
|
|
|
|
|
|
# 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(
|
|
233,
|
|
'Callback request denied due to current conditions',
|
|
)
|
|
|
|
# Update the IV
|
|
self.config_win_list.append(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 the IV
|
|
# (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)
|
|
|
|
|
|
class SimpleCatalogueItem(object):
|
|
|
|
"""Python class that handles a single row in the Video Catalogue.
|
|
|
|
Each mainwin.SimpleCatalogueItem objects 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.status_image = None # Gtk.Image
|
|
self.name_label = None # Gtk.Label
|
|
self.stats_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
|
|
|
|
|
|
# Public class methods
|
|
|
|
|
|
def draw_widgets(self, catalogue_row):
|
|
|
|
"""Called by mainwin.MainWin.video_catalogue_redraw_all() and
|
|
.video_catalogue_update_row().
|
|
|
|
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)
|
|
|
|
hbox = Gtk.Box(
|
|
orientation=Gtk.Orientation.HORIZONTAL,
|
|
spacing=0,
|
|
)
|
|
event_box.add(hbox)
|
|
hbox.set_border_width(self.spacing_size)
|
|
|
|
self.status_image = Gtk.Image()
|
|
hbox.pack_start(self.status_image, False, False, self.spacing_size)
|
|
|
|
vbox = Gtk.Box(
|
|
orientation=Gtk.Orientation.VERTICAL,
|
|
spacing=0,
|
|
)
|
|
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)
|
|
|
|
# Video stats
|
|
self.stats_label = Gtk.Label('', xalign=0)
|
|
vbox.pack_start(self.stats_label, True, True, 0)
|
|
|
|
|
|
def update_widgets(self):
|
|
|
|
"""Called by mainwin.MainWin.video_catalogue_redraw_all() and
|
|
.video_catalogue_update_row().
|
|
|
|
Sets the values displayed by each widget.
|
|
"""
|
|
|
|
self.update_status_image()
|
|
self.update_video_name()
|
|
self.update_video_stats()
|
|
|
|
|
|
def update_status_image(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.dl_flag:
|
|
self.status_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['have_file_small'],
|
|
)
|
|
else:
|
|
self.status_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['no_file_small'],
|
|
)
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
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 + '>' + \
|
|
cgi.escape(
|
|
utils.shorten_string(
|
|
self.video_obj.name,
|
|
self.main_win_obj.long_string_max_len,
|
|
),
|
|
quote=True,
|
|
) + '</span>'
|
|
)
|
|
|
|
|
|
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.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 is not None:
|
|
string = string + ' - Size: ' + size
|
|
else:
|
|
string = string + ' - Size: <i>unknown</i>'
|
|
|
|
date = self.video_obj.get_upload_date_string()
|
|
if date is not None:
|
|
string = string + ' - Date: ' + date
|
|
else:
|
|
string = string + ' - Date: <i>unknown</i>'
|
|
|
|
self.stats_label.set_markup(string)
|
|
|
|
|
|
# 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):
|
|
|
|
"""Python class that handles a single row in the Video Catalogue.
|
|
|
|
Each mainwin.ComplexCatalogueItem objects 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.thumb_image = None # Gtk.Image
|
|
self.name_label = None # Gtk.Label
|
|
self.status_image = None # Gtk.Image
|
|
self.error_image = None # Gtk.Image
|
|
self.warning_image = None # Gtk.Image
|
|
self.descrip_label = None # Gtk.Label
|
|
self.expand_label = None # Gtk.Label
|
|
self.stats_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
|
|
|
|
|
|
# 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
|
|
|
|
|
|
# Public class methods
|
|
|
|
|
|
def draw_widgets(self, catalogue_row):
|
|
|
|
"""Called by mainwin.MainWin.video_catalogue_redraw_all() and
|
|
.video_catalogue_update_row().
|
|
|
|
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)
|
|
|
|
frame = Gtk.Frame()
|
|
event_box.add(frame)
|
|
frame.set_border_width(self.spacing_size)
|
|
|
|
hbox = Gtk.Box(
|
|
orientation=Gtk.Orientation.HORIZONTAL,
|
|
spacing=0,
|
|
)
|
|
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
|
|
vbox = Gtk.Box(
|
|
orientation=Gtk.Orientation.VERTICAL,
|
|
spacing=0,
|
|
)
|
|
hbox.pack_start(vbox, False, False, 0)
|
|
|
|
self.thumb_image = Gtk.Image()
|
|
vbox.pack_start(self.thumb_image, False, False, 0)
|
|
|
|
# Everything to the right of the thumbnail is in a vbox2
|
|
vbox2 = Gtk.Box(
|
|
orientation=Gtk.Orientation.VERTICAL,
|
|
spacing=0,
|
|
)
|
|
hbox.pack_start(vbox2, True, True, self.spacing_size)
|
|
|
|
# Video name
|
|
hbox2 = Gtk.Box(
|
|
orientation=Gtk.Orientation.HORIZONTAL,
|
|
spacing=0,
|
|
)
|
|
vbox2.pack_start(hbox2, True, True, 0)
|
|
|
|
self.name_label = Gtk.Label('', xalign = 0)
|
|
hbox2.pack_start(self.name_label, True, True, 0)
|
|
|
|
# Status/error/warning icons
|
|
self.status_image = Gtk.Image()
|
|
hbox2.pack_end(self.status_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, self.spacing_size)
|
|
|
|
# Video description (incorporating the the More/Less label)
|
|
self.descrip_label = Gtk.Label('', xalign=0)
|
|
vbox2.pack_start(self.descrip_label, True, True, 0)
|
|
self.descrip_label.connect(
|
|
'activate-link',
|
|
self.on_click_descrip_label,
|
|
)
|
|
|
|
# Video stats
|
|
self.stats_label = Gtk.Label('', xalign=0)
|
|
vbox2.pack_start(self.stats_label, True, True, 0)
|
|
|
|
hbox3 = Gtk.Box(
|
|
orientation=Gtk.Orientation.HORIZONTAL,
|
|
spacing=0,
|
|
)
|
|
vbox2.pack_start(hbox3, True, True, 0)
|
|
|
|
# Watch...
|
|
self.watch_label = Gtk.Label('Watch: ', xalign=0)
|
|
hbox3.pack_start(self.watch_label, False, False, 0)
|
|
|
|
# Watch in player
|
|
self.watch_player_label = Gtk.Label('', xalign=0)
|
|
hbox3.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)
|
|
hbox3.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)
|
|
hbox3.pack_start(self.watch_hooktube_label, False, False, 0)
|
|
self.watch_hooktube_label.connect(
|
|
'activate-link',
|
|
self.on_click_watch_hooktube_label,
|
|
)
|
|
|
|
|
|
def update_widgets(self):
|
|
|
|
"""Called by mainwin.MainWin.video_catalogue_redraw_all() and
|
|
.video_catalogue_update_row().
|
|
|
|
Sets the values displayed by each widget.
|
|
"""
|
|
|
|
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()
|
|
|
|
|
|
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_dir:
|
|
|
|
# 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
|
|
thumb_flag = True
|
|
self.thumb_image.set_from_pixbuf(
|
|
self.main_win_obj.app_obj.file_manager_obj.load_to_pixbuf(
|
|
path,
|
|
self.main_win_obj.thumb_width,
|
|
self.main_win_obj.thumb_height,
|
|
),
|
|
)
|
|
|
|
# No thumbnail file found, so use a standard icon 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['video_both_large'],
|
|
)
|
|
elif self.video_obj.fav_flag:
|
|
self.thumb_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['video_left_large'],
|
|
)
|
|
elif self.video_obj.options_obj:
|
|
self.thumb_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['video_right_large'],
|
|
)
|
|
else:
|
|
self.thumb_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['video_none_large'],
|
|
)
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
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 + '>' + \
|
|
cgi.escape(
|
|
utils.shorten_string(
|
|
self.video_obj.name,
|
|
self.main_win_obj.medium_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.
|
|
"""
|
|
|
|
# Set the download status
|
|
if self.video_obj.dl_flag:
|
|
self.status_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['have_file_small'],
|
|
)
|
|
else:
|
|
self.status_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['no_file_small'],
|
|
)
|
|
|
|
# Set an indication of any error/warning messages. If there is an error
|
|
# but no warning, show the error icon in the warning image (so there
|
|
# isn't a large gap in the middle)
|
|
if self.video_obj.error_list and self.video_obj.warning_list:
|
|
|
|
self.warning_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['warning_small'],
|
|
)
|
|
|
|
self.error_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['error_small'],
|
|
)
|
|
|
|
elif self.video_obj.error_list:
|
|
|
|
self.warning_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['error_small'],
|
|
)
|
|
|
|
self.error_image.clear()
|
|
|
|
elif self.video_obj.warning_list:
|
|
|
|
self.warning_image.set_from_pixbuf(
|
|
self.main_win_obj.pixbuf_dict['warning_small'],
|
|
)
|
|
|
|
self.error_image.clear()
|
|
|
|
else:
|
|
|
|
self.error_image.clear()
|
|
self.warning_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.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
|
|
descrip = cgi.escape(self.video_obj.descrip, quote=True)
|
|
line_list = descrip.split('\n')
|
|
|
|
if not self.expand_descrip_flag:
|
|
|
|
string = utils.shorten_string(
|
|
line_list[0],
|
|
self.main_win_obj.long_string_max_len,
|
|
)
|
|
|
|
if len(line_list) > 1:
|
|
self.descrip_label.set_markup(
|
|
'<a href="more">More</a> ' + string,
|
|
)
|
|
else:
|
|
self.descrip_label.set_text(string)
|
|
|
|
else:
|
|
|
|
if len(line_list) > 1:
|
|
self.descrip_label.set_markup(
|
|
'<a href="less">Less</a> ' + descrip,
|
|
)
|
|
else:
|
|
self.descrip_label.set_text(descrip)
|
|
|
|
else:
|
|
self.descrip_label.set_markup('<i>No description set</i>')
|
|
|
|
|
|
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.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 is not None:
|
|
string = string + ' - Size: ' + size
|
|
else:
|
|
string = string + ' - Size: <i>unknown</i>'
|
|
|
|
date = self.video_obj.get_upload_date_string()
|
|
if date is not None:
|
|
string = string + ' - Date: ' + date
|
|
else:
|
|
string = string + ' - Date: <i>unknown</i>'
|
|
|
|
self.stats_label.set_markup(string)
|
|
|
|
|
|
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_dir and self.video_obj.dl_flag:
|
|
|
|
# Link clickable
|
|
self.watch_player_label.set_markup('<a href="watch">Player</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.
|
|
"""
|
|
|
|
if self.video_obj.source:
|
|
|
|
# Convert a YouTube link into a HookTube link (but don't modify any
|
|
# other weblink)
|
|
mod_source = utils.convert_youtube_to_hooktube(
|
|
self.video_obj.source,
|
|
)
|
|
|
|
# For YouTube URLs, offer an alternative website
|
|
if self.video_obj.source != mod_source:
|
|
|
|
# Links clickable
|
|
self.watch_web_label.set_markup(
|
|
'<a href="' + self.video_obj.source + \
|
|
'">YouTube</a>',
|
|
)
|
|
|
|
self.watch_hooktube_label.set_markup(
|
|
'<a href="' + mod_source + '">HookTube</a>',
|
|
)
|
|
|
|
else:
|
|
|
|
self.watch_web_label.set_markup(
|
|
'<a href="' + self.video_obj.source + \
|
|
'">Website</a>',
|
|
)
|
|
|
|
self.watch_hooktube_label.set_text('')
|
|
|
|
else:
|
|
|
|
# Link not clickable
|
|
self.watch_web_label.set_markup('<i>No weblink</i>')
|
|
self.watch_hooktube_label.set_text('')
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
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 (string): Ignored
|
|
|
|
"""
|
|
|
|
if not self.expand_descrip_flag:
|
|
self.expand_descrip_flag = True
|
|
else:
|
|
self.expand_descrip_flag = False
|
|
|
|
self.update_video_descrip()
|
|
|
|
|
|
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 (string): Ignored
|
|
|
|
Returns:
|
|
|
|
True to show the action has been handled
|
|
|
|
"""
|
|
|
|
# Launch the video
|
|
utils.open_file(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)
|
|
|
|
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 (string): Ignored
|
|
|
|
Returns:
|
|
|
|
True to show the action has been handled
|
|
|
|
"""
|
|
|
|
# Launch the video
|
|
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)
|
|
|
|
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 (string): Ignored
|
|
|
|
Returns:
|
|
|
|
True to show the action has been handled
|
|
|
|
"""
|
|
|
|
# Launch the video
|
|
utils.open_file(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)
|
|
|
|
return True
|
|
|
|
|
|
class CatalogueRow(Gtk.ListBoxRow):
|
|
|
|
"""Python class acting as a wrapper for Gtk.ListBoxRow, so that we can
|
|
retrieve the media.Video object displayed in each row.
|
|
|
|
Args:
|
|
|
|
video_obj (media.Video): The video object displayed on this row
|
|
|
|
"""
|
|
|
|
|
|
# Standard class methods
|
|
|
|
|
|
def __init__(self, video_obj):
|
|
|
|
super(Gtk.ListBoxRow, self).__init__()
|
|
|
|
# IV list - class objects
|
|
# -----------------------
|
|
|
|
self.video_obj = video_obj
|
|
|
|
|
|
# (Dialogue window classes)
|
|
|
|
|
|
class AddVideoDialogue(Gtk.Dialog):
|
|
|
|
"""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):
|
|
|
|
Gtk.Dialog.__init__(
|
|
self,
|
|
'Add videos',
|
|
main_win_obj,
|
|
0,
|
|
(
|
|
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_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)
|
|
|
|
frame = Gtk.Frame()
|
|
grid.attach(frame, 0, 1, 1, 1)
|
|
|
|
scrolledwindow = Gtk.ScrolledWindow()
|
|
frame.add(scrolledwindow)
|
|
# (Set enough vertical room for at least five URLs)
|
|
scrolledwindow.set_size_request(-1, 100)
|
|
|
|
textview = Gtk.TextView()
|
|
scrolledwindow.add(textview)
|
|
|
|
# (Store various widgets as IVs, so the calling function can retrieve
|
|
# their contents)
|
|
self.textbuffer = textview.get_buffer()
|
|
|
|
separator = Gtk.HSeparator()
|
|
grid.attach(separator, 0, 2, 1, 1)
|
|
|
|
self.button = Gtk.RadioButton.new_with_label_from_widget(
|
|
None,
|
|
'I want to download these videos automatically',
|
|
)
|
|
grid.attach(self.button, 0, 3, 1, 1)
|
|
|
|
self.button2 = Gtk.RadioButton.new_from_widget(self.button)
|
|
self.button2.set_label(
|
|
'Don\'t download anything, just check the videos',
|
|
)
|
|
grid.attach(self.button2, 0, 4, 1, 1)
|
|
|
|
separator2 = Gtk.HSeparator()
|
|
grid.attach(separator2, 0, 5, 1, 1)
|
|
|
|
# (There are two fixed folders always at the top of this list)
|
|
self.folder_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 not media_data_obj.restrict_flag:
|
|
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)
|
|
|
|
label2 = Gtk.Label('Add the videos to this folder')
|
|
grid.attach(label2, 0, 6, 1, 1)
|
|
|
|
listmodel = Gtk.ListStore(str)
|
|
for item in self.folder_list:
|
|
listmodel.append([item])
|
|
|
|
combo = Gtk.ComboBox.new_with_model(listmodel)
|
|
grid.attach(combo, 0, 7, 1, 1)
|
|
|
|
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)
|
|
|
|
# Paste in the contents of the clipboard (if it contains valid URLs)
|
|
utils.add_links_from_clipboard(main_win_obj.app_obj, self.textbuffer)
|
|
|
|
# Display the dialogue window
|
|
self.show_all()
|
|
|
|
|
|
# Public 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 AddChannelDialogue(Gtk.Dialog):
|
|
|
|
"""Python class handling a dialogue window that adds a channel to the media
|
|
registry.
|
|
|
|
Args:
|
|
|
|
main_win_obj (mainwin.MainWin): The parent main window
|
|
|
|
"""
|
|
|
|
|
|
# Standard class methods
|
|
|
|
|
|
def __init__(self, main_win_obj):
|
|
|
|
Gtk.Dialog.__init__(
|
|
self,
|
|
'Add channel',
|
|
main_win_obj,
|
|
0,
|
|
(
|
|
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_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>',
|
|
)
|
|
|
|
# (Store various widgets as IVs, so the calling function can retrieve
|
|
# their contents)
|
|
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)
|
|
|
|
separator = Gtk.HSeparator()
|
|
grid.attach(separator, 0, 5, 1, 1)
|
|
|
|
self.button = Gtk.RadioButton.new_with_label_from_widget(
|
|
None,
|
|
'I want to download videos from this channel automatically',
|
|
)
|
|
grid.attach(self.button, 0, 6, 1, 1)
|
|
|
|
self.button2 = Gtk.RadioButton.new_from_widget(self.button)
|
|
self.button2.set_label(
|
|
'Don\'t download anything, just check for new videos',
|
|
)
|
|
grid.attach(self.button2, 0, 7, 1, 1)
|
|
|
|
# (There is one fixed folder always at the top of this list)
|
|
self.folder_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 not media_data_obj.restrict_flag \
|
|
and media_data_obj.get_depth() \
|
|
< main_win_obj.app_obj.media_max_level:
|
|
self.folder_list.append(media_data_obj.name)
|
|
|
|
self.folder_list.insert(0, '')
|
|
self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
|
|
|
|
separator2 = Gtk.HSeparator()
|
|
grid.attach(separator2, 0, 8, 1, 1)
|
|
|
|
label4 = Gtk.Label('(Optional) Add this channel inside a folder')
|
|
grid.attach(label4, 0, 9, 1, 1)
|
|
|
|
self.folder_list.sort()
|
|
|
|
listmodel = Gtk.ListStore(str)
|
|
for item in self.folder_list:
|
|
listmodel.append([item])
|
|
|
|
combo = Gtk.ComboBox.new_with_model(listmodel)
|
|
grid.attach(combo, 0, 10, 1, 1)
|
|
|
|
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)
|
|
|
|
# Paste in the contents of the clipboard (if it contains at least one
|
|
# valid URL)
|
|
utils.add_links_from_clipboard(main_win_obj.app_obj, self.entry2)
|
|
|
|
# Display the dialogue window
|
|
self.show_all()
|
|
|
|
|
|
# Public 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):
|
|
|
|
"""Python class handling a dialogue window that adds a playlist to the
|
|
media registry.
|
|
|
|
Args:
|
|
|
|
main_win_obj (mainwin.MainWin): The parent main window
|
|
|
|
"""
|
|
|
|
|
|
# Standard class methods
|
|
|
|
|
|
def __init__(self, main_win_obj):
|
|
|
|
Gtk.Dialog.__init__(
|
|
self,
|
|
'Add playlist',
|
|
main_win_obj,
|
|
0,
|
|
(
|
|
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_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>',
|
|
)
|
|
|
|
# (Store various widgets as IVs, so the calling function can retrieve
|
|
# their contents)
|
|
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)
|
|
|
|
separator = Gtk.HSeparator()
|
|
grid.attach(separator, 0, 5, 1, 1)
|
|
|
|
self.button = Gtk.RadioButton.new_with_label_from_widget(
|
|
None,
|
|
'I want to download videos from this playlist automatically',
|
|
)
|
|
grid.attach(self.button, 0, 6, 1, 1)
|
|
|
|
self.button2 = Gtk.RadioButton.new_from_widget(self.button)
|
|
self.button2.set_label(
|
|
'Don\'t download anything, just check for new videos',
|
|
)
|
|
grid.attach(self.button2, 0, 7, 1, 1)
|
|
|
|
# (There is one fixed folder always at the top of this list)
|
|
self.folder_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 not media_data_obj.restrict_flag \
|
|
and media_data_obj.get_depth() \
|
|
< main_win_obj.app_obj.media_max_level:
|
|
self.folder_list.append(media_data_obj.name)
|
|
|
|
self.folder_list.insert(0, '')
|
|
self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
|
|
|
|
separator2 = Gtk.HSeparator()
|
|
grid.attach(separator2, 0, 8, 1, 1)
|
|
|
|
label4 = Gtk.Label('(Optional) Add this playlist inside a folder')
|
|
grid.attach(label4, 0, 9, 1, 1)
|
|
|
|
self.folder_list.sort()
|
|
|
|
listmodel = Gtk.ListStore(str)
|
|
for item in self.folder_list:
|
|
listmodel.append([item])
|
|
|
|
combo = Gtk.ComboBox.new_with_model(listmodel)
|
|
grid.attach(combo, 0, 10, 1, 1)
|
|
|
|
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)
|
|
|
|
# Paste in the contents of the clipboard (if it contains at least one
|
|
# valid URL)
|
|
utils.add_links_from_clipboard(main_win_obj.app_obj, self.entry2)
|
|
|
|
# Display the dialogue window
|
|
self.show_all()
|
|
|
|
|
|
# Public 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 AddFolderDialogue(Gtk.Dialog):
|
|
|
|
"""Python class handling a dialogue window that adds a folder to the media
|
|
registry.
|
|
|
|
Args:
|
|
|
|
main_win_obj (mainwin.MainWin): The parent main window
|
|
|
|
"""
|
|
|
|
|
|
# Standard class methods
|
|
|
|
|
|
def __init__(self, main_win_obj):
|
|
|
|
Gtk.Dialog.__init__(
|
|
self,
|
|
'Add folder',
|
|
main_win_obj,
|
|
0,
|
|
(
|
|
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_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 = Gtk.HSeparator()
|
|
grid.attach(separator, 0, 2, 1, 1)
|
|
|
|
self.button = Gtk.RadioButton.new_with_label_from_widget(
|
|
None,
|
|
'I want to download videos from this folder automatically',
|
|
)
|
|
grid.attach(self.button, 0, 3, 1, 1)
|
|
|
|
self.button2 = Gtk.RadioButton.new_from_widget(self.button)
|
|
self.button2.set_label(
|
|
'Don\'t download anything, just check for new videos',
|
|
)
|
|
grid.attach(self.button2, 0, 4, 1, 1)
|
|
|
|
# (There is one fixed folder always at the top of this list)
|
|
self.folder_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 not media_data_obj.restrict_flag \
|
|
and media_data_obj.get_depth() \
|
|
< main_win_obj.app_obj.media_max_level:
|
|
self.folder_list.append(media_data_obj.name)
|
|
|
|
self.folder_list.insert(0, '')
|
|
self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
|
|
|
|
separator2 = Gtk.HSeparator()
|
|
grid.attach(separator2, 0, 5, 1, 1)
|
|
|
|
label4 = Gtk.Label(
|
|
'(Optional) Add this folder inside another folder',
|
|
)
|
|
grid.attach(label4, 0, 6, 1, 1)
|
|
|
|
self.folder_list.sort()
|
|
|
|
listmodel = Gtk.ListStore(str)
|
|
for item in self.folder_list:
|
|
listmodel.append([item])
|
|
|
|
combo = Gtk.ComboBox.new_with_model(listmodel)
|
|
grid.attach(combo, 0, 7, 1, 1)
|
|
|
|
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 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 DeleteContainerDialogue(Gtk.Dialog):
|
|
|
|
"""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
|
|
|
|
"""
|
|
|
|
|
|
# Standard class methods
|
|
|
|
|
|
def __init__(self, main_win_obj, media_data_obj):
|
|
|
|
# Prepare variables
|
|
pkg_string = utils.upper_case_first(__main__. __packagename__)
|
|
|
|
if isinstance(media_data_obj, media.Channel):
|
|
obj_type = 'channel'
|
|
elif isinstance(media_data_obj, media.Playlist):
|
|
obj_type = 'playlist'
|
|
elif isinstance(media_data_obj, media.Folder):
|
|
obj_type = 'folder'
|
|
else:
|
|
return self.app_obj.system_error(
|
|
234,
|
|
'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
|
|
Gtk.Dialog.__init__(
|
|
self,
|
|
'Delete ' + obj_type,
|
|
main_win_obj,
|
|
0,
|
|
(
|
|
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_row_spacing(main_win_obj.spacing_size)
|
|
|
|
if not total_count:
|
|
|
|
if obj_type == 'folder':
|
|
|
|
label = Gtk.Label(
|
|
'This ' + obj_type + ' does not contain any videos,' \
|
|
+ ' channels,\nplaylists or folders (but there might be' \
|
|
+ ' some files\nin ' + pkg_string + '\'s data directory)',
|
|
)
|
|
|
|
else:
|
|
label = Gtk.Label(
|
|
'This ' + obj_type + ' does not contain any videos (but' \
|
|
+ ' there might\nbe some files in ' + pkg_string \
|
|
+ '\'s data directory)',
|
|
)
|
|
|
|
grid.attach(label, 0, 0, 1, 5)
|
|
label.set_alignment(0, 0.5)
|
|
|
|
else:
|
|
|
|
label = Gtk.Label('This ' + obj_type + ' contains:')
|
|
grid.attach(label, 0, 0, 1, 1)
|
|
label.set_alignment(0, 0.5)
|
|
|
|
if folder_count == 1:
|
|
label_string = '<b>1</b> folder'
|
|
else:
|
|
label_string = '<b>' + str(folder_count) + '</b> folders'
|
|
|
|
label2 = Gtk.Label()
|
|
grid.attach(label2, 0, 1, 1, 1)
|
|
label2.set_markup(label_string)
|
|
|
|
if channel_count == 1:
|
|
label_string = '<b>1</b> channel'
|
|
else:
|
|
label_string = '<b>' + str(channel_count) + '</b> channels'
|
|
|
|
label3 = Gtk.Label()
|
|
grid.attach(label3, 0, 2, 1, 1)
|
|
label3.set_markup(label_string)
|
|
|
|
if playlist_count == 1:
|
|
label_string = '<b>1</b> playlist'
|
|
else:
|
|
label_string = '<b>' + str(playlist_count) + '</b> playlists'
|
|
|
|
label4 = Gtk.Label()
|
|
grid.attach(label4, 0, 3, 1, 1)
|
|
label4.set_markup(label_string)
|
|
|
|
if self.video_count == 1:
|
|
label_string = '<b>1</b> video'
|
|
else:
|
|
label_string = '<b>' + str(self.video_count) + '</b> videos'
|
|
|
|
label5 = Gtk.Label()
|
|
grid.attach(label5, 0, 4, 1, 1)
|
|
label5.set_markup(label_string)
|
|
|
|
separator = Gtk.HSeparator()
|
|
grid.attach(separator, 0, 5, 1, 1)
|
|
|
|
label6 = Gtk.Label(
|
|
'Do you want to delete the ' + obj_type + ' from ' + pkg_string \
|
|
+ '\'s data\ndirectory, deleting all of its files, or do you' \
|
|
+ ' just want to\nremove the ' + obj_type + ' from this list?',
|
|
)
|
|
grid.attach(label6, 0, 6, 1, 1)
|
|
label6.set_alignment(0, 0.5)
|
|
|
|
self.button = Gtk.RadioButton.new_with_label_from_widget(
|
|
None,
|
|
'Just remove the ' + obj_type + ' from this list',
|
|
)
|
|
grid.attach(self.button, 0, 7, 1, 1)
|
|
|
|
self.button2 = Gtk.RadioButton.new_from_widget(self.button)
|
|
self.button2.set_label(
|
|
'Delete all files',
|
|
)
|
|
grid.attach(self.button2, 0, 8, 1, 1)
|
|
|
|
# Display the dialogue window
|
|
self.show_all()
|
|
|