Update to v2.4.093
This commit is contained in:
parent
2ae0c8a122
commit
f1c666c47e
35
CHANGES
35
CHANGES
@ -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)
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
|
14
README.rst
14
README.rst
@ -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
|
||||
|
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
9614
locale/nl_NL/LC_MESSAGES/messages.pot
Normal file
9614
locale/nl_NL/LC_MESSAGES/messages.pot
Normal file
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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 |
2
setup.py
2
setup.py
@ -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',
|
||||
|
@ -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().
|
||||
|
@ -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:
|
||||
|
@ -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().
|
||||
|
143
tartube/media.py
143
tartube/media.py
@ -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
|
||||
|
@ -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
@ -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.
|
||||
|
@ -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):
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user