Updated to v1.3.077
This commit is contained in:
parent
3755621e6e
commit
08723760c1
67
CHANGES
67
CHANGES
@ -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)
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
119
README.rst
119
README.rst
@ -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>`__.
|
||||||
|
@ -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"
|
||||||
|
@ -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 |
2
setup.py
2
setup.py
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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().
|
||||||
|
@ -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
193
tartube/media.py
193
tartube/media.py
@ -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
|
||||||
|
@ -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']]
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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:
|
||||||
|
107
tartube/utils.py
107
tartube/utils.py
@ -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 & - so we need to strip that
|
|
||||||
uri = re.sub('\&', '&', 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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user