Redesign errors/warnings tab, various fixes

master
A S Lewis 2022-04-05 17:18:06 +01:00
parent 49e1ce0593
commit 8493d8b6d1
33 changed files with 13977 additions and 11629 deletions

View File

@ -61,7 +61,7 @@ For a full list of new features and fixes, see `recent changes <CHANGES>`__.
Stable release: **v2.3.484 (31 Mar 2022)**
Development release: **v2.3.484 (31 Mar 2022)**
Development release: **v2.3.518 (5 Apr 2022)**
Official packages (also available from the `Github release page <https://github.com/axcore/tartube/releases>`__):
@ -896,7 +896,7 @@ You can create as many scheduled downloads as you like. Scheduled downloads are
By default, **Tartube** downloads videos as quickly as possible, one link (URL) at a time. A link might point to an individual video, or it might point to a whole channel or playlist. **Tartube** will try to download every video associated with the link.
A **Custom download** enables you to modify this behaviour, if desired. You can use it to fetch videos from a mirror, add random delays, download video clips, ignore videos without subtitles, or to download videos with the adverts removed.
A **Custom download** enables you to modify this behaviour, if desired. You can use it to fetch videos from a mirror, add random delays, download video clips, download (or ignore) only livestreams, ignore videos without subtitles, or to download videos with the adverts removed.
It's important to note that a custom download behaves exactly like a regular download until you specify the new behaviour.

View File

@ -1 +1 @@
2.3.484
2.3.518

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
icons/large/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
icons/small/live_old.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
# Tartube v2.3.484 installer script for MS Windows
# Tartube v2.3.518 installer script for MS Windows
#
# Copyright (C) 2019-2022 A S Lewis
#
@ -294,7 +294,7 @@
;Name and file
Name "Tartube"
OutFile "install-tartube-2.3.484-64bit.exe"
OutFile "install-tartube-2.3.518-64bit.exe"
;Default installation folder
InstallDir "$LOCALAPPDATA\Tartube"
@ -397,7 +397,7 @@ Section "Tartube" SecClient
# "Publisher" "A S Lewis"
# WriteRegStr HKLM \
# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
# "DisplayVersion" "2.3.484"
# "DisplayVersion" "2.3.518"
# Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.3.484'
__date__ = '31 Mar 2022'
__version__ = '2.3.518'
__date__ = '5 Apr 2022'
__copyright__ = 'Copyright \xa9 2019-2022 A S Lewis'
__license__ = """
Copyright \xa9 2019-2022 A S Lewis.

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.3.484'
__date__ = '31 Mar 2022'
__version__ = '2.3.518'
__date__ = '5 Apr 2022'
__copyright__ = 'Copyright \xa9 2019-2022 A S Lewis'
__license__ = """
Copyright \xa9 2019-2022 A S Lewis.

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.3.484'
__date__ = '31 Mar 2022'
__version__ = '2.3.518'
__date__ = '5 Apr 2022'
__copyright__ = 'Copyright \xa9 2019-2022 A S Lewis'
__license__ = """
Copyright \xa9 2019-2022 A S Lewis.

View File

@ -1,4 +1,4 @@
.TH man 1 "31 Mar 2022" "2.3.484" "tartube man page"
.TH man 1 "5 Apr 2022" "2.3.518" "tartube man page"
.SH NAME
tartube \- GUI front-end for youtube-dl
.SH SYNOPSIS

View File

@ -1,6 +1,6 @@
[Desktop Entry]
Name=Tartube
Version=2.3.484
Version=2.3.518
Exec=tartube
Icon=tartube
Type=Application

View File

@ -185,7 +185,7 @@ for path in glob.glob('sounds/*'):
# Setup
setuptools.setup(
name='tartube',
version='2.3.484',
version='2.3.518',
description='GUI front-end for youtube-dl',
long_description=long_description,
long_description_content_type='text/plain',

File diff suppressed because it is too large Load Diff

View File

@ -514,9 +514,18 @@ class DownloadManager(threading.Thread):
# Tell the Progress List (or Classic Progress List) to display any
# remaining download statistics immediately
if not self.operation_classic_flag:
self.app_obj.main_win_obj.progress_list_display_dl_stats()
GObject.timeout_add(
0,
self.app_obj.main_win_obj.progress_list_display_dl_stats,
)
else:
self.app_obj.main_win_obj.classic_mode_tab_display_dl_stats()
GObject.timeout_add(
0,
self.app_obj.main_win_obj.classic_mode_tab_display_dl_stats,
)
# Any media.Video objects which have been marked as doomed, can now be
# destroyed
@ -883,7 +892,7 @@ class DownloadManager(threading.Thread):
self.doomed_video_list.append(video_obj)
def nudge_progress_bar (self):
def nudge_progress_bar(self):
"""Can be called by anything.
@ -897,7 +906,9 @@ class DownloadManager(threading.Thread):
if self.current_item_obj:
self.app_obj.main_win_obj.update_progress_bar(
GObject.timeout_add(
0,
self.app_obj.main_win_obj.update_progress_bar,
self.current_item_obj.media_data_obj.name,
self.job_count,
len(self.download_list_obj.download_item_list),
@ -1395,7 +1406,11 @@ class DownloadWorker(threading.Thread):
# If the downloads.VideoDownloader object collected any youtube-dl
# error/warning messages, display them in the Error List
if media_data_obj.error_list or media_data_obj.warning_list:
app_obj.main_win_obj.errors_list_add_row(media_data_obj)
GObject.timeout_add(
0,
app_obj.main_win_obj.errors_list_add_operation_msg,
media_data_obj,
)
# In the event of an error, nothing updates the video's row in the
# Video Catalogue, and therefore the error icon won't be visible
@ -1779,7 +1794,9 @@ class DownloadWorker(threading.Thread):
if not self.download_item_obj.operation_classic_flag:
main_win_obj.progress_list_receive_dl_stats(
GObject.timeout_add(
0,
main_win_obj.progress_list_receive_dl_stats,
self.download_item_obj,
dl_stat_dict,
last_flag,
@ -1791,13 +1808,17 @@ class DownloadWorker(threading.Thread):
if last_flag \
and isinstance(self.download_item_obj.media_data_obj, media.Video):
main_win_obj.results_list_update_tooltip(
GObject.timeout_add(
0,
main_win_obj.results_list_update_tooltip,
self.download_item_obj.media_data_obj,
)
else:
main_win_obj.classic_mode_tab_receive_dl_stats(
GObject.timeout_add(
0,
main_win_obj.classic_mode_tab_receive_dl_stats,
self.download_item_obj,
dl_stat_dict,
last_flag,
@ -2042,7 +2063,9 @@ class DownloadList(object):
# Videos in a private folder's .child_list can't be
# downloaded (since they are also a child of a
# channel, playlist or a public folder)
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
301,
_('Cannot download videos in a private folder'),
)
@ -2160,7 +2183,9 @@ class DownloadList(object):
if not download_manager_obj.operation_classic_flag:
main_win_obj.progress_list_receive_dl_stats(
GObject.timeout_add(
0,
main_win_obj.progress_list_receive_dl_stats,
this_item,
dl_stat_dict,
True, # Final set of statistics for this item
@ -2168,7 +2193,9 @@ class DownloadList(object):
else:
main_win_obj.classic_mode_tab_receive_dl_stats(
GObject.timeout_add(
0,
main_win_obj.classic_mode_tab_receive_dl_stats,
this_item,
dl_stat_dict,
True, # Final set of statistics for this item
@ -2289,7 +2316,9 @@ class DownloadList(object):
if self.operation_classic_flag:
self.app_obj.system_error(
GObject.timeout_add(
0,
self.app_obj.system_error,
302,
'Invalid argument in Classic Mode tab download operation',
)
@ -2381,20 +2410,36 @@ class DownloadList(object):
if custom_flag \
and self.custom_dl_obj \
and self.custom_dl_obj.dl_by_video_flag \
and self.custom_dl_obj.dl_precede_flag \
and self.custom_dl_obj.dl_if_subs_flag \
and (
not media_data_obj.subs_list \
or (
self.custom_dl_obj.dl_if_subs_list \
and not utils.match_subs(
self.custom_dl_obj,
media_data_obj.subs_list,
and self.custom_dl_obj.dl_by_video_flag:
if self.custom_dl_obj.dl_precede_flag \
and self.custom_dl_obj.dl_if_subs_flag \
and (
not media_data_obj.subs_list \
or (
self.custom_dl_obj.dl_if_subs_list \
and not utils.match_subs(
self.custom_dl_obj,
media_data_obj.subs_list,
)
)
)
):
return None
):
return None
elif (
self.custom_dl_obj.ignore_stream_flag \
and media_data_obj.live_mode
) or (
self.custom_dl_obj.ignore_old_stream_flag \
and media_data_obj.was_live_flag
) or (
self.custom_dl_obj.dl_if_stream_flag \
and not media_data_obj.live_mode
) or (
self.custom_dl_obj.dl_if_old_stream_flag \
and not media_data_obj.was_live_flag
):
return
# Don't download videos in channels/playlists/folders which have been
# marked unavailable, because their external directory is not
@ -2550,7 +2595,10 @@ class DownloadList(object):
"""
if self.app_obj.classic_options_obj is not None:
if media_data_obj.options_obj is not None:
# (Download options specified by the Drag and Drop tab)
options_manager_obj = media_data_obj.options_obj
elif self.app_obj.classic_options_obj is not None:
options_manager_obj = self.app_obj.classic_options_obj
else:
options_manager_obj = self.app_obj.general_options_obj
@ -3049,6 +3097,19 @@ class VideoDownloader(object):
# ...where 'type' is the string 'error' or 'warning', and 'message'
# is the error/warning generated
self.video_msg_buffer_dict = {}
# Errors/warnings for individual media.Video objects requires special
# handling. We can't predict where, in the check/download process,
# the first error/warning will occur
# Dictionary of videos which have been assigned an error/warning
# by this instance of the VideoDownloader. The first error/warning
# removes any errors/warnings generated by previous operations.
# The call to self.confirm_new_video(), .confirm_old_video() and
# .confirm_sim_video() removes any errors/warnings generated by
# previous operations by consulting this dictionary
# Dictionary in the form
# key = The video ID (corresponds to media.Video.vid)
# value = True (not required)
self.video_error_warning_dict = {}
# For channels/playlists, a list of child media.Video objects, used to
# track missing videos (when required)
@ -3147,16 +3208,19 @@ class VideoDownloader(object):
# Reset the errors/warnings stored in the media data object, the
# last time it was checked/downloaded
self.download_item_obj.media_data_obj.reset_error_warning()
# If two channels/playlists/folders share a download destination,
# we don't want to download both of them at the same time
# If this media data obj shares a download destination with another
# downloads.DownloadWorker, wait until that download has finished
# before starting this one
if not isinstance(
if isinstance(
self.download_item_obj.media_data_obj,
media.Video,
):
self.download_item_obj.media_data_obj.set_block_flag(False)
else:
# If two channels/playlists/folders share a download
# destination, we don't want to download both of them at the
# same time
# If this media data obj shares a download destination with
# another downloads.DownloadWorker, wait until that download
# has finished before starting this one
while self.download_manager_obj.check_master_slave(
self.download_item_obj.media_data_obj,
):
@ -3236,7 +3300,9 @@ class VideoDownloader(object):
# playlist
self.stop()
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
303,
'Enforced timeout because downloader took too long to' \
+ ' fetch a video\'s JSON data',
@ -3292,7 +3358,10 @@ class VideoDownloader(object):
# (The message must be visible in the Errors/Warnings tab, the
# Output tab and/or the terminal)
self.download_item_obj.media_data_obj.set_error(internal_msg)
self.set_error(
self.download_item_obj.media_data_obj,
internal_msg,
)
if app_obj.ytdl_output_stderr_flag:
app_obj.main_win_obj.output_tab_write_stderr(
@ -3407,7 +3476,8 @@ class VideoDownloader(object):
# Stop downloading this URL
self.stop()
media_data_obj.set_error(
self.set_error(
media_data_obj,
'\'' + media_data_obj.name + '\' ' + _(
'This video has a URL that points to a channel or a' \
+ ' playlist, not a video',
@ -3547,11 +3617,14 @@ class VideoDownloader(object):
"""
# Import the main application (for convenience)
# Create shortcut variables (for convenience)
app_obj = self.download_manager_obj.app_obj
media_data_obj = self.download_item_obj.media_data_obj
# Error/warning handling for individual videos
video_obj = None
# Special case: don't add videos to the Tartube database at all
media_data_obj = self.download_item_obj.media_data_obj
if not isinstance(media_data_obj, media.Video) \
and media_data_obj.dl_no_db_flag:
@ -3621,7 +3694,9 @@ class VideoDownloader(object):
)
# Update the main window
app_obj.announce_video_download(
GObject.timeout_add(
0,
app_obj.announce_video_download,
self.download_item_obj,
video_obj,
self.compile_mini_options_dict(
@ -3650,6 +3725,17 @@ class VideoDownloader(object):
# The probable video ID, if captured, can now be reset
self.probable_video_id = None
if video_obj:
# If no errors/warnings were received during this operation,
# errors/warnings that already exist (from previous operations)
# can now be cleared
if not video_obj.dbid in self.video_error_warning_dict:
video_obj.reset_error_warning()
# This confirmation clears a video marked as blocked
video_obj.set_block_flag(False)
# This VideoDownloader can now stop, if required to do so after a video
# has been checked/downloaded
if self.stop_soon_flag:
@ -3691,11 +3777,11 @@ class VideoDownloader(object):
# Contact SponsorBlock server to fetch video slice data
if app_obj.custom_sblock_mirror != '' \
and app_obj.sblock_fetch_flag \
and video_obj.vid != None \
and (not video_obj.slice_list or app_obj.sblock_replace_flag):
and dummy_obj.vid != None \
and (not dummy_obj.slice_list or app_obj.sblock_replace_flag):
utils.fetch_slice_data(
app_obj,
video_obj,
dummy_obj,
self.download_worker_obj.worker_id,
True, # Write to terminal, if allowed
)
@ -3740,9 +3826,14 @@ class VideoDownloader(object):
app_obj = self.download_manager_obj.app_obj
media_data_obj = self.download_item_obj.media_data_obj
# Error/warning handling for individual videos
if isinstance(media_data_obj, media.Video):
video_obj = media_data_obj
else:
video_obj = None
# Special case: don't add videos to the Tartube database at all
if not isinstance(media_data_obj, media.Video) \
and media_data_obj.dl_no_db_flag:
if video_obj is None and media_data_obj.dl_no_db_flag:
# Register the download with DownloadManager, so that download
# limits can be applied, if required
@ -3763,12 +3854,14 @@ class VideoDownloader(object):
self.download_manager_obj.register_video('old')
# All other cases
elif isinstance(media_data_obj, media.Video):
elif video_obj:
if not media_data_obj.dl_flag:
app_obj.mark_video_downloaded(
media_data_obj,
GObject.timeout_add(
0,
app_obj.mark_video_downloaded,
video_obj,
True, # Video is downloaded
True, # Video is not new
)
@ -3786,7 +3879,9 @@ class VideoDownloader(object):
if not match_obj.dl_flag:
app_obj.mark_video_downloaded(
GObject.timeout_add(
0,
app_obj.mark_video_downloaded,
match_obj,
True, # Video is downloaded
True, # Video is not new
@ -3832,13 +3927,19 @@ class VideoDownloader(object):
# container's sub-directory, which (probably) explains
# why we couldn't find a match. Don't add anything to the
# Results List
app_obj.announce_video_clone(video_obj)
GObject.timeout_add(
0,
app_obj.announce_video_clone,
video_obj,
)
else:
# Do add an entry to the Results List (as well as updating
# the Video Catalogue, as normal)
app_obj.announce_video_download(
GObject.timeout_add(
0,
app_obj.announce_video_download,
self.download_item_obj,
video_obj,
self.compile_mini_options_dict(
@ -3849,6 +3950,17 @@ class VideoDownloader(object):
# The probable video ID, if captured, can now be reset
self.probable_video_id = None
if video_obj:
# If no errors/warnings were received during this operation,
# errors/warnings that already exist (from previous operations)
# can now be cleared
if not video_obj.dbid in self.video_error_warning_dict:
video_obj.reset_error_warning()
# This confirmation clears a video marked as blocked
video_obj.set_block_flag(False)
# This VideoDownloader can now stop, if required to do so after a video
# has been checked/downloaded
if self.stop_soon_flag:
@ -3888,7 +4000,9 @@ class VideoDownloader(object):
full_path = json_dict['_filename']
path, filename, extension = self.extract_filename(full_path)
else:
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
304,
'Missing filename in JSON data',
)
@ -3967,6 +4081,14 @@ class VideoDownloader(object):
else:
live_flag = False
if 'was_live' in json_dict:
if json_dict['was_live']:
was_live_flag = True
else:
was_live_flag = False
else:
was_live_flag = False
if 'comments' in json_dict:
comment_list = json_dict['comments']
else:
@ -4111,6 +4233,9 @@ class VideoDownloader(object):
app_obj.main_win_obj.descrip_line_max_len,
)
if was_live_flag:
video_obj.set_was_live_flag(True)
if comment_list and app_obj.comment_store_flag:
video_obj.set_comments(comment_list)
@ -4233,6 +4358,9 @@ class VideoDownloader(object):
app_obj.main_win_obj.descrip_line_max_len,
)
if was_live_flag:
video_obj.set_was_live_flag(True)
if not video_obj.comment_list and comment_list:
video_obj.set_comments(comment_list)
@ -4262,7 +4390,9 @@ class VideoDownloader(object):
# Deal with livestreams
if video_obj.live_mode != 2 and live_flag:
app_obj.mark_video_live(
GObject.timeout_add(
0,
app_obj.mark_video_live,
video_obj,
2, # Livestream is broadcasting
{}, # No livestream data
@ -4272,7 +4402,9 @@ class VideoDownloader(object):
elif video_obj.live_mode != 0 and not live_flag:
app_obj.mark_video_live(
GObject.timeout_add(
0,
app_obj.mark_video_live,
video_obj,
0, # Livestream has finished
{}, # Reset any livestream data
@ -4402,7 +4534,9 @@ class VideoDownloader(object):
and not app_obj.ffmpeg_manager_obj.convert_webp(thumb_path):
app_obj.set_ffmpeg_fail_flag(True)
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
305,
app_obj.ffmpeg_fail_msg,
)
@ -4431,7 +4565,9 @@ class VideoDownloader(object):
# etc, but not 'keep_description', etc
if update_results_flag:
app_obj.announce_video_download(
GObject.timeout_add(
0,
app_obj.announce_video_download,
self.download_item_obj,
video_obj,
# No call to self.compile_mini_options_dict, because this
@ -4492,6 +4628,17 @@ class VideoDownloader(object):
if update_results_flag:
self.download_manager_obj.register_video('sim')
if video_obj:
# If no errors/warnings were received during this operation,
# errors/warnings that already exist (from previous operations)
# can now be cleared
if not video_obj.dbid in self.video_error_warning_dict:
video_obj.reset_error_warning()
# This confirmation clears a video marked as blocked
video_obj.set_block_flag(False)
# Stop checking videos in this channel/playlist, if a limit has been
# reached
if stop_flag:
@ -4577,7 +4724,8 @@ class VideoDownloader(object):
# New channel/playlist could not be created (for some reason), so
# stop downloading from this URL
self.stop()
media_data_obj.set_error(
self.set_error(
media_data_obj,
'\'' + media_data_obj.name + '\' ' + _(
'This video has a URL that points to a channel or a' \
+ ' playlist, not a video',
@ -4993,8 +5141,9 @@ class VideoDownloader(object):
json_dict = json.loads(stdout)
except:
self.download_manager_obj.app_obj.system_error(
GObject.timeout_add(
0,
self.download_manager_obj.app_obj.system_error,
306,
'Invalid JSON data received from server',
)
@ -5473,8 +5622,7 @@ class VideoDownloader(object):
The error/warning is stored temporarily in self.video_msg_buffer_dict()
until it can be passed on to the media.Video. (If the media.Video still
does not exist, pass it on to the parent channel/playlist/folder
instead).
does not exist, pass it on to the parent channel/playlist instead.)
Args:
@ -5484,7 +5632,9 @@ class VideoDownloader(object):
if not vid in self.video_msg_buffer_dict:
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
999,
'Missing VID in video error/warning buffer',
)
@ -5519,31 +5669,36 @@ class VideoDownloader(object):
if video_obj is None:
# No matching media.Video found; assign the error/warning to
# the parent channel/playlist/folder instead
# the parent channel/playlist instead
if mini_list[0] == 'warning':
self.download_item_obj.media_data_obj.set_warning(
self.set_warning(
self.download_item_obj.media_data_obj,
mini_list[1],
)
else:
self.download_item_obj.media_data_obj.set_error(
self.set_error(
self.download_item_obj.media_data_obj,
mini_list[1],
)
else:
if mini_list[0] == 'warning':
video_obj.set_warning(mini_list[1])
self.set_warning(video_obj, mini_list[1])
else:
video_obj.set_error(mini_list[1])
self.set_error(video_obj, mini_list[1])
# Code in downloads.DownloadWorker.run_video_downloader()
# calls mainwin.MainWin.errors_list_add_row() for the
# main downloads.DownloadItem and its errors/warnings; but
# for a child video, we have to call it directly
# calls mainwin.MainWin.errors_list_add_operation_msg() for
# the main downloads.DownloadItem and its errors/warnings;
# but for a child video, we have to call it directly
# The True argument means 'display the last error/warning only'
# in case the same video generates several errors
app_obj.main_win_obj.errors_list_add_row(video_obj, True)
app_obj.main_win_obj.errors_list_add_operation_msg(
video_obj,
True,
)
GObject.timeout_add(
0,
@ -5581,7 +5736,9 @@ class VideoDownloader(object):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.download_manager_obj.app_obj.system_error(
GObject.timeout_add(
0,
self.download_manager_obj.app_obj.system_error,
999,
'Malformed STDOUT or STDERR data',
)
@ -5703,7 +5860,7 @@ class VideoDownloader(object):
When youtube-dl produces an error or warning (in its STDERR), pass that
error/warning on to the appropriate media data object: the video
responsible, if possible, or the parent channel/playlist/folder if not.
responsible, if possible, or the parent channel/playlist if not.
Args:
@ -5817,26 +5974,35 @@ class VideoDownloader(object):
# errors/warnings to dummy media.Video objects in a channel/
# playlist
if msg_type == 'warning':
self.download_item_obj.media_data_obj.set_warning(data)
self.set_warning(
self.download_item_obj.media_data_obj,
data,
)
else:
self.download_item_obj.media_data_obj.set_error(data)
self.set_error(
self.download_item_obj.media_data_obj,
data,
)
elif new_obj:
# We created a new media.Video object just a moment ago, so
# assign the error/warning to it directly
if msg_type == 'warning':
new_obj.set_warning(data)
self.set_warning(new_obj, data)
else:
new_obj.set_error(data)
self.set_error(new_obj, data)
# Code in downloads.DownloadWorker.run_video_downloader()
# calls mainwin.MainWin.errors_list_add_row() for the
# main downloads.DownloadItem and its errors/warnings; but
# for a child video, we have to call it directly
# calls mainwin.MainWin.errors_list_add_operation_msg() for
# the main downloads.DownloadItem and its errors/warnings;
# but for a child video, we have to call it directly
# The True argument means 'display the last error/warning only'
# in case the same video generates several errors
app_obj.main_win_obj.errors_list_add_row(new_obj, True)
app_obj.main_win_obj.errors_list_add_operation_msg(
new_obj,
True,
)
GObject.timeout_add(
0,
@ -5852,9 +6018,15 @@ class VideoDownloader(object):
# ID (in which case, the error/warning can be assigned to it
# directly)
if msg_type == 'warning':
self.download_item_obj.media_data_obj.set_warning(data)
self.set_warning(
self.download_item_obj.media_data_obj,
data,
)
else:
self.download_item_obj.media_data_obj.set_error(data)
self.set_error(
self.download_item_obj.media_data_obj,
data,
)
GObject.timeout_add(
0,
@ -5864,13 +6036,18 @@ class VideoDownloader(object):
elif vid is None:
# We are downloading a channel/playlist/folder and the video ID
# is not known, so assign the error/warning to the channel/
# playlist/folder
# We are downloading a channel/playlist and the video ID is not
# known, so assign the error/warning to the channel/playlist
if msg_type == 'warning':
self.download_item_obj.media_data_obj.set_warning(data)
self.set_warning(
self.download_item_obj.media_data_obj,
data,
)
else:
self.download_item_obj.media_data_obj.set_error(data)
self.set_error(
self.download_item_obj.media_data_obj,
data,
)
else:
@ -5883,6 +6060,62 @@ class VideoDownloader(object):
self.video_msg_buffer_dict[vid] = [ [msg_type, data] ]
def set_error(self, media_data_obj, msg):
"""Wrapper for media.Video.set_error().
Args:
media_data_obj (media.Video, media.Channel or media.Playlist):
The media data object to update. Only videos are updated by
this function
msg (str): The error message for this video
"""
if isinstance(media_data_obj, media.Video):
if not media_data_obj.dbid in self.video_error_warning_dict:
# The new error is the first error/warning generated during
# this operation; remove any errors/warnings from previuos
# operations
media_data_obj.reset_error_warning()
self.video_error_warning_dict[media_data_obj.dbid] = True
# Set the new error
media_data_obj.set_error(msg)
def set_warning(self, media_data_obj, msg):
"""Wrapper for media.Video.set_warning().
Args:
media_data_obj (media.Video, media.Channel or media.Playlist):
The media data object to update. Only videos are updated by
this function
msg (str): The warning message for this video
"""
if isinstance(media_data_obj, media.Video):
if not media_data_obj.dbid in self.video_error_warning_dict:
# The new warning is the first error/warning generated during
# this operation; remove any errors/warnings from previuos
# operations
media_data_obj.reset_error_warning()
self.video_error_warning_dict[media_data_obj.dbid] = True
# Set the new warning
media_data_obj.set_warning(msg)
def set_return_code(self, code):
"""Called by self.do_download(), .create_child_process(),
@ -6854,7 +7087,9 @@ class ClipDownloader(object):
except:
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
307,
_(
'Failed to copy the original video\'s' \
@ -6908,14 +7143,18 @@ class ClipDownloader(object):
elif not orig_video_obj.dl_flag:
# Mark the video as downloaded
app_obj.mark_video_downloaded(
GObject.timeout_add(
0,
app_obj.mark_video_downloaded,
orig_video_obj,
True, # Video is downloaded
)
# Do add an entry to the Results List (as well as updating the
# Video Catalogue, as normal)
app_obj.announce_video_download(
GObject.timeout_add(
0,
app_obj.announce_video_download,
self.download_item_obj,
orig_video_obj,
# No call to self.compile_mini_options_dict, because this
@ -7372,7 +7611,9 @@ class ClipDownloader(object):
convert_path,
):
app_obj.set_ffmpeg_fail_flag(True)
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
308,
app_obj.ffmpeg_fail_msg,
)
@ -7415,7 +7656,9 @@ class ClipDownloader(object):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.download_manager_obj.app_obj.system_error(
GObject.timeout_add(
0,
self.download_manager_obj.app_obj.system_error,
999,
'Malformed STDOUT or STDERR data',
)
@ -8232,7 +8475,9 @@ class StreamDownloader(object):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.download_manager_obj.app_obj.system_error(
GObject.timeout_add(
0,
self.download_manager_obj.app_obj.system_error,
999,
'Malformed STDOUT or STDERR data',
)
@ -8364,7 +8609,9 @@ class StreamDownloader(object):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.download_manager_obj.app_obj.system_error(
GObject.timeout_add(
0,
self.download_manager_obj.app_obj.system_error,
999,
'Malformed STDOUT or STDERR data',
)
@ -8809,7 +9056,9 @@ class JSONFetcher(object):
local_thumb_path
):
app_obj.set_ffmpeg_fail_flag(True)
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
309,
app_obj.ffmpeg_fail_msg,
)
@ -8926,7 +9175,9 @@ class JSONFetcher(object):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.download_manager_obj.app_obj.system_error(
GObject.timeout_add(
0,
self.download_manager_obj.app_obj.system_error,
999,
'Malformed STDOUT or STDERR data',
)
@ -8943,7 +9194,9 @@ class JSONFetcher(object):
# Broadcasting livestream detected; create a new media.Video
# object
app_obj.create_livestream_from_download(
GObject.timeout_add(
0,
app_obj.create_livestream_from_download,
self.container_obj,
2, # Livestream has started
self.video_name,
@ -8958,9 +9211,10 @@ class JSONFetcher(object):
live_data_dict = utils.extract_livestream_data(data)
if live_data_dict:
# Waiting livestream detected; create a new media.Video
# object
app_obj.create_livestream_from_download(
# Waiting livestream detected; create a new media.Video object
GObject.timeout_add(
0,
app_obj.create_livestream_from_download,
self.container_obj,
1, # Livestream waiting to start
self.video_name,
@ -9411,7 +9665,9 @@ class MiniJSONFetcher(object):
return json.loads(stdout)
except:
app_obj.system_error(
GObject.timeout_add(
0,
app_obj.system_error,
310,
'Invalid JSON data received from server',
)
@ -9448,7 +9704,9 @@ class MiniJSONFetcher(object):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.download_manager_obj.app_obj.system_error(
GObject.timeout_add(
0,
self.download_manager_obj.app_obj.system_error,
999,
'Malformed STDOUT or STDERR data',
)
@ -9468,7 +9726,9 @@ class MiniJSONFetcher(object):
if self.video_obj.live_mode == 1:
# Waiting livestream has gone live
app_obj.mark_video_live(
GObject.timeout_add(
0,
app_obj.mark_video_live,
self.video_obj,
2, # Livestream is broadcasting
{}, # No livestream data
@ -9486,7 +9746,9 @@ class MiniJSONFetcher(object):
and not json_dict['is_live']:
# Broadcasting livestream has finished
app_obj.mark_video_live(
GObject.timeout_add(
0,
app_obj.mark_video_live,
self.video_obj,
0, # Livestream has finished
{}, # Reset any livestream data
@ -9654,6 +9916,7 @@ class CustomDLManager(object):
# of time between this value and self.delay_max. Ignored if
# self.delay_flag is False
self.delay_min = 0
# During a custom download, any videos whose source URL is YouTube can
# be diverted to another website. This IV uses the values:
# 'default' - Use the original YouTube URL
@ -9669,6 +9932,21 @@ class CustomDLManager(object):
# other value of self.divert_mode
self.divert_website = ''
# If True, don't download broadcasting livestreams. Ignored if
# self.dl_by_video_flag is False
self.ignore_stream_flag = False
# If True, don't download finished livestreams. Ignored if
# self.dl_by_video_flag is False
self.ignore_old_stream_flag = False
# If True, only download broadcasting livestreams. Ignored if
# self.dl_by_video_flag is False. Mutually incompatible with
# self.ignore_stream_flag
self.dl_if_stream_flag = False
# If True, only download finished livestreams. Ignored if
# self.dl_by_video_flag is False. Mutually incompatible with
# self.ignore_old_stream_flag
self.dl_if_old_stream_flag = False
# Public class methods

View File

@ -710,6 +710,7 @@ LARGE_ICON_DICT = {
'attention_large': 'attention.png',
'channel_large': 'channel.png',
'copy_large': 'copy.png',
'cursor_large': 'cursor.png',
'error_large': 'error.png',
'folder_large': 'folder_yellow.png',
'folder_fixed_large': 'folder_green.png',
@ -765,6 +766,8 @@ SMALL_ICON_DICT = {
'likes_small': 'likes.png',
'have_file_small': 'have_file.png',
'live_now_small': 'live_now.png',
'live_old_small': 'live_old.png',
'live_old_no_file_small': 'live_old_no_file.png',
'live_wait_small': 'live_wait.png',
'no_file_small': 'no_file.png',
'slice_small': 'slice.png',

View File

@ -540,7 +540,9 @@ class InfoManager(threading.Thread):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.app_obj.system_error(
GObject.timeout_add(
0,
self.app_obj.system_error,
601,
'Malformed STDOUT or STDERR data',
)

View File

@ -258,6 +258,10 @@ class TartubeApp(Gtk.Application):
# Instance variable (IV) list - other
# -----------------------------------
# Flag set to True when startup is complete. Set to True by the last
# line of code in self.start_continue()
self.startup_complete_flag = False
# Custom locale (can match one of the values in formats.LOCALE_LIST)
self.custom_locale = locale.getdefaultlocale()[0]
@ -448,6 +452,9 @@ class TartubeApp(Gtk.Application):
# Flag set to True if an icon should be displayed in the system tray
self.show_status_icon_flag = True
# Flag set to True if Tartube should open in the system tray. Ignored
# if self.show_status_icon_flag is False
self.open_in_tray_flag = False
# Flag set to True if the main window should close to the tray, rather
# than halting the application altogether. Ignored if
# self.show_status_icon_flag is False
@ -478,13 +485,24 @@ class TartubeApp(Gtk.Application):
# Flag set to True if operation warning messages should be shown in the
# Errors/Warnings tab
self.operation_warning_show_flag = True
# Flag set to True if the date (as well as the time) should be shown in
# the Errors/Warnings tab
self.system_msg_show_date_flag = True
# Flag set to True if the channel/playlist/folder name should be shown
# in the Errors/Warnings tab
self.system_msg_show_container_flag = True
# Flag set to True if the video name should be shown in the Errors/
# Warnings tab
self.system_msg_show_video_flag = True
# Flag set to True if the multi-line messages should be shown in the
# Errors/ Warnings tab
self.system_msg_show_multi_line_flag = True
# Flag set to True if the total number of system error/warning messages
# shown in the tab label is not reset until the 'Clear the list'
# button is explicitly clicked (normally, the total numbers are
# reset when the user switches to a different tab)
# visible (not including hidden messages) in the tab label is not
# reset until the 'Clear the list' button is explicitly clicked
# (normally, the total numbers are reset when the user switches to a
# different tab)
self.system_msg_keep_totals_flag = False
# For quick lookup, the directory in which the 'tartube' executable
@ -1332,6 +1350,16 @@ class TartubeApp(Gtk.Application):
r'^downloads$',
__main__.__packagename__,
]
# Extended list of forbidden names for channels, playlists and folders
# (on MS Windows). Each item is still illegal if followed by a file
# extension, e.g. 'LPT1.txt'
self.illegal_name_mswin_list = [
'CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8',
'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8',
'LPT9',
]
# Temporary dictionary of channel/playlist names extracted from the
# child video's metadata. The user can use this dictionary to
# update channel/playlists names (for example, if they have been
@ -1488,6 +1516,11 @@ class TartubeApp(Gtk.Application):
# The options.OptionsManager object used in the Classic Mode tab. If
# None, then self.general_options_obj is used
self.classic_options_obj = None
# An ordered list of options.OptionsManager .uids, used in the
# Drag and Drop tab. The list does not have to contain every (or
# even any) items, but there must be no duplicates
# Maximum size is 16 (any more items are ignored)
self.classic_dropzone_list = []
# Flag set to True if the General Options Manager
# (self.general_options_obj) should be cloned whenever the user
# applies a new options manager to a media data object (e.g. by
@ -2946,6 +2979,46 @@ class TartubeApp(Gtk.Application):
)
self.add_action(classic_download_button_action)
# Drag and Drop tab actions
# -------------------------
# Buttons
drag_drop_add_button_action = Gio.SimpleAction.new(
'drag_drop_add_button',
None,
)
drag_drop_add_button_action.connect(
'activate',
self.on_button_drag_drop_add,
)
self.add_action(drag_drop_add_button_action)
# Errors/Warnings tab actions
# ----------------------------
# Buttons
apply_error_filter_button_action = Gio.SimpleAction.new(
'apply_error_filter_toolbutton',
None,
)
apply_error_filter_button_action.connect(
'activate',
self.on_button_apply_error_filter,
)
self.add_action(apply_error_filter_button_action)
cancel_error_filter_button_action = Gio.SimpleAction.new(
'cancel_error_filter_toolbutton',
None,
)
cancel_error_filter_button_action.connect(
'activate',
self.on_button_cancel_error_filter,
)
self.add_action(cancel_error_filter_button_action)
def do_activate(self):
@ -3064,12 +3137,22 @@ class TartubeApp(Gtk.Application):
# Set the General Options Manager
self.general_options_obj = self.create_download_options('general')
self.general_options_obj.set_general_options()
# Apply a different set of download options to the Classic Mode tab, by
# default
self.classic_options_obj = self.create_download_options('classic')
# (In the Classic Mode tab, the average user doesn't need these extra
# files)
self.classic_options_obj.set_classic_mode_options()
# Create a third set of download options for use in the Drag and Drop
# tab
mp3_options_obj = self.create_download_options('mp3')
mp3_options_obj.set_mp3_options()
# Add these options to the Drag and Drop Grid
self.classic_dropzone_list = [
self.general_options_obj.uid,
self.classic_options_obj.uid,
mp3_options_obj.uid,
]
# Set the current FFmpeg Options Manager
self.ffmpeg_options_obj = self.create_ffmpeg_options('default')
@ -3246,14 +3329,19 @@ class TartubeApp(Gtk.Application):
if self.debug_open_top_left_flag:
self.main_win_obj.move(0, 0)
# Make the main window visible
self.main_win_obj.show_all()
# Prepare to add an icon to the system tray, making it visible only if
# required
# Prepare to add an icon to the system tray. It becomes actually
# visible only when settings specify that
# Also, the main window must remain invisible, if settings specify that
# Tartube should open in the system tray
self.status_icon_obj = mainwin.StatusIcon(self)
if self.show_status_icon_flag:
self.status_icon_obj.show_icon()
if self.open_in_tray_flag:
self.main_win_obj.force_invisible()
else:
self.main_win_obj.show_all()
else:
self.main_win_obj.show_all()
# Part 6 - Select a database file
# -------------------------------
@ -3456,8 +3544,16 @@ class TartubeApp(Gtk.Application):
scheduled_obj.set_only_time(time.time() + wait_time)
# Part 13 - Any debug stuff can go here
# Part 13 - Startup complete
# -------------------------------------
# (This flag is necessary, so that Tartube can open in the system tray,
# if settings require that)
self.startup_complete_flag = False
# Part 14 - Any debug stuff can go here
# -------------------------------------
pass
@ -3608,7 +3704,7 @@ class TartubeApp(Gtk.Application):
"""Can be called by anything.
Wrapper function for mainwin.MainWin.errors_list_add_system_error().
Wrapper function for mainwin.MainWin.errors_list_add_system_msg().
Args:
@ -3635,7 +3731,8 @@ class TartubeApp(Gtk.Application):
if self.main_win_obj and self.system_error_show_flag:
GObject.timeout_add(
0,
self.main_win_obj.errors_list_add_system_error,
self.main_win_obj.errors_list_add_system_msg,
'error',
error_code,
msg,
)
@ -3649,7 +3746,7 @@ class TartubeApp(Gtk.Application):
"""Can be called by anything.
Wrapper function for mainwin.MainWin.errors_list_add_system_warning().
Wrapper function for mainwin.MainWin.errors_list_add_system_msg().
Args:
@ -3664,7 +3761,8 @@ class TartubeApp(Gtk.Application):
if self.main_win_obj and self.system_warning_show_flag:
GObject.timeout_add(
0,
self.main_win_obj.errors_list_add_system_warning,
self.main_win_obj.errors_list_add_system_msg,
'warning',
error_code,
msg,
)
@ -3924,6 +4022,9 @@ class TartubeApp(Gtk.Application):
if version >= 1003024: # v1.3.024
self.show_status_icon_flag = json_dict['show_status_icon_flag']
if version >= 2003504: # v1.3.504
self.open_in_tray_flag = json_dict['open_in_tray_flag']
if version >= 1003024: # v1.3.024
self.close_to_tray_flag = json_dict['close_to_tray_flag']
if version >= 2003125: # v2.3.125
self.restore_posn_from_tray_flag \
@ -3948,6 +4049,13 @@ class TartubeApp(Gtk.Application):
if version >= 2003116: # v2.3.116
self.system_msg_show_date_flag \
= json_dict['system_msg_show_date_flag']
if version >= 2003513: # v2.3.513
self.system_msg_show_container_flag \
= json_dict['system_msg_show_container_flag']
self.system_msg_show_video_flag \
= json_dict['system_msg_show_video_flag']
self.system_msg_show_multi_line_flag \
= json_dict['system_msg_show_multi_line_flag']
if version >= 1000007: # v1.0.007
self.system_msg_keep_totals_flag \
= json_dict['system_msg_keep_totals_flag']
@ -4489,7 +4597,7 @@ class TartubeApp(Gtk.Application):
= json_dict['ytdlp_filter_options_flag']
# Having loaded the config file, set various file paths...
if self.data_dir_use_first_flag:
if self.data_dir_use_first_flag and self.data_dir_alt_list:
self.data_dir = self.data_dir_alt_list[0]
self.update_data_dirs()
@ -4997,6 +5105,7 @@ class TartubeApp(Gtk.Application):
'drag_thumb_path_flag': self.drag_thumb_path_flag,
'show_status_icon_flag': self.show_status_icon_flag,
'open_in_tray_flag': self.open_in_tray_flag,
'close_to_tray_flag': self.close_to_tray_flag,
'restore_posn_from_tray_flag': self.restore_posn_from_tray_flag,
@ -5008,6 +5117,11 @@ class TartubeApp(Gtk.Application):
'operation_error_show_flag': self.operation_error_show_flag,
'operation_warning_show_flag': self.operation_warning_show_flag,
'system_msg_show_date_flag': self.system_msg_show_date_flag,
'system_msg_show_container_flag': \
self.system_msg_show_container_flag,
'system_msg_show_video_flag': self.system_msg_show_video_flag,
'system_msg_show_multi_line_flag': \
self.system_msg_show_multi_line_flag,
'system_msg_keep_totals_flag': self.system_msg_keep_totals_flag,
'data_dir': self.data_dir,
@ -5451,7 +5565,9 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.video_catalogue_reset()
self.main_win_obj.progress_list_reset()
self.main_win_obj.results_list_reset()
self.main_win_obj.show_all()
# If opening Tartube in the system tray, we can't call .show_all()
if self.startup_complete_flag or not self.open_in_tray_flag:
self.main_win_obj.show_all()
# Most main widgets are desensitised, until the database file has been
# loaded
@ -5576,6 +5692,8 @@ class TartubeApp(Gtk.Application):
# self.classic_options_list = load_dict['classic_options_list']
if version >= 2001007: # v2.1.007
self.classic_options_obj = load_dict['classic_options_obj']
if version >= 2003487: # v2.3.487
self.classic_dropzone_list = load_dict['classic_dropzone_list']
if version >= 2002149: # v2.2.149
self.ffmpeg_reg_count = load_dict['ffmpeg_reg_count']
self.ffmpeg_reg_dict = load_dict['ffmpeg_reg_dict']
@ -5657,9 +5775,11 @@ class TartubeApp(Gtk.Application):
):
self.main_win_obj.hide_system_menu_item.set_active(False)
# Repopulate the Video Index, showing the new data
if self.main_win_obj:
# Repopulate the Video Index, showing the new data
self.main_win_obj.video_index_catalogue_reset()
# Repopulate the Drag and Drop tab
self.main_win_obj.drag_drop_grid_reset()
return True
@ -5689,12 +5809,19 @@ class TartubeApp(Gtk.Application):
options_obj_list = [self.general_options_obj]
if self.classic_options_obj:
options_obj_list.append(self.classic_options_obj)
for options_obj in self.options_reg_dict.values():
if options_obj != self.general_options_obj \
and (
self.classic_options_obj is None \
or options_obj != self.general_options_obj
):
options_obj_list.append(options_obj)
options_media_list = []
for media_data_obj in self.media_reg_dict.values():
if media_data_obj.options_obj is not None \
and not media_data_obj.options_obj in options_obj_list:
options_obj_list.append(media_data_obj.options_obj)
# options_obj_list.append(media_data_obj.options_obj)
options_media_list.append(media_data_obj)
if version < 3012: # v0.3.012
@ -6899,6 +7026,58 @@ class TartubeApp(Gtk.Application):
if scheduled_obj.start_mode == 'none':
scheduled_obj.start_mode = 'disabled'
if version < 2003487: # v2.3.487
# This version provides options.OptionsManager objects with a
# short description
for options_obj in options_obj_list:
if options_obj == self.general_options_obj:
options_obj.descrip = _(
'General (default) download options',
)
elif self.classic_options_obj \
and options_obj == self.classic_options_obj:
options_obj.descrip = _(
'Download options for the Classic Mode tab',
)
else:
options_obj.descrip = options_obj.name
# This version also adds options.OptionsManager objects to an
# ordered list for use in the Drag and Drop tab
self.classic_dropzone_list = [self.general_options_obj.uid]
if self.classic_options_obj:
self.classic_dropzone_list.append(self.classic_options_obj.uid)
# If the user has already created an options manager called 'mp3',
# use it; otherwise create a new one (as self.start() does)
match_flag = False
for options_obj in options_obj_list:
if options_obj.name == 'mp3':
match_flag = True
self.classic_dropzone_list.append(options_obj.uid)
break
if not match_flag:
mp3_options_obj = self.create_download_options('mp3')
mp3_options_obj.set_mp3_options()
self.classic_dropzone_list.append(mp3_options_obj.uid)
if version < 2003510: # v2.3.510
# This version adds new IVs to downloads.CustomDLManager objects
for custom_dl_obj in self.custom_dl_reg_dict.values():
custom_dl_obj.ignore_stream_flag = False
custom_dl_obj.ignore_old_stream_flag = False
custom_dl_obj.dl_if_stream_flag = False
custom_dl_obj.dl_if_old_stream_flag = False
# --- Do this last, or the call to .check_integrity_db() fails -------
# --------------------------------------------------------------------
@ -7002,6 +7181,7 @@ class TartubeApp(Gtk.Application):
'options_reg_dict' : self.options_reg_dict,
'general_options_obj' : self.general_options_obj,
'classic_options_obj' : self.classic_options_obj,
'classic_dropzone_list': self.classic_dropzone_list,
# FFmpeg options
'ffmpeg_reg_count' : self.ffmpeg_reg_count,
'ffmpeg_reg_dict' : self.ffmpeg_reg_dict,
@ -9718,7 +9898,7 @@ class TartubeApp(Gtk.Application):
for scheduled_obj in media_data_list:
scheduled_obj.set_last_time(int(time.time()))
# Conver the 'operation_type' for this download operation from
# Convert the 'operation_type' for this download operation from
# 'custom_real' to 'custom_sim' or 'real', as required
if first_obj.dl_mode == 'custom_real':
@ -12420,6 +12600,35 @@ class TartubeApp(Gtk.Application):
json_dict['chapters'],
)
if mode == 'default':
if 'is_live' in json_dict \
and json_dict['is_live'] \
and not video_obj.live_mode \
and not video_obj.was_live_flag:
self.mark_video_live(
video_obj,
2,
{},
True, # Don't update Video Index
True, # Don't update Video Catalogue
True, # Don't sort the parent container
)
elif 'was_live' in json_dict \
and json_dict['was_live']:
if video_obj.live_mode:
self.mark_video_live(
video_obj,
0,
{},
True, # Don't update Video Index
True, # Don't update Video Catalogue
True, # Don't sort the parent container
)
elif not video_obj.was_live_flag:
video_obj.set_was_live_flag(True)
def update_video_from_filesystem(self, video_obj, video_path,
override_flag=False):
@ -18915,6 +19124,11 @@ class TartubeApp(Gtk.Application):
if isinstance(config_win_obj, config.SystemPrefWin):
config_win_obj.setup_options_dl_list_tab_update_treeview()
# Remove any associated dropzone, and update the Drag and Drop tab
if options_obj.uid in self.classic_dropzone_list:
self.classic_dropzone_list.remove(options_obj.uid)
self.main_win_obj.drag_drop_grid_reset()
def apply_classic_download_options(self, options_obj):
@ -20097,6 +20311,8 @@ class TartubeApp(Gtk.Application):
space of the Video Catalogue grid (when visible), and increase/
reduces the size of the grid, if necessary.
Resets any confirmation messages in the Drag and Drop tab.
Returns:
1 to keep the timer going, or None to halt it
@ -20124,6 +20340,11 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.video_catalogue_retry_insert_items,
)
# Reset confirmation messages in the Drag and Drop tab, if it's time
# to reset them
for wrapper_obj in self.main_win_obj.drag_drop_dict.values():
wrapper_obj.check_reset()
# Return 1 to keep the timer going
return 1
@ -20390,6 +20611,25 @@ class TartubeApp(Gtk.Application):
# (Menu item and toolbar button callbacks)
def on_button_apply_error_filter(self, action, par):
"""Called from a callback in self.do_startup().
Applies a filter to the Errors List, hiding any messages which don't
match the search text specified by the user.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Apply the filter
self.main_win_obj.errors_list_apply_filter()
def on_button_apply_filter(self, action, par):
"""Called from a callback in self.do_startup().
@ -20416,6 +20656,24 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.video_catalogue_apply_filter()
def on_button_cancel_error_filter(self, action, par):
"""Called from a callback in self.do_startup().
Cancels the filter, restoring filtered messages in the Errors List.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Apply the filter
self.main_win_obj.errors_list_cancel_filter()
def on_button_cancel_filter(self, action, par):
"""Called from a callback in self.do_startup().
@ -21047,6 +21305,24 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.video_catalogue_unshow_date()
def on_button_drag_drop_add(self, action, par):
"""Called from a callback in self.do_startup().
Adds a new dropzone in the Drag and Drop tab.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Open the popup menu
self.main_win_obj.drag_drop_add_dropzone()
def on_button_find_date(self, action, par):
"""Called from a callback in self.do_startup().
@ -21702,48 +21978,61 @@ class TartubeApp(Gtk.Application):
keep_open_flag = self.dialogue_keep_open_flag
# Remove leading/trailing whitespace from the name; make
# sure the name is not excessively long
# sure the name is not excessively long; reject system
# illegal names
name = utils.tidy_up_container_name(
self,
name,
self.container_name_max_len,
)
if name == '':
keep_open_flag = False
self.dialogue_manager_obj.show_msg_dialogue(
_('That name is not permitted on your system'),
'error',
'ok',
)
# Find the parent media data object (a media.Folder), if
# specified
parent_obj = None
if parent_name and parent_name in self.media_name_dict:
dbid = self.media_name_dict[parent_name]
parent_obj = self.media_reg_dict[dbid]
else:
if self.dialogue_keep_open_flag \
and self.dialogue_keep_container_flag:
suggest_parent_name = parent_name
# Find the parent media data object (a media.Folder),
# if specified
parent_obj = None
if parent_name and parent_name in self.media_name_dict:
dbid = self.media_name_dict[parent_name]
parent_obj = self.media_reg_dict[dbid]
# Create the new channel
channel_obj = self.add_channel(
name,
parent_obj,
source,
dl_sim_flag,
)
if self.dialogue_keep_open_flag \
and self.dialogue_keep_container_flag:
suggest_parent_name = parent_name
# Add the channel to Video Index
if channel_obj:
# Create the new channel
channel_obj = self.add_channel(
name,
parent_obj,
source,
dl_sim_flag,
)
if suggest_parent_name is not None \
and suggest_parent_name \
== self.main_win_obj.video_index_current:
# The channel has been added to the currently
# selected folder; the True argument tells the
# function not to select the channel
self.main_win_obj.video_index_add_row(
channel_obj,
True,
)
# Add the channel to Video Index
if channel_obj:
else:
# Do select the new channel
self.main_win_obj.video_index_add_row(channel_obj)
if suggest_parent_name is not None \
and suggest_parent_name \
== self.main_win_obj.video_index_current:
# The channel has been added to the currently
# selected folder; the True argument tells
# the function not to select the channel
self.main_win_obj.video_index_add_row(
channel_obj,
True,
)
else:
# Do select the new channel
self.main_win_obj.video_index_add_row(
channel_obj,
)
def on_menu_add_folder(self, action, par):
@ -21821,35 +22110,44 @@ class TartubeApp(Gtk.Application):
# Remove leading/trailing whitespace from the name; make sure
# the name is not excessively long
name = utils.tidy_up_container_name(
self,
name,
self.container_name_max_len,
)
if name == '':
self.dialogue_manager_obj.show_msg_dialogue(
_('That name is not permitted on your system'),
'error',
'ok',
)
# Find the parent media data object (a media.Folder), if
# specified
parent_obj = None
if parent_name and parent_name in self.media_name_dict:
dbid = self.media_name_dict[parent_name]
parent_obj = self.media_reg_dict[dbid]
else:
# Create the new folder
folder_obj = self.add_folder(name, parent_obj, dl_sim_flag)
# Find the parent media data object (a media.Folder), if
# specified
parent_obj = None
if parent_name and parent_name in self.media_name_dict:
dbid = self.media_name_dict[parent_name]
parent_obj = self.media_reg_dict[dbid]
# Add the folder to the Video Index
if folder_obj:
# Create the new folder
folder_obj = self.add_folder(name, parent_obj, dl_sim_flag)
if self.main_win_obj.video_index_current:
# The new folder has been added inside the currently
# selected folder; the True argument tells the
# function not to select the new folder
self.main_win_obj.video_index_add_row(
folder_obj,
True,
)
# Add the folder to the Video Index
if folder_obj:
else:
# Do select the new folder
self.main_win_obj.video_index_add_row(folder_obj)
if self.main_win_obj.video_index_current:
# The new folder has been added inside the
# currently selected folder; the True argument
# tells the function not to select the new folder
self.main_win_obj.video_index_add_row(
folder_obj,
True,
)
else:
# Do select the new folder
self.main_win_obj.video_index_add_row(folder_obj)
def on_menu_add_playlist(self, action, par):
@ -21961,46 +22259,58 @@ class TartubeApp(Gtk.Application):
# Remove leading/trailing whitespace from the name; make
# sure the name is not excessively long
name = utils.tidy_up_container_name(
self,
name,
self.container_name_max_len,
)
if name == '':
keep_open_flag = False
self.dialogue_manager_obj.show_msg_dialogue(
_('That name is not permitted on your system'),
'error',
'ok',
)
# Find the parent media data object (a media.Folder), if
# specified
parent_obj = None
if parent_name and parent_name in self.media_name_dict:
dbid = self.media_name_dict[parent_name]
parent_obj = self.media_reg_dict[dbid]
else:
if self.dialogue_keep_open_flag \
and self.dialogue_keep_container_flag:
suggest_parent_name = parent_name
# Find the parent media data object (a media.Folder),
# if specified
parent_obj = None
if parent_name and parent_name in self.media_name_dict:
dbid = self.media_name_dict[parent_name]
parent_obj = self.media_reg_dict[dbid]
# Create the playlist
playlist_obj = self.add_playlist(
name,
parent_obj,
source,
dl_sim_flag,
)
if self.dialogue_keep_open_flag \
and self.dialogue_keep_container_flag:
suggest_parent_name = parent_name
# Add the playlist to the Video Index
if playlist_obj:
# Create the playlist
playlist_obj = self.add_playlist(
name,
parent_obj,
source,
dl_sim_flag,
)
if suggest_parent_name is not None \
and suggest_parent_name \
== self.main_win_obj.video_index_current:
# The playlist has been added to the currently
# selected folder; the True argument tells the
# function not to select the playlist
self.main_win_obj.video_index_add_row(
playlist_obj,
True,
)
# Add the playlist to the Video Index
if playlist_obj:
else:
# Do select the new playlist
self.main_win_obj.video_index_add_row(playlist_obj)
if suggest_parent_name is not None \
and suggest_parent_name \
== self.main_win_obj.video_index_current:
# The playlist has been added to the currently
# selected folder; the True argument tells
# the function not to select the playlist
self.main_win_obj.video_index_add_row(
playlist_obj,
True,
)
else:
# Do select the new playlist
self.main_win_obj.video_index_add_row(
playlist_obj,
)
def on_menu_add_video(self, action, par):
@ -23453,6 +23763,16 @@ class TartubeApp(Gtk.Application):
self.catalogue_sort_mode = mode
def add_classic_dropzone_list(self, value):
self.classic_dropzone_list.append(value)
def del_classic_dropzone_list(self, value):
self.classic_dropzone_list.remove(value)
def set_classic_format_convert_flag(self, flag):
if not flag:
@ -24155,6 +24475,14 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.output_tab_update_page_size()
def set_open_in_tray_flag(self, flag):
if not flag:
self.open_in_tray_flag = False
else:
self.open_in_tray_flag = True
def set_operation_auto_restart_flag(self, flag):
if not flag:
@ -24447,15 +24775,10 @@ class TartubeApp(Gtk.Application):
def set_show_pretty_dates_flag(self, flag):
"""Called by config.SystemPrefWin.on_pretty_date_button_toggled().
Shows/hides the status icon in the system tray.
"""
if not flag:
self.show_pretty_dates_flag = False
else:
self.show_status_icon_flag = True
self.show_pretty_dates_flag = True
# Redraw the Video Catalogue, but only if something was already drawn
# there (and keep the current page number)
@ -24610,6 +24933,14 @@ class TartubeApp(Gtk.Application):
self.system_msg_keep_totals_flag = True
def set_system_msg_show_container_flag(self, flag):
if not flag:
self.system_msg_show_container_flag = False
else:
self.system_msg_show_container_flag = True
def set_system_msg_show_date_flag(self, flag):
if not flag:
@ -24618,6 +24949,22 @@ class TartubeApp(Gtk.Application):
self.system_msg_show_date_flag = True
def set_system_msg_show_multi_line_flag(self, flag):
if not flag:
self.system_msg_show_multi_line_flag = False
else:
self.system_msg_show_multi_line_flag = True
def set_system_msg_show_video_flag(self, flag):
if not flag:
self.system_msg_show_video_flag = False
else:
self.system_msg_show_video_flag = True
def set_system_warning_show_flag(self, flag):
if not flag:

File diff suppressed because it is too large Load Diff

View File

@ -1841,11 +1841,11 @@ class Video(GenericMedia):
# same way, except that for the former, this flag is set to True
# (and a different background colour is used in the Video Catalogue)
self.live_debut_flag = False
# Flag set to True for a video which was a livestream (self.live_mode
# = 1 or 2), but is now not (self.live_mode = 0). Once a livestream
# video has been marked as a normal video, it can't be marked as a
# livestream again. (This prevents any problems in reading the RSS
# feeds from continually marking an old video as a livestream again)
# Flag set to True for a video which was a livestream, but is now not.
# Once a livestream video has been marked as a normal video, it can't
# be marked as a livestream again. (This prevents any problems in
# reading the RSS feeds from continually re-marking an old video as a
# livestream)
self.was_live_flag = False
# The time (matches time.time()) at which a livestream is due to start.
# YouTube supplies an approximate time (perhaps in hours or days),

View File

@ -34,6 +34,8 @@ import formats
import mainapp
import media
import utils
# Use same gettext translations
from mainapp import _
# Classes
@ -611,11 +613,15 @@ class OptionsManager(object):
# Unique ID for this options manager
self.uid = uid
# A non-unique name for this options manager. Managers that are
# attached to a media data object have the same name as that object.
# (The name is not unique because, for example, videos could have the
# attached to a media data object have the same name as that object
# (The name is not unique because, for example, videos could have the
# same name as a channel; it's up to the user to avoid duplicate
# names)
# Empty strings are not valid as names
self.name = name
# A short description, intended for use in the Drag and Drop tab
# Empty strings are valid as descriptions
self.descrip = name
# If this object is attached to a media data object, the .dbid of that
# object; otherwise None
self.dbid = dbid
@ -874,15 +880,35 @@ class OptionsManager(object):
}
def set_classic_mode_options(self):
def set_general_options(self):
"""Called by mainapp.TartubeApp.start().
Configures this object with a suitable description.
"""
self.descrip = _('General (default) download options')
def set_classic_mode_options(self, no_descrip_flag=False):
"""Called by mainapp.TartubeApp.start() and
.apply_classic_download_options().
When the user applies download options in the Classic Mode tab, a few
options should have different default values; this function sets them.
Also called by self.set_mp3_options().
Configures this object for use in the Classic Mode tab.
Args:
no_descrip_flag (bool): Set to True when called from
self.set_mp3_options()
"""
if not no_descrip_flag:
self.descrip = _('Download options for the Classic Mode tab')
self.options_dict['write_description'] = False
self.options_dict['write_info'] = False
self.options_dict['write_annotations'] = False
@ -904,6 +930,24 @@ class OptionsManager(object):
self.options_dict['sim_keep_thumbnail'] = False
def set_mp3_options(self):
"""Called by mainapp.TartubeApp.start().
Configures this object to download MP3s.
"""
self.descrip = _('Download and convert to MP3 (requires FFmpeg)')
# (Actual downloads take place in the Classic Mode tab, so use the same
# file options)
self.set_classic_mode_options(True)
# (Convert everything to MP3)
self.options_dict['extract_audio'] = True
self.options_dict['audio_format'] = 'mp3'
# Set accessors

File diff suppressed because it is too large Load Diff

View File

@ -407,7 +407,9 @@ class ProcessManager(threading.Thread):
return False
# Update the main window's progress bar
self.app_obj.main_win_obj.update_progress_bar(
GObject.timeout_add(
0,
self.app_obj.main_win_obj.update_progress_bar,
orig_video_obj.name,
self.job_count,
self.job_total,

View File

@ -239,7 +239,9 @@ class RefreshManager(threading.Thread):
# Update the main window's progress bar
self.job_count += 1
self.app_obj.main_win_obj.update_progress_bar(
GObject.timeout_add(
0,
self.app_obj.main_win_obj.update_progress_bar,
media_data_obj.name,
self.job_count,
self.job_total,
@ -506,7 +508,9 @@ class RefreshManager(threading.Thread):
# Update the main window's progress bar
self.job_count += 1
self.app_obj.main_win_obj.update_progress_bar(
GObject.timeout_add(
0,
self.app_obj.main_win_obj.update_progress_bar,
media_data_obj.name,
self.job_count,
self.job_total,

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.3.484'
__date__ = '31 Mar 2022'
__version__ = '2.3.518'
__date__ = '5 Apr 2022'
__copyright__ = 'Copyright \xa9 2019-2022 A S Lewis'
__license__ = """
Copyright \xa9 2019-2022 A S Lewis.

View File

@ -540,7 +540,9 @@ class TidyManager(threading.Thread):
# Update the main window's progress bar
self.job_count += 1
self.app_obj.main_win_obj.update_progress_bar(
GObject.timeout_add(
0,
self.app_obj.main_win_obj.update_progress_bar,
media_data_obj.name,
self.job_count,
self.job_total,

View File

@ -403,9 +403,20 @@ class UpdateManager(threading.Thread):
"""
if not system_cmd_flag:
self.app_obj.main_win_obj.output_tab_write_stdout(1, msg)
GObject.timeout_add(
0,
self.app_obj.main_win_obj.output_tab_write_stdout,
1,
msg,
)
else:
self.app_obj.main_win_obj.output_tab_write_system_cmd(1, msg)
GObject.timeout_add(
0,
self.app_obj.main_win_obj.output_tab_write_system_cmd,
1,
msg,
)
def install_ytdl(self):
@ -659,7 +670,9 @@ class UpdateManager(threading.Thread):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.app_obj.system_error(
GObject.timeout_add(
0,
self.app_obj.system_error,
701,
'Malformed STDOUT or STDERR data',
)
@ -721,7 +734,9 @@ class UpdateManager(threading.Thread):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.app_obj.system_error(
GObject.timeout_add(
0,
self.app_obj.system_error,
701,
'Malformed STDOUT or STDERR data',
)
@ -787,7 +802,9 @@ class UpdateManager(threading.Thread):
or (mini_list[1] != 'stdout' and mini_list[1] != 'stderr'):
# Just in case...
self.app_obj.system_error(
GObject.timeout_add(
0,
self.app_obj.system_error,
702,
'Malformed STDOUT or STDERR data',
)

View File

@ -3037,10 +3037,22 @@ def shorten_string_two_lines(string, num_chars):
# To keep the code simple, the algorithm ends here, with this
# word on a separate line. This may produce a return string
# containing only line
if current_line != '':
line_list.append(current_line)
# Exception: try to split URLs on the '/' character neareest
# to the end of the first 'num_chars' characters of 'word'
shortended_word = word[0:num_chars]
pos = shortended_word.rfind('/')
if pos > -1:
pos += 1
line_list.append(word[0:pos])
line_list.append(word[pos:])
else:
if current_line != '':
line_list.append(current_line)
line_list.append(word[0:num_chars] + '...')
line_list.append(word[0:num_chars] + '...')
break
else:
@ -3099,9 +3111,8 @@ def strip_whitespace(string):
"""
if string:
string = re.sub(r'^\s+', '', string)
string = re.sub(r'\s+$', '', string)
if string is not None:
string = string.strip()
return string
@ -3130,8 +3141,7 @@ def strip_whitespace_multiline(string):
mod_list = []
for line in line_list:
line = re.sub(r'^\s+', '', line)
line = re.sub(r'\s+$', '', line)
line = line.strip()
if re.search('\S', line):
mod_list.append(line)
@ -3139,7 +3149,7 @@ def strip_whitespace_multiline(string):
return "\n".join(mod_list)
def tidy_up_container_name(string, max_length):
def tidy_up_container_name(app_obj, string, max_length):
"""Called by mainapp.TartubeApp.on_menu_add_channel(),
.on_menu_add_playlist() and .on_menu_add_folder().
@ -3155,6 +3165,8 @@ def tidy_up_container_name(string, max_length):
Args:
app_obj (mainapp.TartubeApp): The main application
string (str): The string to convert
max_length (int): The maximum length of the converted string (should be
@ -3162,16 +3174,39 @@ def tidy_up_container_name(string, max_length):
Returns:
The converted string
The converted string, or an empty string for an irretrievable name
"""
if string:
string = re.sub(r'^\s+', '', string)
string = re.sub(r'\s+$', '', string)
string = string.strip()
string = re.sub(r'\s+', ' ', string)
string = re.sub(r'[\/\\]', '-', string)
# Get rid of ASCII control characters (illegal on Windows, a pain in
# the behind on POSIX)
string = re.sub(r'[\x00-\x1F]', '', string)
if os.name != 'nt':
# Forbidden characters on POSIX: /
# Forbidden on MacOS, depending on context: :
string = re.sub(r'[\/\:]', '-', string)
else:
# Illegal filenames
if string in app_obj.illegal_name_mswin_list:
return ''
for illegal in app_obj.illegal_name_mswin_list:
if re.search('^' + illegal + '\.'):
return ''
# Forbidden characters on MS Windows: < > : " / \ | ? *
string = re.sub(r'[\<\>\:\/\\\|\?\*]', '-', string)
string = re.sub(r'[\"]', '\'', string)
# Cannot end with a dot
string = re.sub(r'\.+$', '', string)
return string[0:max_length]