Updated to v1.3.077

This commit is contained in:
A S Lewis 2020-01-26 16:26:57 +00:00
parent 3755621e6e
commit 08723760c1
16 changed files with 2307 additions and 739 deletions

67
CHANGES
View File

@ -1,3 +1,70 @@
v1.3.077 (26 Jan 2019)
-------------------------------------------------------------------------------
MAJOR NEW FEATURES
- Drag and drop (for example, from a web browser into Tartube's main window)
is now fully working on Linux/BSD. On MS Windows, drag and drop does not
work at all for any Gtk application. It is unlikely that the Tartube
authors can do anything about this (Git #35)
- The 'Add new video(s)' dialogue window can now handle URLs representing
channels and playlists, as well as URLs representing individual videos.
During a download operation, if Tartube is expecting an individual video
but receives a channel/playlist, it will automatically create a new
channel, and download videos into that channel. You can change this default
behaviour, if you want (Edit > System preferences... > URL flexibility
preferences)
- To change the name of the new channel/playlist, right-click it and select
'Filesystem > Rename default location...'
- If Tartube creates a channel, which should really be a playlist, then you
can now convert one to the other. Right-click a channel and select
'Channel actions > Convert to playlist'. Right-click a playlist and select
'Playlist actions > Convert to channel'
- In the download options windows, it's now very easy to tell Tartube to
convert videos to sound files. Open the window by clicking 'Edit >
General download options...', click the 'Hide advanced download options'
button if necessary, click the 'Sound only' tab, select your preferences,
and apply them by clicking the OK button at the bottom of the window
- You can now see the download options applied to a video, channel, playlist
or folder without having to download anything. Right-click a video/channel/
playlist/folder and select 'Downloads > Show system command'
- During a download operation, the system commands used are now visible (by
default) in the Output Tab. The system command can also be displayed in the
terminal, if required; this is disabled by default
MINOR NEW FEATURES
- In the Output Tab, the summary page is now hidden by default. To make it
visible, click 'Edit > System Preferences... > Output >
Show a summary of active threads' and then restart Tartube
- In the Errors/Warnings Tab, added checkbuttons to filter out errors and/or
warning messages, if required (Git #50)
- In the Progress tab, in the top half of the window, you can now right-click
an unnamed video to open it in your web browser. This will be useful in
identifying videos that did not download, and whose name is unknown to
Tartube (Git #51)
- Columns in the Progress tab have been rearranged a little, so that the
user can more easily see how quickly the download is progressing, when
Tartube's main window is small
MAJOR FIXES
- Fixed multiple issues with Tartube, when running under Python 3.8
- Replaced all remaining references to the Python os.rename() function, which
can cause crashes on some filesystems (Git #34)
- Fixed crashes caused by the new YouTube error messages (January 2020), which
some versions of youtube-dl cannot handle correctly
- Fixed issues with the default location for videos, again. Fixed an issue
with adding folders inside the currently selected folder (Git #36, #46)
MINOR FIXES
- Fixed various Gtk warning messages, visible only on some systems
- Videos whose name contains an ampersand (&) character could not be opened by
clicking the 'Media player' label in the Video Catalogue. Fixed
- The properties windows for videos, channels and playlists showed a folder
icon, instead of a video/channel/playlist icon. Fixed
- The popup menu in the Progress tab, in the top half of the tab, did not work
as intended during a download operation, and again after a download
operation. Fixed both sets of issues
- Coloured text was not displayed in the Output Tab correctly. Fixed
v1.3.048 (23 Jan 2019) v1.3.048 (23 Jan 2019)
------------------------------------------------------------------------------- -------------------------------------------------------------------------------

View File

@ -15,11 +15,9 @@ Works with YouTube, BitChute, and hundreds of other websites
* `5 Installation`_ * `5 Installation`_
* `6 Getting started`_ * `6 Getting started`_
* `7. Frequently-Asked Questions`_ * `7. Frequently-Asked Questions`_
* `8. Future plans`_ * `8. Contributing`_
* `9. Known issues`_ * `9. Authors`_
* `10. Contributing`_ * `10. License`_
* `11. Authors`_
* `12. License`_
1 Introduction 1 Introduction
============== ==============
@ -79,11 +77,11 @@ Problems can be reported at `our GitHub page <https://github.com/axcore/tartube/
4 Downloads 4 Downloads
=========== ===========
Latest version: **v1.3.048 (23 Jan 2019)** Latest version: **v1.3.077 (26 Jan 2019)**
- `MS Windows (32-bit) installer <https://sourceforge.net/projects/tartube/files/v1.3.048/install-tartube-1.3.048-32bit.exe/download>`__ from Sourceforge - `MS Windows (32-bit) installer <https://sourceforge.net/projects/tartube/files/v1.3.077/install-tartube-1.3.077-32bit.exe/download>`__ from Sourceforge
- `MS Windows (64-bit) installer <https://sourceforge.net/projects/tartube/files/v1.3.048/install-tartube-1.3.048-64bit.exe/download>`__ from Sourceforge - `MS Windows (64-bit) installer <https://sourceforge.net/projects/tartube/files/v1.3.077/install-tartube-1.3.077-64bit.exe/download>`__ from Sourceforge
- `Source code <https://sourceforge.net/projects/tartube/files/v1.3.048/tartube_v1.3.048.tar.gz/download>`__ from Sourceforge - `Source code <https://sourceforge.net/projects/tartube/files/v1.3.077/tartube_v1.3.077.tar.gz/download>`__ from Sourceforge
- `Source code <https://github.com/axcore/tartube>`__ and `support <https://github.com/axcore/tartube/issues>`__ from GitHub - `Source code <https://github.com/axcore/tartube>`__ and `support <https://github.com/axcore/tartube/issues>`__ from GitHub
5 Installation 5 Installation
@ -270,12 +268,12 @@ Videos saved to the **Temporary Videos** folder are deleted when **Tartube** shu
6.6 Adding videos 6.6 Adding videos
----------------- -----------------
You can add individual videos by clicking the **'Videos'** button near the top of the window. A popup window will appear. You can add individual videos by clicking the **'Videos'** button near the top of the window. A dialogue window will appear.
.. image:: screenshots/example4.png .. image:: screenshots/example4.png
:alt: Adding videos :alt: Adding videos
Copy and paste the video's URL into the popup window. You can copy and paste as many URLs as you like. Copy and paste the video's URL into the dialogue window. You can copy and paste as many URLs as you like.
When you're finished, click the **OK** button. When you're finished, click the **OK** button.
@ -294,9 +292,29 @@ You can also add a whole channel by clicking the **'Channel'** button or a whole
.. image:: screenshots/example6.png .. image:: screenshots/example6.png
:alt: Adding a channel :alt: Adding a channel
Copy and paste the channel's URL into the popup window. You should also give the channel a name. The channel's name is usually the name used on the website (but you can choose any name you like). Copy and paste the channel's URL into the dialogue window. You should also give the channel a name. The channel's name is usually the name used on the website (but you can choose any name you like).
6.8 Adding folders 6.8 Adding videos, channels and playlists together
--------------------------------------------------
When adding a long list of URLs, containing a mixture of channels, playlists and individual videos, it's quicker to add them all at the same time. Click the **'Videos'** button near the top of the window, and paste all the links into the dialogue window.
**Tartube** doesn't know anything about these links until you actually download them (or check them). If it's expecting an individual video, but receives a channel or a playlist, **Tartube** will the handle the conversion for you.
By default, **Tartube** converts a link into a channel, when necessary. You can change this behaviour, if you want to.
- In **Tartube**'s main window, click **Edit > System preferences... > Operations**
- Select one of the buttons listed under **URL flexibility preferences**
Unfortunately, there is no way for **Tartube** to distinguish a channel from a playlist. Most video websites don't supply that information.
If your list of URLs contains a mixture of channels and playlists, you can convert one to the other after the download has finished.
- In **Tartube**'s main window, right-click a channel, and select **Channel actions > Convert to playlist**
- Alternatively, right-click a playlist, and select **Channel actions > Convert to channel**
- After converting, you can set a name for the new channel/playlist by right-clicking it, and selecting **Filesystem > Rename default location...**
6.9 Adding folders
------------------ ------------------
The left-hand side of the window will quickly still filling up. It's a good idea to create some folders, and to store your channels/playlists inside those folders. The left-hand side of the window will quickly still filling up. It's a good idea to create some folders, and to store your channels/playlists inside those folders.
@ -311,7 +329,7 @@ Then repeat that process to create a folder called **Music**. You can then drag-
.. image:: screenshots/example8.png .. image:: screenshots/example8.png
:alt: A channel inside a folder :alt: A channel inside a folder
6.9 Things you can do 6.10 Things you can do
---------------------- ----------------------
Once you've finished adding videos, channels, playlists and folders, there are basically four things **Tartube** can do: Once you've finished adding videos, channels, playlists and folders, there are basically four things **Tartube** can do:
@ -331,7 +349,7 @@ To **Check** or **Download** videos, channels and playlists, use the buttons nea
**Protip:** Do a **'Check'** operation before you do **'Refresh'** operation **Protip:** Do a **'Check'** operation before you do **'Refresh'** operation
6.10 General download options 6.11 General download options
----------------------------- -----------------------------
**youtube-dl** offers a large number of download options. This is how to set them. **youtube-dl** offers a large number of download options. This is how to set them.
@ -343,7 +361,7 @@ To **Check** or **Download** videos, channels and playlists, use the buttons nea
A new window opens. Any changes you make in this window aren't actually applied until you click the **'Apply'** or **'OK'** buttons. A new window opens. Any changes you make in this window aren't actually applied until you click the **'Apply'** or **'OK'** buttons.
6.11 Other download options 6.12 Other download options
--------------------------- ---------------------------
Those are the *default* download options. If you want to apply a *different* set of download options to a particular channel or particular playlist, you can do so. Those are the *default* download options. If you want to apply a *different* set of download options to a particular channel or particular playlist, you can do so.
@ -372,7 +390,7 @@ The previous set of download options still applies to everything in the **Music*
.. image:: screenshots/example13.png .. image:: screenshots/example13.png
:alt: Download options applied to the Village People channel :alt: Download options applied to the Village People channel
6.12 Favourite videos 6.13 Favourite videos
--------------------- ---------------------
You can mark channels, playlists and even whole folders as favourites. You can mark channels, playlists and even whole folders as favourites.
@ -382,7 +400,7 @@ You can mark channels, playlists and even whole folders as favourites.
When you do that, any videos you download will appear in the **Favourite Videos** folder (as well as in their normal location). When you do that, any videos you download will appear in the **Favourite Videos** folder (as well as in their normal location).
6.13 Watching videos 6.14 Watching videos
-------------------- --------------------
If you've downloaded a video, you can watch it by clicking the word **Player**. If you've downloaded a video, you can watch it by clicking the word **Player**.
@ -394,7 +412,7 @@ If you haven't downloaded the video yet, you can watch it online by clicking the
If it's a YouTube video that is restricted (not available in certain regions, or without confirming your age), it's often possible to watch the same video without restrictions on the **HookTube** website. If it's a YouTube video that is restricted (not available in certain regions, or without confirming your age), it's often possible to watch the same video without restrictions on the **HookTube** website.
6.14 Combining channels, playlists and folders 6.15 Combining channels, playlists and folders
---------------------------------------------- ----------------------------------------------
**Tartube** can download videos from several channels and/or playlists into a single directory (folder) on your computer's hard drive. There are three situations in which this might be useful: **Tartube** can download videos from several channels and/or playlists into a single directory (folder) on your computer's hard drive. There are three situations in which this might be useful:
@ -403,7 +421,7 @@ If it's a YouTube video that is restricted (not available in certain regions, or
- A creator releases their videos on **BitChute** as well as on **YouTube**. You have added both channels, but you don't want to download duplicate videos - A creator releases their videos on **BitChute** as well as on **YouTube**. You have added both channels, but you don't want to download duplicate videos
- You don't care about keeping videos in separate directories/folders on your filesystem. You just want to download all videos to one place - You don't care about keeping videos in separate directories/folders on your filesystem. You just want to download all videos to one place
6.14.1 Combining one channel and many playlists 6.15.1 Combining one channel and many playlists
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A creator might have a single channel, and several playlists. The playlists contain videos from that channel (but not necessarily *every* video). A creator might have a single channel, and several playlists. The playlists contain videos from that channel (but not necessarily *every* video).
@ -417,7 +435,7 @@ The solution is to tell **Tartube** to store all the videos from the channel and
- Now, right-click on each playlist in turn and select **Playlist actions > Set download destination...** - Now, right-click on each playlist in turn and select **Playlist actions > Set download destination...**
- In the dialogue window, click **Choose a different directory/folder**, select the name of the channel, then click the **OK button** - In the dialogue window, click **Choose a different directory/folder**, select the name of the channel, then click the **OK button**
6.14.2 Combining channels from different websites 6.15.2 Combining channels from different websites
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A creator might release their videos on **YouTube**, but also on a site like **BitChute**. Sometimes they will only release a particular video on **BitChute**. A creator might release their videos on **YouTube**, but also on a site like **BitChute**. Sometimes they will only release a particular video on **BitChute**.
@ -433,7 +451,7 @@ The solution is to tell **Tartube** to store videos from both channels in a sing
It doesn't matter which of the two channels you use as the download destination. There is also no limit to the number of parallel channels, so if a creator uploads videos to a dozen different websites, you can add them all. It doesn't matter which of the two channels you use as the download destination. There is also no limit to the number of parallel channels, so if a creator uploads videos to a dozen different websites, you can add them all.
6.14.3 Download all videos to a single folder 6.15.3 Download all videos to a single folder
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you don't care about keeping videos in separate directories/folders on your filesystem, you can download *all* videos into the **Unsorted videos** folder. Regardless of whether you have added one channel or a thousand, all the videos will be stored in that one place. If you don't care about keeping videos in separate directories/folders on your filesystem, you can download *all* videos into the **Unsorted videos** folder. Regardless of whether you have added one channel or a thousand, all the videos will be stored in that one place.
@ -444,7 +462,7 @@ If you don't care about keeping videos in separate directories/folders on your f
Alternatively, you could select **Temporary Videos**. If you do, videos will be deleted when you shut down **Tartube** (and will not be re-downloaded in the future). Alternatively, you could select **Temporary Videos**. If you do, videos will be deleted when you shut down **Tartube** (and will not be re-downloaded in the future).
6.15 Archiving videos 6.16 Archiving videos
--------------------- ---------------------
You can tell **Tartube** to automatically delete videos after some period of time. This is useful if you don't have an infinitely large hard drive. You can tell **Tartube** to automatically delete videos after some period of time. This is useful if you don't have an infinitely large hard drive.
@ -463,7 +481,7 @@ You can also archive all the videos in a channel, playlist or folder.
- This action applies to *all* videos that are *currently* in the folder, including the contents of any channels and playlists in that folder - This action applies to *all* videos that are *currently* in the folder, including the contents of any channels and playlists in that folder
- It doesn't apply to any videos you might download in the future - It doesn't apply to any videos you might download in the future
6.16 Exporting/importing the Tartube database 6.17 Exporting/importing the Tartube database
--------------------------------------------- ---------------------------------------------
You can export the contents of **Tartube**'s database and, at any time in the future, import that information into a different **Tartube** database, perhaps on a different computer. You can export the contents of **Tartube**'s database and, at any time in the future, import that information into a different **Tartube** database, perhaps on a different computer.
@ -483,7 +501,7 @@ This is how to import the data into a different **Tartube** database.
- Select the export file you created earlier - Select the export file you created earlier
- A dialogue window will appear. You can choose how much of the database you want to import - A dialogue window will appear. You can choose how much of the database you want to import
6.17 Importing videos from other applications 6.18 Importing videos from other applications
--------------------------------------------- ---------------------------------------------
**Tartube** is a GUI front-end for `youtube-dl <https://youtube-dl.org/>`__, but it is not the only one. If you've downloaded videos using another application, this is how to add them to Tartube's database. **Tartube** is a GUI front-end for `youtube-dl <https://youtube-dl.org/>`__, but it is not the only one. If you've downloaded videos using another application, this is how to add them to Tartube's database.
@ -494,7 +512,7 @@ This is how to import the data into a different **Tartube** database.
- In the **Tartube** menu, click **Operations > Refresh database**. **Tartube** will search for video files, and try to match them with the contents of its database - In the **Tartube** menu, click **Operations > Refresh database**. **Tartube** will search for video files, and try to match them with the contents of its database
- The whole process might some time, so be patient - The whole process might some time, so be patient
6.18 Converting to audio 6.19 Converting to audio
------------------------ ------------------------
**Tartube** can automatically extract the audio from its downloaded videos, if that's what you want. **Tartube** can automatically extract the audio from its downloaded videos, if that's what you want.
@ -504,9 +522,18 @@ The first step is to make sure that either FFmpeg or AVconv is installed on your
The remaining steps are simple: The remaining steps are simple:
- In **Tartube**'s main window, click **Edit > General download options...** - In **Tartube**'s main window, click **Edit > General download options...**
- In the new window, if the **Post-processing** tab is not visible, then click the button **Show advanced download options**
- Now click on the **Post-processing** tab In the new window, if the **Post-processing** tab is not visible, do this:
- Click the button **Post-process video files to convert them to audio-only files** to select it
- Click the **Sound Only** tab
- Select the checkbox **Download each video, extract the sound, and then discard the original videos**
- In the boxes below, select an audio format and an audio quality
- Click the **OK** button at the bottom of the window to apply your changes
If the **Post-processing** tab *is* visible, do this:
- Click on the **Post-processing** tab
- Select the checkbox **Post-process video files to convert them to audio-only files**
- If you want, click the button **Keep video file after post-processing it** to select it - If you want, click the button **Keep video file after post-processing it** to select it
- In the box labelled **Audio format of the post-processed file**, specify what type of audio file you want - **.mp3**, **.wav**, etc - In the box labelled **Audio format of the post-processed file**, specify what type of audio file you want - **.mp3**, **.wav**, etc
- Click the **OK** button at the bottom of the window to apply your changes - Click the **OK** button at the bottom of the window to apply your changes
@ -535,7 +562,7 @@ Note that Tartube does not create backup copies of the videos you've downloaded.
**Q: I want to convert the video files to audio files!** **Q: I want to convert the video files to audio files!**
A: See `6.18 Converting to audio`_ A: See `6.19 Converting to audio`_
**Q: I want to see all the videos on a single page, not spread over several pages!** **Q: I want to see all the videos on a single page, not spread over several pages!**
@ -579,42 +606,18 @@ The NSIS scripts used to create the installers can be found here:
The scripts contain full instructions, so you should be able to create your own installer, and compare it with the official one. The scripts contain full instructions, so you should be able to create your own installer, and compare it with the official one.
8. Future plans 8. Contributing
=============== ===============
- Fix the endless crashes **DONE**
- Support for multiple databases (so you can store videos on two external hard drives at the same time)
- Add download scheduling **DONE**
- Add video archiving **DONE**
- Allow selection of multiple videos in the catalogue, so the same action can be applied to all of them at the same time **DONE**
- Tie channels and playlists together, so that they won't both download the same video **DONE**
- Add tooltips for everything **DONE**
- Add more youtube-dl options **DONE**
- Expand this guide to explain all features of Tartube
9. Known issues
===============
- Tartube crashes continuously and often **FIXED**
- Alphabetic sorting of channels/playlists/folders doesn't always work as intended, due to an unresolved Gtk issue **FIXED**
- Channels/playlists/folder selection does not always work as intended, due to an unresolved Gtk issue **FIXED**
- Users can type in comboboxes, but this should not be possible **FIXED**
- Some MS Windows users report that Tartube will install, but not run **FIXED**
- Some MS Windows users report that Tartube doesn't recognise FFmpeg **FIXED**
- Installation via **pip** does not work
10. Contributing
================
- Report a bug: Use the Github - Report a bug: Use the Github
`issues <https://github.com/axcore/tartube/issues>`__ page `issues <https://github.com/axcore/tartube/issues>`__ page
11. Authors 9. Authors
=========== ==========
See the `AUTHORS <AUTHORS>`__ file. See the `AUTHORS <AUTHORS>`__ file.
12. License 10. License
=========== ===========
Tartube is licensed under the `GNU General Public License v3.0 <https://www.gnu.org/licenses/gpl-3.0.en.html>`__. Tartube is licensed under the `GNU General Public License v3.0 <https://www.gnu.org/licenses/gpl-3.0.en.html>`__.

View File

@ -1,4 +1,4 @@
# Tartube v1.3.048 installer script for MS Windows # Tartube v1.3.077 installer script for MS Windows
# #
# Copyright (C) 2019-2020 A S Lewis # Copyright (C) 2019-2020 A S Lewis
# #
@ -139,7 +139,7 @@
;Name and file ;Name and file
Name "Tartube" Name "Tartube"
OutFile "install-tartube-1.3.048-32bit.exe" OutFile "install-tartube-1.3.077-32bit.exe"
;Default installation folder ;Default installation folder
InstallDir "$LOCALAPPDATA\Tartube" InstallDir "$LOCALAPPDATA\Tartube"
@ -244,7 +244,7 @@ Section "Tartube" SecClient
"Publisher" "A S Lewis" "Publisher" "A S Lewis"
WriteRegStr HKLM \ WriteRegStr HKLM \
"Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
"DisplayVersion" "1.3.048" "DisplayVersion" "1.3.077"
# Create uninstaller # Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe" WriteUninstaller "$INSTDIR\Uninstall.exe"

View File

@ -1,4 +1,4 @@
# Tartube v1.3.048 installer script for MS Windows # Tartube v1.3.077 installer script for MS Windows
# #
# Copyright (C) 2019-2020 A S Lewis # Copyright (C) 2019-2020 A S Lewis
# #
@ -140,7 +140,7 @@
;Name and file ;Name and file
Name "Tartube" Name "Tartube"
OutFile "install-tartube-1.3.048-64bit.exe" OutFile "install-tartube-1.3.077-64bit.exe"
;Default installation folder ;Default installation folder
InstallDir "$LOCALAPPDATA\Tartube" InstallDir "$LOCALAPPDATA\Tartube"
@ -245,7 +245,7 @@ Section "Tartube" SecClient
"Publisher" "A S Lewis" "Publisher" "A S Lewis"
WriteRegStr HKLM \ WriteRegStr HKLM \
"Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
"DisplayVersion" "1.3.048" "DisplayVersion" "1.3.077"
# Create uninstaller # Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe" WriteUninstaller "$INSTDIR\Uninstall.exe"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -62,7 +62,7 @@ if env_var_value is not None:
# Setup # Setup
setuptools.setup( setuptools.setup(
name='tartube', name='tartube',
version='1.3.053', version='1.3.077',
description='GUI front-end for youtube-dl', description='GUI front-end for youtube-dl',
# long_description=long_description, # long_description=long_description,
long_description="""Tartube is a GUI front-end for youtube-dl, partly based long_description="""Tartube is a GUI front-end for youtube-dl, partly based

View File

@ -1148,7 +1148,6 @@ class GenericEditWin(GenericConfigWin):
entry.set_width_chars(8) entry.set_width_chars(8)
main_win_obj = self.app_obj.main_win_obj main_win_obj = self.app_obj.main_win_obj
parent_obj = self.edit_obj.parent_obj
if isinstance(self.edit_obj, media.Channel): if isinstance(self.edit_obj, media.Channel):
icon_path = main_win_obj.icon_dict['channel_small'] icon_path = main_win_obj.icon_dict['channel_small']
elif isinstance(self.edit_obj, media.Playlist): elif isinstance(self.edit_obj, media.Playlist):
@ -1190,8 +1189,14 @@ class GenericEditWin(GenericConfigWin):
) )
label2.set_hexpand(False) label2.set_hexpand(False)
parent_obj = self.edit_obj.parent_obj
if parent_obj: if parent_obj:
icon_path2 = main_win_obj.icon_dict['folder_small'] if isinstance(parent_obj, media.Channel):
icon_path2 = main_win_obj.icon_dict['channel_small']
elif isinstance(parent_obj, media.Playlist):
icon_path2 = main_win_obj.icon_dict['playlist_small']
else:
icon_path2 = main_win_obj.icon_dict['folder_small']
else: else:
icon_path2 = main_win_obj.icon_dict['folder_black_small'] icon_path2 = main_win_obj.icon_dict['folder_black_small']
@ -1955,6 +1960,8 @@ class OptionsEditWin(GenericEditWin):
self.setup_others_tab() self.setup_others_tab()
if not self.app_obj.simple_options_flag: if not self.app_obj.simple_options_flag:
self.setup_advanced_tab() self.setup_advanced_tab()
else:
self.setup_sound_only_tab()
def setup_general_tab(self): def setup_general_tab(self):
@ -3374,6 +3381,70 @@ class OptionsEditWin(GenericEditWin):
) )
def setup_sound_only_tab(self):
"""Called by self.setup_tabs().
Sets up the 'Sound Only' tab.
"""
tab, grid = self.add_notebook_tab('_Sound only')
grid_width = 4
# Sound only options
self.add_label(grid,
'<u>Sound only options</u>',
0, 0, grid_width, 1,
)
# (The MS Windows installer includes FFmpeg)
text = 'Download each video, extract the sound, and then discard the' \
+ ' original videos'
if os.name != 'nt':
text += '\n(requires that FFmpeg or AVConv is installed on your' \
+ ' system)'
self.add_checkbutton(grid,
text,
'extract_audio',
0, 1, grid_width, 1,
)
label = self.add_label(grid,
'Use this audio format: ',
0, 2, 1, 1,
)
label.set_hexpand(False)
combo_list = formats.AUDIO_FORMAT_LIST
combo_list.insert(0, '')
combo = self.add_combo(grid,
combo_list,
'audio_format',
1, 2, 1, 1,
)
combo.set_hexpand(True)
label2 = self.add_label(grid,
'Use this audio quality: ',
2, 2, 1, 1,
)
label2.set_hexpand(False)
combo2_list = [
['High', '0'],
['Medium', '5'],
['Low', '9'],
]
combo2 = self.add_combo_with_data(grid,
combo2_list,
'audio_quality',
3, 2, 1, 1,
)
combo2.set_hexpand(True)
# (Tab support functions) # (Tab support functions)
@ -5345,89 +5416,143 @@ class SystemPrefWin(GenericPrefWin):
checkbutton6.connect('toggled', self.on_reverse_button_toggled) checkbutton6.connect('toggled', self.on_reverse_button_toggled)
checkbutton7 = self.add_checkbutton(grid, checkbutton7 = self.add_checkbutton(grid,
'Show system warning messages in the \'Errors/Warnings\' tab',
self.app_obj.system_warning_show_flag,
True, # Can be toggled by user
0, 7, 1, 1,
)
checkbutton7.connect('toggled', self.on_warning_button_toggled)
checkbutton8 = self.add_checkbutton(grid,
'Don\'t remove number of system messages from tab label until' \ 'Don\'t remove number of system messages from tab label until' \
+ ' \'Clear\' button is clicked', + ' \'Clear\' button is clicked',
self.app_obj.system_msg_keep_totals_flag, self.app_obj.system_msg_keep_totals_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 8, 1, 1, 0, 7, 1, 1,
) )
checkbutton8.connect('toggled', self.on_system_keep_button_toggled) checkbutton7.connect('toggled', self.on_system_keep_button_toggled)
# System tray preferences # System tray preferences
self.add_label(grid, self.add_label(grid,
'<u>System tray preferences</u>', '<u>System tray preferences</u>',
0, 9, 1, 1, 0, 8, 1, 1,
) )
checkbutton9 = self.add_checkbutton(grid, checkbutton8 = self.add_checkbutton(grid,
'Show icon in system tray', 'Show icon in system tray',
self.app_obj.show_status_icon_flag, self.app_obj.show_status_icon_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 9, 1, 1,
)
checkbutton8.set_hexpand(False)
# signal connnect appears below
checkbutton9 = self.add_checkbutton(grid,
'Close to the tray, rather than closing the application',
self.app_obj.close_to_tray_flag,
True, # Can be toggled by user
0, 10, 1, 1, 0, 10, 1, 1,
) )
checkbutton9.set_hexpand(False) checkbutton9.set_hexpand(False)
# signal connnect appears below checkbutton9.connect('toggled', self.on_close_to_tray_toggled)
checkbutton10 = self.add_checkbutton(grid,
'Close to the tray, rather than closing the application',
self.app_obj.close_to_tray_flag,
True, # Can be toggled by user
0, 11, 1, 1,
)
checkbutton10.set_hexpand(False)
checkbutton10.connect('toggled', self.on_close_to_tray_toggled)
if not self.app_obj.show_status_icon_flag: if not self.app_obj.show_status_icon_flag:
checkbutton10.set_sensitive(False) checkbutton9.set_sensitive(False)
# signal connect from above # signal connect from above
checkbutton9.connect( checkbutton8.connect(
'toggled', 'toggled',
self.on_show_status_icon_toggled, self.on_show_status_icon_toggled,
checkbutton10, checkbutton9,
) )
# Dialogue window preferences # Dialogue window preferences
self.add_label(grid, self.add_label(grid,
'<u>Dialogue window preferences</u>', '<u>Dialogue window preferences</u>',
0, 13, 1, 1, 0, 11, 1, 1,
) )
checkbutton11 = self.add_checkbutton(grid, checkbutton10 = self.add_checkbutton(grid,
'When adding channels/playlists, keep the dialogue window open', 'When adding channels/playlists, keep the dialogue window open',
self.app_obj.dialogue_keep_open_flag, self.app_obj.dialogue_keep_open_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 14, 1, 1, 0, 12, 1, 1,
) )
checkbutton11.set_hexpand(False) checkbutton10.set_hexpand(False)
# signal connnect appears below # signal connnect appears below
checkbutton12 = self.add_checkbutton(grid, checkbutton11 = self.add_checkbutton(grid,
'When adding videos/channels/playlists, copy URLs from the' \ 'When adding videos/channels/playlists, copy URLs from the' \
+ ' system clipboard', + ' system clipboard',
self.app_obj.dialogue_copy_clipboard_flag, self.app_obj.dialogue_copy_clipboard_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 15, 1, 1, 0, 13, 1, 1,
) )
checkbutton12.set_hexpand(False) checkbutton11.set_hexpand(False)
checkbutton12.connect('toggled', self.on_clipboard_button_toggled) checkbutton11.connect('toggled', self.on_clipboard_button_toggled)
if self.app_obj.dialogue_keep_open_flag: if self.app_obj.dialogue_keep_open_flag:
checkbutton12.set_sensitive(False) checkbutton11.set_sensitive(False)
# signal connect from above # signal connect from above
checkbutton11.connect( checkbutton10.connect(
'toggled', 'toggled',
self.on_keep_open_button_toggled, self.on_keep_open_button_toggled,
checkbutton12, checkbutton11,
) )
# Error/warning preferences
self.add_label(grid,
'<u>Error/warning preferences</u>',
0, 14, 1, 1,
)
checkbutton12 = self.add_checkbutton(grid,
'Show system error messages in the \'Errors/Warnings\' tab',
self.app_obj.system_error_show_flag,
True, # Can be toggled by user
0, 15, 1, 1,
)
checkbutton12.connect('toggled', self.on_error_button_toggled)
checkbutton13 = self.add_checkbutton(grid,
'Show system warning messages in the \'Errors/Warnings\' tab',
self.app_obj.system_warning_show_flag,
True, # Can be toggled by user
0, 16, 1, 1,
)
checkbutton13.connect('toggled', self.on_warning_button_toggled)
checkbutton14 = self.add_checkbutton(grid,
'Ignore \'Requested formats are incompatible for merge\' warnings',
self.app_obj.ignore_merge_warning_flag,
True, # Can be toggled by user
0, 17, 1, 1,
)
checkbutton14.connect('toggled', self.on_merge_button_toggled)
checkbutton15 = self.add_checkbutton(grid,
'Ignore YouTube copyright errors',
self.app_obj.ignore_yt_copyright_flag,
True, # Can be toggled by user
0, 18, 1, 1,
)
checkbutton15.connect('toggled', self.on_copyright_button_toggled)
checkbutton16 = self.add_checkbutton(grid,
'Ignore \'Child process exited with non-zero code\' errors',
self.app_obj.ignore_child_process_exit_flag,
True, # Can be toggled by user
0, 19, 1, 1,
)
checkbutton16.connect('toggled', self.on_child_process_button_toggled)
checkbutton17 = self.add_checkbutton(grid,
'Ignore \'There are no annotations to write\' warnings',
self.app_obj.ignore_no_annotations_flag,
True, # Can be toggled by user
0, 20, 1, 1,
)
checkbutton17.connect('toggled', self.on_no_annotations_button_toggled)
checkbutton18 = self.add_checkbutton(grid,
'Ignore \'Video doesn\'t have subtitles\' warnings',
self.app_obj.ignore_no_subtitles_flag,
True, # Can be toggled by user
0, 21, 1, 1,
)
checkbutton18.connect('toggled', self.on_no_subtitles_button_toggled)
def setup_videos_tab(self): def setup_videos_tab(self):
@ -5799,17 +5924,86 @@ class SystemPrefWin(GenericPrefWin):
'default', 'default',
) )
# URL flexibility preferences
self.add_label(grid,
'<u>URL flexibility preferences</u>',
0, 7, grid_width, 1,
)
radiobutton4 = self.add_radiobutton(grid,
None,
'If a video\'s URL represents a channel/playlist, not a video,' \
+ ' don\'t download it',
0, 8, grid_width, 1,
)
# Signal connect appears below
radiobutton5 = self.add_radiobutton(grid,
radiobutton4,
# 'If a URL represents a channel/playlist, not a video, download' \
# + ' multiple videos',
'...or, download multiple videos into the containing folder',
0, 9, grid_width, 1,
)
if self.app_obj.operation_convert_mode == 'multi':
radiobutton5.set_active(True)
# Signal connect appears below
radiobutton6 = self.add_radiobutton(grid,
radiobutton5,
# 'If a URL represents a channel/playlist, not a video, convert' \
# + ' the video to a channel',
'...or, create a new channel, and download the videos into that',
0, 10, grid_width, 1,
)
if self.app_obj.operation_convert_mode == 'channel':
radiobutton6.set_active(True)
# Signal connect appears below
radiobutton7 = self.add_radiobutton(grid,
radiobutton6,
# 'If a URL represents a channel/playlist, not a video, convert' \
# + ' the video to a playlist',
'...or, create a new playlist, and download the videos into that',
0, 11, grid_width, 1,
)
if self.app_obj.operation_convert_mode == 'playlist':
radiobutton7.set_active(True)
# Signal connect appears below
# Signal connects from above
radiobutton4.connect(
'toggled',
self.on_convert_from_button_toggled,
'disable',
)
radiobutton5.connect(
'toggled',
self.on_convert_from_button_toggled,
'multi',
)
radiobutton6.connect(
'toggled',
self.on_convert_from_button_toggled,
'channel',
)
radiobutton7.connect(
'toggled',
self.on_convert_from_button_toggled,
'playlist',
)
# Performance limits # Performance limits
self.add_label(grid, self.add_label(grid,
'<u>Performance limits</u>', '<u>Performance limits</u>',
0, 7, grid_width, 1, 0, 12, grid_width, 1,
) )
checkbutton3 = self.add_checkbutton(grid, checkbutton3 = self.add_checkbutton(grid,
'Limit simultaneous downloads to', 'Limit simultaneous downloads to',
self.app_obj.num_worker_apply_flag, self.app_obj.num_worker_apply_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 8, 1, 1, 0, 13, 1, 1,
) )
checkbutton3.set_hexpand(False) checkbutton3.set_hexpand(False)
checkbutton3.connect('toggled', self.on_worker_button_toggled) checkbutton3.connect('toggled', self.on_worker_button_toggled)
@ -5819,7 +6013,7 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.num_worker_max, self.app_obj.num_worker_max,
1, # Step 1, # Step
self.app_obj.num_worker_default, self.app_obj.num_worker_default,
1, 8, 1, 1, 1, 13, 1, 1,
) )
spinbutton.connect('value-changed', self.on_worker_spinbutton_changed) spinbutton.connect('value-changed', self.on_worker_spinbutton_changed)
@ -5827,7 +6021,7 @@ class SystemPrefWin(GenericPrefWin):
'Limit download speed to', 'Limit download speed to',
self.app_obj.bandwidth_apply_flag, self.app_obj.bandwidth_apply_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 9, 1, 1, 0, 14, 1, 1,
) )
checkbutton4.set_hexpand(False) checkbutton4.set_hexpand(False)
checkbutton4.connect('toggled', self.on_bandwidth_button_toggled) checkbutton4.connect('toggled', self.on_bandwidth_button_toggled)
@ -5837,7 +6031,7 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.bandwidth_max, self.app_obj.bandwidth_max,
1, # Step 1, # Step
self.app_obj.bandwidth_default, self.app_obj.bandwidth_default,
1, 9, 1, 1, 1, 14, 1, 1,
) )
spinbutton2.connect( spinbutton2.connect(
'value-changed', 'value-changed',
@ -5846,14 +6040,14 @@ class SystemPrefWin(GenericPrefWin):
self.add_label(grid, self.add_label(grid,
'KiB/s', 'KiB/s',
2, 9, 1, 1, 2, 14, 1, 1,
) )
checkbutton5 = self.add_checkbutton(grid, checkbutton5 = self.add_checkbutton(grid,
'Limit video resolution (overriding video format options) to', 'Limit video resolution (overriding video format options) to',
self.app_obj.video_res_apply_flag, self.app_obj.video_res_apply_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 10, 1, 1, 0, 15, 1, 1,
) )
checkbutton5.set_hexpand(False) checkbutton5.set_hexpand(False)
checkbutton5.connect('toggled', self.on_video_res_button_toggled) checkbutton5.connect('toggled', self.on_video_res_button_toggled)
@ -5861,7 +6055,7 @@ class SystemPrefWin(GenericPrefWin):
combo = self.add_combo(grid, combo = self.add_combo(grid,
formats.VIDEO_RESOLUTION_LIST, formats.VIDEO_RESOLUTION_LIST,
None, None,
1, 10, 1, 1, 1, 15, 1, 1,
) )
combo.set_active( combo.set_active(
formats.VIDEO_RESOLUTION_LIST.index( formats.VIDEO_RESOLUTION_LIST.index(
@ -5873,7 +6067,7 @@ class SystemPrefWin(GenericPrefWin):
# Time-saving preferences # Time-saving preferences
self.add_label(grid, self.add_label(grid,
'<u>Time-saving preferences</u>', '<u>Time-saving preferences</u>',
0, 11, grid_width, 1, 0, 16, grid_width, 1,
) )
checkbutton6 = self.add_checkbutton(grid, checkbutton6 = self.add_checkbutton(grid,
@ -5881,20 +6075,20 @@ class SystemPrefWin(GenericPrefWin):
+ ' sending videos we already have', + ' sending videos we already have',
self.app_obj.operation_limit_flag, self.app_obj.operation_limit_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 12, grid_width, 1, 0, 17, grid_width, 1,
) )
checkbutton6.set_hexpand(False) checkbutton6.set_hexpand(False)
# Signal connect appears below # Signal connect appears below
self.add_label(grid, self.add_label(grid,
'Stop after this many videos (when checking)', 'Stop after this many videos (when checking)',
0, 13, 1, 1, 0, 18, 1, 1,
) )
entry = self.add_entry(grid, entry = self.add_entry(grid,
self.app_obj.operation_check_limit, self.app_obj.operation_check_limit,
True, True,
1, 13, 1, 1, 1, 18, 1, 1,
) )
entry.set_hexpand(False) entry.set_hexpand(False)
entry.set_width_chars(4) entry.set_width_chars(4)
@ -5904,13 +6098,13 @@ class SystemPrefWin(GenericPrefWin):
self.add_label(grid, self.add_label(grid,
'Stop after this many videos (when downloading)', 'Stop after this many videos (when downloading)',
0, 14, 1, 1, 0, 19, 1, 1,
) )
entry2 = self.add_entry(grid, entry2 = self.add_entry(grid,
self.app_obj.operation_download_limit, self.app_obj.operation_download_limit,
True, True,
1, 14, 1, 1, 1, 19, 1, 1,
) )
entry2.set_hexpand(False) entry2.set_hexpand(False)
entry2.set_width_chars(4) entry2.set_width_chars(4)
@ -5929,7 +6123,7 @@ class SystemPrefWin(GenericPrefWin):
# Download options preferences # Download options preferences
self.add_label(grid, self.add_label(grid,
'<u>Download options preferences</u>', '<u>Download options preferences</u>',
0, 15, grid_width, 1, 0, 20, grid_width, 1,
) )
checkbutton7 = self.add_checkbutton(grid, checkbutton7 = self.add_checkbutton(grid,
@ -5937,7 +6131,7 @@ class SystemPrefWin(GenericPrefWin):
+ ' download options', + ' download options',
self.app_obj.auto_clone_options_flag, self.app_obj.auto_clone_options_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 16, grid_width, 1, 0, 21, grid_width, 1,
) )
checkbutton7.set_hexpand(False) checkbutton7.set_hexpand(False)
checkbutton7.connect('toggled', self.on_auto_clone_button_toggled) checkbutton7.connect('toggled', self.on_auto_clone_button_toggled)
@ -6095,52 +6289,6 @@ class SystemPrefWin(GenericPrefWin):
) )
checkbutton2.connect('toggled', self.on_json_button_toggled) checkbutton2.connect('toggled', self.on_json_button_toggled)
# Message filter preferences
self.add_label(grid,
'<u>Message filter preferences</u>',
0, 10, grid_width, 1,
)
checkbutton3 = self.add_checkbutton(grid,
'Ignore \'Requested formats are incompatible for merge\' warnings',
self.app_obj.ignore_merge_warning_flag,
True, # Can be toggled by user
0, 11, grid_width, 1,
)
checkbutton3.connect('toggled', self.on_merge_button_toggled)
checkbutton4 = self.add_checkbutton(grid,
'Ignore YouTube copyright errors',
self.app_obj.ignore_yt_copyright_flag,
True, # Can be toggled by user
0, 12, grid_width, 1,
)
checkbutton4.connect('toggled', self.on_copyright_button_toggled)
checkbutton5 = self.add_checkbutton(grid,
'Ignore \'Child process exited with non-zero code\' errors',
self.app_obj.ignore_child_process_exit_flag,
True, # Can be toggled by user
0, 13, grid_width, 1,
)
checkbutton5.connect('toggled', self.on_child_process_button_toggled)
checkbutton6 = self.add_checkbutton(grid,
'Ignore \'There are no annotations to write\' warnings',
self.app_obj.ignore_no_annotations_flag,
True, # Can be toggled by user
0, 14, grid_width, 1,
)
checkbutton6.connect('toggled', self.on_no_annotations_button_toggled)
checkbutton7 = self.add_checkbutton(grid,
'Ignore \'Video doesn\'t have subtitles\' warnings',
self.app_obj.ignore_no_subtitles_flag,
True, # Can be toggled by user
0, 15, grid_width, 1,
)
checkbutton7.connect('toggled', self.on_no_subtitles_button_toggled)
def setup_output_tab(self): def setup_output_tab(self):
@ -6157,171 +6305,203 @@ class SystemPrefWin(GenericPrefWin):
0, 0, 1, 1, 0, 0, 1, 1,
) )
checkbutton = self.add_checkbutton(grid, checkbutton = self.add_checkbutton(grid,
'Display output from youtube-dl\'s STDOUT in the Output Tab', 'Display youtube-dl system commands in the Output Tab',
self.app_obj.ytdl_output_stdout_flag, self.app_obj.ytdl_output_system_cmd_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 1, 1, 1, 0, 1, 1, 1,
) )
checkbutton.set_hexpand(False) checkbutton.set_hexpand(False)
# Signal connect appears below checkbutton.connect('toggled', self.on_output_system_button_toggled)
checkbutton2 = self.add_checkbutton(grid, checkbutton2 = self.add_checkbutton(grid,
'...but don\'t write each video\'s JSON data', 'Display output from youtube-dl\'s STDOUT in the Output Tab',
self.app_obj.ytdl_output_ignore_json_flag, self.app_obj.ytdl_output_stdout_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 2, 1, 1, 0, 2, 1, 1,
) )
checkbutton2.set_hexpand(False) checkbutton2.set_hexpand(False)
checkbutton2.connect('toggled', self.on_output_json_button_toggled) # Signal connect appears below
if not self.app_obj.ytdl_output_stdout_flag:
checkbutton2.set_sensitive(False)
checkbutton3 = self.add_checkbutton(grid, checkbutton3 = self.add_checkbutton(grid,
'...but don\'t write each video\'s download progress', '...but don\'t write each video\'s JSON data',
self.app_obj.ytdl_output_ignore_progress_flag, self.app_obj.ytdl_output_ignore_json_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 3, 1, 1, 0, 3, 1, 1,
) )
checkbutton3.set_hexpand(False) checkbutton3.set_hexpand(False)
checkbutton3.connect('toggled', self.on_output_progress_button_toggled) checkbutton3.connect('toggled', self.on_output_json_button_toggled)
if not self.app_obj.ytdl_output_stdout_flag: if not self.app_obj.ytdl_output_stdout_flag:
checkbutton3.set_sensitive(False) checkbutton3.set_sensitive(False)
# Signal connect from above
checkbutton.connect(
'toggled',
self.on_output_stdout_button_toggled,
checkbutton2,
checkbutton3,
)
checkbutton4 = self.add_checkbutton(grid, checkbutton4 = self.add_checkbutton(grid,
'Display output from youtube-dl\'s STDERR in the Output Tab', '...but don\'t write each video\'s download progress',
self.app_obj.ytdl_output_stderr_flag, self.app_obj.ytdl_output_ignore_progress_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 4, 1, 1, 0, 4, 1, 1,
) )
checkbutton4.set_hexpand(False) checkbutton4.set_hexpand(False)
checkbutton4.connect('toggled', self.on_output_stderr_button_toggled) checkbutton4.connect('toggled', self.on_output_progress_button_toggled)
if not self.app_obj.ytdl_output_stdout_flag:
checkbutton4.set_sensitive(False)
# Signal connect from above
checkbutton2.connect(
'toggled',
self.on_output_stdout_button_toggled,
checkbutton3,
checkbutton4,
)
checkbutton5 = self.add_checkbutton(grid, checkbutton5 = self.add_checkbutton(grid,
'Empty pages in the Output Tab at the start of every operation', 'Display output from youtube-dl\'s STDERR in the Output Tab',
self.app_obj.ytdl_output_start_empty_flag, self.app_obj.ytdl_output_stderr_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 5, 1, 1, 0, 5, 1, 1,
) )
checkbutton5.set_hexpand(False) checkbutton5.set_hexpand(False)
checkbutton5.connect('toggled', self.on_output_empty_button_toggled) checkbutton5.connect('toggled', self.on_output_stderr_button_toggled)
checkbutton6 = self.add_checkbutton(grid,
'Empty pages in the Output Tab at the start of every operation',
self.app_obj.ytdl_output_start_empty_flag,
True, # Can be toggled by user
0, 6, 1, 1,
)
checkbutton6.set_hexpand(False)
checkbutton6.connect('toggled', self.on_output_empty_button_toggled)
checkbutton7 = self.add_checkbutton(grid,
'Show a summary of active threads (changes are applied when ' \
+ utils.upper_case_first(__main__.__packagename__) + ' restarts',
self.app_obj.ytdl_output_show_summary_flag,
True, # Can be toggled by user
0, 7, 1, 1,
)
checkbutton7.set_hexpand(False)
checkbutton7.connect('toggled', self.on_output_summary_button_toggled)
# Terminal window preferences # Terminal window preferences
self.add_label(grid, self.add_label(grid,
'<u>Terminal window preferences</u>', '<u>Terminal window preferences</u>',
0, 6, 1, 1,
)
checkbutton6 = self.add_checkbutton(grid,
'Write output from youtube-dl\'s STDOUT to the terminal window',
self.app_obj.ytdl_write_stdout_flag,
True, # Can be toggled by user
0, 7, 1, 1,
)
checkbutton6.set_hexpand(False)
# Signal connect appears below
checkbutton7 = self.add_checkbutton(grid,
'...but don\'t write each video\'s JSON data',
self.app_obj.ytdl_write_ignore_json_flag,
True, # Can be toggled by user
0, 8, 1, 1, 0, 8, 1, 1,
) )
checkbutton7.set_hexpand(False)
checkbutton7.connect('toggled', self.on_terminal_json_button_toggled)
if not self.app_obj.ytdl_write_stdout_flag:
checkbutton7.set_sensitive(False)
checkbutton8 = self.add_checkbutton(grid, checkbutton8 = self.add_checkbutton(grid,
'...but don\'t write each video\'s download progress', 'Write youtube-dl system commands to the terminal window',
self.app_obj.ytdl_write_ignore_progress_flag, self.app_obj.ytdl_write_system_cmd_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 9, 1, 1, 0, 9, 1, 1,
) )
checkbutton8.set_hexpand(False) checkbutton8.set_hexpand(False)
checkbutton8.connect( checkbutton8.connect('toggled', self.on_terminal_system_button_toggled)
'toggled',
self.on_terminal_progress_button_toggled,
)
if not self.app_obj.ytdl_write_stdout_flag:
checkbutton8.set_sensitive(False)
# Signal connect from above
checkbutton6.connect(
'toggled',
self.on_terminal_stdout_button_toggled,
checkbutton7,
checkbutton8,
)
checkbutton9 = self.add_checkbutton(grid, checkbutton9 = self.add_checkbutton(grid,
'Write output from youtube-dl\'s STDERR to the terminal window', 'Write output from youtube-dl\'s STDOUT to the terminal window',
self.app_obj.ytdl_write_stderr_flag, self.app_obj.ytdl_write_stdout_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 10, 1, 1, 0, 10, 1, 1,
) )
checkbutton9.set_hexpand(False) checkbutton9.set_hexpand(False)
checkbutton9.connect('toggled', self.on_terminal_stderr_button_toggled) # Signal connect appears below
checkbutton10 = self.add_checkbutton(grid,
'...but don\'t write each video\'s JSON data',
self.app_obj.ytdl_write_ignore_json_flag,
True, # Can be toggled by user
0, 11, 1, 1,
)
checkbutton10.set_hexpand(False)
checkbutton10.connect('toggled', self.on_terminal_json_button_toggled)
if not self.app_obj.ytdl_write_stdout_flag:
checkbutton10.set_sensitive(False)
checkbutton11 = self.add_checkbutton(grid,
'...but don\'t write each video\'s download progress',
self.app_obj.ytdl_write_ignore_progress_flag,
True, # Can be toggled by user
0, 12, 1, 1,
)
checkbutton11.set_hexpand(False)
checkbutton11.connect(
'toggled',
self.on_terminal_progress_button_toggled,
)
if not self.app_obj.ytdl_write_stdout_flag:
checkbutton11.set_sensitive(False)
# Signal connect from above
checkbutton9.connect(
'toggled',
self.on_terminal_stdout_button_toggled,
checkbutton10,
checkbutton11,
)
checkbutton12 = self.add_checkbutton(grid,
'Write output from youtube-dl\'s STDERR to the terminal window',
self.app_obj.ytdl_write_stderr_flag,
True, # Can be toggled by user
0, 13, 1, 1,
)
checkbutton12.set_hexpand(False)
checkbutton12.connect(
'toggled',
self.on_terminal_stderr_button_toggled,
)
# Special preferences # Special preferences
self.add_label(grid, self.add_label(grid,
'<u>Special preferences (applies to both the Output Tab and the' \ '<u>Special preferences (applies to both the Output Tab and the' \
+ ' terminal window)</u>', + ' terminal window)</u>',
0, 11, 1, 1, 0, 14, 1, 1,
) )
checkbutton10 = self.add_checkbutton(grid, checkbutton13 = self.add_checkbutton(grid,
'Write verbose output (youtube-dl debugging mode)', 'Write verbose output (youtube-dl debugging mode)',
self.app_obj.ytdl_write_verbose_flag, self.app_obj.ytdl_write_verbose_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 12, 1, 1, 0, 15, 1, 1,
) )
checkbutton10.set_hexpand(False) checkbutton13.set_hexpand(False)
checkbutton10.connect('toggled', self.on_verbose_button_toggled) checkbutton13.connect('toggled', self.on_verbose_button_toggled)
# Refresh operation preferences # Refresh operation preferences
self.add_label(grid, self.add_label(grid,
'<u>Refresh operation preferences</u>', '<u>Refresh operation preferences</u>',
0, 13, 1, 1, 0, 16, 1, 1,
) )
checkbutton11 = self.add_checkbutton(grid, checkbutton14 = self.add_checkbutton(grid,
'During a refresh operation, show all matching videos in the' \ 'During a refresh operation, show all matching videos in the' \
+ ' Output Tab', + ' Output Tab',
self.app_obj.refresh_output_videos_flag, self.app_obj.refresh_output_videos_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 14, 1, 1, 0, 17, 1, 1,
) )
checkbutton11.set_hexpand(False) checkbutton14.set_hexpand(False)
# Signal connect appears below # Signal connect appears below
checkbutton12 = self.add_checkbutton(grid, checkbutton15 = self.add_checkbutton(grid,
'...also show all non-matching videos', '...also show all non-matching videos',
self.app_obj.refresh_output_verbose_flag, self.app_obj.refresh_output_verbose_flag,
True, # Can be toggled by user True, # Can be toggled by user
0, 15, 1, 1, 0, 18, 1, 1,
) )
checkbutton12.set_hexpand(False) checkbutton15.set_hexpand(False)
checkbutton12.connect( checkbutton15.connect(
'toggled', 'toggled',
self.on_refresh_verbose_button_toggled, self.on_refresh_verbose_button_toggled,
) )
if not self.app_obj.refresh_output_videos_flag: if not self.app_obj.refresh_output_videos_flag:
checkbutton10.set_sensitive(False) checkbutton11.set_sensitive(False)
# Signal connect from above # Signal connect from above
checkbutton11.connect( checkbutton14.connect(
'toggled', 'toggled',
self.on_refresh_videos_button_toggled, self.on_refresh_videos_button_toggled,
checkbutton12, checkbutton15,
) )
@ -6644,6 +6824,26 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_close_to_tray_flag(False) self.app_obj.set_close_to_tray_flag(False)
def on_convert_from_button_toggled(self, radiobutton, mode):
"""Called from callback in self.setup_operations_tab().
Set what happens when downloading a media.Video object whose URL
represents a channel/playlist.
Args:
radiobutton (Gtk.RadioButton): The widget clicked
mode (str): The new value for the IV: 'disable', 'multi',
'channel' or 'playlist'
"""
if radiobutton.get_active():
self.app_obj.set_operation_convert_mode(mode)
def on_copyright_button_toggled(self, checkbutton): def on_copyright_button_toggled(self, checkbutton):
"""Called from callback in self.setup_ytdl_tab(). """Called from callback in self.setup_ytdl_tab().
@ -6978,6 +7178,29 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_operation_download_limit(int(text)) self.app_obj.set_operation_download_limit(int(text))
def on_error_button_toggled(self, checkbutton):
"""Called from callback in self.setup_windows_tab().
Enables/disables system errors in the 'Errors/Warnings' tab. Toggling
the corresponding Gtk.CheckButton in the Errors/Warnings tab sets the
IV (and makes sure the two checkbuttons have the same status).
Args:
checkbutton (Gtk.CheckButton): The widget clicked
"""
other_flag \
= self.app_obj.main_win_obj.show_error_checkbutton.get_active()
if (checkbutton.get_active() and not other_flag):
self.app_obj.main_win_obj.show_error_checkbutton.set_active(True)
elif (not checkbutton.get_active() and other_flag):
self.app_obj.main_win_obj.show_error_checkbutton.set_active(False)
def on_expand_tree_toggled(self, checkbutton): def on_expand_tree_toggled(self, checkbutton):
"""Called from callback in self.setup_general_tab(). """Called from callback in self.setup_general_tab().
@ -7249,6 +7472,26 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_ytdl_output_start_empty_flag(False) self.app_obj.set_ytdl_output_start_empty_flag(False)
def on_output_summary_button_toggled(self, checkbutton):
"""Called from a callback in self.setup_output_tab().
Enables/disables displaying a summary page in the Output Tab.
Args:
checkbutton (Gtk.CheckButton): The widget clicked
"""
if checkbutton.get_active() \
and not self.app_obj.ytdl_output_show_summary_flag:
self.app_obj.set_ytdl_output_show_summary_flag(True)
elif not checkbutton.get_active() \
and self.app_obj.ytdl_output_show_summary_flag:
self.app_obj.set_ytdl_output_show_summary_flag(False)
def on_output_stderr_button_toggled(self, checkbutton): def on_output_stderr_button_toggled(self, checkbutton):
"""Called from a callback in self.setup_ytdl_tab(). """Called from a callback in self.setup_ytdl_tab().
@ -7343,6 +7586,26 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_ytdl_output_ignore_progress_flag(False) self.app_obj.set_ytdl_output_ignore_progress_flag(False)
def on_output_system_button_toggled(self, checkbutton):
"""Called from a callback in self.setup_ytdl_tab().
Enables/disables writing youtube-dl system commands to the Output Tab.
Args:
checkbutton (Gtk.CheckButton): The widget clicked
"""
if checkbutton.get_active() \
and not self.app_obj.ytdl_output_system_cmd_flag:
self.app_obj.set_ytdl_output_system_cmd_flag(True)
elif not checkbutton.get_active() \
and self.app_obj.ytdl_output_system_cmd_flag:
self.app_obj.set_ytdl_output_system_cmd_flag(False)
def on_refresh_videos_button_toggled(self, checkbutton, checkbutton2): def on_refresh_videos_button_toggled(self, checkbutton, checkbutton2):
"""Called from a callback in self.setup_output_tab(). """Called from a callback in self.setup_output_tab().
@ -7708,6 +7971,26 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_ytdl_write_ignore_progress_flag(False) self.app_obj.set_ytdl_write_ignore_progress_flag(False)
def on_terminal_system_button_toggled(self, checkbutton):
"""Called from a callback in self.setup_ytdl_tab().
Enables/disables writing youtube-dl system commands to the terminal.
Args:
checkbutton (Gtk.CheckButton): The widget clicked
"""
if checkbutton.get_active() \
and not self.app_obj.ytdl_write_system_cmd_flag:
self.app_obj.set_ytdl_write_system_cmd_flag(True)
elif not checkbutton.get_active() \
and self.app_obj.ytdl_write_system_cmd_flag:
self.app_obj.set_ytdl_write_system_cmd_flag(False)
def on_update_combo_changed(self, combo): def on_update_combo_changed(self, combo):
"""Called from a callback in self.setup_ytdl_tab(). """Called from a callback in self.setup_ytdl_tab().
@ -7750,7 +8033,9 @@ class SystemPrefWin(GenericPrefWin):
"""Called from callback in self.setup_general_tab(). """Called from callback in self.setup_general_tab().
Enables/disables system warnings in the 'Errors/Warnings' tab. Enables/disables system warnings in the 'Errors/Warnings' tab. Toggling
the corresponding Gtk.CheckButton in the Errors/Warnings tab sets the
IV (and makes sure the two checkbuttons have the same status).
Args: Args:
@ -7758,12 +8043,15 @@ class SystemPrefWin(GenericPrefWin):
""" """
if checkbutton.get_active() \ main_win_obj = self.app_obj.main_win_obj
and not self.app_obj.system_warning_show_flag:
self.app_obj.set_system_warning_show_flag(True) other_flag \
elif not checkbutton.get_active() \ = self.app_obj.main_win_obj.show_warning_checkbutton.get_active()
and self.app_obj.system_warning_show_flag:
self.app_obj.set_system_warning_show_flag(False) if (checkbutton.get_active() and not other_flag):
main_win_obj.show_warning_checkbutton.set_active(True)
elif (not checkbutton.get_active() and other_flag):
main_win_obj.show_warning_checkbutton.set_active(False)
def on_worker_button_toggled(self, checkbutton): def on_worker_button_toggled(self, checkbutton):

View File

@ -152,6 +152,15 @@ class DownloadManager(threading.Thread):
# objects which have been allocated to a worker) # objects which have been allocated to a worker)
self.job_count = 0 self.job_count = 0
# If mainapp.TartubeApp.operation_convert_mode is set to any value
# other than 'disable', then a media.Video object whose URL
# represents a channel/playlist is converted into multiple new
# media.Video objects, one for each video actually downloaded
# The original media.Video object is added to this list, via a call to
# self.mark_video_as_doomed(). At the end of the whole download
# operation, any media.Video object in this list is destroyed
self.doomed_video_list = []
# Code # Code
# ---- # ----
@ -159,7 +168,7 @@ class DownloadManager(threading.Thread):
# Create an object for converting download options stored in # Create an object for converting download options stored in
# downloads.DownloadWorker.options_list into a list of youtube-dl # downloads.DownloadWorker.options_list into a list of youtube-dl
# command line options # command line options
self.options_parser_obj = options.OptionsParser(self) self.options_parser_obj = options.OptionsParser(self.app_obj)
# Create a list of downloads.DownloadWorker objects, each one handling # Create a list of downloads.DownloadWorker objects, each one handling
# one of several simultaneous downloads # one of several simultaneous downloads
@ -334,6 +343,16 @@ class DownloadManager(threading.Thread):
self.app_obj.main_win_obj.output_tab_update_pages, self.app_obj.main_win_obj.output_tab_update_pages,
) )
# Any media.Video objects which have been marked as doomed, can now be
# destroyed
for video_obj in self.doomed_video_list:
self.app_obj.delete_video(
video_obj,
True, # Delete any files associated with the video
True, # Don't update the Video Index yet
True, # Don't update the Video Catalogue yet
)
# When youtube-dl reports it is finished, there is a short delay before # When youtube-dl reports it is finished, there is a short delay before
# the final downloaded video(s) actually exist in the filesystem # the final downloaded video(s) actually exist in the filesystem
# Therefore, mainwin.MainWin.progress_list_display_dl_stats() may not # Therefore, mainwin.MainWin.progress_list_display_dl_stats() may not
@ -514,6 +533,32 @@ class DownloadManager(threading.Thread):
return None return None
def mark_video_as_doomed(self, video_obj):
"""Called by VideoDownloader.check_dl_is_correct_type().
When youtube-dl reports the URL associated with a download item
object contains multiple videos (or potentially contains multiple
videos), then the URL represents a channel or playlist, not a video.
If the channel/playlist was about to be downloaded into a media.Video
object, then the calling function takes action to prevent it.
It then calls this function to mark the old media.Video object to be
destroyed, once the download operation is complete.
Args:
video_obj (media.Video): The video object whose URL is not a video,
and which must be destroyed
"""
if isinstance(video_obj, media.Video) \
and not video_obj in self.doomed_video_list:
self.doomed_video_list.append(video_obj)
def remove_worker(self, worker_obj): def remove_worker(self, worker_obj):
"""Called by self.run(). """Called by self.run().
@ -779,8 +824,8 @@ class DownloadWorker(threading.Thread):
self.download_item_obj = download_item_obj self.download_item_obj = download_item_obj
self.options_manager_obj = download_item_obj.options_manager_obj self.options_manager_obj = download_item_obj.options_manager_obj
self.options_list = self.download_manager_obj.options_parser_obj.parse( self.options_list = self.download_manager_obj.options_parser_obj.parse(
download_item_obj, download_item_obj.media_data_obj,
self.options_manager_obj.options_dict, self.options_manager_obj,
) )
self.available_flag = False self.available_flag = False
@ -1006,7 +1051,10 @@ class DownloadList(object):
# (The manager might be specified by obj itself, or it might be # (The manager might be specified by obj itself, or it might be
# specified by obj's parent, or we might use the default # specified by obj's parent, or we might use the default
# options.OptionsManager) # options.OptionsManager)
options_manager_obj = self.get_options_manager(media_data_obj) options_manager_obj = utils.get_options_manager(
self.app_obj,
media_data_obj,
)
# Ignore private folders, and don't download any of their children # Ignore private folders, and don't download any of their children
# (because they are all children of some other non-private folder) # (because they are all children of some other non-private folder)
@ -1099,40 +1147,6 @@ class DownloadList(object):
return None return None
def get_options_manager(self, media_data_obj):
"""Called by self.create_item() or by this function recursively.
Fetches the options.OptionsManager which applies to the specified media
data object.
The media data object might specify its own options.OptionsManager, or
we might have to use the parent's, or the parent's parent's (and so
on). As a last resort, use General Options Manager.
Args:
obj(media.Video, media.Channel, media.Playlist, media.Folder):
A media data object
Returns:
The options.OptionsManager object that applies to the specified
media data object
"""
if DEBUG_FUNC_FLAG:
utils.debug_time('dld 1026 get_options_manager')
if media_data_obj.options_obj:
return media_data_obj.options_obj
elif media_data_obj.parent_obj:
return self.get_options_manager(media_data_obj.parent_obj)
else:
return self.app_obj.general_options_obj
@synchronise(_SYNC_LOCK) @synchronise(_SYNC_LOCK)
def move_item_to_bottom(self, download_item_obj): def move_item_to_bottom(self, download_item_obj):
@ -1441,6 +1455,16 @@ class VideoDownloader(object):
# The time to wait, in seconds # The time to wait, in seconds
self.last_sim_video_wait_time = 60 self.last_sim_video_wait_time = 60
# If mainapp.TartubeApp.operation_convert_mode is set to any value
# other than 'disable', then a media.Video object whose URL
# represents a channel/playlist is converted into multiple new
# media.Video objects, one for each video actually downloaded
# Flag set to True when self.download_item_obj.media_data_obj is a
# media.Video object, but a channel/playlist is detected (regardless
# of the value of mainapp.TartubeApp.operation_convert_mode)
self.url_is_not_video_flag = False
# Code # Code
# ---- # ----
# Initialise IVs depending on whether this is a real or simulated # Initialise IVs depending on whether this is a real or simulated
@ -1515,7 +1539,26 @@ class VideoDownloader(object):
time.sleep(self.long_sleep_time) time.sleep(self.long_sleep_time)
# Prepare a system command... # Prepare a system command...
cmd_list = self.get_system_cmd() cmd_list = utils.generate_system_cmd(
app_obj,
self.download_item_obj.media_data_obj,
self.download_worker_obj.options_list,
self.dl_sim_flag,
)
# ...display it in the Output Tab (if required)...
if app_obj.ytdl_output_system_cmd_flag:
space = ' '
app_obj.main_win_obj.output_tab_write_system_cmd(
self.download_worker_obj.worker_id,
space.join(cmd_list),
)
# ...and the terminal (if required)...
if app_obj.ytdl_write_system_cmd_flag:
space = ' '
print(space.join(cmd_list))
# ...and create a new child process using that command # ...and create a new child process using that command
self.create_child_process(cmd_list) self.create_child_process(cmd_list)
@ -1690,23 +1733,79 @@ class VideoDownloader(object):
When youtube-dl reports the URL associated with the download item When youtube-dl reports the URL associated with the download item
object contains multiple videos (or potentially contains multiple object contains multiple videos (or potentially contains multiple
videos), then the URL is a channel or playlist, not a video. videos), then the URL represents a channel or playlist, not a video.
This function checks whether a channel/playlist is about to be
downloaded into a media.Video object. If so, it takes action to prevent
that from happening.
The action taken depends on the value of
mainapp.TartubeApp.operation_convert_mode.
Return values:
False if a channel/playlist was about to be downloaded into a
media.Video object, which has since been replaced by a new
media.Channel/media.Playlist object
True in all other situations (including when a channel/playlist was
about to be downloaded into a media.Video object, which was
not replaced by a new media.Channel/media.Playlist object)
Cannot store data for a channel or playlist in a media.Video object,
so stop the child process immediately and display a system error.
""" """
if DEBUG_FUNC_FLAG: if DEBUG_FUNC_FLAG:
utils.debug_time('dld 1600 check_dl_is_correct_type') utils.debug_time('dld 1600 check_dl_is_correct_type')
app_obj = self.download_manager_obj.app_obj
media_data_obj = self.download_item_obj.media_data_obj
if isinstance(self.download_item_obj.media_data_obj, media.Video): if isinstance(self.download_item_obj.media_data_obj, media.Video):
self.stop() # If the mode is 'disable', or if it the original media.Video
self.download_item_obj.media_data_obj.set_error( # object is contained in a channel or a playlist, then we must
'The video \'' + self.download_item_obj.media_data_obj.name \ # stop downloading this URL immediately
+ '\' has a source URL that points to a channel or a' \ if app_obj.operation_convert_mode == 'disable' \
+ ' playlist, not a video', or not isinstance(
) self.download_item_obj.media_data_obj.parent_obj,
media.Folder,
):
self.url_is_not_video_flag = True
# Stop downloading this URL
self.stop()
media_data_obj.set_error(
'The video \'' + media_data_obj.name \
+ '\' has a source URL that points to a channel or a' \
+ ' playlist, not a video',
)
# Don't allow self.confirm_sim_video() to be called
return False
# Otherwise, we can create new media.Video objects for each
# video downloaded/checked. The new objects may be placd into a
# new media.Channel or media.Playlist object
elif not self.url_is_not_video_flag:
self.url_is_not_video_flag = True
# Mark the original media.Video object to be destroyed at the
# end of the download operation
self.download_manager_obj.mark_video_as_doomed(media_data_obj)
if app_obj.operation_convert_mode != 'multi':
# Create a new media.Channel or media.Playlist object and
# add it to the download manager
# Then halt this job, so the new channel/playlist object
# can be downloaded
self.convert_video_to_container()
# Don't allow self.confirm_sim_video() to be called
return False
# Do allow self.confirm_sim_video() to be called
return True
def close(self): def close(self):
@ -1749,21 +1848,37 @@ class VideoDownloader(object):
utils.debug_time('dld 1649 confirm_new_video') utils.debug_time('dld 1649 confirm_new_video')
if not self.video_num in self.video_check_dict: if not self.video_num in self.video_check_dict:
app_obj = self.download_manager_obj.app_obj
self.video_check_dict[self.video_num] = filename self.video_check_dict[self.video_num] = filename
# Create a new media.Video object for the video # Create a new media.Video object for the video
app_obj = self.download_manager_obj.app_obj if self.url_is_not_video_flag:
video_obj = app_obj.create_video_from_download(
self.download_item_obj,
dir_path,
filename,
extension,
True, # Don't sort parent containers yet
)
# If downloading from a playlist, remember the video's index in video_obj = app_obj.convert_video_from_download(
# that playlist self.download_item_obj.media_data_obj.parent_obj,
if isinstance(video_obj.parent_obj, media.Playlist): self.download_item_obj.options_manager_obj,
dir_path,
filename,
extension,
True, # Don't sort parent containers yet
)
else:
video_obj = app_obj.create_video_from_download(
self.download_item_obj,
dir_path,
filename,
extension,
True, # Don't sort parent containers yet
)
# If downloading from a channel/playlist, remember the video's
# index. (The server supplies an index even for a channel, and
# the user might want to convert a channel to a playlist)
if isinstance(video_obj.parent_obj, media.Channel) \
or isinstance(video_obj.parent_obj, media.Playlist):
video_obj.set_index(self.video_num) video_obj.set_index(self.video_num)
# Fetch the options.OptionsManager object used for this download # Fetch the options.OptionsManager object used for this download
@ -1995,9 +2110,32 @@ class VideoDownloader(object):
# Does an existing media.Video object match this video? # Does an existing media.Video object match this video?
media_data_obj = self.download_item_obj.media_data_obj media_data_obj = self.download_item_obj.media_data_obj
video_obj = None video_obj = None
if isinstance(media_data_obj, media.Video):
if self.url_is_not_video_flag:
# media_data_obj has a URL which represents a channel or playlist,
# but media_data_obj itself is a media.Video object
# media_data_obj's parent is a media.Folder object. Check its
# child objects, looking for a matching video
# (video_obj is set to None, if no match is found)
video_obj = media_data_obj.parent_obj.find_matching_video(
app_obj,
filename,
)
if not video_obj:
video_obj = media_data_obj.parent_obj.find_matching_video(
app_obj,
name,
)
elif isinstance(media_data_obj, media.Video):
# media_data_obj is a media.Video object
video_obj = media_data_obj video_obj = media_data_obj
else: else:
# media_data_obj is a media.Channel or media.Playlist object. Check # media_data_obj is a media.Channel or media.Playlist object. Check
# its child objects, looking for a matching video # its child objects, looking for a matching video
# (video_obj is set to None, if no match is found) # (video_obj is set to None, if no match is found)
@ -2011,15 +2149,28 @@ class VideoDownloader(object):
# No matching media.Video object found, so create a new one # No matching media.Video object found, so create a new one
new_flag = True new_flag = True
video_obj = app_obj.create_video_from_download( if self.url_is_not_video_flag:
self.download_item_obj,
path, video_obj = app_obj.convert_video_from_download(
filename, self.download_item_obj.media_data_obj.parent_obj,
extension, self.download_item_obj.options_manager_obj,
# Don't sort parent container objects yet; wait for path,
# mainwin.MainWin.results_list_update_row() to do it filename,
True, extension,
) # Don't sort parent container objects yet; wait for
# mainwin.MainWin.results_list_update_row() to do it
True,
)
else:
video_obj = app_obj.create_video_from_download(
self.download_item_obj,
path,
filename,
extension,
True,
)
# Update its IVs with the JSON information we extracted # Update its IVs with the JSON information we extracted
if filename is not None: if filename is not None:
@ -2045,10 +2196,11 @@ class VideoDownloader(object):
app_obj.main_win_obj.descrip_line_max_len, app_obj.main_win_obj.descrip_line_max_len,
) )
# Only save the playlist index when this video is actually stored # If downloading from a channel/playlist, remember the video's
# inside a media.Playlist object # index. (The server supplies an index even for a channel, and
if isinstance(video_obj.parent_obj, media.Playlist) \ # the user might want to convert a channel to a playlist)
and playlist_index is not None: if isinstance(video_obj.parent_obj, media.Channel) \
or isinstance(video_obj.parent_obj, media.Playlist):
video_obj.set_index(playlist_index) video_obj.set_index(playlist_index)
# Now we can sort the parent containers # Now we can sort the parent containers
@ -2113,11 +2265,11 @@ class VideoDownloader(object):
app_obj.main_win_obj.descrip_line_max_len, app_obj.main_win_obj.descrip_line_max_len,
) )
# Only save the playlist index when this video is actually stored # If downloading from a channel/playlist, remember the video's
# inside a media.Playlist object # index. (The server supplies an index even for a channel, and
if not video_obj.index \ # the user might want to convert a channel to a playlist)
and isinstance(video_obj.parent_obj, media.Playlist) \ if isinstance(video_obj.parent_obj, media.Channel) \
and playlist_index is not None: or isinstance(video_obj.parent_obj, media.Playlist):
video_obj.set_index(playlist_index) video_obj.set_index(playlist_index)
# Deal with the video description, JSON data and thumbnail, according # Deal with the video description, JSON data and thumbnail, according
@ -2248,6 +2400,104 @@ class VideoDownloader(object):
self.stop_now_flag = True self.stop_now_flag = True
def convert_video_to_container (self):
"""Called by self.check_dl_is_correct_type().
Creates a new media.Channel or media.Playlist object to replace an
existing media.Video object. The new object is given some of the
properties of the old one.
This function doesn't destroy the old object; DownloadManager.run()
handles that.
"""
app_obj = self.download_manager_obj.app_obj
old_video_obj = self.download_item_obj.media_data_obj
container_obj = old_video_obj.parent_obj
# Some media.Folder objects cannot contain channels or playlists (for
# example, the 'Unsorted Videos' folder)
# If that is the case, the new channel/playlist is created without a
# parent. Otherwise, it is created at the same location as the
# original media.Video object
if container_obj.restrict_flag:
container_obj = None
# Decide on a name for the new channel/playlist, e.g. 'channel_1' or
# 'playlist_4'. The name must not already be in use. The user can
# customise the name when they're ready
# (Prevent any possibility of an infinite loop by giving up after
# thousands of attempts)
name = None
new_container_obj = None
for n in range (1, 9999):
test_name = app_obj.operation_convert_mode + '_' + str(n)
if not test_name in app_obj.media_name_dict:
name = test_name
break
if name is not None:
# Create the new channel/playlist. Very unlikely that the old
# media.Video object has its .dl_sim_flag set, but we'll use it
# nonetheless
if app_obj.operation_convert_mode == 'channel':
new_container_obj = app_obj.add_channel(
name,
container_obj, # May be None
source = old_video_obj.source,
dl_sim_flag = old_video_obj.dl_sim_flag,
)
else:
new_container_obj = app_obj.add_playlist(
name,
container_obj, # May be None
source = old_video_obj.source,
dl_sim_flag = old_video_obj.dl_sim_flag,
)
if new_container_obj is None:
# New channel/playlist could not be created (for some reason), so
# stop downloading from this URL
self.stop()
media_data_obj.set_error(
'The video \'' + media_data_obj.name \
+ '\' has a source URL that points to a channel or a' \
+ ' playlist, not a video',
)
else:
# Update IVs for the new channel/playlist object
new_container_obj.set_options_obj(old_video_obj.options_obj)
new_container_obj.set_source(old_video_obj.source)
# Add the new channel/playlist to the Video Index (but don't
# select it)
app_obj.main_win_obj.video_index_add_row(new_container_obj, True)
# Add the new channel/playlist to the download manager's list of
# things to download...
new_download_item_obj \
= self.download_manager_obj.download_list_obj.create_item(
new_container_obj,
)
# ...and add a row the Progress List
app_obj.main_win_obj.progress_list_add_row(
new_download_item_obj.item_id,
new_download_item_obj.media_data_obj,
)
# Stop this download job, allowing the replacement one to start
self.stop()
def create_child_process(self, cmd_list): def create_child_process(self, cmd_list):
"""Called by self.do_download() immediately after the call to """Called by self.do_download() immediately after the call to
@ -2453,8 +2703,8 @@ class VideoDownloader(object):
dl_stat_dict['playlist_size'] = stdout_list[5] dl_stat_dict['playlist_size'] = stdout_list[5]
self.video_total = stdout_list[5] self.video_total = stdout_list[5]
# If downloading an individual video, rather than a channel or # If youtube-dl is about to download a channel or playlist into
# a playlist, stop the download immediately # a media.Video object, decide what to do to prevent it
self.check_dl_is_correct_type() self.check_dl_is_correct_type()
# Remove the 'and merged' part of the STDOUT message when using # Remove the 'and merged' part of the STDOUT message when using
@ -2565,15 +2815,26 @@ class VideoDownloader(object):
'Invalid JSON data received from server', 'Invalid JSON data received from server',
) )
# (JSON is valid) if json_dict:
self.confirm_sim_video(json_dict)
self.video_num += 1 # If youtube-dl is about to download a channel or playlist
dl_stat_dict['playlist_index'] = self.video_num # into a media.Video object, decide what to do to prevent
self.video_total += 1 # The called function returns a True/False value,
dl_stat_dict['playlist_size'] = self.video_total # specifically to allow this code block to call
# self.confirm_sim_video when required
# v1.3.063 At this poitn, self.video_num can be None or 0
# for a URL that's an individual video, but > 0 for a URL
# that's actually a channel/playlist
if not self.video_num \
or self.check_dl_is_correct_type():
self.confirm_sim_video(json_dict)
dl_stat_dict['status'] = formats.ACTIVE_STAGE_CHECKING self.video_num += 1
dl_stat_dict['playlist_index'] = self.video_num
self.video_total += 1
dl_stat_dict['playlist_size'] = self.video_total
dl_stat_dict['status'] = formats.ACTIVE_STAGE_CHECKING
elif stdout_list[0][0] != '[' or stdout_list[0] == '[debug]': elif stdout_list[0][0] != '[' or stdout_list[0] == '[debug]':
@ -2621,68 +2882,6 @@ class VideoDownloader(object):
dl_stat_dict['status'] = None dl_stat_dict['status'] = None
def get_system_cmd(self):
"""Called by self.do_download().
Based on YoutubeDLDownloader._get_cmd().
Prepare the system command that creates the child process, executing
youtube-dl.
Returns:
Python list that contains the system command to execute.
"""
if DEBUG_FUNC_FLAG:
utils.debug_time('dld 2540 get_system_cmd')
# Import things for convenience
app_obj = self.download_manager_obj.app_obj
media_data_obj = self.download_item_obj.media_data_obj
options_list = self.download_worker_obj.options_list
# Simulate the download, rather than actually downloading videos, if
# required
if self.dl_sim_flag:
options_list.append('--dump-json')
# If actually downloading videos, create an archive file so that, if
# the user deletes the videos, youtube-dl won't try to download them
# again
elif app_obj.allow_ytdl_archive_flag:
# (Create the archive file in the media data object's own
# sub-directory, not the alternative download destination, as
# this helps youtube-dl to work the way we want it)
if isinstance(media_data_obj, media.Video):
dl_path = media_data_obj.parent_obj.get_dir(app_obj)
else:
dl_path = media_data_obj.get_dir(app_obj)
options_list.append('--download-archive')
options_list.append(
os.path.abspath(os.path.join(dl_path, 'ytdl-archive.txt')),
)
# Show verbose output (youtube-dl debugging mode), if required
if app_obj.ytdl_write_verbose_flag:
options_list.append('--verbose')
# Supply youtube-dl with the path to the ffmpeg/avconv binary, if the
# user has provided one
if app_obj.ffmpeg_path is not None:
options_list.append('--ffmpeg-location')
options_list.append('"' + app_obj.ffmpeg_path + '"')
# Set the list
cmd_list = [app_obj.ytdl_path] + options_list + [media_data_obj.source]
return cmd_list
def is_child_process_alive(self): def is_child_process_alive(self):
"""Called by self.do_download() and self.stop(). """Called by self.do_download() and self.stop().

View File

@ -28,7 +28,6 @@ from gi.repository import Gtk, GObject, GdkPixbuf
# Import Python standard modules # Import Python standard modules
from gi.repository import Gio from gi.repository import Gio
import cgi
import datetime import datetime
import json import json
import math import math
@ -255,8 +254,14 @@ class TartubeApp(Gtk.Application):
# the most recently checked or downloaded video appears at the top # the most recently checked or downloaded video appears at the top
# of the list) # of the list)
self.results_list_reverse_flag = False self.results_list_reverse_flag = False
# Flag set to True if system warning messages should be shown (system # Flag set to True if system error messages should be shown in the
# error messages are always shown) # Errors/Warnings tab
# NB The check is applied by self.system_error(); any part of the
# code could call mainwin.MainWin.errors_list_add_system_warning()
# directly, which would bypass this flag
self.system_error_show_flag = True
# Flag set to True if system warning messages should be shown in the
# Errors/Warnings tab
# NB The check is applied by self.system_warning(); any part of the # NB The check is applied by self.system_warning(); any part of the
# code could call mainwin.MainWin.errors_list_add_system_warning() # code could call mainwin.MainWin.errors_list_add_system_warning()
# directly, which would bypass this flag # directly, which would bypass this flag
@ -388,8 +393,11 @@ class TartubeApp(Gtk.Application):
# self.ytdl_update_dict, set by self.start() # self.ytdl_update_dict, set by self.start()
self.ytdl_update_current = None self.ytdl_update_current = None
# Flag set to True if output youtube-dl's STDOUT should be displayed in # Flag set to True if youtube-dl system commands should be displayed in
# the Output Tab # the Output Tab
self.ytdl_output_system_cmd_flag = True
# Flag set to True if youtube-dl's STDOUT should be displayed in the
# Output Tab
self.ytdl_output_stdout_flag = True self.ytdl_output_stdout_flag = True
# Flag set to True if we should ignore JSON output when displaying text # Flag set to True if we should ignore JSON output when displaying text
# in the Output Tab (ignored if self.ytdl_output_stdout_flag is # in the Output Tab (ignored if self.ytdl_output_stdout_flag is
@ -405,9 +413,15 @@ class TartubeApp(Gtk.Application):
# Flag set to True if pages in the Output Tab should be emptied at the # Flag set to True if pages in the Output Tab should be emptied at the
# start of each operation # start of each operation
self.ytdl_output_start_empty_flag = True self.ytdl_output_start_empty_flag = True
# Flag set to True if a summary page should be visible in the Output
# Tab. Changes to this flag are applied when Tartube restarts
self.ytdl_output_show_summary_flag = False
# Flag set to True if output youtube-dl's STDOUT should be written to # Flag set to True if youtube-dl system commands should be written to
# the terminal window # the terminal window
self.ytdl_write_system_cmd_flag = False
# Flag set to True if youtube-dl's STDOUT should be written to the
# terminal window
self.ytdl_write_stdout_flag = False self.ytdl_write_stdout_flag = False
# Flag set to True if we should ignore JSON output when writing to the # Flag set to True if we should ignore JSON output when writing to the
# terminal window (ignored if self.ytdl_write_stdout_flag is False) # terminal window (ignored if self.ytdl_write_stdout_flag is False)
@ -704,6 +718,25 @@ class TartubeApp(Gtk.Application):
# desktop notification, or 'default' to do neither # desktop notification, or 'default' to do neither
# NB Desktop notifications don't work on MS Windows # NB Desktop notifications don't work on MS Windows
self.operation_dialogue_mode = 'dialogue' self.operation_dialogue_mode = 'dialogue'
# What to do when the user creates a media.Video object whose URL
# represents a channel or playlist
# 'channel' to create a new media.Channel object, and place all the
# downloaded videos inside it (the original media.Video object is
# destroyed)
# 'playlist' to create a new media.Playlist object, and place all the
# downloaded videos inside it (the original media.Video object is
# destroyed)
# 'multi' to create a new media.Video object for each downloaded video,
# placed in the same folder as the original media.Video object (the
# original is destroyed)
# 'disable' to download nothing from the URL
# There are some restrictions. If the original media.Video object is
# contained in a folder whose .restrict_flag is False, and if the
# mode is 'channel' or 'playlist', then the new channel/playlist is
# not created in that folder. If the original media.Video object is
# contained in a channel or playlist, all modes to default to
# 'disable'
self.operation_convert_mode = 'channel'
# Flag set to True if self.update_video_from_filesystem() should get # Flag set to True if self.update_video_from_filesystem() should get
# the video duration, if not already known, using the moviepy.editor # the video duration, if not already known, using the moviepy.editor
# module (an optional dependency) # module (an optional dependency)
@ -1687,8 +1720,8 @@ class TartubeApp(Gtk.Application):
Error codes for this function and for self.system_warning are Error codes for this function and for self.system_warning are
currently assigned thus: currently assigned thus:
100-199: mainapp.py (in use: 101-134) 100-199: mainapp.py (in use: 101-135)
200-299: mainwin.py (in use: 201-239) 200-299: mainwin.py (in use: 201-240)
300-399: downloads.py (in use: 301-304) 300-399: downloads.py (in use: 301-304)
400-499: config.py (in use: 401-404) 400-499: config.py (in use: 401-404)
@ -1697,7 +1730,7 @@ class TartubeApp(Gtk.Application):
if DEBUG_FUNC_FLAG: if DEBUG_FUNC_FLAG:
utils.debug_time('app 1696 system_error') utils.debug_time('app 1696 system_error')
if self.main_win_obj: if self.main_win_obj and self.system_error_show_flag:
self.main_win_obj.errors_list_add_system_error(error_code, msg) self.main_win_obj.errors_list_add_system_error(error_code, msg)
else: else:
# Emergency fallback: display in the terminal window # Emergency fallback: display in the terminal window
@ -1834,6 +1867,9 @@ class TartubeApp(Gtk.Application):
if version >= 1000029: # v1.0.029 if version >= 1000029: # v1.0.029
self.results_list_reverse_flag \ self.results_list_reverse_flag \
= json_dict['results_list_reverse_flag'] = json_dict['results_list_reverse_flag']
if version >= 1003069: # v1.3.069
self.system_error_show_flag \
= json_dict['system_error_show_flag']
if version >= 6006: # v0.6.006 if version >= 6006: # v0.6.006
self.system_warning_show_flag \ self.system_warning_show_flag \
= json_dict['system_warning_show_flag'] = json_dict['system_warning_show_flag']
@ -1864,6 +1900,9 @@ class TartubeApp(Gtk.Application):
self.ytdl_update_list = json_dict['ytdl_update_list'] self.ytdl_update_list = json_dict['ytdl_update_list']
self.ytdl_update_current = json_dict['ytdl_update_current'] self.ytdl_update_current = json_dict['ytdl_update_current']
if version >= 1003074: # v1.3.074
self.ytdl_output_system_cmd_flag \
= json_dict['ytdl_output_system_cmd_flag']
if version >= 1002030: # v1.2.030 if version >= 1002030: # v1.2.030
self.ytdl_output_stdout_flag = json_dict['ytdl_output_stdout_flag'] self.ytdl_output_stdout_flag = json_dict['ytdl_output_stdout_flag']
self.ytdl_output_ignore_json_flag \ self.ytdl_output_ignore_json_flag \
@ -1873,7 +1912,13 @@ class TartubeApp(Gtk.Application):
self.ytdl_output_stderr_flag = json_dict['ytdl_output_stderr_flag'] self.ytdl_output_stderr_flag = json_dict['ytdl_output_stderr_flag']
self.ytdl_output_start_empty_flag \ self.ytdl_output_start_empty_flag \
= json_dict['ytdl_output_start_empty_flag'] = json_dict['ytdl_output_start_empty_flag']
if version >= 1003064: # v1.3.064
self.ytdl_output_show_summary_flag \
= json_dict['ytdl_output_show_summary_flag']
if version >= 1003074: # v1.3.074
self.ytdl_write_system_cmd_flag \
= json_dict['ytdl_write_system_cmd_flag']
self.ytdl_write_stdout_flag = json_dict['ytdl_write_stdout_flag'] self.ytdl_write_stdout_flag = json_dict['ytdl_write_stdout_flag']
if version >= 5004: # v0.5.004 if version >= 5004: # v0.5.004
self.ytdl_write_ignore_json_flag \ self.ytdl_write_ignore_json_flag \
@ -1932,6 +1977,8 @@ class TartubeApp(Gtk.Application):
# self.operation_dialogue_flag = json_dict['operation_dialogue_flag'] # self.operation_dialogue_flag = json_dict['operation_dialogue_flag']
if version >= 1003028: # v1.3.028 if version >= 1003028: # v1.3.028
self.operation_dialogue_mode = json_dict['operation_dialogue_mode'] self.operation_dialogue_mode = json_dict['operation_dialogue_mode']
if version >= 1003060: # v1.3.060
self.operation_convert_mode = json_dict['operation_convert_mode']
self.use_module_moviepy_flag = json_dict['use_module_moviepy_flag'] self.use_module_moviepy_flag = json_dict['use_module_moviepy_flag']
# # Removed v0.5.003 # # Removed v0.5.003
@ -2085,6 +2132,7 @@ class TartubeApp(Gtk.Application):
'close_to_tray_flag': self.close_to_tray_flag, 'close_to_tray_flag': self.close_to_tray_flag,
'results_list_reverse_flag': self.results_list_reverse_flag, 'results_list_reverse_flag': self.results_list_reverse_flag,
'system_error_show_flag': self.system_error_show_flag,
'system_warning_show_flag': self.system_warning_show_flag, 'system_warning_show_flag': self.system_warning_show_flag,
'system_msg_keep_totals_flag': self.system_msg_keep_totals_flag, 'system_msg_keep_totals_flag': self.system_msg_keep_totals_flag,
@ -2099,13 +2147,17 @@ class TartubeApp(Gtk.Application):
'ytdl_update_list': self.ytdl_update_list, 'ytdl_update_list': self.ytdl_update_list,
'ytdl_update_current': self.ytdl_update_current, 'ytdl_update_current': self.ytdl_update_current,
'ytdl_output_system_cmd_flag': self.ytdl_output_system_cmd_flag,
'ytdl_output_stdout_flag': self.ytdl_output_stdout_flag, 'ytdl_output_stdout_flag': self.ytdl_output_stdout_flag,
'ytdl_output_ignore_json_flag': self.ytdl_output_ignore_json_flag, 'ytdl_output_ignore_json_flag': self.ytdl_output_ignore_json_flag,
'ytdl_output_ignore_progress_flag': \ 'ytdl_output_ignore_progress_flag': \
self.ytdl_output_ignore_progress_flag, self.ytdl_output_ignore_progress_flag,
'ytdl_output_stderr_flag': self.ytdl_output_stderr_flag, 'ytdl_output_stderr_flag': self.ytdl_output_stderr_flag,
'ytdl_output_start_empty_flag': self.ytdl_output_start_empty_flag, 'ytdl_output_start_empty_flag': self.ytdl_output_start_empty_flag,
'ytdl_output_show_summary_flag': \
self.ytdl_output_show_summary_flag,
'ytdl_write_system_cmd_flag': self.ytdl_write_system_cmd_flag,
'ytdl_write_stdout_flag': self.ytdl_write_stdout_flag, 'ytdl_write_stdout_flag': self.ytdl_write_stdout_flag,
'ytdl_write_ignore_json_flag': self.ytdl_write_ignore_json_flag, 'ytdl_write_ignore_json_flag': self.ytdl_write_ignore_json_flag,
'ytdl_write_ignore_progress_flag': \ 'ytdl_write_ignore_progress_flag': \
@ -2144,6 +2196,7 @@ class TartubeApp(Gtk.Application):
'operation_auto_update_flag': self.operation_auto_update_flag, 'operation_auto_update_flag': self.operation_auto_update_flag,
'operation_save_flag': self.operation_save_flag, 'operation_save_flag': self.operation_save_flag,
'operation_dialogue_mode': self.operation_dialogue_mode, 'operation_dialogue_mode': self.operation_dialogue_mode,
'operation_convert_mode': self.operation_convert_mode,
'use_module_moviepy_flag': self.use_module_moviepy_flag, 'use_module_moviepy_flag': self.use_module_moviepy_flag,
'dialogue_copy_clipboard_flag': self.dialogue_copy_clipboard_flag, 'dialogue_copy_clipboard_flag': self.dialogue_copy_clipboard_flag,
@ -2814,7 +2867,7 @@ class TartubeApp(Gtk.Application):
os.remove(daily_bu_path) os.remove(daily_bu_path)
shutil.move(temp_bu_path, daily_bu_path) shutil.move(temp_bu_path, daily_bu_path)
else: else:
os.remove(temp_bu_path) os.remove(temp_bu_path)
@ -3126,6 +3179,7 @@ class TartubeApp(Gtk.Application):
self.fixed_all_folder = self.add_folder( self.fixed_all_folder = self.add_folder(
'All Videos', 'All Videos',
None, # No parent folder None, # No parent folder
False, # Allow downloads
True, # Fixed (folder cannot be removed) True, # Fixed (folder cannot be removed)
True, # Private True, # Private
True, # Can only contain videos True, # Can only contain videos
@ -3135,6 +3189,7 @@ class TartubeApp(Gtk.Application):
self.fixed_fav_folder = self.add_folder( self.fixed_fav_folder = self.add_folder(
'Favourite Videos', 'Favourite Videos',
None, # No parent folder None, # No parent folder
False, # Allow downloads
True, # Fixed (folder cannot be removed) True, # Fixed (folder cannot be removed)
True, # Private True, # Private
True, # Can only contain videos True, # Can only contain videos
@ -3145,6 +3200,7 @@ class TartubeApp(Gtk.Application):
self.fixed_new_folder = self.add_folder( self.fixed_new_folder = self.add_folder(
'New Videos', 'New Videos',
None, # No parent folder None, # No parent folder
False, # Allow downloads
True, # Fixed (folder cannot be removed) True, # Fixed (folder cannot be removed)
True, # Private True, # Private
True, # Can only contain videos True, # Can only contain videos
@ -3154,6 +3210,7 @@ class TartubeApp(Gtk.Application):
self.fixed_temp_folder = self.add_folder( self.fixed_temp_folder = self.add_folder(
'Temporary Videos', 'Temporary Videos',
None, # No parent folder None, # No parent folder
False, # Allow downloads
True, # Fixed (folder cannot be removed) True, # Fixed (folder cannot be removed)
False, # Public False, # Public
False, # Can contain any media data object False, # Can contain any media data object
@ -3163,6 +3220,7 @@ class TartubeApp(Gtk.Application):
self.fixed_misc_folder = self.add_folder( self.fixed_misc_folder = self.add_folder(
'Unsorted Videos', 'Unsorted Videos',
None, # No parent folder None, # No parent folder
False, # Allow downloads
True, # Fixed (folder cannot be removed) True, # Fixed (folder cannot be removed)
False, # Public False, # Public
True, # Can only contain videos True, # Can only contain videos
@ -4131,7 +4189,6 @@ class TartubeApp(Gtk.Application):
# (Download operation support functions) # (Download operation support functions)
def create_video_from_download(self, download_item_obj, dir_path, \ def create_video_from_download(self, download_item_obj, dir_path, \
filename, extension, no_sort_flag=False): filename, extension, no_sort_flag=False):
@ -4221,6 +4278,7 @@ class TartubeApp(Gtk.Application):
video_obj = self.add_video( video_obj = self.add_video(
other_parent_obj, other_parent_obj,
None, None,
False,
no_sort_flag, no_sort_flag,
) )
@ -4228,6 +4286,7 @@ class TartubeApp(Gtk.Application):
video_obj = self.add_video( video_obj = self.add_video(
media_data_obj, media_data_obj,
None, None,
False,
no_sort_flag, no_sort_flag,
) )
@ -4248,6 +4307,99 @@ class TartubeApp(Gtk.Application):
return video_obj return video_obj
def convert_video_from_download(self, container_obj, options_manager_obj,
dir_path, filename, extension, no_sort_flag=False):
"""Called downloads.VideoDownloader.confirm_new_video() and
.confirm_sim_video().
A modified version of self.create_video_from_download, called when
youtube-dl is about to download a channel or playlist into a
media.Video object.
Args:
container_obj (media.Folder): The folder into which a replacement
media.Video object is to be created
options_manager_obj (options.OptionsManager): The download options
for this media data object
dir_path (string): The full path to the directory in which the
video is saved, e.g. '/home/yourname/tartube/downloads/Videos'
filename (string): The video's filename, e.g. 'My Video'
extension (string): The video's extension, e.g. '.mp4'
no_sort_flag (True or False): True when called by
downloads.VideoDownloader.confirm_sim_video(), because the
video's parent containers (including the 'All Videos' folder)
should delay sorting their lists of child objects until that
calling function is ready. False when called by anything else
Returns:
video_obj (media.Video) - The video object created
"""
if DEBUG_FUNC_FLAG:
utils.debug_time('app 4166 convert_video_from_download')
# Does the container object already contain this video?
video_obj = None
for child_obj in container_obj.child_list:
child_file_dir = None
if child_obj.file_dir is not None:
child_file_dir = os.path.abspath(
os.path.join(
self.downloads_dir,
child_obj.file_dir,
),
)
if isinstance(child_obj, media.Video) \
and child_file_dir \
and child_file_dir == dir_path \
and child_obj.file_name \
and child_obj.file_name == filename:
video_obj = child_obj
if video_obj is None:
# Create a new media data object for the video
override_name \
= options_manager_obj.options_dict['use_fixed_folder']
if override_name is not None \
and override_name in self.media_name_dict:
other_dbid = self.media_name_dict[override_name]
other_container_obj = self.media_reg_dict[other_dbid]
video_obj = self.add_video(
other_container_obj,
None,
False,
no_sort_flag,
)
else:
video_obj = self.add_video(
container_obj,
None,
False,
no_sort_flag,
)
# Since we have them to hand, set the video's file path IVs
# immediately
video_obj.set_file(filename, extension)
return video_obj
def announce_video_download(self, download_item_obj, video_obj, \ def announce_video_download(self, download_item_obj, video_obj, \
keep_description=None, keep_info=None, keep_annotations=None, keep_description=None, keep_info=None, keep_annotations=None,
keep_thumbnail=None): keep_thumbnail=None):
@ -4731,7 +4883,8 @@ class TartubeApp(Gtk.Application):
# (Add media data objects) # (Add media data objects)
def add_video(self, parent_obj, source=None, no_sort_flag=False): def add_video(self, parent_obj, source=None, dl_sim_flag=False,
no_sort_flag=False):
"""Can be called by anything. Mostly called by """Can be called by anything. Mostly called by
self.create_video_from_download() and self.on_menu_add_video(). self.create_video_from_download() and self.on_menu_add_video().
@ -4746,7 +4899,10 @@ class TartubeApp(Gtk.Application):
source (string): The video's source URL, if known source (string): The video's source URL, if known
no_sort_flag (True or False): True when dl_sim_flag (bool): If True, the video object's .dl_sim_flag IV is
set to True, which forces simulated downloads
no_sort_flag (bool): True when
self.create_video_from_download() is called by self.create_video_from_download() is called by
downloads.VideoDownloader.confirm_sim_video(), because the downloads.VideoDownloader.confirm_sim_video(), because the
video's parent containers (including the 'All Videos' folder) video's parent containers (including the 'All Videos' folder)
@ -4789,6 +4945,9 @@ class TartubeApp(Gtk.Application):
if source is not None: if source is not None:
video_obj.set_source(source) video_obj.set_source(source)
if dl_sim_flag:
video_obj.set_dl_sim_flag(True)
# Update IVs # Update IVs
self.media_reg_count += 1 self.media_reg_count += 1
self.media_reg_dict[video_obj.dbid] = video_obj self.media_reg_dict[video_obj.dbid] = video_obj
@ -4974,8 +5133,8 @@ class TartubeApp(Gtk.Application):
return playlist_obj return playlist_obj
def add_folder(self, name, parent_obj=None, fixed_flag=False, \ def add_folder(self, name, parent_obj=None, dl_sim_flag=False,
priv_flag=False, restrict_flag=False, temp_flag=False): fixed_flag=False, priv_flag=False, restrict_flag=False, temp_flag=False):
"""Can be called by anything. Mostly called by """Can be called by anything. Mostly called by
self.on_menu_add_folder(). self.on_menu_add_folder().
@ -4989,8 +5148,12 @@ class TartubeApp(Gtk.Application):
parent_obj (media.Folder): The media data object for which the new parent_obj (media.Folder): The media data object for which the new
media.Channel object is a child (if any) media.Channel object is a child (if any)
fixed_flag, priv_flag, restrict_flag, temp_flag (True, False): dl_sim_flag (bool): If True, the folders .dl_sim_flag IV is set to
flags sent to the object's .__init__() function True, which forces simulated downloads for any videos,
channels or playlists contained in the folder
fixed_flag, priv_flag, restrict_flag, temp_flag (bool): Flags sent
to the object's .__init__() function
Returns: Returns:
@ -5033,6 +5196,9 @@ class TartubeApp(Gtk.Application):
temp_flag, temp_flag,
) )
if dl_sim_flag:
folder_obj.set_dl_sim_flag(True)
# Update IVs # Update IVs
self.media_reg_count += 1 self.media_reg_count += 1
self.media_reg_dict[folder_obj.dbid] = folder_obj self.media_reg_dict[folder_obj.dbid] = folder_obj
@ -5369,6 +5535,96 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.video_index_select_row(source_obj) self.main_win_obj.video_index_select_row(source_obj)
# (Convert channels to playlists, and vice-versa)
def convert_remote_container(self, old_obj):
"""Called by mainwin.MainWin.on_video_index_convert_container().
Converts a media.Channel object into a media.Playlist object, or vice-
versa.
Usually called after the user has copy-pasted a list of URLs into the
mainwin.AddVideoDialogue window, some of which actually represent
channels or playlists, not individual videos. During the next
download operation, new channels or playlists can be automatically
created (depending on the value of self.operation_convert_mode
The user can then convert a channel to a playlist, and back again, as
required.
Args:
old_obj (media.Channel, media.Playlist): The media data object to
convert
"""
if DEBUG_FUNC_FLAG:
utils.debug_time('app 5392 delete_video')
if (
not isinstance(old_obj, media.Channel) \
and not isinstance(old_obj, media.Playlist)
) or self.current_manager_obj:
return self.system_error(
135,
'Convert container request failed sanity check',
)
# If old_obj is a media.Channel, create a playlist. If old_obj is
# a media.Playlist, create a channel
if isinstance(old_obj, media.Channel):
new_obj = self.add_playlist(
old_obj.name,
old_obj.parent_obj,
old_obj.source,
old_obj.dl_sim_flag,
)
elif isinstance(old_obj, media.Playlist):
new_obj = self.add_channel(
old_obj.name,
old_obj.parent_obj,
old_obj.source,
old_obj.dl_sim_flag,
)
# Move any children from the old object to the new one
for child_obj in old_obj.child_list:
# The True argument means to delay sorting the child list
new_obj.add_child(child_obj, True)
child_obj.set_parent_obj(new_obj)
# Deal with alternative download destinations
if old_obj.master_dbid:
new_obj.set_master_dbid(self, old_obj.master_dbid)
master_obj = self.media_reg_dict[old_obj.master_dbid]
master_obj.del_slave_dbid(old_obj.dbid)
for slave_dbid in old_obj.slave_dbid_list:
slave_obj = self.media_reg_dict[slave_dbid]
slave_obj.set_master_dbid(self, new_obj.dbid)
# Copy remaining properties from the old object to the new one
new_obj.clone_properties(old_obj)
# Remove the old object from the media data registry.
# self.media_name_dict should already be updated
del self.media_reg_dict[old_obj.dbid]
if old_obj.dbid in self.media_top_level_list:
self.media_top_level_list.remove(old_obj.dbid)
# Remove the old object from the Video Index...
self.main_win_obj.video_index_delete_row(old_obj)
# ...and add the new one, selecting it at the same time
self.main_win_obj.video_index_add_row(new_obj)
# (Delete media data objects) # (Delete media data objects)
@ -7282,7 +7538,7 @@ class TartubeApp(Gtk.Application):
) )
else: else:
utils.open_file(cgi.escape(path, quote=True)) utils.open_file(path)
def download_watch_videos(self, video_list, watch_flag=True): def download_watch_videos(self, video_list, watch_flag=True):
@ -8155,6 +8411,7 @@ class TartubeApp(Gtk.Application):
# Retrieve user choices from the dialogue window... # Retrieve user choices from the dialogue window...
name = dialogue_win.entry.get_text() name = dialogue_win.entry.get_text()
dl_sim_flag = dialogue_win.button2.get_active()
# ...and find the name of the parent media data object (a # ...and find the name of the parent media data object (a
# media.Folder), if one was specified... # media.Folder), if one was specified...
@ -8197,7 +8454,7 @@ class TartubeApp(Gtk.Application):
parent_obj = self.media_reg_dict[dbid] parent_obj = self.media_reg_dict[dbid]
# Create the new folder # Create the new folder
folder_obj = self.add_folder(name, parent_obj) folder_obj = self.add_folder(name, parent_obj, dl_sim_flag)
# Add the folder to the Video Index # Add the folder to the Video Index
if folder_obj: if folder_obj:
@ -8379,6 +8636,8 @@ class TartubeApp(Gtk.Application):
False, False,
) )
dl_sim_flag = dialogue_win.button2.get_active()
# ...and find the parent media data object (a media.Channel, # ...and find the parent media data object (a media.Channel,
# media.Playlist or media.Folder)... # media.Playlist or media.Folder)...
parent_name = self.fixed_misc_folder.name parent_name = self.fixed_misc_folder.name
@ -8412,7 +8671,7 @@ class TartubeApp(Gtk.Application):
if parent_obj.check_duplicate_video(line): if parent_obj.check_duplicate_video(line):
duplicate_list.append(line) duplicate_list.append(line)
else: else:
self.add_video(parent_obj, line) self.add_video(parent_obj, line, dl_sim_flag)
# In the Video Index, select the parent media data object, which # In the Video Index, select the parent media data object, which
# updates both the Video Index and the Video Catalogue # updates both the Video Index and the Video Catalogue
@ -9246,10 +9505,20 @@ class TartubeApp(Gtk.Application):
self.operation_check_limit = value self.operation_check_limit = value
def set_operation_convert_mode(self, mode):
if DEBUG_FUNC_FLAG:
utils.debug_time('app 9220 set_operation_convert_mode')
if mode == 'disable' or mode == 'multi' or mode == 'channel' \
or mode == 'playlist':
self.operation_convert_mode = mode
def set_operation_dialogue_mode(self, mode): def set_operation_dialogue_mode(self, mode):
if DEBUG_FUNC_FLAG: if DEBUG_FUNC_FLAG:
utils.debug_time('app 9220 set_operation_dialogue_mode') utils.debug_time('app 9221 set_operation_dialogue_mode')
if mode == 'default' or mode == 'desktop' or mode == 'dialogue': if mode == 'default' or mode == 'desktop' or mode == 'dialogue':
self.operation_dialogue_mode = mode self.operation_dialogue_mode = mode
@ -9421,6 +9690,17 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.enable_tooltips(True) self.main_win_obj.enable_tooltips(True)
def set_system_error_show_flag(self, flag):
if DEBUG_FUNC_FLAG:
utils.debug_time('app 9381 set_system_error_show_flag')
if not flag:
self.system_error_show_flag = False
else:
self.system_error_show_flag = True
def set_system_msg_keep_totals_flag(self, flag): def set_system_msg_keep_totals_flag(self, flag):
if DEBUG_FUNC_FLAG: if DEBUG_FUNC_FLAG:
@ -9435,7 +9715,7 @@ class TartubeApp(Gtk.Application):
def set_system_warning_show_flag(self, flag): def set_system_warning_show_flag(self, flag):
if DEBUG_FUNC_FLAG: if DEBUG_FUNC_FLAG:
utils.debug_time('app 9406 xxset_system_warning_show_flagxxx') utils.debug_time('app 9406 set_system_warning_show_flag')
if not flag: if not flag:
self.system_warning_show_flag = False self.system_warning_show_flag = False
@ -9535,10 +9815,21 @@ class TartubeApp(Gtk.Application):
self.refresh_output_videos_flag = True self.refresh_output_videos_flag = True
def set_ytdl_output_show_summary_flag(self, flag):
if DEBUG_FUNC_FLAG:
utils.debug_time('app 9509 set_ytdl_output_show_summary_flag')
if not flag:
self.ytdl_output_show_summary_flag = False
else:
self.ytdl_output_show_summary_flag = True
def set_ytdl_output_start_empty_flag(self, flag): def set_ytdl_output_start_empty_flag(self, flag):
if DEBUG_FUNC_FLAG: if DEBUG_FUNC_FLAG:
utils.debug_time('app 9509 set_ytdl_output_start_empty_flag') utils.debug_time('app 9510 set_ytdl_output_start_empty_flag')
if not flag: if not flag:
self.ytdl_output_start_empty_flag = False self.ytdl_output_start_empty_flag = False
@ -9590,6 +9881,17 @@ class TartubeApp(Gtk.Application):
self.ytdl_output_stdout_flag = True self.ytdl_output_stdout_flag = True
def set_ytdl_output_system_cmd_flag(self, flag):
if DEBUG_FUNC_FLAG:
utils.debug_time('app 9554 set_ytdl_output_system_cmd_flag')
if not flag:
self.ytdl_output_system_cmd_flag = False
else:
self.ytdl_output_system_cmd_flag = True
def set_ytdl_path(self, path): def set_ytdl_path(self, path):
if DEBUG_FUNC_FLAG: if DEBUG_FUNC_FLAG:
@ -9650,6 +9952,17 @@ class TartubeApp(Gtk.Application):
self.ytdl_write_stdout_flag = True self.ytdl_write_stdout_flag = True
def set_ytdl_write_system_cmd_flag(self, flag):
if DEBUG_FUNC_FLAG:
utils.debug_time('app 9614 set_ytdl_write_system_cmd_flag')
if not flag:
self.ytdl_write_system_cmd_flag = False
else:
self.ytdl_write_system_cmd_flag = True
def set_ytdl_write_verbose_flag(self, flag): def set_ytdl_write_verbose_flag(self, flag):
if DEBUG_FUNC_FLAG: if DEBUG_FUNC_FLAG:

File diff suppressed because it is too large Load Diff

View File

@ -95,6 +95,11 @@ class GenericMedia(object):
self.options_obj = options_obj self.options_obj = options_obj
def set_parent_obj(self, parent_obj):
self.parent_obj = parent_obj
def set_warning(self, msg): def set_warning(self, msg):
# The media.Folder object has no error/warning IVs (and shouldn't # The media.Folder object has no error/warning IVs (and shouldn't
@ -333,6 +338,78 @@ class GenericContainer(GenericMedia):
return None return None
def find_matching_video(self, app_obj, name):
"""Can be called by anything.
Checks all of this object's child objects, looking for a media.Video
object with a matching name.
Args:
app_obj (mainapp.TartubeApp): The main application
name (string): The name of the media.Video object to find
Returns:
The first matching media.Video object found, or None if no matching
videos are found.
"""
method = app_obj.match_method
first = app_obj.match_first_chars
ignore = app_obj.match_ignore_chars * -1
# Defend against two different of a name from the same video, one with
# punctuation marks stripped away, and double quotes converted to
# single quotes (thanks, YouTube!) by replacing those characters with
# whitespace
# (After extensive testing, this is the only regex sequence I could
# find that worked)
test_name = name[:]
# Remove punctuation
test_name = re.sub(r'\W+', ' ', test_name, flags=re.UNICODE)
# Also need to replace underline characters
test_name = re.sub(r'[\_\s]+', ' ', test_name)
# Also need to remove leading/trailing whitespace, in case the original
# video name started/ended with a question mark or something like
# that
test_name = re.sub(r'^\s+', '', test_name)
test_name = re.sub(r'\s+$', '', test_name)
for child_obj in self.child_list:
if isinstance(child_obj, Video):
child_name = child_obj.name[:]
child_name = re.sub(
r'\W+',
' ',
child_name,
flags=re.UNICODE,
)
child_name = re.sub(r'[\_\s]+', ' ', child_name)
child_name = re.sub(r'^\s+', '', child_name)
child_name = re.sub(r'\s+$', '', child_name)
if (
method == 'exact_match' \
and child_name == test_name
) or (
method == 'match_first' \
and child_name[:first] == test_name[:first]
) or (
method == 'ignore_last' \
and child_name[:ignore] == test_name[:ignore]
):
return child_obj
# No matches found
return None
def get_depth(self): def get_depth(self):
"""Can be called by anything. """Can be called by anything.
@ -737,11 +814,6 @@ class GenericContainer(GenericMedia):
self.name = name self.name = name
def set_parent_obj(self, parent_obj):
self.parent_obj = parent_obj
# Get accessors # Get accessors
@ -900,78 +972,6 @@ class GenericRemoteContainer(GenericContainer):
return 0 return 0
def find_matching_video(self, app_obj, name):
"""Can be called by anything.
Checks all of this object's child objects, looking for a media.Video
object with a matching name.
Args:
app_obj (mainapp.TartubeApp): The main application
name (string): The name of the media.Video object to find
Returns:
The first matching media.Video object found, or None if no matching
videos are found.
"""
method = app_obj.match_method
first = app_obj.match_first_chars
ignore = app_obj.match_ignore_chars * -1
# Defend against two different of a name from the same video, one with
# punctuation marks stripped away, and double quotes converted to
# single quotes (thanks, YouTube!) by replacing those characters with
# whitespace
# (After extensive testing, this is the only regex sequence I could
# find that worked)
test_name = name[:]
# Remove punctuation
test_name = re.sub(r'\W+', ' ', test_name, flags=re.UNICODE)
# Also need to replace underline characters
test_name = re.sub(r'[\_\s]+', ' ', test_name)
# Also need to remove leading/trailing whitespace, in case the original
# video name started/ended with a question mark or something like
# that
test_name = re.sub(r'^\s+', '', test_name)
test_name = re.sub(r'\s+$', '', test_name)
for child_obj in self.child_list:
if isinstance(child_obj, Video):
child_name = child_obj.name[:]
child_name = re.sub(
r'\W+',
' ',
child_name,
flags=re.UNICODE,
)
child_name = re.sub(r'[\_\s]+', ' ', child_name)
child_name = re.sub(r'^\s+', '', child_name)
child_name = re.sub(r'\s+$', '', child_name)
if (
method == 'exact_match' \
and child_name == test_name
) or (
method == 'match_first' \
and child_name[:first] == test_name[:first]
) or (
method == 'ignore_last' \
and child_name[:ignore] == test_name[:ignore]
):
return child_obj
# No matches found
return None
def sort_children(self): def sort_children(self):
"""Can be called by anything. For example, called by self.add_child(). """Can be called by anything. For example, called by self.add_child().
@ -1000,6 +1000,36 @@ class GenericRemoteContainer(GenericContainer):
# Set accessors # Set accessors
def clone_properties(self, other_obj):
"""Called by mainapp.TartubeApp.convert_remote_container() only.
Copies properties from a media data object (about to be deleted) to
this media data object.
Some properties are handled by the calling function; this function
handles the rest of them.
Args:
other_obj (media.Channel, media.Playlist): The object whose
properties should be copied
"""
self.options_obj = other_obj.options_obj
self.nickname = other_obj.nickname
self.source = other_obj.source
self.dl_sim_flag = other_obj.dl_sim_flag
self.dl_disable_flag = other_obj.dl_disable_flag
self.fav_flag = other_obj.fav_flag
self.new_count = other_obj.new_count
self.fav_count = other_obj.fav_count
self.dl_count = other_obj.dl_count
self.error_list = other_obj.error_list.copy()
self.warning_list = other_obj.warning_list.copy()
def set_source(self, source): def set_source(self, source):
self.source = source self.source = source
@ -1106,9 +1136,12 @@ class Video(GenericMedia):
self.receive_time = None self.receive_time = None
# The video's duration (in integer seconds) # The video's duration (in integer seconds)
self.duration = None self.duration = None
# For videos in a playlist (i.e. a media.Video object whose parent is # For videos in a channel or playlist (i.e. a media.Video object whose
# a media.Playlist object), the video's index in the playlist. For # parent is a media.Channel or media.Playlist object), the video's
# all other situations, the value remains as None # index in the channel/playlist. (The server supplies an index even
# for a channel, and the user might want to convert a channel to a
# playlist)
# For videos whose parent is a media.Folder, the value remains as None
self.index = None self.index = None
# Video description. A string of any length, containing newline # Video description. A string of any length, containing newline

View File

@ -571,10 +571,8 @@ class OptionsManager(object):
class OptionsParser(object): class OptionsParser(object):
"""Called by downloads.DownloadManager.__init__(). """Called by downloads.DownloadManager.__init__() and by
mainwin.SystemCmdDialogue.update_textbuffer().
Each download operation, handled by the downloads.DownloadManager, creates
an instance of this class.
This object converts the download options specified by an This object converts the download options specified by an
options.OptionsManager object into a list of youtube-dl command line options.OptionsManager object into a list of youtube-dl command line
@ -582,8 +580,7 @@ class OptionsParser(object):
Args: Args:
download_manager_obj (downloads.DownloadManager) - The parent app_obj (mainapp.TartubeApp): The main application
download manager object
""" """
@ -591,12 +588,12 @@ class OptionsParser(object):
# Standard class methods # Standard class methods
def __init__(self, download_manager_obj): def __init__(self, app_obj):
# IV list - class objects # IV list - class objects
# ----------------------- # -----------------------
# The parent downloads.DownloadManager object # The main application
self.download_manager_obj = download_manager_obj self.app_obj = app_obj
# IV list - other # IV list - other
@ -812,7 +809,7 @@ class OptionsParser(object):
# Public class methods # Public class methods
def parse(self, download_item_obj, options_dict): def parse(self, media_data_obj, options_manager_obj):
"""Called by downloads.DownloadWorker.prepare_download(). """Called by downloads.DownloadWorker.prepare_download().
@ -822,11 +819,11 @@ class OptionsParser(object):
Args: Args:
download_item_obj (downloads.DownloadItem) - The object handling media_data_obj (media.Video, media.Channel, media.Playlist,
the download media.Folder): The media data object being downloaded
options_dict (dict): Python dictionary containing download options; options_manager_obj (options.OptionsManager): The object containing
taken from options.OptionsManager.options_dict the download options for this media data object
Returns: Returns:
@ -838,11 +835,11 @@ class OptionsParser(object):
options_list = ['--newline'] options_list = ['--newline']
# Create a copy of the dictionary... # Create a copy of the dictionary...
copy_dict = options_dict.copy() copy_dict = options_manager_obj.options_dict.copy()
# ...then modify various values in the copy. Set the 'save_path' option # ...then modify various values in the copy. Set the 'save_path' option
self.build_save_path(download_item_obj, copy_dict) self.build_save_path(media_data_obj, copy_dict)
# Set the 'video_format' option # Set the 'video_format' option
self.build_video_format(download_item_obj, copy_dict) self.build_video_format(copy_dict)
# Set the 'min_filesize' and 'max_filesize' options # Set the 'min_filesize' and 'max_filesize' options
self.build_file_sizes(copy_dict) self.build_file_sizes(copy_dict)
# Set the 'limit_rate' option # Set the 'limit_rate' option
@ -851,12 +848,9 @@ class OptionsParser(object):
# Reset the 'playlist_start', 'playlist_end' and 'max_downloads' # Reset the 'playlist_start', 'playlist_end' and 'max_downloads'
# options if we're not downloading a video in a playlist # options if we're not downloading a video in a playlist
if ( if (
isinstance(download_item_obj.media_data_obj, media.Video) \ isinstance(media_data_obj, media.Video) \
and not isinstance( and not isinstance(media_data_obj.parent_obj, media.Playlist)
download_item_obj.media_data_obj.parent_obj, ) or not isinstance(media_data_obj, media.Playlist):
media.Playlist,
)
) or not isinstance(download_item_obj.media_data_obj, media.Playlist):
copy_dict['playlist_start'] = 1 copy_dict['playlist_start'] = 1
copy_dict['playlist_end'] = 0 copy_dict['playlist_end'] = 0
copy_dict['max_downloads'] = 0 copy_dict['max_downloads'] = 0
@ -998,18 +992,19 @@ class OptionsParser(object):
""" """
# Import the main app (for convenience)
app_obj = self.download_manager_obj.app_obj
# Set the bandwidth limit (e.g. '50K') # Set the bandwidth limit (e.g. '50K')
if app_obj.bandwidth_apply_flag: if self.app_obj.bandwidth_apply_flag:
# The bandwidth limit is divided equally between the workers # The bandwidth limit is divided equally between the workers
limit = int(app_obj.bandwidth_default / app_obj.num_worker_default) limit = int(
self.app_obj.bandwidth_default
/ self.app_obj.num_worker_default
)
copy_dict['limit_rate'] = str(limit) + 'K' copy_dict['limit_rate'] = str(limit) + 'K'
def build_save_path(self, download_item_obj, copy_dict): def build_save_path(self, media_data_obj, copy_dict):
"""Called by self.parse(). """Called by self.parse().
@ -1018,16 +1013,14 @@ class OptionsParser(object):
Args: Args:
download_item_obj (downloads.DownloadItem) - The object handling media_data_obj (media.Video, media.Channel, media.Playlist,
the download media.Folder): The media data object being downloaded
copy_dict (dict): Copy of the original options dictionary. copy_dict (dict): Copy of the original options dictionary.
""" """
# Set the directory in which any downloaded videos will be saved # Set the directory in which any downloaded videos will be saved
app_obj = self.download_manager_obj.app_obj
media_data_obj = download_item_obj.media_data_obj
override_name = copy_dict['use_fixed_folder'] override_name = copy_dict['use_fixed_folder']
if not isinstance(media_data_obj, media.Video) \ if not isinstance(media_data_obj, media.Video) \
@ -1042,15 +1035,9 @@ class OptionsParser(object):
else: else:
if isinstance(media_data_obj, media.Video): if isinstance(media_data_obj, media.Video):
save_path = media_data_obj.parent_obj.get_dir( save_path = media_data_obj.parent_obj.get_dir(self.app_obj)
self.download_manager_obj.app_obj
)
else: else:
save_path = media_data_obj.get_dir( save_path = media_data_obj.get_dir(self.app_obj)
self.download_manager_obj.app_obj
)
# Set the youtube-dl output template for the video's file # Set the youtube-dl output template for the video's file
template = formats.FILE_OUTPUT_CONVERT_DICT[copy_dict['output_format']] template = formats.FILE_OUTPUT_CONVERT_DICT[copy_dict['output_format']]
@ -1063,7 +1050,7 @@ class OptionsParser(object):
) )
def build_video_format(self, download_item_obj, copy_dict): def build_video_format(self, copy_dict):
"""Called by self.parse(). """Called by self.parse().
@ -1072,9 +1059,6 @@ class OptionsParser(object):
Args: Args:
download_item_obj (downloads.DownloadItem) - The object handling
the download
copy_dict (dict): Copy of the original options dictionary. copy_dict (dict): Copy of the original options dictionary.
""" """
@ -1089,18 +1073,17 @@ class OptionsParser(object):
# extractor codes are ignored # extractor codes are ignored
resolution_dict = formats.VIDEO_RESOLUTION_DICT.copy() resolution_dict = formats.VIDEO_RESOLUTION_DICT.copy()
fps_dict = formats.VIDEO_FPS_DICT.copy() fps_dict = formats.VIDEO_FPS_DICT.copy()
app_obj = self.download_manager_obj.app_obj
# If the progressive scan resolution is specified, it overrides all # If the progressive scan resolution is specified, it overrides all
# other video format options # other video format options
height = None height = None
fps = None fps = None
if app_obj.video_res_apply_flag: if self.app_obj.video_res_apply_flag:
height = resolution_dict[app_obj.video_res_default] height = resolution_dict[self.app_obj.video_res_default]
# (Currently, formats.VIDEO_FPS_DICT only lists formats with 60fps) # (Currently, formats.VIDEO_FPS_DICT only lists formats with 60fps)
if app_obj.video_res_default in fps_dict: if self.app_obj.video_res_default in fps_dict:
fps = fps_dict[app_obj.video_res_default] fps = fps_dict[self.app_obj.video_res_default]
elif copy_dict['video_format'] in resolution_dict: elif copy_dict['video_format'] in resolution_dict:
height = resolution_dict[copy_dict['video_format']] height = resolution_dict[copy_dict['video_format']]

View File

@ -35,8 +35,8 @@ import mainapp
# 'Global' variables # 'Global' variables
__packagename__ = 'tartube' __packagename__ = 'tartube'
__version__ = '1.3.053' __version__ = '1.3.077'
__date__ = '23 Jan 2020' __date__ = '26 Jan 2020'
__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' __copyright__ = 'Copyright \xa9 2019-2020 A S Lewis'
__license__ = """ __license__ = """
Copyright \xc2\xa9 2019-2020 A S Lewis. Copyright \xc2\xa9 2019-2020 A S Lewis.

View File

@ -35,8 +35,8 @@ import mainapp
# 'Global' variables # 'Global' variables
__packagename__ = 'tartube' __packagename__ = 'tartube'
__version__ = '1.3.053' __version__ = '1.3.077'
__date__ = '23 Jan 2020' __date__ = '26 Jan 2020'
__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' __copyright__ = 'Copyright \xa9 2019-2020 A S Lewis'
__license__ = """ __license__ = """
Copyright \xc2\xa9 2019-2020 A S Lewis. Copyright \xc2\xa9 2019-2020 A S Lewis.

View File

@ -157,13 +157,20 @@ class UpdateManager(threading.Thread):
# Create a new child process to install either the 64-bit or 32-bit # Create a new child process to install either the 64-bit or 32-bit
# version of FFmpeg, as appropriate # version of FFmpeg, as appropriate
if sys.maxsize <= 2147483647: if sys.maxsize <= 2147483647:
self.create_child_process( binary = 'mingw-w64-i686-ffmpeg'
['pacman', '-S', 'mingw-w64-i686-ffmpeg', '--noconfirm'],
)
else: else:
self.create_child_process( binary = 'mingw-w64-x86_64-ffmpeg'
['pacman', '-S', 'mingw-w64-x86_64-ffmpeg', '--noconfirm'],
) self.create_child_process(
['pacman', '-S', binary, '--noconfirm'],
)
# Show the system command in the Output Tab
space = ' '
self.app_obj.main_win_obj.output_tab_write_system_cmd(
1,
space.join( ['pacman', '-S', binary, '--noconfirm'] ),
)
# So that we can read from the child process STDOUT and STDERR, attach # So that we can read from the child process STDOUT and STDERR, attach
# a file descriptor to the PipeReader objects # a file descriptor to the PipeReader objects
@ -276,6 +283,13 @@ class UpdateManager(threading.Thread):
# Create a new child process using that command # Create a new child process using that command
self.create_child_process(cmd_list) self.create_child_process(cmd_list)
# Show the system command in the Output Tab
space = ' '
self.app_obj.main_win_obj.output_tab_write_system_cmd(
1,
space.join(cmd_list),
)
# So that we can read from the child process STDOUT and STDERR, attach # So that we can read from the child process STDOUT and STDERR, attach
# a file descriptor to the PipeReader objects # a file descriptor to the PipeReader objects
if self.child_process is not None: if self.child_process is not None:

View File

@ -40,6 +40,7 @@ import textwrap
# Import our modules # Import our modules
import formats import formats
import mainapp import mainapp
import media
# Functions # Functions
@ -471,6 +472,75 @@ def format_bytes(num_bytes):
return "%.2f%s" % (output_value, suffix) return "%.2f%s" % (output_value, suffix)
def generate_system_cmd(app_obj, media_data_obj, options_list,
dl_sim_flag=False):
"""Called by downloads.VideoDownloader.do_download() and
mainwin.SystemCmdDialogue.update_textbuffer().
Based on YoutubeDLDownloader._get_cmd().
Prepare the system command that instructs youtube-dl to download the
specified media data object.
Args:
app_obj (mainapp.TartubeApp): The main application
media_data_obj (media.Video, media.Channel, media.Playlist,
media.Folder): The media data object to be downloaded
options_list (list): A list of download options generated by a call to
options.OptionsParser.parse()
dl_sim_flag (bool): True if a simulated download is to take place,
False if a real download is to take place
Returns:
Python list that contains the system command to execute and its
arguments
"""
# Simulate the download, rather than actually downloading videos, if
# required
if dl_sim_flag:
options_list.append('--dump-json')
# If actually downloading videos, create an archive file so that, if the
# user deletes the videos, youtube-dl won't try to download them again
elif app_obj.allow_ytdl_archive_flag:
# (Create the archive file in the media data object's own
# sub-directory, not the alternative download destination, as this
# helps youtube-dl to work the way we want it)
if isinstance(media_data_obj, media.Video):
dl_path = media_data_obj.parent_obj.get_dir(app_obj)
else:
dl_path = media_data_obj.get_dir(app_obj)
options_list.append('--download-archive')
options_list.append(
os.path.abspath(os.path.join(dl_path, 'ytdl-archive.txt')),
)
# Show verbose output (youtube-dl debugging mode), if required
if app_obj.ytdl_write_verbose_flag:
options_list.append('--verbose')
# Supply youtube-dl with the path to the ffmpeg/avconv binary, if the
# user has provided one
if app_obj.ffmpeg_path is not None:
options_list.append('--ffmpeg-location')
options_list.append('"' + app_obj.ffmpeg_path + '"')
# Set the list
cmd_list = [app_obj.ytdl_path] + options_list + [media_data_obj.source]
return cmd_list
def get_encoding(): def get_encoding():
"""Based on the get_encoding() function in youtube-dl-gui. Now called """Based on the get_encoding() function in youtube-dl-gui. Now called
@ -491,6 +561,40 @@ def get_encoding():
return encoding return encoding
def get_options_manager(app_obj, media_data_obj):
"""Can be called by anything, and is then called by this function
recursively.
Fetches the options.OptionsManager which applies to the specified media
data object.
The media data object might specify its own options.OptionsManager, or
we might have to use the parent's, or the parent's parent's (and so
on). As a last resort, use General Options Manager.
Args:
app_obj (mainapp.TartubeApp): The main application
media_data_obj (media.Video, media.Channel, media.Playlist,
media.Folder): A media data object
Returns:
The options.OptionsManager object that applies to the specified
media data object
"""
if media_data_obj.options_obj:
return media_data_obj.options_obj
elif media_data_obj.parent_obj:
return get_options_manager(app_obj, media_data_obj.parent_obj)
else:
return app_obj.general_options_obj
def open_file(uri): def open_file(uri):
"""Can be called by anything. """Can be called by anything.
@ -505,9 +609,6 @@ def open_file(uri):
""" """
if sys.platform == "win32": if sys.platform == "win32":
# v1.2.052. If the video file's filename contains an ampersand, MSWin
# is passed a string containing &amp; - so we need to strip that
uri = re.sub('\&amp;', '&', uri)
os.startfile(uri) os.startfile(uri)
else: else:
opener ="open" if sys.platform == "darwin" else "xdg-open" opener ="open" if sys.platform == "darwin" else "xdg-open"