Redesign errors/warnings tab, various fixes
parent
49e1ce0593
commit
8493d8b6d1
|
@ -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.
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
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
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[Desktop Entry]
|
||||
Name=Tartube
|
||||
Version=2.3.484
|
||||
Version=2.3.518
|
||||
Exec=tartube
|
||||
Icon=tartube
|
||||
Type=Application
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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',
|
||||
|
|
3374
tartube/config.py
3374
tartube/config.py
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
2532
tartube/mainwin.py
2532
tartube/mainwin.py
File diff suppressed because it is too large
Load Diff
|
@ -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),
|
||||
|
|
|
@ -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
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
Loading…
Reference in New Issue