Update to 0.7.0
32
CHANGES
@ -1,3 +1,35 @@
|
|||||||
|
v0.7.0 (7 Jul 2019)
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
- This is the first release candidate for v1.0.0
|
||||||
|
- The MS Windows installer has been redesigned again (thanks to slartie for his
|
||||||
|
generous assistance in getting it working). Some MS Windows 10 users were
|
||||||
|
still complaining that Tartube would not run; this should be fixed now
|
||||||
|
- MS Windows should find that the annoying terminal window is no longer visible
|
||||||
|
- Deleting individual videos, then adding them to the Tartube database again,
|
||||||
|
could cause problems with the statistics displayed in the Video Index (e.g.
|
||||||
|
'All Videos (0, -1)'. Fixed
|
||||||
|
- Deleting and individual video by right-clicking it removed the video from the
|
||||||
|
database, but didn't delete the video file itself. Fixed
|
||||||
|
- If an empty channel/playlist was selected, new videos did not automatically
|
||||||
|
appear in the Video Catalogue during a download operation. Fixed
|
||||||
|
- Fixed some issues with the Temporary Videos folder
|
||||||
|
- Fixed more issues with videos being dislayed in the wrong order in the Video
|
||||||
|
Catalogue
|
||||||
|
- Fixed more issues with the scrollbars not resetting themselves when switching
|
||||||
|
between channels/playlists/folders
|
||||||
|
- After right-clicking a folder, selecting Mark Videos > New didn't work. Fixed
|
||||||
|
- When marking videos as new/favourite, you can now do this to all videos
|
||||||
|
in a folder, including all child channels/playlists/folders, or you can do
|
||||||
|
it just to the videos actually inside that folder. There are several new
|
||||||
|
popup menu options for emptying a folder, or for removing all its videos
|
||||||
|
- Tartube will no longer issue a system error if you drag a folder onto itself
|
||||||
|
in the Video Index
|
||||||
|
- Fixed the remaining issues caused by the Gtk graphics libraries
|
||||||
|
- Confirmed that various Gtk issues present in Gtk3.22 are not present in
|
||||||
|
Gtk 3.24. Users with Gtk 3.22 on their system will be warned to update it.
|
||||||
|
These warnings can be disabled, if required
|
||||||
|
|
||||||
v0.6.0 (4 Jul 2019)
|
v0.6.0 (4 Jul 2019)
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
10
README.rst
@ -18,11 +18,11 @@ Problems can be reported at
|
|||||||
Downloads
|
Downloads
|
||||||
---------
|
---------
|
||||||
|
|
||||||
Latest version: **v0.6.0 (4 July 2019)**
|
Latest version: **v0.7.0 (7 July 2019)**
|
||||||
|
|
||||||
- `MS Windows (32-bit) installer <https://sourceforge.net/projects/tartube/files/v0.6.0/install-tartube-0.6.0-32bit.exe/download>`__ from Sourceforge
|
- `MS Windows (32-bit) installer <https://sourceforge.net/projects/tartube/files/v0.7.0/install-tartube-0.7.0-32bit.exe/download>`__ from Sourceforge
|
||||||
- `MS Windows (64-bit) installer <https://sourceforge.net/projects/tartube/files/v0.6.0/install-tartube-0.6.0-64bit.exe/download>`__ from Sourceforge
|
- `MS Windows (64-bit) installer <https://sourceforge.net/projects/tartube/files/v0.7.0/install-tartube-0.7.0-64bit.exe/download>`__ from Sourceforge
|
||||||
- `Source code <https://sourceforge.net/projects/tartube/files/v0.6.0/tartube_v0.6.0.tar.gz/download>`__ from Sourceforge
|
- `Source code <https://sourceforge.net/projects/tartube/files/v0.7.0/tartube_v0.7.0.tar.gz/download>`__ from Sourceforge
|
||||||
- `Source code <https://github.com/axcore/tartube>`__ and `support <https://github.com/axcore/tartube/issues>`__ from GitHub
|
- `Source code <https://github.com/axcore/tartube>`__ and `support <https://github.com/axcore/tartube/issues>`__ from GitHub
|
||||||
|
|
||||||
Why should I use Tartube?
|
Why should I use Tartube?
|
||||||
@ -94,7 +94,7 @@ Run without installing
|
|||||||
|
|
||||||
1. Download & extract the source
|
1. Download & extract the source
|
||||||
2. Change directory into the **Tartube** directory
|
2. Change directory into the **Tartube** directory
|
||||||
3. Run ``python3 tartube.py``
|
3. Run ``python3 tartube``
|
||||||
|
|
||||||
Getting started
|
Getting started
|
||||||
---------------
|
---------------
|
||||||
|
5
hello_world_mswin.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Shell script to start the Gtk test programme on MS Windows, using the MSYS2
|
||||||
|
# environment provided by the Tartube installer
|
||||||
|
cd tartube
|
||||||
|
python3 hello_world.py
|
BIN
icons/small/system_warning.png
Normal file
After Width: | Height: | Size: 765 B |
@ -4170,54 +4170,62 @@ class SystemPrefWin(GenericPrefWin):
|
|||||||
)
|
)
|
||||||
checkbutton.connect('toggled', self.on_squeeze_button_toggled)
|
checkbutton.connect('toggled', self.on_squeeze_button_toggled)
|
||||||
|
|
||||||
|
checkbutton2 = self.add_checkbutton(grid,
|
||||||
|
'Show system warning messages in the \'Errors/Warnings\' tab',
|
||||||
|
self.app_obj.system_warning_show_flag,
|
||||||
|
True, # Can be toggled by user
|
||||||
|
0, 5, grid_width, 1,
|
||||||
|
)
|
||||||
|
checkbutton2.connect('toggled', self.on_warning_button_toggled)
|
||||||
|
|
||||||
# Operation preferences
|
# Operation preferences
|
||||||
self.add_label(grid,
|
self.add_label(grid,
|
||||||
'<u>Operation preferences</u>',
|
'<u>Operation preferences</u>',
|
||||||
0, 5, grid_width, 1,
|
0, 6, grid_width, 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkbutton2 = self.add_checkbutton(grid,
|
checkbutton3 = self.add_checkbutton(grid,
|
||||||
'Automatically update youtube-dl before every download operation',
|
'Automatically update youtube-dl before every download operation',
|
||||||
self.app_obj.operation_auto_update_flag,
|
self.app_obj.operation_auto_update_flag,
|
||||||
True, # Can be toggled by user
|
True, # Can be toggled by user
|
||||||
0, 6, grid_width, 1,
|
0, 7, grid_width, 1,
|
||||||
)
|
)
|
||||||
checkbutton2.connect('toggled', self.on_auto_update_button_toggled)
|
checkbutton3.connect('toggled', self.on_auto_update_button_toggled)
|
||||||
|
|
||||||
checkbutton3 = self.add_checkbutton(grid,
|
checkbutton4 = self.add_checkbutton(grid,
|
||||||
'Automatically save files at the end of a download/update/' \
|
'Automatically save files at the end of a download/update/' \
|
||||||
+ 'refresh operation',
|
+ 'refresh operation',
|
||||||
self.app_obj.operation_save_flag,
|
self.app_obj.operation_save_flag,
|
||||||
True, # Can be toggled by user
|
True, # Can be toggled by user
|
||||||
0, 7, grid_width, 1,
|
0, 8, grid_width, 1,
|
||||||
)
|
)
|
||||||
checkbutton3.connect('toggled', self.on_save_button_toggled)
|
checkbutton4.connect('toggled', self.on_save_button_toggled)
|
||||||
|
|
||||||
checkbutton4 = self.add_checkbutton(grid,
|
checkbutton5 = self.add_checkbutton(grid,
|
||||||
'Show a dialogue window at the end of a download/update/refresh' \
|
'Show a dialogue window at the end of a download/update/refresh' \
|
||||||
+ ' operation',
|
+ ' operation',
|
||||||
self.app_obj.operation_dialogue_flag,
|
self.app_obj.operation_dialogue_flag,
|
||||||
True, # Can be toggled by user
|
True, # Can be toggled by user
|
||||||
0, 8, grid_width, 1,
|
0, 9, grid_width, 1,
|
||||||
)
|
)
|
||||||
checkbutton4.connect('toggled', self.on_dialogue_button_toggled)
|
checkbutton5.connect('toggled', self.on_dialogue_button_toggled)
|
||||||
|
|
||||||
# Module preferences
|
# Module preferences
|
||||||
self.add_label(grid,
|
self.add_label(grid,
|
||||||
'<u>Module preferences</u>',
|
'<u>Module preferences</u>',
|
||||||
0, 9, grid_width, 1,
|
0, 10, grid_width, 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
checkbutton5 = self.add_checkbutton(grid,
|
checkbutton6 = self.add_checkbutton(grid,
|
||||||
'Use \'moviepy\' module to get a video\'s duration, if not known'
|
'Use \'moviepy\' module to get a video\'s duration, if not known'
|
||||||
+ ' (may be slow)',
|
+ ' (may be slow)',
|
||||||
self.app_obj.use_module_moviepy_flag,
|
self.app_obj.use_module_moviepy_flag,
|
||||||
True, # Can be toggled by user
|
True, # Can be toggled by user
|
||||||
0, 10, grid_width, 1,
|
0, 11, grid_width, 1,
|
||||||
)
|
)
|
||||||
checkbutton5.connect('toggled', self.on_moviepy_button_toggled)
|
checkbutton6.connect('toggled', self.on_moviepy_button_toggled)
|
||||||
if not mainapp.HAVE_MOVIEPY_FLAG:
|
if not mainapp.HAVE_MOVIEPY_FLAG:
|
||||||
checkbutton5.set_sensitive(False)
|
checkbutton6.set_sensitive(False)
|
||||||
|
|
||||||
|
|
||||||
def setup_backups_tab(self):
|
def setup_backups_tab(self):
|
||||||
@ -5288,6 +5296,26 @@ class SystemPrefWin(GenericPrefWin):
|
|||||||
self.app_obj.set_ytdl_write_verbose_flag(False)
|
self.app_obj.set_ytdl_write_verbose_flag(False)
|
||||||
|
|
||||||
|
|
||||||
|
def on_warning_button_toggled(self, checkbutton):
|
||||||
|
|
||||||
|
"""Called from callback in self.setup_general_tab().
|
||||||
|
|
||||||
|
Enables/disables system warnings in the 'Errors/Warnings' tab.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
checkbutton (Gtk.CheckButton): The widget clicked
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if checkbutton.get_active() \
|
||||||
|
and not self.app_obj.system_warning_show_flag:
|
||||||
|
self.app_obj.set_system_warning_show_flag(True)
|
||||||
|
elif not checkbutton.get_active() \
|
||||||
|
and self.app_obj.system_warning_show_flag:
|
||||||
|
self.app_obj.set_system_warning_show_flag(False)
|
||||||
|
|
||||||
|
|
||||||
def on_worker_button_toggled(self, checkbutton):
|
def on_worker_button_toggled(self, checkbutton):
|
||||||
|
|
||||||
"""Called from callback in self.setup_general_tab().
|
"""Called from callback in self.setup_general_tab().
|
||||||
|
@ -1487,11 +1487,12 @@ class VideoDownloader(object):
|
|||||||
|
|
||||||
if not media_data_obj.dl_flag:
|
if not media_data_obj.dl_flag:
|
||||||
|
|
||||||
media_data_obj.set_dl_flag(True)
|
|
||||||
GObject.timeout_add(
|
GObject.timeout_add(
|
||||||
0,
|
0,
|
||||||
app_obj.main_win_obj.video_catalogue_update_row,
|
app_obj.mark_video_downloaded,
|
||||||
media_data_obj,
|
media_data_obj,
|
||||||
|
True, # Video is downloaded
|
||||||
|
True, # Video is not new
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -1503,11 +1504,12 @@ class VideoDownloader(object):
|
|||||||
|
|
||||||
if not match_obj.dl_flag:
|
if not match_obj.dl_flag:
|
||||||
|
|
||||||
match_obj.set_dl_flag(True)
|
|
||||||
GObject.timeout_add(
|
GObject.timeout_add(
|
||||||
0,
|
0,
|
||||||
app_obj.main_win_obj.video_catalogue_update_row,
|
app_obj.mark_video_downloaded,
|
||||||
match_obj,
|
match_obj,
|
||||||
|
True, # Video is downloaded
|
||||||
|
True, # Video is not new
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -465,6 +465,7 @@ SMALL_ICON_DICT = {
|
|||||||
'error_small': 'error.png',
|
'error_small': 'error.png',
|
||||||
'warning_small': 'warning.png',
|
'warning_small': 'warning.png',
|
||||||
'system_error_small': 'system_error.png',
|
'system_error_small': 'system_error.png',
|
||||||
|
'system_warning_small': 'system_warning.png',
|
||||||
}
|
}
|
||||||
|
|
||||||
WIN_ICON_LIST = [
|
WIN_ICON_LIST = [
|
||||||
|
386
lib/mainapp.py
@ -189,6 +189,9 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.main_win_height = 600
|
self.main_win_height = 600
|
||||||
self.config_win_width = 650
|
self.config_win_width = 650
|
||||||
self.config_win_height = 450
|
self.config_win_height = 450
|
||||||
|
# Default size (in pixels) of space between various widgets
|
||||||
|
self.default_spacing_size = 5
|
||||||
|
|
||||||
# Flag set to True if the main toolbar should be compressed (by
|
# Flag set to True if the main toolbar should be compressed (by
|
||||||
# removing the labels); ideal if the toolbar's contents won't fit in
|
# removing the labels); ideal if the toolbar's contents won't fit in
|
||||||
# the standard-sized window (as it almost certainly won't on MS
|
# the standard-sized window (as it almost certainly won't on MS
|
||||||
@ -197,8 +200,19 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.toolbar_squeeze_flag = False
|
self.toolbar_squeeze_flag = False
|
||||||
else:
|
else:
|
||||||
self.toolbar_squeeze_flag = True
|
self.toolbar_squeeze_flag = True
|
||||||
# Default size (in pixels) of space between various widgets
|
# Flag set to True if system warning messages should be shown (system
|
||||||
self.default_spacing_size = 5
|
# error messages are always shown)
|
||||||
|
# NB The check is applied by self.system_warning(); any part of the
|
||||||
|
# code could call mainwin.MainWin.errors_list_add_system_warning()
|
||||||
|
# directly, which would bypass this flag
|
||||||
|
self.system_warning_show_flag = True
|
||||||
|
|
||||||
|
# Gtk v3.22.* produces numerous error/warning messages in the terminal
|
||||||
|
# when the Video Index and Video Catalogue are updated. Whatever the
|
||||||
|
# issues were, they appear to have been fixed by Gtk v3.24.*
|
||||||
|
# Flag set to True by self.start() if Tartube is being run before
|
||||||
|
# Gtk v3.24
|
||||||
|
self.gtk_broken_flag = False
|
||||||
|
|
||||||
# Tartube's data directory (platform-dependant), i.e. 'tartube-data'
|
# Tartube's data directory (platform-dependant), i.e. 'tartube-data'
|
||||||
# Note that, using the MSWin installer, Cygwin gives file paths with
|
# Note that, using the MSWin installer, Cygwin gives file paths with
|
||||||
@ -894,6 +908,16 @@ class TartubeApp(Gtk.Application):
|
|||||||
# Import the script name (for convenience)
|
# Import the script name (for convenience)
|
||||||
script_name = utils.upper_case_first(__main__.__packagename__)
|
script_name = utils.upper_case_first(__main__.__packagename__)
|
||||||
|
|
||||||
|
# Gtk v3.22.* produces numerous error/warning messages in the terminal
|
||||||
|
# when the Video Index and Video Catalogue are updated. Whatever the
|
||||||
|
# issues were, they appear to have been fixed by Gtk v3.24.*
|
||||||
|
major = Gtk.get_major_version()
|
||||||
|
minor = Gtk.get_minor_version()
|
||||||
|
micro = Gtk.get_micro_version()
|
||||||
|
if major < 3 or (major == 3 and minor < 24):
|
||||||
|
|
||||||
|
self.gtk_broken_flag = True
|
||||||
|
|
||||||
# Create the main window
|
# Create the main window
|
||||||
self.main_win_obj = mainwin.MainWin(self)
|
self.main_win_obj = mainwin.MainWin(self)
|
||||||
# If the debugging flag is set, move it to the top-left corner of the
|
# If the debugging flag is set, move it to the top-left corner of the
|
||||||
@ -1117,6 +1141,17 @@ class TartubeApp(Gtk.Application):
|
|||||||
if self.toolbar_squeeze_flag:
|
if self.toolbar_squeeze_flag:
|
||||||
self.main_win_obj.redraw_main_toolbar()
|
self.main_win_obj.redraw_main_toolbar()
|
||||||
|
|
||||||
|
# If the system's Gtk is an early, broken version, display a system
|
||||||
|
# warning
|
||||||
|
if self.gtk_broken_flag:
|
||||||
|
self.system_warning(
|
||||||
|
126,
|
||||||
|
'Gtk v' + str(major) + '.' + str(minor) + '.' + str(micro) \
|
||||||
|
+ ' is broken, which will cause problems when running ' \
|
||||||
|
+ utils.upper_case_first(__main__.__packagename__) \
|
||||||
|
+ '. Please update it to at least Gtk v3.24',
|
||||||
|
)
|
||||||
|
|
||||||
# If file load/save has been disabled, we can now show a dialogue
|
# If file load/save has been disabled, we can now show a dialogue
|
||||||
# window
|
# window
|
||||||
if self.disable_load_save_flag:
|
if self.disable_load_save_flag:
|
||||||
@ -1236,9 +1271,10 @@ class TartubeApp(Gtk.Application):
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
Error codes are currently assigned thus:
|
Error codes for this function and for self.system_warning are
|
||||||
|
currently assigned thus:
|
||||||
|
|
||||||
100-199: mainapp.py (in use: 101-125)
|
100-199: mainapp.py (in use: 101-126)
|
||||||
200-299: mainwin.py (in use: 201-234)
|
200-299: mainwin.py (in use: 201-234)
|
||||||
300-399: downloads.py (in use: 301-304)
|
300-399: downloads.py (in use: 301-304)
|
||||||
400-499: config.py (in use: 401-404)
|
400-499: config.py (in use: 401-404)
|
||||||
@ -1255,6 +1291,32 @@ class TartubeApp(Gtk.Application):
|
|||||||
print('SYSTEM ERROR ' + str(error_code) + ': ' + msg)
|
print('SYSTEM ERROR ' + str(error_code) + ': ' + msg)
|
||||||
|
|
||||||
|
|
||||||
|
def system_warning(self, error_code, msg):
|
||||||
|
|
||||||
|
"""Can be called by anything.
|
||||||
|
|
||||||
|
Wrapper function for mainwin.MainWin.errors_list_add_system_warning().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
code (int): An error code in the range 100-999. This function and
|
||||||
|
self.system_error() share the same error codes
|
||||||
|
|
||||||
|
msg (str): A system error message to display in the main window's
|
||||||
|
Errors List.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if DEBUG_FUNC_FLAG:
|
||||||
|
print('ap 992 system_warning')
|
||||||
|
|
||||||
|
if self.main_win_obj and self.system_warning_show_flag:
|
||||||
|
self.main_win_obj.errors_list_add_system_warning(error_code, msg)
|
||||||
|
else:
|
||||||
|
# Emergency fallback: display in the terminal window
|
||||||
|
print('SYSTEM WARNING ' + str(error_code) + ': ' + msg)
|
||||||
|
|
||||||
|
|
||||||
# (Config/database files load/save)
|
# (Config/database files load/save)
|
||||||
|
|
||||||
|
|
||||||
@ -1323,6 +1385,9 @@ class TartubeApp(Gtk.Application):
|
|||||||
# Set IVs to their new values
|
# Set IVs to their new values
|
||||||
if version >= 5024: # v0.5.024
|
if version >= 5024: # v0.5.024
|
||||||
self.toolbar_squeeze_flag = json_dict['toolbar_squeeze_flag']
|
self.toolbar_squeeze_flag = json_dict['toolbar_squeeze_flag']
|
||||||
|
if version >= 6006: # v0.6.006
|
||||||
|
self.system_warning_show_flag \
|
||||||
|
= json_dict['system_warning_show_flag']
|
||||||
|
|
||||||
self.data_dir = json_dict['data_dir']
|
self.data_dir = json_dict['data_dir']
|
||||||
self.downloads_dir = os.path.abspath(
|
self.downloads_dir = os.path.abspath(
|
||||||
@ -1427,6 +1492,7 @@ class TartubeApp(Gtk.Application):
|
|||||||
'save_time': str(utc.strftime('%H:%M:%S')),
|
'save_time': str(utc.strftime('%H:%M:%S')),
|
||||||
# Data
|
# Data
|
||||||
'toolbar_squeeze_flag': self.toolbar_squeeze_flag,
|
'toolbar_squeeze_flag': self.toolbar_squeeze_flag,
|
||||||
|
'system_warning_show_flag': self.system_warning_show_flag,
|
||||||
|
|
||||||
'data_dir': self.data_dir,
|
'data_dir': self.data_dir,
|
||||||
|
|
||||||
@ -1731,6 +1797,37 @@ class TartubeApp(Gtk.Application):
|
|||||||
and media_data_obj.index is not None:
|
and media_data_obj.index is not None:
|
||||||
media_data_obj.index = int(media_data_obj.index)
|
media_data_obj.index = int(media_data_obj.index)
|
||||||
|
|
||||||
|
if version < 6003: # v0.6.003
|
||||||
|
|
||||||
|
# This version fixes an issue in which deleting an individual video
|
||||||
|
# and then re-adding the same video, downloading it then deleting
|
||||||
|
# it a second time, messes up the parent container's count IVs
|
||||||
|
# Nothing for it but to recalculate them all, just in case
|
||||||
|
for dbid in self.media_name_dict.values():
|
||||||
|
container_obj = self.media_reg_dict[dbid]
|
||||||
|
|
||||||
|
vid_count = new_count = fav_count = dl_count = 0
|
||||||
|
|
||||||
|
for child_obj in container_obj.child_list:
|
||||||
|
if isinstance(child_obj, media.Video):
|
||||||
|
vid_count += 1
|
||||||
|
|
||||||
|
if child_obj.new_flag:
|
||||||
|
new_count += 1
|
||||||
|
|
||||||
|
if child_obj.fav_flag:
|
||||||
|
fav_count += 1
|
||||||
|
|
||||||
|
if child_obj.dl_flag:
|
||||||
|
dl_count += 1
|
||||||
|
|
||||||
|
container_obj.reset_counts(
|
||||||
|
vid_count,
|
||||||
|
new_count,
|
||||||
|
fav_count,
|
||||||
|
dl_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_db(self):
|
def save_db(self):
|
||||||
|
|
||||||
@ -2133,10 +2230,13 @@ class TartubeApp(Gtk.Application):
|
|||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
print('ap 1570 delete_temp_folders')
|
print('ap 1570 delete_temp_folders')
|
||||||
|
|
||||||
for name in self.media_name_dict:
|
# (Must compile a list of top-level container objects first, or Python
|
||||||
|
# will complain about the dictionary changing size)
|
||||||
|
obj_list = []
|
||||||
|
for dbid in self.media_name_dict.values():
|
||||||
|
obj_list.append(self.media_reg_dict[dbid])
|
||||||
|
|
||||||
dbid = self.media_name_dict[name]
|
for media_data_obj in obj_list:
|
||||||
media_data_obj = self.media_reg_dict[dbid]
|
|
||||||
|
|
||||||
if isinstance(media_data_obj, media.Folder) \
|
if isinstance(media_data_obj, media.Folder) \
|
||||||
and media_data_obj.temp_flag:
|
and media_data_obj.temp_flag:
|
||||||
@ -2645,7 +2745,6 @@ class TartubeApp(Gtk.Application):
|
|||||||
# The downloads.DownloadItem handles a download for a video, a channel
|
# The downloads.DownloadItem handles a download for a video, a channel
|
||||||
# or a playlist
|
# or a playlist
|
||||||
media_data_obj = download_item_obj.media_data_obj
|
media_data_obj = download_item_obj.media_data_obj
|
||||||
video_obj = None
|
|
||||||
|
|
||||||
if isinstance(media_data_obj, media.Video):
|
if isinstance(media_data_obj, media.Video):
|
||||||
|
|
||||||
@ -2661,6 +2760,7 @@ class TartubeApp(Gtk.Application):
|
|||||||
# The downloads.DownloadItem object is handling a channel or
|
# The downloads.DownloadItem object is handling a channel or
|
||||||
# playlist
|
# playlist
|
||||||
# Does a media.Video object already exist?
|
# Does a media.Video object already exist?
|
||||||
|
video_obj = None
|
||||||
for child_obj in media_data_obj.child_list:
|
for child_obj in media_data_obj.child_list:
|
||||||
|
|
||||||
if isinstance(child_obj, media.Video) \
|
if isinstance(child_obj, media.Video) \
|
||||||
@ -2869,6 +2969,9 @@ class TartubeApp(Gtk.Application):
|
|||||||
|
|
||||||
json_dict = self.file_manager_obj.load_json(json_path)
|
json_dict = self.file_manager_obj.load_json(json_path)
|
||||||
|
|
||||||
|
if 'title' in json_dict:
|
||||||
|
video_obj.set_name(json_dict['title'])
|
||||||
|
|
||||||
if 'upload_date' in json_dict:
|
if 'upload_date' in json_dict:
|
||||||
# date_string in form YYYYMMDD
|
# date_string in form YYYYMMDD
|
||||||
date_string = json_dict['upload_date']
|
date_string = json_dict['upload_date']
|
||||||
@ -3335,8 +3438,6 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.main_win_obj.video_index_add_row(media_data_obj)
|
self.main_win_obj.video_index_add_row(media_data_obj)
|
||||||
|
|
||||||
# Select the moving object, which redraws the Video Catalogue
|
# Select the moving object, which redraws the Video Catalogue
|
||||||
# !!! TODO BUG: This doesn't work because the .select_iter() call
|
|
||||||
# generates an error
|
|
||||||
self.main_win_obj.video_index_select_row(media_data_obj)
|
self.main_win_obj.video_index_select_row(media_data_obj)
|
||||||
|
|
||||||
|
|
||||||
@ -3364,13 +3465,17 @@ class TartubeApp(Gtk.Application):
|
|||||||
|
|
||||||
# Do some basic checks
|
# Do some basic checks
|
||||||
if source_obj is None or isinstance(source_obj, media.Video) \
|
if source_obj is None or isinstance(source_obj, media.Video) \
|
||||||
or dest_obj is None or isinstance(dest_obj, media.Video) \
|
or dest_obj is None or isinstance(dest_obj, media.Video):
|
||||||
or source_obj == dest_obj:
|
|
||||||
return self.system_error(
|
return self.system_error(
|
||||||
114,
|
114,
|
||||||
'Move container request failed sanity check',
|
'Move container request failed sanity check',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif source_obj == dest_obj:
|
||||||
|
# No need for a system error message if the user drags a folder
|
||||||
|
# onto itself; just do nothing
|
||||||
|
return
|
||||||
|
|
||||||
# Ignore Video Index drag-and-drop during an download/update/refresh
|
# Ignore Video Index drag-and-drop during an download/update/refresh
|
||||||
# operation
|
# operation
|
||||||
elif self.current_manager_obj:
|
elif self.current_manager_obj:
|
||||||
@ -3486,15 +3591,14 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.main_win_obj.video_index_delete_row(source_obj)
|
self.main_win_obj.video_index_delete_row(source_obj)
|
||||||
self.main_win_obj.video_index_add_row(source_obj)
|
self.main_win_obj.video_index_add_row(source_obj)
|
||||||
# Select the moving object, which redraws the Video Catalogue
|
# Select the moving object, which redraws the Video Catalogue
|
||||||
# !!! TODO BUG: This doesn't work because the .select_iter() call
|
|
||||||
# generates an error
|
|
||||||
self.main_win_obj.video_index_select_row(source_obj)
|
self.main_win_obj.video_index_select_row(source_obj)
|
||||||
|
|
||||||
|
|
||||||
# (Delete media data objects)
|
# (Delete media data objects)
|
||||||
|
|
||||||
|
|
||||||
def delete_video(self, video_obj, no_update_index_flag=False):
|
def delete_video(self, video_obj, no_update_index_flag=False,
|
||||||
|
delete_files_flag=False):
|
||||||
|
|
||||||
"""Called by self.delete_temp_folders(), .delete_container(),
|
"""Called by self.delete_temp_folders(), .delete_container(),
|
||||||
mainwin.MainWin.video_catalogue_popup_menu() and a callback in
|
mainwin.MainWin.video_catalogue_popup_menu() and a callback in
|
||||||
@ -3510,6 +3614,11 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.delete_container(), in which case the Video Index is not
|
self.delete_container(), in which case the Video Index is not
|
||||||
updated (because the calling function wants to do that)
|
updated (because the calling function wants to do that)
|
||||||
|
|
||||||
|
delete_files_flag (True or False): True when called by
|
||||||
|
mainwin.MainWin.on_video_catalogue_delete_video, in which case
|
||||||
|
the video and its associated files are deleted from the
|
||||||
|
filesystem
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
@ -3538,21 +3647,61 @@ class TartubeApp(Gtk.Application):
|
|||||||
# Remove the video from our IVs
|
# Remove the video from our IVs
|
||||||
del self.media_reg_dict[video_obj.dbid]
|
del self.media_reg_dict[video_obj.dbid]
|
||||||
|
|
||||||
# (If emptying a temporary folder on startup, the main window won't be
|
# Delete files from the filesystem, if required
|
||||||
# visible yet)
|
if delete_files_flag and video_obj.file_dir:
|
||||||
if self.main_win_obj:
|
|
||||||
# Remove the video from the catalogue, if present
|
|
||||||
self.main_win_obj.video_catalogue_delete_row(video_obj)
|
|
||||||
|
|
||||||
# Update rows in the Video Index
|
video_path = os.path.abspath(
|
||||||
if not no_update_index_flag:
|
os.path.join(
|
||||||
for container_obj in update_list:
|
video_obj.file_dir,
|
||||||
|
video_obj.file_name + video_obj.file_ext,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if os.path.isfile(video_path):
|
||||||
|
os.remove(video_path)
|
||||||
|
|
||||||
|
# Also need to delete the thumbnail, description and/or JSON files
|
||||||
|
# if they exist
|
||||||
|
thumb_path = utils.find_thumbnail(self, video_obj)
|
||||||
|
if thumb_path and os.path.isfile(thumb_path):
|
||||||
|
os.remove(thumb_path)
|
||||||
|
|
||||||
|
descrip_path = os.path.abspath(
|
||||||
|
os.path.join(
|
||||||
|
video_obj.file_dir,
|
||||||
|
video_obj.file_name + '.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if os.path.isfile(descrip_path):
|
||||||
|
os.remove(descrip_path)
|
||||||
|
|
||||||
|
json_path = os.path.abspath(
|
||||||
|
os.path.join(
|
||||||
|
video_obj.file_dir,
|
||||||
|
video_obj.file_name + '.info.json',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if os.path.isfile(json_path):
|
||||||
|
os.remove(json_path)
|
||||||
|
|
||||||
|
# Remove the video from the catalogue, if present
|
||||||
|
self.main_win_obj.video_catalogue_delete_row(video_obj)
|
||||||
|
# Update rows in the Video Index, first checking that the parent
|
||||||
|
# container object is currently drawn there (which it might not be,
|
||||||
|
# if emptying temporary folders on startup)
|
||||||
|
if not no_update_index_flag:
|
||||||
|
for container_obj in update_list:
|
||||||
|
|
||||||
|
if container_obj.name \
|
||||||
|
in self.main_win_obj.video_index_row_dict:
|
||||||
self.main_win_obj.video_index_update_row_text(
|
self.main_win_obj.video_index_update_row_text(
|
||||||
container_obj,
|
container_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_container(self, media_data_obj):
|
def delete_container(self, media_data_obj, empty_flag=False):
|
||||||
|
|
||||||
"""Can be called by anything.
|
"""Can be called by anything.
|
||||||
|
|
||||||
@ -3573,6 +3722,10 @@ class TartubeApp(Gtk.Application):
|
|||||||
media_data_obj (media.Channel, media.Playlist, media.Folder):
|
media_data_obj (media.Channel, media.Playlist, media.Folder):
|
||||||
The container media data object
|
The container media data object
|
||||||
|
|
||||||
|
empty_flag (True or False): If True, the container media data
|
||||||
|
object is to be emptied, rather than being deleted
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
@ -3593,24 +3746,42 @@ class TartubeApp(Gtk.Application):
|
|||||||
# children
|
# children
|
||||||
# (Even though there are no children, we can't guarantee that the
|
# (Even though there are no children, we can't guarantee that the
|
||||||
# sub-directories in Tartube's data directory are empty)
|
# sub-directories in Tartube's data directory are empty)
|
||||||
dialogue_win = mainwin.DeleteContainerDialogue(
|
# Exception: don't prompt for confirmation if media_data_obj is
|
||||||
self.main_win_obj,
|
# somewhere inside a temporary folder
|
||||||
media_data_obj,
|
confirm_flag = True
|
||||||
)
|
delete_file_flag = False
|
||||||
|
parent_obj = media_data_obj.parent_obj
|
||||||
|
|
||||||
response = dialogue_win.run()
|
while parent_obj is not None:
|
||||||
|
if isinstance(parent_obj, media.Folder) and parent_obj.temp_flag:
|
||||||
|
# The media data object is somewhere inside a temporary folder;
|
||||||
|
# no need to prompt for confirmation
|
||||||
|
confirm_flag = False
|
||||||
|
|
||||||
# Retrieve user choices from the dialogue window...
|
parent_obj = parent_obj.parent_obj
|
||||||
if dialogue_win.button2.get_active():
|
|
||||||
delete_file_flag = True
|
|
||||||
else:
|
|
||||||
delete_file_flag = False
|
|
||||||
|
|
||||||
# ...before destroying it
|
if confirm_flag:
|
||||||
dialogue_win.destroy()
|
|
||||||
|
|
||||||
if response != Gtk.ResponseType.OK:
|
# Prompt the user for confirmation
|
||||||
return
|
dialogue_win = mainwin.DeleteContainerDialogue(
|
||||||
|
self.main_win_obj,
|
||||||
|
media_data_obj,
|
||||||
|
empty_flag,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = dialogue_win.run()
|
||||||
|
|
||||||
|
# Retrieve user choices from the dialogue window...
|
||||||
|
if dialogue_win.button2.get_active():
|
||||||
|
delete_file_flag = True
|
||||||
|
else:
|
||||||
|
delete_file_flag = False
|
||||||
|
|
||||||
|
# ...before destroying it
|
||||||
|
dialogue_win.destroy()
|
||||||
|
|
||||||
|
if response != Gtk.ResponseType.OK:
|
||||||
|
return
|
||||||
|
|
||||||
# Get a second confirmation, if required to delete files
|
# Get a second confirmation, if required to delete files
|
||||||
if delete_file_flag:
|
if delete_file_flag:
|
||||||
@ -3624,17 +3795,17 @@ class TartubeApp(Gtk.Application):
|
|||||||
# Arguments passed directly to .delete_container_continue()
|
# Arguments passed directly to .delete_container_continue()
|
||||||
{
|
{
|
||||||
'yes': 'delete_container_continue',
|
'yes': 'delete_container_continue',
|
||||||
'data': media_data_obj,
|
'data': [media_data_obj, empty_flag],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# No second confirmation required, so we can proceed directly to the
|
# No second confirmation required, so we can proceed directly to the
|
||||||
# call to self.delete_container_complete()
|
# call to self.delete_container_complete()
|
||||||
else:
|
else:
|
||||||
self.delete_container_complete(media_data_obj)
|
self.delete_container_complete(media_data_obj, empty_flag)
|
||||||
|
|
||||||
|
|
||||||
def delete_container_continue(self, media_data_obj):
|
def delete_container_continue(self, data_list):
|
||||||
|
|
||||||
"""Called by dialogue.MessageDialogue.on_clicked().
|
"""Called by dialogue.MessageDialogue.on_clicked().
|
||||||
|
|
||||||
@ -3643,25 +3814,36 @@ class TartubeApp(Gtk.Application):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
||||||
media_data_obj (media.Channel, media.Playlist, media.Folder):
|
data_list (list): A list of two items. The first is the container
|
||||||
The container media data object
|
media data object; the second is a flag set to True if the
|
||||||
|
container should be emptied, rather than being deleted
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
print('ap 3034 delete_container_continue')
|
print('ap 3034 delete_container_continue')
|
||||||
|
|
||||||
|
# Unpack the arguments
|
||||||
|
media_data_obj = data_list[0]
|
||||||
|
empty_flag = data_list[1]
|
||||||
|
|
||||||
# Confirmation obtained, so delete the files
|
# Confirmation obtained, so delete the files
|
||||||
container_dir = media_data_obj.get_dir(self)
|
container_dir = media_data_obj.get_dir(self)
|
||||||
if os.path.isdir(container_dir):
|
if os.path.isdir(container_dir):
|
||||||
shutil.rmtree(container_dir)
|
shutil.rmtree(container_dir)
|
||||||
|
|
||||||
|
# If emptying the container rather than deleting it, just create a
|
||||||
|
# replacement (empty) directory on the filesystem
|
||||||
|
if empty_flag:
|
||||||
|
os.makedirs(container_dir)
|
||||||
|
|
||||||
# Now call self.delete_container_complete() to handle the media data
|
# Now call self.delete_container_complete() to handle the media data
|
||||||
# registry
|
# registry
|
||||||
self.delete_container_complete(media_data_obj)
|
self.delete_container_complete(media_data_obj, empty_flag)
|
||||||
|
|
||||||
|
|
||||||
def delete_container_complete(self, media_data_obj, recursive_flag=False):
|
def delete_container_complete(self, media_data_obj, empty_flag,
|
||||||
|
recursive_flag=False):
|
||||||
|
|
||||||
"""Called by self.delete_container(), .delete_container_continue()
|
"""Called by self.delete_container(), .delete_container_continue()
|
||||||
and then recursively by this function.
|
and then recursively by this function.
|
||||||
@ -3677,6 +3859,9 @@ class TartubeApp(Gtk.Application):
|
|||||||
media_data_obj (media.Channel, media.Playlist, media.Folder):
|
media_data_obj (media.Channel, media.Playlist, media.Folder):
|
||||||
The container media data object
|
The container media data object
|
||||||
|
|
||||||
|
empty_flag (True or False): If True, the container media data
|
||||||
|
object is to be emptied, rather than being deleted
|
||||||
|
|
||||||
recursive_flag (True, False): Set to False on the initial call to
|
recursive_flag (True, False): Set to False on the initial call to
|
||||||
this function from some other part of the code, and True when
|
this function from some other part of the code, and True when
|
||||||
this function calls itself recursively
|
this function calls itself recursively
|
||||||
@ -3697,24 +3882,31 @@ class TartubeApp(Gtk.Application):
|
|||||||
if isinstance(child_obj, media.Video):
|
if isinstance(child_obj, media.Video):
|
||||||
self.delete_video(child_obj, True)
|
self.delete_video(child_obj, True)
|
||||||
else:
|
else:
|
||||||
self.delete_container_complete(child_obj, True)
|
self.delete_container_complete(child_obj, False, True)
|
||||||
|
|
||||||
# Remove the container object from its own parent object (if it has
|
if not empty_flag or recursive_flag:
|
||||||
# one)
|
|
||||||
if media_data_obj.parent_obj:
|
|
||||||
media_data_obj.parent_obj.del_child(media_data_obj)
|
|
||||||
|
|
||||||
# Remove the media data object from our IVs
|
# Remove the container object from its own parent object (if it has
|
||||||
del self.media_reg_dict[media_data_obj.dbid]
|
# one)
|
||||||
del self.media_name_dict[media_data_obj.name]
|
if media_data_obj.parent_obj:
|
||||||
if media_data_obj.dbid in self.media_top_level_list:
|
media_data_obj.parent_obj.del_child(media_data_obj)
|
||||||
index = self.media_top_level_list.index(media_data_obj.dbid)
|
|
||||||
del self.media_top_level_list[index]
|
# Remove the media data object from our IVs
|
||||||
|
del self.media_reg_dict[media_data_obj.dbid]
|
||||||
|
del self.media_name_dict[media_data_obj.name]
|
||||||
|
if media_data_obj.dbid in self.media_top_level_list:
|
||||||
|
index = self.media_top_level_list.index(media_data_obj.dbid)
|
||||||
|
del self.media_top_level_list[index]
|
||||||
|
|
||||||
# During the initial call to this function, delete the container
|
# During the initial call to this function, delete the container
|
||||||
# object from the Video Index (which automatically resets the Video
|
# object from the Video Index (which automatically resets the Video
|
||||||
# Catalogue)
|
# Catalogue)
|
||||||
if not recursive_flag:
|
# (If deleting the contents of temporary folders while loading a
|
||||||
|
# Tartube database, the Video Index may not yet have been drawn, so
|
||||||
|
# we have to check for that)
|
||||||
|
if not recursive_flag and not empty_flag \
|
||||||
|
and media_data_obj.name in self.main_win_obj.video_index_row_dict:
|
||||||
|
|
||||||
self.main_win_obj.video_index_delete_row(media_data_obj)
|
self.main_win_obj.video_index_delete_row(media_data_obj)
|
||||||
|
|
||||||
# Also redraw the private folders in the Video Index, to show the
|
# Also redraw the private folders in the Video Index, to show the
|
||||||
@ -3731,6 +3923,14 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.fixed_fav_folder,
|
self.fixed_fav_folder,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif not recursive_flag and empty_flag:
|
||||||
|
|
||||||
|
# When emptying the container, the quickest way to update the Video
|
||||||
|
# Index is just to redraw it from scratch
|
||||||
|
self.main_win_obj.video_index_reset()
|
||||||
|
self.main_win_obj.video_catalogue_reset()
|
||||||
|
self.main_win_obj.video_index_populate()
|
||||||
|
|
||||||
|
|
||||||
# (Change media data object settings, updating all related things)
|
# (Change media data object settings, updating all related things)
|
||||||
|
|
||||||
@ -3839,7 +4039,7 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.main_win_obj.video_index_update_row_text(container_obj)
|
self.main_win_obj.video_index_update_row_text(container_obj)
|
||||||
|
|
||||||
|
|
||||||
def mark_video_downloaded(self, video_obj, flag):
|
def mark_video_downloaded(self, video_obj, dl_flag, not_new_flag=False):
|
||||||
|
|
||||||
"""Can be called by anything.
|
"""Can be called by anything.
|
||||||
|
|
||||||
@ -3852,8 +4052,12 @@ class TartubeApp(Gtk.Application):
|
|||||||
|
|
||||||
video_obj (media.Video): The media.Video object to mark.
|
video_obj (media.Video): The media.Video object to mark.
|
||||||
|
|
||||||
flag (True or False): True to mark the video as downloaded, False
|
dl_flag (True or False): True to mark the video as downloaded,
|
||||||
to mark it as not downloaded.
|
False to mark it as not downloaded.
|
||||||
|
|
||||||
|
not_new_flag (True or False): Set to True when called by
|
||||||
|
downloads.confirm_old_video(). The video is downloaded, but not
|
||||||
|
new
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -3870,13 +4074,13 @@ class TartubeApp(Gtk.Application):
|
|||||||
'Mark video as downloaded request failed sanity check',
|
'Mark video as downloaded request failed sanity check',
|
||||||
)
|
)
|
||||||
|
|
||||||
elif not flag:
|
elif not dl_flag:
|
||||||
|
|
||||||
# Mark video as not downloaded
|
# Mark video as not downloaded
|
||||||
if not video_obj.dl_flag:
|
if not video_obj.dl_flag:
|
||||||
|
|
||||||
# Already marked
|
# Already marked
|
||||||
return
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
@ -3892,15 +4096,16 @@ class TartubeApp(Gtk.Application):
|
|||||||
update_list.append(self.fixed_fav_folder)
|
update_list.append(self.fixed_fav_folder)
|
||||||
|
|
||||||
# Also mark the video as not new
|
# Also mark the video as not new
|
||||||
self.mark_video_new(video_obj, False, True)
|
if not not_new_flag:
|
||||||
|
self.mark_video_new(video_obj, False, True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# Mark video as downloaded
|
# Mark video as downloaded
|
||||||
if video_obj.dl_flag:
|
if video_obj.dl_flag:
|
||||||
|
|
||||||
# Already marked
|
# Already marked
|
||||||
return
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
@ -3921,7 +4126,8 @@ class TartubeApp(Gtk.Application):
|
|||||||
update_list.append(self.fixed_fav_folder)
|
update_list.append(self.fixed_fav_folder)
|
||||||
|
|
||||||
# Also mark the video as new
|
# Also mark the video as new
|
||||||
self.mark_video_new(video_obj, True, True)
|
if not not_new_flag:
|
||||||
|
self.mark_video_new(video_obj, True, True)
|
||||||
|
|
||||||
# Update rows in the Video Index
|
# Update rows in the Video Index
|
||||||
for container_obj in update_list:
|
for container_obj in update_list:
|
||||||
@ -4095,7 +4301,8 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.main_win_obj.video_index_delete_row(folder_obj)
|
self.main_win_obj.video_index_delete_row(folder_obj)
|
||||||
|
|
||||||
|
|
||||||
def mark_container_favourite(self, media_data_obj, flag):
|
def mark_container_favourite(self, media_data_obj, flag,
|
||||||
|
only_child_videos_flag):
|
||||||
|
|
||||||
"""Called by mainwin.MainWin.on_video_index_mark_favourite() and
|
"""Called by mainwin.MainWin.on_video_index_mark_favourite() and
|
||||||
.on_video_index_mark_not_favourite().
|
.on_video_index_mark_not_favourite().
|
||||||
@ -4112,6 +4319,10 @@ class TartubeApp(Gtk.Application):
|
|||||||
flag (True or False): True to mark as favourite, False to mark as
|
flag (True or False): True to mark as favourite, False to mark as
|
||||||
not favourite
|
not favourite
|
||||||
|
|
||||||
|
only_child_videos_flag (True or False): Set to True if only child
|
||||||
|
video objects should be marked; False if the container object
|
||||||
|
and all its descendants should be marked
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
@ -4154,6 +4365,14 @@ class TartubeApp(Gtk.Application):
|
|||||||
and other_obj.fav_flag:
|
and other_obj.fav_flag:
|
||||||
self.mark_video_favourite(other_obj, flag, True)
|
self.mark_video_favourite(other_obj, flag, True)
|
||||||
|
|
||||||
|
elif only_child_videos_flag:
|
||||||
|
|
||||||
|
# Check videos in this folder
|
||||||
|
for other_obj in media_data_obj.child_list:
|
||||||
|
|
||||||
|
if isinstance(other_obj, media.Video):
|
||||||
|
self.mark_video_favourite(other_obj, flag, True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# Check only video objects that are descendants of the specified
|
# Check only video objects that are descendants of the specified
|
||||||
@ -4847,7 +5066,7 @@ class TartubeApp(Gtk.Application):
|
|||||||
'ok',
|
'ok',
|
||||||
)
|
)
|
||||||
|
|
||||||
elif not source or not utils.url_check(source):
|
elif not source or not utils.check_url(source):
|
||||||
self.dialogue_manager_obj.show_msg_dialogue(
|
self.dialogue_manager_obj.show_msg_dialogue(
|
||||||
'You must enter a valid URL',
|
'You must enter a valid URL',
|
||||||
'error',
|
'error',
|
||||||
@ -4955,20 +5174,6 @@ class TartubeApp(Gtk.Application):
|
|||||||
# In the Video Index, select the parent media data object, which
|
# In the Video Index, select the parent media data object, which
|
||||||
# updates both the Video Index and the Video Catalogue
|
# updates both the Video Index and the Video Catalogue
|
||||||
self.main_win_obj.video_index_select_row(parent_obj)
|
self.main_win_obj.video_index_select_row(parent_obj)
|
||||||
# !!! TODO BUG: That doesn't work, possibly because of the gtk_iter
|
|
||||||
# error we have been getting, so artificially update the Video
|
|
||||||
# Catalogue if the parent container is the visible one
|
|
||||||
# if self.main_win_obj.video_index_current is not None \
|
|
||||||
# and self.main_win_obj.video_index_current == parent_obj.name:
|
|
||||||
# self.main_win_obj.video_catalogue_redraw_all(parent_obj.name)
|
|
||||||
if self.main_win_obj.video_index_current is not None \
|
|
||||||
and (
|
|
||||||
self.main_win_obj.video_index_current == parent_obj.name \
|
|
||||||
or self.main_win_obj.video_index_current == 'All Videos'
|
|
||||||
):
|
|
||||||
self.main_win_obj.video_catalogue_redraw_all(
|
|
||||||
self.main_win_obj.video_index_current,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If any duplicates were found, inform the user
|
# If any duplicates were found, inform the user
|
||||||
if duplicate_list:
|
if duplicate_list:
|
||||||
@ -5329,9 +5534,9 @@ class TartubeApp(Gtk.Application):
|
|||||||
print('ap 4445 set_complex_index_flag')
|
print('ap 4445 set_complex_index_flag')
|
||||||
|
|
||||||
if not flag:
|
if not flag:
|
||||||
self.complex_index = False
|
self.complex_index_flag = False
|
||||||
else:
|
else:
|
||||||
self.complex_index = True
|
self.complex_index_flag = True
|
||||||
|
|
||||||
|
|
||||||
def set_db_backup_mode(self, value):
|
def set_db_backup_mode(self, value):
|
||||||
@ -5508,6 +5713,17 @@ class TartubeApp(Gtk.Application):
|
|||||||
self.operation_save_flag = True
|
self.operation_save_flag = True
|
||||||
|
|
||||||
|
|
||||||
|
def set_system_warning_show_flag(self, flag):
|
||||||
|
|
||||||
|
if DEBUG_FUNC_FLAG:
|
||||||
|
print('ap 4563 set_system_warning_show_flag')
|
||||||
|
|
||||||
|
if not flag:
|
||||||
|
self.system_warning_show_flag = False
|
||||||
|
else:
|
||||||
|
self.system_warning_show_flag = True
|
||||||
|
|
||||||
|
|
||||||
def set_toolbar_squeeze_flag(self, flag):
|
def set_toolbar_squeeze_flag(self, flag):
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
|
591
lib/mainwin.py
@ -1124,6 +1124,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
# Progress List
|
# Progress List
|
||||||
self.progress_list_treeview = Gtk.TreeView()
|
self.progress_list_treeview = Gtk.TreeView()
|
||||||
self.progress_list_frame.add(self.progress_list_treeview)
|
self.progress_list_frame.add(self.progress_list_treeview)
|
||||||
|
self.progress_list_treeview.set_can_focus(False)
|
||||||
|
|
||||||
for i, column_title in enumerate(
|
for i, column_title in enumerate(
|
||||||
[
|
[
|
||||||
@ -1169,6 +1170,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
# Results List
|
# Results List
|
||||||
self.results_list_treeview = Gtk.TreeView()
|
self.results_list_treeview = Gtk.TreeView()
|
||||||
self.results_list_frame.add(self.results_list_treeview)
|
self.results_list_frame.add(self.results_list_treeview)
|
||||||
|
self.results_list_treeview.set_can_focus(False)
|
||||||
|
|
||||||
for i, column_title in enumerate(
|
for i, column_title in enumerate(
|
||||||
[
|
[
|
||||||
@ -1277,6 +1279,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
self.errors_list_treeview = Gtk.TreeView()
|
self.errors_list_treeview = Gtk.TreeView()
|
||||||
self.errors_list_frame.add(self.errors_list_treeview)
|
self.errors_list_frame.add(self.errors_list_treeview)
|
||||||
|
self.errors_list_treeview.set_can_focus(False)
|
||||||
|
|
||||||
for i, column_title in enumerate(['', '', 'Time', 'Media', 'Message']):
|
for i, column_title in enumerate(['', '', 'Time', 'Media', 'Message']):
|
||||||
|
|
||||||
@ -1754,7 +1757,8 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
return -1
|
return -1
|
||||||
elif obj1.upload_time < obj2.upload_time:
|
elif obj1.upload_time < obj2.upload_time:
|
||||||
return 1
|
return 1
|
||||||
else:
|
elif obj1.receive_time is not None \
|
||||||
|
and obj2.receive_time is not None:
|
||||||
# In private folders, the most recently received video goes to
|
# In private folders, the most recently received video goes to
|
||||||
# the top of the list
|
# the top of the list
|
||||||
if self.video_index_current_priv_flag:
|
if self.video_index_current_priv_flag:
|
||||||
@ -1774,6 +1778,8 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@ -1810,6 +1816,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
# Set up the widgets
|
# Set up the widgets
|
||||||
self.video_index_treeview = Gtk.TreeView()
|
self.video_index_treeview = Gtk.TreeView()
|
||||||
self.video_index_frame.add(self.video_index_treeview)
|
self.video_index_frame.add(self.video_index_treeview)
|
||||||
|
self.video_index_treeview.set_can_focus(False)
|
||||||
self.video_index_treeview.set_headers_visible(False)
|
self.video_index_treeview.set_headers_visible(False)
|
||||||
# (Detect right-clicks on the treeview)
|
# (Detect right-clicks on the treeview)
|
||||||
self.video_index_treeview.connect(
|
self.video_index_treeview.connect(
|
||||||
@ -2020,13 +2027,6 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
'Video index setup row request failed sanity check',
|
'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
|
# Add a row to the treeview
|
||||||
if media_data_obj.parent_obj:
|
if media_data_obj.parent_obj:
|
||||||
|
|
||||||
@ -2051,9 +2051,6 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Expand rows to make the new media data object visible...
|
|
||||||
self.video_index_treeview.expand_to_path(parent_ref.get_path())
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# The media data object has no parent, so add a row to the
|
# The media data object has no parent, so add a row to the
|
||||||
@ -2075,13 +2072,22 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
)
|
)
|
||||||
self.video_index_row_dict[media_data_obj.name] = tree_ref
|
self.video_index_row_dict[media_data_obj.name] = tree_ref
|
||||||
|
|
||||||
|
if media_data_obj.parent_obj:
|
||||||
|
|
||||||
|
# Expand rows to make the new media data object visible...
|
||||||
|
self.video_index_treeview.expand_to_path(
|
||||||
|
self.video_index_sortmodel.convert_child_path_to_path(
|
||||||
|
parent_ref.get_path(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Select the row (which clears the Video Catalogue)
|
# Select the row (which clears the Video Catalogue)
|
||||||
selection = self.video_index_treeview.get_selection()
|
selection = self.video_index_treeview.get_selection()
|
||||||
selection.select_path(tree_ref.get_path())
|
selection.select_path(
|
||||||
|
self.video_index_sortmodel.convert_child_path_to_path(
|
||||||
# Re-enable auto-sorting, if disabled
|
tree_ref.get_path(),
|
||||||
if self.video_index_no_sort_flag:
|
),
|
||||||
self.video_index_no_sort_flag = False
|
)
|
||||||
|
|
||||||
# Make the changes visible
|
# Make the changes visible
|
||||||
self.video_index_treeview.show_all()
|
self.video_index_treeview.show_all()
|
||||||
@ -2165,19 +2171,27 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
# Select the row, expanding the treeview path to make it visible, if
|
# Select the row, expanding the treeview path to make it visible, if
|
||||||
# necessary
|
# necessary
|
||||||
tree_ref = self.video_index_row_dict[media_data_obj.name]
|
if media_data_obj.parent_obj:
|
||||||
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)
|
# Expand rows to make the new media data object visible...
|
||||||
|
parent_ref \
|
||||||
|
= self.video_index_row_dict[media_data_obj.parent_obj.name]
|
||||||
|
|
||||||
|
self.video_index_treeview.expand_to_path(
|
||||||
|
self.video_index_sortmodel.convert_child_path_to_path(
|
||||||
|
parent_ref.get_path(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Select the row
|
||||||
|
tree_ref = self.video_index_row_dict[media_data_obj.name]
|
||||||
|
|
||||||
selection = self.video_index_treeview.get_selection()
|
selection = self.video_index_treeview.get_selection()
|
||||||
# !!! TODO BUG: This generates a Gtk error:
|
selection.select_path(
|
||||||
# Gtk-CRITICAL **: gtk_tree_model_sort_get_path: assertion
|
self.video_index_sortmodel.convert_child_path_to_path(
|
||||||
# 'priv->stamp == iter->stamp' failed
|
tree_ref.get_path(),
|
||||||
selection.select_iter(tree_iter)
|
),
|
||||||
|
)
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
|
|
||||||
def video_index_update_row_icon(self, media_data_obj):
|
def video_index_update_row_icon(self, media_data_obj):
|
||||||
@ -2551,8 +2565,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
popup_menu.append(refresh_menu_item)
|
popup_menu.append(refresh_menu_item)
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
separator_item = Gtk.SeparatorMenuItem()
|
popup_menu.append(Gtk.SeparatorMenuItem())
|
||||||
popup_menu.append(separator_item)
|
|
||||||
|
|
||||||
# Apply/remove/edit download options, disable downloads
|
# Apply/remove/edit download options, disable downloads
|
||||||
|
|
||||||
@ -2627,8 +2640,89 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
enforce_check_menu_item.set_sensitive(False)
|
enforce_check_menu_item.set_sensitive(False)
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
separator_item2 = Gtk.SeparatorMenuItem()
|
popup_menu.append(Gtk.SeparatorMenuItem())
|
||||||
popup_menu.append(separator_item2)
|
|
||||||
|
# Contents
|
||||||
|
contents_submenu = Gtk.Menu()
|
||||||
|
|
||||||
|
if not isinstance(media_data_obj, media.Folder):
|
||||||
|
|
||||||
|
self.video_index_setup_contents_submenu(
|
||||||
|
contents_submenu,
|
||||||
|
media_data_obj,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# All contents
|
||||||
|
all_contents_submenu = Gtk.Menu()
|
||||||
|
|
||||||
|
self.video_index_setup_contents_submenu(
|
||||||
|
all_contents_submenu,
|
||||||
|
media_data_obj,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
all_contents_submenu.append(Gtk.SeparatorMenuItem())
|
||||||
|
|
||||||
|
empty_folder_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
||||||
|
'_Empty folder',
|
||||||
|
)
|
||||||
|
empty_folder_menu_item.connect(
|
||||||
|
'activate',
|
||||||
|
self.on_video_index_empty_folder,
|
||||||
|
media_data_obj,
|
||||||
|
)
|
||||||
|
all_contents_submenu.append(empty_folder_menu_item)
|
||||||
|
if not media_data_obj.child_list or media_data_obj.priv_flag:
|
||||||
|
empty_folder_menu_item.set_sensitive(False)
|
||||||
|
|
||||||
|
all_contents_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
||||||
|
'_All contents',
|
||||||
|
)
|
||||||
|
all_contents_menu_item.set_submenu(all_contents_submenu)
|
||||||
|
contents_submenu.append(all_contents_menu_item)
|
||||||
|
|
||||||
|
# Just folder videos
|
||||||
|
just_videos_submenu = Gtk.Menu()
|
||||||
|
|
||||||
|
self.video_index_setup_contents_submenu(
|
||||||
|
just_videos_submenu,
|
||||||
|
media_data_obj,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
just_videos_submenu.append(Gtk.SeparatorMenuItem())
|
||||||
|
|
||||||
|
empty_videos_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
||||||
|
'_Remove videos',
|
||||||
|
)
|
||||||
|
empty_videos_menu_item.connect(
|
||||||
|
'activate',
|
||||||
|
self.on_video_index_remove_videos,
|
||||||
|
media_data_obj,
|
||||||
|
)
|
||||||
|
just_videos_submenu.append(empty_videos_menu_item)
|
||||||
|
if not media_data_obj.child_list or media_data_obj.priv_flag:
|
||||||
|
empty_videos_menu_item.set_sensitive(False)
|
||||||
|
|
||||||
|
just_videos_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
||||||
|
'_Just folder videos',
|
||||||
|
)
|
||||||
|
just_videos_menu_item.set_submenu(just_videos_submenu)
|
||||||
|
contents_submenu.append(just_videos_menu_item)
|
||||||
|
|
||||||
|
contents_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
||||||
|
utils.upper_case_first(media_type) + ' co_ntents',
|
||||||
|
)
|
||||||
|
contents_menu_item.set_submenu(contents_submenu)
|
||||||
|
popup_menu.append(contents_menu_item)
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
popup_menu.append(Gtk.SeparatorMenuItem())
|
||||||
|
|
||||||
# Show properties/downloads, hide folder
|
# Show properties/downloads, hide folder
|
||||||
show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
||||||
@ -2683,57 +2777,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
move_top_menu_item.set_sensitive(False)
|
move_top_menu_item.set_sensitive(False)
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
separator_item3 = Gtk.SeparatorMenuItem()
|
popup_menu.append(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 items
|
||||||
delete_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
delete_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
||||||
@ -2753,6 +2797,72 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
popup_menu.popup(None, None, None, None, event.button, event.time)
|
popup_menu.popup(None, None, None, None, event.button, event.time)
|
||||||
|
|
||||||
|
|
||||||
|
def video_index_setup_contents_submenu(self, submenu, media_data_obj,
|
||||||
|
only_child_videos_flag=False):
|
||||||
|
|
||||||
|
"""Called by self.video_index_popup_menu().
|
||||||
|
|
||||||
|
Sets up a submenu for handling the contents of a channel, playlist
|
||||||
|
or folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
submenu (Gtk.Menu): The submenu to set up, currently empty
|
||||||
|
|
||||||
|
media_data_obj (media.Channel, media.Playlist, media.Folder): The
|
||||||
|
channel, playlist or folder whose contents should be modified
|
||||||
|
by items in the sub-menu
|
||||||
|
|
||||||
|
only_child_videos_flag (True or False): Set to True when only a
|
||||||
|
folder's child videos (not anything in its child channels,
|
||||||
|
playlists or folders) should be modified by items in the
|
||||||
|
sub-menu; False if all child objects should be modified
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
mark_new_menu_item = Gtk.MenuItem.new_with_mnemonic('Mark as _new')
|
||||||
|
mark_new_menu_item.connect(
|
||||||
|
'activate',
|
||||||
|
self.on_video_index_mark_new,
|
||||||
|
media_data_obj,
|
||||||
|
only_child_videos_flag,
|
||||||
|
)
|
||||||
|
submenu.append(mark_new_menu_item)
|
||||||
|
if media_data_obj == self.app_obj.fixed_new_folder:
|
||||||
|
mark_new_menu_item.set_sensitive(False)
|
||||||
|
|
||||||
|
mark_old_menu_item = Gtk.MenuItem.new_with_mnemonic('Mark as n_ot new')
|
||||||
|
mark_old_menu_item.connect(
|
||||||
|
'activate',
|
||||||
|
self.on_video_index_mark_not_new,
|
||||||
|
media_data_obj,
|
||||||
|
only_child_videos_flag,
|
||||||
|
)
|
||||||
|
submenu.append(mark_old_menu_item)
|
||||||
|
|
||||||
|
mark_fav_menu_item = Gtk.MenuItem.new_with_mnemonic('Mark _favourite')
|
||||||
|
mark_fav_menu_item.connect(
|
||||||
|
'activate',
|
||||||
|
self.on_video_index_mark_favourite,
|
||||||
|
media_data_obj,
|
||||||
|
only_child_videos_flag,
|
||||||
|
)
|
||||||
|
submenu.append(mark_fav_menu_item)
|
||||||
|
if media_data_obj == self.app_obj.fixed_fav_folder:
|
||||||
|
mark_fav_menu_item.set_sensitive(False)
|
||||||
|
|
||||||
|
mark_not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic(
|
||||||
|
'Mark not f_avourite',
|
||||||
|
)
|
||||||
|
mark_not_fav_menu_item.connect(
|
||||||
|
'activate',
|
||||||
|
self.on_video_index_mark_not_favourite,
|
||||||
|
media_data_obj,
|
||||||
|
only_child_videos_flag,
|
||||||
|
)
|
||||||
|
submenu.append(mark_not_fav_menu_item)
|
||||||
|
|
||||||
|
|
||||||
# (Video Catalogue)
|
# (Video Catalogue)
|
||||||
|
|
||||||
|
|
||||||
@ -2777,9 +2887,9 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
self.video_catalogue_dict = {}
|
self.video_catalogue_dict = {}
|
||||||
|
|
||||||
# Set up the widgets
|
# Set up the widgets
|
||||||
listbox = Gtk.ListBox()
|
self.catalogue_listbox = Gtk.ListBox()
|
||||||
self.catalogue_frame.add(listbox)
|
self.catalogue_frame.add(self.catalogue_listbox)
|
||||||
self.catalogue_listbox = listbox
|
self.catalogue_listbox.set_can_focus(False)
|
||||||
|
|
||||||
self.catalogue_listbox.set_sort_func(
|
self.catalogue_listbox.set_sort_func(
|
||||||
self.video_catalogue_auto_sort,
|
self.video_catalogue_auto_sort,
|
||||||
@ -2791,7 +2901,8 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
self.catalogue_frame.show_all()
|
self.catalogue_frame.show_all()
|
||||||
|
|
||||||
|
|
||||||
def video_catalogue_redraw_all(self, name, page_num=1):
|
def video_catalogue_redraw_all(self, name, page_num=1,
|
||||||
|
reset_scroll_flag=False):
|
||||||
|
|
||||||
"""Called from callbacks in self.on_video_index_selection_changed(),
|
"""Called from callbacks in self.on_video_index_selection_changed(),
|
||||||
mainapp.TartubeApp.on_button_switch_view(),
|
mainapp.TartubeApp.on_button_switch_view(),
|
||||||
@ -2834,6 +2945,11 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
range 1 to self.catalogue_toolbar_last_page). If None, the
|
range 1 to self.catalogue_toolbar_last_page). If None, the
|
||||||
current page is drawn
|
current page is drawn
|
||||||
|
|
||||||
|
reset_scroll_flag (True or False): Set to True when called by
|
||||||
|
self.on_video_index_selection_changed(). The scrollbars must
|
||||||
|
always be reset when switching between channels/playlist/
|
||||||
|
folders
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
@ -2842,12 +2958,11 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
# If actually switching to a different channel/playlist/folder, or a
|
# If actually switching to a different channel/playlist/folder, or a
|
||||||
# different page on the same channel/playlist/folder, must reset the
|
# different page on the same channel/playlist/folder, must reset the
|
||||||
# scrollbars later in the function
|
# scrollbars later in the function
|
||||||
if self.video_index_current is None \
|
if not reset_scroll_flag:
|
||||||
or self.video_index_current != name \
|
if self.video_index_current is None \
|
||||||
or self.catalogue_toolbar_current_page != page_num:
|
or self.video_index_current != name \
|
||||||
reset_scroll_flag = True
|
or self.catalogue_toolbar_current_page != page_num:
|
||||||
else:
|
reset_scroll_flag = True
|
||||||
reset_scroll_flag = False
|
|
||||||
|
|
||||||
# The parent media data object is a media.Channel, media.playlist or
|
# The parent media data object is a media.Channel, media.playlist or
|
||||||
# media.Folder object
|
# media.Folder object
|
||||||
@ -3062,33 +3177,12 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
missing_obj = self.app_obj.media_reg_dict[dbid]
|
missing_obj = self.app_obj.media_reg_dict[dbid]
|
||||||
|
|
||||||
# Create a new catalogue item
|
# Create a new catalogue item
|
||||||
if self.app_obj.catalogue_mode == 'simple_hide_parent' \
|
self.video_catalogue_insert_item(missing_obj)
|
||||||
or self.app_obj.catalogue_mode == 'simple_show_parent':
|
|
||||||
catalogue_item_obj = SimpleCatalogueItem(
|
|
||||||
self,
|
|
||||||
missing_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
catalogue_item_obj = ComplexCatalogueItem(
|
|
||||||
self,
|
|
||||||
missing_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.video_catalogue_dict[dbid] = catalogue_item_obj
|
# Page is not full, so just create a new catalogue item
|
||||||
|
self.video_catalogue_insert_item(video_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(missing_obj)
|
|
||||||
self.catalogue_listbox.add(wrapper_obj)
|
|
||||||
|
|
||||||
# Populate the row with widgets...
|
|
||||||
catalogue_item_obj.draw_widgets(wrapper_obj)
|
|
||||||
# ...and give them their initial appearance
|
|
||||||
catalogue_item_obj.update_widgets()
|
|
||||||
|
|
||||||
# Update widgets in the toolbar
|
# Update widgets in the toolbar
|
||||||
self.video_catalogue_toolbar_update(
|
self.video_catalogue_toolbar_update(
|
||||||
@ -3104,6 +3198,50 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
self.catalogue_listbox.show_all()
|
self.catalogue_listbox.show_all()
|
||||||
|
|
||||||
|
|
||||||
|
def video_catalogue_insert_item(self, video_obj):
|
||||||
|
|
||||||
|
"""Called by self.video_catalogue_update_row() (only).
|
||||||
|
|
||||||
|
Adds a new mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem
|
||||||
|
to the Video Catalogue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
video_obj (media.Video): The video for which a new catalogue item
|
||||||
|
should be created
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create the new catalogue item
|
||||||
|
if self.app_obj.catalogue_mode == 'simple_hide_parent' \
|
||||||
|
or self.app_obj.catalogue_mode == 'simple_show_parent':
|
||||||
|
catalogue_item_obj = SimpleCatalogueItem(
|
||||||
|
self,
|
||||||
|
video_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
catalogue_item_obj = ComplexCatalogueItem(
|
||||||
|
self,
|
||||||
|
video_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.video_catalogue_dict[video_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.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()
|
||||||
|
|
||||||
|
|
||||||
def video_catalogue_delete_row(self, video_obj):
|
def video_catalogue_delete_row(self, video_obj):
|
||||||
|
|
||||||
"""Called by mainapp.TartubeApp.delete_video(),
|
"""Called by mainapp.TartubeApp.delete_video(),
|
||||||
@ -4241,7 +4379,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
utils.upper_case_first(__main__.__packagename__) + ' error',
|
utils.upper_case_first(__main__.__packagename__) + ' error',
|
||||||
)
|
)
|
||||||
row_list.append(
|
row_list.append(
|
||||||
utils.tidy_up_long_string(str(error_code) + ': ' + msg),
|
utils.tidy_up_long_string('#' + str(error_code) + ': ' + msg),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a new row in the treeview. Doing the .show_all() first
|
# Create a new row in the treeview. Doing the .show_all() first
|
||||||
@ -4256,6 +4394,63 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
self.errors_list_refresh_label()
|
self.errors_list_refresh_label()
|
||||||
|
|
||||||
|
|
||||||
|
def errors_list_add_system_warning(self, error_code, msg):
|
||||||
|
|
||||||
|
"""Can be called by anything. The quickest way is to call
|
||||||
|
mainapp.TartubeApp.system_warning(), which acts as a wrapper for this
|
||||||
|
function.
|
||||||
|
|
||||||
|
Display a system warning 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 warning message to display
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if DEBUG_FUNC_FLAG:
|
||||||
|
print('mw 3631 errors_list_add_system_warning')
|
||||||
|
|
||||||
|
# Prepare the icons
|
||||||
|
pixbuf = self.pixbuf_dict['warning_small']
|
||||||
|
pixbuf2 = self.pixbuf_dict['system_warning_small']
|
||||||
|
|
||||||
|
# Prepare the new row in the treeview
|
||||||
|
row_list = []
|
||||||
|
utc = datetime.datetime.utcfromtimestamp(time.time())
|
||||||
|
time_string = str(utc.strftime('%H:%M:%S'))
|
||||||
|
|
||||||
|
row_list.append(pixbuf)
|
||||||
|
row_list.append(pixbuf2)
|
||||||
|
row_list.append(time_string)
|
||||||
|
row_list.append(
|
||||||
|
utils.upper_case_first(__main__.__packagename__) + ' warning',
|
||||||
|
)
|
||||||
|
row_list.append(
|
||||||
|
utils.tidy_up_long_string('#' + str(error_code) + ': ' + msg) \
|
||||||
|
+ '\n' + utils.tidy_up_long_string(
|
||||||
|
'To disable system warning messages, click Edit >' \
|
||||||
|
+ ' System preferences... > General, and then deselect \'' \
|
||||||
|
+ 'Show system warning messages in the \'Errors/Warnings\'' \
|
||||||
|
+ ' tab\'',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new row in the treeview. Doing the .show_all() first
|
||||||
|
# prevents a Gtk error (for unknown reasons)
|
||||||
|
self.errors_list_treeview.show_all()
|
||||||
|
self.errors_list_liststore.append(row_list)
|
||||||
|
|
||||||
|
# (Don't update the Errors/Warnings tab label if it's the visible
|
||||||
|
# tab)
|
||||||
|
if self.visible_tab_num != 2:
|
||||||
|
self.tab_warning_count += 1
|
||||||
|
self.errors_list_refresh_label()
|
||||||
|
|
||||||
|
|
||||||
def errors_list_refresh_label(self):
|
def errors_list_refresh_label(self):
|
||||||
|
|
||||||
"""Called by self.errors_list_add_row(),
|
"""Called by self.errors_list_add_row(),
|
||||||
@ -4394,7 +4589,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
menu_item (Gtk.MenuItem): The clicked menu item
|
menu_item (Gtk.MenuItem): The clicked menu item
|
||||||
|
|
||||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
media_data_obj (media.Channel, media.Playlist or media.Folder):
|
||||||
The clicked media data object
|
The clicked media data object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -4553,6 +4748,28 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def on_video_index_empty_folder(self, menu_item, media_data_obj):
|
||||||
|
|
||||||
|
"""Called from a callback in self.video_index_popup_menu().
|
||||||
|
|
||||||
|
Empties the folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
menu_item (Gtk.MenuItem): The clicked menu item
|
||||||
|
|
||||||
|
media_data_obj (media.Folder): The clicked media data object
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if DEBUG_FUNC_FLAG:
|
||||||
|
print('mw 3945 on_video_index_empty_folder')
|
||||||
|
|
||||||
|
# The True flag tells the function to empty the container, rather than
|
||||||
|
# delete it
|
||||||
|
self.app_obj.delete_container(media_data_obj, True)
|
||||||
|
|
||||||
|
|
||||||
def on_video_index_enforce_check(self, menu_item, 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().
|
"""Called from a callback in self.video_index_popup_menu().
|
||||||
@ -4586,7 +4803,8 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
self.video_index_update_row_text(media_data_obj)
|
self.video_index_update_row_text(media_data_obj)
|
||||||
|
|
||||||
|
|
||||||
def on_video_index_mark_favourite(self, menu_item, media_data_obj):
|
def on_video_index_mark_favourite(self, menu_item, media_data_obj,
|
||||||
|
only_child_videos_flag):
|
||||||
|
|
||||||
"""Called from a callback in self.video_index_popup_menu().
|
"""Called from a callback in self.video_index_popup_menu().
|
||||||
|
|
||||||
@ -4600,15 +4818,24 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||||
The clicked media data object
|
The clicked media data object
|
||||||
|
|
||||||
|
only_child_videos_flag (True or False): Set to True if only child
|
||||||
|
video objects should be marked; False if all descendants should
|
||||||
|
be marked
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
print('mw 4010 on_video_index_mark_favourite')
|
print('mw 4010 on_video_index_mark_favourite')
|
||||||
|
|
||||||
self.app_obj.mark_container_favourite(media_data_obj, True)
|
self.app_obj.mark_container_favourite(
|
||||||
|
media_data_obj,
|
||||||
|
True,
|
||||||
|
only_child_videos_flag,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def on_video_index_mark_not_favourite(self, menu_item, media_data_obj):
|
def on_video_index_mark_not_favourite(self, menu_item, media_data_obj,
|
||||||
|
only_child_videos_flag):
|
||||||
|
|
||||||
"""Called from a callback in self.video_index_popup_menu().
|
"""Called from a callback in self.video_index_popup_menu().
|
||||||
|
|
||||||
@ -4622,12 +4849,20 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||||
The clicked media data object
|
The clicked media data object
|
||||||
|
|
||||||
|
only_child_videos_flag (True or False): Set to True if only child
|
||||||
|
video objects should be marked; False if all descendants should
|
||||||
|
be marked
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
print('mw 4032 on_video_index_mark_not_favourite')
|
print('mw 4032 on_video_index_mark_not_favourite')
|
||||||
|
|
||||||
self.app_obj.mark_container_favourite(media_data_obj, False)
|
self.app_obj.mark_container_favourite(
|
||||||
|
media_data_obj,
|
||||||
|
False,
|
||||||
|
only_child_videos_flag,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def on_video_index_hide_folder(self, menu_item, media_data_obj):
|
def on_video_index_hide_folder(self, menu_item, media_data_obj):
|
||||||
@ -4651,7 +4886,8 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
self.app_obj.mark_folder_hidden(media_data_obj, True)
|
self.app_obj.mark_folder_hidden(media_data_obj, True)
|
||||||
|
|
||||||
|
|
||||||
def on_video_index_mark_new(self, menu_item, media_data_obj):
|
def on_video_index_mark_new(self, menu_item, media_data_obj,
|
||||||
|
only_child_videos_flag):
|
||||||
|
|
||||||
"""Called from a callback in self.video_index_popup_menu().
|
"""Called from a callback in self.video_index_popup_menu().
|
||||||
|
|
||||||
@ -4666,6 +4902,10 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||||
The clicked media data object
|
The clicked media data object
|
||||||
|
|
||||||
|
only_child_videos_flag (True or False): Set to True if only child
|
||||||
|
video objects should be marked; False if all descendants should
|
||||||
|
be marked
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
@ -4690,17 +4930,27 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
if isinstance(other_obj, media.Video) and other_obj.fav_flag:
|
if isinstance(other_obj, media.Video) and other_obj.fav_flag:
|
||||||
self.app_obj.mark_video_new(other_obj, True)
|
self.app_obj.mark_video_new(other_obj, True)
|
||||||
|
|
||||||
|
elif only_child_videos_flag:
|
||||||
|
|
||||||
|
# Check only videos that are children of the specified media data
|
||||||
|
# object
|
||||||
|
for other_obj in media_data_obj.child_list:
|
||||||
|
if isinstance(other_obj, media.Video):
|
||||||
|
self.app_obj.mark_video_new(other_obj, True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# Check only videos that are descendants of the specified media
|
# Check only videos that are descendants of the specified media
|
||||||
# data object
|
# data object
|
||||||
for other_obj in media_data_obj.compile_all_videos( [] ):
|
for other_obj in media_data_obj.compile_all_videos( [] ):
|
||||||
|
|
||||||
|
# (Only downloaded videos can be marked as new)
|
||||||
if other_obj.dl_flag:
|
if other_obj.dl_flag:
|
||||||
self.app_obj.mark_video_new(other_obj, True)
|
self.app_obj.mark_video_new(other_obj, True)
|
||||||
|
|
||||||
|
|
||||||
def on_video_index_mark_not_new(self, menu_item, media_data_obj):
|
def on_video_index_mark_not_new(self, menu_item, media_data_obj,
|
||||||
|
only_child_videos_flag):
|
||||||
|
|
||||||
"""Called from a callback in self.video_index_popup_menu().
|
"""Called from a callback in self.video_index_popup_menu().
|
||||||
|
|
||||||
@ -4714,6 +4964,10 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||||
The clicked media data object
|
The clicked media data object
|
||||||
|
|
||||||
|
only_child_videos_flag (True or False): Set to True if only child
|
||||||
|
video objects should be marked; False if all descendants should
|
||||||
|
be marked
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
@ -4737,6 +4991,14 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
if isinstance(other_obj, media.Video) and other_obj.fav_flag:
|
if isinstance(other_obj, media.Video) and other_obj.fav_flag:
|
||||||
self.app_obj.mark_video_new(other_obj, False)
|
self.app_obj.mark_video_new(other_obj, False)
|
||||||
|
|
||||||
|
elif only_child_videos_flag:
|
||||||
|
|
||||||
|
# Check only videos that are children of the specified media data
|
||||||
|
# object
|
||||||
|
for other_obj in media_data_obj.child_list:
|
||||||
|
if isinstance(other_obj, media.Video):
|
||||||
|
self.app_obj.mark_video_new(other_obj, False)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# Check only videos that are descendants of the specified media
|
# Check only videos that are descendants of the specified media
|
||||||
@ -4827,6 +5089,29 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
self.app_obj.remove_download_options(media_data_obj)
|
self.app_obj.remove_download_options(media_data_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def on_video_index_remove_videos(self, menu_item, media_data_obj):
|
||||||
|
|
||||||
|
"""Called from a callback in self.video_index_popup_menu().
|
||||||
|
|
||||||
|
Empties all child videos of a folder object, but doesn't remove any
|
||||||
|
child channel, playlist or folder objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
menu_item (Gtk.MenuItem): The clicked menu item
|
||||||
|
|
||||||
|
media_data_obj (media.Folder): The clicked media data object
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if DEBUG_FUNC_FLAG:
|
||||||
|
print('mw 4222 on_video_index_remove_videos')
|
||||||
|
|
||||||
|
for child_obj in media_data_obj.child_list:
|
||||||
|
if isinstance(child_obj, media.Video):
|
||||||
|
self.app_obj.delete_video(child_obj)
|
||||||
|
|
||||||
|
|
||||||
def on_video_index_right_click(self, treeview, event):
|
def on_video_index_right_click(self, treeview, event):
|
||||||
|
|
||||||
"""Called from callback in self.setup_videos_tab().
|
"""Called from callback in self.setup_videos_tab().
|
||||||
@ -4870,7 +5155,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
def on_video_index_selection_changed(self, selection):
|
def on_video_index_selection_changed(self, selection):
|
||||||
|
|
||||||
"""Called from callback in self.on_video_index_selection_changed().
|
"""Called from callback in self.video_index_reset().
|
||||||
|
|
||||||
Also called from callbacks in mainapp.TartubeApp.on_menu_test,
|
Also called from callbacks in mainapp.TartubeApp.on_menu_test,
|
||||||
.on_button_switch_view() and .on_menu_add_video().
|
.on_button_switch_view() and .on_menu_add_video().
|
||||||
@ -4893,7 +5178,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
else:
|
else:
|
||||||
name = model[iter][1]
|
name = model[iter][1]
|
||||||
|
|
||||||
# Don't update the Video Catalogue during certain proecudres, such as
|
# Don't update the Video Catalogue during certain procedures, such as
|
||||||
# removing a row from the Video Index (in which case, the flag will
|
# removing a row from the Video Index (in which case, the flag will
|
||||||
# be set)
|
# be set)
|
||||||
if not self.ignore_video_index_select_flag:
|
if not self.ignore_video_index_select_flag:
|
||||||
@ -4905,7 +5190,6 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
self.video_index_current = name
|
self.video_index_current = name
|
||||||
self.video_catalogue_redraw_all(name)
|
|
||||||
|
|
||||||
dbid = self.app_obj.media_name_dict[name]
|
dbid = self.app_obj.media_name_dict[name]
|
||||||
media_data_obj = self.app_obj.media_reg_dict[dbid]
|
media_data_obj = self.app_obj.media_reg_dict[dbid]
|
||||||
@ -4916,6 +5200,10 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
else:
|
else:
|
||||||
self.video_index_current_priv_flag = False
|
self.video_index_current_priv_flag = False
|
||||||
|
|
||||||
|
# Redraw the Video Catalogue, on the first page, and reset its
|
||||||
|
# scrollbars back to the top
|
||||||
|
self.video_catalogue_redraw_all(name, 1, True)
|
||||||
|
|
||||||
|
|
||||||
def on_video_index_show_downloads(self, menu_item, media_data_obj):
|
def on_video_index_show_downloads(self, menu_item, media_data_obj):
|
||||||
|
|
||||||
@ -5045,7 +5333,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
print('mw 4439 on_video_catalogue_delete_video')
|
print('mw 4439 on_video_catalogue_delete_video')
|
||||||
|
|
||||||
self.app_obj.delete_video(media_data_obj)
|
self.app_obj.delete_video(media_data_obj, False, True)
|
||||||
|
|
||||||
|
|
||||||
def on_video_catalogue_download(self, menu_item, media_data_obj):
|
def on_video_catalogue_download(self, menu_item, media_data_obj):
|
||||||
@ -7300,13 +7588,16 @@ class DeleteContainerDialogue(Gtk.Dialog):
|
|||||||
media_data_obj (media.Channel, media.Playlist or media.Folder): The
|
media_data_obj (media.Channel, media.Playlist or media.Folder): The
|
||||||
container media data object to be deleted
|
container media data object to be deleted
|
||||||
|
|
||||||
|
empty_flag (True or False): If True, the container media data object is
|
||||||
|
to be emptied, rather than being deleted
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Standard class methods
|
# Standard class methods
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, main_win_obj, media_data_obj):
|
def __init__(self, main_win_obj, media_data_obj, empty_flag):
|
||||||
|
|
||||||
if DEBUG_FUNC_FLAG:
|
if DEBUG_FUNC_FLAG:
|
||||||
print('mw 6453 __init__')
|
print('mw 6453 __init__')
|
||||||
@ -7331,9 +7622,14 @@ class DeleteContainerDialogue(Gtk.Dialog):
|
|||||||
folder_count = media_data_obj.count_descendants( [0, 0, 0, 0, 0] )
|
folder_count = media_data_obj.count_descendants( [0, 0, 0, 0, 0] )
|
||||||
|
|
||||||
# Create the dialogue window
|
# Create the dialogue window
|
||||||
|
if not empty_flag:
|
||||||
|
title = 'Delete ' + obj_type
|
||||||
|
else:
|
||||||
|
title = 'Empty ' + obj_type
|
||||||
|
|
||||||
Gtk.Dialog.__init__(
|
Gtk.Dialog.__init__(
|
||||||
self,
|
self,
|
||||||
'Delete ' + obj_type,
|
title,
|
||||||
main_win_obj,
|
main_win_obj,
|
||||||
Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||||
(
|
(
|
||||||
@ -7417,18 +7713,35 @@ class DeleteContainerDialogue(Gtk.Dialog):
|
|||||||
separator = Gtk.HSeparator()
|
separator = Gtk.HSeparator()
|
||||||
grid.attach(separator, 0, 5, 1, 1)
|
grid.attach(separator, 0, 5, 1, 1)
|
||||||
|
|
||||||
label6 = Gtk.Label(
|
if not empty_flag:
|
||||||
'Do you want to delete the ' + obj_type + ' from ' + pkg_string \
|
label6 = Gtk.Label(
|
||||||
+ '\'s data\ndirectory, deleting all of its files, or do you' \
|
'Do you want to delete the ' + obj_type + ' from ' \
|
||||||
+ ' just want to\nremove the ' + obj_type + ' from this list?',
|
+ pkg_string + '\'s data\ndirectory, deleting all of its' \
|
||||||
)
|
+ ' files, or do you just want to\nremove the ' + obj_type \
|
||||||
|
+ ' from this list?',
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
label6 = Gtk.Label(
|
||||||
|
'Do you want to empty the ' + obj_type + ' in ' \
|
||||||
|
+ pkg_string + '\'s data\ndirectory, deleting all of its' \
|
||||||
|
+ ' files, or do you just want to\nempty the ' + obj_type \
|
||||||
|
+ ' in this list?',
|
||||||
|
)
|
||||||
|
|
||||||
grid.attach(label6, 0, 6, 1, 1)
|
grid.attach(label6, 0, 6, 1, 1)
|
||||||
label6.set_alignment(0, 0.5)
|
label6.set_alignment(0, 0.5)
|
||||||
|
|
||||||
self.button = Gtk.RadioButton.new_with_label_from_widget(
|
if not empty_flag:
|
||||||
None,
|
self.button = Gtk.RadioButton.new_with_label_from_widget(
|
||||||
'Just remove the ' + obj_type + ' from this list',
|
None,
|
||||||
)
|
'Just remove the ' + obj_type + ' from this list',
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.button = Gtk.RadioButton.new_with_label_from_widget(
|
||||||
|
None,
|
||||||
|
'Just empty the ' + obj_type + ' in this list',
|
||||||
|
)
|
||||||
|
|
||||||
grid.attach(self.button, 0, 7, 1, 1)
|
grid.attach(self.button, 0, 7, 1, 1)
|
||||||
|
|
||||||
self.button2 = Gtk.RadioButton.new_from_widget(self.button)
|
self.button2 = Gtk.RadioButton.new_from_widget(self.button)
|
||||||
|
51
lib/media.py
@ -315,6 +315,23 @@ class GenericContainer(GenericMedia):
|
|||||||
# Set accessors
|
# Set accessors
|
||||||
|
|
||||||
|
|
||||||
|
def reset_counts(self, vid_count, new_count, fav_count, dl_count):
|
||||||
|
|
||||||
|
"""Called by mainapp.TartubeApp.update_db().
|
||||||
|
|
||||||
|
When a database created by an earlier version of Tartube is loaded,
|
||||||
|
the calling function updates IVs as required.
|
||||||
|
|
||||||
|
This function is called if this object's video counts need to be
|
||||||
|
changed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.vid_count = vid_count
|
||||||
|
self.new_count = new_count
|
||||||
|
self.fav_count = fav_count
|
||||||
|
self.dl_count = dl_count
|
||||||
|
|
||||||
|
|
||||||
def inc_dl_count(self):
|
def inc_dl_count(self):
|
||||||
|
|
||||||
self.dl_count += 1
|
self.dl_count += 1
|
||||||
@ -450,7 +467,8 @@ class GenericRemoteContainer(GenericContainer):
|
|||||||
return -1
|
return -1
|
||||||
elif obj1.upload_time < obj2.upload_time:
|
elif obj1.upload_time < obj2.upload_time:
|
||||||
return 1
|
return 1
|
||||||
else:
|
elif obj1.receive_time is not None \
|
||||||
|
and obj2.receive_time is not None:
|
||||||
if obj1.receive_time < obj2.receive_time:
|
if obj1.receive_time < obj2.receive_time:
|
||||||
return -1
|
return -1
|
||||||
elif obj1.receive_time > obj2.receive_time:
|
elif obj1.receive_time > obj2.receive_time:
|
||||||
@ -796,8 +814,8 @@ class Video(GenericMedia):
|
|||||||
|
|
||||||
def set_name(self, name):
|
def set_name(self, name):
|
||||||
|
|
||||||
"""Called by mainwin.MainWin.results_list_update_row() to set the name
|
"""Called by mainapp.TartubeApp.update_video_when_file_found() to set
|
||||||
of an unnamed video, replacing the default name (specified by
|
the name of an unnamed video, replacing the default name (specified by
|
||||||
mainapp.TartubeApp.default_video_name).
|
mainapp.TartubeApp.default_video_name).
|
||||||
|
|
||||||
Also called by media.VideoDownloader.confirm_sim_video().
|
Also called by media.VideoDownloader.confirm_sim_video().
|
||||||
@ -1066,6 +1084,9 @@ class Channel(GenericRemoteContainer):
|
|||||||
# Set accessors
|
# Set accessors
|
||||||
|
|
||||||
|
|
||||||
|
# def reset_counts(): # Inherited from GenericContainer
|
||||||
|
|
||||||
|
|
||||||
# def set_dl_sim_flag(): # Inherited from GenericMedia
|
# def set_dl_sim_flag(): # Inherited from GenericMedia
|
||||||
|
|
||||||
|
|
||||||
@ -1197,6 +1218,9 @@ class Playlist(GenericRemoteContainer):
|
|||||||
# Set accessors
|
# Set accessors
|
||||||
|
|
||||||
|
|
||||||
|
# def reset_counts(): # Inherited from GenericContainer
|
||||||
|
|
||||||
|
|
||||||
# def set_dl_sim_flag(): # Inherited from GenericMedia
|
# def set_dl_sim_flag(): # Inherited from GenericMedia
|
||||||
|
|
||||||
|
|
||||||
@ -1434,7 +1458,8 @@ class Folder(GenericContainer):
|
|||||||
return -1
|
return -1
|
||||||
elif obj1.upload_time < obj2.upload_time:
|
elif obj1.upload_time < obj2.upload_time:
|
||||||
return 1
|
return 1
|
||||||
else:
|
elif obj1.receive_time is not None \
|
||||||
|
and obj2.receive_time is not None:
|
||||||
# In private folders (e.g. 'All Videos'), the most
|
# In private folders (e.g. 'All Videos'), the most
|
||||||
# recently received video goes to the top of the list
|
# recently received video goes to the top of the list
|
||||||
if self.priv_flag:
|
if self.priv_flag:
|
||||||
@ -1455,6 +1480,8 @@ class Folder(GenericContainer):
|
|||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
@ -1494,21 +1521,7 @@ class Folder(GenericContainer):
|
|||||||
# Set accessors
|
# Set accessors
|
||||||
|
|
||||||
|
|
||||||
def reset_counts(self, vid_count, new_count, fav_count, dl_count):
|
# def reset_counts(): # Inherited from GenericContainer
|
||||||
|
|
||||||
"""Called by mainapp.TartubeApp.update_db().
|
|
||||||
|
|
||||||
When a database created by an earlier version of Tartube is loaded,
|
|
||||||
the calling function updates IVs as required.
|
|
||||||
|
|
||||||
This function is called if this object's video counts need to be
|
|
||||||
changed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.vid_count = vid_count
|
|
||||||
self.new_count = new_count
|
|
||||||
self.fav_count = fav_count
|
|
||||||
self.dl_count = dl_count
|
|
||||||
|
|
||||||
|
|
||||||
# def set_dl_sim_flag(): # Inherited from GenericMedia
|
# def set_dl_sim_flag(): # Inherited from GenericMedia
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
..\..\..\mingw32\bin\python3.exe hello_world.py
|
..\..\..\usr\bin\mintty.exe -h always /bin/env MSYSTEM=MINGW32 /bin/bash -lc /home/user/tartube/hello_world_mswin.sh
|
||||||
PAUSE
|
PAUSE
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
..\..\..\mingw64\bin\python3.exe hello_world.py
|
..\..\..\usr\bin\mintty.exe -h always /bin/env MSYSTEM=MINGW64 /bin/bash -lc /home/user/tartube/hello_world_mswin.sh
|
||||||
PAUSE
|
PAUSE
|
||||||
|
@ -1,2 +1 @@
|
|||||||
..\..\..\mingw32\bin\python3.exe tartube
|
..\..\..\usr\bin\mintty.exe -w hide /bin/env MSYSTEM=MINGW32 /bin/bash -lc /home/user/tartube/tartube_mswin.sh
|
||||||
PAUSE
|
|
||||||
|
@ -1,2 +1 @@
|
|||||||
..\..\..\mingw64\bin\python3.exe tartube
|
..\..\..\usr\bin\mintty.exe -w hide /bin/env MSYSTEM=MINGW64 /bin/bash -lc /home/user/tartube/tartube_mswin.sh
|
||||||
PAUSE
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Tartube v0.6.0 installer script for MS Windows
|
# Tartube v0.7.0 installer script for MS Windows
|
||||||
#
|
#
|
||||||
# Copyright (C) 2019 A S Lewis
|
# Copyright (C) 2019 A S Lewis
|
||||||
#
|
#
|
||||||
@ -207,7 +207,7 @@
|
|||||||
|
|
||||||
;Name and file
|
;Name and file
|
||||||
Name "Tartube"
|
Name "Tartube"
|
||||||
OutFile "install-tartube-0.6.0-32bit.exe"
|
OutFile "install-tartube-0.7.0-32bit.exe"
|
||||||
|
|
||||||
;Default installation folder
|
;Default installation folder
|
||||||
InstallDir "$LOCALAPPDATA\Tartube"
|
InstallDir "$LOCALAPPDATA\Tartube"
|
||||||
@ -286,23 +286,19 @@ Section "Tartube" SecClient
|
|||||||
CreateDirectory "$SMPROGRAMS\Tartube"
|
CreateDirectory "$SMPROGRAMS\Tartube"
|
||||||
CreateShortCut "$SMPROGRAMS\Tartube\Tartube.lnk" \
|
CreateShortCut "$SMPROGRAMS\Tartube\Tartube.lnk" \
|
||||||
"$INSTDIR\msys32\home\user\tartube\tartube_32bit.bat" \
|
"$INSTDIR\msys32\home\user\tartube\tartube_32bit.bat" \
|
||||||
"" \
|
"" "$INSTDIR\tartube_icon.ico" "" SW_SHOWMINIMIZED
|
||||||
"$INSTDIR\tartube_icon.ico"
|
|
||||||
CreateShortCut "$SMPROGRAMS\Tartube\Uninstall Tartube.lnk" \
|
CreateShortCut "$SMPROGRAMS\Tartube\Uninstall Tartube.lnk" \
|
||||||
"$INSTDIR\Uninstall.exe" \
|
"$INSTDIR\Uninstall.exe" \
|
||||||
"" \
|
"" "$INSTDIR\tartube_icon.ico"
|
||||||
"$INSTDIR\tartube_icon.ico"
|
|
||||||
# Temporary Start Menu link for bugfixing on MS Windows 10
|
# Temporary Start Menu link for bugfixing on MS Windows 10
|
||||||
CreateShortCut "$SMPROGRAMS\Tartube\Gtk graphics test.lnk" \
|
CreateShortCut "$SMPROGRAMS\Tartube\Test Gtk graphics.lnk" \
|
||||||
"$INSTDIR\msys32\home\user\tartube\hello_world_32bit.bat" \
|
"$INSTDIR\msys32\home\user\tartube\hello_world_32bit.bat" \
|
||||||
"" \
|
"" "$INSTDIR\tartube_icon.ico" ""
|
||||||
"$INSTDIR\tartube_icon.ico"
|
|
||||||
|
|
||||||
# Desktop icon
|
# Desktop icon
|
||||||
CreateShortcut "$DESKTOP\Tartube.lnk" \
|
CreateShortcut "$DESKTOP\Tartube.lnk" \
|
||||||
"$INSTDIR\msys32\home\user\tartube\tartube_32bit.bat" \
|
"$INSTDIR\msys32\home\user\tartube\tartube_32bit.bat" \
|
||||||
"" \
|
"" "$INSTDIR\tartube_icon.ico" "" SW_SHOWMINIMIZED
|
||||||
"$INSTDIR\tartube_icon.ico"
|
|
||||||
|
|
||||||
# Store installation folder
|
# Store installation folder
|
||||||
WriteRegStr HKLM \
|
WriteRegStr HKLM \
|
||||||
@ -316,7 +312,7 @@ Section "Tartube" SecClient
|
|||||||
"Publisher" "A S Lewis"
|
"Publisher" "A S Lewis"
|
||||||
WriteRegStr HKLM \
|
WriteRegStr HKLM \
|
||||||
"Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
|
"Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
|
||||||
"DisplayVersion" "0.6.0"
|
"DisplayVersion" "0.7.0"
|
||||||
|
|
||||||
# Create uninstaller
|
# Create uninstaller
|
||||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Tartube v0.6.0 installer script for MS Windows
|
# Tartube v0.7.0 installer script for MS Windows
|
||||||
#
|
#
|
||||||
# Copyright (C) 2019 A S Lewis
|
# Copyright (C) 2019 A S Lewis
|
||||||
#
|
#
|
||||||
@ -207,7 +207,7 @@
|
|||||||
|
|
||||||
;Name and file
|
;Name and file
|
||||||
Name "Tartube"
|
Name "Tartube"
|
||||||
OutFile "install-tartube-0.6.0-64bit.exe"
|
OutFile "install-tartube-0.7.0-64bit.exe"
|
||||||
|
|
||||||
;Default installation folder
|
;Default installation folder
|
||||||
InstallDir "$LOCALAPPDATA\Tartube"
|
InstallDir "$LOCALAPPDATA\Tartube"
|
||||||
@ -286,23 +286,19 @@ Section "Tartube" SecClient
|
|||||||
CreateDirectory "$SMPROGRAMS\Tartube"
|
CreateDirectory "$SMPROGRAMS\Tartube"
|
||||||
CreateShortCut "$SMPROGRAMS\Tartube\Tartube.lnk" \
|
CreateShortCut "$SMPROGRAMS\Tartube\Tartube.lnk" \
|
||||||
"$INSTDIR\msys64\home\user\tartube\tartube_64bit.bat" \
|
"$INSTDIR\msys64\home\user\tartube\tartube_64bit.bat" \
|
||||||
"" \
|
"" "$INSTDIR\tartube_icon.ico" "" SW_SHOWMINIMIZED
|
||||||
"$INSTDIR\tartube_icon.ico"
|
|
||||||
CreateShortCut "$SMPROGRAMS\Tartube\Uninstall Tartube.lnk" \
|
CreateShortCut "$SMPROGRAMS\Tartube\Uninstall Tartube.lnk" \
|
||||||
"$INSTDIR\Uninstall.exe" \
|
"$INSTDIR\Uninstall.exe" \
|
||||||
"" \
|
"" "$INSTDIR\tartube_icon.ico"
|
||||||
"$INSTDIR\tartube_icon.ico"
|
|
||||||
# Temporary Start Menu link for bugfixing on MS Windows 10
|
# Temporary Start Menu link for bugfixing on MS Windows 10
|
||||||
CreateShortCut "$SMPROGRAMS\Tartube\Gtk graphics test.lnk" \
|
CreateShortCut "$SMPROGRAMS\Tartube\Test Gtk graphics.lnk" \
|
||||||
"$INSTDIR\msys64\home\user\tartube\hello_world_64bit.bat" \
|
"$INSTDIR\msys64\home\user\tartube\hello_world_64bit.bat" \
|
||||||
"" \
|
"" "$INSTDIR\tartube_icon.ico"
|
||||||
"$INSTDIR\tartube_icon.ico"
|
|
||||||
|
|
||||||
# Desktop icon
|
# Desktop icon
|
||||||
CreateShortcut "$DESKTOP\Tartube.lnk" \
|
CreateShortcut "$DESKTOP\Tartube.lnk" \
|
||||||
"$INSTDIR\msys64\home\user\tartube\tartube_64bit.bat" \
|
"$INSTDIR\msys64\home\user\tartube\tartube_64bit.bat" \
|
||||||
"" \
|
"" "$INSTDIR\tartube_icon.ico" "" SW_SHOWMINIMIZED
|
||||||
"$INSTDIR\tartube_icon.ico"
|
|
||||||
|
|
||||||
# Store installation folder
|
# Store installation folder
|
||||||
WriteRegStr HKLM \
|
WriteRegStr HKLM \
|
||||||
@ -316,7 +312,7 @@ Section "Tartube" SecClient
|
|||||||
"Publisher" "A S Lewis"
|
"Publisher" "A S Lewis"
|
||||||
WriteRegStr HKLM \
|
WriteRegStr HKLM \
|
||||||
"Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
|
"Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
|
||||||
"DisplayVersion" "0.6.0"
|
"DisplayVersion" "0.7.0"
|
||||||
|
|
||||||
# Create uninstaller
|
# Create uninstaller
|
||||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||||
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 192 KiB |
2
setup.py
@ -35,7 +35,7 @@ import setuptools
|
|||||||
# Setup
|
# Setup
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name='tartube',
|
name='tartube',
|
||||||
version='0.6.0',
|
version='0.7.0',
|
||||||
description='GUI front-end for youtube-dl',
|
description='GUI front-end for youtube-dl',
|
||||||
# long_description=long_description,
|
# long_description=long_description,
|
||||||
long_description="""Tartube is a GUI front-end for youtube-dl, partly based
|
long_description="""Tartube is a GUI front-end for youtube-dl, partly based
|
||||||
|
4
tartube
@ -34,8 +34,8 @@ from lib import mainapp
|
|||||||
|
|
||||||
# 'Global' variables
|
# 'Global' variables
|
||||||
__packagename__ = 'tartube'
|
__packagename__ = 'tartube'
|
||||||
__version__ = '0.6.0'
|
__version__ = '0.7.0'
|
||||||
__date__ = '4 Jul 2019'
|
__date__ = '7 Jul 2019'
|
||||||
__copyright__ = 'Copyright \xa9 2019 A S Lewis'
|
__copyright__ = 'Copyright \xa9 2019 A S Lewis'
|
||||||
__license__ = """
|
__license__ = """
|
||||||
Copyright \xc2\xa9 2019 A S Lewis.
|
Copyright \xc2\xa9 2019 A S Lewis.
|
||||||
|
5
tartube_mswin.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Shell script to start Tartube on MS Windows, using the MSYS2 environment
|
||||||
|
# provided by the Tartube installer
|
||||||
|
cd tartube
|
||||||
|
python3 tartube
|