tartube/lib/mainwin.py

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()