Update to v2.4.093

master
A S Lewis 2022-07-30 15:56:24 +01:00
parent 2ae0c8a122
commit f1c666c47e
25 changed files with 17775 additions and 7413 deletions

35
CHANGES
View File

@ -1,3 +1,38 @@
v2.4.093 (31 Jul 2022)
-------------------------------------------------------------------------------
MAJOR NEW FEATURES
- In the menu button in the top-right corner of the Classic Mode tab, there is
a new 'Enable one-click downloads' setting. Any valid URLs copied into the
box are downloaded automatically (or added to the existing download)
- Unlisted videos, which youtube-dl can't normally detecT, can now be inserted
into a channel. Right-click the channel and select 'Channel actions >
Insert videos...'. In order to check or download these unlisted videos,
you should either select them, right-click and choose 'Download videos'; or
alternatively, set up custom downloads to download each video individually
(Git #445)
MINOR NEW FEATURES
- In the preferences window, Operations > Livestreams, you can now block all
livestreams from being checked/downloaded. This only works when yt-dlp is
the downloader (Reddit thread)
- In the download options window, you can now use the option '--playlist-items'
(Git #438)
- In the setup wizard window, the text of the 'Install FFmpeg' button now
changes to 'Re-install FFmpeg' after the first installation attempt,
successful or not (for clarity). Both the download and install sizes are
now visible
- Tartube translations can now be prepared using Weblate. See
https://hosted.weblate.org/projects/tartube/ (Git #428)
MAJOR FIXES
- Updated to latest version of MSYS2, with which FFmpeg can be installed
correctly (Git #371, #444)
MINOR FIXES
- Fixed rare Python errors after right-clicking videos in the Progress List
and selecting 'Stop now', 'Stop after this video', etc
v2.4.077 (8 Jun 2022)
-------------------------------------------------------------------------------

View File

@ -66,16 +66,16 @@ For a full list of new features and fixes, see `recent changes <CHANGES>`__.
3 Downloads
===========
Stable release: **v2.4.077 (8 Jun 2022)**
Stable release: **v2.4.093 (31 Jul 2022)**
Development release: **v2.4.077 (8 Jun 2022)**
Development release: **v2.4.093 (31 Jul 2022)**
Official packages (also available from the `Github release page <https://github.com/axcore/tartube/releases>`__):
- `MS Windows (64-bit) installer <https://sourceforge.net/projects/tartube/files/v2.4.077/install-tartube-2.4.077-64bit.exe/download>`__ and `portable edition <https://sourceforge.net/projects/tartube/files/v2.4.077/tartube-2.4.077-64bit-portable.zip/download>`__ from Sourceforge
- `MS Windows (64-bit) installer <https://sourceforge.net/projects/tartube/files/v2.4.093/install-tartube-2.4.093-64bit.exe/download>`__ and `portable edition <https://sourceforge.net/projects/tartube/files/v2.4.093/tartube-2.4.093-64bit-portable.zip/download>`__ from Sourceforge
- Tartube is no longer supported on MS Windows (32-bit) - see `7.23 Doesn't work on 32-bit Windows`_
- `DEB package (for Debian-based distros, e.g. Ubuntu, Linux Mint) <https://sourceforge.net/projects/tartube/files/v2.4.077/python3-tartube_2.4.077.deb/download>`__ from Sourceforge
- `RPM package (for RHEL-based distros, e.g. Fedora) <https://sourceforge.net/projects/tartube/files/v2.4.077/tartube-2.4.077.rpm/download>`__ from Sourceforge
- `DEB package (for Debian-based distros, e.g. Ubuntu, Linux Mint) <https://sourceforge.net/projects/tartube/files/v2.4.093/python3-tartube_2.4.093.deb/download>`__ from Sourceforge
- `RPM package (for RHEL-based distros, e.g. Fedora) <https://sourceforge.net/projects/tartube/files/v2.4.093/tartube-2.4.093.rpm/download>`__ from Sourceforge
Official 'Strict' packages:
@ -92,7 +92,7 @@ Semi-official packages (Linux):
Source code:
- `Source code <https://sourceforge.net/projects/tartube/files/v2.4.077/tartube_v2.4.077.tar.gz/download>`__ from Sourceforge
- `Source code <https://sourceforge.net/projects/tartube/files/v2.4.093/tartube_v2.4.093.tar.gz/download>`__ from Sourceforge
- `Source code <https://github.com/axcore/tartube>`__ and `support <https://github.com/axcore/tartube/issues>`__ from GitHub
- In case this Github repository is taken down, there is an official backup `here <https://gitlab.com/axcore/tartube>`__
@ -1497,7 +1497,7 @@ Every few minutes, **Tartube** checks whether a livestream (or debut) has starte
6.24.2 Customising livestreams
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can modify how often livestreams are checked (and whether they are checked at all). Click **Livestreams > Livestream preferences...**.
You can modify how often livestreams are detected. If you are using **yt-dlp**, you can also prevent livestreams from being downloaded at all. Click **Livestreams > Livestream preferences...**.
.. image:: screenshots/example26.png
:alt: Livestream preferences

View File

@ -1 +1 @@
2.4.077
2.4.093

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.4.077'
__date__ = '8 Jun 2022'
__version__ = '2.4.093'
__date__ = '31 Jul 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.4.077'
__date__ = '8 Jun 2022'
__version__ = '2.4.093'
__date__ = '31 Jul 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.4.077'
__date__ = '8 Jun 2022'
__version__ = '2.4.093'
__date__ = '31 Jul 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 "8 Jun 2022" "2.4.077" "tartube man page"
.TH man 1 "31 Jul 2022" "2.4.093" "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.4.077
Version=2.4.093
Exec=tartube
Icon=tartube
Type=Application

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

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

View File

@ -8474,39 +8474,51 @@ class OptionsEditWin(GenericEditWin):
self.add_tooltip('--playlist-end NUMBER', label2, spinbutton2)
label3 = self.add_label(grid,
_('Abort operation after downloading this many videos'),
_('Download playlist range, in form START:STOP:STEP'),
0, (row_count + 4), 1, 1,
)
entry = self.add_entry(grid,
'playlist_items',
1, (row_count + 4), 1, 1,
)
entry.set_hexpand(True)
self.add_tooltip('--playlist-items ITEM_SPEC', label3, entry)
label4 = self.add_label(grid,
_('Abort operation after downloading this many videos'),
0, (row_count + 5), 1, 1,
)
spinbutton3 = self.add_spinbutton(grid,
0, None, 1,
'max_downloads',
1, (row_count + 4), 1, 1,
1, (row_count + 5), 1, 1,
)
self.add_tooltip('--max-downloads NUMBER', label3, spinbutton3)
self.add_tooltip('--max-downloads NUMBER', label4, spinbutton3)
checkbutton = self.add_checkbutton(grid,
_('Abort downloading the playlist if an error occurs'),
'abort_on_error',
0, (row_count + 5), grid_width, 1,
0, (row_count + 6), grid_width, 1,
)
self.add_tooltip('--abort-on-error', checkbutton)
checkbutton2 = self.add_checkbutton(grid,
_('Download playlist in reverse order'),
'playlist_reverse',
0, (row_count + 6), grid_width, 1,
0, (row_count + 7), grid_width, 1,
)
self.add_tooltip('--playlist-reverse', checkbutton2)
checkbutton3 = self.add_checkbutton(grid,
_('Download playlist in random order'),
'playlist_random',
0, (row_count + 7), grid_width, 1,
0, (row_count + 8), grid_width, 1,
)
self.add_tooltip('--playlist-random', checkbutton3)
return row_count + 7
return row_count + 8
def downloads_size_limit_widgets(self, grid, row_count):
@ -19223,7 +19235,6 @@ class SystemPrefWin(GenericPrefWin):
# (IVs used to handle widget changes in the 'Custom' tab)
self.custom_liststore = None # Gtk.ListStore
# (IVs used to handle widget changes in the 'Livestream' tab)
self.livestream_label = None # Gtk.Label
self.livestream_radiobutton = None # Gtk.RadioButton
self.livestream_radiobutton2 = None # Gtk.RadioButton
self.livestream_radiobutton3 = None # Gtk.RadioButton
@ -23598,59 +23609,70 @@ class SystemPrefWin(GenericPrefWin):
)
checkbutton = self.add_checkbutton(grid,
_('Do not check/download any livestream [yt-dlp only]'),
self.app_obj.block_livestreams_flag,
True, # Can be toggled by user
0, 1, 1, 1,
)
checkbutton.connect(
'toggled',
self.on_block_livestreams_button_toggled,
)
checkbutton2 = self.add_checkbutton(grid,
_('Detect livestreams announced within this many days'),
self.app_obj.enable_livestreams_flag,
True, # Can be toggled by user
0, 1, 1, 1,
0, 2, 1, 1,
)
# (Signal connect appears below)
spinbutton = self.add_spinbutton(grid,
0, None, 1, self.app_obj.livestream_max_days,
1, 1, 1, 1,
1, 2, 1, 1,
)
if not self.app_obj.enable_livestreams_flag:
spinbutton.set_sensitive(False)
# (Signal connect appears below)
checkbutton2 = self.add_checkbutton(grid,
checkbutton3 = self.add_checkbutton(grid,
_('How often to check the status of livestreams (in minutes)'),
self.app_obj.scheduled_livestream_flag,
True, # Can be toggled by user
0, 2, 1, 1,
0, 3, 1, 1,
)
if not self.app_obj.enable_livestreams_flag:
checkbutton2.set_sensitive(False)
checkbutton3.set_sensitive(False)
# (Signal connect appears below)
spinbutton2 = self.add_spinbutton(grid,
1, None, 1, self.app_obj.scheduled_livestream_wait_mins,
1, 2, 1, 1,
1, 3, 1, 1,
)
if not self.app_obj.enable_livestreams_flag \
or not self.app_obj.scheduled_livestream_flag:
spinbutton2.set_sensitive(False)
# (Signal connect appears below)
checkbutton3 = self.add_checkbutton(grid,
checkbutton4 = self.add_checkbutton(grid,
_('Check more frequently when a livestream is due to start'),
self.app_obj.scheduled_livestream_extra_flag,
True, # Can be toggled by user
0, 3, grid_width, 1,
0, 4, grid_width, 1,
)
if not self.app_obj.enable_livestreams_flag \
or not self.app_obj.scheduled_livestream_flag:
checkbutton3.set_sensitive(False)
checkbutton3.connect(
checkbutton4.set_sensitive(False)
checkbutton4.connect(
'toggled',
self.on_extra_livestreams_button_toggled,
)
# (Signal connects from above)
checkbutton.connect(
checkbutton2.connect(
'toggled',
self.on_enable_livestreams_button_toggled,
checkbutton2,
checkbutton3,
checkbutton4,
spinbutton,
spinbutton2,
)
@ -23660,10 +23682,10 @@ class SystemPrefWin(GenericPrefWin):
self.on_livestream_max_days_spinbutton_changed,
)
checkbutton2.connect(
checkbutton3.connect(
'toggled',
self.on_scheduled_livestreams_button_toggled,
checkbutton3,
checkbutton4,
spinbutton2,
)
@ -23678,7 +23700,7 @@ class SystemPrefWin(GenericPrefWin):
'Broadcasting livestream preferences (compatible websites' \
+ ' only)',
) + '</u>',
0, 4, grid_width, 1,
0, 5, grid_width, 1,
)
self.add_label(grid,
@ -23686,11 +23708,6 @@ class SystemPrefWin(GenericPrefWin):
'These settings apply when downloading videos individually,' \
+ ' for example with a custom download',
) + '</i>',
0, 5, grid_width, 1,
)
self.livestream_label = self.add_label(grid,
'',
0, 6, grid_width, 1,
)
@ -23781,7 +23798,7 @@ class SystemPrefWin(GenericPrefWin):
)
# (More widgets)
checkbutton3 = self.add_checkbutton(grid,
checkbutton5 = self.add_checkbutton(grid,
_(
'Bypass usual limits on simultaneous downloads, so that' \
+ ' all livestreams can be downloaded',
@ -23790,7 +23807,7 @@ class SystemPrefWin(GenericPrefWin):
True, # Can be toggled by user
0, 9, grid_width, 1,
)
checkbutton3.connect(
checkbutton5.connect(
'toggled',
self.on_worker_bypass_button_toggled,
)
@ -23810,7 +23827,7 @@ class SystemPrefWin(GenericPrefWin):
self.on_livestream_timeout_spinbutton_changed,
)
checkbutton4 = self.add_checkbutton(grid,
checkbutton6 = self.add_checkbutton(grid,
_(
'When the livestream download is stopped manually, mark the' \
+ ' video as downloaded',
@ -23819,12 +23836,12 @@ class SystemPrefWin(GenericPrefWin):
True, # Can be toggled by user
0, 11, grid_width, 1,
)
checkbutton4.connect(
checkbutton6.connect(
'toggled',
self.on_livestream_stop_button_toggled,
)
checkbutton5 = self.add_checkbutton(grid,
checkbutton7 = self.add_checkbutton(grid,
_(
'Check a video before the livestream download (ensures' \
+ ' metadata is downloaded)',
@ -23833,7 +23850,7 @@ class SystemPrefWin(GenericPrefWin):
True, # Can be toggled by user
0, 12, grid_width, 1,
)
checkbutton5.connect(
checkbutton7.connect(
'toggled',
self.on_livestream_force_check_button_toggled,
)
@ -23856,13 +23873,6 @@ class SystemPrefWin(GenericPrefWin):
downloader = self.app_obj.get_downloader()
self.livestream_label.set_markup(
'<i>' + _(
'N.B. To prevent {0} from downloading livestreams at all,' \
+ ' use a custom download',
).format(downloader) + '</i>',
)
self.livestream_radiobutton.set_label(
downloader + ' (' + _('not recommended') + ')',
)
@ -26876,6 +26886,26 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_alt_bandwidth(int(spinbutton.get_value()))
def on_block_livestreams_button_toggled(self, checkbutton):
"""Called from callback in self.setup_operations_livestreams_tab().
Enables/disables checking/downloading livestreams by yt-dlp
Args:
checkbutton (Gtk.CheckButton): The widget clicked
"""
if checkbutton.get_active() \
and not self.app_obj.block_livestreams_flag:
self.app_obj.set_block_livestreams_flag(True)
elif not checkbutton.get_active() \
and self.app_obj.block_livestreams_flag:
self.app_obj.set_block_livestreams_flag(False)
def on_check_comment_fetch_button_toggled(self, checkbutton, checkbutton2):
"""Called from callback in self.setup_operations_comments_tab().

View File

@ -1622,6 +1622,9 @@ class TartubeApp(Gtk.Application):
# command line options manually
self.ffmpeg_simple_options_flag = True
# Flag set to True if checking/downloading livestreams should be
# blocked by yt-dlp (does not work with other downloaders)
self.block_livestreams_flag = False
# Flag set to True if Tartube should try to detect livestreams (on
# compatible websites only)
# This feature is only tested on YouTube. It might work on other
@ -2322,6 +2325,8 @@ class TartubeApp(Gtk.Application):
'live_from_start': False,
'wait_for_video_min': 0,
# Video Selection Options
'--playlist-items': True,
'-I': True, # Alias of --playlist-items
'--break-on-existing': False,
'--break-on-reject': False,
'--skip-playlist-after-errors': True,
@ -4524,9 +4529,10 @@ class TartubeApp(Gtk.Application):
if version < 2002015: # v2.2.015
self.load_config_import_scheduled(version, json_dict)
if version >= 2004085: # v2.4.085
self.block_livestreams_flag = json_dict['block_livestreams_flag']
if version >= 2000037: # v2.0.037
self.enable_livestreams_flag \
= json_dict['enable_livestreams_flag']
self.enable_livestreams_flag = json_dict['enable_livestreams_flag']
if version >= 2000047: # v2.0.047
self.livestream_max_days = json_dict['livestream_max_days']
self.livestream_use_colour_flag \
@ -5476,8 +5482,8 @@ class TartubeApp(Gtk.Application):
'auto_delete_options_flag': self.auto_delete_options_flag,
'simple_options_flag': self.simple_options_flag,
'enable_livestreams_flag': \
self.enable_livestreams_flag,
'block_livestreams_flag': self.block_livestreams_flag,
'enable_livestreams_flag': self.enable_livestreams_flag,
'livestream_max_days': self.livestream_max_days,
'livestream_use_colour_flag': self.livestream_use_colour_flag,
'livestream_simple_colour_flag': \
@ -7471,6 +7477,11 @@ class TartubeApp(Gtk.Application):
self.classic_dropzone_list = mod_list
if version < 2004084: # v2.4.084
# This version adds new options to options.OptionsManager
for options_obj in options_obj_list:
options_obj.options_dict['playlist_items'] = ''
# --- Do this last, or the call to .check_integrity_db() fails -------
# --------------------------------------------------------------------
@ -21778,36 +21789,7 @@ class TartubeApp(Gtk.Application):
"""
# Start the download operation
if not self.classic_custom_dl_flag:
self.download_manager_start('classic_real')
elif self.classic_custom_dl_obj.dl_by_video_flag:
# If the user has opted to download each video independently of its
# channel or playlist, then we have to do a simulated download
# first, in order to collect the URLs of each invidual video
# ('classic_sim')
# When that download operation has finished, we can do a (real)
# custom download for each video ('classic_custom')
self.download_manager_start(
'classic_sim',
False, # Not called by slow timer
[], # Download all URLs
self.classic_custom_dl_obj,
)
else:
# Otherwise, a full custom download can proceed immediately,
# without performing the simulated download first
self.download_manager_start(
'classic_custom',
False, # Not called by slow timer
[], # Download all URLs
self.classic_custom_dl_obj,
)
self.main_win_obj.classic_mode_tab_start_download()
def on_button_classic_ffmpeg(self, action, par):
@ -24850,6 +24832,14 @@ class TartubeApp(Gtk.Application):
self.bandwidth_default = value
def set_block_livestreams_flag(self, flag):
if not flag:
self.block_livestreams_flag = False
else:
self.block_livestreams_flag = True
def set_catalogue_draw_blocked_flag(self, flag):
if not flag:

View File

@ -703,11 +703,19 @@ class MainWin(Gtk.ApplicationWindow):
# Flag set to True when automatic copy/paste has been enabled (always
# disabled on startup)
self.classic_auto_copy_flag = False
# Flag set to True when one-click downloads have been enabled (always
# disabled on startup)
self.classic_one_click_dl_flag = False
# The last text that was copy/pasted from the clipboard. Storing it
# here prevents self.classic_mode_tab_timer_callback() from
# continually re-pasting the same text (for example, when the user
# manually empties the textview)
self.classic_auto_copy_text = None
# Temporary flag set to prevent a second call to
# self.classic_mode_tab_add_urls() before the first one has finished
self.classic_auto_copy_check_flag = False
# Flag set to True just before a call to
# self.classic_mode_tab_add_urls() so that it can't call itself
# IVs for clipboard monitoring, when required
self.classic_clipboard_timer_id = None
self.classic_clipboard_timer_time = 250
@ -3137,6 +3145,12 @@ class MainWin(Gtk.ApplicationWindow):
'paste-clipboard',
self.on_classic_textview_paste,
)
# (If the setting is enabled, start a download operation for any valid
# URL(s), or add the URL(s) to an existing download operation)
self.classic_textbuffer.connect(
'changed',
self.on_classic_textbuffer_changed,
)
# Third row - widgets to set the download destination and video/audio
# format. The user clicks the 'Add URLs' button to create dummy
@ -5986,30 +6000,6 @@ class MainWin(Gtk.ApplicationWindow):
# Separator
actions_submenu.append(Gtk.SeparatorMenuItem())
convert_text = None
if media_type == 'channel':
msg = _('_Convert to playlist')
elif media_type == 'playlist':
msg = _('_Convert to channel')
else:
msg = None
if msg:
convert_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
convert_menu_item.connect(
'activate',
self.on_video_index_convert_container,
media_data_obj,
)
actions_submenu.append(convert_menu_item)
if self.app_obj.current_manager_obj \
or unavailable_flag:
convert_menu_item.set_sensitive(False)
# Separator
actions_submenu.append(Gtk.SeparatorMenuItem())
if isinstance(media_data_obj, media.Folder):
hide_folder_menu_item = Gtk.MenuItem.new_with_mnemonic(
@ -6088,6 +6078,23 @@ class MainWin(Gtk.ApplicationWindow):
# Separator
actions_submenu.append(Gtk.SeparatorMenuItem())
if media_type == 'channel':
insert_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('_Insert videos...'),
)
insert_menu_item.connect(
'activate',
self.on_video_index_insert_videos,
media_data_obj,
)
actions_submenu.append(insert_menu_item)
if self.app_obj.current_manager_obj:
insert_menu_item.set_sensitive(False)
# Separator
actions_submenu.append(Gtk.SeparatorMenuItem())
if media_type == 'channel':
msg = _('_Export channel...')
elif media_type == 'playlist':
@ -6147,6 +6154,30 @@ class MainWin(Gtk.ApplicationWindow):
tidy_menu_item.set_sensitive(False)
actions_submenu.append(tidy_menu_item)
# Separator
actions_submenu.append(Gtk.SeparatorMenuItem())
convert_text = None
if media_type == 'channel':
msg = _('_Convert to playlist')
elif media_type == 'playlist':
msg = _('_Convert to channel')
else:
msg = None
if msg:
convert_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
convert_menu_item.connect(
'activate',
self.on_video_index_convert_container,
media_data_obj,
)
actions_submenu.append(convert_menu_item)
if self.app_obj.current_manager_obj \
or unavailable_flag:
convert_menu_item.set_sensitive(False)
classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('Add to C_lassic Mode tab'),
)
@ -8202,9 +8233,21 @@ class MainWin(Gtk.ApplicationWindow):
)
popup_menu.append(automatic_menu_item)
# One-click downloads
one_click_dl_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
_('E_nable one-click downloads'),
)
if self.classic_one_click_dl_flag:
one_click_dl_menu_item.set_active(True)
one_click_dl_menu_item.connect(
'toggled',
self.on_classic_menu_toggle_one_click_dl,
)
popup_menu.append(one_click_dl_menu_item)
# Remember undownloaded URLs
remember_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
_('_Remember URLs'),
_('_Remember un-downloaded URLs'),
)
if self.app_obj.classic_pending_flag:
remember_menu_item.set_active(True)
@ -12220,30 +12263,41 @@ class MainWin(Gtk.ApplicationWindow):
row_num = self.progress_list_row_dict[item_id]
# Prepare new values for Progress List IVs. Everything after this row
# must have its row number decremented by one
row_dict = {}
for this_item_id in self.progress_list_row_dict.keys():
this_row_num = self.progress_list_row_dict[this_item_id]
# Remove the row. Very rarely this generates a Python error (for
# unknown reasons)
try:
if this_row_num > row_num:
row_dict[this_item_id] = this_row_num - 1
elif this_row_num < row_num:
row_dict[this_item_id] = this_row_num
path = Gtk.TreePath(row_num)
tree_iter = self.progress_list_liststore.get_iter(path)
self.progress_list_liststore.remove(tree_iter)
row_count = self.progress_list_row_count - 1
# Prepare new values for Progress List IVs. Everything after this
# row must have its row number decremented by one
row_dict = {}
for this_item_id in self.progress_list_row_dict.keys():
this_row_num = self.progress_list_row_dict[this_item_id]
# Remove the row
path = Gtk.TreePath(row_num)
tree_iter = self.progress_list_liststore.get_iter(path)
self.progress_list_liststore.remove(tree_iter)
if this_row_num > row_num:
row_dict[this_item_id] = this_row_num - 1
elif this_row_num < row_num:
row_dict[this_item_id] = this_row_num
# Apply updated IVs
self.progress_list_row_dict = row_dict.copy()
if item_id in self.progress_list_temp_dict:
del self.progress_list_temp_dict[item_id]
if item_id in self.progress_list_finish_dict:
del self.progress_list_finish_dict[item_id]
row_count = self.progress_list_row_count - 1
# Apply updated IVs
self.progress_list_row_dict = row_dict.copy()
if item_id in self.progress_list_temp_dict:
del self.progress_list_temp_dict[item_id]
if item_id in self.progress_list_finish_dict:
del self.progress_list_finish_dict[item_id]
except:
return self.app_obj.system_error(
999,
'Cannot remove row in Progress List (row does not exist)',
)
def progress_list_update_video_name(self, download_item_obj, video_obj):
@ -12891,9 +12945,17 @@ class MainWin(Gtk.ApplicationWindow):
"""Called by mainapp.TartubeApp.on_button_classic_add_urls().
Also called by self.on_classic_textbuffer_changed().
In the Classic Mode tab, transfers URLs from the textview into the
Classic Progress List (a treeview), creating a new dummy media.Video
object for each URL, and updating IVs.
Return values:
Returns a list of URLs added to the Classic Progress List (which
may be empty)
"""
# Get the specified download destination
@ -12946,8 +13008,8 @@ class MainWin(Gtk.ApplicationWindow):
# Extract a list of URLs from the textview
url_string = self.classic_textbuffer.get_text(
self.classic_textbuffer.get_start_iter(),
self.classic_textbuffer.get_end_iter(),
self.classic_textbuffer.get_iter_at_mark(self.classic_mark_start),
self.classic_textbuffer.get_iter_at_mark(self.classic_mark_end),
False,
)
@ -13000,11 +13062,19 @@ class MainWin(Gtk.ApplicationWindow):
# Unless the flag is set, any invalid links remain in the textview (but
# in all cases, all valid links are removed from it)
# When this function is called by self.on_classic_textbuffer_changed(),
# Gtk generates a warning when we try to .set_text()
# The only way I can find to get around this is to replace the old
# textbuffer with a new one
self.classic_mode_tab_replace_textbuffer()
if not self.app_obj.classic_duplicate_remove_flag:
self.classic_textbuffer.set_text(invalid_url_string)
else:
self.classic_textbuffer.set_text('')
return mod_list
def classic_mode_tab_insert_url(self, url, options_obj):
@ -13062,6 +13132,32 @@ class MainWin(Gtk.ApplicationWindow):
return True
def classic_mode_tab_replace_textbuffer(self):
"""Called by self.classic_mode_tab_add_urls(), just before replacing
the contents of the Gtk.TextView at the top of the tab.
When that function is called by self.on_classic_textbuffer_changed(),
Gtk generates a warning when we try to .set_text().
The only way I can find to get around this is to replace the old
textbuffer with a new one
"""
self.classic_textbuffer = Gtk.TextBuffer()
self.classic_textview.set_buffer(self.classic_textbuffer)
self.classic_mark_start = self.classic_textbuffer.create_mark(
'mark_start',
self.classic_textbuffer.get_start_iter(),
True, # Left gravity
)
self.classic_mark_end = self.classic_textbuffer.create_mark(
'mark_end',
self.classic_textbuffer.get_end_iter(),
False, # Not left gravity
)
def classic_mode_tab_create_dummy_video(self, url, dest_dir, \
format_str=None):
@ -13146,8 +13242,8 @@ class MainWin(Gtk.ApplicationWindow):
# Extract a list of URLs from the textview
url_string = self.classic_textbuffer.get_text(
self.classic_textbuffer.get_start_iter(),
self.classic_textbuffer.get_end_iter(),
self.classic_textbuffer.get_iter_at_mark(self.classic_mark_start),
self.classic_textbuffer.get_iter_at_mark(self.classic_mark_end),
False,
)
@ -13189,9 +13285,7 @@ class MainWin(Gtk.ApplicationWindow):
"""
self.classic_textbuffer.set_text(
'\n'.join(url_list),
)
self.classic_textbuffer.set_text('\n'.join(url_list))
def classic_mode_tab_find_row_iter(self, dbid):
@ -13404,6 +13498,52 @@ class MainWin(Gtk.ApplicationWindow):
return 1
def classic_mode_tab_start_download(self):
"""Called by mainapp.TartubeApp.on_button_classic_download() and
self.on_classic_textbuffer_changed().
Starts a download operation for the URLs added to the Classic Progress
List.
"""
if self.app_obj.download_manager_obj:
# Download already in progress
return
elif not self.app_obj.classic_custom_dl_flag:
# Start an (ordinary) download operation
self.app_obj.download_manager_start('classic_real')
elif self.app_obj.classic_custom_dl_obj.dl_by_video_flag:
# If the user has opted to download each video independently of its
# channel or playlist, then we have to do a simulated download
# first, in order to collect the URLs of each invidual video
# ('classic_sim')
# When that download operation has finished, we can do a (real)
# custom download for each video ('classic_custom')
self.app_obj.download_manager_start(
'classic_sim',
False, # Not called by slow timer
[], # Download all URLs
self.app_obj.classic_custom_dl_obj,
)
else:
# Otherwise, a full custom download can proceed immediately,
# without performing the simulated download first
self.app_obj.download_manager_start(
'classic_custom',
False, # Not called by slow timer
[], # Download all URLs
self.app_obj.classic_custom_dl_obj,
)
# (Drag and Drop tab)
@ -15362,6 +15502,80 @@ class MainWin(Gtk.ApplicationWindow):
self.app_obj.mark_folder_hidden(media_data_obj, True)
def on_video_index_insert_videos(self, menu_item, media_data_obj):
"""Called from a callback in self.video_index_popup_menu().
Creates a dialogue window to insert one or more videos into a channel.
This is useful when the new videos are unlisted. Videos can be added to
a folder in the usual way.
Args:
menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Channel, media.Playlist):
The clicked media data object
"""
# (Code adapated from mainapp.TartubeApp.on_menu_add_video() )
dialogue_win = InsertVideoDialogue(self, media_data_obj)
response = dialogue_win.run()
# Retrieve user choices from the dialogue window...
text = dialogue_win.textbuffer.get_text(
dialogue_win.textbuffer.get_start_iter(),
dialogue_win.textbuffer.get_end_iter(),
False,
)
# ...and halt the timer, if running
if dialogue_win.clipboard_timer_id:
GObject.source_remove(dialogue_win.clipboard_timer_id)
# ...before destroying the dialogue window
dialogue_win.destroy()
if response == Gtk.ResponseType.OK:
# Split text into a list of lines and filter out invalid URLs
video_list = []
duplicate_list = []
for line in text.split('\n'):
# Remove leading/trailing whitespace
line = utils.strip_whitespace(line)
# Perform checks on the URL. If it passes, remove leading/
# trailing whitespace
if utils.check_url(line):
video_list.append(utils.strip_whitespace(line))
# Check everything in the list against other media.Video objects
# with the same parent folder
for line in video_list:
if media_data_obj.check_duplicate_video(line):
duplicate_list.append(line)
else:
self.app_obj.add_video(media_data_obj, line)
# In the Video Index, select the parent media data object, which
# updates both the Video Index and the Video Catalogue
self.video_index_select_row(media_data_obj)
# If any duplicates were found, inform the user
if duplicate_list:
dialogue_win = mainwin.DuplicateVideoDialogue(
self,
duplicate_list,
)
dialogue_win.run()
dialogue_win.destroy()
def on_video_index_mark_archived(self, menu_item, media_data_obj,
only_child_videos_flag):
@ -19403,32 +19617,6 @@ class MainWin(Gtk.ApplicationWindow):
data.set_text(string, -1)
def on_classic_textview_paste(self, textview):
"""Called from callback in self.setup_classic_mode_tab().
When the user copy-pastes URLs into the textview, insert an initial
newline character, so they don't have to continuously do that
themselves.
Args:
textview (Gtk.TextView): The clicked widget
"""
text = self.classic_textbuffer.get_text(
self.classic_textbuffer.get_start_iter(),
self.classic_textbuffer.get_end_iter(),
# Don't include hidden characters
False,
)
if not (re.search('^\S*$', text)) \
and not (re.search('\n+\s*$', text)):
self.classic_textbuffer.set_text(text + '\n')
def on_classic_dest_dir_combo_changed(self, combo):
"""Called from callback in self.setup_classic_mode_tab().
@ -19726,6 +19914,25 @@ class MainWin(Gtk.ApplicationWindow):
)
def on_classic_menu_toggle_one_click_dl(self, menu_item):
"""Called from a callback in self.classic_popup_menu().
Toggles the one-click download button in the Classic Mode tab.
Args:
menu_item (Gtk.MenuItem): The clicked menu item
"""
# Update IVs
if not self.classic_one_click_dl_flag:
self.classic_one_click_dl_flag = True
else:
self.classic_one_click_dl_flag = False
def on_classic_menu_toggle_remember_urls(self, menu_item):
"""Called from a callback in self.classic_popup_menu().
@ -19970,6 +20177,70 @@ class MainWin(Gtk.ApplicationWindow):
self.classic_progress_list_popup_menu(event, path)
def on_classic_textbuffer_changed(self, textbuffer):
"""Called from callback in self.setup_classic_mode_tab().
If the setting is enabled, start a download operation for any valid
URL(s), or add the URL(s) to an existing download operation.
Args:
textbuffer (Gtk.TextBuffer): The textbuffer for the modified
Gtk.TextView
"""
if self.classic_one_click_dl_flag \
and not self.classic_auto_copy_check_flag:
# (A second signal is received by this function, when the call to
# self.classic_mode_tab_add_urls() resets the textview. Setting
# this flag prevents a second call to that function, before the
# first one has finished)
self.classic_auto_copy_check_flag = True
url_list = self.classic_mode_tab_add_urls()
self.classic_auto_copy_check_flag = False
if url_list and not self.app_obj.download_manager_obj:
self.classic_mode_tab_start_download()
def on_classic_textview_paste(self, textview):
"""Called from callback in self.setup_classic_mode_tab().
When the user copy-pastes URLs into the textview, insert an initial
newline character, so they don't have to continuously do that
themselves.
Args:
textview (Gtk.TextView): The clicked widget
"""
# (Don't bother, if the URLs are going to be downloaded immediately)
if not self.classic_one_click_dl_flag:
text = self.classic_textbuffer.get_text(
self.classic_textbuffer.get_iter_at_mark(
self.classic_mark_start,
),
self.classic_textbuffer.get_iter_at_mark(
self.classic_mark_end,
),
# Don't include hidden characters
False,
)
# (Don't bother inserting the newline if the URLs are going to be
# sent straight to the download manager)
if not (re.search('^\S*$', text)) \
and not (re.search('\n+\s*$', text)):
self.classic_textbuffer.set_text(text + '\n')
def on_bandwidth_spinbutton_changed(self, spinbutton):
"""Called from callback in self.setup_progress_tab().
@ -31401,6 +31672,274 @@ class ImportDialogue(Gtk.Dialog):
mini_dict['import_flag'] = False
class InsertVideoDialogue(Gtk.Dialog):
"""Called by mainwin.MainWin.on_video_index_insert_videos().
Python class handling a dialogue window that inserts invidual video(s)
into a channel.
Args:
main_win_obj (mainwin.MainWin): The parent main window
parent_obj (media.Channel, media.Playlist or media.Folder): Name of
the container into which videos are to be inserted. At the moment,
no calling code specifies a playlist or folder, but such a call is
nevertheless permitted
"""
# Standard class methods
def __init__(self, main_win_obj, parent_obj):
# IV list - class objects
# -----------------------
# Tartube's main window
self.main_win_obj = main_win_obj
# IV list - Gtk widgets
# ---------------------
self.textbuffer = None # Gtk.TextBuffer
self.mark_start = None # Gtk.TextMark
self.mark_end = None # Gtk.TextMark
self.checkbutton = None # Gtk.CheckButton
# IV list - other
# ---------------
# The media.Channel or media.Playlist into which videos are to be
# inserted
self.parent_obj = parent_obj
# Set up IVs for clipboard monitoring, if required
self.clipboard_timer_id = None
self.clipboard_timer_time = 250
# Code
# ----
Gtk.Dialog.__init__(
self,
_('Insert videos'),
main_win_obj,
Gtk.DialogFlags.DESTROY_WITH_PARENT,
(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK,
)
)
self.set_modal(False)
# Set up the dialogue window
box = self.get_content_area()
grid = Gtk.Grid()
box.add(grid)
grid.set_border_width(main_win_obj.spacing_size)
grid.set_row_spacing(main_win_obj.spacing_size)
label = Gtk.Label(_('Copy and paste the links to one or more videos'))
grid.attach(label, 0, 0, 1, 1)
if main_win_obj.app_obj.operation_convert_mode == 'channel':
text = _(
'Links containing multiple videos will be converted to' \
+ ' a channel',
)
elif main_win_obj.app_obj.operation_convert_mode == 'playlist':
text = _(
'Links containing multiple videos will be converted to a' \
+ ' playlist',
)
elif main_win_obj.app_obj.operation_convert_mode == 'multi':
text = _(
'Links containing multiple videos will be downloaded' \
+ ' separately',
)
elif main_win_obj.app_obj.operation_convert_mode == 'disable':
text = _(
'Links containing multiple videos will not be downloaded'
+ ' at all',
)
label = Gtk.Label()
label.set_markup('<i>' + text + '</i>')
grid.attach(label, 0, 1, 1, 1)
frame = Gtk.Frame()
grid.attach(frame, 0, 2, 1, 1)
scrolledwindow = Gtk.ScrolledWindow()
frame.add(scrolledwindow)
# (Set enough vertical room for at several URLs)
scrolledwindow.set_size_request(-1, 150)
textview = Gtk.TextView()
scrolledwindow.add(textview)
textview.set_hexpand(True)
self.textbuffer = textview.get_buffer()
# Some callbacks will complain about invalid iterators, if we try to
# use Gtk.TextIters, so use Gtk.TextMarks instead
self.mark_start = self.textbuffer.create_mark(
'mark_start',
self.textbuffer.get_start_iter(),
True, # Left gravity
)
self.mark_end = self.textbuffer.create_mark(
'mark_end',
self.textbuffer.get_end_iter(),
False, # Not left gravity
)
# Drag-and-drop onto the textview inevitably inserts a URL in the
# middle of another URL. No way to prevent that, but we can disable
# drag-and-drop in the textview altogether, and instead handle it
# from the dialogue window itself
# textview.drag_dest_unset()
self.connect('drag-data-received', self.on_window_drag_data_received)
self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self.drag_dest_set_target_list(None)
self.drag_dest_add_text_targets()
# Separator
grid.attach(Gtk.HSeparator(), 0, 3, 1, 1)
# Display the parent channel/playlist in a combo (so the layout of this
# window is the same as that for AddVideoDialogue)
label2 = Gtk.Label()
grid.attach(label2, 0, 4, 1, 1)
if isinstance(parent_obj, media.Channel):
label2.set_text(_('Insert the videos into this channel:'))
pixbuf = main_win_obj.pixbuf_dict['channel_small']
elif isinstance(parent_obj, media.Playlist):
label2.set_text(_('Insert the videos into this playlist:'))
pixbuf = main_win_obj.pixbuf_dict['playlist_small']
else:
label2.set_text(_('Insert the videos into this folder:'))
pixbuf = main_win_obj.pixbuf_dict['folder_small']
listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
listmodel.append( [pixbuf, ' ' + self.parent_obj.name] )
combo = Gtk.ComboBox.new_with_model(listmodel)
grid.attach(combo, 0, 5, 1, 1)
combo.set_hexpand(True)
renderer_pixbuf = Gtk.CellRendererPixbuf()
combo.pack_start(renderer_pixbuf, False)
combo.add_attribute(renderer_pixbuf, 'pixbuf', 0)
renderer_text = Gtk.CellRendererText()
combo.pack_start(renderer_text, False)
combo.add_attribute(renderer_text, 'text', 1)
combo.set_active(0)
# combo.connect('changed', self.on_combo_changed)
# Separator
grid.attach(Gtk.HSeparator(), 0, 6, 1, 1)
self.checkbutton = Gtk.CheckButton()
grid.attach(self.checkbutton, 0, 7, 1, 1)
self.checkbutton.set_label(_('Enable automatic copy/paste'))
self.checkbutton.connect('toggled', self.on_checkbutton_toggled)
# Paste in the contents of the clipboard (if it contains valid URLs)
if main_win_obj.app_obj.dialogue_copy_clipboard_flag:
utils.add_links_to_textview_from_clipboard(
main_win_obj.app_obj,
self.textbuffer,
self.mark_start,
self.mark_end,
)
# Display the dialogue window
self.show_all()
# Callback class methods
def on_checkbutton_toggled(self, checkbutton):
"""Called from a callback in self.__init__().
Enables/disables clipboard monitoring.
Args:
checkbutton (Gtk.CheckButton): The clicked widget
"""
if not checkbutton.get_active() \
and self.clipboard_timer_id is not None:
# Stop the timer
GObject.source_remove(self.clipboard_timer_id)
self.clipboard_timer_id = None
elif checkbutton.get_active() and self.clipboard_timer_id is None:
# Start the timer
self.clipboard_timer_id = GObject.timeout_add(
self.clipboard_timer_time,
self.clipboard_timer_callback,
)
def on_window_drag_data_received(self, window, context, x, y, data, info,
time):
"""Called a from callback in self.__init__().
Handles drag-and-drop anywhere in the dialogue window.
"""
utils.add_links_to_textview_from_clipboard(
self.main_win_obj.app_obj,
self.textbuffer,
self.mark_start,
self.mark_end,
# Specify the drag-and-drop text, so the called function uses that,
# rather than the clipboard text
data.get_text(),
)
def clipboard_timer_callback(self):
"""Called from a callback in self.on_checkbutton_toggled().
Periodically checks the system's clipboard, and adds any new URLs to
the dialogue window's textview.
"""
utils.add_links_to_textview_from_clipboard(
self.main_win_obj.app_obj,
self.textbuffer,
self.mark_start,
self.mark_end,
)
# Return 1 to keep the timer going
return 1
class MountDriveDialogue(Gtk.Dialog):
"""Called by mainapp.TartubeApp.start().

View File

@ -156,6 +156,71 @@ class GenericContainer(GenericMedia):
# Public class methods
def check_duplicate_video(self, source):
"""Can be called by anything.
Before adding a video to a parent channel/playlist/folder, this
function can be called to check for videos with a duplicate URL.
Args:
source (str): The video URL to check
Returns:
True if any of the child media.Video objects in this folder have
the same source URL; False otherwise
"""
for child_obj in self.child_list:
if isinstance(child_obj, Video) \
and child_obj.source is not None \
and child_obj.source == source:
# Duplicate found
return True
# No duplicate found
return False
def check_duplicate_video_by_path(self, app_obj, path):
"""Can be called by anything.
A modified version of self.check_duplicate_video(), which checks for
media.Video objects with duplicate paths, instead of dupliate URLs.
Args:
app_obj (mainapp.TartubeApp): The main application
path (str): The full file path to check
Returns:
True if any of the child media.Video objects in this folder have
the same source URL; False otherwise
"""
for child_obj in self.child_list:
if isinstance(child_obj, Video) \
and child_obj.file_name is not None:
child_path = child_obj.get_actual_path(app_obj)
if child_path is not None and child_path == path:
# Duplicate found
return True
# No duplicate found
return False
def compile_all_containers(self, container_list):
"""Can be called by anything. Subsequently called by this function
@ -3574,6 +3639,12 @@ class Channel(GenericRemoteContainer):
# def add_child(): # Inherited from GenericRemoteContainer
# def check_duplicate_video(): # Inherited from GenericContainer
# def check_duplicate_video_by_path(): # Inherited from GenericContainer
# def del_child(): # Inherited from GenericContainer
@ -3919,6 +3990,12 @@ class Playlist(GenericRemoteContainer):
# def add_child(): # Inherited from GenericRemoteContainer
# def check_duplicate_video(): # Inherited from GenericContainer
# def check_duplicate_video_by_path(): # Inherited from GenericContainer
# def del_child(): # Inherited from GenericContainer
@ -4312,72 +4389,10 @@ class Folder(GenericContainer):
self.vid_count += 1
def check_duplicate_video(self, source):
"""Called by mainapp.TartubeApp.on_menu_add_video() and
mainwin.MainWin.on_window_drag_data_received().
When the user adds new videos using the 'Add Videos' dialogue window,
the calling function calls this function to check that the folder
doesn't contain a duplicate video (i.e., one whose source URL is the
same).
Args:
source (str): The video URL to check
Returns:
True if any of the child media.Video objects in this folder have
the same source URL; False otherwise
"""
for child_obj in self.child_list:
if isinstance(child_obj, Video) \
and child_obj.source is not None \
and child_obj.source == source:
# Duplicate found
return True
# No duplicate found
return False
# def check_duplicate_video(): # Inherited from GenericContainer
def check_duplicate_video_by_path(self, app_obj, path):
"""Called by mainwin.MainWin.on_window_drag_data_received().
A modified version of self.check_duplicate_video(), which checks for
media.Video objects with duplicate paths, instead of dupliate URLs.
Args:
app_obj (mainapp.TartubeApp): The main application
path (str): The full file path to check
Returns:
True if any of the child media.Video objects in this folder have
the same source URL; False otherwise
"""
for child_obj in self.child_list:
if isinstance(child_obj, Video) \
and child_obj.file_name is not None:
child_path = child_obj.get_actual_path(app_obj)
if child_path is not None and child_path == path:
# Duplicate found
return True
# No duplicate found
return False
# def check_duplicate_video_by_path(): # Inherited from GenericContainer
# def del_child(): # Inherited from GenericContainer

View File

@ -108,6 +108,12 @@ class OptionsManager(object):
playlist_end (int): Playlist index to stop downloading
playlist_items (str): Comma-separated playlist index of the videos to
download, in the form '[START]:[STOP][:STEP]' or 'START-STOP'. Use
negative indices to count from the right and negative STEP to
download in reverse order, e.g. on a playhlist of 15 videos,
'1:3,7,-5::2' downloads the videos at index '1,2,3,7,11,13,15'
max_downloads (int): Maximum number of video files to download from the
given playlist
@ -736,6 +742,7 @@ class OptionsManager(object):
# VIDEO SELECTION
'playlist_start': 1,
'playlist_end': 0,
'playlist_items': '',
'max_downloads': 0,
'min_filesize': 0,
'max_filesize': 0,
@ -1042,6 +1049,8 @@ class OptionsParser(object):
OptionHolder('playlist_start', '--playlist-start', 1),
# --playlist-end NUMBER
OptionHolder('playlist_end', '--playlist-end', 0),
# --playlist-items ITEM_SPEC
OptionHolder('playlist_items', '--playlist-items', ''),
# --max-downloads NUMBER
OptionHolder('max_downloads', '--max-downloads', 0),
# --min-filesize SIZE
@ -1450,8 +1459,20 @@ class OptionsParser(object):
options_list.append(option_holder_obj.switch)
options_list.append(utils.to_string(value))
elif option_holder_obj.name == 'match_filter' \
or option_holder_obj.name == 'external_arg_string' \
elif option_holder_obj.name == 'match_filter':
value = utils.to_string(copy_dict[option_holder_obj.name])
if self.app_obj.block_livestreams_flag:
if value == '':
value = '!is_live'
else:
value += ' \& !is_live'
if value != '':
options_list.append(option_holder_obj.switch)
options_list.append(value)
elif option_holder_obj.name == 'external_arg_string' \
or option_holder_obj.name == 'pp_args':
value = copy_dict[option_holder_obj.name]
if value != '':

File diff suppressed because it is too large Load Diff

View File

@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
__version__ = '2.4.077'
__date__ = '8 Jun 2022'
__version__ = '2.4.093'
__date__ = '31 Jul 2022'
__copyright__ = 'Copyright \xa9 2019-2022 A S Lewis'
__license__ = """
Copyright \xa9 2019-2022 A S Lewis.

View File

@ -647,6 +647,9 @@ class SetupWizWin(GenericWizWin):
# Flag set to True, once the 'More options' button has been clicked,
# so that it is never visible again
self.more_options_flag = False
# Flag set to True after the user has tried install FFmpeg at least
# once (even if the attempt failed)
self.try_install_ffmpeg_flag = False
# Standard length of text in the wizard window
self.text_len = 60
@ -1427,11 +1430,17 @@ class SetupWizWin(GenericWizWin):
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Estimated install size') + ': <b>1.5 GB</b></span>',
+ _('Download size') + ': <b>0.3 GB</b> - ' \
+ _('Install size') + ': <b>1.5 GB</b></span>',
0, (5 + extra_rows), grid_width, 1,
)
self.ffmpeg_button = Gtk.Button(_('Install FFmpeg'))
if not self.try_install_ffmpeg_flag:
msg = _('Install FFmpeg')
else:
msg = _('Reinstall FFmpeg')
self.ffmpeg_button = Gtk.Button(msg)
self.inner_grid.attach(self.ffmpeg_button, 1, (6 + extra_rows), 1, 1)
self.ffmpeg_button.set_hexpand(False)
# (Signal connect appears below)
@ -1782,10 +1791,13 @@ class SetupWizWin(GenericWizWin):
msg,
)
self.ffmpeg_button.set_label(_('Reinstall FFmpeg'))
self.ffmpeg_button.set_sensitive(True)
self.next_button.set_sensitive(True)
self.prev_button.set_sensitive(True)
self.try_install_ffmpeg_flag = True
def refresh_update_combo(self):