diff --git a/CHANGES b/CHANGES
index 1a2f144..f3e80ef 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,115 @@
+v2.2.0 (30 Sep 2020)
+-------------------------------------------------------------------------------
+
+MAJOR NEW FEATURES
+- Tartube is now confirmed to work on MacOS. See the README file for
+ installation instructions
+- Tartube can now handle video thumbnails in the .webp format, as long as
+ FFmpeg is installed. Thumbnails are automatically converted to .jpg (either
+ by youtube-dl, or by Tartube itself, as appropriate). This only affects new
+ downloads. If you want to convert .webp thumbnails you've already
+ downloaded, click Operations > Tidy up files..., select 'Convert .webp
+ thumbnails to .jpg using Ffmpeg', and click OK. (This procedure may take a
+ while if there are thousands of thumbnails to convert) (Git #155 and
+ others)
+- Thumbnails, video description, metadata and annotation files can now be
+ downloaded into a sub-directory, rather than being stored in the same
+ directory as their videos. Thumbnails are stored in /.thumbs, and the
+ others are stored in /.data. On Linux/BSD, those sub-directories are
+ normally invisible by default (typically, pressing CTRL+H will reveal
+ them). This new feature is disabled by default. To enable it, click Edit >
+ Download options... > Files > Write/move files, and select one or more of
+ the checkboxes. The new feature only affects new downloads. If you want to
+ move files you've already downloaded, click Operations > Tidy up files...,
+ select 'Move thumbnails into own folder' and/or 'Move other metadata files
+ into own folder', and click OK (Git #139)
+- You can now select one or more videos, and process them with FFmpeg directly
+ (in other words, after downloads have finished, and without involving
+ youtube-dl). This will be useful if you want to convert one video format to
+ another, change the frame rate, or with countless other tasks. Just select
+ the video(s), right-click them and select 'Process with FFmpeg...'. Since
+ many FFmpeg procedures require a different output filename, you can specify
+ that, too. Note that FFmpeg sometimes takes a very long time; you should
+ test a procedure with a single video, before trying to process hundreds of
+ them (Git #153)
+- Tartube can now use forks of youtube-dl, such as youtube-dlc. (Tartube
+ assumes that a fork is still very similar to the original). A fork can be
+ specified in Edit > System preferences... > youtube-dl (Git #158)
+- New installations of Tartube will now auto-detect the location of youtube-dl,
+ if it is already installed on your system. This will benefit users of the
+ .DEB and .RPM packages, who until now were expected to know how to set the
+ youtube-dl path manually (Git #152)
+- Ffmpeg and AVConv will now also be auto-detected. If you have installed them
+ in unusual locations, you should specify those locations in Edit >
+ System preferences... > youtube-dl > Ffmpeg / AVConv. If not, there is no
+ need to specify either location; just leave the boxes empty. (None of this
+ applies to MS Windows users)
+- When Tartube shuts down unexpectedly, it doesn't have time to mark the
+ database as no longer being in use (i.e. doesn't remove the lockfile). The
+ next time Tartube runs, users were prompted to remove the lockfile, then
+ restart Tartube. Many users were unhappy with this situation, so it has
+ been improved. You will still be prompted to remove the lockfile, but there
+ is no longer any need to restart Tartube
+
+MINOR NEW FEATURES
+- FFmpeg is now required for a lot of Tartube functionality. On new
+ installations, users who have not yet installed FFmpeg will see some
+ additional nag-boxes, and various hints in other configuration windows
+ (Git #155)
+- If users downloadd a video, but only its audio, the video appeared in
+ Tartube's database as downloaded. However, when the user clicked on the
+ 'Player' label, the audio file was not opened in the system's media player.
+ Tartube now checks for an audio file (as well as video files in different
+ formats), if the video file it was expecting does not exist
+- When the user clicks Operations > Update youtube-dl, Tartube now
+ automatically makes the Output tab visible. Hopefully this will avoid
+ confusion for new users, who do not notice that the Check all/Download all
+ buttons have been greyed out. This behaviour can be disabled, if required:
+ click Edit > System preferences... > Output > Output Tab, and deselect
+ 'During an update operation, automatically switch to the Output Tab'
+ (Git #149)
+- The invidio.us website has closed. There are many mirrors available. Tartube
+ now uses invidious.site as its default mirror. To specify a different
+ mirror, click Edit > System preferences... > Operations > Downloads
+- YouTube phased out video annotations in 2019. The Tartube code was unable to
+ download annotation files during a simulated download (for example, with
+ the 'Check all' button). Now that there is no way of testing any fix, the
+ feature has been removed entirely
+- You can now add automatic custom downloads on a schedule (normal downloads
+ were already available). Click Edit > System preferences... > Scheduling
+ (Git #154)
+- Reduced the compulsory delay at the end of many types of operation. The
+ delay time is not the same for all types of operation
+- Tartube debug messages are visible in the terminal window (if open). Before,
+ the user had to edit the source code to enable debug messages. They can
+ now be enabled from within Tartube itself: click Edit > System
+ preferences... > General > Debugging. These settings are not saved, so when
+ Tartube restarts, you will have to re-enable debug messages again
+- In the Progress and Classic Mode tabs, the name of the incoming file (and
+ other similar columns) are no longer artificially shortened (Git #161)
+
+MAJOR FIXES
+- When one channel downloads into videos into another channel's directory,
+ and the other channel is then deleted, the Tartube database did not update
+ itself properly. Fixed
+- Fixed some issues when using the refresh operation to import videos, that had
+ been downloaded by youtube-dl (without Tartube's) help, into Tartube's
+ database (Git #142)
+- Tartube can now download videos using the youtube-dl archive file, even when
+ missing video detection is turned on (Git #154)
+- Tartube sometimes froze on shutdown, after youtube-dl had been updated.
+ Applied the existing MS Windows fix to all operating systems
+
+MINOR FIXES
+- Fixed issues when importing a JSON export file on MS Windows, and a different
+ issue error when importing it on Linux
+- We were not able to fix export issues reported in Git #143, but we have
+ updated the dialogue window, which should give more information about what
+ is causing the error
+- System folders cannot be deleted. The 'Delete folder' popup menu item is now
+ greyed out. (Nothing happened, even when it was clickable)
+- Warnings about broken Gtk have been removed (probably permanently)
+
v2.1.070 (8 Aug 2020)
-------------------------------------------------------------------------------
diff --git a/README.rst b/README.rst
index 564ed01..e309209 100644
--- a/README.rst
+++ b/README.rst
@@ -41,24 +41,28 @@ Problems can be reported at `our GitHub page `__ - see `6.21 Classic Mode`_
-- **Tartube** can now detect livestreams, and alert you when they start - see `6.22 Livestreams`_. This feature is EXPERIMENTAL, has only been tested on **YouTube**, and may not be reliable.
-- If you can contribute a translation to this project, `please read this `__. As a proof of concept, **Tartube** can now be used with either British or American English
+- **Tartube** is confirmed to work on MacOS see `5.2 Installation - MacOS`_
+- For a while, **Tartube** has been unable to display video thumbnails, after **YouTube** started using a format that no-one supports. Both **youtube-dl** and **Tartube** have been updated with workarounds; they will only work if you have installed `FFmpeg `__ (for help with that, see below)
+- If you like tidy directories (folders), you can store the thumbnail and metadata files in a sub-folder, leaving the main folder containing only videos. To enable this, click **Edit > Download options... > Files > Write/move files**
+- Videos can now be sent to FFmpeg directly for processing (in other words, after the download has finished). Right-click one or more videos and select **Process with FFmpeg...**
+- **Tartube** now supports forks of **youtube-dl**, such as `youtube-dlc `__
+
+For a full list of new features and fixes, see `recent changes `__.
3 Downloads
===========
-Latest version: **v2.1.070 (8 Aug 2020)** (see `recent changes `__)
+Latest version: **v2.2.0 (30 Sep 2020)**
Official packages (also available from the `Github release page `__):
-- `MS Windows (64-bit) installer `__ and `portable edition `__ from Sourceforge
-- `MS Windows (32-bit) installer `__ and `portable edition `__ from Sourceforge
-- `DEB package (for Debian-based distros, e.g. Ubuntu, Linux Mint) `__ from Sourceforge
-- `RPM package (for RHEL-based distros, e.g. Fedora) `__ from Sourceforge
+- `MS Windows (64-bit) installer `__ and `portable edition `__ from Sourceforge
+- `MS Windows (32-bit) installer `__ and `portable edition `__ from Sourceforge
+- `DEB package (for Debian-based distros, e.g. Ubuntu, Linux Mint) `__ from Sourceforge
+- `RPM package (for RHEL-based distros, e.g. Fedora) `__ from Sourceforge
There are also some DEB/RPM packages marked STRICT. In these packages, updates to **youtube-dl** from within **Tartube** have been disabled. If **Tartube** is uploaded to a repository with lots of rules, such as the official Debian repository, then you should probably use the STRICT packages.
@@ -69,7 +73,7 @@ Semi-official packages:
Source code:
-- `Source code `__ from Sourceforge
+- `Source code `__ from Sourceforge
- `Source code `__ and `support `__ from GitHub
4 Quick start guide
@@ -82,7 +86,7 @@ Source code:
- Start **Tartube** from the Start menu, or by clicking the icon on the desktop
- When prompted, choose a folder where **Tartube** can store videos
- When prompted, let **Tartube** install **youtube-dl** for you
-- It's strongly recommended that you install **FFmpeg**. From the menu, click **Operations > Install FFmpeg**
+- It's strongly recommended that you install `FFmpeg `__ . From the menu, click **Operations > Install FFmpeg**
If you don't want **Tartube** to add videos to its database, click the **Classic Mode** Tab. If you *do* want to update the database, do this instead:
@@ -98,7 +102,7 @@ If you don't want **Tartube** to add videos to its database, click the **Classic
-------------------
- Install **Tartube**, using any of the methods described below
-- It's strongly recommended that you install `Ffmpeg `__ or `AVConv `__, too
+- It's strongly recommended that you install `Ffmpeg `__, too
- Run **Tartube**
- When prompted, choose a directory where **Tartube** can store videos
- Install **youtube-dl** by clicking **Operations > Update youtube-dl**
@@ -123,7 +127,7 @@ MS Windows users should use the installer `available at the Tartube website `__ - see `6.4 Installing FFmpeg / AVConv`_.
Both the installer and the portable edition include a copy of `AtomicParsley `__, so there is no need to install it yourself.
@@ -202,7 +206,7 @@ MacOS users should use the following procedure (with thanks to JeremyShih):
**brew install adwaita-icon-theme**
-- It's recommended that you install `Ffmpeg `__, too
+- It is strongly recommended that you install `Ffmpeg `__, too
**brew install ffmpeg**
@@ -228,6 +232,10 @@ Linux distributions based on Debian, such as Ubuntu and Linux Mint, can install
2. **Tartube** asks you to choose a data directory, so do that
3. Click **Operations > Update youtube-dl**
+It is strongly recommended that you install `Ffmpeg `__, too. On most Debian-based systems, you can open a terminal window and run this command:
+
+ **sudo apt-get install ffmpeg**
+
5.3.2 Install using the RPM package
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -250,6 +258,14 @@ On Fedora, the procedure is:
3. Type: ``pip3 install youtube-dl``
4. You can now run **Tartube**.
+It is strongly recommended that you install `Ffmpeg `__, too. On most RHEL-based systems (for example, Fedora 29-32), you can open a terminal window and run these commands:
+
+ **sudo dnf -y install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm**
+
+ **sudo dnf -y install https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm**
+
+ **sudo apt-get install ffmpeg**
+
5.3.3 Install using the AUR package
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -261,12 +277,16 @@ On Arch-based systems. such as Manjaro, Tartube can be installed using the semi-
4. Type: ``makepkg -si``
5. You can now run **Tartube**.
-5.3.4 Install using the ebuild/AUR packages
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+It is strongly recommended that you install `Ffmpeg `__, too. On most Arch-based systems, you can open a terminal window and run this command:
+
+ **sudo pacman -S ffmpeg**
+
+5.3.4 Install using the ebuild package
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On Gentoo-based systems, **Tartube** can be installed using the semi-official ebuild package, using the link above.
-Tartube requires `youtube-dl `__.
+Tartube requires `youtube-dl `__. It is strongly recommended that you install `Ffmpeg `__, too.
If you're not sure how to install using ebuild, then it might be easier to install from PyPI.
@@ -301,7 +321,7 @@ Here is the procedure for Debian-based distributions, like Ubuntu and Linux Mint
5.3.8 Manual installation
~~~~~~~~~~~~~~~~~~~~~~~~~
-For any other method of installation, the following dependencies are required:
+For any other method of installation on Linux/BSD, the following dependencies are required:
- `Python 3 `__
- `Gtk 3 `__
@@ -314,7 +334,7 @@ These dependencies are optional, but recommended:
- `Python feedparser module `__ - enables **Tartube** to detect livestreams
- `Python moviepy module `__ - if the website doesn't tell **Tartube** about the length of its videos, moviepy can work it out
- `Python playsound module `__ - enables **Tartube** to play an alarm when a livestream starts
-- `Ffmpeg `__ or `AVConv `__ - required for various video post-processing tasks; see the section below if you want to use FFmpeg or AVConv
+- `Ffmpeg `__ - required for various video post-processing tasks; see the section below if you want to use FFmpeg
- `AtomicParsley `__ - required for embedding thumbnails in audio files
5.3.9 Install from source
@@ -342,7 +362,7 @@ After installing dependencies (see above):
* `6.1 Choose where to save videos`_
* `6.2 Check youtube-dl is updated`_
* `6.3 Setting youtube-dl's location`_
-* `6.4 Setting the location of FFmpeg / AVConv`_
+* `6.4 Installing FFmpeg / AVConv`_
* `6.4.1 On MS Windows`_
* `6.4.2 On Linux/BSD/MacOS`_
* `6.5 Introducing system folders`_
@@ -380,6 +400,13 @@ After installing dependencies (see above):
* `6.22.3 Livestream notifications`_
* `6.22.4 Compatible websites`_
* `6.23 Detecting missing videos`_
+* `6.24 More information about FFmpeg and AVConv`_
+* `6.24.1 Using FFmpeg / AVConv with youtube-dl`_
+* `6.24.2 Using FFmpeg directly`_
+* `6.24.3 Changing the filename`_
+* `6.24.4 Changing the video format`_
+* `6.24.5 FFmpeg command-line options`_
+* `6.25 Using youtube-dl forks`_
6.1 Choose where to save videos
-------------------------------
@@ -429,10 +456,21 @@ On other systems, users can modify **Tartube**'s settings. There are several loc
- Try changing the setting **Shell command for update operations**
- Try the update operation again
-6.4 Setting the location of FFmpeg / AVConv
--------------------------------------------
-
-**youtube-dl** can use the `FFmpeg library `__ or the `AVConv library `__ for various video-processing tasks, such as converting video files to audio, and for handling large resolutions (1080p and higher). If you want to use FFmpeg or AVConv, you should first install them on your system.
+6.4 Installing FFmpeg / AVConv
+------------------------------
+
+`FFmpeg `__ and `AVConv `__ are commonly use for various video-processing tasks.
+
+**It is strongly recommended that all users install FFmpeg**. Without it, Tartube won't be able to do any of these things:
+
+- Display thumbnails from **YouTube**
+- Download high-resolution videos from any website
+- Download certain other video formats
+- Convert video files to audio
+
+**youtube-dl** uses FFmpeg by default, but it can use AVConv for certain tasks.
+
+For more information about **Tartube**'s use of Ffmpeg and AVConv, see `6.24 More information about FFmpeg and AVConv`_.
6.4.1 On MS Windows
~~~~~~~~~~~~~~~~~~~
@@ -444,13 +482,17 @@ There is no known method of installing a compatible version of AVConv.
6.4.2 On Linux/BSD/MacOS
~~~~~~~~~~~~~~~~~~~~~~~~
-On all other operating systems, **youtube-dl** might be able to detect FFmpeg/AVConv without any help from you. If not, you can tell **Tartube** where to find FFmpeg/AVConv in this same tab.
+On all other operating systems, **Tartube** and **youtube-dl** should be able to use FFmpeg (and AVConv, if it is also installed) without any help from you.
+
+If the FFmpeg / AVConv executables have been installed to an unusual location, you can tell **Tartube** where to find them.
.. image:: screenshots/example5.png
- :alt: Updating ffmpeg
+ :alt: Updating FFmpeg and AVConv
-- Click **Edit > System preferences... > youtube-dl > Preferences**
-- Click the **Set** button and select the FFmpeg/AVConv executable
+- Click **Edit > System preferences... > youtube-dl > FFmpeg / AVConv**
+- Click the **Set** buttons and select the FFmpeg or AVConv executable
+- Click the **Reset** buttons to remove that selection
+- Click the the **Use default path** buttons to explictly use the normal location for the executables
6.5 Introducing system folders
------------------------------
@@ -549,7 +591,7 @@ Once you've finished adding videos, channels, playlists and folders, you can mak
- **Download** - Actually downloads the videos. If you have disabled downloads for a particular item, **Tartube** will just fetch information about it instead
- **Custom download** - Downloads videos in a non-standard way; see `6.13 Custom downloads`_
- **Refresh** - Examines your filesystem. If you have manually copied any videos into **Tartube**'s data directory, those videos are added to **Tartube**'s database
-- **Update** - Installs or updates **youtube-dl**, as described in `6.2 Check youtube-dl is updated`_. Also installs FFmpeg (on MS Windows only); see `6.4 Setting the location of FFmpeg / AVConv`_
+- **Update** - Installs or updates **youtube-dl**, as described in `6.2 Check youtube-dl is updated`_. Also installs FFmpeg (on MS Windows only); see `6.4 Installing FFmpeg / AVConv`_
- **Info** - Fetches information about a particular video: either the available video/audio formats, or the available subtitles
- **Tidy** - Tidies up **Tartube**'s data directory, as well as checking that downloaded videos still exist and are not corrupted
@@ -643,7 +685,7 @@ If **Tartube** can't download a video from YouTube, it's sometimes possible to o
This only works when requesting individual videos, not whole channels or playlists. You should normally enable independent downloads as well (as described above)
-There are a number of alternative YouTube front-ends available. `HookTube `__ and `Invidious `__ are, at the time of writing, the most famous. However, you can specify any alternative website you like.
+There are a number of alternative YouTube front-ends available, besides `HookTube `__. The original `Invidious `__ closed in September 2020, but there are a number of mirrors, such as `this one `__. To get a list of mirrors, `see this page `__, or use your favourite search engine.
When specifying an alternative website, it's very important that you type the *exact text* that replaces **youtube.com** in a video's URL. For example, you must type **hooktube.com** not **www.hooktube.com** or **http://www.hooktube.com/**.
@@ -670,7 +712,13 @@ If you've downloaded a video, you can watch it by clicking the word **Player**.
If you haven't downloaded the video yet, you can watch it online by clicking the word **Website** or **YouTube**. (One or the other will be visible).
-If it's a YouTube video that is restricted (not available in certain regions, or without confirming your age), it's sometimes possible to watch the same video without restrictions on alternative website, such as `HookTube `__ or `Invidious `__.
+Restricted YouTube videos (not available in your region, or not visible without a Google account) can usually be watched without restrictions on an alternative website, such as `HookTube `__ or an Invidious mirror `such as this one `__.
+
+As mentioned above, the original Invidious has now closed. You can change the Invidious mirror that **Tartube** is using, if you like.
+
+- Click **Edit > System preferences... > Operations > Downloads**
+- Enter a new mirror in the box
+- You can now watch a video by clicking its **Invidious** label
6.15 Filtering and finding videos
---------------------------------
@@ -883,7 +931,7 @@ This is how to import the data into a different **Tartube** database.
**Tartube** can automatically extract the audio from its downloaded videos, if that's what you want.
-The first step is to make sure that either FFmpeg or AVconv is installed on your system - see `6.4 Setting the location of FFmpeg / AVconv`_.
+The first step is to make sure that either FFmpeg or AVconv is installed on your system - see `6.4 Installing FFmpeg / AVConv`_.
The remaining steps are simple:
@@ -1006,7 +1054,7 @@ Now click the **RSS feed** tab. Enter the address (URL) of the RSS feed in the b
6.23 Detecting missing videos
-----------------------------
-Since v2.1.065, **Tartube** has been able to detect videos which you have downloaded, but which have since deleted by the original uploader.
+**Tartube** can detect videos you have downloaded, but which have been since deleted by the original uploader.
This feature is EXPERIMENTAL and may not work as intended.
@@ -1017,7 +1065,79 @@ Having enabled detection, removed videos will appear in the **Missing Videos** f
**Tartube** only detects missing videos when checking/downloading whole channels or playlists. If you interrupt a download, no detection occurs.
-You should note that enabling detection will disable the archive file used by youtube-dl (see `7.9 'Download all' button takes too long`_ ). Download operations may take longer as a result.
+6.24 More information about FFmpeg and AVConv
+---------------------------------------------
+
+6.24.1 Using FFmpeg / AVConv with youtube-dl
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you explicitly set the location of the FFmpeg and/or AVConv executables, then those locations are passed on to youtube-dl when you check or download videos.
+
+If *both* locations are set, only one of them is passed on. Usually, that's the location of FFmpeg. However, if you specify the **prefer_avconv** download option, then that is passed on, instead.
+
+- Click **Edit > General download options...**
+- In the new window, if the **Show advanced download options** button is visible, click it
+- Now click the **Post-processing** tab
+- Click the **Prefer AVConv over FFmpeg** button to select it
+- Make sure the **Prefer FFmpeg over AVConv (default)** button is not selected
+- Click **OK** to apply your changes
+
+For more information about download options, see `6.11 General download options`_.
+
+6.24.2 Using FFmpeg directly
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can call FFmpeg directly, if you want to. (It only works on videos you have actually downloaded.)
+
+This is useful for converting a video file from one format to another, and many other tasks.
+
+- Click a video, or select several videos together
+- Right-click them and select **Process with FFmpeg...**
+- In the new dialogue window, select some FFmpeg options
+
+.. image:: screenshots/example24.png
+ :alt: The FFmpeg options window
+
+6.24.3 Changing the filename
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The first three boxes allow you to change the video's filename. *This might take a very long time, if you don't add options in the other boxes, too.*
+
+The first box allows you to add some text to the end of the filename, something like **modified**, perhaps.
+
+The second and third boxes allow you to search and replace inside the filename.
+
+In the box **If regex matches filename**, you can enter a regular expression (regex). If the pattern matches the filename, the matching portion is substituted for whatever you put in the box **...then apply substitution**.
+
+If you're familiar with regular expressions, then this should need no further explanation: it's a perfectly ordinary regex substitution.
+
+If not, then there are unlimited tutorials available online. Here's a simple example. To replace the word **rabbit** with **dinosaur**, in every filename that contains it, enter **rabbit** in the regex box and **dinosaur** in the substitution box.
+
+6.24.4 Changing the video format
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Converting a video from one format to another is as simple as adding the text **avi** or **mkv** or any other valid video format to the box **Change file extension**,
+
+6.24.5 FFmpeg command-line options
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The last box allows you to specify FFmpeg options directly. For example, to convert the framerate of some videos to 24 fps, enter the following text into the box at the bottom:
+
+ **-r 24**
+
+6.25 Using youtube-dl forks
+---------------------------
+
+`youtube-dl `__ is open-source software, and there are a number of forks available (for example, `youtube-dlc `__).
+
+If a youtube-dl fork is still compatible with the original, then **Tartube** can use it instead of the original.
+
+- Click **Edit > System preferences... > youtube-dl**
+- In the box **youtube-dl compatible fork to use**, enter **youtube-dlc** (or the name of the fork)
+- Click **OK** to close the preferences window
+- Now click **Operations > Update youtube-dlc**, which will download (or update) the fork on your system
+
+To switch back to using the original youtube-dl, just empty the same box.
7 Frequently-Asked Questions
============================
@@ -1253,13 +1373,13 @@ A: See `6.20 Converting to audio`_
A: The solution to both problems is to install FFmpeg, and to set the output format correctly.
-Firstly, make sure FFmpeg is installed on your system - see `6.4 Setting the location of FFmpeg / AVConv`_.
+Firstly, make sure FFmpeg is installed on your system - see `6.4 Installing FFmpeg / AVConv`_.
Secondly, set your desired output format. Open the Download options window (for example, click **Edit > General download options... > Formats > Preferred**). Add a format like **mp4** to the **List of preferred formats**, then add the same format to **If a merge is required after post-processing, output to this format**.
For some reason, youtube-dl ignores the download option unless the format is specified in both places. (You will see a warning if you forget.)
-.. image:: screenshots/example24.png
+.. image:: screenshots/example25.png
:alt: The Download options window
7.15 Too many folders in the main window
@@ -1403,15 +1523,14 @@ A: **Tartube** uses a set of stock icons wherever possible. If those icons are n
*Q: Tartube doesn't download video thumbnails any more! It used to work fine!*
-A: In June 2020, **YouTube** changed its image format from **.jpg** to **.webp**. Unfortunately, most software (including the graphics libraries used by **Tartube**) don't support **.webp** images yet.
+A: In June 2020, **YouTube** changed its image format from **.jpg** to **.webp**. Unfortunately, most software (including the graphics libraries used by **Tartube**) don't support **.webp** images yet. Worse still, **YouTube** begain sending **.webp** thumbnails mislabelled as **.jpg**.
-At the time of writing, a youtube-dl fix is expected. The fix is expected to convert **.webp** thumbnails back to **.jpg** thumbnails, after downloading them. The fix may require that `Ffmpeg `__ is installed on your system.
+In September 2020, **Tartube** and **youtube-dl** added separate fixes for this problem. These fixes both depend on `FFmpeg `__, so they won't work if FFmpeg is not installed on your system - see `6.4 Installing FFmpeg / AVConv`_.
-Tartube can now look for and remove **.webp** fils automatically. You can use this procedure after the youtube-dl fix has been released.
+If you have already downloaded a lot of **.webp** images, you can ask **Tartube** to convert them back to **.jpg**. Once converted, they will be visible in the main window.
* Click **Operations > Tidy up files...**
-* In the dialogue window, click **Delete .webp/malformed .jpg files** to select it, then click the **OK** button
-* When the operation is completed, click the main **Check all** button to re-download thumbnails for all of your videos
+* In the dialogue window, click **Convert .webp files to .jpg using FFmpeg** to select it, then click the **OK** button
7.28 Tartube is not visible in the system tray
----------------------------------------------
diff --git a/icons/status/lcd_tv_off.png b/icons/status/lcd_tv_off.png
deleted file mode 100644
index d1a4a71..0000000
Binary files a/icons/status/lcd_tv_off.png and /dev/null differ
diff --git a/icons/status/status_default_icon_64.png b/icons/status/status_default_icon_64.png
index 9b67966..e902833 100644
Binary files a/icons/status/status_default_icon_64.png and b/icons/status/status_default_icon_64.png differ
diff --git a/icons/status/status_process_icon_64.png b/icons/status/status_process_icon_64.png
new file mode 100644
index 0000000..77a60eb
Binary files /dev/null and b/icons/status/status_process_icon_64.png differ
diff --git a/icons/status/status_process_icon_xmas_64.png b/icons/status/status_process_icon_xmas_64.png
new file mode 100644
index 0000000..a34fe80
Binary files /dev/null and b/icons/status/status_process_icon_xmas_64.png differ
diff --git a/icons/toolbar/test_large.png b/icons/toolbar/test_large.png
deleted file mode 100644
index 2b4cfa5..0000000
Binary files a/icons/toolbar/test_large.png and /dev/null differ
diff --git a/icons/toolbar/test_small.png b/icons/toolbar/test_small.png
deleted file mode 100644
index 485a136..0000000
Binary files a/icons/toolbar/test_small.png and /dev/null differ
diff --git a/locale/en_US/LC_MESSAGES/base.po b/locale/en_US/LC_MESSAGES/base.po
index 82ba5b1..841db0d 100644
--- a/locale/en_US/LC_MESSAGES/base.po
+++ b/locale/en_US/LC_MESSAGES/base.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.1\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-08-08 09:32+0100\n"
+"POT-Creation-Date: 2020-09-30 13:36+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: A S Lewis \n"
"Language-Team: en_US\n"
@@ -16,569 +16,598 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
-#: .././mainapp.py:2277
+#: .././mainapp.py:807
+msgid ""
+"Failed to convert a thumbnail from .webp to .jpg. No more conversions will "
+"be attempted until you install FFmpeg on your system, or (if FFmpeg is "
+"already installed) you set the correct FFmpeg path. To attempt more "
+"conversions, restart Tartube. To stop these messages, disable thumbnail "
+"conversions"
+msgstr ""
+
+#: .././mainapp.py:2330
msgid ""
"Tartube can't create the folder in which its configuration file is saved"
msgstr ""
-#: .././mainapp.py:2476
-#, python-brace-format
-msgid ""
-"Tartube is assuming that Gtk v{0}.{1}.{2} is broken; some minor cosmetic "
-"features are disabled"
-msgstr ""
-
-#: .././mainapp.py:2516
-msgid "The Tartube database file was not loaded, but is no longer protected"
-msgstr ""
-
-#: .././mainapp.py:2519
-msgid "Restart Tartube to load it"
-msgstr ""
-
-#: .././mainapp.py:2528
+#: .././mainapp.py:2542
msgid "Because of an error, file load/save has been disabled"
msgstr ""
-#: .././mainapp.py:2538
+#: .././mainapp.py:2551
msgid "Because of the error, file load/save has been disabled"
msgstr ""
-#: .././mainapp.py:2569
+#: .././mainapp.py:2586
msgid ""
"youtube-dl must be installed before you can use Tartube. Do you want to "
"install youtube-dl now?"
msgstr ""
-#: .././mainapp.py:2624
+#: .././mainapp.py:2617 .././mainwin.py:20148
+msgid ""
+"Without FFmpeg, Tartube cannot download high-resolution videos. If you have "
+"not already installed FFmpeg, then we recommend that you install it now."
+msgstr ""
+
+#: .././mainapp.py:2673
msgid "There is a download operation in progress."
msgstr ""
-#: .././mainapp.py:2626
+#: .././mainapp.py:2675
msgid "There is an update operation in progress."
msgstr ""
-#: .././mainapp.py:2628
+#: .././mainapp.py:2677
msgid "There is a refresh operation in progress."
msgstr ""
-#: .././mainapp.py:2630
+#: .././mainapp.py:2679
msgid "There is an info operation in progress."
msgstr ""
-#: .././mainapp.py:2632
+#: .././mainapp.py:2681
msgid "There is a tidy operation in progress."
msgstr ""
-#: .././mainapp.py:2637
+#: .././mainapp.py:2683
+msgid "There is a process operation in progress."
+msgstr ""
+
+#: .././mainapp.py:2688
msgid "Are you sure you want to quit Tartube?"
msgstr ""
-#: .././mainapp.py:2841
+#: .././mainapp.py:2899
msgid "Failed to load the Tartube config file (failed sanity check)"
msgstr ""
-#: .././mainapp.py:2864
+#: .././mainapp.py:2922
msgid "Failed to load the Tartube config file (file is locked)"
msgstr ""
-#: .././mainapp.py:2895
+#: .././mainapp.py:2953
msgid "Failed to load the Tartube config file (JSON load failure)"
msgstr ""
-#: .././mainapp.py:2913
+#: .././mainapp.py:2971
msgid "Failed to load the Tartube config file (file is invalid)"
msgstr ""
-#: .././mainapp.py:2931
+#: .././mainapp.py:2989
msgid ""
"Failed to load the Tartube config file (file cannot be read by this version)"
msgstr ""
-#: .././mainapp.py:2946
+#: .././mainapp.py:3004
msgid "Failed to load the Tartube config file (missing file type)"
msgstr ""
-#: .././mainapp.py:3545
+#: .././mainapp.py:3650
msgid "Failed to save the Tartube config file (failed sanity check)"
msgstr ""
-#: .././mainapp.py:3802
+#: .././mainapp.py:3928
msgid "Failed to save the Tartube config file (file is locked)"
msgstr ""
-#: .././mainapp.py:3804 .././mainapp.py:3844 .././mainapp.py:4861
-#: .././mainapp.py:4917 .././mainapp.py:4923
+#: .././mainapp.py:3930 .././mainapp.py:3970 .././mainapp.py:5022
+#: .././mainapp.py:5078 .././mainapp.py:5084
msgid "File load/save has been disabled"
msgstr ""
-#: .././mainapp.py:3823
+#: .././mainapp.py:3949
msgid "Failed to save the Tartube config file (file already in use)"
msgstr ""
-#: .././mainapp.py:3843
+#: .././mainapp.py:3969
msgid "Failed to save the Tartube config file"
msgstr ""
-#: .././mainapp.py:3892 .././mainapp.py:3910 .././mainapp.py:3940
+#: .././mainapp.py:4046 .././mainapp.py:4062 .././mainapp.py:4092
msgid "Failed to load the Tartube database file"
msgstr ""
-#: .././mainapp.py:3955
+#: .././mainapp.py:4107
msgid "The Tartube database file is invalid"
msgstr ""
-#: .././mainapp.py:3971
+#: .././mainapp.py:4123
msgid "Database file can't be read by this version of Tartube"
msgstr ""
-#: .././mainapp.py:4278
+#: .././mainapp.py:4430
msgid "Tartube is applying an essential database update"
msgstr ""
-#: .././mainapp.py:4280
+#: .././mainapp.py:4432
msgid "This might take a few minutes, so please be patient"
msgstr ""
-#: .././mainapp.py:4855 .././mainapp.py:4913 .././mainapp.py:4922
+#: .././mainapp.py:5016 .././mainapp.py:5074 .././mainapp.py:5083
msgid "Failed to save the Tartube database file"
msgstr ""
-#: .././mainapp.py:4858
+#: .././mainapp.py:5019
msgid "(Could not make a backup copy of the existing file)"
msgstr ""
-#: .././mainapp.py:4894
+#: .././mainapp.py:5055
msgid "Failed to save the Tartube database file (file already in use)"
msgstr ""
-#: .././mainapp.py:4915
+#: .././mainapp.py:5076
msgid "A backup of the previous file can be found at:"
msgstr ""
-#: .././mainapp.py:5140 .././mainapp.py:5150
+#: .././mainapp.py:5283 .././mainapp.py:5293
msgid "Database file created"
msgstr ""
-#: .././mainapp.py:5201 .././mainapp.py:5253
+#: .././mainapp.py:5461 .././mainapp.py:5513
#, python-brace-format
msgid ""
"Tartube database '{0}' can't be loaded - another instance of Tartube may be "
"using it. If not, you can fix this problem by deleting the lockfile '{1}'"
msgstr ""
-#: .././mainapp.py:5424
+#: .././mainapp.py:5684
msgid "Tartube's database can't be checked while an operation is in progress"
msgstr ""
-#: .././mainapp.py:5621
+#: .././mainapp.py:5881
msgid "Database check complete, no inconsistencies found"
msgstr ""
-#: .././mainapp.py:5648
+#: .././mainapp.py:5908
msgid "Database check complete, problems found:"
msgstr ""
-#: .././mainapp.py:5651
+#: .././mainapp.py:5911
msgid ""
"Do you want to repair these problems? (The database will be fixed, but no "
"files will be deleted)"
msgstr ""
-#: .././mainapp.py:5796
+#: .././mainapp.py:6056
msgid "Database inconsistencies repaired"
msgstr ""
-#: .././mainapp.py:6438
+#: .././mainapp.py:6863
msgid "The user declined to specify a data folder for Tartube"
msgstr ""
-#: .././mainapp.py:6538 .././config.py:10074
+#: .././mainapp.py:6963 .././config.py:10543
msgid "Please select Tartube's data folder"
msgstr ""
-#: .././mainapp.py:6664
+#: .././mainapp.py:7143
msgid ""
"A download operation cannot start if one or more configuration windows are "
"still open"
msgstr ""
-#: .././mainapp.py:6688 .././mainapp.py:6710
+#: .././mainapp.py:7167 .././mainapp.py:7189
#, python-brace-format
msgid "You only have {0} / {1} Mb remaining on your device"
msgstr ""
-#: .././mainapp.py:6713 .././mainapp.py:11657 .././mainapp.py:11773
-#: .././mainapp.py:11947 .././mainwin.py:14237
+#: .././mainapp.py:7192 .././mainapp.py:12461 .././mainapp.py:12577
+#: .././mainapp.py:12751 .././mainwin.py:14443
msgid "Are you sure you want to continue?"
msgstr ""
-#: .././mainapp.py:6794
+#: .././mainapp.py:7273
msgid "There is nothing to check!"
msgstr ""
-#: .././mainapp.py:6796
+#: .././mainapp.py:7275
msgid "There is nothing to download!"
msgstr ""
-#: .././mainapp.py:7006
+#: .././mainapp.py:7487
msgid "Download operation complete"
msgstr ""
-#: .././mainapp.py:7008
+#: .././mainapp.py:7489
msgid "Download operation halted"
msgstr ""
-#: .././mainapp.py:7011 .././mainapp.py:7478 .././mainapp.py:7924
+#: .././mainapp.py:7492 .././mainapp.py:8003 .././mainapp.py:8456
+#: .././mainapp.py:8866
msgid "Time taken:"
msgstr ""
-#: .././mainapp.py:7069
+#: .././mainapp.py:7549
msgid ""
"An update operation cannot start if one or more configuration windows are "
"still open"
msgstr ""
-#: .././mainapp.py:7182
+#: .././mainapp.py:7673
msgid "Installation failed"
msgstr ""
-#: .././mainapp.py:7184
+#: .././mainapp.py:7675
msgid "Installation complete"
msgstr ""
-#: .././mainapp.py:7188
+#: .././mainapp.py:7679
msgid "Update operation failed"
msgstr ""
-#: .././mainapp.py:7190
+#: .././mainapp.py:7681
msgid "Update operation halted"
msgstr ""
-#: .././mainapp.py:7192
+#: .././mainapp.py:7683
msgid "Update operation complete"
msgstr ""
-#: .././mainapp.py:7193
-msgid "youtube-dl version:"
+#: .././mainapp.py:7685
+msgid "version:"
msgstr ""
-#: .././mainapp.py:7197
+#: .././mainapp.py:7689
msgid "(unknown)"
msgstr ""
-#: .././mainapp.py:7271
+#: .././mainapp.py:7701
+msgid "Do you want to install FFmpeg now?"
+msgstr ""
+
+#: .././mainapp.py:7703
+msgid ""
+"(You should click Yes, even if you think FFmpeg is already installed on your "
+"system)"
+msgstr ""
+
+#: .././mainapp.py:7796
msgid ""
"A refresh operation cannot start if one or more configuration windows are "
"still open"
msgstr ""
-#: .././mainapp.py:7284
+#: .././mainapp.py:7809
msgid ""
"During a refresh operation, Tartube analyses its data folder, looking for "
"videos that haven't yet been added to its database"
msgstr ""
-#: .././mainapp.py:7288
+#: .././mainapp.py:7813
msgid ""
"You only need to perform a refresh operation if you have manually copied "
"videos into Tartube's data folder"
msgstr ""
-#: .././mainapp.py:7295
+#: .././mainapp.py:7820
msgid ""
"Before starting a refresh operation, you should click the 'Check all' button "
"in the main window"
msgstr ""
-#: .././mainapp.py:7302
+#: .././mainapp.py:7827
msgid ""
"Before starting a refresh operation, you should right-click the channel and "
"select 'Check channel'"
msgstr ""
-#: .././mainapp.py:7309
+#: .././mainapp.py:7834
msgid ""
"Before starting a refresh operation, you should right-click the playlist and "
"select 'Check playlist'"
msgstr ""
-#: .././mainapp.py:7316
+#: .././mainapp.py:7841
msgid ""
"Before starting a refresh operation, you should right-click the folder and "
"select 'Check folder'"
msgstr ""
-#: .././mainapp.py:7321
+#: .././mainapp.py:7846
msgid "Are you sure you want to proceed with the refresh operation?"
msgstr ""
-#: .././mainapp.py:7473
+#: .././mainapp.py:7998
msgid "Refresh operation complete"
msgstr ""
-#: .././mainapp.py:7475
+#: .././mainapp.py:8000
msgid "Refresh operation halted"
msgstr ""
-#: .././mainapp.py:7575
+#: .././mainapp.py:8100
msgid ""
"An info operation cannot start if one or more configuration windows are "
"still open"
msgstr ""
-#: .././mainapp.py:7688
+#: .././mainapp.py:8212
msgid "Operation failed"
msgstr ""
-#: .././mainapp.py:7690 .././downloads.py:362
+#: .././mainapp.py:8214 .././downloads.py:362
msgid "Operation complete"
msgstr ""
-#: .././mainapp.py:7692
+#: .././mainapp.py:8216
msgid "Click the Output Tab to see the results"
msgstr ""
-#: .././mainapp.py:7790
+#: .././mainapp.py:8323
msgid ""
"A tidy operation cannot start if one or more configuration windows are still "
"open"
msgstr ""
-#: .././mainapp.py:7919
+#: .././mainapp.py:8451
msgid "Tidy operation complete"
msgstr ""
-#: .././mainapp.py:7921
+#: .././mainapp.py:8453
msgid "Tidy operation halted"
msgstr ""
-#: .././mainapp.py:8061 .././mainwin.py:14661
+#: .././mainapp.py:8591 .././mainwin.py:14867
msgid "Livestream has started"
msgstr ""
-#: .././mainapp.py:9316 .././mainapp.py:9492
+#: .././mainapp.py:8720
+msgid ""
+"A process operation cannot start if one or more configuration windows are "
+"still open"
+msgstr ""
+
+#: .././mainapp.py:8861
+msgid "Process operation complete"
+msgstr ""
+
+#: .././mainapp.py:8863
+msgid "Process operation halted"
+msgstr ""
+
+#: .././mainapp.py:10102 .././mainapp.py:10277
msgid "Cannot move anything to:"
msgstr ""
-#: .././mainapp.py:9318 .././mainapp.py:9494
+#: .././mainapp.py:10104 .././mainapp.py:10279
msgid ""
"because a file or folder with the same name already exists (although "
"Tartube's database doesn't know anything about it)"
msgstr ""
-#: .././mainapp.py:9322
+#: .././mainapp.py:10108
msgid ""
"You probably created that file/folder accidentally, in which case you should "
"delete it manually before trying again"
msgstr ""
-#: .././mainapp.py:9336 .././mainapp.py:9512
+#: .././mainapp.py:10122 .././mainapp.py:10297
msgid "Are you sure you want to move this channel:"
msgstr ""
-#: .././mainapp.py:9338 .././mainapp.py:9514
+#: .././mainapp.py:10124 .././mainapp.py:10299
msgid "Are you sure you want to move this playlist:"
msgstr ""
-#: .././mainapp.py:9340 .././mainapp.py:9516
+#: .././mainapp.py:10126 .././mainapp.py:10301
msgid "Are you sure you want to move this folder:"
msgstr ""
-#: .././mainapp.py:9345
+#: .././mainapp.py:10131
msgid ""
"This procedure will move all downloaded files to the top level of Tartube's "
"data folder"
msgstr ""
-#: .././mainapp.py:9446
+#: .././mainapp.py:10231
msgid "Channels, playlists and folders can only be dragged into a folder"
msgstr ""
-#: .././mainapp.py:9459
+#: .././mainapp.py:10244
#, python-brace-format
msgid "The fixed folder '{0}' cannot be moved (but it can still be hidden)"
msgstr ""
-#: .././mainapp.py:9472
+#: .././mainapp.py:10257
#, python-brace-format
msgid "The folder '{0}' can only contain videos"
msgstr ""
-#: .././mainapp.py:9499
+#: .././mainapp.py:10284
msgid ""
"You probably created that file/folder accidentally, in which case, you "
"should delete it manually before trying again"
msgstr ""
-#: .././mainapp.py:9518
+#: .././mainapp.py:10303
msgid "into this folder:"
msgstr ""
-#: .././mainapp.py:9522
+#: .././mainapp.py:10307
msgid "This procedure will move all downloaded files to the new location"
msgstr ""
-#: .././mainapp.py:9528
+#: .././mainapp.py:10313
msgid ""
"WARNING: The destination folder is marked as temporary, so everything inside "
"it will be DELETED when Tartube restarts!"
msgstr ""
-#: .././mainapp.py:9918
+#: .././mainapp.py:10714
msgid ""
"Are you SURE you want to delete files? This procedure cannot be reversed!"
msgstr ""
-#: .././mainapp.py:11641 .././mainapp.py:11757 .././mainapp.py:11931
+#: .././mainapp.py:12445 .././mainapp.py:12561 .././mainapp.py:12735
#, python-brace-format
msgid "The channel contains {0} item(s), so this action may take a while"
msgstr ""
-#: .././mainapp.py:11647 .././mainapp.py:11763 .././mainapp.py:11937
+#: .././mainapp.py:12451 .././mainapp.py:12567 .././mainapp.py:12741
#, python-brace-format
msgid "The playlist contains {0} item(s), so this action may take a while"
msgstr ""
-#: .././mainapp.py:11653 .././mainapp.py:11769 .././mainapp.py:11943
+#: .././mainapp.py:12457 .././mainapp.py:12573 .././mainapp.py:12747
#, python-brace-format
msgid "The folder contains {0} item(s), so this action may take a while"
msgstr ""
-#: .././mainapp.py:12011 .././mainapp.py:14653 .././mainapp.py:14785
-#: .././mainapp.py:14916
+#: .././mainapp.py:12815 .././mainapp.py:15563 .././mainapp.py:15695
+#: .././mainapp.py:15826
#, python-brace-format
msgid "The name '{0}' is not allowed"
msgstr ""
-#: .././mainapp.py:12020
+#: .././mainapp.py:12824
#, python-brace-format
msgid "The name '{0}' is already in use"
msgstr ""
-#: .././mainapp.py:12033
+#: .././mainapp.py:12837
#, python-brace-format
msgid "Failed to rename '{0}'"
msgstr ""
-#: .././mainapp.py:12351
+#: .././mainapp.py:13155
msgid "Select where to save the database export"
msgstr ""
-#: .././mainapp.py:12480
+#: .././mainapp.py:13284
msgid "There is nothing to export!"
msgstr ""
-#: .././mainapp.py:12513 .././mainapp.py:12571
-msgid "Failed to save the database export file"
+#: .././mainapp.py:13324 .././mainapp.py:13390
+msgid "Failed to save the database export file:"
msgstr ""
-#: .././mainapp.py:12578
+#: .././mainapp.py:13398
msgid "Database export file saved to:"
msgstr ""
-#: .././mainapp.py:12615
+#: .././mainapp.py:13435
msgid "Select the database export"
msgstr ""
-#: .././mainapp.py:12640 .././mainapp.py:12654
+#: .././mainapp.py:13460 .././mainapp.py:13474
msgid "Failed to load the database export file"
msgstr ""
-#: .././mainapp.py:12671
+#: .././mainapp.py:13491
msgid "The database export file is invalid"
msgstr ""
-#: .././mainapp.py:12682
+#: .././mainapp.py:13502
msgid "The database export file is invalid (or empty)"
msgstr ""
-#: .././mainapp.py:12726
+#: .././mainapp.py:13546
msgid "Nothing was imported from the database export file"
msgstr ""
#. Show a confirmation
-#: .././mainapp.py:12740
+#: .././mainapp.py:13560
msgid "Imported:"
msgstr ""
-#: .././mainapp.py:12741
+#: .././mainapp.py:13561
msgid "Videos:"
msgstr ""
-#: .././mainapp.py:12742
+#: .././mainapp.py:13562
msgid "Channels:"
msgstr ""
-#: .././mainapp.py:12743
+#: .././mainapp.py:13563
msgid "Playlists:"
msgstr ""
-#: .././mainapp.py:12744
+#: .././mainapp.py:13564
msgid "Folders:"
msgstr ""
-#: .././mainapp.py:13105
+#: .././mainapp.py:13948
msgid ""
"The video file is missing from Tartube's data folder (try downloading the "
"video again!)"
msgstr ""
-#: .././mainapp.py:13800
+#: .././mainapp.py:14708
msgid "Please select a destination folder"
msgstr ""
-#: .././mainapp.py:13974
+#: .././mainapp.py:14882
msgid "No video(s) have been downloaded"
msgstr ""
#. Prompt for confirmation
-#: .././mainapp.py:14064
+#: .././mainapp.py:14972
msgid "Are you sure you want to remove the selected item(s)?"
msgstr ""
-#: .././mainapp.py:14644
+#: .././mainapp.py:15554
msgid "You must give the channel a name"
msgstr ""
-#: .././mainapp.py:14662 .././mainapp.py:14925
+#: .././mainapp.py:15572 .././mainapp.py:15835
msgid "You must enter a valid URL"
msgstr ""
-#: .././mainapp.py:14777
+#: .././mainapp.py:15687
msgid "You must give the folder a name"
msgstr ""
-#: .././mainapp.py:14907
+#: .././mainapp.py:15817
msgid "You must give the playlist a name"
msgstr ""
-#: .././mainapp.py:15062 .././mainwin.py:14132
+#: .././mainapp.py:15972 .././mainwin.py:14338
msgid "The following videos are duplicates:"
msgstr ""
-#: .././mainapp.py:15126
+#: .././mainapp.py:16036
msgid "There were no livestream alerts to cancel"
msgstr ""
-#: .././mainapp.py:15128
+#: .././mainapp.py:16038
msgid "Livestream alerts for 1 video were cancelled"
msgstr "Livestream alerts for 1 video were canceled"
-#: .././mainapp.py:15131
+#: .././mainapp.py:16041
#, python-brace-format
msgid "Livestream alerts for {0} videos were cancelled"
msgstr "Livestream alerts for {0} videos were canceled"
-#: .././mainapp.py:15432
+#: .././mainapp.py:16342
msgid "Data saved"
msgstr ""
-#: .././mainapp.py:15462
+#: .././mainapp.py:16372
msgid "Database saved"
msgstr ""
-#: .././mainapp.py:15686 .././mainwin.py:11087
+#: .././mainapp.py:16627 .././mainwin.py:11127
msgid ""
"Files cannot be recovered, after being deleted. Are you sure you want to "
"continue?"
@@ -587,250 +616,254 @@ msgstr ""
#. Because livestream operations run silently in the background, when
#. the user goes to the trouble of clicking a menu item in the
#. main window's menu, tell them why nothing is happening
-#: .././mainapp.py:15726
+#: .././mainapp.py:16667
msgid "Cannot update existing livestreams because"
msgstr ""
-#: .././mainapp.py:15728
+#: .././mainapp.py:16669
msgid "there is another operation running"
msgstr ""
-#: .././mainapp.py:15730
+#: .././mainapp.py:16671
msgid "they are currently being updated"
msgstr ""
-#: .././mainapp.py:15732
+#: .././mainapp.py:16673
msgid "one or more configuration windows are open"
msgstr ""
-#: .././mainapp.py:15734
+#: .././mainapp.py:16675
msgid "there are no livestreams to update"
msgstr ""
-#: .././mainapp.py:15808
+#: .././mainapp.py:16749
msgid "There is already a channel with that name"
msgstr ""
-#: .././mainapp.py:15810
+#: .././mainapp.py:16751
msgid "There is already a playlist with that name"
msgstr ""
-#: .././mainapp.py:15812
+#: .././mainapp.py:16753
msgid "There is already a folder with that name"
msgstr ""
-#: .././mainapp.py:15815
+#: .././mainapp.py:16756
msgid "(so please choose a different name)"
msgstr ""
-#: .././mainwin.py:715
+#: .././mainwin.py:719
msgid "Tartube cannot start because it cannot find its icons folder"
msgstr ""
#. File column
-#: .././mainwin.py:805
+#: .././mainwin.py:809
msgid "_File"
msgstr ""
-#: .././mainwin.py:812
+#: .././mainwin.py:816
msgid "_Database preferences..."
msgstr ""
-#: .././mainwin.py:821
+#: .././mainwin.py:825
msgid "_Save database"
msgstr ""
-#: .././mainwin.py:827
+#: .././mainwin.py:831
msgid "Save _all"
msgstr ""
-#: .././mainwin.py:836
+#: .././mainwin.py:840
msgid "_Close to tray"
msgstr ""
#. Quit
-#: .././mainwin.py:841 .././mainwin.py:17368
+#: .././mainwin.py:845 .././mainwin.py:17587
msgid "_Quit"
msgstr ""
#. Edit column
-#: .././mainwin.py:846
+#: .././mainwin.py:850
msgid "_Edit"
msgstr ""
-#: .././mainwin.py:853
+#: .././mainwin.py:857
msgid "_System preferences..."
msgstr ""
-#: .././mainwin.py:859
+#: .././mainwin.py:863
msgid "_General download options..."
msgstr ""
#. Media column
-#: .././mainwin.py:865
+#: .././mainwin.py:869
msgid "_Media"
msgstr ""
-#: .././mainwin.py:872
+#: .././mainwin.py:876
msgid "Add _videos..."
msgstr ""
-#: .././mainwin.py:878
+#: .././mainwin.py:882
msgid "Add _channel..."
msgstr ""
-#: .././mainwin.py:884
+#: .././mainwin.py:888
msgid "Add _playlist..."
msgstr ""
-#: .././mainwin.py:890
+#: .././mainwin.py:894
msgid "Add _folder..."
msgstr ""
-#: .././mainwin.py:899
+#: .././mainwin.py:903
msgid "_Export from database"
msgstr ""
-#: .././mainwin.py:907
+#: .././mainwin.py:911
msgid "_JSON export file"
msgstr ""
-#: .././mainwin.py:913
+#: .././mainwin.py:917
msgid "Plain _text export file"
msgstr ""
-#: .././mainwin.py:919
+#: .././mainwin.py:923
msgid "_Import into database"
msgstr ""
-#: .././mainwin.py:928
+#: .././mainwin.py:932
msgid "_Switch between views"
msgstr ""
-#: .././mainwin.py:933
+#: .././mainwin.py:937
msgid "Show _hidden folders"
msgstr ""
-#: .././mainwin.py:943
+#: .././mainwin.py:950
msgid "_Add test media"
msgstr ""
+#: .././mainwin.py:958
+msgid "_Run test code"
+msgstr ""
+
#. Operations column
#. Add this tab...
-#: .././mainwin.py:949 .././config.py:7993
+#: .././mainwin.py:964 .././config.py:8246
msgid "_Operations"
msgstr ""
#. Check all
-#: .././mainwin.py:956 .././mainwin.py:17339
+#: .././mainwin.py:971 .././mainwin.py:17558
msgid "_Check all"
msgstr ""
#. Download all
-#: .././mainwin.py:962 .././mainwin.py:17346
+#: .././mainwin.py:977 .././mainwin.py:17565
msgid "_Download all"
msgstr ""
-#: .././mainwin.py:967
+#: .././mainwin.py:982
msgid "C_ustom download all"
msgstr ""
-#: .././mainwin.py:975
+#: .././mainwin.py:990
msgid "_Refresh database..."
msgstr ""
-#: .././mainwin.py:984
-msgid "Update _youtube-dl"
+#: .././mainwin.py:1000
+msgid "U_pdate"
msgstr ""
-#: .././mainwin.py:990
-msgid "_Test youtube-dl..."
+#: .././mainwin.py:1006
+msgid "_Test"
msgstr ""
-#: .././mainwin.py:999
+#: .././mainwin.py:1015
msgid "_Install FFmpeg"
msgstr ""
-#: .././mainwin.py:1010
+#: .././mainwin.py:1026
msgid "Tidy up _files..."
msgstr ""
-#: .././mainwin.py:1021 .././mainwin.py:17357
+#: .././mainwin.py:1037 .././mainwin.py:17576
msgid "_Stop current operation"
msgstr ""
#. Livestreams column
-#: .././mainwin.py:1028 .././config.py:8263
+#: .././mainwin.py:1044 .././config.py:8545
msgid "_Livestreams"
msgstr ""
-#: .././mainwin.py:1035
+#: .././mainwin.py:1051
msgid "_Livestream preferences..."
msgstr ""
-#: .././mainwin.py:1044
+#: .././mainwin.py:1060
msgid "_Update existing livestreams"
msgstr ""
-#: .././mainwin.py:1049
+#: .././mainwin.py:1065
msgid "_Cancel all livestream alerts"
msgstr ""
#. Help column
-#: .././mainwin.py:1054
+#: .././mainwin.py:1070
msgid "_Help"
msgstr ""
-#: .././mainwin.py:1060
+#: .././mainwin.py:1076
msgid "_About..."
msgstr ""
-#: .././mainwin.py:1065
+#: .././mainwin.py:1081
msgid "Go to _website"
msgstr ""
-#: .././mainwin.py:1071
+#: .././mainwin.py:1087
msgid "Send _feedback"
msgstr ""
-#: .././mainwin.py:1108
+#: .././mainwin.py:1124
msgid "Videos"
msgstr ""
-#: .././mainwin.py:1118
+#: .././mainwin.py:1134
msgid "Add new video(s)"
msgstr ""
-#: .././mainwin.py:1127
+#: .././mainwin.py:1143
msgid "Channel"
msgstr ""
-#: .././mainwin.py:1137
+#: .././mainwin.py:1153
msgid "Add a new channel"
msgstr ""
-#: .././mainwin.py:1148
+#: .././mainwin.py:1164
msgid "Playlist"
msgstr ""
-#: .././mainwin.py:1158
+#: .././mainwin.py:1174
msgid "Add a new playlist"
msgstr ""
-#: .././mainwin.py:1169
+#: .././mainwin.py:1185
msgid "Folder"
msgstr ""
-#: .././mainwin.py:1179
+#: .././mainwin.py:1195
msgid "Add a new folder"
msgstr ""
-#: .././mainwin.py:1193
+#: .././mainwin.py:1209
msgid "Check"
msgstr ""
-#: .././mainwin.py:1204 .././mainwin.py:1436 .././mainwin.py:3027
-#: .././mainwin.py:3197
+#: .././mainwin.py:1220 .././mainwin.py:1429 .././mainwin.py:3044
+#: .././mainwin.py:3216
msgid "Check all videos, channels, playlists and folders"
msgstr ""
@@ -839,21 +872,21 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:1214 .././mainwin.py:15718 .././mainwin.py:15726
-#: .././mainwin.py:15977 .././mainwin.py:15989 .././mainwin.py:16705
+#: .././mainwin.py:1230 .././mainwin.py:15927 .././mainwin.py:15935
+#: .././mainwin.py:16190 .././mainwin.py:16202 .././mainwin.py:16919
msgid "Download"
msgstr ""
-#: .././mainwin.py:1225 .././mainwin.py:1444 .././mainwin.py:3035
-#: .././mainwin.py:3203
+#: .././mainwin.py:1241 .././mainwin.py:1437 .././mainwin.py:3052
+#: .././mainwin.py:3222
msgid "Download all videos, channels, playlists and folders"
msgstr ""
-#: .././mainwin.py:1240
+#: .././mainwin.py:1256
msgid "Stop"
msgstr ""
-#: .././mainwin.py:1252
+#: .././mainwin.py:1268
msgid "Stop the current operation"
msgstr ""
@@ -863,176 +896,168 @@ msgstr ""
#. produces no error)
#. selection = treeview.get_selection()
#. selection.set_mode(Gtk.SelectionMode.MULTIPLE)
-#: .././mainwin.py:1264 .././config.py:6748
+#: .././mainwin.py:1280 .././config.py:6922
msgid "Switch"
msgstr ""
-#: .././mainwin.py:1275
+#: .././mainwin.py:1291
msgid "Switch between simple and complex views"
msgstr ""
-#: .././mainwin.py:1289 .././config.py:8403
-msgid "Test"
-msgstr ""
-
-#: .././mainwin.py:1300
-msgid "Add test media data objects"
-msgstr ""
-
-#: .././mainwin.py:1313
+#: .././mainwin.py:1306
msgid "Quit"
msgstr ""
-#: .././mainwin.py:1323
+#: .././mainwin.py:1316
msgid "Close Tartube"
msgstr ""
-#: .././mainwin.py:1345
+#: .././mainwin.py:1338
msgid "_Videos"
msgstr ""
-#: .././mainwin.py:1353
+#: .././mainwin.py:1346
msgid "_Progress"
msgstr ""
-#: .././mainwin.py:1361
+#: .././mainwin.py:1354
msgid "_Classic Mode"
msgstr ""
-#: .././mainwin.py:1369
+#: .././mainwin.py:1362
msgid "_Output"
msgstr ""
-#: .././mainwin.py:1378 .././config.py:5398 .././config.py:5750
+#: .././mainwin.py:1371 .././config.py:5442 .././config.py:5794
msgid "_Errors / Warnings"
msgstr ""
-#: .././mainwin.py:1434 .././mainwin.py:3025 .././mainwin.py:3194
+#: .././mainwin.py:1427 .././mainwin.py:3042 .././mainwin.py:3213
msgid "Check all"
msgstr ""
-#: .././mainwin.py:1442 .././mainwin.py:2482 .././mainwin.py:3033
+#: .././mainwin.py:1435 .././mainwin.py:2493 .././mainwin.py:3050
msgid "Download all"
msgstr ""
-#: .././mainwin.py:1499
+#: .././mainwin.py:1492
msgid "Page"
msgstr ""
-#: .././mainwin.py:1511
+#: .././mainwin.py:1504
msgid "Set visible page"
msgstr ""
-#: .././mainwin.py:1535 .././mainwin.py:1843 .././mainwin.py:1904
-#: .././mainwin.py:2336
+#: .././mainwin.py:1528 .././mainwin.py:1836 .././mainwin.py:1904
+#: .././mainwin.py:2340
msgid "Size"
msgstr ""
-#: .././mainwin.py:1546
+#: .././mainwin.py:1539
msgid "Set page size"
msgstr ""
-#: .././mainwin.py:1567
+#: .././mainwin.py:1560
msgid "Go to first page"
msgstr ""
-#: .././mainwin.py:1582
+#: .././mainwin.py:1575
msgid "Go to previous page"
msgstr ""
-#: .././mainwin.py:1599
+#: .././mainwin.py:1592
msgid "Go to next page"
msgstr ""
-#: .././mainwin.py:1614
+#: .././mainwin.py:1607
msgid "Go to last page"
msgstr ""
-#: .././mainwin.py:1629
+#: .././mainwin.py:1622
msgid "Scroll up"
msgstr ""
-#: .././mainwin.py:1644
+#: .././mainwin.py:1637
msgid "Scroll down"
msgstr ""
-#: .././mainwin.py:1662 .././mainwin.py:3438
+#: .././mainwin.py:1655 .././mainwin.py:3455
msgid "Show filter options"
msgstr ""
-#: .././mainwin.py:1675
+#: .././mainwin.py:1668
msgid "Sort by"
msgstr ""
-#: .././mainwin.py:1690 .././mainwin.py:3510
+#: .././mainwin.py:1683 .././mainwin.py:3527
msgid "Sort alphabetically"
msgstr ""
-#: .././mainwin.py:1700
+#: .././mainwin.py:1693
msgid "Filter"
msgstr ""
-#: .././mainwin.py:1709
+#: .././mainwin.py:1702
msgid "Enter search text"
msgstr ""
-#: .././mainwin.py:1714
+#: .././mainwin.py:1707
msgid "Regex"
msgstr ""
-#: .././mainwin.py:1722
+#: .././mainwin.py:1715
msgid "Select if search text is a regex"
msgstr ""
-#: .././mainwin.py:1739
+#: .././mainwin.py:1732
msgid "Filter videos"
msgstr ""
-#: .././mainwin.py:1756
+#: .././mainwin.py:1749
msgid "Cancel filter"
msgstr ""
-#: .././mainwin.py:1767
+#: .././mainwin.py:1760
msgid "Find date"
msgstr ""
-#: .././mainwin.py:1781
+#: .././mainwin.py:1774
msgid "Find videos by date"
msgstr ""
-#: .././mainwin.py:1836
+#: .././mainwin.py:1829
msgid "TRANSLATOR'S NOTE: Ext is short for a file extension, e.g. .EXE"
msgstr ""
-#: .././mainwin.py:1841 .././mainwin.py:2334
+#: .././mainwin.py:1834 .././mainwin.py:2338
msgid "Source"
msgstr ""
-#: .././mainwin.py:1841 .././mainwin.py:2334
+#: .././mainwin.py:1834 .././mainwin.py:2338
msgid "Status"
msgstr ""
-#: .././mainwin.py:1842 .././mainwin.py:2335
+#: .././mainwin.py:1835 .././mainwin.py:2339
msgid "Incoming file"
msgstr ""
-#: .././mainwin.py:1842 .././mainwin.py:2335
+#: .././mainwin.py:1835 .././mainwin.py:2339
msgid "Ext"
msgstr ""
-#: .././mainwin.py:1842 .././mainwin.py:2335
+#: .././mainwin.py:1835 .././mainwin.py:2339
msgid "Speed"
msgstr ""
-#: .././mainwin.py:1842 .././mainwin.py:2335
+#: .././mainwin.py:1835 .././mainwin.py:2339
msgid "ETA"
msgstr ""
-#: .././mainwin.py:1904 .././config.py:5662
+#: .././mainwin.py:1904 .././config.py:5706
msgid "New videos"
msgstr ""
-#: .././mainwin.py:1904 .././config.py:5175
+#: .././mainwin.py:1904 .././config.py:5219
msgid "Duration"
msgstr ""
@@ -1040,7 +1065,7 @@ msgstr ""
msgid "Date"
msgstr ""
-#: .././mainwin.py:1905 .././config.py:5146
+#: .././mainwin.py:1905 .././config.py:5190
msgid "File"
msgstr ""
@@ -1048,42 +1073,42 @@ msgstr ""
msgid "Downloaded to"
msgstr ""
-#: .././mainwin.py:1961
+#: .././mainwin.py:1965
msgid "Max downloads"
msgstr ""
-#: .././mainwin.py:1984
+#: .././mainwin.py:1988
msgid "D/L speed (KiB/s)"
msgstr ""
-#: .././mainwin.py:2010 .././config.py:2402
+#: .././mainwin.py:2014 .././config.py:2404
msgid "Video resolution"
msgstr ""
-#: .././mainwin.py:2045
+#: .././mainwin.py:2049
msgid "Hide rows when they are finished"
msgstr ""
-#: .././mainwin.py:2058
+#: .././mainwin.py:2062
msgid "Add newest videos to the top of the list"
msgstr ""
-#: .././mainwin.py:2117
+#: .././mainwin.py:2121
msgid "This tab emulates the classic youtube-dl-gui interface"
msgstr ""
-#: .././mainwin.py:2125
+#: .././mainwin.py:2129
msgid "Videos downloaded here are not added to Tartube's database"
msgstr ""
-#: .././mainwin.py:2147
+#: .././mainwin.py:2151
msgid "Open the Classic Mode menu"
msgstr ""
#. Second row - a textview for entering URLs. If automatic copy/paste is
#. enabled, URLs are automatically copied into this textview
#. --------------------------------------------------------------------
-#: .././mainwin.py:2154
+#: .././mainwin.py:2158
msgid "Enter URLs below"
msgstr ""
@@ -1093,955 +1118,967 @@ msgstr ""
#. the specified destination and format
#. --------------------------------------------------------------------
#. Destination directory
-#: .././mainwin.py:2193
+#: .././mainwin.py:2197
msgid "Destination:"
msgstr ""
-#: .././mainwin.py:2230
+#: .././mainwin.py:2234
msgid "Add a new destination folder"
msgstr ""
-#: .././mainwin.py:2249
+#: .././mainwin.py:2253
msgid "Open the destination folder"
msgstr ""
#. Video/audio format
-#: .././mainwin.py:2254
+#: .././mainwin.py:2258
msgid "Format:"
msgstr ""
-#: .././mainwin.py:2257
+#: .././mainwin.py:2261
msgid "Default"
msgstr ""
-#: .././mainwin.py:2257 .././mainwin.py:13380
+#: .././mainwin.py:2261 .././mainwin.py:13586
msgid "Video:"
msgstr ""
-#: .././mainwin.py:2261 .././mainwin.py:13380
+#: .././mainwin.py:2265 .././mainwin.py:13586
msgid "Audio:"
msgstr ""
-#: .././mainwin.py:2291
+#: .././mainwin.py:2295
msgid "Add URLs"
msgstr ""
-#: .././mainwin.py:2297
+#: .././mainwin.py:2301
msgid "Add these URLs"
msgstr ""
-#: .././mainwin.py:2380
+#: .././mainwin.py:2391
msgid "Remove from list"
msgstr ""
-#: .././mainwin.py:2403
+#: .././mainwin.py:2414
msgid "Play video"
msgstr ""
#. Signal connect below
-#: .././mainwin.py:2419 .././config.py:2755 .././config.py:6785
+#: .././mainwin.py:2430 .././config.py:2799 .././config.py:6959
msgid "Move up"
msgstr ""
#. Signal connect below
#. signal connect appears below
-#: .././mainwin.py:2440 .././config.py:2759 .././config.py:6793
+#: .././mainwin.py:2451 .././config.py:2803 .././config.py:6967
msgid "Move down"
msgstr ""
-#: .././mainwin.py:2456
+#: .././mainwin.py:2467
msgid "Re-download"
msgstr ""
-#: .././mainwin.py:2479
+#: .././mainwin.py:2490
msgid "Stop download"
msgstr ""
-#: .././mainwin.py:2489
+#: .././mainwin.py:2500
msgid "Download the URLs above"
msgstr ""
-#: .././mainwin.py:2552
+#: .././mainwin.py:2563
msgid "Time"
msgstr ""
-#: .././mainwin.py:2552
+#: .././mainwin.py:2563
msgid "Type"
msgstr ""
-#: .././mainwin.py:2552
+#: .././mainwin.py:2563
msgid "Message"
msgstr ""
-#: .././mainwin.py:2586
+#: .././mainwin.py:2597
msgid "Show Tartube errors"
msgstr ""
-#: .././mainwin.py:2599
+#: .././mainwin.py:2610
msgid "Show Tartube warnings"
msgstr ""
-#: .././mainwin.py:2612
+#: .././mainwin.py:2623
msgid "Show server errors"
msgstr ""
-#: .././mainwin.py:2630
+#: .././mainwin.py:2641
msgid "Show server warnings"
msgstr ""
-#: .././mainwin.py:2642
+#: .././mainwin.py:2653
msgid "Clear list"
msgstr ""
-#: .././mainwin.py:2953 .././mainwin.py:2981
+#: .././mainwin.py:2966 .././mainwin.py:2996
msgid "Checking..."
msgstr ""
-#: .././mainwin.py:2955 .././mainwin.py:2983
+#: .././mainwin.py:2968 .././mainwin.py:2998
msgid "Downloading..."
msgstr ""
-#: .././mainwin.py:2957 .././mainwin.py:2985
+#: .././mainwin.py:2970 .././mainwin.py:3000
msgid "Refreshing..."
msgstr ""
-#: .././mainwin.py:2959 .././mainwin.py:2987
+#: .././mainwin.py:2972 .././mainwin.py:3002
msgid "Tidying..."
msgstr ""
-#: .././mainwin.py:3173
+#: .././mainwin.py:2974
+msgid "FFmpeg processing..."
+msgstr ""
+
+#: .././mainwin.py:3004
+msgid "FFmpeg Processing..."
+msgstr ""
+
+#: .././mainwin.py:3192
msgid "Installing"
msgstr ""
-#: .././mainwin.py:3176
+#: .././mainwin.py:3195
msgid "Updating"
msgstr ""
-#: .././mainwin.py:3179 .././mainwin.py:3182
+#: .././mainwin.py:3198 .././mainwin.py:3201
msgid "Fetching"
msgstr ""
-#: .././mainwin.py:3185
+#: .././mainwin.py:3204
msgid "Testing"
msgstr ""
-#: .././mainwin.py:3461
+#: .././mainwin.py:3478
msgid "Hide filter options"
msgstr ""
-#: .././mainwin.py:3526
+#: .././mainwin.py:3543
msgid "Sort by date"
msgstr ""
-#: .././mainwin.py:3752
+#: .././mainwin.py:3769
msgid "_Check channel"
msgstr ""
-#: .././mainwin.py:3754
+#: .././mainwin.py:3771
msgid "_Check playlist"
msgstr ""
-#: .././mainwin.py:3756
+#: .././mainwin.py:3773
msgid "_Check folder"
msgstr ""
-#: .././mainwin.py:3773
+#: .././mainwin.py:3790
msgid "_Download channel"
msgstr ""
-#: .././mainwin.py:3775
+#: .././mainwin.py:3792
msgid "_Download playlist"
msgstr ""
-#: .././mainwin.py:3777
+#: .././mainwin.py:3794
msgid "_Download folder"
msgstr ""
-#: .././mainwin.py:3794
+#: .././mainwin.py:3811
msgid "C_ustom download channel"
msgstr ""
-#: .././mainwin.py:3796
+#: .././mainwin.py:3813
msgid "C_ustom download playlist"
msgstr ""
-#: .././mainwin.py:3798
+#: .././mainwin.py:3815
msgid "C_ustom download folder"
msgstr ""
-#: .././mainwin.py:3843
+#: .././mainwin.py:3860
msgid "_Empty folder"
msgstr ""
-#: .././mainwin.py:3855
+#: .././mainwin.py:3872
msgid "_All contents"
msgstr ""
-#: .././mainwin.py:3873
+#: .././mainwin.py:3890
msgid "_Remove videos"
msgstr ""
-#: .././mainwin.py:3885
+#: .././mainwin.py:3902
msgid "_Just folder videos"
msgstr ""
-#: .././mainwin.py:3891
+#: .././mainwin.py:3908
msgid "Channel co_ntents"
msgstr ""
-#: .././mainwin.py:3893
+#: .././mainwin.py:3910
msgid "Playlist co_ntents"
msgstr ""
-#: .././mainwin.py:3895
+#: .././mainwin.py:3912
msgid "Folder co_ntents"
msgstr ""
-#: .././mainwin.py:3907
+#: .././mainwin.py:3924
msgid "_Move to top level"
msgstr ""
-#: .././mainwin.py:3924
+#: .././mainwin.py:3941
msgid "_Convert to playlist"
msgstr ""
-#: .././mainwin.py:3926
+#: .././mainwin.py:3943
msgid "_Convert to channel"
msgstr ""
-#: .././mainwin.py:3948
+#: .././mainwin.py:3965
msgid "_Hide folder"
msgstr ""
-#: .././mainwin.py:3958
+#: .././mainwin.py:3975
msgid "_Rename channel..."
msgstr ""
-#: .././mainwin.py:3960
+#: .././mainwin.py:3977
msgid "_Rename playlist..."
msgstr ""
-#: .././mainwin.py:3962
+#: .././mainwin.py:3979
msgid "_Rename folder..."
msgstr ""
-#: .././mainwin.py:3979
+#: .././mainwin.py:3996
msgid "Set _nickname..."
msgstr ""
-#: .././mainwin.py:3994
+#: .././mainwin.py:4011
msgid "Set _URL..."
msgstr ""
-#: .././mainwin.py:4006
+#: .././mainwin.py:4023
msgid "Set _download destination..."
msgstr ""
-#: .././mainwin.py:4022
+#: .././mainwin.py:4039
msgid "_Export channel..."
msgstr ""
-#: .././mainwin.py:4024
+#: .././mainwin.py:4041
msgid "_Export playlist..."
msgstr ""
-#: .././mainwin.py:4026
+#: .././mainwin.py:4043
msgid "_Export folder..."
msgstr ""
-#: .././mainwin.py:4039
+#: .././mainwin.py:4056
msgid "Re_fresh channel"
msgstr ""
-#: .././mainwin.py:4041
+#: .././mainwin.py:4058
msgid "Re_fresh playlist"
msgstr ""
-#: .././mainwin.py:4043
+#: .././mainwin.py:4060
msgid "Re_fresh folder"
msgstr ""
-#: .././mainwin.py:4060
+#: .././mainwin.py:4077
msgid "_Tidy up channel"
msgstr ""
-#: .././mainwin.py:4062
+#: .././mainwin.py:4079
msgid "_Tidy up playlist"
msgstr ""
-#: .././mainwin.py:4064
+#: .././mainwin.py:4081
msgid "_Tidy up folder"
msgstr ""
-#: .././mainwin.py:4081 .././mainwin.py:4870 .././mainwin.py:5842
+#: .././mainwin.py:4098 .././mainwin.py:4903 .././mainwin.py:5904
msgid "Add to _Classic Mode tab"
msgstr ""
-#: .././mainwin.py:4094
+#: .././mainwin.py:4111
msgid "Channel _actions"
msgstr ""
-#: .././mainwin.py:4096
+#: .././mainwin.py:4113
msgid "Playlist _actions"
msgstr ""
-#: .././mainwin.py:4098
+#: .././mainwin.py:4115
msgid "Folder _actions"
msgstr ""
-#: .././mainwin.py:4118 .././mainwin.py:4432
+#: .././mainwin.py:4135 .././mainwin.py:4450
msgid "_Apply download options..."
msgstr ""
-#: .././mainwin.py:4136 .././mainwin.py:4446
+#: .././mainwin.py:4153 .././mainwin.py:4464
msgid "_Remove download options"
msgstr ""
-#: .././mainwin.py:4152 .././mainwin.py:4458
+#: .././mainwin.py:4169 .././mainwin.py:4476
msgid "_Edit download options..."
msgstr ""
-#: .././mainwin.py:4168
+#: .././mainwin.py:4185
msgid "_Show system command"
msgstr ""
-#: .././mainwin.py:4181
+#: .././mainwin.py:4198
msgid "_Disable checking/downloading"
msgstr ""
-#: .././mainwin.py:4193
+#: .././mainwin.py:4210
msgid "_Just disable downloading"
msgstr ""
-#: .././mainwin.py:4218 .././mainwin.py:4517
+#: .././mainwin.py:4235 .././mainwin.py:4535
msgid "D_ownloads"
msgstr ""
-#: .././mainwin.py:4226
+#: .././mainwin.py:4243
msgid "Channel _properties..."
msgstr ""
-#: .././mainwin.py:4228
+#: .././mainwin.py:4245
msgid "Playlist _properties..."
msgstr ""
-#: .././mainwin.py:4230
+#: .././mainwin.py:4247
msgid "Folder _properties..."
msgstr ""
-#: .././mainwin.py:4246
+#: .././mainwin.py:4263
msgid "_Default location"
msgstr ""
-#: .././mainwin.py:4259
+#: .././mainwin.py:4276
msgid "_Actual location"
msgstr ""
-#: .././mainwin.py:4271
+#: .././mainwin.py:4288
msgid "_Show"
msgstr ""
-#: .././mainwin.py:4280
+#: .././mainwin.py:4297
msgid "D_elete channel"
msgstr ""
-#: .././mainwin.py:4282
+#: .././mainwin.py:4299
msgid "D_elete playlist"
msgstr ""
-#: .././mainwin.py:4284
+#: .././mainwin.py:4301
msgid "D_elete folder"
msgstr ""
-#: .././mainwin.py:4343
+#: .././mainwin.py:4361
msgid "_Check video"
msgstr ""
-#: .././mainwin.py:4365
+#: .././mainwin.py:4383
msgid "_Download video"
msgstr ""
-#: .././mainwin.py:4386
+#: .././mainwin.py:4404
msgid "Re-_download this video"
msgstr ""
-#: .././mainwin.py:4399
+#: .././mainwin.py:4417
msgid "C_ustom download video"
msgstr ""
-#: .././mainwin.py:4474
+#: .././mainwin.py:4492
msgid "Show system _command"
msgstr ""
-#: .././mainwin.py:4484
+#: .././mainwin.py:4502
msgid "_Test system command"
msgstr ""
-#: .././mainwin.py:4499
+#: .././mainwin.py:4517
msgid "_Disable downloads"
msgstr ""
-#: .././mainwin.py:4529
+#: .././mainwin.py:4541 .././mainwin.py:4930 .././mainwin.py:5404
+msgid "_Process with FFmpeg..."
+msgstr ""
+
+#: .././mainwin.py:4561
msgid "Video is _archived"
msgstr ""
-#: .././mainwin.py:4542
+#: .././mainwin.py:4574
msgid "Video is _bookmarked"
msgstr ""
-#: .././mainwin.py:4553
+#: .././mainwin.py:4585
msgid "Video is _favourite"
msgstr ""
-#: .././mainwin.py:4564
+#: .././mainwin.py:4596
msgid "Video is _missing"
msgstr ""
-#: .././mainwin.py:4580
+#: .././mainwin.py:4612
msgid "Video is _new"
msgstr ""
-#: .././mainwin.py:4593
+#: .././mainwin.py:4625
msgid "Video is in _waiting list"
msgstr ""
-#: .././mainwin.py:4604
+#: .././mainwin.py:4636
msgid "_Mark video"
msgstr ""
-#: .././mainwin.py:4615
+#: .././mainwin.py:4647
msgid "_Location"
msgstr ""
-#: .././mainwin.py:4625
+#: .././mainwin.py:4657
msgid "_Properties..."
msgstr ""
-#: .././mainwin.py:4637
+#: .././mainwin.py:4669
msgid "_Show video"
msgstr ""
-#: .././mainwin.py:4646
+#: .././mainwin.py:4678
msgid "Available _formats"
msgstr ""
-#: .././mainwin.py:4656
+#: .././mainwin.py:4688
msgid "Available _subtitles"
msgstr ""
-#: .././mainwin.py:4666
+#: .././mainwin.py:4698
msgid "_Fetch"
msgstr ""
#. Delete video
-#: .././mainwin.py:4677
+#: .././mainwin.py:4709
msgid "D_elete video"
msgstr ""
#. Check/download videos
-#: .././mainwin.py:4772
+#: .././mainwin.py:4804
msgid "_Check videos"
msgstr ""
-#: .././mainwin.py:4791
+#: .././mainwin.py:4823
msgid "_Download videos"
msgstr ""
-#: .././mainwin.py:4810
+#: .././mainwin.py:4842
msgid "C_ustom download videos"
msgstr ""
-#: .././mainwin.py:4828
+#: .././mainwin.py:4860
msgid "D_ownload and watch"
msgstr ""
-#: .././mainwin.py:4845 .././mainwin.py:5758
+#: .././mainwin.py:4878 .././mainwin.py:5820
msgid "Watch in _player"
msgstr ""
-#: .././mainwin.py:4855 .././mainwin.py:5773 .././mainwin.py:5784
+#: .././mainwin.py:4888 .././mainwin.py:5835 .././mainwin.py:5846
msgid "Watch on _website"
msgstr ""
-#: .././mainwin.py:4886 .././mainwin.py:5956
+#: .././mainwin.py:4919 .././mainwin.py:6018
msgid "_Mark for download"
msgstr ""
-#: .././mainwin.py:4898 .././mainwin.py:5967
+#: .././mainwin.py:4944 .././mainwin.py:6029
msgid "_Download"
msgstr ""
-#: .././mainwin.py:4908
+#: .././mainwin.py:4954
msgid "_Download and watch"
msgstr ""
-#: .././mainwin.py:4919 .././mainwin.py:5987
+#: .././mainwin.py:4965 .././mainwin.py:6049
msgid "_Temporary"
msgstr ""
-#: .././mainwin.py:4937
+#: .././mainwin.py:4984
msgid "_Archived"
msgstr ""
-#: .././mainwin.py:4950
+#: .././mainwin.py:4997
msgid "Not a_rchived"
msgstr ""
-#: .././mainwin.py:4966
+#: .././mainwin.py:5013
msgid "_Bookmarked"
msgstr ""
-#: .././mainwin.py:4979
+#: .././mainwin.py:5026
msgid "Not b_ookmarked"
msgstr ""
-#: .././mainwin.py:4995
+#: .././mainwin.py:5042
msgid "_Favourite"
msgstr "_Favorite"
-#: .././mainwin.py:5008
+#: .././mainwin.py:5055
msgid "Not fa_vourite"
msgstr "Not fa_vorite"
-#: .././mainwin.py:5024
+#: .././mainwin.py:5071
msgid "_Missing"
msgstr ""
-#: .././mainwin.py:5037
+#: .././mainwin.py:5084
msgid "Not m_issing"
msgstr ""
-#: .././mainwin.py:5053
+#: .././mainwin.py:5100
msgid "_New"
msgstr ""
-#: .././mainwin.py:5066
+#: .././mainwin.py:5113
msgid "Not n_ew"
msgstr ""
-#: .././mainwin.py:5082
+#: .././mainwin.py:5129
msgid "In _waiting list"
msgstr ""
-#: .././mainwin.py:5095
+#: .././mainwin.py:5142
msgid "Not in w_aiting list"
msgstr ""
-#: .././mainwin.py:5108
+#: .././mainwin.py:5155
msgid "_Mark videos"
msgstr ""
-#: .././mainwin.py:5117
+#: .././mainwin.py:5164
msgid "Show p_roperties..."
msgstr ""
#. Delete videos
-#: .././mainwin.py:5132
+#: .././mainwin.py:5179
msgid "D_elete videos"
msgstr ""
#. Stop check/download
-#: .././mainwin.py:5197
+#: .././mainwin.py:5244
msgid "_Stop now"
msgstr ""
-#: .././mainwin.py:5211
+#: .././mainwin.py:5258
msgid "Stop after this _video"
msgstr ""
-#: .././mainwin.py:5226
+#: .././mainwin.py:5273
msgid "Stop after these v_ideos"
msgstr ""
-#: .././mainwin.py:5241
+#: .././mainwin.py:5288
msgid "Download _next"
msgstr ""
-#: .././mainwin.py:5253
+#: .././mainwin.py:5300
msgid "Download _last"
msgstr ""
-#: .././mainwin.py:5276
+#: .././mainwin.py:5323
msgid "Watch on _YouTube"
msgstr ""
-#: .././mainwin.py:5286
+#: .././mainwin.py:5333
msgid "Watch on _HookTube"
msgstr ""
-#: .././mainwin.py:5296
+#: .././mainwin.py:5343
msgid "Watch on _Invidious"
msgstr ""
-#: .././mainwin.py:5308
+#: .././mainwin.py:5355
msgid "Watch on _Website"
msgstr ""
#. Delete video
-#: .././mainwin.py:5360
+#: .././mainwin.py:5421
msgid "_Delete video"
msgstr ""
-#: .././mainwin.py:5392 .././mainwin.py:18036 .././mainwin.py:18531
-#: .././mainwin.py:18884
+#: .././mainwin.py:5453 .././mainwin.py:18257 .././mainwin.py:18752
+#: .././mainwin.py:19105
msgid "Enable automatic copy/paste"
msgstr ""
-#: .././mainwin.py:5394
+#: .././mainwin.py:5455
msgid "Disable automatic copy/paste"
msgstr ""
-#: .././mainwin.py:5410
+#: .././mainwin.py:5471
msgid "Use _classic download options"
msgstr ""
-#: .././mainwin.py:5423
+#: .././mainwin.py:5484
msgid "Use _general download options"
msgstr ""
-#: .././mainwin.py:5434
+#: .././mainwin.py:5495
msgid "_Edit classic download options"
msgstr ""
-#: .././mainwin.py:5450
-msgid "Update youtube-dl"
+#: .././mainwin.py:5511 .././mainwin.py:21825
+msgid "Update"
msgstr ""
#. Get URL
-#: .././mainwin.py:5504
+#: .././mainwin.py:5565
msgid "Get _URL"
msgstr ""
#. Get command
-#: .././mainwin.py:5513
+#: .././mainwin.py:5574
msgid "Get _command"
msgstr ""
-#: .././mainwin.py:5523
+#: .././mainwin.py:5584
msgid "_Open destination"
msgstr ""
-#: .././mainwin.py:5564
+#: .././mainwin.py:5625
msgid "Mark as _archived"
msgstr ""
-#: .././mainwin.py:5575
+#: .././mainwin.py:5636
msgid "Mark as not a_rchived"
msgstr ""
-#: .././mainwin.py:5589
+#: .././mainwin.py:5650
msgid "Mark as _bookmarked"
msgstr ""
-#: .././mainwin.py:5601
+#: .././mainwin.py:5662
msgid "Mark as not b_ookmarked"
msgstr ""
-#: .././mainwin.py:5614
+#: .././mainwin.py:5675
msgid "Mark as _favourite"
msgstr "Mark as _favorite"
-#: .././mainwin.py:5627
+#: .././mainwin.py:5688
msgid "Mark as not fa_vourite"
msgstr "Mark as not fa_vorite"
-#: .././mainwin.py:5641
+#: .././mainwin.py:5702
msgid "Mark as _missing"
msgstr ""
-#: .././mainwin.py:5654
+#: .././mainwin.py:5715
msgid "Mark as not m_issing"
msgstr ""
-#: .././mainwin.py:5671
+#: .././mainwin.py:5732
msgid "Mark as _new"
msgstr ""
-#: .././mainwin.py:5683
+#: .././mainwin.py:5744
msgid "Mark as not n_ew"
msgstr ""
-#: .././mainwin.py:5697
+#: .././mainwin.py:5758
msgid "Mark as in _waiting list"
msgstr ""
-#: .././mainwin.py:5709
+#: .././mainwin.py:5770
msgid "Mark as not in wai_ting list"
msgstr ""
-#: .././mainwin.py:5741 .././mainwin.py:5977
+#: .././mainwin.py:5802 .././mainwin.py:6039
msgid "Download and _watch"
msgstr ""
-#: .././mainwin.py:5798
+#: .././mainwin.py:5860
msgid "_YouTube"
msgstr ""
-#: .././mainwin.py:5808
+#: .././mainwin.py:5870
msgid "_HookTube"
msgstr ""
-#: .././mainwin.py:5818
+#: .././mainwin.py:5880
msgid "_Invidious"
msgstr ""
-#: .././mainwin.py:5828
+#: .././mainwin.py:5890
msgid "TRANSLATOR'S NOTE: Watch on YouTube, Watch on HookTube, etc"
msgstr ""
-#: .././mainwin.py:5833
+#: .././mainwin.py:5895
msgid "W_atch on"
msgstr ""
-#: .././mainwin.py:5862
+#: .././mainwin.py:5924
msgid "Auto _notify"
msgstr ""
-#: .././mainwin.py:5878
+#: .././mainwin.py:5940
msgid "Auto _sound alarm"
msgstr ""
-#: .././mainwin.py:5893
+#: .././mainwin.py:5955
msgid "Auto _open"
msgstr ""
-#: .././mainwin.py:5906
+#: .././mainwin.py:5968
msgid "_Download on start"
msgstr ""
-#: .././mainwin.py:5919
+#: .././mainwin.py:5981
msgid "Download on _stop"
msgstr ""
-#: .././mainwin.py:5935
+#: .././mainwin.py:5997
msgid "Not a _livestream"
msgstr ""
-#: .././mainwin.py:5945 .././config.py:5285
+#: .././mainwin.py:6007 .././config.py:5329
msgid "_Livestream"
msgstr ""
-#: .././mainwin.py:6788
+#: .././mainwin.py:6850
msgid ""
"TRANSLATOR'S NOTE: V = number of videos B = (number of videos) bookmarked D "
"= downloaded F = favourite L = live/livestream M = missing N = new W = in "
"waiting list E = (number of) errors W = warnings"
msgstr ""
-#: .././mainwin.py:6795
+#: .././mainwin.py:6857
msgid "V:"
msgstr ""
-#: .././mainwin.py:6796
+#: .././mainwin.py:6858
msgid "B:"
msgstr ""
-#: .././mainwin.py:6797
+#: .././mainwin.py:6859
msgid "D:"
msgstr ""
-#: .././mainwin.py:6798
+#: .././mainwin.py:6860
msgid "F:"
msgstr ""
-#: .././mainwin.py:6799
+#: .././mainwin.py:6861
msgid "L:"
msgstr ""
-#: .././mainwin.py:6800
+#: .././mainwin.py:6862
msgid "M:"
msgstr ""
-#: .././mainwin.py:6801
+#: .././mainwin.py:6863
msgid "N:"
msgstr ""
-#: .././mainwin.py:6802 .././mainwin.py:6813
+#: .././mainwin.py:6864 .././mainwin.py:6875
msgid "W:"
msgstr ""
-#: .././mainwin.py:6812
+#: .././mainwin.py:6874
msgid "E:"
msgstr ""
-#: .././mainwin.py:7838 .././mainwin.py:8518
+#: .././mainwin.py:7895 .././mainwin.py:8546
msgid "Waiting"
msgstr ""
-#: .././mainwin.py:8978
+#: .././mainwin.py:9003
msgid ""
"TRANSLATOR'S NOTE: Thread means a computer processor thread. If you're not "
"sure how to translate it, just use 'Page #', as in Page #1, Page #2, etc"
msgstr ""
-#: .././mainwin.py:8985
+#: .././mainwin.py:9010
msgid "Thread"
msgstr ""
-#: .././mainwin.py:8988
+#: .././mainwin.py:9013
msgid "_Summary"
msgstr ""
-#: .././mainwin.py:9516
+#: .././mainwin.py:9556
msgid "Tartube error"
msgstr ""
-#: .././mainwin.py:9569
+#: .././mainwin.py:9609
msgid "Tartube warning"
msgstr ""
-#: .././mainwin.py:9602
+#: .././mainwin.py:9642
msgid "_Errors"
msgstr ""
-#: .././mainwin.py:9606
+#: .././mainwin.py:9646
msgid "Warnings"
msgstr ""
-#: .././mainwin.py:10896
+#: .././mainwin.py:10936
msgid "The URL is not valid"
msgstr ""
-#: .././mainwin.py:14219
+#: .././mainwin.py:14425
#, python-brace-format
msgid "The channel contains {0} items, so this action may take a while"
msgstr ""
-#: .././mainwin.py:14226
+#: .././mainwin.py:14432
#, python-brace-format
msgid "The playlist contains {0} items, so this action may take a while"
msgstr ""
-#: .././mainwin.py:14233
+#: .././mainwin.py:14439
#, python-brace-format
msgid "The folder contains {0} items, so this action may take a while"
msgstr ""
-#: .././mainwin.py:14614 .././mainwin.py:15532
+#: .././mainwin.py:14820 .././mainwin.py:15741
msgid "Originally from:"
msgstr ""
-#: .././mainwin.py:14627 .././mainwin.py:15545
+#: .././mainwin.py:14833 .././mainwin.py:15754
msgid "From channel:"
msgstr ""
-#: .././mainwin.py:14629 .././mainwin.py:15547
+#: .././mainwin.py:14835 .././mainwin.py:15756
msgid "From playlist:"
msgstr ""
-#: .././mainwin.py:14631 .././mainwin.py:15549
+#: .././mainwin.py:14837 .././mainwin.py:15758
msgid "From folder:"
msgstr ""
-#: .././mainwin.py:14657
+#: .././mainwin.py:14863
msgid "Livestream has not started yet"
msgstr ""
-#: .././mainwin.py:14666 .././mainwin.py:14672 .././mainwin.py:15596
-#: .././mainwin.py:15603
+#: .././mainwin.py:14872 .././mainwin.py:14878 .././mainwin.py:15805
+#: .././mainwin.py:15812
msgid "Duration:"
msgstr ""
-#: .././mainwin.py:14672 .././mainwin.py:14678 .././mainwin.py:14687
-#: .././mainwin.py:15603 .././mainwin.py:15610 .././mainwin.py:15620
-#: .././media.py:319 .././media.py:329 .././media.py:1546 .././media.py:1552
-#: .././media.py:1562
+#: .././mainwin.py:14878 .././mainwin.py:14884 .././mainwin.py:14893
+#: .././mainwin.py:15812 .././mainwin.py:15819 .././mainwin.py:15829
+#: .././media.py:320 .././media.py:330 .././media.py:1552 .././media.py:1558
+#: .././media.py:1568
msgid "unknown"
msgstr ""
-#: .././mainwin.py:14676 .././mainwin.py:14678 .././mainwin.py:15607
-#: .././mainwin.py:15609
+#: .././mainwin.py:14882 .././mainwin.py:14884 .././mainwin.py:15816
+#: .././mainwin.py:15818
msgid "Size:"
msgstr ""
-#: .././mainwin.py:14685 .././mainwin.py:14687 .././mainwin.py:15617
-#: .././mainwin.py:15619
+#: .././mainwin.py:14891 .././mainwin.py:14893 .././mainwin.py:15826
+#: .././mainwin.py:15828
msgid "Date:"
msgstr ""
-#: .././mainwin.py:15012
+#: .././mainwin.py:15218
msgid "Watch:"
msgstr ""
-#: .././mainwin.py:15081
+#: .././mainwin.py:15287
msgid "Temporary:"
msgstr ""
-#: .././mainwin.py:15124
+#: .././mainwin.py:15330
msgid "Marked:"
msgstr ""
-#: .././mainwin.py:15504 .././mainwin.py:15566
+#: .././mainwin.py:15713 .././mainwin.py:15775
msgid "Show the full description"
msgstr ""
-#: .././mainwin.py:15505 .././mainwin.py:15567
+#: .././mainwin.py:15714 .././mainwin.py:15776
msgid "More"
msgstr ""
-#: .././mainwin.py:15517 .././mainwin.py:15575
+#: .././mainwin.py:15726 .././mainwin.py:15784
msgid "Show the short description"
msgstr ""
-#: .././mainwin.py:15518 .././mainwin.py:15576
+#: .././mainwin.py:15727 .././mainwin.py:15785
msgid "Less"
msgstr ""
-#: .././mainwin.py:15636
+#: .././mainwin.py:15845
msgid "Live:"
msgstr ""
-#: .././mainwin.py:15639 .././mainwin.py:15641 .././mainwin.py:15645
-#: .././mainwin.py:15883 .././mainwin.py:15885 .././mainwin.py:15889
-#: .././mainwin.py:16342
+#: .././mainwin.py:15848 .././mainwin.py:15850 .././mainwin.py:15854
+#: .././mainwin.py:16096 .././mainwin.py:16098 .././mainwin.py:16102
+#: .././mainwin.py:16555
msgid "Notify"
msgstr ""
-#: .././mainwin.py:15649 .././mainwin.py:15893
+#: .././mainwin.py:15858 .././mainwin.py:16106
msgid "When the livestream starts, notify the user"
msgstr ""
-#: .././mainwin.py:15660 .././mainwin.py:15662 .././mainwin.py:15899
-#: .././mainwin.py:15901 .././mainwin.py:16209
+#: .././mainwin.py:15869 .././mainwin.py:15871 .././mainwin.py:16112
+#: .././mainwin.py:16114 .././mainwin.py:16422
msgid "Alarm"
msgstr ""
-#: .././mainwin.py:15666 .././mainwin.py:15905
+#: .././mainwin.py:15875 .././mainwin.py:16118
msgid "When the livestream starts, sound an alarm"
msgstr ""
-#: .././mainwin.py:15671 .././mainwin.py:15673 .././mainwin.py:15911
-#: .././mainwin.py:15913 .././mainwin.py:16387
+#: .././mainwin.py:15880 .././mainwin.py:15882 .././mainwin.py:16124
+#: .././mainwin.py:16126 .././mainwin.py:16600
msgid "Open"
msgstr ""
-#: .././mainwin.py:15677 .././mainwin.py:15917
+#: .././mainwin.py:15886 .././mainwin.py:16130
msgid "When the livestream starts, open it"
msgstr ""
-#: .././mainwin.py:15682 .././mainwin.py:15684 .././mainwin.py:15923
-#: .././mainwin.py:15925 .././mainwin.py:16253
+#: .././mainwin.py:15891 .././mainwin.py:15893 .././mainwin.py:16136
+#: .././mainwin.py:16138 .././mainwin.py:16466
msgid "D/L on start"
msgstr ""
-#: .././mainwin.py:15688 .././mainwin.py:15929
+#: .././mainwin.py:15897 .././mainwin.py:16142
msgid "When the livestream starts, download it"
msgstr ""
-#: .././mainwin.py:15693 .././mainwin.py:15695 .././mainwin.py:15935
-#: .././mainwin.py:15937 .././mainwin.py:16298
+#: .././mainwin.py:15902 .././mainwin.py:15904 .././mainwin.py:16148
+#: .././mainwin.py:16150 .././mainwin.py:16511
msgid "D/L on stop"
msgstr ""
-#: .././mainwin.py:15699 .././mainwin.py:15941
+#: .././mainwin.py:15908 .././mainwin.py:16154
msgid "When the livestream stops, download it"
msgstr ""
-#: .././mainwin.py:15725
+#: .././mainwin.py:15934
msgid "Download this video"
msgstr ""
-#: .././mainwin.py:15736
+#: .././mainwin.py:15945
msgid "Watch in your media player"
msgstr ""
@@ -2049,37 +2086,37 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15737 .././mainwin.py:17036
+#: .././mainwin.py:15946 .././mainwin.py:17253
msgid "Player"
msgstr ""
-#: .././mainwin.py:15745
+#: .././mainwin.py:15955
msgid ""
"TRANSLATOR'S NOTE: If you want to use &, use & - if you want to use a "
"different word (e.g. French et), then just use that word"
msgstr ""
-#: .././mainwin.py:15753
+#: .././mainwin.py:15963
msgid "Download and watch in your media player"
msgstr ""
-#: .././mainwin.py:15754
+#: .././mainwin.py:15964
msgid "Download & watch"
msgstr ""
-#: .././mainwin.py:15761
+#: .././mainwin.py:15971
msgid "Not downloaded"
msgstr ""
-#: .././mainwin.py:15787
+#: .././mainwin.py:15997
msgid "Watch on YouTube"
msgstr ""
-#: .././mainwin.py:15788 .././mainwin.py:17081
+#: .././mainwin.py:15998 .././mainwin.py:17298
msgid "YouTube"
msgstr ""
-#: .././mainwin.py:15800
+#: .././mainwin.py:16010
msgid "Watch on HookTube"
msgstr ""
@@ -2087,11 +2124,11 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15801 .././mainwin.py:16846
+#: .././mainwin.py:16011 .././mainwin.py:17062
msgid "HookTube"
msgstr ""
-#: .././mainwin.py:15810
+#: .././mainwin.py:16023
msgid "Watch on Invidious"
msgstr ""
@@ -2099,7 +2136,7 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15811 .././mainwin.py:16890
+#: .././mainwin.py:16024 .././mainwin.py:17106
msgid "Invidious"
msgstr ""
@@ -2107,24 +2144,24 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15829 .././mainwin.py:16935
+#: .././mainwin.py:16042 .././mainwin.py:17151
msgid "Other"
msgstr ""
-#: .././mainwin.py:15849
+#: .././mainwin.py:16062
msgid "Watch on website"
msgstr ""
-#: .././mainwin.py:15850 .././mainwin.py:17083
+#: .././mainwin.py:16063 .././mainwin.py:17300
msgid "Website"
msgstr ""
#. Links not clickable
-#: .././mainwin.py:15861
+#: .././mainwin.py:16074
msgid "No link"
msgstr ""
-#: .././mainwin.py:15970
+#: .././mainwin.py:16183
msgid "Download to a temporary folder later"
msgstr ""
@@ -2132,15 +2169,15 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15971 .././mainwin.py:15988 .././mainwin.py:16802
+#: .././mainwin.py:16184 .././mainwin.py:16201 .././mainwin.py:17018
msgid "Mark for download"
msgstr ""
-#: .././mainwin.py:15976
+#: .././mainwin.py:16189
msgid "Download to a temporary folder"
msgstr ""
-#: .././mainwin.py:15982
+#: .././mainwin.py:16195
msgid "Download to a temporary folder, then watch"
msgstr ""
@@ -2148,12 +2185,12 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15983 .././mainwin.py:15990 .././mainwin.py:16759
+#: .././mainwin.py:16196 .././mainwin.py:16203 .././mainwin.py:16974
msgid "D/L and watch"
msgstr ""
#. Archived/not archived
-#: .././mainwin.py:16014
+#: .././mainwin.py:16227
msgid "Prevent automatic deletion of the video"
msgstr ""
@@ -2161,21 +2198,21 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16018 .././mainwin.py:16022 .././mainwin.py:16431
+#: .././mainwin.py:16231 .././mainwin.py:16235 .././mainwin.py:16644
msgid "Archived"
msgstr ""
#. Bookmarked/not bookmarked
-#: .././mainwin.py:16027
+#: .././mainwin.py:16240
msgid "Show video in Bookmarks folder"
msgstr ""
-#: .././mainwin.py:16031 .././mainwin.py:16035
+#: .././mainwin.py:16244 .././mainwin.py:16248
msgid "Bookmarked"
msgstr ""
#. Favourite/not favourite
-#: .././mainwin.py:16040
+#: .././mainwin.py:16253
msgid "Show in Favourite Videos folder"
msgstr "Show in Favorite Videos folder"
@@ -2183,12 +2220,12 @@ msgstr "Show in Favorite Videos folder"
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16044 .././mainwin.py:16048 .././mainwin.py:16521
+#: .././mainwin.py:16257 .././mainwin.py:16261 .././mainwin.py:16734
msgid "Favourite"
msgstr "Favorite"
#. Missing/not missing
-#: .././mainwin.py:16052
+#: .././mainwin.py:16265
msgid "Mark video as removed by creator"
msgstr ""
@@ -2196,12 +2233,12 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16056 .././mainwin.py:16060 .././mainwin.py:16566
+#: .././mainwin.py:16269 .././mainwin.py:16273 .././mainwin.py:16779
msgid "Missing"
msgstr ""
#. New/not new
-#: .././mainwin.py:16065
+#: .././mainwin.py:16278
msgid "Mark video as never watched"
msgstr ""
@@ -2209,36 +2246,36 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16069 .././mainwin.py:16073 .././mainwin.py:16604
+#: .././mainwin.py:16282 .././mainwin.py:16286 .././mainwin.py:16817
msgid "New"
msgstr ""
#. In waiting list/not in waiting list
-#: .././mainwin.py:16078
+#: .././mainwin.py:16291
msgid "Show in Waiting Videos folder"
msgstr ""
-#: .././mainwin.py:16081
+#: .././mainwin.py:16294
msgid "In waiting list"
msgstr ""
-#: .././mainwin.py:16085
+#: .././mainwin.py:16298
msgid "In Waiting list"
msgstr ""
-#: .././mainwin.py:16204
+#: .././mainwin.py:16417
msgid "Undo alarm"
msgstr ""
-#: .././mainwin.py:16248 .././mainwin.py:16293
+#: .././mainwin.py:16461 .././mainwin.py:16506
msgid "Don't D/L"
msgstr ""
-#: .././mainwin.py:16337
+#: .././mainwin.py:16550
msgid "Undo notify"
msgstr ""
-#: .././mainwin.py:16382
+#: .././mainwin.py:16595
msgid "Undo open"
msgstr ""
@@ -2246,7 +2283,7 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16476
+#: .././mainwin.py:16689
msgid "Not bookmarked"
msgstr ""
@@ -2254,2815 +2291,2991 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16649
+#: .././mainwin.py:16862
msgid "Not in waiting list"
msgstr ""
-#: .././mainwin.py:17713
+#: .././mainwin.py:17934
msgid "Tartube failed to start because:"
msgstr ""
-#: .././mainwin.py:17722
+#: .././mainwin.py:17943
msgid "If you don't know how to resolve this error, please contact the authors"
msgstr ""
-#: .././mainwin.py:17727
+#: .././mainwin.py:17948
msgid "here"
msgstr ""
#. 'OK' button
-#: .././mainwin.py:17730 .././mainwin.py:20016 .././config.py:426
-#: .././config.py:1602
+#: .././mainwin.py:17951 .././mainwin.py:20423 .././config.py:428
+#: .././config.py:1604
msgid "OK"
msgstr ""
-#: .././mainwin.py:17781 .././mainwin.py:20793 .././mainwin.py:20888
+#: .././mainwin.py:18002 .././mainwin.py:21402 .././mainwin.py:21497
msgid "Welcome to Tartube!"
msgstr ""
-#: .././mainwin.py:17913
+#: .././mainwin.py:18134
msgid "Add channel"
msgstr ""
-#: .././mainwin.py:17932
+#: .././mainwin.py:18153
msgid "Enter the channel name"
msgstr ""
-#: .././mainwin.py:17937
+#: .././mainwin.py:18158
msgid "(Use the channel's real name or a customised name)"
msgstr "(Use the channel's real name or a customized name)"
-#: .././mainwin.py:17945
+#: .././mainwin.py:18166
msgid "Copy and paste a link to the channel"
msgstr ""
-#: .././mainwin.py:17992
+#: .././mainwin.py:18213
msgid "(Optional) Add this channel inside a folder"
msgstr ""
-#: .././mainwin.py:18022
+#: .././mainwin.py:18243
msgid "I want to download videos from this channel automatically"
msgstr ""
-#: .././mainwin.py:18029 .././mainwin.py:18316 .././mainwin.py:18524
+#: .././mainwin.py:18250 .././mainwin.py:18537 .././mainwin.py:18745
msgid "Don't download anything, just check for new videos"
msgstr ""
-#: .././mainwin.py:18217
+#: .././mainwin.py:18438
msgid "Add folder"
msgstr ""
-#: .././mainwin.py:18236
+#: .././mainwin.py:18457
msgid "Enter the folder name"
msgstr ""
-#: .././mainwin.py:18279
+#: .././mainwin.py:18500
msgid "(Optional) Add this folder inside another folder"
msgstr ""
-#: .././mainwin.py:18310
+#: .././mainwin.py:18531
msgid "I want to download videos from this folder automatically"
msgstr ""
-#: .././mainwin.py:18408
+#: .././mainwin.py:18629
msgid "Add playlist"
msgstr ""
-#: .././mainwin.py:18427
+#: .././mainwin.py:18648
msgid "Enter the playlist name"
msgstr ""
-#: .././mainwin.py:18432
+#: .././mainwin.py:18653
msgid "(Use the playlist's real name or a customised name)"
msgstr "(Use the playlist's real name or a customized name)"
-#: .././mainwin.py:18440
+#: .././mainwin.py:18661
msgid "Copy and paste a link to the playlist"
msgstr ""
-#: .././mainwin.py:18487
+#: .././mainwin.py:18708
msgid "(Optional) Add this playlist inside a folder"
msgstr ""
-#: .././mainwin.py:18517
+#: .././mainwin.py:18738
msgid "I want to download videos from this playlist automatically"
msgstr ""
-#: .././mainwin.py:18714
+#: .././mainwin.py:18935
msgid "Add videos"
msgstr ""
-#: .././mainwin.py:18733
+#: .././mainwin.py:18954
msgid "Copy and paste the links to one or more videos"
msgstr ""
-#: .././mainwin.py:18739
+#: .././mainwin.py:18960
msgid "Links containing multiple videos will be converted to a channel"
msgstr ""
-#: .././mainwin.py:18746
+#: .././mainwin.py:18967
msgid "Links containing multiple videos will be converted to a playlist"
msgstr ""
-#: .././mainwin.py:18753
+#: .././mainwin.py:18974
msgid "Links containing multiple videos will be downloaded separately"
msgstr ""
-#: .././mainwin.py:18760
+#: .././mainwin.py:18981
msgid "Links containing multiple videos will not be downloaded at all"
msgstr ""
-#: .././mainwin.py:18842
+#: .././mainwin.py:19063
msgid "Add the videos to this folder"
msgstr ""
-#: .././mainwin.py:18872
+#: .././mainwin.py:19093
msgid "I want to download these videos automatically"
msgstr ""
-#: .././mainwin.py:18878
+#: .././mainwin.py:19099
msgid "Don't download anything, just check the videos"
msgstr ""
-#: .././mainwin.py:19043
+#: .././mainwin.py:19264
msgid "Select a date"
msgstr ""
-#: .././mainwin.py:19149
+#: .././mainwin.py:19370
msgid "Delete channel"
msgstr ""
-#: .././mainwin.py:19151
+#: .././mainwin.py:19372
msgid "Delete playlist"
msgstr ""
-#: .././mainwin.py:19153
+#: .././mainwin.py:19374
msgid "Delete folder"
msgstr ""
-#: .././mainwin.py:19156
+#: .././mainwin.py:19377
msgid "Empty channel"
msgstr ""
-#: .././mainwin.py:19158
+#: .././mainwin.py:19379
msgid "Empty playlist"
msgstr ""
-#: .././mainwin.py:19160
+#: .././mainwin.py:19381
msgid "Empty folder"
msgstr ""
-#: .././mainwin.py:19194
+#: .././mainwin.py:19415
msgid "This channel does not contain any videos"
msgstr ""
-#: .././mainwin.py:19196
+#: .././mainwin.py:19417
msgid "This playlist does not contain any videos"
msgstr ""
-#: .././mainwin.py:19198
+#: .././mainwin.py:19419
msgid "This folder doesn't contain anything"
msgstr ""
-#: .././mainwin.py:19204
+#: .././mainwin.py:19425
msgid "(but there might be some files in Tartube's data folder)"
msgstr ""
-#: .././mainwin.py:19217
+#: .././mainwin.py:19438
msgid "This channel contains:"
msgstr ""
-#: .././mainwin.py:19219
+#: .././mainwin.py:19440
msgid "This playlist contains:"
msgstr ""
-#: .././mainwin.py:19221
+#: .././mainwin.py:19442
msgid "This folder contains:"
msgstr ""
-#: .././mainwin.py:19228
+#: .././mainwin.py:19449
msgid "1 folder"
msgstr ""
-#: .././mainwin.py:19230
+#: .././mainwin.py:19451
#, python-brace-format
msgid "{0} folders"
msgstr ""
-#: .././mainwin.py:19237
+#: .././mainwin.py:19458
msgid "1 channel"
msgstr ""
-#: .././mainwin.py:19239
+#: .././mainwin.py:19460
#, python-brace-format
msgid "{0} channels"
msgstr ""
-#: .././mainwin.py:19246
+#: .././mainwin.py:19467
msgid "1 playlist"
msgstr ""
-#: .././mainwin.py:19248
+#: .././mainwin.py:19469
#, python-brace-format
msgid "{0} playlists"
msgstr ""
-#: .././mainwin.py:19255 .././mainwin.py:19680
+#: .././mainwin.py:19476 .././mainwin.py:19901
msgid "1 video"
msgstr ""
-#: .././mainwin.py:19257 .././mainwin.py:19683
+#: .././mainwin.py:19478 .././mainwin.py:19904
#, python-brace-format
msgid "{0} videos"
msgstr ""
-#: .././mainwin.py:19270
+#: .././mainwin.py:19491
msgid ""
"Do you want to delete the channel from Tartube's data folder, or do you just "
"want to remove the channel from this list?"
msgstr ""
-#: .././mainwin.py:19276
+#: .././mainwin.py:19497
msgid ""
"Do you want to delete the playlist from Tartube's data folder, or do you "
"just want to remove the playlist from this list?"
msgstr ""
-#: .././mainwin.py:19282
+#: .././mainwin.py:19503
msgid ""
"Do you want to delete the folder from Tartube's data folder, or do you just "
"want to remove the folder from this list?"
msgstr ""
-#: .././mainwin.py:19291
+#: .././mainwin.py:19512
msgid ""
"Do you want to empty the channel in Tartube's data folder, or do you just "
"want to empty the channel in this list?"
msgstr ""
-#: .././mainwin.py:19297
+#: .././mainwin.py:19518
msgid ""
"Do you want to empty the playlist in Tartube's data folder, or do you just "
"want to empty the playlist in this list?"
msgstr ""
-#: .././mainwin.py:19303
+#: .././mainwin.py:19524
msgid ""
"Do you want to empty the folder in Tartube's data folder, or do you just "
"want to empty the folder in this list?"
msgstr ""
-#: .././mainwin.py:19320
+#: .././mainwin.py:19541
msgid "Just remove the channel from this list"
msgstr ""
-#: .././mainwin.py:19322
+#: .././mainwin.py:19543
msgid "Just remove the playlist from this list"
msgstr ""
-#: .././mainwin.py:19324
+#: .././mainwin.py:19545
msgid "Just remove the folder from this list"
msgstr ""
-#: .././mainwin.py:19329
+#: .././mainwin.py:19550
msgid "Just empty the channel in this list"
msgstr ""
-#: .././mainwin.py:19331
+#: .././mainwin.py:19552
msgid "Just empty the playlist in this list"
msgstr ""
-#: .././mainwin.py:19333
+#: .././mainwin.py:19554
msgid "Just empty the folder in this list"
msgstr ""
-#: .././mainwin.py:19339
+#: .././mainwin.py:19560
msgid "Delete all files"
msgstr ""
-#: .././mainwin.py:19391
+#: .././mainwin.py:19612
msgid "Export from database"
msgstr ""
-#: .././mainwin.py:19415
+#: .././mainwin.py:19636
msgid ""
"Tartube is ready to export a partial summary of its database, containing a "
"list of videos, channels, playlists and/or folders (but not including the "
"videos themselves)"
msgstr ""
-#: .././mainwin.py:19422
+#: .././mainwin.py:19643
msgid ""
"Tartube is ready to export a summary of its database, containing a list of "
"videos, channels, playlists and/or folders (but not including the videos "
"themselves)"
msgstr ""
-#: .././mainwin.py:19438
+#: .././mainwin.py:19659
msgid "Choose what should be included:"
msgstr ""
-#: .././mainwin.py:19446
+#: .././mainwin.py:19667
msgid "Include lists of videos"
msgstr ""
-#: .././mainwin.py:19451
+#: .././mainwin.py:19672
msgid "Include channels"
msgstr ""
-#: .././mainwin.py:19456
+#: .././mainwin.py:19677
msgid "Include playlists"
msgstr ""
-#: .././mainwin.py:19461
+#: .././mainwin.py:19682
msgid "Preserve folder structure"
msgstr ""
-#: .././mainwin.py:19469
+#: .././mainwin.py:19690
msgid "Export as plain text"
msgstr ""
-#: .././mainwin.py:19555
+#: .././mainwin.py:19776
msgid "Import into database"
msgstr ""
-#: .././mainwin.py:19578
+#: .././mainwin.py:19799
msgid "Choose which items to import"
msgstr ""
-#: .././mainwin.py:19599
+#: .././mainwin.py:19820
msgid "Import"
msgstr ""
-#: .././mainwin.py:19615
+#: .././mainwin.py:19836
msgid "Name"
msgstr ""
-#: .././mainwin.py:19635
+#: .././mainwin.py:19856
msgid "Import videos"
msgstr ""
-#: .././mainwin.py:19640
+#: .././mainwin.py:19861
msgid "Merge channels/playlists/folders"
msgstr ""
#. Bottom strip
-#: .././mainwin.py:19643 .././mainwin.py:21615
+#: .././mainwin.py:19864 .././mainwin.py:22242
msgid "Select all"
msgstr ""
-#: .././mainwin.py:19648
+#: .././mainwin.py:19869
msgid "Unselect all"
msgstr ""
-#: .././mainwin.py:19910
+#: .././mainwin.py:20122
+msgid "Install youtube-dl and FFmpeg"
+msgstr ""
+
+#: .././mainwin.py:20141
+msgid ""
+"Tartube could not auto-detect youtube-dl on your system. youtube-dl must be "
+"installed before you can use Tartube."
+msgstr ""
+
+#: .././mainwin.py:20163
+msgid "I have now installed youtube-dl, please detect its location"
+msgstr ""
+
+#: .././mainwin.py:20175
+msgid ""
+"I have now installed youtube-dl, please open the preferences window so I can "
+"set its location manually"
+msgstr ""
+
+#: .././mainwin.py:20317
msgid "Mount drive"
msgstr ""
-#: .././mainwin.py:19934
+#: .././mainwin.py:20341
msgid "The Tartube data folder is set to:"
msgstr ""
-#: .././mainwin.py:19947
+#: .././mainwin.py:20354
msgid "...but this folder doesn't exist"
msgstr ""
-#: .././mainwin.py:19950
+#: .././mainwin.py:20357
msgid "...but Tartube cannot write to this folder"
msgstr ""
-#: .././mainwin.py:19960
+#: .././mainwin.py:20367
msgid "I have mounted the drive, please try again"
msgstr ""
-#: .././mainwin.py:19966
+#: .././mainwin.py:20373
msgid "Use this data folder:"
msgstr ""
-#: .././mainwin.py:19993
+#: .././mainwin.py:20400
msgid "Select a different data folder"
msgstr ""
-#: .././mainwin.py:19999
+#: .././mainwin.py:20406
msgid "Use the default data folder"
msgstr ""
-#: .././mainwin.py:20005
+#: .././mainwin.py:20412
msgid "Shut down Tartube"
msgstr ""
#. 'Cancel' button
-#: .././mainwin.py:20012 .././config.py:435
+#: .././mainwin.py:20419 .././config.py:437
msgid "Cancel"
msgstr ""
-#: .././mainwin.py:20138
+#: .././mainwin.py:20545
msgid "The folder still doesn't exist. Please try a different option"
msgstr ""
-#: .././mainwin.py:20205
+#: .././mainwin.py:20620
+msgid "Process videos with FFmpeg"
+msgstr ""
+
+#: .././mainwin.py:20642
+msgid "Process 1 video with the following options:"
+msgstr ""
+
+#: .././mainwin.py:20644
+#, python-brace-format
+msgid "Process {0} videos with the following options:"
+msgstr ""
+
+#: .././mainwin.py:20652
+msgid "Reset all"
+msgstr ""
+
+#: .././mainwin.py:20660
+msgid "Add to end of filename:"
+msgstr ""
+
+#: .././mainwin.py:20670
+msgid "If regex matches filename:"
+msgstr ""
+
+#: .././mainwin.py:20678
+msgid "...then apply substitution:"
+msgstr ""
+
+#: .././mainwin.py:20686
+msgid "Change file extension:"
+msgstr ""
+
+#: .././mainwin.py:20696
+msgid "FFmpeg command-line options:"
+msgstr ""
+
+#: .././mainwin.py:20718
+msgid "If the video has a new name/extension, delete the original"
+msgstr ""
+
+#: .././mainwin.py:20727
+msgid "Remember these options for the next time"
+msgstr ""
+
+#: .././mainwin.py:20806
msgid "Stale lockfile"
msgstr ""
-#: .././mainwin.py:20242
+#: .././mainwin.py:20843
msgid ""
-"Failed to load the Tartube database file, because another instance of "
-"Tartube seems to be using it"
+"Failed to load the Tartube database file, because another copy of Tartube "
+"seems to be using it"
msgstr ""
-#: .././mainwin.py:20249
+#: .././mainwin.py:20850
+msgid "Do you want to load it anyway?"
+msgstr ""
+
+#: .././mainwin.py:20856
msgid ""
-"If you are SURE that this is the only instance of Tartube running on your "
-"system. click 'Yes' to remove the protection (and then restart Tartube)"
+"(Only click 'Yes' if you are sure that other copies of Tartube are not using "
+"the database right now)"
msgstr ""
-#: .././mainwin.py:20254
-msgid "If you are not sure, then click 'No'"
+#: .././mainwin.py:20868
+msgid "Yes, load the file"
msgstr ""
-#: .././mainwin.py:20262
-msgid "Yes, I'm sure"
+#: .././mainwin.py:20875
+msgid "No, just shut down Tartube"
msgstr ""
-#: .././mainwin.py:20269
-msgid "No, I'm not sure"
+#: .././mainwin.py:20877
+msgid "No, don't load the file"
msgstr ""
-#: .././mainwin.py:20363
+#: .././mainwin.py:20972
msgid "Rename channel"
msgstr ""
-#: .././mainwin.py:20365
+#: .././mainwin.py:20974
msgid "Rename playlist"
msgstr ""
-#: .././mainwin.py:20367
+#: .././mainwin.py:20976
msgid "Rename folder"
msgstr ""
-#: .././mainwin.py:20391
+#: .././mainwin.py:21000
msgid "Set the new name for the channel:"
msgstr ""
-#: .././mainwin.py:20393
+#: .././mainwin.py:21002
msgid "Set the new name for the playlist:"
msgstr ""
-#: .././mainwin.py:20395
+#: .././mainwin.py:21004
msgid "Set the new name for the folder:"
msgstr ""
-#: .././mainwin.py:20401
+#: .././mainwin.py:21010
msgid "N.B. This procedure will modify your filesystem!\n"
msgstr ""
-#: .././mainwin.py:20462
+#: .././mainwin.py:21071
msgid "Set download destination"
msgstr ""
-#: .././mainwin.py:20487
+#: .././mainwin.py:21096
msgid ""
"This channel can store its videos in its own system folder, or it can store "
"them in a different system folder"
msgstr ""
-#: .././mainwin.py:20492
+#: .././mainwin.py:21101
msgid ""
"This playlist can store its videos in its own system folder, or it can store "
"them in a different folder"
msgstr ""
-#: .././mainwin.py:20497
+#: .././mainwin.py:21106
msgid ""
"This folder can store its videos in its own system folder, or it can store "
"them in a different system folder"
msgstr ""
-#: .././mainwin.py:20505
+#: .././mainwin.py:21114
msgid "Choose a different system folder if:"
msgstr ""
-#: .././mainwin.py:20508
+#: .././mainwin.py:21117
msgid ""
"1. You want to add a channel and its playlists, without downloading the same "
"video twice"
msgstr ""
-#: .././mainwin.py:20515
+#: .././mainwin.py:21124
msgid ""
"2. A video creator has channels on both YouTube and BitChute, and you want "
"to add both without downloading the same video twice"
msgstr ""
-#: .././mainwin.py:20528
+#: .././mainwin.py:21137
msgid "Use this channel's own folder"
msgstr ""
-#: .././mainwin.py:20530
+#: .././mainwin.py:21139
msgid "Use this playlist's own folder"
msgstr ""
-#: .././mainwin.py:20532
+#: .././mainwin.py:21141
msgid "Use this folder's own system folder"
msgstr ""
-#: .././mainwin.py:20823
+#: .././mainwin.py:21432
msgid "Tartube's data folder will be:"
msgstr ""
-#: .././mainwin.py:20838
+#: .././mainwin.py:21447
msgid "Use this folder"
msgstr ""
-#: .././mainwin.py:20843
+#: .././mainwin.py:21452
msgid "Choose a different folder"
msgstr ""
-#: .././mainwin.py:20919
+#: .././mainwin.py:21528
msgid "Click OK to create a folder in which Tartube can store its videos"
msgstr ""
-#: .././mainwin.py:20926
+#: .././mainwin.py:21535
msgid ""
"If you have used Tartube before, you can select an existing folder instead "
"of creating a new one"
msgstr ""
-#: .././mainwin.py:20981
+#: .././mainwin.py:21590
msgid "Set nickname"
msgstr ""
-#: .././mainwin.py:21006
+#: .././mainwin.py:21615
#, python-brace-format
msgid ""
"Set a nickname for the channel '{0}' (or leave it blank to reset the "
"nickname)"
msgstr ""
-#: .././mainwin.py:21011
+#: .././mainwin.py:21620
#, python-brace-format
msgid ""
"Set a nickname for the playlist '{0}' (or leave it blank to reset the "
"nickname)"
msgstr ""
-#: .././mainwin.py:21016
+#: .././mainwin.py:21625
#, python-brace-format
msgid ""
"Set a nickname for the folder '{0}' (or leave it blank to reset the nickname)"
msgstr ""
-#: .././mainwin.py:21079
+#: .././mainwin.py:21688
msgid "Set URL"
msgstr ""
-#: .././mainwin.py:21104
+#: .././mainwin.py:21713
#, python-brace-format
msgid "Update the URL for the channel '{0}'"
msgstr ""
-#: .././mainwin.py:21108
+#: .././mainwin.py:21717
#, python-brace-format
msgid "Update the URL for the playlist '{0}'"
msgstr ""
-#: .././mainwin.py:21172
+#: .././mainwin.py:21781
msgid "Show system command"
msgstr ""
-#: .././mainwin.py:21216
-msgid "Update"
-msgstr ""
-
-#: .././mainwin.py:21225
+#: .././mainwin.py:21834
msgid "Copy to clipboard"
msgstr ""
-#: .././mainwin.py:21399
-msgid "Test youtube-dl"
+#: .././mainwin.py:22008 .././config.py:8685
+msgid "Test"
msgstr ""
-#: .././mainwin.py:21419
+#: .././mainwin.py:22028
msgid "URL of the video to download (optional)"
msgstr ""
-#: .././mainwin.py:21430
-msgid "youtube-dl command line options (optional)"
+#: .././mainwin.py:22039
+msgid "Command line options (optional)"
msgstr ""
-#: .././mainwin.py:21509
+#: .././mainwin.py:22120
msgid "Tidy up files"
msgstr ""
-#: .././mainwin.py:21511
+#: .././mainwin.py:22122
msgid "Tidy up channel"
msgstr ""
-#: .././mainwin.py:21513
+#: .././mainwin.py:22124
msgid "Tidy up playlist"
msgstr ""
-#: .././mainwin.py:21515
+#: .././mainwin.py:22126
msgid "Tidy up folder"
msgstr ""
-#: .././mainwin.py:21544
+#: .././mainwin.py:22155
msgid "Check that videos are not corrupted"
msgstr ""
-#: .././mainwin.py:21549
+#: .././mainwin.py:22160
msgid "Delete corrupted video files"
msgstr ""
-#: .././mainwin.py:21559
+#: .././mainwin.py:22170
msgid "Check that videos do/don't exist"
msgstr ""
-#: .././mainwin.py:21566
+#: .././mainwin.py:22177
msgid ""
"Delete downloaded video files (doesn't remove videos from Tartube's database)"
msgstr ""
-#: .././mainwin.py:21578
+#: .././mainwin.py:22189
msgid "Also delete all video/audio files with the same name"
msgstr ""
-#: .././mainwin.py:21587
-msgid "Delete all description files"
+#: .././mainwin.py:22197
+msgid "Delete all archive files"
msgstr ""
-#: .././mainwin.py:21591
-msgid "Delete all metadata (JSON) files"
+#: .././mainwin.py:22202
+msgid "Move thumbnails into own folder"
msgstr ""
-#: .././mainwin.py:21595
-msgid "Delete all annotation files"
-msgstr ""
-
-#: .././mainwin.py:21599
+#: .././mainwin.py:22207
msgid "Delete all thumbnail files"
msgstr ""
-#: .././mainwin.py:21607
-msgid "Delete .webp/malformed .jpg files"
+#: .././mainwin.py:22213
+msgid "Convert .webp thumbnails to .jpg using FFmpeg"
msgstr ""
-#: .././mainwin.py:21611
-msgid "Delete all youtube-dl archive files"
+#: .././mainwin.py:22222
+msgid "Move other metadata files into own folder"
msgstr ""
-#: .././mainwin.py:21620
+#: .././mainwin.py:22230
+msgid "Delete all description files"
+msgstr ""
+
+#: .././mainwin.py:22234
+msgid "Delete all metadata (JSON) files"
+msgstr ""
+
+#: .././mainwin.py:22238
+msgid "Delete all annotation files"
+msgstr ""
+
+#. (signal_connect appears below)
+#: .././mainwin.py:22247
msgid "Select none"
msgstr ""
#. 'Reset' button
#. (signal_connect appears below)
-#: .././config.py:408 .././config.py:8910
+#: .././config.py:410 .././config.py:9331 .././config.py:9372
msgid "Reset"
msgstr ""
-#: .././config.py:412
+#: .././config.py:414
msgid "Reset changes without closing the window"
msgstr ""
#. 'Apply' button
-#: .././config.py:417
+#: .././config.py:419
msgid "Apply"
msgstr ""
-#: .././config.py:421
+#: .././config.py:423
msgid "Apply changes without closing the window"
msgstr ""
-#: .././config.py:429
+#: .././config.py:431
msgid "Apply changes"
msgstr ""
-#: .././config.py:438
+#: .././config.py:440
msgid "Cancel changes"
msgstr ""
-#: .././config.py:1279
+#: .././config.py:1281
msgid "Listed as"
msgstr ""
-#: .././config.py:1291
+#: .././config.py:1293
msgid "Contained in"
msgstr ""
-#: .././config.py:1350
+#: .././config.py:1352
msgid "Channel URL"
msgstr ""
-#: .././config.py:1352
+#: .././config.py:1354
msgid "Playlist URL"
msgstr ""
-#: .././config.py:1354 .././config.py:2370
+#: .././config.py:1356 .././config.py:2372
msgid "Video URL"
msgstr ""
-#: .././config.py:1384
+#: .././config.py:1386
msgid "Download to"
msgstr ""
-#: .././config.py:1423
+#: .././config.py:1425
msgid "Location"
msgstr ""
-#: .././config.py:1444
+#: .././config.py:1446
msgid "Download _options"
msgstr ""
-#: .././config.py:1448 .././config.py:1968 .././config.py:2967
-#: .././config.py:3006
+#: .././config.py:1450 .././config.py:1970 .././config.py:3011
+#: .././config.py:3050
msgid "Download options"
msgstr ""
-#: .././config.py:1452
+#: .././config.py:1454
msgid "Apply download options"
msgstr ""
-#: .././config.py:1459
+#: .././config.py:1461
msgid "Edit download options"
msgstr ""
-#: .././config.py:1466
+#: .././config.py:1468
msgid "Remove download options"
msgstr ""
-#: .././config.py:1605
+#: .././config.py:1607
msgid "Close this window"
msgstr ""
#. Add this tab...
-#: .././config.py:2155 .././config.py:5134 .././config.py:5593
-#: .././config.py:5952 .././config.py:6192
+#: .././config.py:2157 .././config.py:5178 .././config.py:5637
+#: .././config.py:5996 .././config.py:6250
msgid "_General"
msgstr ""
-#: .././config.py:2161
+#: .././config.py:2163
msgid "General options"
msgstr ""
-#: .././config.py:2172
+#: .././config.py:2174
msgid "These options have been applied to:"
msgstr ""
-#: .././config.py:2178
+#: .././config.py:2180
msgid "All channels, playlists and folders"
msgstr ""
-#: .././config.py:2213
-msgid ""
-"Extra youtube-dl command line options (e.g. --help; do not use -o or --"
-"output)"
-msgstr ""
-
-#: .././config.py:2241
-msgid "Hide advanced download options"
+#: .././config.py:2215
+msgid "Extra command line options (e.g. --help; do not use -o or --output)"
msgstr ""
#: .././config.py:2243
+msgid "Hide advanced download options"
+msgstr ""
+
+#: .././config.py:2245
msgid "Show advanced download options"
msgstr ""
-#: .././config.py:2253
+#: .././config.py:2255
msgid "Import general download options into this window"
msgstr ""
-#: .././config.py:2268
+#: .././config.py:2270
msgid "Completely reset all download options to their default values"
msgstr ""
#. Add this tab...
-#: .././config.py:2282
+#: .././config.py:2284
msgid "_Files"
msgstr ""
-#: .././config.py:2302
+#: .././config.py:2304
msgid "File _names"
msgstr ""
-#: .././config.py:2310
+#: .././config.py:2312
msgid "File name options"
msgstr ""
-#: .././config.py:2315
+#: .././config.py:2317
msgid "Format for video file names"
msgstr ""
-#: .././config.py:2339
-msgid "youtube-dl file output template"
+#: .././config.py:2341
+msgid "File output template"
msgstr ""
-#: .././config.py:2359
+#: .././config.py:2361
msgid "Add to template:"
msgstr ""
-#: .././config.py:2364 .././config.py:5023
+#: .././config.py:2366 .././config.py:5067
msgid "Video properties"
msgstr ""
-#: .././config.py:2366
+#: .././config.py:2368
msgid "Video ID"
msgstr ""
-#: .././config.py:2367
+#: .././config.py:2369
msgid "Video title"
msgstr ""
-#: .././config.py:2368
+#: .././config.py:2370
msgid "Alternative video ID"
msgstr ""
-#: .././config.py:2369
+#: .././config.py:2371
msgid "Secondary video title"
msgstr ""
-#: .././config.py:2371
+#: .././config.py:2373
msgid "Video filename extension"
msgstr ""
-#: .././config.py:2372
+#: .././config.py:2374
msgid "Video licence"
msgstr "Video license"
-#: .././config.py:2373
+#: .././config.py:2375
msgid "Age restriction (years)"
msgstr ""
-#: .././config.py:2374
+#: .././config.py:2376
msgid "Is a livestream"
msgstr ""
-#: .././config.py:2375
+#: .././config.py:2377
msgid "Autonumber videos, starting at 0"
msgstr ""
-#: .././config.py:2377
+#: .././config.py:2379
msgid "Creator/uploader"
msgstr ""
-#: .././config.py:2379 .././config.py:2380
+#: .././config.py:2381 .././config.py:2382
msgid "Full name of video uploader"
msgstr ""
-#: .././config.py:2381
+#: .././config.py:2383
msgid "Nickname/ID of video uploader"
msgstr ""
-#: .././config.py:2382
+#: .././config.py:2384
msgid "Channel name"
msgstr ""
-#: .././config.py:2383
+#: .././config.py:2385
msgid "Channel ID"
msgstr ""
-#: .././config.py:2384
+#: .././config.py:2386
msgid "Playlist name"
msgstr ""
-#: .././config.py:2385
+#: .././config.py:2387
msgid "Playlist ID"
msgstr ""
-#: .././config.py:2386
+#: .././config.py:2388
msgid "Video index in playlist"
msgstr ""
-#: .././config.py:2388
+#: .././config.py:2390
msgid "Date/time/location"
msgstr ""
-#: .././config.py:2390
+#: .././config.py:2392
msgid "Release date (YYYYMMDD)"
msgstr ""
-#: .././config.py:2391
+#: .././config.py:2393
msgid "Release time (UNIX timestamp)"
msgstr ""
-#: .././config.py:2392
+#: .././config.py:2394
msgid "Upload data (YYYYMMDD)"
msgstr ""
-#: .././config.py:2393
+#: .././config.py:2395
msgid "Video length (seconds)"
msgstr ""
-#: .././config.py:2394
+#: .././config.py:2396
msgid "Filming location"
msgstr ""
-#: .././config.py:2396 .././config.py:2398
+#: .././config.py:2398 .././config.py:2400
msgid "Video format"
msgstr ""
-#: .././config.py:2399
-msgid "youtube-dl format code"
+#: .././config.py:2401
+msgid "Video format code"
msgstr ""
-#: .././config.py:2400
+#: .././config.py:2402
msgid "Video width"
msgstr ""
-#: .././config.py:2401
+#: .././config.py:2403
msgid "Video height"
msgstr ""
-#: .././config.py:2403
+#: .././config.py:2405
msgid "Video frame rate"
msgstr ""
-#: .././config.py:2404
+#: .././config.py:2406
msgid "Average video/audio bitrate (KiB/s)"
msgstr ""
-#: .././config.py:2405
+#: .././config.py:2407
msgid "Average video bitrate (KiB/s)"
msgstr ""
-#: .././config.py:2406
+#: .././config.py:2408
msgid "Average audio bitrate (KiB/s)"
msgstr ""
-#: .././config.py:2408
+#: .././config.py:2410
msgid "Ratings/comments"
msgstr ""
-#: .././config.py:2410
+#: .././config.py:2412
msgid "Number of views"
msgstr ""
-#: .././config.py:2411
+#: .././config.py:2413
msgid "Number of positive ratings"
msgstr ""
-#: .././config.py:2412
+#: .././config.py:2414
msgid "Number of negative ratings"
msgstr ""
-#: .././config.py:2413
+#: .././config.py:2415
msgid "Average rating"
msgstr ""
-#: .././config.py:2414
+#: .././config.py:2416
msgid "Number of reposts"
msgstr ""
-#: .././config.py:2415
+#: .././config.py:2417
msgid "Number of comments"
msgstr ""
-#: .././config.py:2451
+#: .././config.py:2453
msgid "Add"
msgstr ""
#. Add this tab...
-#: .././config.py:2479 .././config.py:6549
+#: .././config.py:2481 .././config.py:6723
msgid "_Filesystem"
msgstr ""
-#: .././config.py:2489
+#: .././config.py:2491
msgid "Filesystem options"
msgstr ""
-#: .././config.py:2494
+#: .././config.py:2496
msgid "Restrict filenames to ASCII characters"
msgstr ""
-#: .././config.py:2500
+#: .././config.py:2502
msgid "Use the server's file modification time"
msgstr ""
-#: .././config.py:2507
+#: .././config.py:2509
msgid "Filesystem overrides"
msgstr ""
-#: .././config.py:2512
+#: .././config.py:2514
msgid "Download all videos into this folder"
msgstr ""
-#: .././config.py:2566
-msgid "_Write files"
+#: .././config.py:2568
+msgid "_Write/move files"
msgstr ""
-#: .././config.py:2572
-msgid "Write other file options"
+#: .././config.py:2576
+msgid "File write options"
msgstr ""
-#: .././config.py:2577
+#: .././config.py:2581
msgid "Write video's description to a .description file"
msgstr ""
-#: .././config.py:2583
+#: .././config.py:2587
msgid "Write video's metadata to an .info.json file"
msgstr ""
-#: .././config.py:2589
+#: .././config.py:2594
msgid "Write video's annotations to an .annotations.xml file"
msgstr ""
-#: .././config.py:2595
-msgid "Write the video's thumbnail to the same folder"
+#: .././config.py:2602
+msgid ""
+"Annotations are not downloaded when checking videos/channels/playlists/"
+"folders"
msgstr ""
#: .././config.py:2609
+msgid "Write the video's thumbnail to the same folder"
+msgstr ""
+
+#: .././config.py:2616
+msgid "File move options"
+msgstr ""
+
+#: .././config.py:2621
+msgid "Move video's description file into a sub-folder"
+msgstr ""
+
+#: .././config.py:2627
+msgid "Write video's metadata file into a sub-folder"
+msgstr ""
+
+#: .././config.py:2633
+msgid "Write video's annotations file into a sub-folder"
+msgstr ""
+
+#: .././config.py:2639
+msgid "Write the video's thumbnail into a sub-folder"
+msgstr ""
+
+#: .././config.py:2653
msgid "_Keep files"
msgstr ""
-#: .././config.py:2615
+#: .././config.py:2659
msgid "Options during real (not simulated) downloads"
msgstr ""
-#: .././config.py:2621 .././config.py:2652
+#: .././config.py:2665 .././config.py:2696
msgid "Keep the description file after Tartube shuts down"
msgstr ""
-#: .././config.py:2627 .././config.py:2658
+#: .././config.py:2671 .././config.py:2702
msgid "Keep the metadata file after Tartube shuts down"
msgstr ""
-#: .././config.py:2633 .././config.py:2664
+#: .././config.py:2677 .././config.py:2708
msgid "Keep the annotations file after Tartube shuts down"
msgstr ""
-#: .././config.py:2639 .././config.py:2670
+#: .././config.py:2683 .././config.py:2714
msgid "Keep the thumbnail file after Tartube shuts down"
msgstr ""
-#: .././config.py:2646
+#: .././config.py:2690
msgid "Options during simulated (not real) downloads"
msgstr ""
#. Add this tab...
-#: .././config.py:2684
+#: .././config.py:2728
msgid "F_ormats"
msgstr ""
-#: .././config.py:2703
+#: .././config.py:2747
msgid "_Preferred"
msgstr ""
-#: .././config.py:2711
+#: .././config.py:2755
msgid "Preferred format options"
msgstr ""
-#: .././config.py:2717
+#: .././config.py:2761
msgid "Recognised video/audio formats"
msgstr ""
-#: .././config.py:2728
+#: .././config.py:2772
msgid "Add format"
msgstr ""
-#: .././config.py:2734
+#: .././config.py:2778
msgid "List of preferred formats"
msgstr ""
-#: .././config.py:2751
+#: .././config.py:2795
msgid "Remove format"
msgstr ""
-#: .././config.py:2804
+#: .././config.py:2848
msgid "If a merge is required after post-processing, output to this format:"
msgstr ""
#. Add this tab...
-#: .././config.py:2830 .././config.py:3524
+#: .././config.py:2874 .././config.py:3568
msgid "_Advanced"
msgstr ""
-#: .././config.py:2839
+#: .././config.py:2883
msgid "Multiple format options"
msgstr ""
-#: .././config.py:2848
+#: .././config.py:2892
msgid ""
-"Multiple formats will not be downloaded, because youtube-dl is creating an "
-"archive file"
+"Multiple formats will not be downloaded, because an archive file will be "
+"created"
msgstr ""
-#: .././config.py:2851
+#: .././config.py:2895
msgid "The archive file can be disabled in the System Preferences window"
msgstr ""
-#: .././config.py:2860
+#: .././config.py:2904
msgid ""
"For each video, download the first available format from the preferred list"
msgstr ""
-#: .././config.py:2874
+#: .././config.py:2918
msgid ""
"From the preferred list, download the first format that's available for all "
"videos"
msgstr ""
-#: .././config.py:2888
+#: .././config.py:2932
msgid "For each video, download all available formats from the preferred list"
msgstr ""
-#: .././config.py:2901
+#: .././config.py:2945
msgid "Download all available formats for all videos"
msgstr ""
-#: .././config.py:2934
+#: .././config.py:2978
msgid "Other format options"
msgstr ""
-#: .././config.py:2939
+#: .././config.py:2983
msgid "Prefer free video formats, unless one is specified above"
msgstr ""
-#: .././config.py:2945
+#: .././config.py:2989
msgid "Do not download DASH-related data for YouTube videos"
msgstr ""
#. Add this tab...
-#: .././config.py:2961 .././config.py:2980 .././config.py:8017
+#: .././config.py:3005 .././config.py:3024 .././config.py:8270
msgid "_Downloads"
msgstr ""
-#: .././config.py:3023
+#: .././config.py:3067
msgid "_Playlists"
msgstr ""
-#: .././config.py:3038
+#: .././config.py:3082
msgid "_Size limits"
msgstr ""
-#: .././config.py:3052
+#: .././config.py:3096
msgid "_Dates"
msgstr ""
-#: .././config.py:3064
+#: .././config.py:3108
msgid "_Views"
msgstr ""
-#: .././config.py:3077
+#: .././config.py:3121
msgid "_Filtering"
msgstr ""
-#: .././config.py:3091
+#: .././config.py:3135
msgid "_External"
msgstr ""
-#: .././config.py:3103
+#: .././config.py:3147
msgid "_Sound only"
msgstr ""
-#: .././config.py:3108
+#: .././config.py:3152
msgid "Sound only options"
msgstr ""
-#: .././config.py:3114
+#: .././config.py:3158
msgid ""
"Download each video, extract the sound, and then discard the original videos"
msgstr ""
-#: .././config.py:3119
+#: .././config.py:3163
msgid "(requires that FFmpeg or AVConv is installed on your system)"
msgstr ""
-#: .././config.py:3129
+#: .././config.py:3173
msgid "Use this audio format:"
msgstr ""
-#: .././config.py:3144
+#: .././config.py:3188
msgid "Use this audio quality:"
msgstr ""
-#: .././config.py:3150 .././config.py:3223
+#: .././config.py:3194 .././config.py:3267
msgid "High"
msgstr ""
-#: .././config.py:3151 .././config.py:3224
+#: .././config.py:3195 .././config.py:3268
msgid "Medium"
msgstr ""
-#: .././config.py:3152 .././config.py:3225
+#: .././config.py:3196 .././config.py:3269
msgid "Low"
msgstr ""
-#: .././config.py:3170
-msgid "_Post-process"
+#: .././config.py:3214
+msgid "_Post-processing"
msgstr ""
-#: .././config.py:3176 .././config.py:3493
+#: .././config.py:3220 .././config.py:3537
msgid "Post-processing options"
msgstr ""
-#: .././config.py:3182
+#: .././config.py:3226
msgid "Post-process video files to convert them to audio-only files"
msgstr ""
-#: .././config.py:3189
-msgid "Prefer avconv over ffmpeg"
+#: .././config.py:3233
+msgid "Prefer AVConv over FFmpeg"
msgstr ""
-#: .././config.py:3197
-msgid "Prefer ffmpeg over avconv (default)"
+#: .././config.py:3241
+msgid "Prefer FFmpeg over AVConv (default)"
msgstr ""
-#: .././config.py:3205
+#: .././config.py:3249
msgid "Audio format of the post-processed file"
msgstr ""
-#: .././config.py:3218
+#: .././config.py:3262
msgid "Audio quality of the post-processed file"
msgstr ""
-#: .././config.py:3235
+#: .././config.py:3279
msgid "Encode video to another format, if necessary"
msgstr ""
-#: .././config.py:3247
+#: .././config.py:3291
msgid "Arguments to pass to post-processor"
msgstr ""
-#: .././config.py:3257
+#: .././config.py:3301
msgid "Keep original file after processing it"
msgstr ""
-#: .././config.py:3264
+#: .././config.py:3308
msgid "Merge subtitles file with video (.mp4 only)"
msgstr ""
-#: .././config.py:3275
+#: .././config.py:3319
msgid "Embed thumbnail in audio file as cover art"
msgstr ""
-#: .././config.py:3281
+#: .././config.py:3325
msgid "Write metadata to the video file"
msgstr ""
-#: .././config.py:3287
+#: .././config.py:3331
msgid "Automatically correct known faults of the file"
msgstr ""
-#: .././config.py:3293
+#: .././config.py:3337
msgid "Do nothing"
msgstr ""
-#: .././config.py:3294
+#: .././config.py:3338
msgid "Warn, but do nothing"
msgstr ""
-#: .././config.py:3295
+#: .././config.py:3339
msgid "Fix if possible, otherwise warn"
msgstr ""
#. Add this tab...
-#: .././config.py:3312
+#: .././config.py:3356
msgid "S_ubtitles"
msgstr ""
-#: .././config.py:3329
+#: .././config.py:3373
msgid "_Options"
msgstr ""
-#: .././config.py:3333
+#: .././config.py:3377
msgid "Subtitles options"
msgstr ""
-#: .././config.py:3339
+#: .././config.py:3383
msgid "Don't download the subtitles file"
msgstr ""
-#: .././config.py:3350
+#: .././config.py:3394
msgid "Download the automatic subtitles file (YouTube only)"
msgstr ""
-#: .././config.py:3362
+#: .././config.py:3406
msgid "Download all available subtitles files"
msgstr ""
-#: .././config.py:3374
+#: .././config.py:3418
msgid "Download subtitles file for these languages:"
msgstr ""
-#: .././config.py:3397
+#: .././config.py:3441
msgid "Add language"
msgstr ""
-#: .././config.py:3410
+#: .././config.py:3454
msgid "Remove language"
msgstr ""
-#: .././config.py:3468
+#: .././config.py:3512
msgid "_More options"
msgstr ""
-#: .././config.py:3474
+#: .././config.py:3518
msgid "Subtitle format options"
msgstr ""
-#: .././config.py:3480
+#: .././config.py:3524
msgid "Preferred subtitle format(s), e.g. 'srt', 'vtt', 'srt/ass/vtt/lrc/best'"
msgstr ""
-#: .././config.py:3498
+#: .././config.py:3542
msgid "Applies to .mp4 videos only; requires FFmpeg/AVConv"
msgstr ""
-#: .././config.py:3505
+#: .././config.py:3549
msgid "During post-processing, merge subtitles file with video"
msgstr ""
-#: .././config.py:3544
+#: .././config.py:3588
msgid "_Authentication"
msgstr ""
-#: .././config.py:3552
+#: .././config.py:3596
msgid "Authentication options"
msgstr ""
-#: .././config.py:3557
+#: .././config.py:3601
msgid "Username with which to log in"
msgstr ""
-#: .././config.py:3567
+#: .././config.py:3611
msgid "Password with which to log in"
msgstr ""
-#: .././config.py:3577
+#: .././config.py:3621
msgid "Password required for this URL"
msgstr ""
-#: .././config.py:3587
+#: .././config.py:3631
msgid "Two-factor authentication code"
msgstr ""
-#: .././config.py:3597
+#: .././config.py:3641
msgid "Use .netrc authentication data"
msgstr ""
-#: .././config.py:3610
+#: .././config.py:3654
msgid "_Network"
msgstr ""
-#: .././config.py:3616
+#: .././config.py:3660
msgid "Network options"
msgstr ""
-#: .././config.py:3621
+#: .././config.py:3665
msgid "Use this HTTP/HTTPS proxy"
msgstr ""
-#: .././config.py:3631
+#: .././config.py:3675
msgid "Time to wait for socket connection, before giving up"
msgstr ""
-#: .././config.py:3641
+#: .././config.py:3685
msgid "Bind with this Client-side IP address"
msgstr ""
-#: .././config.py:3651
+#: .././config.py:3695
msgid "Connect using IPv4 only"
msgstr ""
-#: .././config.py:3657
+#: .././config.py:3701
msgid "Connect using IPv6 only"
msgstr ""
-#: .././config.py:3671
+#: .././config.py:3715
msgid "_Geo-restriction"
msgstr ""
-#: .././config.py:3679
+#: .././config.py:3723
msgid "Geo-restriction options"
msgstr ""
-#: .././config.py:3684
+#: .././config.py:3728
msgid "Use this proxy to verify IP address"
msgstr ""
-#: .././config.py:3694
+#: .././config.py:3738
msgid "Bypass using fake X-Forwarded-For HTTP header"
msgstr ""
-#: .././config.py:3700
+#: .././config.py:3744
msgid "Don't bypass using fake HTTP header"
msgstr ""
-#: .././config.py:3706
+#: .././config.py:3750
msgid "Bypass geo-restriction with ISO 3166-2 country code"
msgstr ""
-#: .././config.py:3716
+#: .././config.py:3760
msgid "Bypass with explicit IP block in CIDR notation"
msgstr ""
-#: .././config.py:3739
+#: .././config.py:3783
msgid "Workaround options"
msgstr ""
-#: .././config.py:3744
-msgid "Custom user agent for youtube-dl"
+#: .././config.py:3788
+msgid "Custom user agent"
msgstr ""
-#: .././config.py:3754
+#: .././config.py:3798
msgid "Custom referer if video access has restricted domain"
msgstr ""
-#: .././config.py:3764
+#: .././config.py:3808
msgid "Force this encoding (experimental)"
msgstr ""
-#: .././config.py:3774
+#: .././config.py:3818
msgid "Suppress HTTPS certificate validation"
msgstr ""
-#: .././config.py:3781
+#: .././config.py:3825
msgid ""
"Use an unencrypted connection to retrieve information about videos (YouTube "
"only)"
msgstr ""
-#: .././config.py:3862
+#: .././config.py:3906
msgid "Prefer HLS (HTTP Live Streaming)"
msgstr ""
-#: .././config.py:3868
+#: .././config.py:3912
msgid "Prefer FFMpeg over native HLS downloader"
msgstr ""
-#: .././config.py:3874
+#: .././config.py:3918
msgid "Include advertisements (experimental feature)"
msgstr ""
-#: .././config.py:3880
+#: .././config.py:3924
msgid "Ignore errors and continue the download operation"
msgstr ""
-#: .././config.py:3886
+#: .././config.py:3930
msgid "Number of retries"
msgstr ""
-#: .././config.py:3906
+#: .././config.py:3950
msgid "Download videos suitable for this age"
msgstr ""
-#: .././config.py:3926
+#: .././config.py:3970
msgid "Playlist options"
msgstr ""
-#: .././config.py:3932
+#: .././config.py:3976
msgid ""
-"youtube-dl treats channels and playlists the same way, so these options can "
-"be used with both"
+"Channels and playlists are handled in the same way, so these options can be "
+"used with both"
msgstr ""
-#: .././config.py:3939
+#: .././config.py:3983
msgid "Start downloading playlist from index"
msgstr ""
-#: .././config.py:3950
+#: .././config.py:3994
msgid "Stop downloading playlist at index"
msgstr ""
-#: .././config.py:3961
+#: .././config.py:4005
msgid "Abort operation after downloading this many videos"
msgstr ""
-#: .././config.py:3972
+#: .././config.py:4016
msgid "Abort downloading the playlist if an error occurs"
msgstr ""
-#: .././config.py:3978
+#: .././config.py:4022
msgid "Download playlist in reverse order"
msgstr ""
-#: .././config.py:3984
+#: .././config.py:4028
msgid "Download playlist in random order"
msgstr ""
-#: .././config.py:3999
+#: .././config.py:4043
msgid "Video size limit options"
msgstr ""
-#: .././config.py:4004
+#: .././config.py:4048
msgid "Minimum file size for video downloads"
msgstr ""
-#: .././config.py:4021
+#: .././config.py:4065
msgid "Maximum file size for video downloads"
msgstr ""
-#: .././config.py:4048
+#: .././config.py:4092
msgid "Video date options"
msgstr ""
-#: .././config.py:4053
+#: .././config.py:4097
msgid "Only videos uploaded on this date"
msgstr ""
-#: .././config.py:4063 .././config.py:4083 .././config.py:4103
-#: .././config.py:8906
+#: .././config.py:4107 .././config.py:4127 .././config.py:4147
+#: .././config.py:9327 .././config.py:9368
msgid "Set"
msgstr ""
-#: .././config.py:4073
+#: .././config.py:4117
msgid "Only videos uploaded before this date"
msgstr ""
-#: .././config.py:4093
+#: .././config.py:4137
msgid "Only videos uploaded after this date"
msgstr ""
-#: .././config.py:4123
+#: .././config.py:4167
msgid "Video views options"
msgstr ""
-#: .././config.py:4128
+#: .././config.py:4172
msgid "Minimum number of views"
msgstr ""
-#: .././config.py:4139
+#: .././config.py:4183
msgid "Maximum number of views"
msgstr ""
-#: .././config.py:4164
+#: .././config.py:4208
msgid "Video filtering options"
msgstr ""
-#: .././config.py:4169
+#: .././config.py:4213
msgid "Download only matching titles (regex or caseless substring)"
msgstr ""
-#: .././config.py:4180
+#: .././config.py:4224
msgid "Don't download only matching titles (regex or caseless substring)"
msgstr ""
-#: .././config.py:4192
+#: .././config.py:4236
msgid "Generic video filter, for example:"
msgstr ""
-#: .././config.py:4212
+#: .././config.py:4256
msgid "External downloader options"
msgstr ""
-#: .././config.py:4217
+#: .././config.py:4261
msgid "Use this external downloader"
msgstr ""
-#: .././config.py:4234
+#: .././config.py:4278
msgid "Arguments to pass to external downloader"
msgstr ""
-#: .././config.py:4307 .././config.py:4733
+#: .././config.py:4351 .././config.py:4777
msgid "This procedure cannot be reversed. Are you sure you want to continue?"
msgstr ""
-#: .././config.py:4569
+#: .././config.py:4613
msgid ""
"This option won't work unless the format is also added to the list of "
"preferred formats above"
msgstr ""
-#: .././config.py:4793
+#: .././config.py:4837
msgid "When the window is re-opened, some download options will be hidden"
msgstr ""
-#: .././config.py:4802
+#: .././config.py:4846
msgid "Show advanced download options (when window re-opens)"
msgstr ""
-#: .././config.py:4815
+#: .././config.py:4859
msgid "When the window is re-opened, all download options will be visible"
msgstr ""
-#: .././config.py:4824
+#: .././config.py:4868
msgid "Hide advanced download options (when window re-opens)"
msgstr ""
-#: .././config.py:5137 .././config.py:5596 .././config.py:5955
+#: .././config.py:5181 .././config.py:5640 .././config.py:5999
msgid "General properties"
msgstr ""
-#: .././config.py:5168
+#: .././config.py:5212
msgid "Always simulate download of this video"
msgstr ""
-#: .././config.py:5191
+#: .././config.py:5235
msgid "Video has been downloaded"
msgstr ""
-#: .././config.py:5198
+#: .././config.py:5242
msgid "File size"
msgstr ""
-#: .././config.py:5212
+#: .././config.py:5256
msgid "Video is marked as unwatched"
msgstr ""
-#: .././config.py:5219
+#: .././config.py:5263
msgid "Upload time"
msgstr ""
-#: .././config.py:5233
+#: .././config.py:5277
msgid "Video is archived"
msgstr ""
-#: .././config.py:5240
+#: .././config.py:5284
msgid "Video is bookmarked"
msgstr ""
-#: .././config.py:5247
+#: .././config.py:5291
msgid "Receive time"
msgstr ""
-#: .././config.py:5261
+#: .././config.py:5305
msgid "Video is favourite"
msgstr "Video is favorite"
-#: .././config.py:5268
+#: .././config.py:5312
msgid "Video is in waiting list"
msgstr ""
-#: .././config.py:5291
+#: .././config.py:5335
msgid "Livestream properties"
msgstr ""
-#: .././config.py:5296
+#: .././config.py:5340
msgid "Livestream status"
msgstr ""
-#: .././config.py:5307
+#: .././config.py:5351
msgid "Waiting to start"
msgstr ""
-#: .././config.py:5309
+#: .././config.py:5353
msgid "Stream has started"
msgstr ""
-#: .././config.py:5311
+#: .././config.py:5355
msgid "Not a livestream"
msgstr ""
-#: .././config.py:5318
+#: .././config.py:5362
msgid "When the livestream starts, show a desktop notification"
msgstr ""
-#: .././config.py:5327
+#: .././config.py:5371
msgid "When the livestream starts, play an alarm"
msgstr ""
-#: .././config.py:5337
+#: .././config.py:5381
msgid "When the livestream starts, open it in the system's web browser"
msgstr ""
-#: .././config.py:5349
+#: .././config.py:5393
msgid "When the livestream starts, begin downloading it immediately"
msgstr ""
-#: .././config.py:5360 .././config.py:8436
+#: .././config.py:5404 .././config.py:8718
msgid "When a livestream stops, download it (overwriting any earlier file)"
msgstr ""
-#: .././config.py:5376
+#: .././config.py:5420
msgid "_Description"
msgstr ""
-#: .././config.py:5380
+#: .././config.py:5424
msgid "Video description"
msgstr ""
-#: .././config.py:5401 .././config.py:5753
+#: .././config.py:5445 .././config.py:5797
msgid "Errors / Warnings"
msgstr ""
-#: .././config.py:5407
+#: .././config.py:5451
msgid "Error messages produced the last time this video was checked/downloaded"
msgstr ""
-#: .././config.py:5422
+#: .././config.py:5466
msgid ""
"Warning messages produced the last time this video was checked/downloaded"
msgstr ""
-#: .././config.py:5478
+#: .././config.py:5522
msgid "Channel properties"
msgstr ""
-#: .././config.py:5481
+#: .././config.py:5525
msgid "Playlist properties"
msgstr ""
-#: .././config.py:5614
+#: .././config.py:5658
msgid "Always simulate download of videos in this channel"
msgstr ""
-#: .././config.py:5616
+#: .././config.py:5660
msgid "Always simulate download of videos in this playlist"
msgstr ""
-#: .././config.py:5626
+#: .././config.py:5670
msgid "Disable checking/downloading for this channel"
msgstr ""
-#: .././config.py:5628
+#: .././config.py:5672
msgid "Disable checking/downloading for this playlist"
msgstr ""
-#: .././config.py:5638
+#: .././config.py:5682
msgid "This channel is marked as a favourite"
msgstr "This channel is marked as a favorite"
-#: .././config.py:5640
+#: .././config.py:5684
msgid "This playlist is marked as a favourite"
msgstr "This playlist is marked as a favorite"
-#: .././config.py:5650
+#: .././config.py:5694
msgid "Total videos"
msgstr ""
-#: .././config.py:5674
+#: .././config.py:5718
msgid "Favourite videos"
msgstr "Favorite videos"
-#: .././config.py:5686
+#: .././config.py:5730
msgid "Downloaded videos"
msgstr ""
-#: .././config.py:5708
+#: .././config.py:5752
msgid "_RSS feed"
msgstr ""
-#: .././config.py:5711
+#: .././config.py:5755
msgid "RSS feed"
msgstr ""
-#: .././config.py:5717
+#: .././config.py:5761
msgid ""
"If Tartube cannot detect the channel's RSS feed, you can enter the URL here"
msgstr ""
-#: .././config.py:5722
+#: .././config.py:5766
msgid ""
"If Tartube cannot detect the playlist's RSS feed, you can enter the URL here"
msgstr ""
-#: .././config.py:5727
+#: .././config.py:5771
msgid "(The feed is used to detect livestreams on compatible websites)"
msgstr ""
-#: .././config.py:5759
+#: .././config.py:5803
msgid ""
"Error messages produced the last time this channel was checked/downloaded"
msgstr ""
-#: .././config.py:5764
+#: .././config.py:5808
msgid ""
"Error messages produced the last time this playlist was checked/downloaded"
msgstr ""
-#: .././config.py:5782
+#: .././config.py:5826
msgid ""
"Warning messages produced the last time this channel was checked/downloaded"
msgstr ""
-#: .././config.py:5787
+#: .././config.py:5831
msgid ""
"Warning messages produced the last time this playlist was checked/downloaded"
msgstr ""
-#: .././config.py:5844
+#: .././config.py:5888
msgid "Folder properties"
msgstr ""
-#: .././config.py:5972
+#: .././config.py:6016
msgid "Always simulate download of videos"
msgstr ""
-#: .././config.py:5979
+#: .././config.py:6023
msgid "Disable checking/downloading for this folder"
msgstr ""
-#: .././config.py:5986
+#: .././config.py:6030
msgid "This folder is marked as a favourite"
msgstr "This folder is marked as a favorite"
-#: .././config.py:5993
+#: .././config.py:6037
msgid "This folder is hidden"
msgstr ""
-#: .././config.py:6000
+#: .././config.py:6044
msgid "This folder can't be deleted by the user"
msgstr ""
-#: .././config.py:6007
+#: .././config.py:6051
msgid "This is a system-controlled folder"
msgstr ""
-#: .././config.py:6014
+#: .././config.py:6058
msgid "Only videos can be added to this folder"
msgstr ""
-#: .././config.py:6021
+#: .././config.py:6065
msgid "All contents deleted when Tartube shuts down"
msgstr ""
-#: .././config.py:6074
+#: .././config.py:6119
msgid "System preferences"
msgstr ""
-#: .././config.py:6211
+#: .././config.py:6270
msgid "_Language"
msgstr ""
-#: .././config.py:6216
+#: .././config.py:6275
msgid "Language preferences"
msgstr ""
-#: .././config.py:6221
+#: .././config.py:6280
msgid "Language"
msgstr ""
-#: .././config.py:6257
+#: .././config.py:6316
msgid "_Stability"
msgstr ""
-#: .././config.py:6267
+#: .././config.py:6326
msgid "Gtk library"
msgstr ""
-#: .././config.py:6272
+#: .././config.py:6331
msgid "Current version of the system's Gtk library"
msgstr ""
-#: .././config.py:6287
+#: .././config.py:6346
msgid "Gtk stability"
msgstr ""
-#: .././config.py:6326
+#: .././config.py:6385
msgid ""
"Tartube uses the Gtk graphics library. This library is notoriously "
"unreliable and may even causes crashes."
msgstr ""
-#: .././config.py:6333
+#: .././config.py:6392
msgid ""
"By default, some cosmetic features are disabled (for example, in the Videos "
"tab, the list of videos is not updated until the end of a download "
"operation)."
msgstr ""
-#: .././config.py:6341
+#: .././config.py:6400
msgid ""
"If you think that your system Gtk has been fixed (or if you want to test Gtk "
"stability), you can re-enable the cosmetic features."
msgstr ""
-#: .././config.py:6351
+#: .././config.py:6410
msgid "Disable some cosmetic features to prevent crashes and other issues"
msgstr ""
-#: .././config.py:6369
+#: .././config.py:6428
msgid "_Modules"
msgstr ""
-#: .././config.py:6374
+#: .././config.py:6433
msgid "Module availability"
msgstr ""
-#: .././config.py:6380
+#: .././config.py:6439
msgid "feedparser module is available (required for detecting livestreams)"
msgstr ""
-#: .././config.py:6390
+#: .././config.py:6449
msgid "moviepy module is available (finds the length of videos, if unknown)"
msgstr ""
-#: .././config.py:6400
+#: .././config.py:6459
msgid "playsound module is available (sound an alarm when a livestream starts)"
msgstr ""
-#: .././config.py:6410
+#: .././config.py:6469
msgid ""
"XDG module is available (saves the config file in the standard location)"
msgstr ""
-#: .././config.py:6420
+#: .././config.py:6479
+msgid ""
+"Notify module is available (shows desktop notifications; Linux/*BSD only)"
+msgstr ""
+
+#: .././config.py:6489
msgid "Module preferences"
msgstr ""
-#: .././config.py:6426
+#: .././config.py:6495
msgid ""
"Use 'moviepy' module to get a video's duration, if not known (may be slow)"
msgstr ""
-#: .././config.py:6438
+#: .././config.py:6507
msgid "Timeout applied when moviepy checks a video file"
msgstr ""
-#: .././config.py:6463
+#: .././config.py:6532
msgid "_Video matching"
msgstr ""
-#: .././config.py:6471
+#: .././config.py:6540
msgid "Video matching preferences"
msgstr ""
-#: .././config.py:6476
+#: .././config.py:6545
msgid "When matching videos on the filesystem:"
msgstr ""
-#: .././config.py:6482
+#: .././config.py:6551
msgid "The video names must match exactly"
msgstr ""
-#: .././config.py:6489
+#: .././config.py:6558
msgid "The first # characters must match exactly"
msgstr ""
-#: .././config.py:6503
+#: .././config.py:6572
msgid "Ignore the last # characters; the remaining name must match exactly"
msgstr ""
-#: .././config.py:6572
+#: .././config.py:6618
+msgid "_Debugging"
+msgstr ""
+
+#: .././config.py:6626
+msgid "Debugging preferences"
+msgstr ""
+
+#: .././config.py:6632
+msgid ""
+"Debug messages are only visible in the terminal window. These settings are "
+"not saved"
+msgstr ""
+
+#: .././config.py:6639
+msgid "Enable application debug messages (code in mainapp.py)"
+msgstr ""
+
+#: .././config.py:6648 .././config.py:6668
+msgid "...but don't show timer debug messages"
+msgstr ""
+
+#: .././config.py:6659
+msgid "Enable main winddow debug messages (code in mainwin.py)"
+msgstr ""
+
+#: .././config.py:6679
+msgid "Enabled downloader debug messages (code in downloads.py)"
+msgstr ""
+
+#: .././config.py:6746
msgid "_Device"
msgstr ""
-#: .././config.py:6577
+#: .././config.py:6751
msgid "Device preferences"
msgstr ""
-#: .././config.py:6582
+#: .././config.py:6756
msgid "Size of device (in Mb)"
msgstr ""
-#: .././config.py:6594
+#: .././config.py:6768
msgid "Free space on device (in Mb)"
msgstr ""
-#: .././config.py:6606
+#: .././config.py:6780
msgid "Warn user if disk space is less than"
msgstr ""
-#: .././config.py:6624
+#: .././config.py:6798
msgid "Halt downloads if disk space is less than"
msgstr ""
-#: .././config.py:6663
+#: .././config.py:6837
msgid "Configuration preferences"
msgstr ""
-#: .././config.py:6668
+#: .././config.py:6842
msgid "Tartube configuration file loaded from:"
msgstr ""
-#: .././config.py:6696
+#: .././config.py:6870
msgid "D_atabase"
msgstr ""
-#: .././config.py:6702
+#: .././config.py:6876
msgid "Database preferences"
msgstr ""
-#: .././config.py:6707
+#: .././config.py:6881
msgid "Tartube data folder"
msgstr ""
-#: .././config.py:6719
+#: .././config.py:6893
msgid "Change"
msgstr ""
-#: .././config.py:6721
+#: .././config.py:6895
msgid "Change to a different data folder"
msgstr ""
-#: .././config.py:6729
+#: .././config.py:6903
msgid "Recent data folders"
msgstr ""
-#: .././config.py:6750
+#: .././config.py:6924
msgid "Switch to the selected data folder"
msgstr ""
-#: .././config.py:6760
+#: .././config.py:6934
msgid "Forget"
msgstr ""
-#: .././config.py:6763
+#: .././config.py:6937
msgid "Remove the selected data folder from the list"
msgstr ""
-#: .././config.py:6772
+#: .././config.py:6946
msgid "Forget all"
msgstr ""
-#: .././config.py:6775
+#: .././config.py:6949
msgid "Forget every folder in this list (except the current one)"
msgstr ""
-#: .././config.py:6788
+#: .././config.py:6962
msgid "Move the selected folder up the list"
msgstr ""
-#: .././config.py:6796
+#: .././config.py:6970
msgid "Move the selected folder down the list"
msgstr ""
-#: .././config.py:6824
+#: .././config.py:6998
msgid ""
"On startup, load the first database on the list (not the most recently-use "
"one)"
msgstr ""
-#: .././config.py:6834
+#: .././config.py:7008
msgid "If one database is in use, try to load others"
msgstr ""
-#: .././config.py:6842
+#: .././config.py:7016
msgid "Add new data directories to this list"
msgstr ""
-#: .././config.py:6881
+#: .././config.py:7055
msgid "DB _Errors"
msgstr ""
-#: .././config.py:6889
+#: .././config.py:7063
msgid "Database error preferences"
msgstr ""
-#: .././config.py:6894
+#: .././config.py:7068
msgid "Check Tartube's database for inconsistencies, and fix them"
msgstr ""
-#: .././config.py:6898
+#: .././config.py:7072
msgid "Check DB"
msgstr ""
-#: .././config.py:6913
+#: .././config.py:7087
msgid "_Backups"
msgstr ""
-#: .././config.py:6917
+#: .././config.py:7091
msgid "Backup preferences"
msgstr ""
-#: .././config.py:6922
+#: .././config.py:7096
msgid ""
"When saving a database file, Tartube makes a backup copy of it (in case "
"something goes wrong)"
msgstr ""
-#: .././config.py:6931
+#: .././config.py:7105
msgid "Delete the backup file as soon as the save procedure is finished"
msgstr ""
-#: .././config.py:6941
+#: .././config.py:7115
msgid "Keep the backup file, replacing any previous backup file"
msgstr ""
-#: .././config.py:6952
+#: .././config.py:7126
msgid ""
"Make a new backup file once per day, after the day's first save procedure"
msgstr ""
-#: .././config.py:6963
+#: .././config.py:7137
msgid "Make a new backup file for every save procedure"
msgstr ""
-#: .././config.py:7004
+#: .././config.py:7178
msgid "_Video deletion"
msgstr ""
-#: .././config.py:7012
+#: .././config.py:7186
msgid "Automatic video deletion preferences"
msgstr ""
-#: .././config.py:7017
+#: .././config.py:7191
msgid "Automatically delete downloaded videos after this many days"
msgstr ""
-#: .././config.py:7031
+#: .././config.py:7205
msgid "...but only delete videos which have been watched"
msgstr ""
-#: .././config.py:7062
+#: .././config.py:7236
msgid "_Temporary folders"
msgstr ""
-#: .././config.py:7068
+#: .././config.py:7242
msgid "Temporary folder preferences"
msgstr ""
-#: .././config.py:7073
+#: .././config.py:7247
msgid "Empty temporary folders when Tartube shuts down"
msgstr ""
-#: .././config.py:7082
+#: .././config.py:7256
msgid "(N.B. Temporary folders are always emptied when Tartube starts up)"
msgstr ""
-#: .././config.py:7090
+#: .././config.py:7264
msgid "Open temporary folders (on the desktop) when Tartube shuts down"
msgstr ""
#. Add this tab...
-#: .././config.py:7116
+#: .././config.py:7290
msgid "_Windows"
msgstr ""
-#: .././config.py:7138
+#: .././config.py:7312
msgid "_Main window"
msgstr ""
-#: .././config.py:7144
+#: .././config.py:7318
msgid "Main window preferences"
msgstr ""
-#: .././config.py:7149
+#: .././config.py:7323
msgid "Remember the size of the main window when shutting down"
msgstr ""
-#: .././config.py:7157
+#: .././config.py:7331
msgid "Don't show the main window toolbar"
msgstr ""
-#: .././config.py:7165
+#: .././config.py:7339
msgid "Don't show labels in the main window toolbar"
msgstr ""
-#: .././config.py:7182
+#: .././config.py:7356
msgid "Show tooltips for videos, channels, playlists and folders"
msgstr ""
-#: .././config.py:7191
+#: .././config.py:7365
msgid ""
"Replace stock icons with custom icons (in case stock icons are not visible)"
msgstr ""
-#: .././config.py:7202
+#: .././config.py:7376
msgid "Show smaller icons in the Video Index (left side of the Videos Tab)"
msgstr ""
-#: .././config.py:7213
+#: .././config.py:7387
msgid ""
"In the Video Index, show detailed statistics about the videos in each "
"channel / playlist / folder"
msgstr ""
-#: .././config.py:7224
+#: .././config.py:7398
msgid ""
"After clicking on a folder, automatically expand/collapse the tree around it"
msgstr ""
-#: .././config.py:7235
+#: .././config.py:7409
msgid "Expand the whole tree, not just the level beneath the clicked folder"
msgstr ""
-#: .././config.py:7256
+#: .././config.py:7430
msgid "Disable the 'Download all' buttons in the toolbar and the Videos Tab"
msgstr ""
-#: .././config.py:7273
+#: .././config.py:7447
msgid "_Tabs"
msgstr ""
-#: .././config.py:7277
+#: .././config.py:7451
msgid "Tab preferences"
msgstr ""
-#: .././config.py:7283
+#: .././config.py:7457
msgid ""
"In the Videos Tab, show 'today' and 'yesterday' as the date, when possible"
msgstr ""
-#: .././config.py:7294
+#: .././config.py:7468
msgid "In the Progress Tab, hide finished videos / channels / playlists"
msgstr ""
-#: .././config.py:7303
+#: .././config.py:7477
msgid "In the Progress Tab, show results in reverse order"
msgstr ""
-#: .././config.py:7311
+#: .././config.py:7485
msgid "When Tartube starts, automatically open the Classic Mode tab"
msgstr ""
-#: .././config.py:7323
+#: .././config.py:7497
msgid "In the Errors/Warnings Tab, don't reset the tab text when it is clicked"
msgstr ""
-#: .././config.py:7341
+#: .././config.py:7515
msgid "_System tray"
msgstr ""
-#: .././config.py:7347
+#: .././config.py:7521
msgid "System tray preferences"
msgstr ""
-#: .././config.py:7352
+#: .././config.py:7526
msgid "Show icon in system tray"
msgstr ""
-#: .././config.py:7361
+#: .././config.py:7535
msgid "Close to the tray, rather than closing the application"
msgstr ""
-#: .././config.py:7387
+#: .././config.py:7561
msgid "_Dialogues"
msgstr ""
-#: .././config.py:7393
+#: .././config.py:7567
msgid "Dialogue window preferences"
msgstr ""
-#: .././config.py:7398
+#: .././config.py:7572
msgid "When adding channels/playlists, keep the dialogue window open"
msgstr ""
-#: .././config.py:7408
+#: .././config.py:7582
msgid "When the dialogue window opens, add URLs from the system clipboard"
msgstr ""
-#: .././config.py:7436
+#: .././config.py:7610
msgid "_Errors/Warnings"
msgstr ""
-#: .././config.py:7444
+#: .././config.py:7618
msgid "Errors/Warnings tab preferences"
msgstr ""
-#: .././config.py:7449
+#: .././config.py:7623
msgid "Show Tartube error messages"
msgstr ""
-#: .././config.py:7457
+#: .././config.py:7631
msgid "Show Tartube warning messages"
msgstr ""
-#: .././config.py:7465
+#: .././config.py:7639
msgid "Show server error messages"
msgstr ""
-#: .././config.py:7476
+#: .././config.py:7650
msgid "Show server warning messages"
msgstr ""
-#: .././config.py:7488
-msgid "youtube-dl error/warning preferences"
+#: .././config.py:7662
+msgid "Downloader error/warning preferences"
msgstr ""
-#: .././config.py:7493
-msgid ""
-"TRANSLATOR'S NOTE: These youtube-dl error messages are always in English"
+#: .././config.py:7667
+msgid "TRANSLATOR'S NOTE: These error messages are always in English"
msgstr ""
-#: .././config.py:7498
+#: .././config.py:7671
msgid "Ignore 'Child process exited with non-zero code' errors"
msgstr ""
-#: .././config.py:7507
+#: .././config.py:7680
msgid "Ignore 'Unable to download video data: HTTP Error 404' errors"
msgstr ""
-#: .././config.py:7516
+#: .././config.py:7689
msgid "Ignore 'Did not get any data blocks' errors"
msgstr ""
-#: .././config.py:7525
+#: .././config.py:7698
msgid "Ignore 'Requested formats are incompatible for merge' warnings"
msgstr ""
-#: .././config.py:7534
+#: .././config.py:7707
msgid "Ignore 'No video formats found' errors"
msgstr ""
-#: .././config.py:7542
+#: .././config.py:7715
msgid "Ignore 'There are no annotations to write' warnings"
msgstr ""
-#: .././config.py:7550
+#: .././config.py:7723
msgid "Ignore 'Video doesn't have subtitles' warnings"
msgstr ""
-#: .././config.py:7566
+#: .././config.py:7739
msgid "_Websites"
msgstr ""
-#: .././config.py:7574
+#: .././config.py:7747
msgid "YouTube error/warning preferences"
msgstr ""
-#: .././config.py:7579
+#: .././config.py:7752
msgid "Ignore YouTube copyright errors"
msgstr ""
-#: .././config.py:7587
+#: .././config.py:7760
msgid "Ignore YouTube age-restriction errors"
msgstr ""
-#: .././config.py:7595
+#: .././config.py:7768
msgid "Ignore YouTube deletion by uploader errors"
msgstr ""
-#: .././config.py:7604
+#: .././config.py:7777
msgid "General preferences"
msgstr ""
-#: .././config.py:7610
+#: .././config.py:7783
msgid ""
"Ignore any errors/warnings which match lines in this list (applies to all "
"websites)"
msgstr ""
-#: .././config.py:7623
+#: .././config.py:7796
msgid "These are ordinary strings"
msgstr ""
-#: .././config.py:7630
+#: .././config.py:7803
msgid "These are regular expressions (regexes)"
msgstr ""
#. Add this tab...
-#: .././config.py:7659
+#: .././config.py:7832
msgid "_Scheduling"
msgstr ""
-#: .././config.py:7676
+#: .././config.py:7849
msgid "_Start"
msgstr ""
-#: .././config.py:7682
+#: .././config.py:7855
msgid "Scheduled start preferences"
msgstr ""
-#: .././config.py:7687
-msgid "Automatic 'Download all' operations"
-msgstr ""
-
-#: .././config.py:7693 .././config.py:7754
-msgid "Disabled"
-msgstr ""
-
-#: .././config.py:7694 .././config.py:7755
-msgid "Performed when Tartube starts"
-msgstr ""
-
-#: .././config.py:7695 .././config.py:7756
-msgid "Performed at regular intervals"
-msgstr ""
-
-#: .././config.py:7715 .././config.py:7776
-msgid "Time (in hours) between operations"
-msgstr ""
-
-#: .././config.py:7748
+#: .././config.py:7861
msgid "Automatic 'Check all' operations"
msgstr ""
-#: .././config.py:7810
-msgid "After an automatic 'Download/Check all' operation, shut down Tartube"
+#: .././config.py:7867 .././config.py:7929 .././config.py:7991
+msgid "Disabled"
msgstr ""
-#: .././config.py:7849
+#: .././config.py:7868 .././config.py:7930 .././config.py:7992
+msgid "Performed when Tartube starts"
+msgstr ""
+
+#: .././config.py:7869 .././config.py:7931 .././config.py:7993
+msgid "Performed at regular intervals"
+msgstr ""
+
+#: .././config.py:7889 .././config.py:7951 .././config.py:8013
+msgid "Time (in hours) between operations"
+msgstr ""
+
+#: .././config.py:7923
+msgid "Automatic 'Download all' operations"
+msgstr ""
+
+#: .././config.py:7985
+msgid "Automatic custom 'Download all' operations"
+msgstr ""
+
+#: .././config.py:8047
+msgid "After an automatic operation, shut down Tartube"
+msgstr ""
+
+#: .././config.py:8102
msgid "S_top"
msgstr ""
-#: .././config.py:7855
+#: .././config.py:8108
msgid "Scheduled stop preferences"
msgstr ""
-#: .././config.py:7860
+#: .././config.py:8113
msgid "Stop all download operations after this much time"
msgstr ""
-#: .././config.py:7908
+#: .././config.py:8161
msgid "Stop all download operations after this many videos"
msgstr ""
-#: .././config.py:7935
+#: .././config.py:8188
msgid "Stop all download operations after this much disk space"
msgstr ""
-#: .././config.py:7978
+#: .././config.py:8231
msgid ""
"N.B. Disk space is estimated. This setting does not apply to simulated "
"downloads"
msgstr ""
-#: .././config.py:8023
+#: .././config.py:8276
msgid "Download operation preferences"
msgstr ""
-#: .././config.py:8029
-msgid "Automatically update youtube-dl before every download operation"
+#: .././config.py:8282
+msgid "Automatically update downloader before every download operation"
msgstr ""
-#: .././config.py:8041
-msgid ""
-"Automatically save files at the end of a download/update/refresh operation"
+#: .././config.py:8294
+msgid "Automatically save files at the end of all operations"
msgstr ""
-#: .././config.py:8052
+#: .././config.py:8304
msgid ""
"When applying download options to something, clone the general download "
"options"
msgstr ""
-#: .././config.py:8063
+#: .././config.py:8315
msgid "For simulated downloads, don't check a video in a folder more than once"
msgstr ""
-#: .././config.py:8080
+#: .././config.py:8326
+msgid "Invidious mirror"
+msgstr ""
+
+#: .././config.py:8332
+msgid "To find an updated list of Invidious mirrors, use any search engine!"
+msgstr ""
+
+#: .././config.py:8345 .././config.py:8486
+msgid "Type the exact text that replaces youtube.com e.g."
+msgstr ""
+
+#: .././config.py:8362
msgid "_Custom"
msgstr ""
-#: .././config.py:8085
+#: .././config.py:8367
msgid "Custom download preferences"
msgstr ""
-#: .././config.py:8091
+#: .././config.py:8373
msgid ""
"In custom downloads, download each video independently of its channel or "
"playlist"
msgstr ""
-#: .././config.py:8102
+#: .././config.py:8384
msgid ""
"In custom downloads, apply a delay after each video/channel/playlist is "
"download"
msgstr ""
-#: .././config.py:8112
+#: .././config.py:8394
msgid "Maximum delay to apply (in minutes)"
msgstr ""
-#: .././config.py:8129
+#: .././config.py:8411
msgid "Minimum delay to apply (in minutes; randomises the actual delay)"
msgstr "Minimum delay to apply (in minutes; randomizes the actual delay)"
-#: .././config.py:8152
+#: .././config.py:8434
msgid "In custom downloads, obtain a YouTube video from the original website"
msgstr ""
-#: .././config.py:8162
+#: .././config.py:8444
msgid "In custom downloads, obtain the video from HookTube rather than YouTube"
msgstr ""
-#: .././config.py:8174
+#: .././config.py:8456
msgid ""
"In custom downloads, obtain the video from Invidious rather than YouTube"
msgstr ""
-#: .././config.py:8186
+#: .././config.py:8468
msgid ""
"In custom downloads, obtain the video from the YouTube front-end specified "
"below"
msgstr ""
-#: .././config.py:8206
-msgid ""
-"Type the exact text that replaces youtube.com e.g. "
-"i>hooktube.com"
-msgstr ""
-
-#: .././config.py:8272
+#: .././config.py:8554
msgid "Livestream preferences (compatible websites only)"
msgstr ""
-#: .././config.py:8278
+#: .././config.py:8560
msgid "Detect livestreams announced within this many days"
msgstr ""
-#: .././config.py:8293
+#: .././config.py:8575
msgid "How often to check the status of livestreams (in minutes)"
msgstr ""
-#: .././config.py:8338
+#: .././config.py:8620
msgid "Video Catalogue options"
msgstr "Video Catalog options"
-#: .././config.py:8343
+#: .././config.py:8625
msgid "Show livestreams with a different background colour"
msgstr "Show livestreams with a different background color"
-#: .././config.py:8356
+#: .././config.py:8638
msgid "Livestream actions (can be toggled for individual videos)"
msgstr ""
-#: .././config.py:8363
+#: .././config.py:8645
msgid "(currently disabled on MS Windows)"
msgstr ""
-#: .././config.py:8368
+#: .././config.py:8650
msgid "When a livestream starts, show a desktop notification"
msgstr ""
-#: .././config.py:8382
+#: .././config.py:8664
msgid "When a livestream starts, sound an alarm"
msgstr ""
-#: .././config.py:8405
+#: .././config.py:8687
msgid "Plays the selected sound effect"
msgstr ""
-#: .././config.py:8412
+#: .././config.py:8694
msgid "When a livestream starts, open it in the system's web browser"
msgstr ""
-#: .././config.py:8424
+#: .././config.py:8706
msgid "When a livestream starts, begin downloading it immediately"
msgstr ""
-#: .././config.py:8457
+#: .././config.py:8739
msgid "_Notifications"
msgstr ""
-#: .././config.py:8463
+#: .././config.py:8745
msgid "Desktop notification preferences"
msgstr ""
-#: .././config.py:8470
-msgid ""
-"Show a dialogue window at the end of a download/update/refresh/info/tidy "
-"operation"
+#: .././config.py:8752
+msgid "Show a dialogue window at the end of an operation"
msgstr ""
-#: .././config.py:8480
-msgid ""
-"Show a desktop notification at the end of a download/update/refresh/info/"
-"tidy operation"
+#: .././config.py:8777
+msgid "Don't notify the user at the end of an operation"
msgstr ""
-#: .././config.py:8494
-msgid ""
-"Don't notify the user at the end of a download/update/refresh/info/tidy "
-"operation"
-msgstr ""
-
-#: .././config.py:8529
+#: .././config.py:8811
msgid "_URL flexibility"
msgstr ""
-#: .././config.py:8535
+#: .././config.py:8817
msgid "URL flexibility preferences"
msgstr ""
-#: .././config.py:8542
+#: .././config.py:8824
msgid ""
"If a video's URL represents a channel/playlist, not a video, don't download "
"it"
msgstr ""
-#: .././config.py:8551
+#: .././config.py:8833
msgid "...or, download multiple videos into the containing folder"
msgstr ""
-#: .././config.py:8561
+#: .././config.py:8843
msgid "...or, create a new channel, and download the videos into that"
msgstr ""
-#: .././config.py:8572
+#: .././config.py:8854
msgid "...or, create a new playlist, and download the videos into that"
msgstr ""
-#: .././config.py:8611
+#: .././config.py:8893
msgid "_Performance"
msgstr ""
-#: .././config.py:8619
+#: .././config.py:8901
msgid "Performance limits"
msgstr ""
-#: .././config.py:8624
+#: .././config.py:8906
msgid "Limit simultaneous downloads to"
msgstr ""
-#: .././config.py:8642
+#: .././config.py:8924
msgid "Limit download speed to"
msgstr ""
-#: .././config.py:8668
+#: .././config.py:8950
msgid "Overriding video format options, limit video resolution to"
msgstr ""
-#: .././config.py:8690
+#: .././config.py:8972
msgid "Time-saving preferences"
msgstr ""
-#: .././config.py:8696
+#: .././config.py:8978
msgid ""
"Stop checking/downloading a channel/playlist when it starts sending videos "
"we already have"
msgstr ""
-#: .././config.py:8707
+#: .././config.py:8989
msgid "Stop after this many videos (when checking)"
msgstr ""
-#: .././config.py:8722
+#: .././config.py:9004
msgid "Stop after this many videos (when downloading)"
msgstr ""
-#: .././config.py:8771
+#: .././config.py:9054
msgid "_File paths"
msgstr ""
-#: .././config.py:8778
+#: .././config.py:9061
msgid "youtube-dl file paths"
msgstr ""
-#: .././config.py:8784
-msgid "youtube-dl executable (system-dependent)"
+#: .././config.py:9067
+msgid "Path to youtube-dl executable"
msgstr ""
-#: .././config.py:8797
-msgid "Default path to youtube-dl executable"
-msgstr ""
-
-#: .././config.py:8810
-msgid "Actual path to use"
-msgstr ""
-
-#: .././config.py:8816
+#. (signal_connect appears below)
+#: .././config.py:9073 .././config.py:9335 .././config.py:9376
+#: .././config.py:13316
msgid "Use default path"
msgstr ""
-#: .././config.py:8821
+#: .././config.py:9078 .././config.py:13328
msgid "Use local path"
msgstr ""
-#: .././config.py:8829
+#: .././config.py:9086 .././config.py:13340
msgid "Use PyPI path"
msgstr ""
-#: .././config.py:8856
-msgid "Shell command for update operations"
+#: .././config.py:9111
+msgid "Command for update operations"
msgstr ""
-#: .././config.py:8890
+#: .././config.py:9146
+msgid "youtube-dl forks"
+msgstr ""
+
+#: .././config.py:9151
+msgid "Use this fork of youtube-dl"
+msgstr ""
+
+#: .././config.py:9166
+msgid ""
+"If you specify a fork (e.g. youtube-dlc), it must be very similar to the "
+"original youtube-dl\n"
+"To use the original youtube-dl, leave the box empty"
+msgstr ""
+
+#: .././config.py:9182
msgid "_Preferences"
msgstr ""
-#: .././config.py:8897
-msgid "Post-processing preferences"
-msgstr ""
-
-#: .././config.py:8902
-msgid "Path to the ffmpeg/avconv binary"
-msgstr ""
-
-#: .././config.py:8925
-msgid "Install from main menu"
-msgstr ""
-
-#: .././config.py:8935
+#: .././config.py:9189
msgid "Missing video preferences"
msgstr ""
-#: .././config.py:8941
+#: .././config.py:9195
msgid ""
"Add videos which have been removed from a channel/playlist to the Missing "
"Videos folder"
msgstr ""
-#: .././config.py:8952
+#: .././config.py:9206
msgid "Only add videos that were uploaded within this many days"
msgstr ""
-#: .././config.py:8993
+#: .././config.py:9247
msgid "Other preferences"
msgstr ""
-#: .././config.py:8999
+#: .././config.py:9253
msgid ""
-"Allow youtube-dl to create its own archive file (so deleted videos are not "
+"Allow downloader to create its own archive file (so deleted videos are not "
"re-downloaded)"
msgstr ""
-#: .././config.py:9010
+#: .././config.py:9264
msgid ""
"Also create an archive file when downloading from the Classic Mode tab (not "
"recommended)"
msgstr ""
-#: .././config.py:9021
+#: .././config.py:9275
msgid "When checking videos, apply a 60-second timeout"
msgstr ""
+#: .././config.py:9285
+msgid ""
+"Convert .webp thumbnails into .jpg thumbnails (using FFmpeg) after "
+"downloading them"
+msgstr ""
+
+#: .././config.py:9303
+msgid "_FFmpeg / AVConv"
+msgstr ""
+
+#: .././config.py:9311
+msgid "Post-processing preferences"
+msgstr ""
+
+#: .././config.py:9316
+msgid ""
+"You only need to set these paths if Tartube cannot find FFmpeg / AVConv "
+"automatically"
+msgstr ""
+
+#: .././config.py:9323
+msgid "Path to the FFmpeg executable"
+msgstr ""
+
+#: .././config.py:9350
+msgid "Install from main menu"
+msgstr ""
+
+#: .././config.py:9364
+msgid "Path to the AVConv executable"
+msgstr ""
+
+#: .././config.py:9391
+msgid "Not supported on MS Windows"
+msgstr ""
+
#. Add this tab...
-#: .././config.py:9038
+#: .././config.py:9414
msgid "Out_put"
msgstr ""
-#: .././config.py:9057
+#: .././config.py:9433
msgid "_Output Tab"
msgstr ""
-#: .././config.py:9063
+#: .././config.py:9439
msgid "Output Tab preferences"
msgstr ""
-#: .././config.py:9068
-msgid "Display youtube-dl system commands in the Output Tab"
+#: .././config.py:9444
+msgid "Display downloader system commands in the Output Tab"
msgstr ""
-#: .././config.py:9077
-msgid "Display output from youtube-dl's STDOUT in the Output Tab"
+#: .././config.py:9453
+msgid "Display output from downloader's STDOUT in the Output Tab"
msgstr ""
-#: .././config.py:9086 .././config.py:9216
+#: .././config.py:9462 .././config.py:9603
msgid "...but don't write each video's JSON data"
msgstr ""
-#: .././config.py:9097 .././config.py:9227
+#: .././config.py:9473 .././config.py:9614
msgid "...but don't write each video's download progress"
msgstr ""
-#: .././config.py:9116
-msgid "Display output from youtube-dl's STDERR in the Output Tab"
+#: .././config.py:9492
+msgid "Display output from downloader's STDERR in the Output Tab"
msgstr ""
-#: .././config.py:9125
+#: .././config.py:9501
msgid "Empty pages in the Output Tab at the start of every operation"
msgstr ""
-#: .././config.py:9135
+#: .././config.py:9511
msgid ""
"Show a summary of active threads (changes are applied when Tartube restarts)"
msgstr ""
-#: .././config.py:9147
+#: .././config.py:9523
+msgid "During an update operation, automatically switch to the Output tab"
+msgstr ""
+
+#: .././config.py:9534
msgid "During a refresh operation, show all matching videos in the Output Tab"
msgstr ""
-#: .././config.py:9158
+#: .././config.py:9545
msgid "...also show all non-matching videos"
msgstr ""
-#: .././config.py:9187
+#: .././config.py:9574
msgid "_Terminal window"
msgstr ""
-#: .././config.py:9193
+#: .././config.py:9580
msgid "Terminal window preferences"
msgstr ""
-#: .././config.py:9198
-msgid "Write youtube-dl system commands to the terminal window"
+#: .././config.py:9585
+msgid "Write downloader system commands to the terminal window"
msgstr ""
-#: .././config.py:9207
-msgid "Write output from youtube-dl's STDOUT to the terminal window"
+#: .././config.py:9594
+msgid "Write output from downloader's STDOUT to the terminal window"
msgstr ""
-#: .././config.py:9249
-msgid "Write output from youtube-dl's STDERR to the terminal window"
+#: .././config.py:9636
+msgid "Write output from downloader's STDERR to the terminal window"
msgstr ""
-#: .././config.py:9268
+#: .././config.py:9655
msgid "_Both"
msgstr ""
-#: .././config.py:9273
+#: .././config.py:9660
msgid ""
"Special preferences (applies to both the Output Tab and the terminal window)"
msgstr ""
-#: .././config.py:9280
-msgid "Write verbose output (youtube-dl debugging mode)"
+#: .././config.py:9667
+msgid "Write verbose output (downloader debugging mode)"
msgstr ""
-#: .././config.py:10105
+#: .././config.py:10574
msgid "Are you sure you want to create a new database at this location?"
msgstr ""
-#: .././config.py:10212
+#: .././config.py:10681
msgid "Are you sure you want to forget this database?"
msgstr ""
-#: .././config.py:10247
+#: .././config.py:10716
msgid "Are you sure you want to forget all databases except the current one?"
msgstr ""
-#: .././config.py:10451
+#: .././config.py:10920
msgid "No database exists at this location:"
msgstr ""
-#: .././config.py:10453
+#: .././config.py:10922
msgid "Do you want to create a new one?"
msgstr ""
-#: .././config.py:10907 .././config.py:11197 .././config.py:12011
+#: .././config.py:11432 .././config.py:11737 .././config.py:12606
msgid "The new setting will be applied when Tartube restarts"
msgstr ""
-#: .././config.py:11950
+#: .././config.py:12508
+msgid "Please select the AVConv executable"
+msgstr ""
+
+#: .././config.py:12545
msgid "Please select the FFmpeg executable"
msgstr ""
-#: .././config.py:12561
+#: .././config.py:13235
msgid "Database file not loaded"
msgstr ""
-#: .././config.py:12596
+#: .././config.py:13255
+msgid "Did not try to load the database file"
+msgstr ""
+
+#: .././config.py:13280
msgid "Database file loaded"
msgstr ""
@@ -5131,17 +5344,17 @@ msgstr ""
msgid "Download did not start"
msgstr ""
-#: .././downloads.py:2420 .././info.py:352 .././updates.py:293
-#: .././updates.py:451
+#: .././downloads.py:2420 .././info.py:342 .././updates.py:277
+#: .././updates.py:445
msgid "Child process exited with non-zero code: {}"
msgstr ""
-#: .././downloads.py:2534 .././downloads.py:3331
+#: .././downloads.py:2534 .././downloads.py:3448
msgid ""
"This video has a URL that points to a channel or a playlist, not a video"
msgstr ""
-#: .././downloads.py:3223
+#: .././downloads.py:3340
msgid "Simulated download of:"
msgstr ""
@@ -5170,476 +5383,494 @@ msgid "years"
msgstr ""
#. System folder names
-#: .././formats.py:777
+#: .././formats.py:779
msgid "All Videos"
msgstr ""
-#: .././formats.py:778
+#: .././formats.py:780
msgid "Bookmarks"
msgstr ""
-#: .././formats.py:779
+#: .././formats.py:781
msgid "Favourite Videos"
msgstr "Favorite Videos"
-#: .././formats.py:780
+#: .././formats.py:782
msgid "Livestreams"
msgstr ""
-#: .././formats.py:781
+#: .././formats.py:783
msgid "Missing Videos"
msgstr ""
-#: .././formats.py:782
+#: .././formats.py:784
msgid "New Videos"
msgstr ""
-#: .././formats.py:783
+#: .././formats.py:785
msgid "Waiting Videos"
msgstr ""
-#: .././formats.py:784
+#: .././formats.py:786
msgid "Temporary Videos"
msgstr ""
-#: .././formats.py:785
+#: .././formats.py:787
msgid "Unsorted Videos"
msgstr ""
-#: .././formats.py:790
+#: .././formats.py:792
msgid "Update using default youtube-dl path"
msgstr ""
-#: .././formats.py:792
+#: .././formats.py:794
msgid "Update using local youtube-dl path"
msgstr ""
-#: .././formats.py:794
+#: .././formats.py:796
msgid "Update using pip"
msgstr ""
-#: .././formats.py:796
+#: .././formats.py:798
msgid "Update using pip (omit --user option)"
msgstr ""
-#: .././formats.py:798
+#: .././formats.py:800
msgid "Update using pip3"
msgstr ""
-#: .././formats.py:800
+#: .././formats.py:802
msgid "Update using pip3 (omit --user option)"
msgstr ""
-#: .././formats.py:802
+#: .././formats.py:804
msgid "Update using pip3 (recommended)"
msgstr ""
-#: .././formats.py:804
+#: .././formats.py:806
msgid "Update using PyPI youtube-dl path"
msgstr ""
-#: .././formats.py:806
+#: .././formats.py:808
msgid "Windows 32-bit update (recommended)"
msgstr ""
-#: .././formats.py:808
+#: .././formats.py:810
msgid "Windows 64-bit update (recommended)"
msgstr ""
-#: .././formats.py:810
+#: .././formats.py:812
msgid "youtube-dl updates are disabled"
msgstr ""
#. Download operation stages
-#: .././formats.py:814
+#: .././formats.py:816
msgid "Queued"
msgstr ""
-#: .././formats.py:815
+#: .././formats.py:817
msgid "Active"
msgstr ""
-#: .././formats.py:816
+#: .././formats.py:818
msgid "Paused"
msgstr ""
#. (not actually used)
-#: .././formats.py:817
+#: .././formats.py:819
msgid "Completed"
msgstr ""
#. (not actually used)
#. Sub-stages of the 'Error' stage
-#: .././formats.py:818 .././formats.py:829
+#: .././formats.py:820 .././formats.py:831
msgid "Error"
msgstr ""
#. Sub-stages of the 'Active' stage
-#: .././formats.py:820
+#: .././formats.py:822
msgid "Pre-processing"
msgstr ""
-#: .././formats.py:821
+#: .././formats.py:823
msgid "Downloading"
msgstr ""
-#: .././formats.py:822
+#: .././formats.py:824
msgid "Post-processing"
msgstr ""
-#: .././formats.py:823
+#: .././formats.py:825
msgid "Checking"
msgstr ""
#. Sub-stages of the 'Completed' stage
-#: .././formats.py:825
+#: .././formats.py:827
msgid "Finished"
msgstr ""
-#: .././formats.py:826
+#: .././formats.py:828
msgid "Warning"
msgstr ""
-#: .././formats.py:827
+#: .././formats.py:829
msgid "Already downloaded"
msgstr ""
#. (not actually used)
-#: .././formats.py:830
+#: .././formats.py:832
msgid "Stopped"
msgstr ""
-#: .././formats.py:831
+#: .././formats.py:833
msgid "Filesize abort"
msgstr ""
-#: .././formats.py:841
+#: .././formats.py:843
msgid ""
"TRANSLATOR'S NOTE: ID refers to a video's unique ID on the website, e.g. on "
"YouTube \"CS9OO0S5w2k\""
msgstr ""
-#: .././formats.py:849
+#: .././formats.py:851
msgid "Custom"
msgstr ""
-#: .././formats.py:850
+#: .././formats.py:852
msgid "ID"
msgstr ""
-#: .././formats.py:851
+#: .././formats.py:853
msgid "Title"
msgstr ""
-#: .././formats.py:852
+#: .././formats.py:854
msgid "Quality"
msgstr ""
-#: .././formats.py:853
+#: .././formats.py:855
msgid "Autonumber"
msgstr ""
-#: .././formats.py:865
+#: .././formats.py:867
msgid "Any format"
msgstr ""
-#: .././info.py:186
-msgid "Starting info operation, testing youtube-dl with specified options"
+#: .././info.py:176
+msgid "Starting info operation, testing downloader with specified options"
msgstr ""
-#: .././info.py:195
+#: .././info.py:185
#, python-brace-format
msgid "Starting info operation, fetching list of video/audio formats for '{0}'"
msgstr ""
-#: .././info.py:202
+#: .././info.py:192
#, python-brace-format
msgid "Starting info operation, fetching list of subtitles for '{0}'"
msgstr ""
-#: .././info.py:343
-msgid "youtube-dl process did not start"
+#: .././info.py:333
+msgid "System process did not start"
msgstr ""
-#: .././info.py:368
+#: .././info.py:358
msgid "Info operation finished"
msgstr ""
#. (The code in self.run() will spot that the child process did not
#. start)
-#: .././info.py:421 .././updates.py:193
+#: .././info.py:408 .././updates.py:180
msgid "Child process did not start"
msgstr ""
-#: .././media.py:314
+#: .././media.py:315
msgid "TRANSLATOR'S NOTE: Source = video/channel/playlist URL"
msgstr ""
#. When the download operation is launched from the Classic Mode
#. tab, there is less to display
-#: .././media.py:317 .././media.py:1544 .././media.py:1560
+#: .././media.py:318 .././media.py:1550 .././media.py:1566
msgid "Source:"
msgstr ""
-#: .././media.py:325
+#: .././media.py:326
msgid "Location:"
msgstr ""
-#: .././media.py:336
+#: .././media.py:337
msgid "Download destination:"
msgstr ""
-#: .././media.py:1515
+#: .././media.py:1521
msgid ""
"TRANSLATOR'S NOTE: WAITING = livestream not started, LIVE = livestream "
"started"
msgstr ""
-#: .././media.py:1520
+#: .././media.py:1526
msgid "WAITING"
msgstr ""
-#: .././media.py:1522
+#: .././media.py:1528
msgid "LIVE"
msgstr ""
-#: .././media.py:1532 .././refresh.py:272 .././refresh.py:540
+#: .././media.py:1538 .././refresh.py:259 .././refresh.py:528
msgid "Channel:"
msgstr ""
-#: .././media.py:1534 .././refresh.py:274 .././refresh.py:542
+#: .././media.py:1540 .././refresh.py:261 .././refresh.py:530
msgid "Playlist:"
msgstr ""
-#: .././media.py:1536 .././refresh.py:276 .././refresh.py:544
+#: .././media.py:1542 .././refresh.py:263 .././refresh.py:532
msgid "Folder:"
msgstr ""
-#: .././media.py:1541
+#: .././media.py:1547
msgid "TRANSLATOR'S NOTE 2: Source = video/channel/playlist URL"
msgstr ""
-#: .././media.py:1550 .././media.py:1567
+#: .././media.py:1556 .././media.py:1573
msgid "File:"
msgstr ""
-#: .././media.py:2042
+#: .././media.py:2240
msgid "Today"
msgstr ""
-#: .././media.py:2044
+#: .././media.py:2242
msgid "Yesterday"
msgstr ""
-#: .././refresh.py:149
+#: .././refresh.py:139
msgid "Starting refresh operation, analysing whole database"
msgstr "Starting refresh operation, analyzing whole database"
-#: .././refresh.py:158
+#: .././refresh.py:148
msgid "Starting refresh operation, analysing '{}'"
msgstr "Starting refresh operation, analyzing '{}'"
-#: .././refresh.py:202
+#: .././refresh.py:192
msgid "Refresh operation finished"
msgstr ""
-#: .././refresh.py:207
+#: .././refresh.py:197
msgid "Number of video files analysed:"
msgstr "Number of video files analyzed:"
-#: .././refresh.py:213
+#: .././refresh.py:203
msgid "Video files already in the database:"
msgstr ""
-#: .././refresh.py:219
+#: .././refresh.py:209
msgid "New videos found and added to the database:"
msgstr ""
-#: .././refresh.py:385 .././tidy.py:518
+#: .././refresh.py:376 .././tidy.py:556
msgid "Checking:"
msgstr ""
-#: .././refresh.py:419 .././refresh.py:592
+#: .././refresh.py:410 .././refresh.py:584
msgid "Match:"
msgstr ""
-#: .././refresh.py:437
+#: .././refresh.py:428
msgid "Non-match:"
msgstr ""
-#: .././refresh.py:485
+#: .././refresh.py:476
msgid "New video:"
msgstr ""
-#: .././refresh.py:491 .././refresh.py:598
+#: .././refresh.py:482 .././refresh.py:590
msgid "Total videos:"
msgstr ""
-#: .././refresh.py:492 .././refresh.py:599
+#: .././refresh.py:483 .././refresh.py:591
msgid "matched:"
msgstr ""
-#: .././refresh.py:493
+#: .././refresh.py:484
msgid "new:"
msgstr ""
-#: .././refresh.py:574
+#: .././refresh.py:566
msgid "Missing:"
msgstr ""
-#: .././refresh.py:600
+#: .././refresh.py:592
msgid "missing:"
msgstr ""
-#: .././tidy.py:226
+#: .././tidy.py:230
msgid "Starting tidy operation, tidying up whole data directory"
msgstr ""
-#: .././tidy.py:235
+#: .././tidy.py:239
#, python-brace-format
msgid "Starting tidy operation, tidying up '{0}'"
msgstr ""
-#: .././tidy.py:241 .././tidy.py:253 .././tidy.py:263 .././tidy.py:273
-#: .././tidy.py:285 .././tidy.py:295 .././tidy.py:305 .././tidy.py:315
-#: .././tidy.py:325 .././tidy.py:335 .././tidy.py:345
+#: .././tidy.py:245 .././tidy.py:257 .././tidy.py:267 .././tidy.py:277
+#: .././tidy.py:289 .././tidy.py:299 .././tidy.py:309 .././tidy.py:319
+#: .././tidy.py:329 .././tidy.py:339 .././tidy.py:350 .././tidy.py:360
+#: .././tidy.py:370
msgid "YES"
msgstr ""
-#: .././tidy.py:243 .././tidy.py:255 .././tidy.py:265 .././tidy.py:275
-#: .././tidy.py:287 .././tidy.py:297 .././tidy.py:307 .././tidy.py:317
-#: .././tidy.py:327 .././tidy.py:337 .././tidy.py:347
+#: .././tidy.py:247 .././tidy.py:259 .././tidy.py:269 .././tidy.py:279
+#: .././tidy.py:291 .././tidy.py:301 .././tidy.py:311 .././tidy.py:321
+#: .././tidy.py:331 .././tidy.py:341 .././tidy.py:352 .././tidy.py:362
+#: .././tidy.py:372
msgid "NO"
msgstr ""
-#: .././tidy.py:247
+#: .././tidy.py:251
msgid "Check videos are not corrupted:"
msgstr ""
-#: .././tidy.py:259
+#: .././tidy.py:263
msgid "Delete corrupted videos:"
msgstr ""
-#: .././tidy.py:269
+#: .././tidy.py:273
msgid "Check videos do/don't exist:"
msgstr ""
-#: .././tidy.py:279
+#: .././tidy.py:283
msgid "Delete all video files:"
msgstr ""
-#: .././tidy.py:291
+#: .././tidy.py:295
msgid "Delete other video/audio files:"
msgstr ""
-#: .././tidy.py:301
-msgid "Delete all description files:"
+#: .././tidy.py:305
+msgid "Delete downloader archive files:"
msgstr ""
-#: .././tidy.py:311
-msgid "Delete all metadata (JSON) files:"
+#: .././tidy.py:315
+msgid "Move thumbnails into own folder:"
msgstr ""
-#: .././tidy.py:321
-msgid "Delete all annotation files:"
-msgstr ""
-
-#: .././tidy.py:331
+#: .././tidy.py:325
msgid "Delete all thumbnail files:"
msgstr ""
-#: .././tidy.py:341
-msgid "Delete .webp/malformed .jpg files:"
+#: .././tidy.py:335
+msgid "Convert .webp thumbnails to .jpg:"
msgstr ""
-#: .././tidy.py:351
-msgid "Delete youtube-dl archive files:"
+#: .././tidy.py:345
+msgid "Move other metadata files into own folder:"
msgstr ""
-#: .././tidy.py:387
+#: .././tidy.py:356
+msgid "Delete all description files:"
+msgstr ""
+
+#: .././tidy.py:366
+msgid "Delete all metadata (JSON) files:"
+msgstr ""
+
+#: .././tidy.py:376
+msgid "Delete all annotation files:"
+msgstr ""
+
+#: .././tidy.py:412
msgid "Tidy operation finished"
msgstr ""
-#: .././tidy.py:394
+#: .././tidy.py:419
msgid "Corrupted videos found:"
msgstr ""
-#: .././tidy.py:400
+#: .././tidy.py:425
msgid "Corrupted videos deleted:"
msgstr ""
-#: .././tidy.py:408
+#: .././tidy.py:433
msgid "New video files detected:"
msgstr ""
-#: .././tidy.py:414
+#: .././tidy.py:439
msgid "Missing video files detected:"
msgstr ""
-#: .././tidy.py:422
+#: .././tidy.py:447
msgid "Non-corrupted video files deleted:"
msgstr ""
-#: .././tidy.py:428
+#: .././tidy.py:453
msgid "Other video/audio files deleted:"
msgstr ""
-#: .././tidy.py:436
-msgid "Description files deleted:"
+#: .././tidy.py:461
+msgid "Downloader archive files deleted:"
msgstr ""
-#: .././tidy.py:444
-msgid "Metadata (JSON) files deleted:"
+#: .././tidy.py:469
+msgid "Thumbnail files moved:"
msgstr ""
-#: .././tidy.py:452
-msgid "Annotation files deleted:"
-msgstr ""
-
-#: .././tidy.py:460
+#: .././tidy.py:477
msgid "Thumbnail files deleted:"
msgstr ""
-#: .././tidy.py:468
-msgid ".webp/malformed .jpg files deleted:"
+#: .././tidy.py:485
+msgid ".webp thumbnails converted to .jpg:"
msgstr ""
-#: .././tidy.py:476
-msgid "youtube-dl archive files deleted:"
+#: .././tidy.py:493
+msgid "Other metadata files moved:"
msgstr ""
-#: .././tidy.py:606
+#: .././tidy.py:501
+msgid "Description files deleted:"
+msgstr ""
+
+#: .././tidy.py:509
+msgid "Metadata (JSON) files deleted:"
+msgstr ""
+
+#: .././tidy.py:517
+msgid "Annotation files deleted:"
+msgstr ""
+
+#: .././tidy.py:647
msgid "Deleted (possibly) corrupted video file:"
msgstr ""
-#: .././tidy.py:621 .././tidy.py:1073
+#: .././tidy.py:662 .././tidy.py:1273
msgid "Video file might be corrupt:"
msgstr ""
-#: .././tidy.py:665
+#: .././tidy.py:703
msgid "Video file exists:"
msgstr ""
-#: .././tidy.py:683
+#: .././tidy.py:721
msgid "Video file doesn't exist:"
msgstr ""
-#: .././updates.py:215
+#: .././updates.py:199
msgid "Starting update operation, installing FFmpeg"
msgstr ""
-#: .././updates.py:289
+#: .././updates.py:273
msgid "FFmpeg installation did not start"
msgstr ""
-#: .././updates.py:306 .././updates.py:467
+#: .././updates.py:290 .././updates.py:461
msgid "Update operation finished"
msgstr ""
-#: .././updates.py:335
-msgid "Starting update operation, installing/updating youtube-dl"
+#: .././updates.py:317
+msgid "Starting update operation, installing/updating "
msgstr ""
-#: .././updates.py:442
-msgid "youtube-dl update did not start"
+#: .././updates.py:436
+msgid "Update did not start"
msgstr ""
diff --git a/nsis/tartube_install_32bit.nsi b/nsis/tartube_install_32bit.nsi
index 371ebfa..e7614de 100644
--- a/nsis/tartube_install_32bit.nsi
+++ b/nsis/tartube_install_32bit.nsi
@@ -1,4 +1,4 @@
-# Tartube v2.1.080 installer script for MS Windows
+# Tartube v2.2.0 installer script for MS Windows
#
# Copyright (C) 2019-2020 A S Lewis
#
@@ -244,7 +244,7 @@
;Name and file
Name "Tartube"
- OutFile "install-tartube-2.1.080-32bit.exe"
+ OutFile "install-tartube-2.2.0-32bit.exe"
;Default installation folder
InstallDir "$LOCALAPPDATA\Tartube"
@@ -347,7 +347,7 @@ Section "Tartube" SecClient
# "Publisher" "A S Lewis"
# WriteRegStr HKLM \
# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
-# "DisplayVersion" "2.1.080"
+# "DisplayVersion" "2.2.0"
# Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
diff --git a/nsis/tartube_install_64bit.nsi b/nsis/tartube_install_64bit.nsi
index 7559c9b..998aa51 100644
--- a/nsis/tartube_install_64bit.nsi
+++ b/nsis/tartube_install_64bit.nsi
@@ -1,4 +1,4 @@
-# Tartube v2.1.080 installer script for MS Windows
+# Tartube v2.2.0 installer script for MS Windows
#
# Copyright (C) 2019-2020 A S Lewis
#
@@ -244,7 +244,7 @@
;Name and file
Name "Tartube"
- OutFile "install-tartube-2.1.080-64bit.exe"
+ OutFile "install-tartube-2.2.0-64bit.exe"
;Default installation folder
InstallDir "$LOCALAPPDATA\Tartube"
@@ -347,7 +347,7 @@ Section "Tartube" SecClient
# "Publisher" "A S Lewis"
# WriteRegStr HKLM \
# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \
-# "DisplayVersion" "2.1.080"
+# "DisplayVersion" "2.2.0"
# Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
diff --git a/pack/bin/tartube b/pack/bin/tartube
index b911bb7..7302ba8 100644
--- a/pack/bin/tartube
+++ b/pack/bin/tartube
@@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
-__version__ = '2.1.080'
-__date__ = '13 Aug 2020'
+__version__ = '2.2.0'
+__date__ = '30 Sep 2020'
__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis'
__license__ = """
Copyright \xa9 2019-2020 A S Lewis.
diff --git a/pack/bin_strict/tartube b/pack/bin_strict/tartube
index 6feedad..25b74c8 100644
--- a/pack/bin_strict/tartube
+++ b/pack/bin_strict/tartube
@@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
-__version__ = '2.1.080'
-__date__ = '13 Aug 2020'
+__version__ = '2.2.0'
+__date__ = '30 Sep 2020'
__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis'
__license__ = """
Copyright \xa9 2019-2020 A S Lewis.
diff --git a/pack/tartube.1 b/pack/tartube.1
index 89242b8..e02ff3a 100644
--- a/pack/tartube.1
+++ b/pack/tartube.1
@@ -1,4 +1,4 @@
-.TH man 1 "13 Aug 2020" "2.1.080" "tartube man page"
+.TH man 1 "30 Sep 2020" "2.2.0" "tartube man page"
.SH NAME
tartube \- GUI front-end for youtube-dl
.SH SYNOPSIS
diff --git a/pack/tartube.desktop b/pack/tartube.desktop
index 4137ed6..1c76c3b 100644
--- a/pack/tartube.desktop
+++ b/pack/tartube.desktop
@@ -1,6 +1,6 @@
[Desktop Entry]
Name=Tartube
-Version=2.1.080
+Version=2.2.0
Exec=tartube
Icon=tartube
Type=Application
diff --git a/screenshots/example11.png b/screenshots/example11.png
index e66d9af..142d4da 100644
Binary files a/screenshots/example11.png and b/screenshots/example11.png differ
diff --git a/screenshots/example12.png b/screenshots/example12.png
index 2b20892..f2bda39 100644
Binary files a/screenshots/example12.png and b/screenshots/example12.png differ
diff --git a/screenshots/example13.png b/screenshots/example13.png
index ec2a57e..8f7c8d7 100644
Binary files a/screenshots/example13.png and b/screenshots/example13.png differ
diff --git a/screenshots/example14.png b/screenshots/example14.png
index 41d171e..8f6c194 100644
Binary files a/screenshots/example14.png and b/screenshots/example14.png differ
diff --git a/screenshots/example15.png b/screenshots/example15.png
index d4c35af..c6af82a 100644
Binary files a/screenshots/example15.png and b/screenshots/example15.png differ
diff --git a/screenshots/example16.png b/screenshots/example16.png
index 48a8f2a..4967bc4 100644
Binary files a/screenshots/example16.png and b/screenshots/example16.png differ
diff --git a/screenshots/example17.png b/screenshots/example17.png
index 48cd30c..073f2ae 100644
Binary files a/screenshots/example17.png and b/screenshots/example17.png differ
diff --git a/screenshots/example20.png b/screenshots/example20.png
index 1874bd1..c57d1dc 100644
Binary files a/screenshots/example20.png and b/screenshots/example20.png differ
diff --git a/screenshots/example21.png b/screenshots/example21.png
index 978f377..02ce4de 100644
Binary files a/screenshots/example21.png and b/screenshots/example21.png differ
diff --git a/screenshots/example22.png b/screenshots/example22.png
index 7a23868..5459c7a 100644
Binary files a/screenshots/example22.png and b/screenshots/example22.png differ
diff --git a/screenshots/example24.png b/screenshots/example24.png
index 1fd39b8..247f3f6 100644
Binary files a/screenshots/example24.png and b/screenshots/example24.png differ
diff --git a/screenshots/example25.png b/screenshots/example25.png
new file mode 100644
index 0000000..1fd39b8
Binary files /dev/null and b/screenshots/example25.png differ
diff --git a/screenshots/example3.png b/screenshots/example3.png
index 18f13d7..9562724 100644
Binary files a/screenshots/example3.png and b/screenshots/example3.png differ
diff --git a/screenshots/example4.png b/screenshots/example4.png
index 068fb1a..cd76f5f 100644
Binary files a/screenshots/example4.png and b/screenshots/example4.png differ
diff --git a/screenshots/example5.png b/screenshots/example5.png
index e4f7b50..d68f699 100644
Binary files a/screenshots/example5.png and b/screenshots/example5.png differ
diff --git a/screenshots/example6.png b/screenshots/example6.png
index 0f854f7..7ae88c6 100644
Binary files a/screenshots/example6.png and b/screenshots/example6.png differ
diff --git a/screenshots/example7.png b/screenshots/example7.png
index 23b41ae..0934a79 100644
Binary files a/screenshots/example7.png and b/screenshots/example7.png differ
diff --git a/screenshots/example8.png b/screenshots/example8.png
index 5e2fb54..649030e 100644
Binary files a/screenshots/example8.png and b/screenshots/example8.png differ
diff --git a/screenshots/tartube.png b/screenshots/tartube.png
index 252905d..f077a43 100644
Binary files a/screenshots/tartube.png and b/screenshots/tartube.png differ
diff --git a/setup.py b/setup.py
index 80bbea5..7abd011 100644
--- a/setup.py
+++ b/setup.py
@@ -44,6 +44,8 @@ unsubscribe people from their favourite channels and/or deliberately
conceal videos that they don't like. Tartube won't do any of those things
- Tartube can, in some circumstances, see videos that are region-blocked
and/or age-restricted
+
+Note for PyPI users: Tartube should be installed with: pip3 install tartube
"""
alt_description = """
@@ -145,7 +147,7 @@ for path in glob.glob('sounds/*'):
# Setup
setuptools.setup(
name='tartube',
- version='2.1.080',
+ version='2.2.0',
description='GUI front-end for youtube-dl',
long_description=long_description,
long_description_content_type='text/plain',
@@ -171,7 +173,8 @@ setuptools.setup(
exclude=('docs', 'icons', 'nsis', 'tests'),
),
include_package_data=True,
- python_requires='>=3.0, <4',
+# python_requires='>=3.0, <4',
+ python_requires='>=3.0',
install_requires=['feedparser', 'pgi', 'playsound', 'requests'],
scripts=[script_exec],
project_urls={
diff --git a/tartube/config.py b/tartube/config.py
index 1c521b6..44d8a1e 100644
--- a/tartube/config.py
+++ b/tartube/config.py
@@ -32,6 +32,7 @@ import os
# Import our modules
import __main__
+import downloads
import formats
import mainapp
import mainwin
@@ -95,8 +96,8 @@ class GenericConfigWin(Gtk.Window):
self.show_all()
# Inform the main window of this window's birth (so that Tartube
- # doesn't allow a download/update/refresh/info/tidy operation to
- # start until all configuration windows have closed)
+ # doesn't allow an operation to start until all configuration windows
+ # have closed)
self.app_obj.main_win_obj.add_child_window(self)
# Add a callback so we can inform the main window of this window's
# destruction
@@ -2211,8 +2212,8 @@ class OptionsEditWin(GenericEditWin):
self.add_label(grid,
_(
- 'Extra youtube-dl command line options (e.g. --help; do not use' \
- + ' -o or --output)',
+ 'Extra command line options (e.g. --help; do not use -o or' \
+ + ' --output)',
),
0, 3, 2, 1,
)
@@ -2337,7 +2338,7 @@ class OptionsEditWin(GenericEditWin):
# Signal connect appears below
self.add_label(grid,
- _('youtube-dl file output template'),
+ _('File output template'),
0, 3, grid_width, 1,
)
@@ -2397,7 +2398,7 @@ class OptionsEditWin(GenericEditWin):
_('Video format'),
[
'format', _('Video format'),
- 'format_id', _('youtube-dl format code'),
+ 'format_id', _('Video format code'),
'width', _('Video width'),
'height', _('Video height'),
'resolution', _('Video resolution'),
@@ -2564,38 +2565,80 @@ class OptionsEditWin(GenericEditWin):
"""
tab, grid = self.add_inner_notebook_tab(
- _('_Write files'),
+ _('_Write/move files'),
inner_notebook,
)
+ grid_width = 2
+
# Write other files options
self.add_label(grid,
- '' + _('Write other file options') + '',
- 0, 0, 1, 1,
+ '' + _('File write options') + '',
+ 0, 0, grid_width, 1,
)
self.add_checkbutton(grid,
_('Write video\'s description to a .description file'),
'write_description',
- 0, 1, 1, 1,
+ 0, 1, grid_width, 1,
)
self.add_checkbutton(grid,
_('Write video\'s metadata to an .info.json file'),
'write_info',
- 0, 2, 1, 1,
+ 0, 2, grid_width, 1,
)
self.add_checkbutton(grid,
- _('Write video\'s annotations to an .annotations.xml file'),
+ _(
+ 'Write video\'s annotations to an .annotations.xml file',
+ ),
'write_annotations',
- 0, 3, 1, 1,
+ 0, 3, grid_width, 1,
+ )
+
+ self.add_label(grid,
+ '' + _(
+ 'Annotations are not downloaded when checking videos/channels/' \
+ + 'playlists/folders'
+ ) + '',
+ 1, 4, 1, 1,
)
self.add_checkbutton(grid,
_('Write the video\'s thumbnail to the same folder'),
'write_thumbnail',
- 0, 4, 1, 1,
+ 0, 5, grid_width, 1,
+ )
+
+ # File move options
+ self.add_label(grid,
+ '' + _('File move options') + '',
+ 0, 6, grid_width, 1,
+ )
+
+ self.add_checkbutton(grid,
+ _('Move video\'s description file into a sub-folder'),
+ 'move_description',
+ 0, 7, grid_width, 1,
+ )
+
+ self.add_checkbutton(grid,
+ _('Write video\'s metadata file into a sub-folder'),
+ 'move_info',
+ 0, 8, grid_width, 1,
+ )
+
+ self.add_checkbutton(grid,
+ _('Write video\'s annotations file into a sub-folder'),
+ 'move_annotations',
+ 0, 9, grid_width, 1,
+ )
+
+ self.add_checkbutton(grid,
+ _('Write the video\'s thumbnail into a sub-folder'),
+ 'move_thumbnail',
+ 0, 10, grid_width, 1,
)
@@ -2846,8 +2889,8 @@ class OptionsEditWin(GenericEditWin):
extra_row = 1
self.add_label(grid,
'' + _(
- 'Multiple formats will not be downloaded, because' \
- + ' youtube-dl is creating an archive file'
+ 'Multiple formats will not be downloaded, because an' \
+ + ' archive file will be created'
) + '\n' + _(
'The archive file can be disabled in the System' \
' Preferences window',
@@ -3168,7 +3211,7 @@ class OptionsEditWin(GenericEditWin):
Sets up the 'Post-processing' tab.
"""
- tab, grid = self.add_notebook_tab(_('_Post-process'))
+ tab, grid = self.add_notebook_tab(_('_Post-processing'))
grid_width = 2
grid.set_column_homogeneous(True)
@@ -3187,7 +3230,7 @@ class OptionsEditWin(GenericEditWin):
)
button = self.add_checkbutton(grid,
- _('Prefer avconv over ffmpeg'),
+ _('Prefer AVConv over FFmpeg'),
'prefer_avconv',
0, 2, 1, 1,
)
@@ -3195,7 +3238,7 @@ class OptionsEditWin(GenericEditWin):
button.set_sensitive(False)
button2 = self.add_checkbutton(grid,
- _('Prefer ffmpeg over avconv (default)'),
+ _('Prefer FFmpeg over AVConv (default)'),
'prefer_ffmpeg',
1, 2, 1, 1,
)
@@ -3742,7 +3785,7 @@ class OptionsEditWin(GenericEditWin):
)
self.add_label(grid,
- _('Custom user agent for youtube-dl'),
+ _('Custom user agent'),
0, 17, 1, 1,
)
@@ -3930,7 +3973,7 @@ class OptionsEditWin(GenericEditWin):
self.add_label(grid,
'' + _(
- 'youtube-dl treats channels and playlists the same way, so' \
+ 'Channels and playlists are handled in the same way, so' \
+ ' these options can be used with both',
) + '',
0, (row_count + 1), grid_width, 1,
@@ -6060,8 +6103,9 @@ class SystemPrefWin(GenericPrefWin):
app_obj (mainapp.TartubeApp): The main application object
init_mode (str): 'db' to automatically open the tab with options for
- switching the Tartube database, 'live' to automatically open the
- tab with livestream options. Any other value is ignored
+ switching the Tartube database, 'paths' to automatically open the
+ tab with youtube-dl paths, 'live' to automatically open the tab
+ with livestream options. Any other value is ignored
"""
@@ -6113,6 +6157,8 @@ class SystemPrefWin(GenericPrefWin):
if init_mode is not None:
if init_mode == 'db':
self.select_switch_db_tab()
+ elif init_mode == 'paths':
+ self.select_paths_tab()
elif init_mode == 'live':
self.select_livestream_tab()
@@ -6150,6 +6196,17 @@ class SystemPrefWin(GenericPrefWin):
self.filesystem_inner_notebook.set_current_page(1)
+ def select_paths_tab(self):
+
+ """Can be called by anything.
+
+ Makes the visible tab the one on which the user can set youtube-dl
+ options.
+ """
+
+ self.notebook.set_current_page(5)
+
+
def select_livestream_tab(self):
"""Can be called by anything.
@@ -6200,6 +6257,7 @@ class SystemPrefWin(GenericPrefWin):
self.setup_general_stability_tab(inner_notebook)
self.setup_general_modules_tab(inner_notebook)
self.setup_general_video_matching_tab(inner_notebook)
+ self.setup_general_debug_tab(inner_notebook)
def setup_general_language_tab(self, inner_notebook):
@@ -6477,7 +6535,7 @@ class SystemPrefWin(GenericPrefWin):
grid_width = 2
- # Video matching preferences
+ # Video matching preferences
self.add_label(grid,
'' + _('Video matching preferences') + '',
0, 0, grid_width, 1,
@@ -6549,6 +6607,111 @@ class SystemPrefWin(GenericPrefWin):
)
+ def setup_general_debug_tab(self, inner_notebook):
+
+ """Called by self.setup_general_tab().
+
+ Sets up the 'Debug' inner notebook tab.
+ """
+
+ tab, grid = self.add_inner_notebook_tab(
+ _('_Debugging'),
+ inner_notebook,
+ )
+
+ grid_width = 2
+
+ # Debugging preferences
+ self.add_label(grid,
+ '' + _('Debugging preferences') + '',
+ 0, 0, grid_width, 1,
+ )
+
+ self.add_label(grid,
+ '' + _(
+ 'Debug messages are only visible in the terminal window. These' \
+ + ' settings are not saved',
+ ) + '',
+ 0, 1, grid_width, 1,
+ )
+
+ checkbutton = self.add_checkbutton(grid,
+ _('Enable application debug messages (code in mainapp.py)'),
+ mainapp.DEBUG_FUNC_FLAG,
+ True, # Can be toggled by user
+ 0, 2, grid_width, 1,
+ )
+ checkbutton.set_active(mainapp.DEBUG_FUNC_FLAG)
+ # (signal_connect appears below)
+
+ checkbutton2 = self.add_checkbutton(grid,
+ _('...but don\'t show timer debug messages'),
+ mainapp.DEBUG_NO_TIMER_FUNC_FLAG,
+ True, # Can be toggled by user
+ 1, 3, 1, 1,
+ )
+ checkbutton2.set_active(mainapp.DEBUG_NO_TIMER_FUNC_FLAG)
+ if not mainapp.DEBUG_FUNC_FLAG:
+ checkbutton2.set_sensitive(False)
+ # (signal_connect appears below)
+
+ checkbutton3 = self.add_checkbutton(grid,
+ _('Enable main winddow debug messages (code in mainwin.py)'),
+ mainwin.DEBUG_FUNC_FLAG,
+ True, # Can be toggled by user
+ 0, 4, grid_width, 1,
+ )
+ checkbutton3.set_active(mainwin.DEBUG_FUNC_FLAG)
+ # (signal_connect appears below)
+
+ checkbutton4 = self.add_checkbutton(grid,
+ _('...but don\'t show timer debug messages'),
+ mainwin.DEBUG_NO_TIMER_FUNC_FLAG,
+ True, # Can be toggled by user
+ 1, 5, 1, 1,
+ )
+ checkbutton4.set_active(mainwin.DEBUG_NO_TIMER_FUNC_FLAG)
+ if not mainwin.DEBUG_FUNC_FLAG:
+ checkbutton4.set_sensitive(False)
+ # (signal_connect appears below)
+
+ checkbutton5 = self.add_checkbutton(grid,
+ _('Enabled downloader debug messages (code in downloads.py)'),
+ downloads.DEBUG_FUNC_FLAG,
+ True, # Can be toggled by user
+ 0, 6, grid_width, 1,
+ )
+ checkbutton5.set_active(downloads.DEBUG_FUNC_FLAG)
+ # (signal_connect appears below)
+
+ # (signal_connects from above)
+ checkbutton.connect(
+ 'toggled', self.on_system_debug_toggled,
+ 'main_app',
+ checkbutton2,
+ )
+ checkbutton2.connect(
+ 'toggled',
+ self.on_system_debug_toggled,
+ 'main_app_no_timer',
+ )
+ checkbutton3.connect(
+ 'toggled', self.on_system_debug_toggled,
+ 'main_win',
+ checkbutton4,
+ )
+ checkbutton4.connect(
+ 'toggled',
+ self.on_system_debug_toggled,
+ 'main_win_no_timer',
+ )
+ checkbutton5.connect(
+ 'toggled',
+ self.on_system_debug_toggled,
+ 'downloads',
+ )
+
+
def setup_filesystem_tab(self):
"""Called by self.setup_tabs().
@@ -7494,15 +7657,14 @@ class SystemPrefWin(GenericPrefWin):
self.on_operation_warning_button_toggled,
)
- # youtube-dl error/warning preferences
+ # Downloader error/warning preferences
self.add_label(grid,
- '' + _('youtube-dl error/warning preferences') + '',
+ '' + _('Downloader error/warning preferences') + '',
0, 3, 1, 1,
)
translate_note = _(
- 'TRANSLATOR\'S NOTE: These youtube-dl error messages are always' \
- + ' in English',
+ 'TRANSLATOR\'S NOTE: These error messages are always in English',
)
checkbutton5 = self.add_checkbutton(grid,
@@ -7694,8 +7856,9 @@ class SystemPrefWin(GenericPrefWin):
0, 0, grid_width, 1,
)
+ # 'Check all'
self.add_label(grid,
- _('Automatic \'Download all\' operations'),
+ _('Automatic \'Check all\' operations'),
0, 1, 1, 1,
)
@@ -7714,9 +7877,9 @@ class SystemPrefWin(GenericPrefWin):
combo.add_attribute(renderer_text, 'text', 1)
combo.set_entry_text_column(1)
- if self.app_obj.scheduled_dl_mode == 'start':
+ if self.app_obj.scheduled_check_mode == 'start':
combo.set_active(1)
- elif self.app_obj.scheduled_dl_mode == 'scheduled':
+ elif self.app_obj.scheduled_check_mode == 'scheduled':
combo.set_active(2)
else:
combo.set_active(0)
@@ -7728,11 +7891,11 @@ class SystemPrefWin(GenericPrefWin):
)
spinbutton = self.add_spinbutton(grid,
- 1, 999, 1, self.app_obj.scheduled_dl_wait_value,
+ 1, 999, 1, self.app_obj.scheduled_check_wait_value,
1, 2, 1, 1,
)
- if self.app_obj.scheduled_dl_mode != 'scheduled':
- spinbutton.set_sensitive(False)
+ if self.app_obj.scheduled_check_mode != 'scheduled':
+ spinbutton.set_sensitive(False)
# Signal connect appears below
store2 = Gtk.ListStore(str, str)
@@ -7748,15 +7911,16 @@ class SystemPrefWin(GenericPrefWin):
combo2.set_entry_text_column(1)
combo2.set_active(
formats.TIME_METRIC_LIST.index(
- self.app_obj.scheduled_dl_wait_unit,
+ self.app_obj.scheduled_check_wait_unit,
)
)
- if self.app_obj.scheduled_dl_mode != 'scheduled':
+ if self.app_obj.scheduled_check_mode != 'scheduled':
combo2.set_sensitive(False)
# Signal connect appears below
+ # 'Download all'
self.add_label(grid,
- _('Automatic \'Check all\' operations'),
+ _('Automatic \'Download all\' operations'),
0, 3, 1, 1,
)
@@ -7775,9 +7939,9 @@ class SystemPrefWin(GenericPrefWin):
combo3.add_attribute(renderer_text, 'text', 1)
combo3.set_entry_text_column(1)
- if self.app_obj.scheduled_check_mode == 'start':
+ if self.app_obj.scheduled_dl_mode == 'start':
combo3.set_active(1)
- elif self.app_obj.scheduled_check_mode == 'scheduled':
+ elif self.app_obj.scheduled_dl_mode == 'scheduled':
combo3.set_active(2)
else:
combo3.set_active(0)
@@ -7789,11 +7953,11 @@ class SystemPrefWin(GenericPrefWin):
)
spinbutton2 = self.add_spinbutton(grid,
- 1, 999, 1, self.app_obj.scheduled_check_wait_value,
+ 1, 999, 1, self.app_obj.scheduled_dl_wait_value,
1, 4, 1, 1,
)
- if self.app_obj.scheduled_check_mode != 'scheduled':
- spinbutton2.set_sensitive(False)
+ if self.app_obj.scheduled_dl_mode != 'scheduled':
+ spinbutton2.set_sensitive(False)
# Signal connect appears below
store4 = Gtk.ListStore(str, str)
@@ -7809,43 +7973,121 @@ class SystemPrefWin(GenericPrefWin):
combo4.set_entry_text_column(1)
combo4.set_active(
formats.TIME_METRIC_LIST.index(
- self.app_obj.scheduled_check_wait_unit,
+ self.app_obj.scheduled_dl_wait_unit,
)
)
- if self.app_obj.scheduled_check_mode != 'scheduled':
+ if self.app_obj.scheduled_dl_mode != 'scheduled':
combo4.set_sensitive(False)
# Signal connect appears below
+ # Custom 'Download all'
+ self.add_label(grid,
+ _('Automatic custom \'Download all\' operations'),
+ 0, 5, 1, 1,
+ )
+
+ store5 = Gtk.ListStore(str, str)
+
+ store5.append( ['none', _('Disabled')] )
+ store5.append( ['start', _('Performed when Tartube starts')] )
+ store5.append( ['scheduled', _('Performed at regular intervals')] )
+
+ combo5 = Gtk.ComboBox.new_with_model(store5)
+ grid.attach(combo5, 1, 5, 2, 1)
+ combo5.set_hexpand(True)
+
+ renderer_text = Gtk.CellRendererText()
+ combo5.pack_start(renderer_text, True)
+ combo5.add_attribute(renderer_text, 'text', 1)
+ combo5.set_entry_text_column(1)
+
+ if self.app_obj.scheduled_custom_mode == 'start':
+ combo5.set_active(1)
+ elif self.app_obj.scheduled_custom_mode == 'scheduled':
+ combo5.set_active(2)
+ else:
+ combo5.set_active(0)
+ # Signal connect appears below
+
+ self.add_label(grid,
+ _('Time (in hours) between operations'),
+ 0, 6, 1, 1,
+ )
+
+ spinbutton3 = self.add_spinbutton(grid,
+ 1, 999, 1, self.app_obj.scheduled_custom_wait_value,
+ 1, 6, 1, 1,
+ )
+ if self.app_obj.scheduled_custom_mode != 'scheduled':
+ spinbutton3.set_sensitive(False)
+ # Signal connect appears below
+
+ store6 = Gtk.ListStore(str, str)
+ for string in formats.TIME_METRIC_LIST:
+ store6.append( [string, formats.TIME_METRIC_TRANS_DICT[string]] )
+
+ combo6 = Gtk.ComboBox.new_with_model(store6)
+ grid.attach(combo6, 2, 6, 1, 1)
+
+ renderer_text = Gtk.CellRendererText()
+ combo6.pack_start(renderer_text, True)
+ combo6.add_attribute(renderer_text, 'text', 1)
+ combo6.set_entry_text_column(1)
+ combo6.set_active(
+ formats.TIME_METRIC_LIST.index(
+ self.app_obj.scheduled_custom_wait_unit,
+ )
+ )
+ if self.app_obj.scheduled_custom_mode != 'scheduled':
+ combo6.set_sensitive(False)
+ # Signal connect appears below
+
checkbutton = self.add_checkbutton(grid,
_(
- 'After an automatic \'Download/Check all\' operation, shut down' \
- + ' Tartube',
+ 'After an automatic operation, shut down Tartube',
),
self.app_obj.scheduled_shutdown_flag,
True, # Can be toggled by user
- 0, 5, grid_width, 1,
+ 0, 7, grid_width, 1,
)
# Signal connects from above
combo.connect(
- 'changed',
- self.on_dl_mode_combo_changed,
- spinbutton, combo2,
- )
- spinbutton.connect('value-changed', self.on_dl_wait_spinbutton_changed)
- combo2.connect('changed', self.on_dl_wait_combo_changed)
-
- combo3.connect(
'changed',
self.on_check_mode_combo_changed,
- spinbutton2,
- combo4,
+ spinbutton,
+ combo2,
)
- spinbutton2.connect(
+ spinbutton.connect(
'value-changed',
self.on_check_wait_spinbutton_changed,
)
- combo4.connect('changed', self.on_check_wait_combo_changed)
+ combo2.connect('changed', self.on_check_wait_combo_changed)
+
+
+
+
+ combo3.connect(
+ 'changed',
+ self.on_dl_mode_combo_changed,
+ spinbutton2, combo4,
+ )
+ spinbutton2.connect(
+ 'value-changed',
+ self.on_dl_wait_spinbutton_changed,
+ )
+ combo4.connect('changed', self.on_dl_wait_combo_changed)
+
+ combo5.connect(
+ 'changed',
+ self.on_custom_mode_combo_changed,
+ spinbutton3, combo6,
+ )
+ spinbutton3.connect(
+ 'value-changed',
+ self.on_custom_wait_spinbutton_changed,
+ )
+ combo6.connect('changed', self.on_custom_wait_combo_changed)
checkbutton.connect('toggled', self.on_scheduled_stop_button_toggled)
@@ -8037,7 +8279,7 @@ class SystemPrefWin(GenericPrefWin):
checkbutton = self.add_checkbutton(grid,
_(
- 'Automatically update youtube-dl before every download operation',
+ 'Automatically update downloader before every download operation',
),
self.app_obj.operation_auto_update_flag,
True, # Can be toggled by user
@@ -8049,8 +8291,7 @@ class SystemPrefWin(GenericPrefWin):
checkbutton2 = self.add_checkbutton(grid,
_(
- 'Automatically save files at the end of a download/update/' \
- + 'refresh operation',
+ 'Automatically save files at the end of all operations',
),
self.app_obj.operation_save_flag,
True, # Can be toggled by user
@@ -8080,6 +8321,36 @@ class SystemPrefWin(GenericPrefWin):
)
checkbutton4.connect('toggled', self.on_operation_sim_button_toggled)
+ # Invidious mirror
+ self.add_label(grid,
+ '' + _('Invidious mirror') + '',
+ 0, 5, 1, 1,
+ )
+
+ self.add_label(grid,
+ _(
+ 'To find an updated list of Invidious mirrors, use any' \
+ + ' search engine!',
+ ),
+ 0, 6, 1, 1,
+ )
+
+ entry = self.add_entry(grid,
+ self.app_obj.custom_invidious_mirror,
+ True,
+ 0, 7, 1, 1,
+ )
+ entry.connect('changed', self.on_invidious_mirror_changed)
+
+ msg = _('Type the exact text that replaces youtube.com e.g.')
+ msg = re.sub('youtube.com', ' youtube.com ', msg)
+
+ self.add_label(grid,
+ '' + msg + ' ' + self.app_obj.default_invidious_mirror \
+ + '',
+ 0, 8, 1, 1,
+ )
+
def setup_operations_custom_tab(self, inner_notebook):
@@ -8212,11 +8483,11 @@ class SystemPrefWin(GenericPrefWin):
if not self.app_obj.custom_dl_divert_mode == 'other':
entry.set_sensitive(False)
+ msg = _('Type the exact text that replaces youtube.com e.g.')
+ msg = re.sub('youtube.com', ' youtube.com ', msg)
+
self.add_label(grid,
- _(
- 'Type the exact text that replaces' \
- + ' youtube.com e.g. hooktube.com',
- ),
+ '' + msg + ' hooktube.com',
0, 10, grid_width, 1,
)
@@ -8478,16 +8749,14 @@ class SystemPrefWin(GenericPrefWin):
radiobutton = self.add_radiobutton(grid,
None,
_(
- 'Show a dialogue window at the end of a download/update/refresh/' \
- + 'info/tidy operation',
+ 'Show a dialogue window at the end of an operation',
),
0, 1, 1, 1,
)
# Signal connect appears below
-
+
if platform.system() != 'Windows' and platform.system != 'Darwin':
- text = 'Show a desktop notification at the end of a download' \
- + '/update/refresh/info/tidy operation'
+ text = 'Show a desktop notification at the end of an operation'
else:
text = 'Show a desktop notification (Linux/*BSD only)'
@@ -8505,8 +8774,7 @@ class SystemPrefWin(GenericPrefWin):
radiobutton3 = self.add_radiobutton(grid,
radiobutton2,
_(
- 'Don\'t notify the user at the end of a download/update/refresh/' \
- + 'info/tidy operation',
+ 'Don\'t notify the user at the end of an operation',
),
0, 3, 1, 1,
)
@@ -8764,7 +9032,7 @@ class SystemPrefWin(GenericPrefWin):
"""
# Add this tab...
- tab, grid = self.add_notebook_tab('_youtube-dl')
+ tab, grid = self.add_notebook_tab('_' + self.app_obj.get_downloader())
# ...and an inner notebook...
self.operations_inner_notebook = self.add_inner_notebook(grid)
@@ -8772,6 +9040,7 @@ class SystemPrefWin(GenericPrefWin):
# ...with its own tabs
self.setup_ytdl_file_paths_tab(self.operations_inner_notebook)
self.setup_ytdl_prefs_tab(self.operations_inner_notebook)
+ self.setup_ytdl_ffmpeg_tab(self.operations_inner_notebook)
def setup_ytdl_file_paths_tab(self, inner_notebook):
@@ -8793,38 +9062,12 @@ class SystemPrefWin(GenericPrefWin):
0, 0, grid_width, 1,
)
-
- label = self.add_label(grid,
- _('youtube-dl executable (system-dependent)'),
+ # youtube-dl file paths
+ self.add_label(grid,
+ _('Path to youtube-dl executable'),
0, 1, 1, 1,
)
- entry = self.add_entry(grid,
- self.app_obj.ytdl_bin,
- False,
- 1, 1, (grid_width - 1), 1,
- )
- entry.set_sensitive(True)
- entry.set_editable(False)
-
- label2 = self.add_label(grid,
- _('Default path to youtube-dl executable'),
- 0, 2, 1, 1,
- )
-
- entry2 = self.add_entry(grid,
- self.app_obj.ytdl_path_default,
- False,
- 1, 2, (grid_width - 1), 1,
- )
- entry2.set_sensitive(True)
- entry2.set_editable(False)
-
- label3 = self.add_label(grid,
- _('Actual path to use'),
- 0, 3, 1, 1,
- )
-
combo_list = [
[
_('Use default path') + ' (' + self.app_obj.ytdl_path_default \
@@ -8851,24 +9094,22 @@ class SystemPrefWin(GenericPrefWin):
store.append( [ mini_list[0], mini_list[1] ] )
combo = Gtk.ComboBox.new_with_model(store)
- grid.attach(combo, 1, 3, (grid_width - 1), 1)
+ grid.attach(combo, 1, 1, (grid_width - 1), 1)
renderer_text = Gtk.CellRendererText()
combo.pack_start(renderer_text, True)
combo.add_attribute(renderer_text, 'text', 0)
combo.set_entry_text_column(0)
-
if self.app_obj.ytdl_path == self.app_obj.ytdl_path_default:
combo.set_active(0)
elif self.app_obj.ytdl_path == self.app_obj.ytdl_path_pypi:
combo.set_active(2)
else:
combo.set_active(1)
+ # (signal_connect appears below)
- combo.connect('changed', self.on_ytdl_path_combo_changed)
-
- label4 = self.add_label(grid,
- _('Shell command for update operations'),
- 0, 4, 1, 1,
+ self.add_label(grid,
+ _('Command for update operations'),
+ 0, 2, 1, 1,
)
store2 = Gtk.ListStore(str, str)
@@ -8876,7 +9117,7 @@ class SystemPrefWin(GenericPrefWin):
store2.append( [item, formats.YTDL_UPDATE_DICT[item]] )
combo2 = Gtk.ComboBox.new_with_model(store2)
- grid.attach(combo2, 1, 4, (grid_width - 1), 1)
+ grid.attach(combo2, 1, 2, (grid_width - 1), 1)
renderer_text = Gtk.CellRendererText()
combo2.pack_start(renderer_text, True)
@@ -8888,9 +9129,46 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.ytdl_update_current,
),
)
- combo2.connect('changed', self.on_update_combo_changed)
if __main__.__pkg_strict_install_flag__:
combo2.set_sensitive(False)
+ # (signal_connect appears below)
+
+ # Update the combos, so that the youtube-dl fork, rather than
+ # youtube-dl itself, is visible (if applicable)
+ self.update_ytdl_combos(store, store2)
+
+ # (signal_connects from above)
+ combo.connect('changed', self.on_ytdl_path_combo_changed)
+ combo2.connect('changed', self.on_update_combo_changed)
+
+ # youtube-dl forks
+ self.add_label(grid,
+ '' + _('youtube-dl forks') + '',
+ 0, 3, grid_width, 1,
+ )
+
+ self.add_label(grid,
+ _('Use this fork of youtube-dl'),
+ 0, 4, 1, 1,
+ )
+
+ entry3 = self.add_entry(grid,
+ self.app_obj.ytdl_fork,
+ True,
+ 1, 4, (grid_width - 1), 1,
+ )
+ entry3.set_sensitive(True)
+ entry3.connect('changed', self.on_ytdl_fork_changed, store, store2)
+
+ self.add_label(grid,
+ ''
+ + _(
+ 'If you specify a fork (e.g. youtube-dlc), it must be very' \
+ + ' similar to the original youtube-dl' \
+ + '\nTo use the original youtube-dl, leave the box empty',
+ ) + '',
+ 0, 5, grid_width, 1,
+ )
def setup_ytdl_prefs_tab(self, inner_notebook):
@@ -8904,50 +9182,12 @@ class SystemPrefWin(GenericPrefWin):
_('_Preferences'),
inner_notebook,
)
- grid_width = 3
-
- # Post-processing preferences
- self.add_label(grid,
- '' + _('Post-processing preferences') + '',
- 0, 0, grid_width, 1,
- )
-
- self.add_label(grid,
- _('Path to the ffmpeg/avconv binary'),
- 0, 1, 1, 1,
- )
-
- button = Gtk.Button(_('Set'))
- grid.attach(button, 1, 1, 1, 1)
- # (signal_connect appears below)
-
- button2 = Gtk.Button(_('Reset'))
- grid.attach(button2, 2, 1, 1, 1)
- # (signal_connect appears below)
-
- entry = self.add_entry(grid,
- self.app_obj.ffmpeg_path,
- False,
- 0, 2, grid_width, 1,
- )
- entry.set_sensitive(True)
- entry.set_editable(False)
- entry.set_hexpand(True)
-
- if os.name == 'nt':
- entry.set_sensitive(False)
- entry.set_text(_('Install from main menu'))
- button.set_sensitive(False)
- button2.set_sensitive(False)
-
- # (signal_connects from above)
- button.connect('clicked', self.on_set_ffmpeg_button_clicked, entry)
- button2.connect('clicked', self.on_reset_ffmpeg_button_clicked, entry)
+ grid_width = 2
# Missing video preferences
self.add_label(grid,
'' + _('Missing video preferences') + '',
- 0, 3, grid_width, 1,
+ 0, 0, grid_width, 1,
)
checkbutton4 = self.add_checkbutton(grid,
@@ -8957,7 +9197,7 @@ class SystemPrefWin(GenericPrefWin):
),
self.app_obj.track_missing_videos_flag,
True, # Can be toggled by user
- 0, 4, grid_width, 1,
+ 0, 1, grid_width, 1,
)
# (signal_connect appears below)
@@ -8967,7 +9207,7 @@ class SystemPrefWin(GenericPrefWin):
),
self.app_obj.track_missing_time_flag,
True, # Can be toggled by user
- 0, 5, 2, 1,
+ 0, 2, 1, 1,
)
if not self.app_obj.track_missing_videos_flag:
checkbutton5.set_sensitive(False)
@@ -8978,7 +9218,7 @@ class SystemPrefWin(GenericPrefWin):
365,
1, # Step
self.app_obj.track_missing_time_days,
- 1, 5, 2, 1,
+ 1, 2, 1, 1,
)
if not self.app_obj.track_missing_videos_flag \
or not self.app_obj.track_missing_time_flag:
@@ -9005,17 +9245,17 @@ class SystemPrefWin(GenericPrefWin):
# Other preferences
self.add_label(grid,
'' + _('Other preferences') + '',
- 0, 6, grid_width, 1,
+ 0, 3, grid_width, 1,
)
checkbutton = self.add_checkbutton(grid,
_(
- 'Allow youtube-dl to create its own archive file (so deleted' \
+ 'Allow downloader to create its own archive file (so deleted' \
+ ' videos are not re-downloaded)',
),
self.app_obj.allow_ytdl_archive_flag,
True, # Can be toggled by user
- 0, 7, grid_width, 1,
+ 0, 4, grid_width, 1,
)
checkbutton.connect('toggled', self.on_archive_button_toggled)
@@ -9026,7 +9266,7 @@ class SystemPrefWin(GenericPrefWin):
),
self.app_obj.classic_ytdl_archive_flag,
True, # Can be toggled by user
- 0, 8, grid_width, 1,
+ 0, 5, grid_width, 1,
)
checkbutton2.connect('toggled', self.on_archive_classic_button_toggled)
@@ -9036,10 +9276,132 @@ class SystemPrefWin(GenericPrefWin):
),
self.app_obj.apply_json_timeout_flag,
True, # Can be toggled by user
- 0, 9, grid_width, 1,
+ 0, 6, grid_width, 1,
)
checkbutton3.connect('toggled', self.on_json_button_toggled)
+ checkbutton4 = self.add_checkbutton(grid,
+ _(
+ 'Convert .webp thumbnails into .jpg thumbnails (using FFmpeg)' \
+ + ' after downloading them',
+ ),
+ self.app_obj.ffmpeg_convert_webp_flag,
+ True, # Can be toggled by user
+ 0, 7, grid_width, 1,
+ )
+ checkbutton4.connect('toggled', self.on_ffmpeg_convert_flag_toggled)
+
+
+ def setup_ytdl_ffmpeg_tab(self, inner_notebook):
+
+ """Called by self.setup_ytdl_tab().
+
+ Sets up the 'Preferences' inner notebook tab.
+ """
+
+ tab, grid = self.add_inner_notebook_tab(
+ _('_FFmpeg / AVConv'),
+ inner_notebook,
+ )
+
+ grid_width = 4
+
+ # Post-processing preferences
+ self.add_label(grid,
+ '' + _('Post-processing preferences') + '',
+ 0, 0, grid_width, 1,
+ )
+ self.add_label(grid,
+ '' + _(
+ 'You only need to set these paths if Tartube cannot find' \
+ + ' FFmpeg / AVConv automatically'
+ ) + '',
+ 0, 1, grid_width, 1,
+ )
+
+ self.add_label(grid,
+ _('Path to the FFmpeg executable'),
+ 0, 2, 1, 1,
+ )
+
+ button = Gtk.Button(_('Set'))
+ grid.attach(button, 1, 2, 1, 1)
+ # (signal_connect appears below)
+
+ button2 = Gtk.Button(_('Reset'))
+ grid.attach(button2, 2, 2, 1, 1)
+ # (signal_connect appears below)
+
+ button3 = Gtk.Button(_('Use default path'))
+ grid.attach(button3, 3, 2, 1, 1)
+ # (signal_connect appears below)
+
+ entry = self.add_entry(grid,
+ self.app_obj.ffmpeg_path,
+ False,
+ 0, 3, grid_width, 1,
+ )
+ entry.set_sensitive(False)
+ entry.set_editable(False)
+ entry.set_hexpand(True)
+
+ if os.name == 'nt':
+ entry.set_sensitive(False)
+ entry.set_text(_('Install from main menu'))
+ button.set_sensitive(False)
+ button2.set_sensitive(False)
+ button3.set_sensitive(False)
+
+ # (signal_connects from above)
+ button.connect('clicked', self.on_set_ffmpeg_button_clicked, entry)
+ button2.connect('clicked', self.on_reset_ffmpeg_button_clicked, entry)
+ button3.connect(
+ 'clicked',
+ self.on_default_ffmpeg_button_clicked, entry,
+ )
+
+ self.add_label(grid,
+ _('Path to the AVConv executable'),
+ 0, 4, 1, 1,
+ )
+
+ button4 = Gtk.Button(_('Set'))
+ grid.attach(button4, 1, 4, 1, 1)
+ # (signal_connect appears below)
+
+ button5 = Gtk.Button(_('Reset'))
+ grid.attach(button5, 2, 4, 1, 1)
+ # (signal_connect appears below)
+
+ button6 = Gtk.Button(_('Use default path'))
+ grid.attach(button6, 3, 4, 1, 1)
+ # (signal_connect appears below)
+
+ entry2 = self.add_entry(grid,
+ self.app_obj.ffmpeg_path,
+ False,
+ 0, 5, grid_width, 1,
+ )
+ entry2.set_sensitive(False)
+ entry2.set_editable(False)
+ entry2.set_hexpand(True)
+
+ if os.name == 'nt':
+ entry2.set_sensitive(False)
+ entry2.set_text(_('Not supported on MS Windows'))
+ button4.set_sensitive(False)
+ button5.set_sensitive(False)
+ button6.set_sensitive(False)
+
+ # (signal_connects from above)
+ button4.connect('clicked', self.on_set_avconv_button_clicked, entry2)
+ button5.connect('clicked', self.on_reset_avconv_button_clicked, entry2)
+ button6.connect(
+ 'clicked',
+ self.on_default_avconv_button_clicked,
+ entry2,
+ )
+
def setup_output_tab(self):
@@ -9079,7 +9441,7 @@ class SystemPrefWin(GenericPrefWin):
)
checkbutton = self.add_checkbutton(grid,
- _('Display youtube-dl system commands in the Output Tab'),
+ _('Display downloader system commands in the Output Tab'),
self.app_obj.ytdl_output_system_cmd_flag,
True, # Can be toggled by user
0, 1, 1, 1,
@@ -9088,7 +9450,7 @@ class SystemPrefWin(GenericPrefWin):
checkbutton.connect('toggled', self.on_output_system_button_toggled)
checkbutton2 = self.add_checkbutton(grid,
- _('Display output from youtube-dl\'s STDOUT in the Output Tab'),
+ _('Display output from downloader\'s STDOUT in the Output Tab'),
self.app_obj.ytdl_output_stdout_flag,
True, # Can be toggled by user
0, 2, 1, 1,
@@ -9127,7 +9489,7 @@ class SystemPrefWin(GenericPrefWin):
)
checkbutton5 = self.add_checkbutton(grid,
- _('Display output from youtube-dl\'s STDERR in the Output Tab'),
+ _('Display output from downloader\'s STDERR in the Output Tab'),
self.app_obj.ytdl_output_stderr_flag,
True, # Can be toggled by user
0, 5, 1, 1,
@@ -9157,36 +9519,47 @@ class SystemPrefWin(GenericPrefWin):
checkbutton7.connect('toggled', self.on_output_summary_button_toggled)
checkbutton8 = self.add_checkbutton(grid,
+ _(
+ 'During an update operation, automatically switch to the Output' \
+ + ' tab',
+ ),
+ self.app_obj.auto_switch_output_flag,
+ True, # Can be toggled by user
+ 0, 8, 1, 1,
+ )
+ checkbutton8.connect('toggled', self.on_auto_switch_button_toggled)
+
+ checkbutton9 = self.add_checkbutton(grid,
_(
'During a refresh operation, show all matching videos in the' \
+ ' Output Tab',
),
self.app_obj.refresh_output_videos_flag,
True, # Can be toggled by user
- 0, 8, 1, 1,
- )
- checkbutton8.set_hexpand(False)
- # Signal connect appears below
-
- checkbutton9 = self.add_checkbutton(grid,
- _('...also show all non-matching videos'),
- self.app_obj.refresh_output_verbose_flag,
- True, # Can be toggled by user
0, 9, 1, 1,
)
checkbutton9.set_hexpand(False)
- checkbutton9.connect(
+ # Signal connect appears below
+
+ checkbutton10 = self.add_checkbutton(grid,
+ _('...also show all non-matching videos'),
+ self.app_obj.refresh_output_verbose_flag,
+ True, # Can be toggled by user
+ 0, 10, 1, 1,
+ )
+ checkbutton10.set_hexpand(False)
+ checkbutton10.connect(
'toggled',
self.on_refresh_verbose_button_toggled,
)
if not self.app_obj.refresh_output_videos_flag:
- checkbutton8.set_sensitive(False)
+ checkbutton9.set_sensitive(False)
# Signal connect from above
- checkbutton8.connect(
+ checkbutton9.connect(
'toggled',
self.on_refresh_videos_button_toggled,
- checkbutton9,
+ checkbutton10,
)
@@ -9209,7 +9582,7 @@ class SystemPrefWin(GenericPrefWin):
)
checkbutton = self.add_checkbutton(grid,
- _('Write youtube-dl system commands to the terminal window'),
+ _('Write downloader system commands to the terminal window'),
self.app_obj.ytdl_write_system_cmd_flag,
True, # Can be toggled by user
0, 1, 1, 1,
@@ -9218,7 +9591,7 @@ class SystemPrefWin(GenericPrefWin):
checkbutton.connect('toggled', self.on_terminal_system_button_toggled)
checkbutton2 = self.add_checkbutton(grid,
- _('Write output from youtube-dl\'s STDOUT to the terminal window'),
+ _('Write output from downloader\'s STDOUT to the terminal window'),
self.app_obj.ytdl_write_stdout_flag,
True, # Can be toggled by user
0, 2, 1, 1,
@@ -9260,7 +9633,7 @@ class SystemPrefWin(GenericPrefWin):
)
checkbutton5 = self.add_checkbutton(grid,
- _('Write output from youtube-dl\'s STDERR to the terminal window'),
+ _('Write output from downloader\'s STDERR to the terminal window'),
self.app_obj.ytdl_write_stderr_flag,
True, # Can be toggled by user
0, 5, 1, 1,
@@ -9291,7 +9664,7 @@ class SystemPrefWin(GenericPrefWin):
)
checkbutton = self.add_checkbutton(grid,
- _('Write verbose output (youtube-dl debugging mode)'),
+ _('Write verbose output (downloader debugging mode)'),
self.app_obj.ytdl_write_verbose_flag,
True, # Can be toggled by user
0, 1, 1, 1,
@@ -9468,6 +9841,27 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_auto_delete_days(spinbutton.get_value())
+ def on_auto_switch_button_toggled(self, checkbutton):
+
+ """Called from callback in self.setup_windows_main_window_tab().
+
+ Enables/disables automatically switching to the Output tab when an
+ update operation starts.
+
+ Args:
+
+ checkbutton (Gtk.CheckButton): The widget clicked
+
+ """
+
+ if checkbutton.get_active() \
+ and not self.app_obj.auto_switch_output_flag:
+ self.app_obj.set_auto_switch_output_flag(True)
+ elif not checkbutton.get_active() \
+ and self.app_obj.auto_switch_output_flag:
+ self.app_obj.set_auto_switch_output_flag(False)
+
+
def on_auto_update_button_toggled(self, checkbutton):
"""Called from callback in self.setup_operations_downloads_tab().
@@ -9980,25 +10374,65 @@ class SystemPrefWin(GenericPrefWin):
entry.set_sensitive(False)
- def on_custom_video_button_toggled(self, checkbutton):
+ def on_custom_mode_combo_changed(self, combo, spinbutton, combo2):
- """Called from callback in self.setup_operations_custom_tab().
+ """Called from a callback in self.setup_scheduling_start_tab().
- Enables/disables downloading videos independently of its channel/
- playlist.
+ Extracts the value visible in the combobox, converts it into another
+ value, and uses that value to update the main application's IV.
Args:
- checkbutton (Gtk.CheckButton): The widget clicked
+ combo (Gtk.ComboBox): The widget clicked
+
+ spinbutton (Gtk.SpinButton): Another widget to be (de)sensitised
+
+ combo2 (Gtk.ComboBox): Another widget to be (de)sensitised
"""
- if checkbutton.get_active() \
- and not self.app_obj.custom_dl_by_video_flag:
- self.app_obj.set_custom_dl_by_video_flag(True)
- elif not checkbutton.get_active() \
- and self.app_obj.custom_dl_by_video_flag:
- self.app_obj.set_custom_dl_by_video_flag(False)
+ tree_iter = combo.get_active_iter()
+ model = combo.get_model()
+ self.app_obj.set_scheduled_custom_mode(model[tree_iter][0])
+ if self.app_obj.scheduled_custom_mode != 'scheduled':
+ spinbutton.set_sensitive(False)
+ combo2.set_sensitive(False)
+ else:
+ spinbutton.set_sensitive(True)
+ combo2.set_sensitive(True)
+
+
+ def on_custom_wait_combo_changed(self, combo):
+
+ """Called from a callback in self.setup_scheduling_start_tab().
+
+ Sets the unit used by the time between scheduled downloads (real, not
+ simualated).
+
+ Args:
+
+ combo (Gtk.ComboBox): The widget clicked
+
+ """
+
+ tree_iter = combo.get_active_iter()
+ model = combo.get_model()
+ self.app_obj.set_scheduled_custom_wait_unit(model[tree_iter][0])
+
+
+ def on_custom_wait_spinbutton_changed(self, spinbutton):
+
+ """Called from callback in self.setup_scheduling_start_tab().
+
+ Sets the interval between scheduled 'Download all' operations.
+
+ Args:
+
+ spinbutton (Gtk.SpinButton): The widget clicked
+
+ """
+
+ self.app_obj.set_scheduled_custom_wait_value(spinbutton.get_value())
def on_custom_textview_changed(self, textbuffer):
@@ -10032,6 +10466,27 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_ignore_custom_msg_list(mod_list)
+ def on_custom_video_button_toggled(self, checkbutton):
+
+ """Called from callback in self.setup_operations_custom_tab().
+
+ Enables/disables downloading videos independently of its channel/
+ playlist.
+
+ Args:
+
+ checkbutton (Gtk.CheckButton): The widget clicked
+
+ """
+
+ if checkbutton.get_active() \
+ and not self.app_obj.custom_dl_by_video_flag:
+ self.app_obj.set_custom_dl_by_video_flag(True)
+ elif not checkbutton.get_active() \
+ and self.app_obj.custom_dl_by_video_flag:
+ self.app_obj.set_custom_dl_by_video_flag(False)
+
+
def on_data_block_button_toggled(self, checkbutton):
"""Called from callback in self.setup_windows_errors_warnings_tab().
@@ -10481,6 +10936,42 @@ class SystemPrefWin(GenericPrefWin):
self.try_switch_db(data_dir, button2)
+ def on_default_avconv_button_clicked(self, button, entry):
+
+ """Called from callback in self.setup_ytdl_ffmpeg_tab().
+
+ Sets the path to the avconv binary to the default path.
+
+ Args:
+
+ button (Gtk.Button): The widget clicked
+
+ entry (Gtk.Entry): Another widget to be modified by this function
+
+ """
+
+ self.app_obj.set_avconv_path(self.app_obj.default_avconv_path)
+ entry.set_text(self.app_obj.avconv_path)
+
+
+ def on_default_ffmpeg_button_clicked(self, button, entry):
+
+ """Called from callback in self.setup_ytdl_ffmpeg_tab().
+
+ Sets the path to the ffmpeg binary to the default path.
+
+ Args:
+
+ button (Gtk.Button): The widget clicked
+
+ entry (Gtk.Entry): Another widget to be modified by this function
+
+ """
+
+ self.app_obj.set_ffmpeg_path(self.app_obj.default_ffmpeg_path)
+ entry.set_text(self.app_obj.ffmpeg_path)
+
+
def on_delay_max_spinbutton_changed(self, spinbutton, spinbutton2):
"""Called from callback in self.setup_operations_custom_tab().
@@ -10852,6 +11343,26 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_full_expand_video_index_flag(False)
+ def on_ffmpeg_convert_flag_toggled(self, checkbutton):
+
+ """Called from callback in self.setup_ytdl_prefs_tab().
+
+ Enables/disables conversion of .webp thumbnails into .jpg thumbnails.
+
+ Args:
+
+ checkbutton (Gtk.CheckButton): The widget clicked
+
+ """
+
+ if checkbutton.get_active() \
+ and not self.app_obj.ffmpeg_convert_webp_flag:
+ self.app_obj.set_ffmpeg_convert_webp_flag(True)
+ elif not checkbutton.get_active() \
+ and self.app_obj.ffmpeg_convert_webp_flag:
+ self.app_obj.set_ffmpeg_convert_webp_flag(False)
+
+
def on_gtk_emulate_button_toggled(self, checkbutton):
"""Called from callback in self.setup_general_stability_tab().
@@ -10945,6 +11456,21 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_ignore_http_404_error_flag(False)
+ def on_invidious_mirror_changed(self, entry):
+
+ """Called from callback in self.setup_operations_custom_tab().
+
+ Sets the Invidious mirror to use.
+
+ Args:
+
+ entry (Gtk.Entry): The widget changed
+
+ """
+
+ self.app_obj.set_custom_invidious_mirror(entry.get_text())
+
+
def on_json_button_toggled(self, checkbutton):
"""Called from callback in self.setup_ytdl_prefs_tab().
@@ -11821,9 +12347,27 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_main_win_save_size_flag(False)
+ def on_reset_avconv_button_clicked(self, button, entry):
+
+ """Called from callback in self.setup_ytdl_avconv_tab().
+
+ Resets the path to the avconv binary.
+
+ Args:
+
+ button (Gtk.Button): The widget clicked
+
+ entry (Gtk.Entry): Another widget to be modified by this function
+
+ """
+
+ self.app_obj.set_avconv_path(None)
+ entry.set_text('')
+
+
def on_reset_ffmpeg_button_clicked(self, button, entry):
- """Called from callback in self.setup_ytdl_prefs_tab().
+ """Called from callback in self.setup_ytdl_ffmpeg_tab().
Resets the path to the ffmpeg binary.
@@ -11945,9 +12489,46 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_scheduled_shutdown_flag(False)
+ def on_set_avconv_button_clicked(self, button, entry):
+
+ """Called from callback in self.setup_ytdl_ffmpeg_tab().
+
+ Opens a window in which the user can select the avconv binary, if it is
+ installed (and if the user wants it).
+
+ Args:
+
+ button (Gtk.Button): The widget clicked
+
+ entry (Gtk.Entry): Another widget to be modified by this function
+
+ """
+
+ dialogue_win = Gtk.FileChooserDialog(
+ _('Please select the AVConv executable'),
+ self,
+ Gtk.FileChooserAction.OPEN,
+ (
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN, Gtk.ResponseType.OK,
+ ),
+ )
+
+ response = dialogue_win.run()
+ if response == Gtk.ResponseType.OK:
+ new_path = dialogue_win.get_filename()
+
+ dialogue_win.destroy()
+
+ if response == Gtk.ResponseType.OK and new_path:
+
+ self.app_obj.set_avconv_path(new_path)
+ entry.set_text(self.app_obj.avconv_path)
+
+
def on_set_ffmpeg_button_clicked(self, button, entry):
- """Called from callback in self.setup_ytdl_prefs_tab().
+ """Called from callback in self.setup_ytdl_ffmpeg_tab().
Opens a window in which the user can select the ffmpeg binary, if it is
installed (and if the user wants it).
@@ -12132,6 +12713,53 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.set_toolbar_squeeze_flag(False)
+ def on_system_debug_toggled(self, checkbutton, debug_type, \
+ checkbutton2=None):
+
+ """Called from callback in self.setup_general_debug_tab().
+
+ Enables/disables system debug messages.
+
+ Args:
+
+ checkbutton (Gtk.CheckButton): The widget clicked
+
+ debug_type (str): Which debug flag to set; one of the strings
+ 'main_app', 'main_app_no_timer', 'main_win',
+ 'main_win_no_timer', 'downloads'
+
+ checkbutton2 (Gtk.CheckButton or None): If specified, this
+ checkbutton is (de)sensitised, depending on the state of
+ the first checkbutton
+
+ """
+
+ flag = checkbutton.get_active()
+ if not flag:
+ flag = False
+ else:
+ flag = True
+
+ if debug_type == 'main_app':
+ mainapp.DEBUG_FUNC_FLAG = flag
+ elif debug_type == 'main_app_no_timer':
+ mainapp.DEBUG_NO_TIMER_FUNC_FLAG = flag
+ elif debug_type == 'main_win':
+ mainwin.DEBUG_FUNC_FLAG = flag
+ elif debug_type == 'main_win_no_timer':
+ mainwin.DEBUG_NO_TIMER_FUNC_FLAG = flag
+ elif debug_type == 'downloads':
+ downloads.DEBUG_FUNC_FLAG = flag
+
+ if checkbutton2:
+
+ if flag:
+ checkbutton2.set_sensitive(True)
+ else:
+ checkbutton2.set_active(False)
+ checkbutton2.set_sensitive(False)
+
+
def on_system_error_button_toggled(self, checkbutton):
"""Called from callback in self.setup_windows_errors_warnings_tab().
@@ -12514,6 +13142,30 @@ class SystemPrefWin(GenericPrefWin):
self.app_obj.main_win_obj.set_video_res(model[tree_iter][0])
+ def on_ytdl_fork_changed(self, entry, store, store2):
+
+ """Called from callback in self.setup_ytdl_file_paths_tab().
+
+ Sets the youtube-dl fork to use, instead of the original youtube-dl
+ itself.
+
+ Args:
+
+ entry (Gtk.Entry): The widget changed
+
+ store, store2 (Gtk.ListStore): The liststores to update
+
+ """
+
+ text = utils.strip_whitespace(entry.get_text())
+ if text == '':
+ self.app_obj.set_ytdl_fork(None)
+ else:
+ self.app_obj.set_ytdl_fork(text)
+
+ self.update_ytdl_combos(store, store2)
+
+
def on_ytdl_path_combo_changed(self, combo):
"""Called from a callback in self.setup_ytdl_file_paths_tab().
@@ -12556,38 +13208,56 @@ class SystemPrefWin(GenericPrefWin):
# Database file already exists, so try to load it now
if not self.app_obj.switch_db([data_dir, self]):
- # Load failed
- if self.app_obj.disable_load_save_flag:
+ # Load failed, and the user chose to shut down Tartube
+ if self.app_obj.disable_load_save_lock_flag:
+
+ return self.app_obj.stop()
+
+ # Load failed for any other reason
+ elif self.app_obj.disable_load_save_flag:
+
button.set_sensitive(False)
- if self.app_obj.disable_load_save_msg is not None:
+ if not self.app_obj.disable_load_save_lock_flag:
- dialogue_win = dialogue_manager_obj.show_msg_dialogue(
- self.app_obj.disable_load_save_msg,
- 'error',
- 'ok',
- self, # Parent window is this window
- )
+ if self.app_obj.disable_load_save_msg is not None:
+ dialogue_win = dialogue_manager_obj.show_msg_dialogue(
+ self.app_obj.disable_load_save_msg,
+ 'error',
+ 'ok',
+ self, # Parent window is this window
+ )
+
+ else:
+
+ dialogue_win = dialogue_manager_obj.show_msg_dialogue(
+ _('Database file not loaded'),
+ 'error',
+ 'ok',
+ self, # Parent window is this window
+ )
+
+ # When load/save is disabled, this preference window can't be
+ # opened
+ # Therefore, if load/save has just been disabled, close this
+ # window after the dialogue window closes
+ dialogue_win.set_modal(True)
+ dialogue_win.run()
+ dialogue_win.destroy()
+ if self.app_obj.disable_load_save_flag:
+ self.destroy()
+
+ # Load not attempted
else:
dialogue_win = dialogue_manager_obj.show_msg_dialogue(
- _('Database file not loaded'),
+ _('Did not try to load the database file'),
'error',
'ok',
self, # Parent window is this window
)
- # When load/save is disabled, this preference window can't be
- # opened
- # Therefore, if load/save has just been disabled, close this
- # window after the dialogue window closes
- dialogue_win.set_modal(True)
- dialogue_win.run()
- dialogue_win.destroy()
- if self.app_obj.disable_load_save_flag:
- self.destroy()
-
else:
# Load succeeded. Redraw the preference window, opening it at the
@@ -12612,3 +13282,69 @@ class SystemPrefWin(GenericPrefWin):
'ok',
self, # Parent window is this window
)
+
+
+ def update_ytdl_combos(self, store, store2):
+
+ """Called initially by self.setup_ytdl_file_paths_tab(), then by
+ self.on_ytdl_fork_changed().
+
+ Updates the contents of the two comboboxes in the tab, so that the
+ youtube-dl fork is visible, rather than yotube-dl itself (if
+ applicable).
+
+ Args:
+
+ store, store2 (Gtk.ListStore): The liststores to update
+
+ """
+
+ fork = standard = 'youtube-dl'
+ if self.app_obj.ytdl_fork is not None:
+ fork = self.app_obj.ytdl_fork
+
+ ytdl_path_default = re.sub(
+ standard,
+ fork,
+ self.app_obj.ytdl_path_default,
+ )
+
+ # First combo: Path to the youtube-dl executable
+ store.set(
+ store.get_iter(Gtk.TreePath(0)),
+ 0,
+ _('Use default path') + ' (' + ytdl_path_default + ')',
+ )
+
+ ytdl_bin = re.sub(
+ standard,
+ fork,
+ self.app_obj.ytdl_bin,
+ )
+
+ store.set(
+ store.get_iter(Gtk.TreePath(1)),
+ 0,
+ _('Use local path') + ' (' + ytdl_bin + ')',
+ )
+
+ ytdl_path_pypi = re.sub(
+ standard,
+ fork,
+ self.app_obj.ytdl_path_pypi,
+ )
+
+ store.set(
+ store.get_iter(Gtk.TreePath(2)),
+ 0,
+ _('Use PyPI path') + ' (' + ytdl_path_pypi + ')',
+ )
+
+ # Second combo: Command for update operations
+ count = -1
+ for item in self.app_obj.ytdl_update_list:
+
+ count += 1
+ descrip = re.sub(standard, fork, formats.YTDL_UPDATE_DICT[item])
+ store2.set(store2.get_iter(Gtk.TreePath(count)), 1, descrip)
+
diff --git a/tartube/downloads.py b/tartube/downloads.py
index 0112837..3265635 100644
--- a/tartube/downloads.py
+++ b/tartube/downloads.py
@@ -2356,8 +2356,8 @@ class VideoDownloader(object):
0,
app_obj.system_error,
302,
- 'Enforced timeout on youtube-dl because it took too long' \
- + ' to fetch a video\'s JSON data',
+ 'Enforced timeout because downloader took too long to' \
+ + ' fetch a video\'s JSON data',
)
# Stop this video downloader, if required to do so, having just
@@ -2580,6 +2580,78 @@ class VideoDownloader(object):
self.stderr_reader.join()
+ def compile_mini_options_dict(self, options_manager_obj):
+
+ """Called by self.confirm_new_video() and .confirm_old_video().
+
+ Compiles a dictionary containing a subset of download options from the
+ specified options.OptionsManager object, to be passed on to
+ mainapp.TartubeApp.announce_video_download().
+
+ Args:
+
+ options_manager_obj (options.OptionsManager): The options manager
+ for this download
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('dld 2457 compile_mini_options_dict')
+
+ mini_options_dict = {
+ 'keep_description': \
+ options_manager_obj.options_dict['keep_description'],
+ 'keep_info': \
+ options_manager_obj.options_dict['keep_info'],
+ 'keep_annotations': \
+ options_manager_obj.options_dict['keep_annotations'],
+ 'keep_thumbnail': \
+ options_manager_obj.options_dict['keep_thumbnail'],
+ 'move_description': \
+ options_manager_obj.options_dict['move_description'],
+ 'move_info': \
+ options_manager_obj.options_dict['move_info'],
+ 'move_annotations': \
+ options_manager_obj.options_dict['move_annotations'],
+ 'move_thumbnail': \
+ options_manager_obj.options_dict['move_thumbnail'],
+ }
+
+ return mini_options_dict
+
+
+ def confirm_archived_video(self, filename):
+
+ """Called by self.extract_stdout_data().
+
+ A modified version of self.confirm_old_video(), called when
+ youtube-dl's 'has already been recorded in archive' message is detected
+ (but only when checking for missing videos).
+
+ Tries to find a match for the video name and, if one is found, marks it
+ as not missing.
+
+ Args:
+
+ filename (str): The video name, which should match the .name of a
+ media.Video object in self.missing_video_check_list
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('dld 2458 confirm_archived_video')
+
+ # Create shortcut variables (for convenience)
+ app_obj = self.download_manager_obj.app_obj
+ media_data_obj = self.download_item_obj.media_data_obj
+
+ # media_data_obj is a media.Channel or media.Playlist object. Check its
+ # child objects, looking for a matching video
+ match_obj = media_data_obj.find_matching_video(app_obj, filename)
+ if match_obj and match_obj in self.missing_video_check_list:
+ self.missing_video_check_list.remove(match_obj)
+
+
def confirm_new_video(self, dir_path, filename, extension):
"""Called by self.extract_stdout_data().
@@ -2653,19 +2725,15 @@ class VideoDownloader(object):
or isinstance(video_obj.parent_obj, media.Playlist):
video_obj.set_index(self.video_num)
- # Fetch the options.OptionsManager object used for this download
- options_manager_obj = self.download_worker_obj.options_manager_obj
-
# Update the main window
GObject.timeout_add(
0,
app_obj.announce_video_download,
self.download_item_obj,
video_obj,
- options_manager_obj.options_dict['keep_description'],
- options_manager_obj.options_dict['keep_info'],
- options_manager_obj.options_dict['keep_annotations'],
- options_manager_obj.options_dict['keep_thumbnail'],
+ self.compile_mini_options_dict(
+ self.download_worker_obj.options_manager_obj,
+ ),
)
# Register the download with DownloadManager, so that download
@@ -2782,11 +2850,6 @@ class VideoDownloader(object):
extension,
)
- # Fetch the options.OptionsManager object used for this
- # download
- options_manager_obj \
- = self.download_worker_obj.options_manager_obj
-
# Update the main window
if media_data_obj.master_dbid != media_data_obj.dbid:
@@ -2809,10 +2872,9 @@ class VideoDownloader(object):
app_obj.announce_video_download,
self.download_item_obj,
video_obj,
- options_manager_obj.options_dict['keep_description'],
- options_manager_obj.options_dict['keep_info'],
- options_manager_obj.options_dict['keep_annotations'],
- options_manager_obj.options_dict['keep_thumbnail'],
+ self.compile_mini_options_dict(
+ self.download_worker_obj.options_manager_obj,
+ ),
)
# This VideoDownloader can now stop, if required to do so after a video
@@ -3118,13 +3180,17 @@ class VideoDownloader(object):
# Deal with the video description, JSON data and thumbnail, according
# to the settings in options.OptionsManager
- options_dict =self.download_worker_obj.options_manager_obj.options_dict
+ options_dict \
+ = self.download_worker_obj.options_manager_obj.options_dict
if descrip and options_dict['write_description']:
+
descrip_path = os.path.abspath(
os.path.join(path, filename + '.description'),
)
+
if not options_dict['sim_keep_description']:
+
descrip_path = utils.convert_path_to_temp(
app_obj,
descrip_path,
@@ -3134,33 +3200,58 @@ class VideoDownloader(object):
# do anything if the call returned None because of a filesystem
# error)
if descrip_path is not None and not os.path.isfile(descrip_path):
+
try:
fh = open(descrip_path, 'wb')
fh.write(descrip.encode('utf-8'))
fh.close()
+
+ if options_dict['move_description']:
+ utils.move_metadata_to_subdir(
+ app_obj,
+ video_obj,
+ '.description',
+ )
+
except:
pass
if options_dict['write_info']:
+
json_path = os.path.abspath(
os.path.join(path, filename + '.info.json'),
)
+
if not options_dict['sim_keep_info']:
json_path = utils.convert_path_to_temp(app_obj, json_path)
if json_path is not None and not os.path.isfile(json_path):
+
try:
with open(json_path, 'w') as outfile:
json.dump(json_dict, outfile, indent=4)
+
+ if options_dict['move_info']:
+ utils.move_metadata_to_subdir(
+ app_obj,
+ video_obj,
+ '.info.json',
+ )
+
except:
pass
- if options_dict['write_annotations']:
- xml_path = os.path.abspath(
- os.path.join(path, filename + '.annotations.xml'),
- )
- if not options_dict['sim_keep_annotations']:
- xml_path = utils.convert_path_to_temp(app_obj, xml_path)
+ # v2.1.101 - Annotations were removed by YouTube in 2019, so this
+ # feature is not available, and will not be available until the
+ # authors have some annotations to test
+# if options_dict['write_annotations']:
+#
+# xml_path = os.path.abspath(
+# os.path.join(path, filename + '.annotations.xml'),
+# )
+#
+# if not options_dict['sim_keep_annotations']:
+# xml_path = utils.convert_path_to_temp(app_obj, xml_path)
if thumbnail and options_dict['write_thumbnail']:
@@ -3181,8 +3272,8 @@ class VideoDownloader(object):
if thumb_path is not None and not os.path.isfile(thumb_path):
- # v2.0.013 The requets module fails if the connection drops
- # v1.2.006 Wiriting the file fails if the directory specified
+ # v2.0.013 The requests module fails if the connection drops
+ # v1.2.006 Writing the file fails if the directory specified
# by thumb_path doesn't exist
# Use 'try' so that neither problem is fatal
try:
@@ -3193,9 +3284,31 @@ class VideoDownloader(object):
except:
pass
+ # Convert .webp thumbnails to .jpg, if required
+ thumb_path = utils.find_thumbnail_webp(app_obj, video_obj)
+ if thumb_path is not None \
+ and not app_obj.ffmpeg_fail_flag \
+ and app_obj.ffmpeg_convert_webp_flag \
+ and not app_obj.ffmpeg_manager_obj.convert_webp(thumb_path):
+
+ app_obj.set_ffmpeg_fail_flag(True)
+ GObject.timeout_add(
+ 0,
+ app_obj.system_error,
+ 307,
+ app_obj.ffmpeg_fail_msg,
+ )
+
+ # Move to the sub-directory, if required
+ if options_dict['move_thumbnail']:
+
+ utils.move_thumbnail_to_subdir(app_obj, video_obj)
+
# If a new media.Video object was created (or if a video whose name is
# unknown, now has a name), add a line to the Results List, as well
# as updating the Video Catalogue
+ # The True argument passes on the download options 'move_description',
+ # etc, but not 'keep_description', etc
if update_results_flag:
GObject.timeout_add(
@@ -3203,6 +3316,10 @@ class VideoDownloader(object):
app_obj.announce_video_download,
self.download_item_obj,
video_obj,
+ # No call to self.compile_mini_options_dict, because this
+ # function deals with download options like
+ # 'move_description' by itself
+ {},
)
else:
@@ -3510,6 +3627,9 @@ class VideoDownloader(object):
stdout_with_spaces_list = stdout.split(' ')
stdout_list = stdout.split()
+ # (Flag set to True when self.confirm_new_video(), etc, are called)
+ confirm_flag = False
+
# Extract the data
stdout_list[0] = stdout_list[0].lstrip('\r')
if stdout_list[0] == '[download]':
@@ -3556,6 +3676,7 @@ class VideoDownloader(object):
)
self.reset_temp_destination()
+ confirm_flag = True
# Get playlist information (when downloading a channel or a
# playlist, this line is received once per video)
@@ -3591,11 +3712,26 @@ class VideoDownloader(object):
self.reset_temp_destination()
self.confirm_old_video(path, filename, extension)
+ confirm_flag = True
# Get filesize abort status
if stdout_list[-1] == 'Aborting.':
dl_stat_dict['status'] = formats.ERROR_STAGE_ABORT
+ # When checking for missing videos, respond to the 'has already
+ # been recorded in archive' message (which is otherwise ignored)
+ if not confirm_flag \
+ and self.missing_video_check_list:
+
+ match = re.search(
+ r'^\[download\]\s(.*)\shas already been recorded in' \
+ + ' archive$',
+ stdout,
+ )
+
+ if match:
+ self.confirm_archived_video(match.group(1))
+
elif stdout_list[0] == '[hlsnative]':
# Get information from the native HLS extractor (see
@@ -4279,7 +4415,7 @@ class JSONFetcher(object):
# Convert a youtube-dl path beginning with ~ (not on MS Windows)
# (code copied from utils.generate_system_cmd() )
- ytdl_path = app_obj.ytdl_path
+ ytdl_path = app_obj.check_downloader(app_obj.ytdl_path)
if os.name != 'nt':
ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path)
@@ -4389,6 +4525,15 @@ class JSONFetcher(object):
local_thumb_path,
)
+ elif options_obj.options_dict['move_thumbnail']:
+ local_thumb_path = os.path.abspath(
+ os.path.join(
+ self.container_obj.get_actual_dir(app_obj),
+ app_obj.thumbs_sub_dir,
+ self.video_name + remote_ext,
+ )
+ )
+
if local_thumb_path:
try:
request_obj = requests.get(self.video_thumb_source)
@@ -4398,6 +4543,22 @@ class JSONFetcher(object):
except:
pass
+ # Convert .webp thumbnails to .jpg, if required
+ if local_thumb_path is not None \
+ and not app_obj.ffmpeg_fail_flag \
+ and app_obj.ffmpeg_convert_webp_flag \
+ and not app_obj.ffmpeg_manager_obj.convert_webp(
+ local_thumb_path
+ ):
+ app_obj.set_ffmpeg_fail_flag(True)
+ GObject.timeout_add(
+ 0,
+ app_obj.system_error,
+ 308,
+ app_obj.ffmpeg_fail_msg,
+ )
+
+
def close(self):
@@ -4816,7 +4977,7 @@ class MiniJSONFetcher(object):
# Convert a youtube-dl path beginning with ~ (not on MS Windows)
# (code copied from utils.generate_system_cmd() )
- ytdl_path = app_obj.ytdl_path
+ ytdl_path = app_obj.check_downloader(app_obj.ytdl_path)
if os.name != 'nt':
ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path)
@@ -5023,7 +5184,7 @@ class MiniJSONFetcher(object):
stdout (str): A string of JSON data as it was received from
youtube-dl (and starting with the character { )
- Return values:
+ Returns:
The JSON data, converted into a Python dictionary
diff --git a/tartube/ffmpeg.py b/tartube/ffmpeg.py
new file mode 100644
index 0000000..4209bbd
--- /dev/null
+++ b/tartube/ffmpeg.py
@@ -0,0 +1,405 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2019-2020 A S Lewis
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see .
+
+
+"""FFmpeg manager classes."""
+
+
+# Import Gtk modules
+# ...
+
+
+# Import other modules
+import os
+import subprocess
+
+
+# Import our modules
+# ...
+
+
+# Classes
+
+
+class FFmpegManager(object):
+
+ """Called by mainapp.TartubeApp.__init__().
+
+ Python class to manage calls to FFmpeg that Tartube wants to make,
+ independently of youtube-dl.
+
+ Most of the code in this file has been updated from youtube-dl itself.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application object
+
+ """
+
+
+ # Standard class methods
+
+
+ def __init__(self, app_obj):
+
+
+ super(FFmpegManager, self).__init__()
+
+ # IV list - class objects
+ # -----------------------
+ # The main application
+ self.app_obj = app_obj
+
+
+ # Public class methods
+
+
+ def convert_webp(self, thumbnail_filename):
+
+ """Called by mainapp.TartubeApp.update_video_when_file_found(),
+ downloads.VideoDownloader.confirm_sim_video() and
+ downloads.JSONFetcher.do_fetch().
+
+ Adapted from youtube-dl/youtube-dl/postprocessor/embedthumbnail.py.
+
+ In June 2020, YouTube changed its image format from .jpg to .webp.
+ Unfortunately, the Gtk library doesn't support that format.
+
+ Worse still, YouTube also began sending .webp thumbnails mislabelled as
+ .jpg.
+
+ In response, in September 2020 youtube-dl implemented a fix for
+ embedded thumbnails, using FFmpeg to convert .webp to .jpg. That code
+ has been adapted here, so that YouTube thumbnails can be converted and
+ made visible in the main window again.
+
+ Args:
+
+ thumbnail_filename (str): Full path to the webp file to be
+ converted to jpg
+
+ Returns:
+
+ False if an attempted conversion fails, or True otherwise
+ (including when no conversion is attempted)
+
+ """
+
+ # Sanity check
+ if not os.path.isfile(thumbnail_filename) \
+ or self.app_obj.ffmpeg_fail_flag:
+ return True
+
+ # Correct extension for .webp files with the wrong extension
+ # (youtube-dl #25687, #25717)
+ _, thumbnail_ext = os.path.splitext(thumbnail_filename)
+ if thumbnail_ext:
+
+ # Remove the initial full stop
+ thumbnail_ext = thumbnail_ext[1:].lower()
+
+ if thumbnail_ext != 'webp' and self.is_webp(thumbnail_filename):
+
+ # .webp mislabelled as .jpg
+ thumbnail_webp_filename = self.replace_extension(
+ thumbnail_filename,
+ 'webp',
+ )
+
+ os.rename(thumbnail_filename, thumbnail_webp_filename)
+ thumbnail_filename = thumbnail_webp_filename
+ thumbnail_ext = 'webp'
+
+ # Convert unsupported thumbnail formats to JPEG
+ # (youtube-dl #25687, #25717)
+ if thumbnail_ext not in ['jpg', 'png']:
+
+ # NB: % is supposed to be escaped with %% but this does not work
+ # for input files so working around with standard substitution
+ escaped_thumbnail_filename = thumbnail_filename.replace('%', '#')
+ os.rename(thumbnail_filename, escaped_thumbnail_filename)
+ escaped_thumbnail_jpg_filename = self.replace_extension(
+ escaped_thumbnail_filename,
+ 'jpg',
+ )
+
+ # Run FFmpeg, which eturns a list in the form
+ # (success_flag, optional_message)
+ result_list = self.run_ffmpeg(
+ escaped_thumbnail_filename,
+ escaped_thumbnail_jpg_filename,
+ ['-bsf:v', 'mjpeg2jpeg'],
+ )
+
+ if not result_list or not result_list[0]:
+
+ # Conversion failed; most likely because FFmpeg is not
+ # installed
+ # Rename back to unescaped
+ os.rename(escaped_thumbnail_filename, thumbnail_filename)
+
+ return False
+
+ else:
+
+ # Conversion succeeded
+ os.remove(escaped_thumbnail_filename)
+ thumbnail_jpg_filename = self.replace_extension(
+ thumbnail_filename,
+ 'jpg',
+ )
+
+ # Rename back to unescaped for further processing
+ os.rename(
+ escaped_thumbnail_jpg_filename,
+ thumbnail_jpg_filename
+ )
+
+ # Procedure complete
+ return True
+
+
+ def _ffmpeg_filename_argument(self, path):
+
+ """Called by self.run_ffmpeg_multiple_files().
+
+ Adapted from youtube-dl/youtube-dl/postprocessor/ffmpeg.py.
+
+ Returns a filename in a format that won't confuse FFmpeg.
+
+ Args:
+
+ path (str): The full path to a file to be processed by FFmpeg
+
+ Returns:
+
+ The modified string
+
+ """
+
+ # Always use 'file:' because the filename may contain ':' (ffmpeg
+ # interprets that as a protocol) or can start with '-' (-- is broken
+ # in ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for
+ # details)
+ # Also leave '-' intact in order not to break streaming to stdout
+ return 'file:' + path if path != '-' else path
+
+
+ def get_executable(self):
+
+ """Called by self.run_ffmpeg_multiple_files().
+
+ Not adapted from youtube-dl.
+
+ Returns the path to the FFmpeg executable, which the user may have
+ specified themselves. If not, assume ffmpeg is in the system path.
+
+ Returns:
+
+ The path to the executable
+
+ """
+
+ if self.app_obj.ffmpeg_path:
+ return self.app_obj.ffmpeg_path
+ else:
+ return 'ffmpeg'
+
+
+ def is_webp(self, path):
+
+ """Called by self.convert_webp() and utils.find_thumbnail_webp().
+
+ Adapted from youtube-dl/youtube-dl/postprocessor/embedthumbnail.py.
+
+ Tests whether a file is a .webp file (perhaps mislabelled as a .jpg
+ file).
+
+ Args:
+
+ path (str): The full path to a file to be processed by FFmpeg
+
+ """
+
+ with open(path, 'rb') as fh:
+ data = fh.read(12)
+
+ return data[0:4] == b'RIFF' and data[8:] == b'WEBP'
+
+
+ def replace_extension(self, path, ext, expected_real_ext=None):
+
+ """Called by self.convert_webp().
+
+ Adapted from youtube-dl/youtube-dl/utils.py.
+
+ Given the full path to a file, replaces the extension, and returns the
+ modified path.
+
+ Args:
+
+ path (str): The full path to a file
+
+ ext (str): The new file extension
+
+ expected_real_ext (str): Not used by Tartube
+
+ Returns:
+
+ The modified path
+
+ """
+
+ name, real_ext = os.path.splitext(path)
+
+ return '{0}.{1}'.format(
+ name if not expected_real_ext \
+ or real_ext[1:] == expected_real_ext \
+ else path,
+ ext,
+ )
+
+
+ def run_ffmpeg(self, input_path, out_path, opt_list, test_flag=False):
+
+ """Can be called by anything.
+
+ Currently called by self.convert_webp() and
+ process.ProcessManager.process_video().
+
+ Adapted from youtube-dl/youtube-dl/postprocessor/ffmpeg.py.
+
+ self.run_ffmpeg_multiple_files() expects a list of files. Pass on
+ this function's parameters in the expected format.
+
+ Args:
+
+ input_path (str): Full path to a file to be processed by FFmpeg
+
+ out_path (str): Full path to FFmpeg's output file
+
+ opt_list (list): List of FFmpeg command line options (may be an
+ empty list)
+
+ test_flag (bool): If True, just returns the FFmpeg system command,
+ rather than executing it
+
+ """
+
+ return self.run_ffmpeg_multiple_files(
+ [ input_path ],
+ out_path,
+ opt_list,
+ test_flag,
+ )
+
+
+ def run_ffmpeg_multiple_files(self, input_path_list, out_path, opt_list, \
+ test_flag=False):
+
+ """Can be called by anything.
+
+ Currently called by self.run_ffmpeg().
+
+ Adapted from youtube-dl/youtube-dl/postprocessor/ffmpeg.py.
+
+ Prepares the FFmpeg system command, and then executes it.
+
+ Args:
+
+ input_path_list (list): List of full paths to files to be
+ processed by FFmpeg. At the moment, Tartube only processes one
+ file at a time
+
+ out_path (str): Full path to FFmpeg's output file
+
+ opt_list (list): List of FFmpeg command line options (may be an
+ empty list)
+
+ test_flag (bool): If True, just returns the FFmpeg system command,
+ rather than executing it
+
+ Return values:
+
+ Returns a list of two items, in the form
+ (success_flag, optional_message)
+
+ """
+
+ # Get the modification time for the oldest file
+ oldest_mtime = min(os.stat(path).st_mtime for path in input_path_list)
+
+ # Prepare the system command
+ files_cmd_list = []
+ for path in input_path_list:
+ files_cmd_list.extend(['-i', self._ffmpeg_filename_argument(path)])
+
+ cmd_list = [self.get_executable(), '-y']
+ cmd_list += ['-loglevel', 'repeat+info']
+ cmd_list += (
+ files_cmd_list
+ + opt_list
+ + [self._ffmpeg_filename_argument(out_path)]
+ )
+
+ # Return the system command only, if required
+ if test_flag:
+ return [ True, cmd_list ]
+
+ # Execute the system command in a subprocess
+ try:
+ p = subprocess.Popen(
+ cmd_list,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ )
+
+ except:
+ # If FFmpeg is not installed on the user's system, this is the
+ # result
+ return [ False, 'Could not find FFmpeg' ]
+
+ stdout, stderr = p.communicate()
+ if p.returncode != 0:
+ stderr = stderr.decode('utf-8', 'replace')
+ return [ False, stderr.strip().split('\n')[-1] ]
+
+ else:
+ return [ self.try_utime(out_path, oldest_mtime, oldest_mtime), '' ]
+
+
+ def try_utime(self, path, atime, mtime):
+
+ """Called by self.run_ffmpeg_multiple_files().
+
+ Adapted from youtube-dl/youtube-dl/postprocessor/common.py.
+
+ Return values:
+
+ True on success, False on failure
+
+ """
+
+ try:
+ os.utime(path, (atime, mtime))
+ return True
+
+ except Exception:
+ return False
+
diff --git a/tartube/files.py b/tartube/files.py
index 5511223..c7047cc 100644
--- a/tartube/files.py
+++ b/tartube/files.py
@@ -136,7 +136,8 @@ class FileManager(threading.Thread):
Returns:
- A GdkPixbuf, or None if the file is missing or can't be loaded
+ A GdkPixbuf (as a tuple), or None if the file is missing or can't
+ be loaded
"""
@@ -144,6 +145,7 @@ class FileManager(threading.Thread):
return None
try:
+ # (Returns a tuple)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(full_path)
except:
return None
diff --git a/tartube/formats.py b/tartube/formats.py
index a4af232..b8de0aa 100644
--- a/tartube/formats.py
+++ b/tartube/formats.py
@@ -344,6 +344,8 @@ while audio_setup_list:
# (Used for detecting video thumbnails. Unfortunately Gtk can't display .webp
# files yet)
IMAGE_FORMAT_LIST = ['.jpg', '.png', '.gif']
+# (The same list including .webp, for any code that needs it)
+IMAGE_FORMAT_EXT_LIST = ['.jpg', '.png', '.gif', '.webp']
FILE_SIZE_UNIT_LIST = [
['Bytes', ''],
@@ -580,6 +582,7 @@ if not xmas_flag:
'info_icon': 'status_info_icon_64.png',
'tidy_icon': 'status_tidy_icon_64.png',
'livestream_icon': 'status_livestream_icon_64.png',
+ 'process_icon': 'status_process_icon_64.png',
}
else:
STATUS_ICON_DICT = {
@@ -591,6 +594,7 @@ else:
'info_icon': 'status_info_icon_xmas_64.png',
'tidy_icon': 'status_tidy_icon_xmas_64.png',
'livestream_icon': 'status_livestream_icon_xmas_64.png',
+ 'process_icon': 'status_process_icon_xmas_64.png',
}
TOOLBAR_ICON_DICT = {
@@ -610,8 +614,6 @@ TOOLBAR_ICON_DICT = {
'tool_stop_small': 'stop_small.png',
'tool_switch_large': 'switch_large.png',
'tool_switch_small': 'switch_small.png',
- 'tool_test_large': 'test_large.png',
- 'tool_test_small': 'test_small.png',
'tool_video_large': 'video_large.png',
'tool_video_small': 'video_small.png',
}
diff --git a/tartube/info.py b/tartube/info.py
index df9862a..63f6c3d 100644
--- a/tartube/info.py
+++ b/tartube/info.py
@@ -42,10 +42,6 @@ import utils
from mainapp import _
-# Debugging flag (calls utils.debug_time at the start of every function)
-DEBUG_FUNC_FLAG = False
-
-
# Classes
@@ -98,9 +94,6 @@ class InfoManager(threading.Thread):
def __init__(self, app_obj, info_type, media_data_obj, url_string,
options_string):
- if DEBUG_FUNC_FLAG:
- utils.debug_time('iop 102 __init__')
-
super(InfoManager, self).__init__()
# IV list - class objects
@@ -176,14 +169,11 @@ class InfoManager(threading.Thread):
application with the result of the process (success or failure).
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('iop 180 run')
-
# Show information about the info operation in the Output Tab
if self.info_type == 'test_ytdl':
msg = _(
- 'Starting info operation, testing youtube-dl with specified' \
+ 'Starting info operation, testing downloader with specified' \
+ ' options',
)
@@ -206,7 +196,7 @@ class InfoManager(threading.Thread):
self.app_obj.main_win_obj.output_tab_write_stdout(1, msg)
# Convert a path beginning with ~ (not on MS Windows)
- ytdl_path = self.app_obj.ytdl_path
+ ytdl_path = self.app_obj.check_downloader(self.app_obj.ytdl_path)
if os.name != 'nt':
ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path)
@@ -340,7 +330,7 @@ class InfoManager(threading.Thread):
# situations)
if self.child_process is None:
- msg = _('youtube-dl process did not start')
+ msg = _('System process did not start')
self.stderr_list.append(msg)
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
@@ -391,9 +381,6 @@ class InfoManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('iop 395 create_child_process')
-
info = preexec = None
if os.name == 'nt':
@@ -436,9 +423,6 @@ class InfoManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('iop 440 is_child_process_alive')
-
if self.child_process is None:
return False
@@ -455,9 +439,6 @@ class InfoManager(threading.Thread):
Terminates the child process.
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('iop 459 stop_info_operation')
-
if self.is_child_process_alive():
if os.name == 'nt':
diff --git a/tartube/mainapp.py b/tartube/mainapp.py
index bc82af7..ef084e3 100644
--- a/tartube/mainapp.py
+++ b/tartube/mainapp.py
@@ -88,12 +88,14 @@ import __main__
import config
import dialogue
import downloads
+import ffmpeg
import files
import formats
import info
import mainwin
import media
import options
+import process
import refresh
import testing
import tidy
@@ -158,9 +160,9 @@ class TartubeApp(Gtk.Application):
# In the main window's menu, show a menu item for adding a set of
# media data objects for testing
self.debug_test_media_menu_flag = False
- # In the main window's toolbar, show a toolbar item for adding a set of
- # media data objects for testing
- self.debug_test_media_toolbar_flag = False
+ # In the main window's menu, show a menu item for executing some
+ # arbitrary test code (by calling testing.run_test_code())
+ self.debug_test_code_menu_flag = False
# Open the main window in the top-left corner of the desktop
self.debug_open_top_left_flag = False
# Automatically open the system preferences window on startup
@@ -181,7 +183,7 @@ class TartubeApp(Gtk.Application):
# The existence of the fake main window, which is always invisible,
# allows that code to create modal Gtk dialogue windows
self.fake_main_win_obj = None
- # The system tray icon (a mainapp.StatusIcon object, inheriting from
+ # The system tray icon (a mainwin.StatusIcon object, inheriting from
# Gtk.StatusIcon)
self.status_icon_obj = None
#
@@ -226,6 +228,11 @@ class TartubeApp(Gtk.Application):
# object. It checks media.Video objects marked as livestreams, to
# see whether have started or stopped broadcasting
self.livestream_manager_obj = None
+ # A process operation is handled by a process.ProcessManager object. It
+ # sends a list of media.Video objects to FFmpeg for processing
+ # The current process.ProcessManager object, if a process operation is
+ # in progress (or None, if not)
+ self.process_manager_obj = None
#
# When an operation is in progress, the manager object is stored here
# (so code can quickly check if an operation is in progress, or not)
@@ -237,6 +244,9 @@ class TartubeApp(Gtk.Application):
# The file manager, files.FileManager, for loading thumbnail, icon
# and JSON files safely (i.e. without causing a Gtk crash)
self.file_manager_obj = files.FileManager()
+ # The FFmpeg manager, for when Tartube needs to call FFmpeg directly.
+ # Most of the code has been adapted from youtube-dl
+ self.ffmpeg_manager_obj = ffmpeg.FFmpegManager(self)
# The message dialogue manager, dialogue.DialogueManager, for showing
# message dialogue windows safely (i.e. without causing a Gtk crash)
self.dialogue_manager_obj = None
@@ -277,6 +287,7 @@ class TartubeApp(Gtk.Application):
self.main_win_width = 800
self.main_win_height = 600
self.config_win_width = 650
+ self.config_win_width = 650
self.config_win_height = 450
self.paned_min_size = 200
# Default size (in pixels) of space between various widgets
@@ -519,6 +530,26 @@ class TartubeApp(Gtk.Application):
),
)
+ # When the user tries to switch databases (in a call to
+ # self.switch_db() ), we make backup copies of those IVs. If the
+ # switch fails, then their values can be restored, and the user can
+ # continue using the old database as normal
+ self.backup_data_dir = None
+ self.backup_downloads_dir = None
+ self.backup_alt_downloads_dir = None
+ self.backup_backup_dir = None
+ self.backup_temp_dir = None
+ self.backup_temp_dl_dir = None
+ self.backup_temp_test_dir = None
+ self.backup_data_dir_alt_list = None
+
+ # The user can opt to move thumbnails to a '.thumbs' sub-directory, and
+ # other metadata files to a '.data' sub-directory (by setting the
+ # download options 'move_description', etc)
+ # The names of those sub-directories
+ self.thumbs_sub_dir = '.thumbs'
+ self.metadata_sub_dir = '.data'
+
# The directory in which sound files are found, set in the call to
# self.find_sound_effects()
self.sound_dir = None
@@ -631,12 +662,12 @@ class TartubeApp(Gtk.Application):
# update operation. Initially given the same value as
# self.ytdl_path_default
# On MSWin, this value doesn't change. On Linux, depending on how
- # youtube-dl was installed, it might be '/usr/bin/youtube-dl' or just
- # 'youtube-dl'
+ # youtube-dl was installed, it might be '/usr/bin/youtube-dl',
+ # '~/.local/bin/youtube-dl' or just 'youtube-dl'
self.ytdl_path = None
# The shell command to use during an update operation depends on how
# youtube-dl was installed
- # Depending on the operatin system, Tartube provides some of these
+ # Depending on the operating system, Tartube provides some of these
# methods (listed here with the description visible to the user):
#
# 'ytdl_update_default_path'
@@ -673,6 +704,20 @@ class TartubeApp(Gtk.Application):
# self.ytdl_update_dict, set by self.start()
self.ytdl_update_current = None
+ # Flag set to True if the Output tab should be revealed automatically
+ # during an update operation
+ self.auto_switch_output_flag = True
+ # Flag set to True if an update operation has succeeded at least once
+ # (the first time, we try to auto-detect youtube-dl's location)
+ self.ytdl_update_once_flag = False
+ # If specified the name of a youtube-dl fork to use, instead of the
+ # original youtube-dl. When specified, all system commands replace
+ # youtube-dl with this value
+ # If not specified, the value should be None (not an empty string)
+ # (Tartube assumes that the fork is largely compatible with the
+ # original)
+ self.ytdl_fork = None
+
# Flag set to True if youtube-dl system commands should be displayed in
# the Output Tab
self.ytdl_output_system_cmd_flag = True
@@ -737,11 +782,61 @@ class TartubeApp(Gtk.Application):
# If 0, the moviepy procedure is allowed to hang indefinitely
self.refresh_moviepy_timeout = 10
- # Path to the ffmpeg/avconv binary (or the directory containing the
- # binary). If set to any value besides None,
- # downloads.VideoDownloader will pass the value to youtube-dl using
- # its --ffmpeg-location option
+ # Paths to the post-processor binaries. If neither is set, we assume
+ # that FFmpeg and AVConv are in the user's path. If one is set to any
+ # value besides None, it is passed to youtube-dl. If both are set,
+ # then one of them is passed to youtube-dl: AVConv if the download
+ # option 'prefer_avconv' applies, FFmpeg if not
+ # None of these values are used on MS Windows
+ # Default path to the FFmpeg binary
+ self.default_ffmpeg_path = '/usr/bin/ffmpeg'
+ # Path to the FFmpeg binary
self.ffmpeg_path = None
+ # Default path to the AVConv binary
+ self.default_avconv_path = '/usr/local/bin/avconv'
+ # Path to the AVConv binary
+ self.avconv_path = None
+ # Flag set to True when a call to FFmpegManager.convert_webp() fails,
+ # indicating that Ffmpeg is not installed on the user's system
+ # When True, the code will not attempt to convert any more .webp
+ # thumbnails to .jpg (until the user restarts Tartube)
+ self.ffmpeg_fail_flag = False
+ # The system error message to display when failure occurs (used
+ # several times, so defined here)
+ self.ffmpeg_fail_msg = _(
+ 'Failed to convert a thumbnail from .webp to .jpg. No more' \
+ + ' conversions will be attempted until you install FFmpeg on' \
+ + ' your system, or (if FFmpeg is already installed) you set the' \
+ + ' correct FFmpeg path. To attempt more conversions, restart' \
+ + ' Tartube. To stop these messages, disable thumbnail' \
+ + ' conversions',
+ )
+ # Flag set to True if Tartube should attempt to convert .webp
+ # thumbnails (from YouTube), which can't be displayed in the main
+ # window, into .jpg thumbnails, which can be displayed
+ # Ignored if self.ffmpeg_fail_flag is True
+ self.ffmpeg_convert_webp_flag = True
+
+ # IVs used to set the options and the output file, when the user passes
+ # video(s) directly to FFmpeg (e.g. by right-clicking the video, and
+ # selecting 'Process with FFmpeg...). The IVs are applied in this
+ # order
+ # Text to add to the end of every filename
+ self.ffmpeg_add_string = ''
+ # Regex and substitution to apply to every filename
+ self.ffmpeg_regex_string = ''
+ self.ffmpeg_substitute_string = ''
+ # New file extension (specify this to convert, e.g. webm to avi)
+ self.ffmpeg_ext_string = ''
+ # A string of FFmpeg options (split into a list, before they are used)
+ self.ffmpeg_option_string = ''
+ # Flag set to True if the old video file should be deleted, if the
+ # FFmpeg output file has a different name (e.g. if the file
+ # extension has changed)
+ self.ffmpeg_delete_flag = False
+ # Flag set to True if these IVs should be set, whenever the
+ # mainwin.ProcessDialogue window is used
+ self.ffmpeg_keep_flag = False
# Flag set to True if the General Options Manager
# (self.general_options_obj) should be cloned whenever the user
@@ -763,7 +858,7 @@ class TartubeApp(Gtk.Application):
# At the end of the download operation, the timer continues running for
# a few seconds, to give new files a chance to appear in the
# filesystem. The maximum time to wait (in seconds)
- self.dl_timer_final_time = 10
+ self.dl_timer_final_time = 5
# Once that extra time has been applied, the time (matches time.time())
# at which to stop waiting
self.dl_timer_check_time = None
@@ -802,10 +897,16 @@ class TartubeApp(Gtk.Application):
# above (in Mb)
self.disk_space_abs_limit = 50
+ # Default invidio.us mirror to use (the original site closed in
+ # September 2020); this value never changes
+ self.default_invidious_mirror = 'invidious.site'
+ # Custom mirror to use (can be set by the user)
+ self.custom_invidious_mirror = self.default_invidious_mirror
+
# Custom download operation settings
# If True, during a custom download, download every video which is
- # marked as not downloaded (often after a 'Check all' operation);
- # don't download channels/playlists directly
+ # marked as not downloaded (often after clicking the 'Check all'
+ # button); don't download channels/playlists directly
self.custom_dl_by_video_flag = False
# During a custom download, any videos whose source URL is YouTube can
# be diverted to another website
@@ -847,7 +948,7 @@ class TartubeApp(Gtk.Application):
# a few seconds, to prevent various Gtk errors (and occasionally
# crashes) for systems with Gtk < 3.24. The maximum time to wait (in
# seconds)
- self.update_timer_final_time = 5
+ self.update_timer_final_time = 3
# Once that extra time has been applied, the time (matches time.time())
# at which to stop waiting
self.update_timer_check_time = None
@@ -862,7 +963,7 @@ class TartubeApp(Gtk.Application):
# a few seconds, to prevent various Gtk errors (and occasionally
# crashes) for systems with Gtk < 3.24. The maximum time to wait (in
# seconds)
- self.refresh_timer_final_time = 5
+ self.refresh_timer_final_time = 2
# Once that extra time has been applied, the time (matches time.time())
# at which to stop waiting
self.refresh_timer_check_time = None
@@ -901,6 +1002,20 @@ class TartubeApp(Gtk.Application):
# at which to stop waiting
self.tidy_timer_check_time = None
+ # During a process operation, a separate GObject timer runs, so that
+ # the Output Tab can be updated at regular intervals
+ # The timer's ID (None when no timer is running)
+ self.process_timer_id = None
+ # The timer interval time (in milliseconds)
+ self.process_timer_time = 500
+ # At the end of most operations, the timer continues running for a few
+ # seconds, to prevent various Gtk errors. There are no such issues
+ # with a process operation, so the wait time is only 1
+ self.process_timer_final_time = 1
+ # Once that extra time has been applied, the time (matches time.time())
+ # at which to stop waiting
+ self.process_timer_check_time = None
+
# During any operation (except livestream operations), a flag set to
# True if the operation was halted by the user, rather than being
# allowed to complete naturally
@@ -1076,25 +1191,6 @@ class TartubeApp(Gtk.Application):
# self.announce_video_download()
self.watch_after_dl_list = []
- # Automatic 'Download all' download operations - 'none' to disable,
- # 'start' to perform the operation whenever Tartube starts, or
- # 'scheduled' to perform the operation at regular intervals
- self.scheduled_dl_mode = 'none'
- # The time between 'scheduled' 'Download all' operations, if enabled
- self.scheduled_dl_wait_value = 2
- # ...using this unit (any of the values in formats.TIME_METRIC_LIST)
- self.scheduled_dl_wait_unit = 'hours'
- # The time (system time, in seconds) at which the last 'Download all'
- # operation started (regardless of whether it was 'scheduled' or not)
- self.scheduled_dl_last_time = 0
- # If self.scheduled_dl_mode is 'start', on startup we wait a few
- # seconds (for aesthetic reasons). The number of seconds to wait
- self.scheduled_dl_start_wait_time = 3
- # The time (system time, in seconds) at which the scheduled download
- # operation should start (if no other operation has started in the
- # meantime)
- self.scheduled_dl_start_check_time = None
-
# Automatic 'Check all' download operations - 'none' to disable,
# 'start' to perform the operation whenever Tartube starts, or
# 'scheduled' to perform the operation at regular intervals
@@ -1114,9 +1210,49 @@ class TartubeApp(Gtk.Application):
# meantime)
self.scheduled_check_start_check_time = None
- # Flag set to True if Tartube should shut down after a 'Download all'
- # operation (if self.scheduled_dl_mode is not 'none'), and after a
- # 'Check all' operation (if self.scheduled_check_mode is not 'none')
+ # Automatic 'Download all' download operations - 'none' to disable,
+ # 'start' to perform the operation whenever Tartube starts, or
+ # 'scheduled' to perform the operation at regular intervals
+ self.scheduled_dl_mode = 'none'
+ # The time between 'scheduled' 'Download all' operations, if enabled
+ self.scheduled_dl_wait_value = 2
+ # ...using this unit (any of the values in formats.TIME_METRIC_LIST)
+ self.scheduled_dl_wait_unit = 'hours'
+ # The time (system time, in seconds) at which the last 'Download all'
+ # operation started (regardless of whether it was 'scheduled' or not)
+ self.scheduled_dl_last_time = 0
+ # If self.scheduled_dl_mode is 'start', on startup we wait a few
+ # seconds (for aesthetic reasons). The number of seconds to wait
+ self.scheduled_dl_start_wait_time = 3
+ # The time (system time, in seconds) at which the scheduled download
+ # operation should start (if no other operation has started in the
+ # meantime)
+ self.scheduled_dl_start_check_time = None
+
+ # Automatic custom 'Download all' operations - 'none' to disable,
+ # 'start' to perform the operation whenever Tartube starts, or
+ # 'scheduled' to perform the operation at regular intervals
+ self.scheduled_custom_mode = 'none'
+ # The time between 'scheduled' 'Download all' operations, if enabled
+ self.scheduled_custom_wait_value = 2
+ # ...using this unit (any of the values in formats.TIME_METRIC_LIST)
+ self.scheduled_custom_wait_unit = 'hours'
+ # The time (system time, in seconds) at which the last 'Download all'
+ # operation started (regardless of whether it was 'scheduled' or not)
+ self.scheduled_custom_last_time = 0
+ # If self.scheduled_custom_mode is 'start', on startup we wait a few
+ # seconds (for aesthetic reasons). The number of seconds to wait
+ self.scheduled_custom_start_wait_time = 3
+ # The time (system time, in seconds) at which the scheduled download
+ # operation should start (if no other operation has started in the
+ # meantime)
+ self.scheduled_custom_start_check_time = None
+
+ # Flag set to True if Tartube should shut down after a 'Check all'
+ # operation (if self.scheduled_check_mode is not 'none'), after a
+ # 'Download all' operation (if self.scheduled_dl_mode is not 'none'),
+ # and after a custom 'Download all' operation (if
+ # self.scheduled_custom_mode is not 'none')
self.scheduled_shutdown_flag = False
# Flag set to True if Tartube should try to detect livestreams (on
@@ -1197,7 +1333,7 @@ class TartubeApp(Gtk.Application):
# Flag set to True if a download operation should auto-stop after
# downloading videos of a certain combined size (applies to real
# downloads only; the specified size is approximate, because it
- # relies on th video size reported by youtube-dl, and doesn't take
+ # relies on the video size reported by youtube-dl, and doesn't take
# account of thumbnails, JSON data, and so on)
self.autostop_size_flag = False
# Auto-stop after this amount of diskspace (minimum value 1)...
@@ -1253,6 +1389,10 @@ class TartubeApp(Gtk.Application):
# contained in a channel or playlist, all modes to default to
# 'disable'
self.operation_convert_mode = 'channel'
+ # Flag set to True on MS Windows (only), if the user should be prompted
+ # to install FFmpeg after a successful update operation (to install
+ # youtube-dl)
+ self.prompt_ffmpeg_flag = False
# Flag set to True if self.update_video_from_filesystem() should get
# the video duration, if not already known, using the moviepy.editor
# module (an optional dependency)
@@ -1574,6 +1714,14 @@ class TartubeApp(Gtk.Application):
test_menu_action.connect('activate', self.on_menu_test)
self.add_action(test_menu_action)
+ if self.debug_test_code_menu_flag:
+ test_code_menu_action = Gio.SimpleAction.new(
+ 'test_code_menu',
+ None,
+ )
+ test_code_menu_action.connect('activate', self.on_menu_test_code)
+ self.add_action(test_code_menu_action)
+
# 'Operations' column
check_all_menu_action = Gio.SimpleAction.new('check_all_menu', None)
check_all_menu_action.connect(
@@ -1759,11 +1907,6 @@ class TartubeApp(Gtk.Application):
)
self.add_action(switch_view_button_action)
- if self.debug_test_media_toolbar_flag:
- test_button_action = Gio.SimpleAction.new('test_toolbutton', None)
- test_button_action.connect('activate', self.on_menu_test)
- self.add_action(test_button_action)
-
quit_button_action = Gio.SimpleAction.new('quit_toolbutton', None)
quit_button_action.connect('activate', self.on_menu_quit)
self.add_action(quit_button_action)
@@ -2087,9 +2230,11 @@ class TartubeApp(Gtk.Application):
GObject.source_remove(self.info_timer_id)
if self.tidy_timer_id:
GObject.source_remove(self.tidy_timer_id)
+ if self.process_timer_id:
+ GObject.source_remove(self.process_timer_id)
- # Don't prompt the user before halting a download/update/refresh/info/
- # tidy operation, as we would do in calls to self.stop()
+ # Don't prompt the user before halting an operation, as we would do in
+ # calls to self.stop()
if self.download_manager_obj:
self.download_manager_obj.stop_download_operation()
elif self.update_manager_obj:
@@ -2100,6 +2245,8 @@ class TartubeApp(Gtk.Application):
self.info_manager_obj.stop_info_operation()
elif self.tidy_manager_obj:
self.tidy_manager_obj.stop_tidy_operation()
+ elif self.process_manager_obj:
+ self.process_manager_obj.stop_process_operation()
# If there is a lock on the database file, release it
self.remove_db_lock_file()
@@ -2111,11 +2258,8 @@ class TartubeApp(Gtk.Application):
# Stop immediately
Gtk.Application.do_shutdown(self)
- if os.name == 'nt':
- # Under MS Windows, all methods of shutting down after an update
- # operation fail - except this method
- os._exit(0)
-
+ # After an update operation, only this method might work
+ os._exit(0)
# Still here? Do a brute-force exit
exit()
@@ -2137,113 +2281,7 @@ class TartubeApp(Gtk.Application):
# ---------------------------------------------------------
# Set youtube-dl path IVs
- if os.name == 'nt':
-
- if 'PROGRAMFILES(X86)' in os.environ:
- # 64-bit MS Windows
- recommended = 'ytdl_update_win_64'
- python_path = '..\\..\\..\\mingw64\\bin\python3.exe'
- pip_path = '..\\..\\..\\mingw64\\bin\pip3-script.py'
- else:
- # 32-bit MS Windows
- recommended = 'ytdl_update_win_32'
- python_path = '..\\..\\..\\mingw32\\bin\python3.exe'
- pip_path = '..\\..\\..\\mingw32\\bin\pip3-script.py'
-
- self.ytdl_bin = 'youtube-dl'
- self.ytdl_path_default = 'youtube-dl'
- self.ytdl_path = 'youtube-dl'
- self.ytdl_update_dict = {
- recommended: [
- python_path,
- pip_path,
- 'install',
- '--upgrade',
- 'youtube-dl',
- ],
- 'ytdl_update_pip3': [
- 'pip3', 'install', '--upgrade', 'youtube-dl',
- ],
- 'ytdl_update_pip': [
- 'pip', 'install', '--upgrade', 'youtube-dl',
- ],
- 'ytdl_update_default_path': [
- self.ytdl_path_default, '-U',
- ],
- 'ytdl_update_local_path': [
- 'youtube-dl', '-U',
- ],
- }
- self.ytdl_update_list = [
- recommended,
- 'ytdl_update_pip3',
- 'ytdl_update_pip',
- 'ytdl_update_default_path',
- 'ytdl_update_local_path',
- ]
- self.ytdl_update_current = recommended
-
- elif __main__.__pkg_strict_install_flag__:
-
- self.ytdl_bin = 'youtube-dl'
- self.ytdl_path_default = os.path.abspath(
- os.path.join(os.sep, 'usr', 'bin', self.ytdl_bin),
- )
- self.ytdl_path = self.ytdl_path_pypi
-
- self.ytdl_update_dict = {
- 'ytdl_update_disabled': [],
- }
- self.ytdl_update_list = [
- 'ytdl_update_disabled',
- ]
- self.ytdl_update_current = 'ytdl_update_disabled'
-
- else:
-
- self.ytdl_bin = 'youtube-dl'
- self.ytdl_path_default = os.path.abspath(
- os.path.join(os.sep, 'usr', 'bin', self.ytdl_bin),
- )
-
- if __main__.__pkg_install_flag__:
- self.ytdl_path = self.ytdl_path_pypi
- else:
- self.ytdl_path = 'youtube-dl'
-
- self.ytdl_update_dict = {
- 'ytdl_update_pip3_recommend': [
- 'pip3', 'install', '--upgrade', '--user', 'youtube-dl',
- ],
- 'ytdl_update_pip3_omit_user': [
- 'pip3', 'install', '--upgrade', 'youtube-dl',
- ],
- 'ytdl_update_pip': [
- 'pip', 'install', '--upgrade', '--user', 'youtube-dl',
- ],
- 'ytdl_update_pip_omit_user': [
- 'pip', 'install', '--upgrade', 'youtube-dl',
- ],
- 'ytdl_update_default_path': [
- self.ytdl_path_default, '-U',
- ],
- 'ytdl_update_local_path': [
- 'youtube-dl', '-U',
- ],
- 'ytdl_update_pypi_path': [
- self.ytdl_path_pypi, '-U',
- ],
- }
- self.ytdl_update_list = [
- 'ytdl_update_pip3_recommend',
- 'ytdl_update_pip3_omit_user',
- 'ytdl_update_pip',
- 'ytdl_update_pip_omit_user',
- 'ytdl_update_default_path',
- 'ytdl_update_local_path',
- 'ytdl_update_pypi_path',
- ]
- self.ytdl_update_current = 'ytdl_update_pip3_recommend'
+ self.setup_paths()
# Set the General Options Manager
self.general_options_obj = options.OptionsManager()
@@ -2480,132 +2518,129 @@ class TartubeApp(Gtk.Application):
self.allow_db_save_flag = True
self.save_db()
- # Part 5 - Warn user about broken Gtk
- # -----------------------------------
-
- # Display a warning about Gtk stability issues, if required
- if self.gtk_emulate_broken_flag:
- self.system_warning(
- 102,
- _(
- 'Tartube is assuming that Gtk v{0}.{1}.{2} is broken;' \
- + ' some minor cosmetic features are disabled',
- ).format(
- str(self.gtk_version_major),
- str(self.gtk_version_minor),
- str(self.gtk_version_micro),
- ),
- )
-
- # Part 6 - Warn user about failed loads
+ # Part 5 - Warn user about failed loads
# -------------------------------------
- # If file load/save has been disabled, we can now show a dialogue
- # window
- if self.disable_load_save_flag:
+ # After a stale lockfile, when the user clicked 'No', just shut down
+ if self.disable_load_save_lock_flag:
- remove_flag = False
+ return self.main_win_obj.destroy()
+
+ # If file load/save has been disabled for any other reason, we can now
+ # show a dialogue window
+ elif self.disable_load_save_flag:
# (If self.show_classic_tab_on_startup_flag, then the Classic Mode
# Tab is visible. This looks weird, so quickly switch back to
# the Videos Tab)
self.main_win_obj.notebook.set_current_page(0)
- if self.disable_load_save_lock_flag:
+ if self.disable_load_save_msg is None:
- dialogue_win = mainwin.RemoveLockFileDialogue(
- self.main_win_obj,
+ self.file_error_dialogue(
+ _(
+ 'Because of an error, file load/save has been disabled',
+ ),
)
- dialogue_win.run()
- remove_flag = dialogue_win.remove_flag
- dialogue_win.destroy()
+ else:
- if remove_flag:
- self.remove_stale_lock_file()
- # (Don't need to display the error messages just below)
- self.disable_load_save_lock_flag = False
-
- self.file_error_dialogue(
- _(
- 'The Tartube database file was not loaded, but is no'\
- + ' longer protected',
- ) + '\n\n' \
- + _('Restart Tartube to load it'),
+ self.file_error_dialogue(
+ self.disable_load_save_msg + '\n\n' \
+ + _(
+ 'Because of the error, file load/save has been disabled',
)
+ )
- if not remove_flag:
-
- if self.disable_load_save_msg is None:
-
- self.file_error_dialogue(
- _(
- 'Because of an error, file load/save has been' \
- + ' disabled',
- ),
- )
-
- else:
-
- self.file_error_dialogue(
- self.disable_load_save_msg + '\n\n' \
- + _(
- 'Because of the error, file load/save has been' \
- + ' disabled',
- )
- )
-
- # Part 7 - Start system timers
+ # Part 6 - Start system timers
# ----------------------------
- # Start the script's GObject slow timer
- self.script_slow_timer_id = GObject.timeout_add(
- self.script_slow_timer_time,
- self.script_slow_timer_callback,
- )
+ if not self.disable_load_save_flag:
- # Start the script's GObject fast timer
- self.script_fast_timer_id = GObject.timeout_add(
- self.script_fast_timer_time,
- self.script_fast_timer_callback,
- )
+ # Start the script's GObject slow timer
+ self.script_slow_timer_id = GObject.timeout_add(
+ self.script_slow_timer_time,
+ self.script_slow_timer_callback,
+ )
- # Part 8 - Automatically start update/download operations, if required
+ # Start the script's GObject fast timer
+ self.script_fast_timer_id = GObject.timeout_add(
+ self.script_fast_timer_time,
+ self.script_fast_timer_callback,
+ )
+
+ # Part 7 - Automatically start update/download operations, if required
# --------------------------------------------------------------------
if not self.disable_load_save_flag:
- # For new installations, MS Windows must be prompted to perform an
- # update operation, which installs youtube-dl on their system
- if new_config_flag and os.name == 'nt':
+ if new_config_flag:
- self.dialogue_manager_obj.show_msg_dialogue(
- _(
- 'youtube-dl must be installed before you can use' \
- + ' Tartube. Do you want to install youtube-dl now?',
- ),
- 'question',
- 'yes-no',
- None, # Parent window is main window
- {
- 'yes': 'update_manager_start',
- # Install youtube-dl, not FFmpeg
- 'data': 'ytdl',
- },
- )
+ # For new installations, MS Windows must be prompted to perform
+ # an update operation, which installs youtube-dl on their
+ # system
+ if os.name == 'nt':
+
+ self.dialogue_manager_obj.show_msg_dialogue(
+ _(
+ 'youtube-dl must be installed before you can use' \
+ + ' Tartube. Do you want to install youtube-dl now?',
+ ),
+ 'question',
+ 'yes-no',
+ None, # Parent window is main window
+ {
+ 'yes': 'update_manager_start',
+ # Install youtube-dl, not FFmpeg
+ 'data': 'ytdl',
+ },
+ )
+
+ # (Prompt to install FFmpeg too, when youtube-dl is
+ # successfully installed)
+ self.prompt_ffmpeg_flag = True
+
+ # For new installations on other operating systems, if the
+ # location of youtube-dl wasn't auto-detected, show a message
+ # about it
+ elif self.ytdl_path == self.ytdl_bin:
+
+ dialogue_win = mainwin.InstallDialogue(self.main_win_obj)
+ dialogue_win.run()
+ dialogue_win.destroy()
+
+ # Otherwise, just show a recommendation about installing FFmpeg
+ elif not os.path.isfile(self.default_ffmpeg_path):
+
+ self.dialogue_manager_obj.show_msg_dialogue(
+ _(
+ 'Without FFmpeg, Tartube cannot download high' \
+ + '-resolution videos. If you have not already' \
+ + ' installed FFmpeg, then we recommend that you' \
+ + ' install it now.',
+ ),
+ 'info',
+ 'ok',
+ None, # Parent window is main window
+ )
# If a download operation (real or simulated) is scheduled to occur
# on startup, then set the time at which
# self.script_fast_timer_callback() should initiate it
+ elif self.scheduled_check_mode == 'start':
+
+ self.scheduled_check_start_check_time \
+ = time.time() + self.scheduled_check_start_wait_time
+
elif self.scheduled_dl_mode == 'start':
self.scheduled_dl_start_check_time \
= time.time() + self.scheduled_dl_start_wait_time
- elif self.scheduled_check_mode == 'start':
+ elif self.scheduled_custom_mode == 'start':
- self.scheduled_check_start_check_time \
- = time.time() + self.scheduled_check_start_wait_time
+ self.scheduled_custom_start_check_time \
+ = time.time() + self.scheduled_custom_start_wait_time
def stop(self):
@@ -2614,7 +2649,7 @@ class TartubeApp(Gtk.Application):
mainwin.MainWin.on_quit_menu_item().
Before terminating the Tartube app, gets confirmation from the user (if
- a download/update/refresh/info/tidy operation is in progress).
+ an operation is in progress).
If no operation is in progress, calls self.stop_continue() to terminate
the app now. Otherwise, self.stop_continue() is only called when the
@@ -2631,8 +2666,7 @@ class TartubeApp(Gtk.Application):
self.livestream_manager_obj.stop_livestream_operation()
self.stop_continue()
- # If a download/update/refresh/info/tidy operation is in progress, get
- # confirmation before stopping
+ # If an operation is in progress, get confirmation before stopping
elif self.current_manager_obj:
if self.download_manager_obj:
@@ -2643,8 +2677,10 @@ class TartubeApp(Gtk.Application):
string = _('There is a refresh operation in progress.')
elif self.info_manager_obj:
string = _('There is an info operation in progress.')
- else:
+ elif self.tidy_manager_obj:
string = _('There is a tidy operation in progress.')
+ elif self.process_manager_obj:
+ string = _('There is a process operation in progress.')
# If the user clicks 'yes', call self.stop_continue() to complete
# the shutdown
@@ -2692,6 +2728,9 @@ class TartubeApp(Gtk.Application):
elif self.tidy_manager_obj:
self.tidy_manager_obj.stop_tidy_operation()
+ elif self.process_manager_obj:
+ self.process_manager_obj.stop_process_operation()
+
# Stop the GObject timers immediately. So this action is not repeated
# in the standard call to self.do_shutdown, reset the IVs
if self.script_slow_timer_id:
@@ -2722,6 +2761,10 @@ class TartubeApp(Gtk.Application):
GObject.source_remove(self.tidy_timer_id)
self.tidy_timer_id = None
+ if self.process_timer_id:
+ GObject.source_remove(self.process_timer_id)
+ self.process_timer_id = None
+
# Empty any temporary folders from the database (if allowed; those
# temporary folders are always deleted when Tartube starts)
# Otherwise, open the temporary folders on the desktop, if allowd
@@ -2762,9 +2805,9 @@ class TartubeApp(Gtk.Application):
Error codes for this function and for self.system_warning are
currently assigned thus:
- 100-199: mainapp.py (in use: 101-163)
+ 100-199: mainapp.py (in use: 101-166)
200-299: mainwin.py (in use: 201-256)
- 300-399: downloads.py (in use: 301-306)
+ 300-399: downloads.py (in use: 301-308)
400-499: config.py (in use: 401-404)
"""
@@ -2815,7 +2858,7 @@ class TartubeApp(Gtk.Application):
Loads the Tartube config file. If loading fails, disables all file
loading/saving.
- Return values:
+ Returns:
True if this appears to be a new Tartube installation, False
otherwise (regardless of whether loading the config file
@@ -3088,6 +3131,15 @@ class TartubeApp(Gtk.Application):
# update IVs were overhauled several times)
self.load_config_ytdl_update(version, json_dict)
+ if version >= 2001086: # v2.1.086:
+ self.auto_switch_output_flag = json_dict['auto_switch_output_flag']
+ if version >= 2001117: # v2.1.117:
+ self.ytdl_update_once_flag = json_dict['ytdl_update_once_flag']
+ else:
+ # Don't auto-detect youtube-dl if this installation is not the
+ # first one (as the user won't be expecting that)
+ self.ytdl_update_once_flag = True
+
if version >= 1003074: # v1.3.074
self.ytdl_output_system_cmd_flag \
= json_dict['ytdl_output_system_cmd_flag']
@@ -3139,6 +3191,9 @@ class TartubeApp(Gtk.Application):
if version >= 1004024: # v1.4.024
self.custom_dl_by_video_flag = json_dict['custom_dl_by_video_flag']
+ if version >= 2001094: # v2.1.094
+ self.custom_invidious_mirror = json_dict['custom_invidious_mirror']
+
if version >= 1004052: # v1.4.052
self.custom_dl_divert_mode = json_dict['custom_dl_divert_mode']
elif version >= 1004024: # v1.4.024
@@ -3154,6 +3209,27 @@ class TartubeApp(Gtk.Application):
if version >= 1001054: # v1.1.054
self.ffmpeg_path = json_dict['ffmpeg_path']
+ if version >= 2001095: # v2.1.095
+ self.avconv_path = json_dict['avconv_path']
+ else:
+ # (Before this version, .ffmpeg_path was used for the avconv binary
+ # too)
+ if re.search(r'avconv', self.ffmpeg_path):
+ self.avconv_path = self.ffmpeg_path
+ self.ffmpeg_path = None
+ if version >= 2001098: # v2.1.098
+ self.ffmpeg_convert_webp_flag \
+ = json_dict['ffmpeg_convert_webp_flag']
+
+ if version >= 2001104: # v2.1.104
+ self.ffmpeg_add_string = json_dict['ffmpeg_add_string']
+ self.ffmpeg_regex_string = json_dict['ffmpeg_regex_string']
+ self.ffmpeg_substitute_string \
+ = json_dict['ffmpeg_substitute_string']
+ self.ffmpeg_ext_string = json_dict['ffmpeg_ext_string']
+ self.ffmpeg_option_string = json_dict['ffmpeg_option_string']
+ self.ffmpeg_delete_flag = json_dict['ffmpeg_delete_flag']
+ self.ffmpeg_keep_flag = json_dict['ffmpeg_keep_flag']
if version >= 3029: # v0.3.029
self.operation_limit_flag = json_dict['operation_limit_flag']
@@ -3195,6 +3271,15 @@ class TartubeApp(Gtk.Application):
self.scheduled_shutdown_flag \
= json_dict['scheduled_shutdown_flag']
+ if version >= 2001110: # v2.1.110
+ self.scheduled_custom_mode = json_dict['scheduled_custom_mode']
+ self.scheduled_custom_wait_value \
+ = json_dict['scheduled_custom_wait_value']
+ self.scheduled_custom_wait_unit \
+ = json_dict['scheduled_custom_wait_unit']
+ self.scheduled_custom_last_time \
+ = json_dict['scheduled_custom_last_time']
+
if version >= 2000037: # v2.0.037
self.enable_livestreams_flag \
= json_dict['enable_livestreams_flag']
@@ -3515,6 +3600,11 @@ class TartubeApp(Gtk.Application):
self.ytdl_update_dict[recommended] = mod_list
+ # (In version v2.1.083, added support for youtube-dl forks)
+ if (version >= 2001083):
+
+ self.ytdl_fork = json_dict['ytdl_fork']
+
def save_config(self):
@@ -3653,6 +3743,10 @@ class TartubeApp(Gtk.Application):
'ytdl_update_list': self.ytdl_update_list,
'ytdl_update_current': self.ytdl_update_current,
+ 'auto_switch_output_flag': self.auto_switch_output_flag,
+ 'ytdl_update_once_flag': self.ytdl_update_once_flag,
+ 'ytdl_fork': self.ytdl_fork,
+
'ytdl_output_system_cmd_flag': self.ytdl_output_system_cmd_flag,
'ytdl_output_stdout_flag': self.ytdl_output_stdout_flag,
'ytdl_output_ignore_json_flag': self.ytdl_output_ignore_json_flag,
@@ -3683,6 +3777,8 @@ class TartubeApp(Gtk.Application):
'disk_space_stop_flag': self.disk_space_stop_flag,
'disk_space_stop_limit': self.disk_space_stop_limit,
+ 'custom_invidious_mirror': self.custom_invidious_mirror,
+
'custom_dl_by_video_flag': self.custom_dl_by_video_flag,
'custom_dl_divert_mode': self.custom_dl_divert_mode,
'custom_dl_divert_website': self.custom_dl_divert_website,
@@ -3691,20 +3787,35 @@ class TartubeApp(Gtk.Application):
'custom_dl_delay_min': self.custom_dl_delay_min,
'ffmpeg_path': self.ffmpeg_path,
+ 'avconv_path': self.avconv_path,
+ 'ffmpeg_convert_webp_flag': self.ffmpeg_convert_webp_flag,
+
+ 'ffmpeg_add_string': self.ffmpeg_add_string,
+ 'ffmpeg_regex_string': self.ffmpeg_regex_string,
+ 'ffmpeg_substitute_string': self.ffmpeg_substitute_string,
+ 'ffmpeg_ext_string': self.ffmpeg_ext_string,
+ 'ffmpeg_option_string': self.ffmpeg_option_string,
+ 'ffmpeg_delete_flag': self.ffmpeg_delete_flag,
+ 'ffmpeg_keep_flag': self.ffmpeg_keep_flag,
'operation_limit_flag': self.operation_limit_flag,
'operation_check_limit': self.operation_check_limit,
'operation_download_limit': self.operation_download_limit,
+ 'scheduled_check_mode': self.scheduled_check_mode,
+ 'scheduled_check_wait_value': self.scheduled_check_wait_value,
+ 'scheduled_check_wait_unit': self.scheduled_check_wait_unit,
+ 'scheduled_check_last_time': self.scheduled_check_last_time,
+
'scheduled_dl_mode': self.scheduled_dl_mode,
'scheduled_dl_wait_value': self.scheduled_dl_wait_value,
'scheduled_dl_wait_unit': self.scheduled_dl_wait_unit,
'scheduled_dl_last_time': self.scheduled_dl_last_time,
- 'scheduled_check_mode': self.scheduled_check_mode,
- 'scheduled_check_wait_value': self.scheduled_check_wait_value,
- 'scheduled_check_wait_unit': self.scheduled_check_wait_unit,
- 'scheduled_check_last_time': self.scheduled_check_last_time,
+ 'scheduled_custom_mode': self.scheduled_custom_mode,
+ 'scheduled_custom_wait_value': self.scheduled_custom_wait_value,
+ 'scheduled_custom_wait_unit': self.scheduled_custom_wait_unit,
+ 'scheduled_custom_last_time': self.scheduled_custom_last_time,
'scheduled_shutdown_flag': self.scheduled_shutdown_flag,
@@ -3871,13 +3982,18 @@ class TartubeApp(Gtk.Application):
os.remove(lock_path)
- def load_db(self):
+ def load_db(self, switch_flag=False):
"""Called by self.start() and .switch_db().
Loads the Tartube database file. If loading fails, disables all file
loading/saving.
+ Args:
+
+ switch_flag (bool): True when called by self.switch_db(), False
+ otherwise
+
Returns:
True on success, False on failure
@@ -3901,24 +4017,29 @@ class TartubeApp(Gtk.Application):
lock_path = path + '.lock'
if os.path.isfile(lock_path):
- # (The True argument signals that the user should be prompted
- # to artificially remove the lockfile)
- self.disable_load_save(
- _('Failed to load the Tartube database file'),
- True,
+ dialogue_win = mainwin.RemoveLockFileDialogue(
+ self.main_win_obj,
+ switch_flag,
)
- return False
+ dialogue_win.run()
+ remove_flag = dialogue_win.remove_flag
+ dialogue_win.destroy()
- else:
+ if remove_flag:
- # Place our own lock on the database file
- try:
- fh = open(lock_path, 'a').close()
- self.db_lock_file_path = lock_path
+ # The user thinks it's safe to ignore the stale lockfile
+ self.remove_stale_lock_file()
- except:
+ elif switch_flag:
+ # Let the calling code show a dialogue window
+ return False
+
+ else:
+
+ # Failed to load database on startup, and therefore
+ # Tartube will shut down
# (The True argument signals that the user should be
# prompted to artificially remove the lockfile)
self.disable_load_save(
@@ -3928,6 +4049,22 @@ class TartubeApp(Gtk.Application):
return False
+ # Place our own lock on the database file
+ try:
+ fh = open(lock_path, 'a').close()
+ self.db_lock_file_path = lock_path
+
+ except:
+
+ # (The True argument signals that the user should be prompted
+ # to artificially remove the lockfile)
+ self.disable_load_save(
+ _('Failed to load the Tartube database file'),
+ True,
+ )
+
+ return False
+
# Reset main window tabs now so the user can't manipulate their widgets
# during the load
# (Don't reset the Erors/Warnings tab, as failed attempts to load a
@@ -4783,6 +4920,15 @@ class TartubeApp(Gtk.Application):
if isinstance(child_obj, media.Video):
child_obj.missing_flag = False
+ if version < 2001089: # v2.1.089
+
+ # This version adds new options to options.OptionsManager
+ for options_obj in options_obj_list:
+ options_obj.options_dict['move_description'] = False
+ options_obj.options_dict['move_info'] = False
+ options_obj.options_dict['move_annotations'] = False
+ options_obj.options_dict['move_thumbnail'] = False
+
def save_db(self):
@@ -5049,9 +5195,10 @@ class TartubeApp(Gtk.Application):
else:
self.remove_db_lock_file()
- # Delete Tartube's temporary folder from the filesystem
- if os.path.isdir(self.temp_dir):
- shutil.rmtree(self.temp_dir)
+ # If the new database file is not loaded for any reason, then we can
+ # restore the values of various IVs. (As far as the user is
+ # concerned, nothing has happened)
+ self.backup_data_variables_before_switch()
# Update IVs for the new location of the data directory
self.data_dir = path
@@ -5097,27 +5244,8 @@ class TartubeApp(Gtk.Application):
if not self.make_directory(self.backup_dir):
return False
- # (The temporary data directory should be emptied, if it already
- # exists)
- if os.path.isdir(self.temp_dir):
- try:
- shutil.rmtree(self.temp_dir)
-
- except:
- if not self.make_directory(self.temp_dir):
- return False
- else:
- shutil.rmtree(self.temp_dir)
-
- if not os.path.isdir(self.temp_dir):
- if not self.make_directory(self.temp_dir):
- return self.main_win_obj.destroy()
-
- if not os.path.isdir(self.temp_dl_dir):
- if not self.make_directory(self.temp_dl_dir):
- return self.main_win_obj.destroy()
-
- # If the database file itself exists; load it. If not, create it
+ # If the database file itself doesn't exist, create it. Otherwise, try
+ # to load it
db_path = os.path.abspath(
os.path.join(self.data_dir, self.db_file_name),
)
@@ -5167,20 +5295,137 @@ class TartubeApp(Gtk.Application):
'ok',
)
+ # Update temporary directories for both the old and new
+ # database locations
+ self.update_temporary_dirs_after_switch()
+
+ # Reset the backup values for various IVs that we no longer need
+ self.clear_data_variables_after_switch()
+
return True
+ elif not self.load_db(True):
+
+ # Failed to load the database file. Restore the values for various
+ # IVs
+ self.restore_data_variables_after_switch()
+
+ return False
+
else:
- if not self.load_db():
+ # Successfully loaded the database file. Update temporary
+ # directories for both the old and new database locations
+ self.update_temporary_dirs_after_switch()
- return False
+ # Reset the backup values for various IVs that we no longer need
+ self.clear_data_variables_after_switch()
- else:
+ # Save the config file, to preserve the new location of the data
+ # directory
+ self.save_config()
+ return True
- # Save the config file, to preserve the new location of the
- # data directory
- self.save_config()
- return True
+
+ def backup_data_variables_before_switch(self):
+
+ """Called by self.switch_db().
+
+ Before loading the replacement database, make a backup copy of several
+ IVs. If the load fails, then those values can be restored (in a call to
+ self.restore_data_variables_after_switch() ), and the user can continue
+ using the previous database, as before.
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 4846 backup_data_variables_before_switch')
+
+ self.backup_data_dir = self.data_dir
+ self.backup_downloads_dir = self.downloads_dir
+ self.backup_alt_downloads_dir = self.alt_downloads_dir
+ self.backup_backup_dir = self.backup_dir
+ self.backup_temp_dir = self.temp_dir
+ self.backup_temp_dl_dir = self.temp_dl_dir
+ self.backup_temp_test_dir = self.temp_test_dir
+ self.backup_data_dir_alt_list = self.data_dir_alt_list.copy()
+
+
+ def clear_data_variables_after_switch(self):
+
+ """Called by self.switch_db().
+
+ After succesfully loading a replacement database, reset the backup
+ copies of several IVs we made, in case the load failed.
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 4848 clear_data_variables_after_switch')
+
+ self.backup_data_dir = None
+ self.backup_downloads_dir = None
+ self.backup_alt_downloads_dir = None
+ self.backup_backup_dir = None
+ self.backup_temp_dir = None
+ self.backup_temp_dl_dir = None
+ self.backup_temp_test_dir = None
+ self.backup_data_dir_alt_list = None
+
+
+ def restore_data_variables_after_switch(self):
+
+ """Called by self.switch_db().
+
+ After failing to load a replacement database, restore the original
+ values of several IVs, so the user can continue using the previous
+ database, as before.
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 4849 restore_data_variables_after_switch')
+
+ self.data_dir = self.backup_data_dir
+ self.downloads_dir = self.backup_downloads_dir
+ self.alt_downloads_dir = self.backup_alt_downloads_dir
+ self.dir = self.backup_dir
+ self.temp_dir = self.backup_temp_dir
+ self.temp_dl_dir = self.backup_temp_dl_dir
+ self.temp_test_dir = self.backup_temp_test_dir
+ self.data_dir_alt_list = self.backup_data_dir_alt_list.copy()
+
+
+ def update_temporary_dirs_after_switch(self):
+
+ """Called by self.switch_db().
+
+ After succesfully loading a replacement database, remove temporary
+ directories, both for the old and new database files.
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 4847 update_temporary_dirs_after_switch')
+
+ # For the old database, delete Tartube's temporary folder from the
+ # filesystem
+ if os.path.isdir(self.backup_temp_dir):
+ shutil.rmtree(self.backup_temp_dir)
+
+ # For the new database, the temporary data directory should be emptied,
+ # if it already exists)
+ if os.path.isdir(self.temp_dir):
+ try:
+ shutil.rmtree(self.temp_dir)
+
+ except:
+ if not self.make_directory(self.temp_dir):
+ return False
+ else:
+ shutil.rmtree(self.temp_dir)
+
+ if not os.path.isdir(self.temp_dir):
+ self.make_directory(self.temp_dir)
+
+ if not os.path.isdir(self.temp_dl_dir):
+ self.make_directory(self.temp_dl_dir)
def choose_alt_db(self):
@@ -5814,6 +6059,154 @@ class TartubeApp(Gtk.Application):
)
+ def setup_paths(self):
+
+ """Called by self.start().
+
+ Sets the default values of various IVs handling the path of the
+ installed youtube-dl.
+
+ On MS Windows, these are fixed. On other operating systems, we try to
+ auto-detect youtube-dl's location, if possible.
+ """
+
+ # Set youtube-dl path IVs
+ if os.name == 'nt':
+
+ if 'PROGRAMFILES(X86)' in os.environ:
+ # 64-bit MS Windows
+ recommended = 'ytdl_update_win_64'
+ python_path = '..\\..\\..\\mingw64\\bin\python3.exe'
+ pip_path = '..\\..\\..\\mingw64\\bin\pip3-script.py'
+ else:
+ # 32-bit MS Windows
+ recommended = 'ytdl_update_win_32'
+ python_path = '..\\..\\..\\mingw32\\bin\python3.exe'
+ pip_path = '..\\..\\..\\mingw32\\bin\pip3-script.py'
+
+ self.ytdl_bin = 'youtube-dl'
+ self.ytdl_path_default = 'youtube-dl'
+ self.ytdl_path = 'youtube-dl'
+ self.ytdl_update_dict = {
+ recommended: [
+ python_path,
+ pip_path,
+ 'install',
+ '--upgrade',
+ 'youtube-dl',
+ ],
+ 'ytdl_update_pip3': [
+ 'pip3', 'install', '--upgrade', 'youtube-dl',
+ ],
+ 'ytdl_update_pip': [
+ 'pip', 'install', '--upgrade', 'youtube-dl',
+ ],
+ 'ytdl_update_default_path': [
+ self.ytdl_path_default, '-U',
+ ],
+ 'ytdl_update_local_path': [
+ 'youtube-dl', '-U',
+ ],
+ }
+ self.ytdl_update_list = [
+ recommended,
+ 'ytdl_update_pip3',
+ 'ytdl_update_pip',
+ 'ytdl_update_default_path',
+ 'ytdl_update_local_path',
+ ]
+ self.ytdl_update_current = recommended
+
+ else:
+
+ self.ytdl_bin = 'youtube-dl'
+ self.ytdl_path_default = os.path.abspath(
+ os.path.join(os.sep, 'usr', 'bin', self.ytdl_bin),
+ )
+
+ # Set up the shell commands for updating youtube-dl
+ if __main__.__pkg_strict_install_flag__:
+
+ self.ytdl_update_dict = {
+ 'ytdl_update_disabled': [],
+ }
+ self.ytdl_update_list = [
+ 'ytdl_update_disabled',
+ ]
+ self.ytdl_update_current = 'ytdl_update_disabled'
+
+ else:
+
+ self.ytdl_update_dict = {
+ 'ytdl_update_pip3_recommend': [
+ 'pip3', 'install', '--upgrade', '--user', 'youtube-dl',
+ ],
+ 'ytdl_update_pip3_omit_user': [
+ 'pip3', 'install', '--upgrade', 'youtube-dl',
+ ],
+ 'ytdl_update_pip': [
+ 'pip', 'install', '--upgrade', '--user', 'youtube-dl',
+ ],
+ 'ytdl_update_pip_omit_user': [
+ 'pip', 'install', '--upgrade', 'youtube-dl',
+ ],
+ 'ytdl_update_default_path': [
+ self.ytdl_path_default, '-U',
+ ],
+ 'ytdl_update_local_path': [
+ 'youtube-dl', '-U',
+ ],
+ 'ytdl_update_pypi_path': [
+ self.ytdl_path_pypi, '-U',
+ ],
+ }
+ self.ytdl_update_list = [
+ 'ytdl_update_pip3_recommend',
+ 'ytdl_update_pip3_omit_user',
+ 'ytdl_update_pip',
+ 'ytdl_update_pip_omit_user',
+ 'ytdl_update_default_path',
+ 'ytdl_update_local_path',
+ 'ytdl_update_pypi_path',
+ ]
+
+ # Auto-detect the location of youtube-dl, and set the perferred
+ # shell command
+ self.auto_detect_paths()
+
+
+ def auto_detect_paths(self):
+
+ """Can be called by anything (initially called by self.setup_paths() ).
+
+ Tries to auto-detect the location of youtube-dl, and updates IVs
+ accordingly.
+ """
+
+ # Should not be called on MS Windows
+ if os.name != 'nt':
+
+ pypi_path = re.sub(
+ '^\~', os.path.expanduser('~'),
+ self.ytdl_path_pypi,
+ )
+
+ if os.path.isfile(self.ytdl_path_default):
+ self.ytdl_path = self.ytdl_path_default
+ elif os.path.isfile(pypi_path) \
+ or __main__.__pkg_install_flag__:
+ self.ytdl_path = self.ytdl_path_pypi
+ else:
+ self.ytdl_path = self.ytdl_bin
+
+ if self.ytdl_path == self.ytdl_path_default:
+ self.ytdl_update_current = 'ytdl_update_default_path'
+ elif self.ytdl_path == self.ytdl_path_pypi:
+ self.ytdl_update_current = 'ytdl_update_pip3_recommend'
+ else:
+ self.ytdl_update_current = 'ytdl_update_local_path'
+
+
def auto_delete_old_videos(self):
"""Called by self.load_db().
@@ -6154,7 +6547,7 @@ class TartubeApp(Gtk.Application):
media_data_obj (media.Folder): The media data object to test
- Return values:
+ Returns:
True if it's one of the either recognised fixed folders, False
otherwise
@@ -6620,7 +7013,61 @@ class TartubeApp(Gtk.Application):
return False
- # (Download/Update/Refresh/Info/Tidy operations)
+ def check_downloader(self, arg):
+
+ """Called by several functions as they prepare a system command to
+ execute.
+
+ The specified value is one of the arguments in the system command,
+ containing the text 'youtube-dl'.
+
+ If self.ytdl_fork is specified, substitutes the fork for the original,
+ and returns the modified value.
+
+ If the specified value doesn't actually contain 'youtube-dl', then it
+ is returned unmodified.
+
+ Args:
+
+ arg (str): An argument in a system command, which should contain
+ 'youtube-dl'
+
+ Returns:
+
+ The modified (or original) value
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 6220 check_downloader')
+
+ if self.ytdl_fork is not None:
+ return re.sub('youtube-dl', self.ytdl_fork, arg)
+ else:
+ return arg
+
+
+ def get_downloader(self):
+
+ """Can be called by anything.
+
+ If a youtube-dl fork (self.ytdl_fork) has been specified, returns it.
+
+ Otherwise returns the string 'youtube-dl' (self.ytdl_bin).
+
+ Returns:
+
+ The string described above
+
+ """
+
+ if (self.ytdl_fork is not None):
+ return self.ytdl_fork
+ else:
+ return self.ytdl_bin
+
+
+ # (Operations)
def download_manager_start(self, operation_type, \
@@ -6666,8 +7113,9 @@ class TartubeApp(Gtk.Application):
# aesthetic reasons, we actually wait a few seconds before
# initiatin those operations. If the user starts a download operation
# before that happens, then cancel the scheduled one
- self.scheduled_dl_start_check_time = None
self.scheduled_check_start_check_time = None
+ self.scheduled_dl_start_check_time = None
+ self.scheduled_custom_start_check_time = None
# If a livestream operation is running, tell it to stop immediately
if self.livestream_manager_obj:
@@ -6676,12 +7124,11 @@ class TartubeApp(Gtk.Application):
# If a livestream operation was running, this IV should now be reset
if self.current_manager_obj:
- # Download/update/refresh/info/tidy operation already in progress
+ # Operation already in progress
if not automatic_flag:
self.system_error(
107,
- 'Download, update, refresh, info or tidy operation' \
- + ' already in progress',
+ 'An operation is already in progress',
)
return
@@ -6848,6 +7295,8 @@ class TartubeApp(Gtk.Application):
if not media_data_list:
if operation_type == 'sim':
self.scheduled_check_last_time = int(time.time())
+ elif operation_type == 'custom':
+ self.scheduled_custom_last_time = int(time.time())
else:
self.scheduled_dl_last_time = int(time.time())
@@ -6968,8 +7417,8 @@ class TartubeApp(Gtk.Application):
else:
time_num = int(time.time() - self.download_manager_obj.start_time)
- # Any code can check whether a download/update/refresh/info/tidy
- # operation is in progress, or not, by checking this IV
+ # Any code can check whether an operation is in progress, or not, by
+ # checking this IV
self.current_manager_obj = None
self.download_manager_obj = None
@@ -7086,11 +7535,10 @@ class TartubeApp(Gtk.Application):
# If a livestream operation was running, this IV should now be reset
if self.current_manager_obj:
- # Download/update/refresh/info/tidy operation already in progress
+ # Operation already in progress
return self.system_error(
108,
- 'Download, update, refresh, info or tidy operation already' \
- + ' in progress',
+ 'Operation already in progress',
)
elif self.main_win_obj.config_win_list:
@@ -7117,7 +7565,7 @@ class TartubeApp(Gtk.Application):
)
elif update_type == 'ffmpeg' and os.name != 'nt':
- # The Update operation can only install FFmpeg on the MS Windows
+ # The update operation can only install FFmpeg on the MS Windows
# installation of Tartube. It should not be possible to call this
# function, but we'll show an error message anyway
return self.system_error(
@@ -7128,8 +7576,10 @@ class TartubeApp(Gtk.Application):
# During an update operation, certain widgets are modified and/or
# desensitised
- self.main_win_obj.output_tab_reset_pages()
self.main_win_obj.sensitise_check_dl_buttons(False, update_type)
+ self.main_win_obj.output_tab_reset_pages()
+ if self.auto_switch_output_flag:
+ self.main_win_obj.output_tab_show_first_page()
# During an update operation, a GObject timer runs, so that the Output
# Tab can be updated at regular intervals
@@ -7182,8 +7632,8 @@ class TartubeApp(Gtk.Application):
success_flag = self.update_manager_obj.success_flag
ytdl_version = self.update_manager_obj.ytdl_version
- # Any code can check whether a download/update/refresh/info/tidy
- # operation is in progress, or not, by checking this IV
+ # Any code can check whether an operation is in progress, or not, by
+ # checking this IV
self.current_manager_obj = None
self.update_manager_obj = None
@@ -7192,6 +7642,15 @@ class TartubeApp(Gtk.Application):
self.update_timer_id = None
self.update_timer_check_time = None
+ # If this is the first successful update operation, auto-detect
+ # youtube-dl's actual location (but not on MS Windows, for which the
+ # location is set in stone)
+ if success_flag and not self.ytdl_update_once_flag:
+
+ self.ytdl_update_once_flag = True
+ if os.name != 'nt':
+ self.auto_detect_paths()
+
# After an update operation, save files, if allowed
if self.operation_save_flag:
self.save_config()
@@ -7222,16 +7681,49 @@ class TartubeApp(Gtk.Application):
msg = _('Update operation halted')
else:
msg = _('Update operation complete') \
- + '\n\n' + _('youtube-dl version:') + ' '
+ + '\n\n' + self.get_downloader() + ' ' \
+ + _('version:') + ' '
if ytdl_version is not None:
msg += ytdl_version
else:
msg += _('(unknown)')
- if self.operation_dialogue_mode == 'dialogue':
- self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok')
- elif self.operation_dialogue_mode == 'desktop':
- self.main_win_obj.notify_desktop(None, msg)
+
+ # (The first time Tartube runs, on MS Windows only, nag the user to
+ # install FFmpeg right away)
+ if success_flag \
+ and not self.operation_halted_flag \
+ and not self.operation_waiting_flag \
+ and self.prompt_ffmpeg_flag:
+
+ self.operation_halted_flag = False
+ self.prompt_ffmpeg_flag = False
+
+ msg += '\n\n' + _('Do you want to install FFmpeg now?') \
+ + '\n\n' + _(
+ '(You should click Yes, even if you think FFmpeg is already' \
+ + ' installed on your system)',
+ )
+
+ self.dialogue_manager_obj.show_msg_dialogue(
+ msg,
+ 'question',
+ 'yes-no',
+ None, # Parent window is main window
+ # Arguments passed directly to .update_manager_start()
+ {
+ 'yes': 'update_manager_start',
+ 'data': 'ffmpeg',
+ },
+ )
+
+ return
+
+ elif self.operation_dialogue_mode == 'dialogue':
+ self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok')
+
+ elif self.operation_dialogue_mode == 'desktop':
+ self.main_win_obj.notify_desktop(None, msg)
# Reset operation IVs
self.operation_halted_flag = False
@@ -7281,21 +7773,23 @@ class TartubeApp(Gtk.Application):
# If a livestream operation was running, this IV should now be reset
if self.current_manager_obj:
- # Download/update/refresh/info/tidy operation already in progress
+
+ # Operation already in progress
return self.system_error(
111,
- 'Download, update, refresh, info or tidy operation already' \
- + ' in progress',
+ 'Operation already in progress',
)
elif media_data_obj is not None \
and isinstance(media_data_obj, media.Video):
+
return self.system_error(
112,
'Refresh operation cannot be applied to an individual video',
)
elif self.main_win_obj.config_win_list:
+
# Refresh operation is not allowed when a configuration window is
# open
self.dialogue_manager_obj.show_msg_dialogue(
@@ -7572,11 +8066,11 @@ class TartubeApp(Gtk.Application):
# If a livestream operation was running, this IV should now be reset
if self.current_manager_obj:
- # Download/update/refresh/info/tidy operation already in progress
+
+ # Operation already in progress
return self.system_error(
113,
- 'Download, update, refresh, info or tidy operation already' \
- + ' in progress',
+ 'Operation already in progress',
)
elif info_type != 'formats' \
@@ -7637,9 +8131,8 @@ class TartubeApp(Gtk.Application):
except:
pass
- # Initiate the info operation. Any code can check whether a
- # download/update/refresh/info/tidy operation is in progress, or not,
- # by checking this IV
+ # Initiate the info operation. Any code can check whether an operation
+ # is in progress, or not, by checking this IV
self.current_manager_obj = info.InfoManager(
self,
info_type,
@@ -7686,8 +8179,8 @@ class TartubeApp(Gtk.Application):
output_list = self.info_manager_obj.output_list.copy()
url_string = self.info_manager_obj.url_string
- # Any code can check whether a download/update/refresh/info/tidy
- # operation is in progress, or not, by checking this IV
+ # Any code can check whether an operation is in progress, or not, by
+ # checking this IV
self.current_manager_obj = None
self.info_manager_obj = None
@@ -7781,6 +8274,20 @@ class TartubeApp(Gtk.Application):
name should be deleted (as artefacts of post-processing
with FFmpeg or AVConv)
+ del_archive_flag: True if all youtube-dl archive files should
+ be deleted
+
+ move_thumb_flag: True if all thumbnail files should be moved
+ into a subdirectory
+
+ del_thumb_flag: True if all thumbnail files should be deleted
+
+ convert_webp_flag: True if all .webp thumbnail files should be
+ converted to .jpg
+
+ move_data_flag: True if description, metadata (JSON) and
+ annotation files should be moved into a subdirectory
+
del_descrip_flag: True if all description files should be
deleted
@@ -7789,11 +8296,6 @@ class TartubeApp(Gtk.Application):
del_xml_flag: True if all annotation files should be deleted
- del_thumb_flag: True if all thumbnail files should be deleted
-
- del_archive_flag: True if all youtube-dl archive files should
- be deleted
-
"""
@@ -7806,11 +8308,11 @@ class TartubeApp(Gtk.Application):
# If a livestream operation was running, this IV should now be reset
if self.current_manager_obj:
- # Download/update/refresh/info/tidy operation already in progress
+
+ # Operation already in progress
return self.system_error(
116,
- 'Download, update, refresh, info or tidy operation already' \
- + ' in progress',
+ 'Operation already in progress',
)
elif self.main_win_obj.config_win_list:
@@ -7853,9 +8355,8 @@ class TartubeApp(Gtk.Application):
self.tidy_timer_callback,
)
- # Initiate the tidy operation. Any code can check whether a
- # download/update/refresh/info/tidy operation is in progress, or not,
- # by checking this IV
+ # Initiate the tidy operation. Any code can check whether an operation
+ # is in progress, or not by checking this IV
self.current_manager_obj = tidy.TidyManager(self, choices_dict)
self.tidy_manager_obj = self.current_manager_obj
@@ -7901,8 +8402,8 @@ class TartubeApp(Gtk.Application):
else:
time_num = int(time.time() - self.tidy_manager_obj.start_time)
- # Any code can check whether a download/update/refresh/info/tidy
- # operation is in progress, or not, by checking this IV
+ # Any code can check whether an operation is in progress, or not, by
+ # checking this IV
self.current_manager_obj = None
self.tidy_manager_obj = None
@@ -7988,9 +8489,8 @@ class TartubeApp(Gtk.Application):
if DEBUG_FUNC_FLAG:
utils.debug_time('app 7649 livestream_manager_start')
- # Download/update/refresh/info/tidy/livestream operation already in
- # progress, or a configuration window is open, or there are no
- # livestreams to check:
+ # Operation already in progress, or a configuration window is open, or
+ # there are no livestreams to check:
if self.current_manager_obj \
or self.livestream_manager_obj \
or self.main_win_obj.config_win_list \
@@ -8004,9 +8504,8 @@ class TartubeApp(Gtk.Application):
# time at which this operation began
self.scheduled_livestream_last_time = int(time.time())
- # Initiate the livestream operation. Any code can check whether a
- # download/update/refresh/info/tidy/livestream operation is in
- # progress, or not, by checking this IV
+ # Initiate the livestream operation. Any code can check whether an
+ # operation is in progress, or not, by checking this IV
# (NB Since livestream operations run silently in the background and
# since no functionality is disabled during a livestream operation,
# self.current_manager_obj remains set to None)
@@ -8162,6 +8661,221 @@ class TartubeApp(Gtk.Application):
self.download_manager_obj.nudge_progress_bar()
+ def process_manager_start(self, option_string, add_string, regex_string,
+ substitute_string, ext_string, delete_flag, video_list):
+
+ """Can be called by anything.
+
+ Initiates a process operation to send one or more videos to FFmpeg for
+ processing. Tartube's media data registry is not updated.
+
+ Creates a new proces.ProcessManager object to handle the process
+ operation. When the operation is complete,
+ self.process_manager_finished() is called.
+
+ Args:
+
+ option_string (str): A string of FFmpeg options (usually a copy of
+ self.ffmpeg_option_string)
+
+ add_string (str): Text to add to the end of every filename (usually
+ a copy of self.ffmpeg_add_string)
+
+ regex_string, substitute_string (str): A regex substitution to
+ apply to every filename (usually a copy of
+ self.ffmpeg_regex_string and .ffmpeg_substitute_string);
+ ignored if regex_string is an empty string, not ignored if
+ substitute_string is an empty string
+
+ ext_string (str): The replacement file extension to use (usually
+ a copy of self.ffmpeg_ext_string); ignored if an empty string
+
+ delete_flag (bool): True if the old video file should be deleted
+ (and media.Video IVs updated) if FFmpeg's output file has a
+ different name (for example, if the file extension has
+ changed); False otherwise
+
+ video_list (list): A listof media.Video objects (should contain at
+ least one video)
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 7686 process_manager_start')
+
+ if self.current_manager_obj:
+
+ # Operation already in progress
+ return self.system_error(
+ 164,
+ 'Operation already in progress',
+ )
+
+ elif self.main_win_obj.config_win_list:
+
+ # Process operation is not allowed when a configuration window is
+ # open
+ if not automatic_flag:
+ self.dialogue_manager_obj.show_msg_dialogue(
+ _(
+ 'A process operation cannot start if one or more' \
+ + ' configuration windows are still open',
+ ),
+ 'error',
+ 'ok',
+ )
+
+ return
+
+ elif not video_list:
+
+ return self.system_error(
+ 165,
+ 'Process operation requires at least one video',
+ )
+
+ # (The calling code should have filtered out non-downloaded videos, but
+ # we'll check anyway)
+ for video_obj in video_list:
+
+ if video_obj.file_name is None or not video_obj.dl_flag:
+ return self.system_error(
+ 166,
+ 'Process operation cannot process a video, if it has not' \
+ + ' been downloaded',
+ )
+
+ # (Process operations don't modify the media data registry, therefore
+ # they are allowed to run when a configuration window is open)
+
+ # (Process operations should not cause Gtk stability issues)
+
+ # During a process operation, show a progress bar in the Videos Tab
+ self.main_win_obj.show_progress_bar('process')
+ # Reset the Output Tab
+ self.main_win_obj.output_tab_reset_pages()
+ # (De)sensitise other widgets, as appropriate
+ self.main_win_obj.sensitise_operation_widgets(False, True)
+ # Make the widget changes visible
+ self.main_win_obj.show_all()
+
+ # During a process operation, a GObject timer runs, so that the Output
+ # Tab can be updated at regular intervals
+ # Create the timer
+ self.process_timer_id = GObject.timeout_add(
+ self.process_timer_time,
+ self.process_timer_callback,
+ )
+
+ # Initiate the process operation. Any code can check whether an
+ # operation is in progress, or not, by checking this IV
+ self.current_manager_obj = process.ProcessManager(
+ self,
+ option_string,
+ add_string,
+ regex_string,
+ substitute_string,
+ ext_string,
+ delete_flag,
+ video_list,
+ )
+
+ self.process_manager_obj = self.current_manager_obj
+
+ # Update the status icon in the system tray
+ self.status_icon_obj.update_icon()
+
+
+ def process_manager_halt_timer(self):
+
+ """Called by process.ProcessManager.run() when that function has
+ finished.
+
+ During a process operation, a GObject timer was running. Let it
+ continue running for a few seconds more.
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 7687 process_manager_halt_timer')
+
+ if self.process_timer_id:
+ self.process_timer_check_time \
+ = int(time.time()) + self.process_timer_final_time
+
+
+ def process_manager_finished(self):
+
+ """Called by self.process_timer_callback().
+
+ The process operation has finished, so update IVs and main window
+ widgets.
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 7688 process_manager_finished')
+
+ # Get the time taken by the process operation, so we can convert it
+ # into a nice string below (e.g. '05:15')
+ # For some reason, ProcessManager.stop_time() might not be set, so we
+ # need to check for that
+ if self.process_manager_obj.stop_time is not None:
+ time_num = int(
+ self.process_manager_obj.stop_time \
+ - self.process_manager_obj.start_time
+ )
+ else:
+ time_num = int(time.time() - self.process_manager_obj.start_time)
+
+ # Any code can check whether an operation is in progress, or not, by
+ # checking this IV
+ self.current_manager_obj = None
+ self.process_manager_obj = None
+
+ # Stop the timer and reset IVs
+ GObject.source_remove(self.process_timer_id)
+ self.process_timer_id = None
+ self.process_timer_check_time = None
+
+ # After a process operation, save files, if allowed
+ if self.operation_save_flag:
+ self.save_config()
+ self.save_db()
+
+ # Update the status icon in the system tray
+ self.status_icon_obj.update_icon()
+ # Remove the progress bar in the Videos Tab
+ self.main_win_obj.hide_progress_bar()
+ # Any remaining messages generated by process.ProcessManager should be
+ # shown in the Output Tab immediately
+ self.main_win_obj.output_tab_update_pages()
+ # (De)sensitise other widgets, as appropriate
+ self.main_win_obj.sensitise_operation_widgets(True)
+ # Make the widget changes visible (not necessary if the main window has
+ # been closed to the system tray)
+ if self.main_win_obj.is_visible():
+ self.main_win_obj.show_all()
+
+ # Then show a dialogue window/desktop notification, if allowed
+ if self.operation_dialogue_mode != 'default':
+
+ if not self.operation_halted_flag:
+ msg = _('Process operation complete')
+ else:
+ msg = _('Process operation halted')
+
+ if time_num >= 10:
+ msg += '\n\n' + _('Time taken:') + ' ' \
+ + utils.convert_seconds_to_string(time_num, True)
+
+ if self.operation_dialogue_mode == 'dialogue':
+ self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok')
+ elif self.operation_dialogue_mode == 'desktop':
+ self.main_win_obj.notify_desktop(None, msg)
+
+ # Reset operation IVs
+ self.operation_halted_flag = False
+
+
# (Download operation support functions)
def create_video_from_download(self, download_item_obj, dir_path, \
@@ -8378,8 +9092,7 @@ class TartubeApp(Gtk.Application):
def announce_video_download(self, download_item_obj, video_obj, \
- keep_description=None, keep_info=None, keep_annotations=None,
- keep_thumbnail=None):
+ mini_options_dict):
"""Called by downloads.VideoDownloader.confirm_new_video(),
.confirm_old_video() and .confirm_sim_video().
@@ -8394,12 +9107,13 @@ class TartubeApp(Gtk.Application):
video_obj (media.Video): The video object for the downloaded video
- keep_description (True, False, None):
- keep_info (True, False, None):
- keep_annotations (True, False, None):
- keep_thumbnail (True, False, None):
- Settings from the options.OptionsManager object used to
- download the video (set to 'None' for a simulated download)
+ mini_options_dict (dict): A dictionary containing a subset of
+ download options from the the options.OptionsManager object
+ used to download the video. It contains zero, some or all of
+ the following download options:
+
+ keep_description keep_info keep_annotations keep_thumbnail
+ move_description move_info move_annotations move_thumbnail
"""
@@ -8415,10 +9129,7 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.results_list_add_row(
download_item_obj,
video_obj,
- keep_description,
- keep_info,
- keep_annotations,
- keep_thumbnail,
+ mini_options_dict,
)
@@ -8520,8 +9231,10 @@ class TartubeApp(Gtk.Application):
it
'row_num': not required by this function
'keep_description', 'keep_info', 'keep_annotations',
- 'keep_thumbnail': flags from the options.OptionsManager
- object used for to download the video (not added to the
+ 'keep_thumbnail', 'move_description', 'move_info',
+ 'move_annotations', 'move_thumbnail': flags from the
+ options.OptionsManager object used for to download the
+ video ('keep_description', etc, are not not added to the
dictionary at all for simulated downloads)
mkv_flag (bool): If the warning 'Requested formats are incompatible
@@ -8557,46 +9270,79 @@ class TartubeApp(Gtk.Application):
# directly
self.update_video_from_filesystem(video_obj, video_path)
- # Delete the description, JSON, annotations and thumbnail files, if
- # required to do so
+ # If FFmpeg is installed, convert .webp thumbnail files to .jpg
+ thumb_path = utils.find_thumbnail_webp(self, video_obj)
+ if thumb_path is not None \
+ and not self.ffmpeg_fail_flag \
+ and self.ffmpeg_convert_webp_flag \
+ and not self.ffmpeg_manager_obj.convert_webp(thumb_path):
+
+ self.ffmpeg_fail_flag = True
+ self.system_error(163, self.ffmpeg_fail_msg)
+
+ # Discard the description, JSON, annotations and thumbnail files, if
+ # required to do so. The files are moved to Tartube's temporary
+ # directory, to be deleted at or before the next startup
+ # If the files aren't discarded, move them into the sub-directories
+ # '.thumbs' or '.data', if required
+
+ # Description file
if 'keep_description' in temp_dict \
and not temp_dict['keep_description']:
- old_path = video_obj.get_actual_path_by_ext(self, '.description')
+ old_path = video_obj.check_actual_path_by_ext(self, '.description')
+ if old_path is not None:
- if os.path.isfile(old_path):
utils.convert_path_to_temp(
self,
old_path,
True, # Move the file
)
+ elif 'move_description' in temp_dict \
+ and temp_dict['move_description']:
+
+ utils.move_metadata_to_subdir(self, video_obj, '.description')
+
+ # JSON file
if 'keep_info' in temp_dict and not temp_dict['keep_info']:
- old_path = video_obj.get_actual_path_by_ext(self, '.info.json')
+ old_path = video_obj.check_actual_path_by_ext(self, '.info.json')
+ if old_path is not None:
- if os.path.isfile(old_path):
utils.convert_path_to_temp(
self,
old_path,
True, # Move the file
)
+ elif 'move_info' in temp_dict and temp_dict['move_info']:
+
+ utils.move_metadata_to_subdir(self, video_obj, '.info.json')
+
+ # Annotations file
if 'keep_annotations' in temp_dict \
and not temp_dict['keep_annotations']:
- old_path = video_obj.get_actual_path_by_ext(
+ old_path = video_obj.check_actual_path_by_ext(
self,
'.annotations.xml',
)
- if os.path.isfile(old_path):
+ if old_path is not None:
+
utils.convert_path_to_temp(
self,
old_path,
True, # Move the file
)
+ elif 'move_annotations' in temp_dict \
+ and temp_dict['move_annotations']:
+
+ utils.move_metadata_to_subdir(self, video_obj, '.annotations.xml')
+
+ # Thumbnail
if 'keep_thumbnail' in temp_dict and not temp_dict['keep_thumbnail']:
old_path = utils.find_thumbnail(self, video_obj)
@@ -8608,6 +9354,10 @@ class TartubeApp(Gtk.Application):
True, # Move the file
)
+ elif 'move_thumbnail' in temp_dict and temp_dict['move_thumbnail']:
+
+ utils.move_thumbnail_to_subdir(self, video_obj)
+
# Mark the video as (fully) downloaded (and update everything else)
self.mark_video_downloaded(video_obj, True)
@@ -8688,9 +9438,8 @@ class TartubeApp(Gtk.Application):
if DEBUG_FUNC_FLAG:
utils.debug_time('app 8336 update_video_from_json')
- json_path = video_obj.get_actual_path_by_ext(self, '.info.json')
-
- if os.path.isfile(json_path):
+ json_path = video_obj.check_actual_path_by_ext(self, '.info.json')
+ if json_path is not None:
json_dict = self.file_manager_obj.load_json(json_path)
@@ -9472,8 +10221,7 @@ class TartubeApp(Gtk.Application):
# onto itself; just do nothing
return
- # Ignore Video Index drag-and-drop during an download/update/refresh/
- # info/tidy operation
+ # Ignore Video Index drag-and-drop during an operation
elif self.current_manager_obj:
return
@@ -9835,9 +10583,20 @@ class TartubeApp(Gtk.Application):
for ext in ext_list:
- file_path = video_obj.get_default_path_by_ext(self, ext)
- if os.path.isfile(file_path):
- os.remove(file_path)
+ main_path = video_obj.get_default_path_by_ext(self, ext)
+ if os.path.isfile(main_path):
+ os.remove(main_path)
+
+ else:
+
+ subdir_path \
+ = video_obj.get_default_path_in_subdirectory_by_ext(
+ self,
+ ext,
+ )
+
+ if os.path.isfile(subdir_path):
+ os.remove(subdir_path)
# (Thumbnails might be in one of two locations, so are handled
# separately)
@@ -10136,7 +10895,7 @@ class TartubeApp(Gtk.Application):
.mark_container_missing(), .mark_container_new() and
mainwin.MainWin.on_video_index_mark_bookmark(), etc.
- The operation to mark a container's video as bookmarked or not
+ The procecure to mark a container's video as bookmarked or not
bookmarked (etc) can take a very long time, especially if there are
thousands of videos.
@@ -11666,7 +12425,7 @@ class TartubeApp(Gtk.Application):
elif count < self.main_win_obj.mark_video_lower_limit:
- # The operation should be quick
+ # The procedure should be quick
for child_obj in video_list:
self.mark_video_favourite(child_obj, fav_flag)
@@ -11782,7 +12541,7 @@ class TartubeApp(Gtk.Application):
elif count < self.main_win_obj.mark_video_lower_limit:
- # The operation should be quick
+ # The procedure should be quick
for child_obj in video_list:
self.mark_video_missing(child_obj, missing_flag)
@@ -11956,7 +12715,7 @@ class TartubeApp(Gtk.Application):
elif count < self.main_win_obj.mark_video_lower_limit:
- # The operation should be quick
+ # The procedure should be quick
for child_obj in video_list:
self.mark_video_new(child_obj, new_flag)
@@ -12554,9 +13313,17 @@ class TartubeApp(Gtk.Application):
with open(file_path, 'w') as outfile:
json.dump(json_dict, outfile, indent=4)
- except:
+# # DEBUG: Git 143: provide more information on the exception
+# except:
+# return self.dialogue_manager_obj.show_msg_dialogue(
+# _('Failed to save the database export file'),
+# 'error',
+# 'ok',
+# )
+ except Exception as e:
return self.dialogue_manager_obj.show_msg_dialogue(
- _('Failed to save the database export file'),
+ _('Failed to save the database export file:') \
+ + '\n\n' + str(e),
'error',
'ok',
)
@@ -12612,9 +13379,17 @@ class TartubeApp(Gtk.Application):
for line in line_list:
outfile.write(line + '\n')
- except:
+# # DEBUG: Git 143: provide more information on the exception
+# except:
+# return self.dialogue_manager_obj.show_msg_dialogue(
+# _('Failed to save the database export file'),
+# 'error',
+# 'ok',
+# )
+ except Exception as e:
return self.dialogue_manager_obj.show_msg_dialogue(
- _('Failed to save the database export file'),
+ _('Failed to save the database export file:') \
+ + '\n\n' + str(e),
'error',
'ok',
)
@@ -13145,9 +13920,30 @@ class TartubeApp(Gtk.Application):
utils.debug_time('app 12322 watch_video_in_player')
path = video_obj.get_actual_path(self)
+ if os.path.isfile(path):
- if not os.path.isfile(path):
+ utils.open_file(path)
+ else:
+
+ name, ext = os.path.splitext(path)
+
+ # Because it's so easy to convert the original video to a different
+ # format (including audio formats), search for one of those,
+ # before reporting an error
+ for test_ext in (formats.VIDEO_FORMAT_LIST):
+ test_path = name + '.' + test_ext
+ if os.path.isfile(test_path):
+ utils.open_file(test_path)
+ return
+
+ for test_ext in (formats.AUDIO_FORMAT_LIST):
+ test_path = name + '.' + test_ext
+ if os.path.isfile(test_path):
+ utils.open_file(test_path)
+ return
+
+ # Video is completely missing
self.dialogue_manager_obj.show_msg_dialogue(
_(
'The video file is missing from Tartube\'s data folder' \
@@ -13157,9 +13953,6 @@ class TartubeApp(Gtk.Application):
'ok',
)
- else:
- utils.open_file(path)
-
def download_watch_videos(self, video_list, watch_flag=True):
@@ -13372,7 +14165,22 @@ class TartubeApp(Gtk.Application):
and not self.current_manager_obj \
and not self.main_win_obj.config_win_list:
- if self.scheduled_dl_mode == 'scheduled':
+ if self.scheduled_check_mode == 'scheduled':
+
+ wait_time = self.scheduled_check_wait_value \
+ * formats.TIME_METRIC_DICT[self.scheduled_check_wait_unit]
+
+ if (self.scheduled_check_last_time + wait_time) < time.time():
+
+ self.download_manager_start(
+ 'sim', # 'Check all'
+ True, # This function is the calling function
+ )
+
+ # Return 1 to keep the timer going
+ return 1
+
+ elif self.scheduled_dl_mode == 'scheduled':
wait_time = self.scheduled_dl_wait_value \
* formats.TIME_METRIC_DICT[self.scheduled_dl_wait_unit]
@@ -13387,15 +14195,15 @@ class TartubeApp(Gtk.Application):
# Return 1 to keep the timer going
return 1
- elif self.scheduled_check_mode == 'scheduled':
+ elif self.scheduled_custom_mode == 'scheduled':
- wait_time = self.scheduled_check_wait_value \
- * formats.TIME_METRIC_DICT[self.scheduled_check_wait_unit]
+ wait_time = self.scheduled_custom_wait_value \
+ * formats.TIME_METRIC_DICT[self.scheduled_custom_wait_unit]
- if (self.scheduled_check_last_time + wait_time) < time.time():
+ if (self.scheduled_custom_last_time + wait_time) < time.time():
self.download_manager_start(
- 'sim', # 'Check all'
+ 'custom', # Custom 'Download all'
True, # This function is the calling function
)
@@ -13445,7 +14253,16 @@ class TartubeApp(Gtk.Application):
# Check scheduled operations
current_time = time.time()
- if self.scheduled_dl_start_check_time is not None \
+
+ if self.scheduled_check_start_check_time is not None \
+ and self.scheduled_check_start_check_time < current_time:
+
+ self.download_manager_start(
+ 'sim', # 'Check all'
+ True, # This function is the calling function
+ )
+
+ elif self.scheduled_dl_start_check_time is not None \
and self.scheduled_dl_start_check_time < current_time:
self.download_manager_start(
@@ -13453,11 +14270,11 @@ class TartubeApp(Gtk.Application):
True, # This function is the calling function
)
- elif self.scheduled_check_start_check_time is not None \
- and self.scheduled_check_start_check_time < current_time:
+ elif self.scheduled_custom_start_check_time is not None \
+ and self.scheduled_custom_start_check_time < current_time:
self.download_manager_start(
- 'sim', # 'Check all'
+ 'custom', # 'Download all'
True, # This function is the calling function
)
@@ -13744,6 +14561,50 @@ class TartubeApp(Gtk.Application):
self.tidy_manager_finished()
+ def process_timer_callback(self):
+
+ """Called by GObject timer created by self.process_manager_continue().
+
+ During a process operation, a GObject timer runs, so that the Output
+ Tab can be updated at regular intervals.
+
+ For the benefit of systems with Gtk < 3.24, the timer continues running
+ for a few seconds at the end of the process operation.
+
+ During process operations, messages generated by process.ProcessManager
+ are temporarily stored (because Gtk widgets cannot be updated from
+ within a thread). This function calls
+ mainwin.MainWin.output_tab_update_pages() to display those messages in
+ the Output Tab.
+
+ Returns:
+
+ 1 to keep the timer going
+
+ """
+
+ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG:
+ utils.debug_time('app 12885 process_timer_callback')
+
+ if self.process_timer_check_time is None:
+
+ self.main_win_obj.output_tab_update_pages()
+ # Process operation still in progress, return 1 to keep the timer
+ # going
+ return 1
+
+ elif self.process_timer_check_time > time.time():
+
+ self.main_win_obj.output_tab_update_pages()
+ # Cooldown time not yet finished, return 1 to keep the timer going
+ return 1
+
+ else:
+ # The process operation has finished. The call to
+ # self.process_manager_finished() destroys the timer
+ self.process_manager_finished()
+
+
# (Menu item and toolbar button callbacks)
@@ -14478,6 +15339,8 @@ class TartubeApp(Gtk.Application):
self.info_manager_obj.stop_info_operation()
elif self.tidy_manager_obj:
self.tidy_manager_obj.stop_tidy_operation()
+ elif self.process_manager_obj:
+ self.process_manager_obj.stop_process_operation()
def on_button_switch_view(self, action, par):
@@ -15614,6 +16477,34 @@ class TartubeApp(Gtk.Application):
)
+ def on_menu_test_code(self, action, par):
+
+ """Called from a callback in self.do_startup().
+
+ Executes some arbitrary test code. This function can only be called if
+ the debugging flags are set.
+
+ Args:
+
+ action (Gio.SimpleAction): Object generated by Gio
+
+ par (None): Ignored
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 14738 on_menu_test_code')
+
+ result = testing.run_test_code(self)
+
+ self.dialogue_manager_obj.show_msg_dialogue(
+ 'Test code executed\n\nResult: ' + str(result),
+ 'info',
+ 'ok',
+ None, # Parent window is main window
+ )
+
+
def on_menu_test_ytdl(self, action, par):
"""Called from a callback in self.do_startup().
@@ -15693,12 +16584,14 @@ class TartubeApp(Gtk.Application):
'exist_flag': dialogue_win.checkbutton3.get_active(),
'del_video_flag': dialogue_win.checkbutton4.get_active(),
'del_others_flag': dialogue_win.checkbutton5.get_active(),
- 'del_descrip_flag': dialogue_win.checkbutton6.get_active(),
- 'del_json_flag': dialogue_win.checkbutton7.get_active(),
- 'del_xml_flag': dialogue_win.checkbutton8.get_active(),
- 'del_thumb_flag': dialogue_win.checkbutton9.get_active(),
- 'del_webp_flag': dialogue_win.checkbutton10.get_active(),
- 'del_archive_flag': dialogue_win.checkbutton11.get_active(),
+ 'del_archive_flag': dialogue_win.checkbutton6.get_active(),
+ 'move_thumb_flag': dialogue_win.checkbutton7.get_active(),
+ 'del_thumb_flag': dialogue_win.checkbutton8.get_active(),
+ 'convert_webp_flag': dialogue_win.checkbutton9.get_active(),
+ 'move_data_flag': dialogue_win.checkbutton10.get_active(),
+ 'del_descrip_flag': dialogue_win.checkbutton11.get_active(),
+ 'del_json_flag': dialogue_win.checkbutton12.get_active(),
+ 'del_xml_flag': dialogue_win.checkbutton13.get_active(),
}
# Now destroy the window
@@ -15711,22 +16604,23 @@ class TartubeApp(Gtk.Application):
if not choices_dict['corrupt_flag'] \
and not choices_dict['exist_flag'] \
and not choices_dict['del_video_flag'] \
+ and not choices_dict['del_thumb_flag'] \
+ and not choices_dict['convert_webp_flag'] \
and not choices_dict['del_descrip_flag'] \
and not choices_dict['del_json_flag'] \
and not choices_dict['del_xml_flag'] \
- and not choices_dict['del_thumb_flag'] \
- and not choices_dict['del_webp_flag'] \
- and not choices_dict['del_archive_flag']:
+ and not choices_dict['del_archive_flag'] \
+ and not choices_dict['move_thumb_flag'] \
+ and not choices_dict['move_data_flag']:
return
# Prompt the user for confirmation, before deleting any files
if choices_dict['del_corrupt_flag'] \
or choices_dict['del_video_flag'] \
+ or choices_dict['del_thumb_flag'] \
or choices_dict['del_descrip_flag'] \
or choices_dict['del_json_flag'] \
or choices_dict['del_xml_flag'] \
- or choices_dict['del_thumb_flag'] \
- or choices_dict['del_webp_flag'] \
or choices_dict['del_archive_flag']:
self.dialogue_manager_obj.show_msg_dialogue(
@@ -16028,6 +16922,17 @@ class TartubeApp(Gtk.Application):
del self.media_reg_auto_open_dict[video_obj.dbid]
+ def set_auto_switch_output_flag(self, flag):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15161 set_auto_switch_output_flag')
+
+ if not flag:
+ self.auto_switch_output_flag = False
+ else:
+ self.auto_switch_output_flag = True
+
+
def set_autostop_size_flag(self, flag):
if DEBUG_FUNC_FLAG:
@@ -16101,6 +17006,14 @@ class TartubeApp(Gtk.Application):
self.autostop_videos_value = value
+ def set_avconv_path(self, path):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15235 set_avconv_path')
+
+ self.avconv_path = path
+
+
def set_bandwidth_apply_flag(self, flag):
"""Called by mainwin.MainWin.on_bandwidth_checkbutton_changed().
@@ -16259,6 +17172,23 @@ class TartubeApp(Gtk.Application):
)
+ def set_custom_invidious_mirror(self, value):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15357 set_custom_invidious_mirror')
+
+ self.custom_invidious_mirror = value
+
+ # The Video Catalogue must be redrawn to reset the 'Invidious' label
+ # (but only when ComplexCatalogueItems are visible)
+ if self.catalogue_mode != 'simple_hide_parent' \
+ and self.catalogue_mode != 'simple_show_parent':
+ self.main_win_obj.video_catalogue_redraw_all(
+ self.main_win_obj.video_index_current,
+ self.main_win_obj.catalogue_toolbar_current_page,
+ )
+
+
def set_custom_locale(self, value):
if DEBUG_FUNC_FLAG:
@@ -16350,7 +17280,7 @@ class TartubeApp(Gtk.Application):
def set_delete_on_shutdown_flag(self, flag):
if DEBUG_FUNC_FLAG:
- utils.debug_time('app 15451 set_delete_on_shutdown_flag')
+ utils.debug_time('app 15452 set_delete_on_shutdown_flag')
if not flag:
self.delete_on_shutdown_flag = False
@@ -16443,18 +17373,82 @@ class TartubeApp(Gtk.Application):
self.enable_livestreams_flag = True
+ def set_ffmpeg_convert_webp_flag(self, flag):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15537 set_ffmpeg_convert_webp_flag')
+
+ if not flag:
+ self.ffmpeg_convert_webp_flag = False
+ else:
+ self.ffmpeg_convert_webp_flag = True
+
+
+ def set_ffmpeg_fail_flag(self, flag):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15538 set_ffmpeg_fail_flag')
+
+ if not flag:
+ self.ffmpeg_fail_flag = False
+ else:
+ self.ffmpeg_fail_flag = True
+
+
+ def set_ffmpeg_delete_flag(self, flag):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15539 set_ffmpeg_delete_flag')
+
+ if not flag:
+ self.ffmpeg_delete_flag = False
+ else:
+ self.ffmpeg_delete_flag = True
+
+
+ def set_ffmpeg_keep_flag(self, flag):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15540 set_ffmpeg_keep_flag')
+
+ if not flag:
+ self.ffmpeg_keep_flag = False
+ else:
+ self.ffmpeg_keep_flag = True
+
+
+ def set_ffmpeg_option_strings(self, add_string, regex_string, \
+ substitute_string, ext_string):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15541 set_ffmpeg_option_strings')
+
+ self.ffmpeg_add_string = add_string
+ self.ffmpeg_regex_string = regex_string
+ self.ffmpeg_substitute_string = substitute_string
+ self.ffmpeg_ext_string = ext_string
+
+
def set_ffmpeg_path(self, path):
if DEBUG_FUNC_FLAG:
- utils.debug_time('app 15547 set_ffmpeg_path')
+ utils.debug_time('app 15542 set_ffmpeg_path')
self.ffmpeg_path = path
+ def set_ffmpeg_option_string(self, string):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 15540 set_ffmpeg_option_string')
+
+ self.ffmpeg_option_string = string
+
+
def set_full_expand_video_index_flag(self, flag):
if DEBUG_FUNC_FLAG:
- utils.debug_time('app 15555 set_full_expand_video_index_flag')
+ utils.debug_time('app 15556 set_full_expand_video_index_flag')
if not flag:
self.full_expand_video_index_flag = False
@@ -16966,10 +17960,34 @@ class TartubeApp(Gtk.Application):
self.scheduled_check_wait_value = value
+ def set_scheduled_custom_mode(self, value):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 16062 set_scheduled_custom_mode')
+
+ self.scheduled_custom_mode = value
+
+
+ def set_scheduled_custom_wait_unit(self, unit):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 16063 set_scheduled_custom_wait_unit')
+
+ self.scheduled_custom_wait_unit = unit
+
+
+ def set_scheduled_custom_wait_value(self, value):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 16064 set_scheduled_custom_wait_value')
+
+ self.scheduled_custom_wait_value = value
+
+
def set_scheduled_dl_mode(self, value):
if DEBUG_FUNC_FLAG:
- utils.debug_time('app 16062 set_scheduled_dl_mode')
+ utils.debug_time('app 16065 set_scheduled_dl_mode')
self.scheduled_dl_mode = value
@@ -17279,6 +18297,14 @@ class TartubeApp(Gtk.Application):
self.video_res_default = value
+ def set_ytdl_fork(self, value):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('app 16301 set_ytdl_fork')
+
+ self.ytdl_fork = value
+
+
def set_ytdl_output_ignore_json_flag(self, flag):
if DEBUG_FUNC_FLAG:
diff --git a/tartube/mainwin.py b/tartube/mainwin.py
index 00c7c6b..a6f8ace 100644
--- a/tartube/mainwin.py
+++ b/tartube/mainwin.py
@@ -128,6 +128,7 @@ class MainWin(Gtk.ApplicationWindow):
self.import_db_menu_item = None # Gtk.MenuItem
self.switch_view_menu_item = None # Gtk.MenuItem
self.test_menu_item = None # Gtk.MenuItem
+ self.test_code_menu_item = None # Gtk.MenuItem
self.show_hidden_menu_item = None # Gtk.MenuItem
self.check_all_menu_item = None # Gtk.MenuItem
self.download_all_menu_item = None # Gtk.MenuItem
@@ -457,8 +458,10 @@ class MainWin(Gtk.ApplicationWindow):
# 'row_num': the row on the treeview, matching
# self.results_list_row_count
# 'keep_description', 'keep_info', 'keep_annotations',
- # 'keep_thumbnail': flags from the options.OptionsManager
- # object used for to download the video (not added to the
+ # 'keep_thumbnail', 'move_description', 'move_info',
+ # 'move_annotations', 'move_thumbnail': flags from the
+ # options.OptionsManager object used for to download the
+ # video ('keep_description', etc, are not not added to the
# dictionary at all for simulated downloads)
self.results_list_temp_list = []
@@ -553,10 +556,9 @@ class MainWin(Gtk.ApplicationWindow):
self.visible_tab_num = 0
# List of configuration windows (anything inheriting from
- # config.GenericConfigWin) that are currently open. A download/
- # update/refresh/info/tidy operation cannot start when one of these
- # windows are open (and the windows cannot be opened during such an
- # operation)
+ # config.GenericConfigWin) that are currently open. An operation
+ # cannot start when one of these windows are open (and the windows
+ # cannot be opened during such an operation)
self.config_win_list = []
# Dialogue window IVs
@@ -936,17 +938,28 @@ class MainWin(Gtk.ApplicationWindow):
media_sub_menu.append(self.show_hidden_menu_item)
self.show_hidden_menu_item.set_action_name('app.show_hidden_menu')
- if self.app_obj.debug_test_media_menu_flag:
+ if self.app_obj.debug_test_media_menu_flag \
+ or self.app_obj.debug_test_code_menu_flag:
# Separator
media_sub_menu.append(Gtk.SeparatorMenuItem())
+ if self.app_obj.debug_test_media_menu_flag:
+
self.test_menu_item = Gtk.MenuItem.new_with_mnemonic(
_('_Add test media'),
)
media_sub_menu.append(self.test_menu_item)
self.test_menu_item.set_action_name('app.test_menu')
+ if self.app_obj.debug_test_code_menu_flag:
+
+ self.test_code_menu_item = Gtk.MenuItem.new_with_mnemonic(
+ _('_Run test code'),
+ )
+ media_sub_menu.append(self.test_code_menu_item)
+ self.test_code_menu_item.set_action_name('app.test_code_menu')
+
# Operations column
ops_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Operations'))
self.menubar.add(ops_menu_column)
@@ -982,14 +995,15 @@ class MainWin(Gtk.ApplicationWindow):
# Separator
ops_sub_menu.append(Gtk.SeparatorMenuItem())
+ downloader = self.app_obj.get_downloader()
self.update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic(
- _('Update _youtube-dl'),
+ _('U_pdate') + ' ' + downloader,
)
ops_sub_menu.append(self.update_ytdl_menu_item)
self.update_ytdl_menu_item.set_action_name('app.update_ytdl_menu')
self.test_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic(
- _('_Test youtube-dl...'),
+ _('_Test') + ' ' + downloader,
)
ops_sub_menu.append(self.test_ytdl_menu_item)
self.test_ytdl_menu_item.set_action_name('app.test_ytdl_menu')
@@ -1280,29 +1294,6 @@ class MainWin(Gtk.ApplicationWindow):
'app.switch_view_toolbutton',
)
- if self.app_obj.debug_test_media_toolbar_flag:
-
- if not squeeze_flag:
- self.test_toolbutton = Gtk.ToolButton.new(
- Gtk.Image.new_from_pixbuf(
- self.pixbuf_dict['tool_test_small'],
- ),
- )
- self.test_toolbutton.set_label(_('Test'))
- self.test_toolbutton.set_is_important(True)
- else:
- self.test_toolbutton = Gtk.ToolButton.new(
- Gtk.Image.new_from_pixbuf(
- self.pixbuf_dict['tool_test_large'],
- ),
- )
-
- self.main_toolbar.insert(self.test_toolbutton, -1)
- self.test_toolbutton.set_tooltip_text(
- _('Add test media data objects'),
- )
- self.test_toolbutton.set_action_name('app.test_toolbutton')
-
if squeeze_flag:
self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1)
@@ -1875,6 +1866,13 @@ class MainWin(Gtk.ApplicationWindow):
)
self.progress_list_treeview.set_model(self.progress_list_liststore)
+ # Limit the size of the 'Source' and 'Incoming file' columns. The
+ # others always contain few characters, so let them expand as they
+ # please
+ for column in [4, 7]:
+ column_obj = self.progress_list_treeview.get_column(column)
+ column_obj.set_fixed_width(200)
+
# Lower half
frame2 = Gtk.Frame()
self.progress_paned.add2(frame2)
@@ -1950,6 +1948,10 @@ class MainWin(Gtk.ApplicationWindow):
)
self.results_list_treeview.set_model(self.results_list_liststore)
+ # Limit the size of the 'New videos' column (the 'Downloaded to' column
+ column_obj = self.results_list_treeview.get_column(3)
+ column_obj.set_fixed_width(300)
+
# Strip of widgets at the bottom, arranged in a grid
grid = Gtk.Grid()
vbox.pack_start(grid, False, False, 0)
@@ -2357,6 +2359,13 @@ class MainWin(Gtk.ApplicationWindow):
self.classic_progress_liststore,
)
+ # Limit the size of the 'Source' and 'Incoming file' columns. The
+ # others always contain few characters, so let them expand as they
+ # please
+ for column in [2, 5]:
+ column_obj = self.classic_progress_treeview.get_column(column)
+ column_obj.set_fixed_width(150)
+
# Fifth row - a strip of buttons that apply to rows in the Classic
# Progres List. We use another new hbox to avoid messing up the
# grid layout
@@ -2903,9 +2912,10 @@ class MainWin(Gtk.ApplicationWindow):
def show_progress_bar(self, operation_type):
"""Called by mainapp.TartubeApp.download_manager_continue(),
- .refresh_manager_continue(), .tidy_manager_start().
+ .refresh_manager_continue(), .tidy_manager_start(),
+ .process_manager_start().
- At the start of a download/refresh/tidy operation, replace
+ At the start of a download/refresh/tidy/process operation, replace
self.download_media_button with a progress bar (and a label just above
it).
@@ -2913,8 +2923,8 @@ class MainWin(Gtk.ApplicationWindow):
operation_type (str): The type of operation: 'download' for a
download operation, 'check' for a download operation with
- simulated downloads, 'refresh' for a refresh operation, or
- 'tidy' for a tidy operation
+ simulated downloads, 'refresh' for a refresh operation, 'tidy'
+ for a tidy operation, or 'process' for a process operation
"""
@@ -2930,7 +2940,8 @@ class MainWin(Gtk.ApplicationWindow):
elif operation_type != 'check' \
and operation_type != 'download' \
and operation_type != 'refresh' \
- and operation_type != 'tidy':
+ and operation_type != 'tidy' \
+ and operation_type != 'process':
return self.app_obj.system_error(
202,
'Invalid operation type supplied to progress bar',
@@ -2957,8 +2968,10 @@ class MainWin(Gtk.ApplicationWindow):
self.check_media_button.set_label(_('Downloading...'))
elif operation_type == 'refresh':
self.check_media_button.set_label(_('Refreshing...'))
- else:
+ elif operation_type == 'tidy':
self.check_media_button.set_label(_('Tidying...'))
+ else:
+ self.check_media_button.set_label(_('FFmpeg processing...'))
# (Put the progress bar inside a box, so it doesn't touch the divider,
# because that doesn't look nice)
@@ -2985,8 +2998,10 @@ class MainWin(Gtk.ApplicationWindow):
self.progress_bar.set_text(_('Downloading...'))
elif operation_type == 'refresh':
self.progress_bar.set_text(_('Refreshing...'))
- else:
+ elif operation_type == 'tidy':
self.progress_bar.set_text(_('Tidying...'))
+ else:
+ self.progress_bar.set_text(_('FFmpeg Processing...'))
# Make the changes visible
self.button_box.show_all()
@@ -3076,11 +3091,11 @@ class MainWin(Gtk.ApplicationWindow):
"""Called by downloads.DownloadManager.run(),
refresh.RefreshManager.refresh_from_default_destination(),
- .refresh_from_actual_destination() and
- tidy.TidyManager.tidy_directory().
+ .refresh_from_actual_destination(), tidy.TidyManager.tidy_directory()
+ and process.Processmanager.process_video().
- During a download/refresh/tidy operation, updates the progress bar just
- below the Video Index.
+ During a download/refresh/tidy/process operation, updates the progress
+ bar just below the Video Index.
Args:
@@ -3171,12 +3186,14 @@ class MainWin(Gtk.ApplicationWindow):
if not finish_flag:
+ downloader = self.app_obj.get_downloader();
+
if operation_type == 'ffmpeg':
self.check_media_button.set_label(_('Installing'))
self.download_media_button.set_label('FFmpeg')
elif operation_type == 'ytdl':
self.check_media_button.set_label(_('Updating'))
- self.download_media_button.set_label('youtube-dl')
+ self.download_media_button.set_label(downloader)
elif operation_type == 'formats':
self.check_media_button.set_label(_('Fetching'))
self.download_media_button.set_label('format list')
@@ -3185,7 +3202,7 @@ class MainWin(Gtk.ApplicationWindow):
self.download_media_button.set_label('subtitle list')
else:
self.check_media_button.set_label(_('Testing'))
- self.download_media_button.set_label('youtube-dl')
+ self.download_media_button.set_label(downloader)
self.check_media_button.set_sensitive(False)
self.download_media_button.set_sensitive(False)
@@ -3312,8 +3329,7 @@ class MainWin(Gtk.ApplicationWindow):
if DEBUG_FUNC_FLAG:
utils.debug_time('mwn 3182 enable_dl_all_buttons')
- # This setting doesn't apply during a download/update/refresh/info/tidy
- # operation
+ # This setting doesn't apply during an operation
if not self.app_obj.current_manager_obj:
self.download_all_menu_item.set_sensitive(True)
self.download_all_toolbutton.set_sensitive(True)
@@ -3331,8 +3347,7 @@ class MainWin(Gtk.ApplicationWindow):
if DEBUG_FUNC_FLAG:
utils.debug_time('mwn 3201 disable_dl_all_buttons')
- # This setting doesn't apply during a download/update/refresh/info/tidy
- # operation
+ # This setting doesn't apply during an operation
if not self.app_obj.current_manager_obj:
self.download_all_menu_item.set_sensitive(False)
self.download_all_toolbutton.set_sensitive(False)
@@ -4291,7 +4306,8 @@ class MainWin(Gtk.ApplicationWindow):
self.on_video_index_delete_container,
media_data_obj,
)
- if self.app_obj.current_manager_obj:
+ if self.app_obj.current_manager_obj \
+ or (media_type == 'folder' and media_data_obj.fixed_flag):
delete_menu_item.set_sensitive(False)
popup_menu.append(delete_menu_item)
@@ -4521,6 +4537,20 @@ class MainWin(Gtk.ApplicationWindow):
downloads_menu_item.set_submenu(downloads_submenu)
popup_menu.append(downloads_menu_item)
+ process_menu_item = Gtk.MenuItem.new_with_mnemonic(
+ _('_Process with FFmpeg...'),
+ )
+ process_menu_item.connect(
+ 'activate',
+ self.on_video_catalogue_process_ffmpeg,
+ video_obj,
+ )
+ popup_menu.append(process_menu_item)
+ if self.app_obj.current_manager_obj \
+ or not video_obj.file_name \
+ or not video_obj.dl_flag:
+ process_menu_item.set_sensitive(False)
+
# Separator
popup_menu.append(Gtk.SeparatorMenuItem())
@@ -4838,6 +4868,7 @@ class MainWin(Gtk.ApplicationWindow):
if not source_flag \
or self.app_obj.update_manager_obj \
or self.app_obj.refresh_manager_obj \
+ or self.app_obj.process_manager_obj \
or live_flag:
dl_watch_menu_item.set_sensitive(False)
@@ -4894,6 +4925,19 @@ class MainWin(Gtk.ApplicationWindow):
)
temp_submenu.append(mark_temp_dl_menu_item)
+ # Process with FFmpeg
+ process_menu_item = Gtk.MenuItem.new_with_mnemonic(
+ _('_Process with FFmpeg...'),
+ )
+ process_menu_item.connect(
+ 'activate',
+ self.on_video_catalogue_process_ffmpeg_multi,
+ video_list,
+ )
+ popup_menu.append(process_menu_item)
+ if self.app_obj.current_manager_obj or not dl_flag:
+ process_menu_item.set_sensitive(False)
+
# Separator
temp_submenu.append(Gtk.SeparatorMenuItem())
@@ -4925,6 +4969,7 @@ class MainWin(Gtk.ApplicationWindow):
if not video_obj.source \
or self.app_obj.update_manager_obj \
or self.app_obj.refresh_manager_obj \
+ or self.app_obj.process_manager_obj \
or temp_folder_flag \
or live_flag:
temp_menu_item.set_sensitive(False)
@@ -5355,6 +5400,20 @@ class MainWin(Gtk.ApplicationWindow):
# Watch video
self.add_watch_video_menu_items(popup_menu, video_obj)
+ process_menu_item = Gtk.MenuItem.new_with_mnemonic(
+ _('_Process with FFmpeg...'),
+ )
+ process_menu_item.connect(
+ 'activate',
+ self.on_video_catalogue_process_ffmpeg,
+ video_obj,
+ )
+ popup_menu.append(process_menu_item)
+ if self.app_obj.current_manager_obj \
+ or not video_obj.file_name \
+ or not video_obj.dl_flag:
+ process_menu_item.set_sensitive(False)
+
# Separator
popup_menu.append(Gtk.SeparatorMenuItem())
@@ -5449,7 +5508,7 @@ class MainWin(Gtk.ApplicationWindow):
# Update youtube-dl
update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic(
- _('Update youtube-dl'),
+ _('Update') + ' ' + self.app_obj.get_downloader(),
)
update_ytdl_menu_item.connect(
'activate',
@@ -5751,6 +5810,7 @@ class MainWin(Gtk.ApplicationWindow):
if not video_obj.source \
or self.app_obj.update_manager_obj \
or self.app_obj.refresh_manager_obj \
+ or self.app_obj.process_manager_obj \
or video_obj.live_mode != 0:
dl_watch_menu_item.set_sensitive(False)
@@ -7366,8 +7426,8 @@ class MainWin(Gtk.ApplicationWindow):
# All items added. Force the Gtk.ListBox to sort its rows, so that
# videos are displayed in the correct order
# v1.3.112 this call is suspected of causing occasional crashes due
- # to Gtk issues. Disable it, if a download/refresh/tidy operation
- # is in progress
+ # to Gtk issues. Disable it, if a download/refresh/tidy/
+ # livestream operation is in progress
if not self.app_obj.gtk_emulate_broken_flag or (
not self.app_obj.download_manager_obj \
and not self.app_obj.refresh_manager_obj \
@@ -7830,12 +7890,7 @@ class MainWin(Gtk.ApplicationWindow):
),
)
row_list.append(pixbuf)
- row_list.append(
- utils.shorten_string(
- media_data_obj.name,
- self.medium_string_max_len,
- ),
- )
+ row_list.append(media_data_obj.name)
row_list.append(None)
row_list.append(_('Waiting'))
row_list.append(None)
@@ -7990,10 +8045,7 @@ class MainWin(Gtk.ApplicationWindow):
string = string + '/1'
else:
- string = utils.shorten_string(
- dl_stat_dict[key],
- self.medium_string_max_len,
- )
+ string = dl_stat_dict[key]
self.progress_list_liststore.set(
self.progress_list_liststore.get_iter(tree_path),
@@ -8124,10 +8176,7 @@ class MainWin(Gtk.ApplicationWindow):
self.progress_list_liststore.set(
self.progress_list_liststore.get_iter(tree_path),
4,
- utils.shorten_string(
- video_obj.name,
- self.medium_string_max_len,
- ),
+ video_obj.name,
)
@@ -8162,8 +8211,7 @@ class MainWin(Gtk.ApplicationWindow):
def results_list_add_row(self, download_item_obj, video_obj, \
- keep_description=None, keep_info=None, keep_annotations=None,
- keep_thumbnail=None):
+ mini_options_dict):
"""Called by mainapp.TartubeApp.announce_video_download().
@@ -8186,12 +8234,13 @@ class MainWin(Gtk.ApplicationWindow):
video_obj (media.Video): The media data object for the downloaded
video
- keep_description (True, False, None):
- keep_info (True, False, None):
- keep_annotations (True, False, None):
- keep_thumbnail (bool): Settings from the options.OptionsManager
- object used to download the video (all of them set to 'None'
- for a simulated download)
+ mini_options_dict (dict): A dictionary containing a subset of
+ download options from the the options.OptionsManager object
+ used to download the video. It contains zero, some or all of
+ the following download options:
+
+ keep_description keep_info keep_annotations keep_thumbnail
+ move_description move_info move_annotations move_thumbnail
"""
@@ -8235,12 +8284,7 @@ class MainWin(Gtk.ApplicationWindow):
),
)
row_list.append(pixbuf)
- row_list.append(
- utils.shorten_string(
- video_obj.nickname,
- self.medium_string_max_len,
- ),
- )
+ row_list.append(video_obj.nickname)
# (For a simulated download, the video duration (etc) will already be
# available, so we can display those values)
@@ -8263,12 +8307,7 @@ class MainWin(Gtk.ApplicationWindow):
row_list.append(video_obj.dl_flag)
row_list.append(pixbuf2)
- row_list.append(
- utils.shorten_string(
- video_obj.parent_obj.name,
- self.medium_string_max_len,
- ),
- )
+ row_list.append(video_obj.parent_obj.name)
# Create a new row in the treeview. Doing the .show_all() first
# prevents a Gtk error (for unknown reasons)
@@ -8286,17 +8325,8 @@ class MainWin(Gtk.ApplicationWindow):
'row_num': self.results_list_row_count,
}
- if keep_description is not None:
- temp_dict['keep_description'] = keep_description
-
- if keep_info is not None:
- temp_dict['keep_info'] = keep_info
-
- if keep_annotations is not None:
- temp_dict['keep_annotations'] = keep_annotations
-
- if keep_thumbnail is not None:
- temp_dict['keep_thumbnail'] = keep_thumbnail
+ for key in mini_options_dict.keys():
+ temp_dict[key] = mini_options_dict[key]
# Update IVs
self.results_list_temp_list.append(temp_dict)
@@ -8398,10 +8428,7 @@ class MainWin(Gtk.ApplicationWindow):
self.results_list_liststore.set(
row_iter,
3,
- utils.shorten_string(
- video_obj.nickname,
- self.medium_string_max_len,
- ),
+ video_obj.nickname,
)
if video_obj.duration is not None:
@@ -8433,10 +8460,7 @@ class MainWin(Gtk.ApplicationWindow):
self.results_list_liststore.set(
row_iter,
9,
- utils.shorten_string(
- video_obj.parent_obj.name,
- self.medium_string_max_len,
- ),
+ video_obj.parent_obj.name,
)
else:
@@ -8510,12 +8534,14 @@ class MainWin(Gtk.ApplicationWindow):
),
),
)
- row_list.append(
- utils.shorten_string(
- dummy_obj.source,
- self.medium_string_max_len,
- ),
- )
+
+ # (Don't display the https:// bit, that's just wasted space
+ source = dummy_obj.source
+ match = re.search('^https?\:\/\/(.*)', source)
+ if match:
+ source = match.group(1)
+
+ row_list.append(source)
row_list.append(None)
row_list.append(_('Waiting'))
row_list.append(None)
@@ -8883,10 +8909,7 @@ class MainWin(Gtk.ApplicationWindow):
string = string + '/1'
else:
- string = utils.shorten_string(
- dl_stat_dict[key],
- self.medium_string_max_len,
- )
+ string = dl_stat_dict[key]
self.classic_progress_liststore.set(
self.classic_progress_liststore.get_iter(row_path),
@@ -9322,14 +9345,29 @@ class MainWin(Gtk.ApplicationWindow):
adj.set_value(adj.get_upper() - adj.get_page_size())
+ def output_tab_show_first_page(self):
+
+ """Called by mainapp.TartubeApp.update_manager_start.
+
+ Switches to the first tab of the Output Tab (not including the summary
+ tab, if it's open).
+ """
+
+ self.notebook.set_current_page(3)
+ if not self.output_tab_summary_flag:
+ self.output_notebook.set_current_page(0)
+ else:
+ self.output_notebook.set_current_page(1)
+
+
def output_tab_reset_pages(self):
"""Called by mainapp.TartubeApp.download_manager_continue(),
.update_manager_start(), .refresh_manager_continue(),
.info_manager_start() and .tidy_manager_start().
- At the start of a download/update/refresh/info/tidy operation, empty
- the pages in the Output Tab (if allowed).
+ At the start of an operation, empty the pages in the Output Tab (if
+ allowed).
"""
if DEBUG_FUNC_FLAG:
@@ -10158,7 +10196,7 @@ class MainWin(Gtk.ApplicationWindow):
count = len(media_data_obj.child_list)
if count < self.mark_video_lower_limit:
- # The operation should be quick
+ # The procedure should be quick
for child_obj in media_data_obj.child_list:
if isinstance(child_obj, media.Video):
self.app_obj.mark_video_bookmark(child_obj, True)
@@ -10211,7 +10249,7 @@ class MainWin(Gtk.ApplicationWindow):
count = len(media_data_obj.child_list)
if count < self.mark_video_lower_limit:
- # The operation should be quick
+ # The procedure should be quick
for child_obj in media_data_obj.child_list:
if isinstance(child_obj, media.Video):
self.app_obj.mark_video_bookmark(child_obj, False)
@@ -10450,7 +10488,7 @@ class MainWin(Gtk.ApplicationWindow):
count = len(media_data_obj.child_list)
if count < self.mark_video_lower_limit:
- # The operation should be quick
+ # The procedure should be quick
for child_obj in media_data_obj.child_list:
if isinstance(child_obj, media.Video):
self.app_obj.mark_video_waiting(child_obj, True)
@@ -10503,7 +10541,7 @@ class MainWin(Gtk.ApplicationWindow):
count = len(media_data_obj.child_list)
if count < self.mark_video_lower_limit:
- # The operation should be quick
+ # The procedure should be quick
for child_obj in media_data_obj.child_list:
if isinstance(child_obj, media.Video):
self.app_obj.mark_video_waiting(child_obj, False)
@@ -11454,10 +11492,12 @@ class MainWin(Gtk.ApplicationWindow):
utils.debug_time('mwn 10899 on_video_catalogue_dl_and_watch')
# Can't download the video if it has no source, or if an update/
- # refresh operation has started since the popup menu was created
+ # refresh/process operation has started since the popup menu was
+ # created
if not media_data_obj.dl_flag or not media_data_obj.source \
or self.app_obj.update_manager_obj \
- or self.app_obj.refresh_manager_obj:
+ or self.app_obj.refresh_manager_obj \
+ or self.app_obj.process_manager_obj:
# Download the video, and mark it to be opened in the system's
# default media player as soon as the download operation is
@@ -11493,10 +11533,12 @@ class MainWin(Gtk.ApplicationWindow):
mod_list.append(media_data_obj)
# Can't download the videos if none have no source, or if an update/
- # refresh operation has started since the popup menu was created
+ # refresh/process operation has started since the popup menu was
+ # created
if mod_list \
and not self.app_obj.update_manager_obj \
- and not self.app_obj.refresh_manager_obj:
+ or self.app_obj.refresh_manager_obj \
+ or self.app_obj.process_manager_obj:
# Download the videos, and mark them to be opened in the system's
# default media player as soon as the download operation is
@@ -11841,12 +11883,13 @@ class MainWin(Gtk.ApplicationWindow):
utils.debug_time('mwn 11284 on_video_catalogue_mark_temp_dl')
# Can't mark the video for download if it has no source, or if an
- # update/refresh/tidy operation has started since the popup menu was
- # created
+ # update/refresh/tidy/process operation has started since the popup
+ # menu was created
if media_data_obj.source \
and not self.app_obj.update_manager_obj \
and not self.app_obj.refresh_manager_obj \
- and not self.app_obj.tidy_manager_obj:
+ and not self.app_obj.tidy_manager_obj \
+ and not self.app_obj.process_manager_obj:
# Create a new media.Video object in the 'Temporary Videos' folder
# (but don't download anything now)
@@ -11891,12 +11934,13 @@ class MainWin(Gtk.ApplicationWindow):
mod_list.append(media_data_obj)
# Can't mark the videos for download if they have no source, or if an
- # update/refresh/tidy operation has started since the popup menu was
- # created
+ # update/refresh/tidy/process operation has started since the popup
+ # menu was created
if mod_list \
and not self.app_obj.update_manager_obj \
and not self.app_obj.refresh_manager_obj \
- and not self.app_obj.tidy_manager_obj:
+ and not self.app_obj.tidy_manager_obj \
+ and not self.app_obj.process_manager_obj:
for media_data_obj in mod_list:
@@ -11980,6 +12024,157 @@ class MainWin(Gtk.ApplicationWindow):
)
+ def on_video_catalogue_process_ffmpeg(self, menu_item, media_data_obj):
+
+ """Called from a callback in self.video_catalogue_popup_menu() and
+ .results_list_popup_menu().
+
+ Sends the right-clicked media.Video object to FFmpeg for
+ post-processing.
+
+ Args:
+
+ menu_item (Gtk.MenuItem): The clicked menu item
+
+ media_data_obj (media.Video): The clicked video object
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 11389 on_video_catalogue_process_ffmpeg')
+
+ # Can't start a process operation if another operation has started
+ # since the popup menu was created, or if the video hasn't been
+ # downloaded
+ if not self.app_obj.current_manager_obj and media_data_obj.dl_flag:
+
+ # (There is a lot of code, so use one function instead of two)
+ self.on_video_catalogue_process_ffmpeg_multi(
+ menu_item,
+ [ media_data_obj ],
+ )
+
+
+ def on_video_catalogue_process_ffmpeg_multi(self, menu_item, \
+ media_data_list):
+
+ """Called from a callback in self.video_catalogue_multi_popup_menu().
+ For efficiency, also called by
+ self.on_video_catalogue_process_ffmpeg().
+
+ Sends the right-clicked media.Video objects to FFmpeg for
+ post-processing.
+
+ Args:
+
+ menu_item (Gtk.MenuItem): The clicked menu item
+
+ media_data_obj (media.Video): The clicked video object
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time(
+ 'mwn 11390 on_video_catalogue_process_ffmpeg_multi',
+ )
+
+ # Can't start a process operation if another operation has started
+ # since the popup menu was created
+ if self.app_obj.current_manager_obj:
+ return
+
+ # Filter out any media.Video objects whose videos haven't been
+ # downloaded
+ mod_list = []
+ for video_obj in media_data_list:
+
+ if video_obj.file_name and video_obj.dl_flag:
+ mod_list.append(video_obj)
+
+ if not mod_list:
+
+ self.app_obj.dialogue_manager_obj.show_msg_dialogue(
+ 'Only downloaded videos can be processed by FFmpeg',
+ 'error',
+ 'ok',
+ )
+
+ return
+
+ # Show the dialogue window, so the user can set FFmpeg options
+ dialogue_win = ProcessDialogue(self, len(mod_list))
+ response = dialogue_win.run()
+
+ # Retrieve the FFmpeg options from the dialogue window
+ option_string = dialogue_win.textbuffer.get_text(
+ dialogue_win.textbuffer.get_start_iter(),
+ dialogue_win.textbuffer.get_end_iter(),
+ False,
+ )
+
+ add_string = dialogue_win.entry.get_text()
+ regex_string = dialogue_win.entry2.get_text()
+ substitute_string = dialogue_win.entry3.get_text()
+ ext_string = dialogue_win.entry4.get_text()
+
+ delete_flag = dialogue_win.checkbutton.get_active()
+ keep_flag = dialogue_win.checkbutton2.get_active()
+
+ # ...before destroying it
+ dialogue_win.destroy()
+
+ # If the user has specified at least one options, then we can proceed
+ if response == Gtk.ResponseType.OK \
+ and (
+ option_string != '' \
+ or add_string != '' \
+ or regex_string != '' \
+ or ext_string != ''
+ ):
+ # Divide the option string into lines, remove empty lines, remove
+ # leading/trailing whitespace, and recombine into a single string
+ option_string = utils.strip_whitespace_multiline(
+ option_string,
+ )
+
+ # For the other strings, just remove leading and/or trailing
+ # whitespace, as appropriate
+ add_string = re.sub(r'\s+$', '', add_string)
+# regex_string = utils.strip_whitespace(regex_string)
+# substitute_string = utils.strip_whitespace(substitute_string)
+ ext_string = utils.strip_whitespace(ext_string)
+
+ # Add a full stop to the beginning of the file extension, if not
+ # already present
+ if ext_string != '' and ext_string[0:1] != '.':
+ ext_string = '.' + ext_string
+
+ # Update IVs
+ self.app_obj.set_ffmpeg_keep_flag(keep_flag)
+ if keep_flag:
+
+ self.app_obj.set_ffmpeg_option_strings(
+ add_string,
+ regex_string,
+ substitute_string,
+ ext_string,
+ )
+
+ self.app_obj.set_ffmpeg_delete_flag(delete_flag)
+
+ # Start the process operation, which sends the specified video to
+ # FFmpeg for processing with the specified options
+ self.app_obj.process_manager_start(
+ option_string,
+ add_string,
+ regex_string,
+ substitute_string,
+ ext_string,
+ delete_flag,
+ mod_list,
+ )
+
+
def on_video_catalogue_re_download(self, menu_item, media_data_obj):
"""Called from a callback in self.video_catalogue_popup_menu().
@@ -12231,11 +12426,13 @@ class MainWin(Gtk.ApplicationWindow):
utils.debug_time('mwn 11657 on_video_catalogue_temp_dl')
# Can't download the video if it has no source, or if an update/
- # refresh/tidy operation has started since the popup menu was created
+ # refresh/tidy/process operation has started since the popup menu was
+ # created
if media_data_obj.source \
and not self.app_obj.update_manager_obj \
and not self.app_obj.refresh_manager_obj \
- and not self.app_obj.tidy_manager_obj:
+ and not self.app_obj.tidy_manager_obj \
+ and not self.app_obj.process_manager_obj:
# Create a new media.Video object in the 'Temporary Videos' folder
new_media_data_obj = self.app_obj.add_video(
@@ -12297,12 +12494,14 @@ class MainWin(Gtk.ApplicationWindow):
mod_list.append(media_data_obj)
# Can't download the videos if none have no source, or if an update/
- # refresh/tidy operation has started since the popup menu was created
+ # refresh/tidy/process operation has started since the popup menu was
+ # created
ready_list = []
if mod_list \
and not self.app_obj.update_manager_obj \
and not self.app_obj.refresh_manager_obj \
- and not self.app_obj.tidy_manager_obj:
+ and not self.app_obj.tidy_manager_obj \
+ and not self.app_obj.process_manager_obj:
for media_data_obj in mod_list:
@@ -12379,8 +12578,7 @@ class MainWin(Gtk.ApplicationWindow):
# If the user specified either (or both) a URL and youtube-dl
# options, then we can proceed
if response == Gtk.ResponseType.OK \
- and (source != '' or options_string != ''):
-
+ and (re.search('\S', source) or re.search('\S', options_string)):
# Start the info operation, which issues the youtube-dl command
# with the specified options
self.app_obj.info_manager_start(
@@ -12776,7 +12974,10 @@ class MainWin(Gtk.ApplicationWindow):
# Launch the video
utils.open_file(
- utils.convert_youtube_to_invidious(media_data_obj.source),
+ utils.convert_youtube_to_invidious(
+ self.app_obj,
+ media_data_obj.source,
+ ),
)
# Mark the video as not new (having been watched)
@@ -13213,7 +13414,10 @@ class MainWin(Gtk.ApplicationWindow):
# Launch the video
utils.open_file(
- utils.convert_youtube_to_invidious(media_data_obj.source),
+ utils.convert_youtube_to_invidious(
+ self.app_obj,
+ media_data_obj.source,
+ ),
)
# Mark the video as not new (having been watched)
@@ -15316,14 +15520,17 @@ class ComplexCatalogueItem(object):
if path:
# Thumbnail file exists, so use it
- thumb_flag = True
- self.thumb_image.set_from_pixbuf(
- self.main_win_obj.app_obj.file_manager_obj.load_to_pixbuf(
- path,
- self.main_win_obj.thumb_width,
- self.main_win_obj.thumb_height,
- ),
- )
+ app_obj = self.main_win_obj.app_obj
+ # (Returns a tuple, who knows why)
+ arglist = app_obj.file_manager_obj.load_to_pixbuf(
+ path,
+ self.main_win_obj.thumb_width,
+ self.main_win_obj.thumb_height,
+ ),
+
+ if arglist[0]:
+ self.thumb_image.set_from_pixbuf(arglist[0])
+ thumb_flag = True
# No thumbnail file found, so use a standard icon file
if not thumb_flag:
@@ -15741,7 +15948,8 @@ class ComplexCatalogueItem(object):
elif self.video_obj.source \
and not self.main_win_obj.app_obj.update_manager_obj \
- and not self.main_win_obj.app_obj.refresh_manager_obj:
+ and not self.main_win_obj.app_obj.refresh_manager_obj \
+ and not self.main_win_obj.app_obj.process_manager_obj:
translate_note = _(
'TRANSLATOR\'S NOTE: If you want to use &, use &' \
@@ -15806,7 +16014,10 @@ class ComplexCatalogueItem(object):
self.watch_invidious_label.set_markup(
'' \
@@ -16095,7 +16306,7 @@ class ComplexCatalogueItem(object):
Checks whether the fifth row of labels (for temporary actions) should
be visible, or not.
- Return values:
+ Returns:
True if the row should be visible, False if not
@@ -16123,7 +16334,7 @@ class ComplexCatalogueItem(object):
Checks whether the sixth row of labels (for marked video actions)
should be visible, or not.
- Return values:
+ Returns:
True if the row should be visible, False if not
@@ -16676,12 +16887,13 @@ class ComplexCatalogueItem(object):
if DEBUG_FUNC_FLAG:
utils.debug_time('mwn 15734 on_click_temp_dl_label')
- # Can't download the video if an update/refresh/tidy operation is in
- # progress
+ # Can't download the video if an update/refresh/info/tidy/process
+ # operation is in progress
if not self.main_win_obj.app_obj.update_manager_obj \
and not self.main_win_obj.app_obj.refresh_manager_obj \
and not self.main_win_obj.app_obj.info_manager_obj \
- and not self.main_win_obj.app_obj.tidy_manager_obj:
+ and not self.main_win_obj.app_obj.tidy_manager_obj \
+ and not self.main_win_obj.app_obj.process_manager_obj:
# Create a new media.Video object in the 'Temporary Videos' folder
new_media_data_obj = self.main_win_obj.app_obj.add_video(
@@ -16731,11 +16943,12 @@ class ComplexCatalogueItem(object):
if DEBUG_FUNC_FLAG:
utils.debug_time('mwn 15789 on_click_temp_dl_watch_label')
- # Can't download the video if an update/refresh/tidy operation is in
- # progress
+ # Can't download the video if an update/refresh/tidy/process operation
+ # is in progress
if not self.main_win_obj.app_obj.update_manager_obj \
and not self.main_win_obj.app_obj.refresh_manager_obj \
- and not self.main_win_obj.app_obj.tidy_manager_obj:
+ and not self.main_win_obj.app_obj.tidy_manager_obj \
+ and not self.main_win_obj.app_obj.process_manager_obj:
# Create a new media.Video object in the 'Temporary Videos' folder
new_media_data_obj = self.main_win_obj.app_obj.add_video(
@@ -16785,11 +16998,12 @@ class ComplexCatalogueItem(object):
if DEBUG_FUNC_FLAG:
utils.debug_time('mwn 15843 on_click_temp_mark_label')
- # Can't mark the video for download if an update/refresh/tidy operation
- # is in progress
+ # Can't mark the video for download if an update/refresh/tidy/process
+ # operation is in progress
if not self.main_win_obj.app_obj.update_manager_obj \
and not self.main_win_obj.app_obj.refresh_manager_obj \
- and not self.main_win_obj.app_obj.tidy_manager_obj:
+ and not self.main_win_obj.app_obj.tidy_manager_obj \
+ and not self.main_win_obj.app_obj.process_manager_obj:
# Create a new media.Video object in the 'Temporary Videos' folder
new_media_data_obj = self.main_win_obj.app_obj.add_video(
@@ -17007,7 +17221,8 @@ class ComplexCatalogueItem(object):
elif not self.video_obj.dl_flag and self.video_obj.source \
and not self.main_win_obj.app_obj.update_manager_obj \
- and not self.main_win_obj.app_obj.refresh_manager_obj:
+ and not self.main_win_obj.app_obj.refresh_manager_obj \
+ and not self.main_win_obj.app_obj.process_manager_obj:
# Download the video, and mark it to be opened in the system's
# default media player as soon as the download operation is
@@ -17244,8 +17459,8 @@ class StatusIcon(Gtk.StatusIcon):
def update_icon(self):
- """Called by self.setup(), and then by mainapp.TartubeApp whenever a
- download/update/refresh/info/tidy operation starts or stops.
+ """Called by self.setup(), and then by mainapp.TartubeApp whenever am
+ operation starts or stops.
Updates the status icon with the correct icon file. The icon file used
depends on whether an operation is in progress or not, and which one.
@@ -17269,6 +17484,8 @@ class StatusIcon(Gtk.StatusIcon):
icon = formats.STATUS_ICON_DICT['tidy_icon']
elif self.app_obj.livestream_manager_obj:
icon = formats.STATUS_ICON_DICT['livestream_icon']
+ elif self.app_obj.process_manager_obj:
+ icon = formats.STATUS_ICON_DICT['process_icon']
else:
icon = formats.STATUS_ICON_DICT['default_icon']
@@ -17421,9 +17638,9 @@ class StatusIcon(Gtk.StatusIcon):
"""Called from a callback in self.popup_menu().
- Stops the current download/update/refresh/info/tidy operation (but not
- livestream operations, which run in the background and are halted
- immediately, if a different type of operation wants to start).
+ Stops the current operation (but not livestream operations, which run
+ in the background and are halted immediately, if a different type of
+ operation wants to start).
Args:
@@ -17448,6 +17665,8 @@ class StatusIcon(Gtk.StatusIcon):
self.app_obj.info_manager_obj.stop_info_operation()
elif self.app_obj.tidy_manager_obj:
self.app_obj.tidy_manager_obj.stop_tidy_operation()
+ elif self.app_obj.process_manager_obj:
+ self.app_obj.processs_manager_obj.stop_process_operation()
def on_quit_menu_item(self, menu_item):
@@ -19855,6 +20074,192 @@ class ImportDialogue(Gtk.Dialog):
mini_dict['import_flag'] = False
+class InstallDialogue(Gtk.Dialog):
+
+ """Called by mainapp.TartubeApp.start() when Tartube first runs on any OS
+ besides MS Windows, when youtube-dl's location was not auto-detected.
+
+ Warns the user about installing both youtube-dl and FFmpeg. If the user
+ clicks the button, auto-detects youtube-dl's location again, and updates
+ IVs accordingly.
+
+ Args:
+
+ main_win_obj (mainwin.MainWin): The parent main window
+
+ """
+
+
+ # Standard class methods
+
+
+ def __init__(self, main_win_obj):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 18541 __init__')
+
+ # IV list - class objects
+ # -----------------------
+ # Tartube's main window
+ self.main_win_obj = main_win_obj
+
+
+ # IV list - Gtk widgets
+ # ---------------------
+ # (none)
+
+
+ # IV list - other
+ # ---------------
+ # (none)
+
+
+ # Code
+ # ----
+
+ Gtk.Dialog.__init__(
+ self,
+ _('Install youtube-dl and FFmpeg'),
+ main_win_obj,
+ Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ )
+
+ self.set_modal(False)
+
+ # Set up the dialogue window
+ box = self.get_content_area()
+
+ grid = Gtk.Grid()
+ box.add(grid)
+ grid.set_border_width(main_win_obj.spacing_size)
+ grid.set_row_spacing(main_win_obj.spacing_size)
+
+ label_length = self.main_win_obj.long_string_max_len
+ label = Gtk.Label(
+ utils.tidy_up_long_string(
+ _(
+ 'Tartube could not auto-detect youtube-dl on your system.' \
+ + ' youtube-dl must be installed before you can use Tartube.',
+ ),
+ label_length,
+ ) + '\n\n' \
+ + utils.tidy_up_long_string(
+ _(
+ 'Without FFmpeg, Tartube cannot download high-resolution' \
+ + ' videos. If you have not already installed FFmpeg, then' \
+ + ' we recommend that you install it now.',
+ ),
+ label_length,
+ ),
+ )
+ grid.attach(label, 0, 0, 1, 1)
+
+ # Separator
+ grid.attach(Gtk.HSeparator(), 0, 1, 1, 1)
+
+ button = Gtk.Button.new_with_label(
+ utils.tidy_up_long_string(
+ _(
+ 'I have now installed youtube-dl, please detect its location',
+ ),
+ label_length,
+ ),
+ )
+ grid.attach(button, 0, 2, 1, 1)
+ button.set_hexpand(False)
+ button.connect('clicked', self.on_auto_detect_clicked)
+
+ button2 = Gtk.Button.new_with_label(
+ utils.tidy_up_long_string(
+ _(
+ 'I have now installed youtube-dl, please open the' \
+ + ' preferences window so I can set its location manually',
+ ),
+ label_length,
+ ),
+ )
+ grid.attach(button2, 0, 3, 1, 1)
+ button2.set_hexpand(False)
+ button2.connect('clicked', self.on_open_config_win_clicked)
+
+ # Separator
+ grid.attach(Gtk.HSeparator(), 0, 4, 1, 1)
+
+ button3 = Gtk.Button.new_with_label('Just close this window')
+ grid.attach(button3, 0, 5, 1, 1)
+ button3.set_hexpand(False)
+ button3.connect('clicked', self.on_close_win_clicked)
+
+ # Display the dialogue window
+ self.show_all()
+
+
+ # Public class methods
+
+
+ def on_auto_detect_clicked(self, button):
+
+ """Called from a callback in self.__init__().
+
+ Mark all channels/playlists/folders to be imported.
+
+ Args:
+
+ button (Gtk.Button): The widget clicked
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 18542 on_auto_detect_clicked')
+
+ # Auto-detect youtube-dl location, and update IVs accordingly
+ self.main_win_obj.app_obj.auto_detect_paths()
+ # Open the system preferences window at the right page (as a
+ # confirmation)
+ config.SystemPrefWin(self.main_win_obj.app_obj, 'paths')
+ # Destroy this window
+ self.destroy()
+
+
+ def on_open_config_win_clicked(self, button):
+
+ """Called from a callback in self.__init__().
+
+ Mark all channels/playlists/folders to be not imported.
+
+ Args:
+
+ button (Gtk.Button): The widget clicked
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 18543 on_open_config_win_clicked')
+
+ # Open the system preferences window at the right page
+ config.SystemPrefWin(self.main_win_obj.app_obj, 'paths')
+ # Destroy this window
+ self.destroy()
+
+
+ def on_close_win_clicked(self, button):
+
+ """Called from a callback in self.__init__().
+
+ Destroys the window.
+
+ Args:
+
+ button (Gtk.Button): The widget clicked
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 18544 on_close_win_clicked')
+
+ self.destroy()
+
+
class MountDriveDialogue(Gtk.Dialog):
"""Called by mainapp.TartubeApp.start() and .make_directory().
@@ -20165,9 +20570,199 @@ class MountDriveDialogue(Gtk.Dialog):
self.destroy()
+class ProcessDialogue(Gtk.Dialog):
+
+ """Called by MainWin.on_video_catalogue_process_ffmpeg_multi().
+
+ Python class handling a dialogue window that lets the user set FFmpeg
+ options, before sending video(s) to FFmpeg for processing (in a process
+ operation).
+
+ Args:
+
+ main_win_obj (mainwin.MainWin): The parent main window
+
+ video_count (int): The number of videos to be sent to FFmpeg
+
+ """
+
+
+ # Standard class methods
+
+
+ def __init__(self, main_win_obj, video_count):
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 19169 __init__')
+
+ # IV list - class objects
+ # -----------------------
+ # Tartube's main window
+ self.main_win_obj = main_win_obj
+
+
+ # IV list - Gtk widgets
+ # ---------------------
+ self.textbuffer = None # Gtk.TextBuffer
+ self.checkbutton = None # Gtk.CheckButton
+ self.checkbutton2 = None # Gtk.CheckButton
+ self.entry = None # Gtk.Entry
+ self.entry2 = None # Gtk.Entry
+ self.entry3 = None # Gtk.Entry
+ self.entry4 = None # Gtk.Entry
+
+
+ # Code
+ # ----
+
+ Gtk.Dialog.__init__(
+ self,
+ _('Process videos with FFmpeg'),
+ main_win_obj,
+ Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ (
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OK, Gtk.ResponseType.OK,
+ )
+ )
+
+ self.set_modal(False)
+
+ # Set up the dialogue window
+ box = self.get_content_area()
+
+ grid = Gtk.Grid()
+ box.add(grid)
+ grid.set_border_width(main_win_obj.spacing_size)
+ grid.set_row_spacing(main_win_obj.spacing_size)
+ grid.set_column_spacing(main_win_obj.spacing_size)
+ grid_width = 3
+
+ if video_count == 1:
+ msg = _('Process 1 video with the following options:')
+ else:
+ msg = _('Process {0} videos with the following options:').format(
+ str(video_count),
+ )
+
+ label = Gtk.Label(msg)
+ grid.attach(label, 0, 0, (grid_width - 1), 1)
+ label.set_alignment(0, 0.5)
+
+ button = Gtk.Button.new_with_label(_('Reset all'))
+ grid.attach(button, (grid_width - 1), 0, 1, 1)
+ button.set_hexpand(False)
+ button.connect('clicked', self.on_reset_button_clicked)
+
+ # Separator
+ grid.attach(Gtk.HSeparator(), 0, 1, grid_width, 1)
+
+ label2 = Gtk.Label(_('Add to end of filename:'))
+ grid.attach(label2, 0, 2, 1, 1)
+ label2.set_alignment(0, 0.5)
+
+ # (Store various widgets as IVs, so the calling function can retrieve
+ # their contents)
+ self.entry = Gtk.Entry()
+ grid.attach(self.entry, 1, 2, (grid_width - 1), 1)
+ self.entry.set_text(main_win_obj.app_obj.ffmpeg_add_string)
+
+ label3 = Gtk.Label(_('If regex matches filename:'))
+ grid.attach(label3, 0, 3, 1, 1)
+ label3.set_alignment(0, 0.5)
+
+ self.entry2 = Gtk.Entry()
+ grid.attach(self.entry2, 1, 3, (grid_width - 1), 1)
+ self.entry2.set_text(main_win_obj.app_obj.ffmpeg_regex_string)
+
+ label4 = Gtk.Label(_('...then apply substitution:'))
+ grid.attach(label4, 0, 4, 1, 1)
+ label4.set_alignment(0, 0.5)
+
+ self.entry3 = Gtk.Entry()
+ grid.attach(self.entry3, 1, 4, (grid_width - 1), 1)
+ self.entry3.set_text(main_win_obj.app_obj.ffmpeg_substitute_string)
+
+ label5 = Gtk.Label(_('Change file extension:'))
+ grid.attach(label5, 0, 5, 1, 1)
+ label5.set_alignment(0, 0.5)
+
+ self.entry4 = Gtk.Entry()
+ grid.attach(self.entry4, 1, 5, (grid_width - 1), 1)
+ self.entry4.set_text(main_win_obj.app_obj.ffmpeg_ext_string)
+
+ frame = Gtk.Frame()
+ grid.attach(frame, 0, 6, grid_width, 1)
+ frame.set_label(_('FFmpeg command-line options:'))
+
+ scrolled = Gtk.ScrolledWindow()
+ frame.add(scrolled)
+ scrolled.set_size_request(400, 100)
+
+ textview = Gtk.TextView()
+ scrolled.add(textview)
+ textview.set_wrap_mode(Gtk.WrapMode.WORD)
+ textview.set_hexpand(False)
+ textview.set_editable(True)
+
+ self.textbuffer = textview.get_buffer()
+ # Initialise the textbuffer's contents
+ self.textbuffer.set_text(main_win_obj.app_obj.ffmpeg_option_string)
+
+ self.checkbutton = Gtk.CheckButton()
+ grid.attach(self.checkbutton, 0, 7, (grid_width - 1), 1)
+ self.checkbutton.set_active(
+ main_win_obj.app_obj.ffmpeg_delete_flag
+ )
+ self.checkbutton.set_label(
+ _('If the video has a new name/extension, delete the original'),
+ )
+
+ self.checkbutton2 = Gtk.CheckButton()
+ grid.attach(self.checkbutton2, 0, 8, grid_width, 1)
+ self.checkbutton2.set_active(
+ main_win_obj.app_obj.ffmpeg_keep_flag
+ )
+ self.checkbutton2.set_label(
+ _('Remember these options for the next time'),
+ )
+
+ # Separator
+ grid.attach(Gtk.HSeparator(), 0, 9, grid_width, 1)
+
+ # Display the dialogue window
+ self.show_all()
+
+
+ # Public class methods
+
+
+ def on_reset_button_clicked(self, button):
+
+ """Called from a callback in self.__init__().
+
+ When the 'Reset all' button is clicked, resets the textview and entry
+ boxes.
+
+ Args:
+
+ button (Gtk.Button): The widget clicked
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 19169 on_reset_button_clicked')
+
+ self.textbuffer.set_text('')
+ self.entry.set_text('')
+ self.entry2.set_text('')
+ self.entry3.set_text('')
+ self.entry4.set_text('')
+
+
class RemoveLockFileDialogue(Gtk.Dialog):
- """Called by mainapp.TartubeApp.start().
+ """Called by mainapp.TartubeApp.load_db().
Python class handling a dialogue window that asks the user what to do,
if the database file can't be loaded because it's protected by a lockfile.
@@ -20176,13 +20771,17 @@ class RemoveLockFileDialogue(Gtk.Dialog):
main_win_obj (mainwin.MainWin): The parent main window
+ switch_flag (bool): False when Tartube starts; True when a database
+ had already been loaded, and the user is trying to switch to a
+ different one
+
"""
# Standard class methods
- def __init__(self, main_win_obj):
+ def __init__(self, main_win_obj, switch_flag):
if DEBUG_FUNC_FLAG:
utils.debug_time('mwn 19197 __init__')
@@ -20242,18 +20841,23 @@ class RemoveLockFileDialogue(Gtk.Dialog):
utils.tidy_up_long_string(
_(
'Failed to load the Tartube database file, because another' \
- + ' instance of Tartube seems to be using it',
+ + ' copy of Tartube seems to be using it',
),
label_length,
) + '\n\n' \
+ utils.tidy_up_long_string(
_(
- 'If you are SURE that this is the only instance of Tartube' \
- + ' running on your system. click \'Yes\' to remove the' \
- + ' protection (and then restart Tartube)',
+ 'Do you want to load it anyway?',
),
label_length,
- ) + '\n\n' + _('If you are not sure, then click \'No\''),
+ ) + '\n\n' \
+ + utils.tidy_up_long_string(
+ _(
+ '(Only click \'Yes\' if you are sure that other copies of' \
+ + ' Tartube are not using the database right now)',
+ ),
+ label_length,
+ )
)
grid.attach(label, 1, 0, grid_width, 1)
@@ -20261,15 +20865,18 @@ class RemoveLockFileDialogue(Gtk.Dialog):
grid.attach(Gtk.HSeparator(), 1, 1, grid_width, 1)
button = Gtk.Button.new_with_label(
- _('Yes, I\'m sure'),
+ _('Yes, load the file'),
)
grid.attach(button, 1, 2, 1, 1)
button.set_hexpand(True)
button.connect('clicked', self.on_yes_button_clicked)
- button2 = Gtk.Button.new_with_label(
- _('No, I\'m not sure'),
- )
+ if not switch_flag:
+ msg = _('No, just shut down Tartube')
+ else:
+ msg = _('No, don\'t load the file')
+
+ button2 = Gtk.Button.new_with_label(msg)
grid.attach(button2, 1, 3, 1, 1)
button2.set_hexpand(True)
button2.connect('clicked', self.on_no_button_clicked)
@@ -21398,7 +22005,7 @@ class TestCmdDialogue(Gtk.Dialog):
Gtk.Dialog.__init__(
self,
- _('Test youtube-dl'),
+ _('Test') + ' ' + main_win_obj.app_obj.get_downloader(),
main_win_obj,
Gtk.DialogFlags.DESTROY_WITH_PARENT,
(
@@ -21429,7 +22036,7 @@ class TestCmdDialogue(Gtk.Dialog):
self.entry.set_text(source_url)
label2 = Gtk.Label(
- _('youtube-dl command line options (optional)'),
+ _('Command line options (optional)'),
)
grid.attach(label2, 0, 2, 1, 1)
@@ -21502,6 +22109,8 @@ class TidyDialogue(Gtk.Dialog):
self.checkbutton9 = None # Gtk.CheckButton
self.checkbutton10 = None # Gtk.CheckButton
self.checkbutton11 = None # Gtk.CheckButton
+ self.checkbutton12 = None # Gtk.CheckButton
+ self.checkbutton13 = None # Gtk.CheckButton
# Code
@@ -21544,7 +22153,7 @@ class TidyDialogue(Gtk.Dialog):
self.checkbutton = Gtk.CheckButton()
grid.attach(self.checkbutton, 0, 0, 1, 1)
self.checkbutton.set_label(_('Check that videos are not corrupted'))
- self.checkbutton.connect('toggled', self.on_checkbutton_toggled)
+ # (signal_connect appears below)
self.checkbutton2 = Gtk.CheckButton()
grid.attach(self.checkbutton2, 0, 1, 1, 1)
@@ -21561,7 +22170,7 @@ class TidyDialogue(Gtk.Dialog):
self.checkbutton3.set_label(_('Check that videos do/don\'t exist'))
self.checkbutton4 = Gtk.CheckButton()
- grid.attach(self.checkbutton4, 0, 3, 1, 2)
+ grid.attach(self.checkbutton4, 0, 3, 1, 1)
self.checkbutton4.set_label(
utils.tidy_up_long_string(
_(
@@ -21571,10 +22180,10 @@ class TidyDialogue(Gtk.Dialog):
label_length,
),
)
- self.checkbutton4.connect('toggled', self.on_checkbutton4_toggled)
+ # (signal_connect appears below)
self.checkbutton5 = Gtk.CheckButton()
- grid.attach(self.checkbutton5, 0, 5, 1, 1)
+ grid.attach(self.checkbutton5, 0, 4, 1, 2)
self.checkbutton5.set_label(
utils.tidy_up_long_string(
_('Also delete all video/audio files with the same name'),
@@ -21583,46 +22192,70 @@ class TidyDialogue(Gtk.Dialog):
)
self.checkbutton5.set_sensitive(False)
- # Right column
self.checkbutton6 = Gtk.CheckButton()
- grid.attach(self.checkbutton6, 1, 0, 1, 1)
- self.checkbutton6.set_label(_('Delete all description files'))
+ grid.attach(self.checkbutton6, 0, 6, 1, 1)
+ self.checkbutton6.set_label(_('Delete all archive files'))
+ # Right column
self.checkbutton7 = Gtk.CheckButton()
- grid.attach(self.checkbutton7, 1, 1, 1, 1)
- self.checkbutton7.set_label(_('Delete all metadata (JSON) files'))
+ grid.attach(self.checkbutton7, 1, 0, 1, 1)
+ self.checkbutton7.set_label(_('Move thumbnails into own folder'))
+ # (signal_connect appears below)
self.checkbutton8 = Gtk.CheckButton()
- grid.attach(self.checkbutton8, 1, 2, 1, 1)
- self.checkbutton8.set_label(_('Delete all annotation files'))
+ grid.attach(self.checkbutton8, 1, 1, 1, 1)
+ self.checkbutton8.set_label(_('Delete all thumbnail files'))
self.checkbutton9 = Gtk.CheckButton()
- grid.attach(self.checkbutton9, 1, 3, 1, 1)
- self.checkbutton9.set_label(_('Delete all thumbnail files'))
+ grid.attach(self.checkbutton9, 1, 2, 1, 1)
+ self.checkbutton9.set_label(
+ utils.tidy_up_long_string(
+ _('Convert .webp thumbnails to .jpg using FFmpeg'),
+ label_length,
+ ),
+ )
- # v2.1.027. In June 2020, YouTube started serving .webp thumbnails.
- # At the time of writing, Gtk can't display them. A youtube-dl fix is
- # expected, which will convert .webp thumbnails to .jpg; in
- # anticipation of that, we add an option to remove .webp files
self.checkbutton10 = Gtk.CheckButton()
- grid.attach(self.checkbutton10, 1, 4, 1, 1)
- self.checkbutton10.set_label(_('Delete .webp/malformed .jpg files'))
+ grid.attach(self.checkbutton10, 1, 3, 1, 1)
+ self.checkbutton10.set_label(
+ utils.tidy_up_long_string(
+ _('Move other metadata files into own folder'),
+ label_length,
+ ),
+ )
+ # (signal_connect appears below)
self.checkbutton11 = Gtk.CheckButton()
- grid.attach(self.checkbutton11, 1, 5, 1, 1)
- self.checkbutton11.set_label(_('Delete all youtube-dl archive files'))
+ grid.attach(self.checkbutton11, 1, 4, 1, 1)
+ self.checkbutton11.set_label(_('Delete all description files'))
+
+ self.checkbutton12 = Gtk.CheckButton()
+ grid.attach(self.checkbutton12, 1, 5, 1, 1)
+ self.checkbutton12.set_label(_('Delete all metadata (JSON) files'))
+
+ self.checkbutton13 = Gtk.CheckButton()
+ grid.attach(self.checkbutton13, 1, 6, 1, 1)
+ self.checkbutton13.set_label(_('Delete all annotation files'))
# Bottom strip
button = Gtk.Button.new_with_label(_('Select all'))
- grid.attach(button, 0, 6, 1, 1)
+ grid.attach(button, 0, 7, 1, 1)
button.set_hexpand(False)
- button.connect('clicked', self.on_select_all_clicked)
+ # (signal_connect appears below)
- button = Gtk.Button.new_with_label(_('Select none'))
- grid.attach(button, 1, 6, 1, 1)
- button.set_hexpand(False)
- button.connect('clicked', self.on_select_none_clicked)
+ button2 = Gtk.Button.new_with_label(_('Select none'))
+ grid.attach(button2, 1, 7, 1, 1)
+ button2.set_hexpand(False)
+ # (signal_connect appears below)
+
+ # (signal_connects from above)
+ self.checkbutton.connect('toggled', self.on_checkbutton_toggled)
+ self.checkbutton4.connect('toggled', self.on_checkbutton4_toggled)
+ self.checkbutton7.connect('toggled', self.on_checkbutton12_toggled)
+ self.checkbutton10.connect('toggled', self.on_checkbutton13_toggled)
+ button.connect('clicked', self.on_select_all_clicked)
+ button2.connect('clicked', self.on_select_none_clicked)
# Display the dialogue window
self.show_all()
@@ -21676,6 +22309,63 @@ class TidyDialogue(Gtk.Dialog):
self.checkbutton5.set_sensitive(True)
+ def on_checkbutton12_toggled(self, checkbutton):
+
+ """Called from a callback in self.__init__().
+
+ When the 'Move thumbnails into to own folder' button is toggled, update
+ other widgets.
+
+ Args:
+
+ checkbutton (Gtk.CheckButton): The clicked widget
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 20580 on_checkbutton12_toggled')
+
+ if not checkbutton.get_active():
+ self.checkbutton8.set_sensitive(True)
+
+ else:
+ self.checkbutton8.set_active(False)
+ self.checkbutton8.set_sensitive(False)
+
+
+ def on_checkbutton13_toggled(self, checkbutton):
+
+ """Called from a callback in self.__init__().
+
+ When the 'Move other metadata files into own folder' button is toggled,
+ update other widgets.
+
+ Args:
+
+ checkbutton (Gtk.CheckButton): The clicked widget
+
+ """
+
+ if DEBUG_FUNC_FLAG:
+ utils.debug_time('mwn 20581 on_checkbutton13_toggled')
+
+ if not checkbutton.get_active():
+
+ self.checkbutton11.set_sensitive(True)
+ self.checkbutton12.set_sensitive(True)
+ self.checkbutton13.set_sensitive(True)
+
+ else:
+
+ self.checkbutton11.set_active(False)
+ self.checkbutton12.set_active(False)
+ self.checkbutton13.set_active(False)
+
+ self.checkbutton11.set_sensitive(False)
+ self.checkbutton12.set_sensitive(False)
+ self.checkbutton13.set_sensitive(False)
+
+
def on_select_all_clicked(self, button):
"""Called from a callback in self.__init__().
@@ -21698,10 +22388,17 @@ class TidyDialogue(Gtk.Dialog):
self.checkbutton5.set_active(True)
self.checkbutton6.set_active(True)
self.checkbutton7.set_active(True)
- self.checkbutton8.set_active(True)
+ self.checkbutton8.set_active(False)
self.checkbutton9.set_active(True)
self.checkbutton10.set_active(True)
- self.checkbutton11.set_active(True)
+ self.checkbutton11.set_active(False)
+ self.checkbutton12.set_active(False)
+ self.checkbutton13.set_active(False)
+
+ self.checkbutton8.set_sensitive(False)
+ self.checkbutton11.set_sensitive(False)
+ self.checkbutton12.set_sensitive(False)
+ self.checkbutton13.set_sensitive(False)
def on_select_none_clicked(self, button):
@@ -21730,3 +22427,15 @@ class TidyDialogue(Gtk.Dialog):
self.checkbutton9.set_active(False)
self.checkbutton10.set_active(False)
self.checkbutton11.set_active(False)
+ self.checkbutton12.set_active(False)
+ self.checkbutton13.set_active(False)
+
+ if not mainapp.HAVE_MOVIEPY_FLAG \
+ or self.main_win_obj.app_obj.refresh_moviepy_timeout == 0:
+ self.checkbutton.set_sensitive(False)
+ self.checkbutton2.set_sensitive(False)
+
+ self.checkbutton8.set_sensitive(True)
+ self.checkbutton11.set_sensitive(True)
+ self.checkbutton12.set_sensitive(True)
+ self.checkbutton13.set_sensitive(True)
diff --git a/tartube/media.py b/tartube/media.py
index acfca87..d8d652a 100644
--- a/tartube/media.py
+++ b/tartube/media.py
@@ -33,6 +33,7 @@ import time
# Import our modules
+import formats
import mainapp
import utils
# Use same gettext translations
@@ -1593,10 +1594,12 @@ class Video(GenericMedia):
"""
- descrip_path = self.get_actual_path_by_ext(app_obj, '.description')
- text = app_obj.file_manager_obj.load_text(descrip_path)
- if text is not None:
- self.set_video_descrip(text, max_length)
+ descrip_path = self.check_actual_path_by_ext(app_obj, '.description')
+ if (descrip_path):
+
+ text = app_obj.file_manager_obj.load_text(descrip_path)
+ if text is not None:
+ self.set_video_descrip(text, max_length)
# Set accessors
@@ -1846,6 +1849,10 @@ class Video(GenericMedia):
app_obj (mainapp.TartubeApp): The main application
+ Returns:
+
+ The path described above
+
"""
return os.path.abspath(
@@ -1880,6 +1887,10 @@ class Video(GenericMedia):
ext (str): The extension, e.g. 'png' or '.png'
+ Returns:
+
+ The full file path (the file may or may not exist)
+
"""
# Add the full stop, if not supplied by the calling function
@@ -1894,6 +1905,129 @@ class Video(GenericMedia):
)
+ def get_actual_path_in_subdirectory_by_ext(self, app_obj, ext):
+
+ """Can be called by anything.
+
+ Modified version of self.get_actual_path_by_ext().
+
+ The file might be stored in the same directory as its video, or in the
+ sub-directory '.thumbs' (for thumbnails) or '.data' (for everything
+ else).
+
+ self.get_actual_path_by_ext() returns the former; this function returns
+ the latter.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application
+
+ ext (str): The extension, e.g. 'png' or '.png'
+
+ Returns:
+
+ The full file path (the file may or may not exist)
+
+ """
+
+ # Add the full stop, if not supplied by the calling function
+ if not ext.find('.') == 0:
+ ext = '.' + ext
+
+ # There are two sub-directories, one for thumbnails, one for metadata
+ if ext in formats.IMAGE_FORMAT_EXT_LIST:
+
+ return os.path.abspath(
+ os.path.join(
+ self.parent_obj.get_actual_dir(app_obj),
+ app_obj.thumbs_sub_dir,
+ self.file_name + ext,
+ ),
+ )
+
+ else:
+
+ return os.path.abspath(
+ os.path.join(
+ self.parent_obj.get_actual_dir(app_obj),
+ app_obj.metadata_sub_dir,
+ self.file_name + ext,
+ ),
+ )
+
+
+ def check_actual_path_by_ext(self, app_obj, ext):
+
+ """Can be called by anything.
+
+ Modified version of self.get_actual_path_by_ext().
+
+ The file has the same name as its video, but with a different extension
+ (for example, the video's thumbnail file).
+
+ The file might be stored in the same directory as its video, or in the
+ sub-directory '.thumbs' (for thumbnails) or '.data' (for everything
+ else).
+
+ This function checks to see whether the file exists in the same
+ directory as its folder and, if so, returns the file path. If not, it
+ checks to see whether the file exists in the '.thumbs' or '.data'
+ sub-directory and, if so, returns the file path.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application
+
+ ext (str): The extension, e.g. 'png' or '.png'
+
+ Returns:
+
+ The full path to the file if it exists, or None if not
+
+ """
+
+ # Add the full stop, if not supplied by the calling function
+ if not ext.find('.') == 0:
+ ext = '.' + ext
+
+ # Check the normal location
+ main_path = os.path.abspath(
+ os.path.join(
+ self.parent_obj.get_actual_dir(app_obj),
+ self.file_name + ext,
+ ),
+ )
+
+ if os.path.isfile(main_path):
+ return main_path
+
+ # Check the sub-directory location
+ if ext in formats.IMAGE_FORMAT_EXT_LIST:
+
+ subdir_path = os.path.abspath(
+ os.path.join(
+ self.parent_obj.get_actual_dir(app_obj),
+ app_obj.thumbs_sub_dir,
+ self.file_name + ext,
+ ),
+ )
+
+ else:
+
+ subdir_path = os.path.abspath(
+ os.path.join(
+ self.parent_obj.get_actual_dir(app_obj),
+ app_obj.metadata_sub_dir,
+ self.file_name + ext,
+ ),
+ )
+
+ if os.path.isfile(subdir_path):
+ return subdir_path
+ else:
+ return None
+
+
def get_default_path(self, app_obj):
"""Can be called by anything.
@@ -1909,6 +2043,10 @@ class Video(GenericMedia):
app_obj (mainapp.TartubeApp): The main application
+ Returns:
+
+ The full file path (the file may or may not exist)
+
"""
return os.path.abspath(
@@ -1938,6 +2076,10 @@ class Video(GenericMedia):
ext (str): The extension, e.g. 'png' or '.png'
+ Returns:
+
+ The full file path (the file may or may not exist)
+
"""
# Add the full stop, if not supplied by the calling function
@@ -1952,6 +2094,57 @@ class Video(GenericMedia):
)
+ def get_default_path_in_subdirectory_by_ext(self, app_obj, ext):
+
+ """Can be called by anything.
+
+ Modified version of self.get_default_path_by_ext().
+
+ The file might be stored in the same directory as its video, or in the
+ sub-directory '.thumbs' (for thumbnails) or '.data' (for everything
+ else).
+
+ self.get_default_path_by_ext() returns the former; this function
+ returns the latter.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application
+
+ ext (str): The extension, e.g. 'png' or '.png'
+
+ Returns:
+
+ The full file path (the file may or may not exist)
+
+ """
+
+ # Add the full stop, if not supplied by the calling function
+ if not ext.find('.') == 0:
+ ext = '.' + ext
+
+ # There are two sub-directories, one for thumbnails, one for metadata
+ if ext in formats.IMAGE_FORMAT_EXT_LIST:
+
+ return os.path.abspath(
+ os.path.join(
+ self.parent_obj.get_default_dir(app_obj),
+ app_obj.thumbs_sub_dir,
+ self.file_name + ext,
+ ),
+ )
+
+ else:
+
+ return os.path.abspath(
+ os.path.join(
+ self.parent_obj.get_default_dir(app_obj),
+ app_obj.metadata_sub_dir,
+ self.file_name + ext,
+ ),
+ )
+
+
def get_file_size_string(self):
"""Can be called by anything.
diff --git a/tartube/options.py b/tartube/options.py
index c19b39f..968c600 100644
--- a/tartube/options.py
+++ b/tartube/options.py
@@ -190,10 +190,6 @@ class OptionsManager(object):
write_thumbnail (bool): If True youtube-dl will write thumbnail image
to disc
- VERBOSITY / SIMULATION OPTIONS
-
- youtube_dl_debug (bool): When True, will pass '-v' flag to youtube-dl
-
WORKAROUNDS
force_encoding (str): Force the specified encoding
@@ -338,6 +334,15 @@ class OptionsManager(object):
TARTUBE OPTIONS (not passed to youtube-dl directly)
+ move_description (bool):
+ move_info (bool):
+ move_annotations (bool):
+ move_thumbnail (bool):
+ During a download operation (real or simulated), if these values
+ are True, the video description/JSON/annotations files are moved to
+ a '.data' sub-directory, and the thumbnails are moved to a
+ '.thumbs' sub-directory, inside the directory containing the videos
+
keep_description (bool):
keep_info (bool):
keep_annotations (bool):
@@ -594,6 +599,10 @@ class OptionsManager(object):
'min_filesize_unit' : '',
'extra_cmd_string' : '',
# TARTUBE OPTIONS
+ 'move_description': False,
+ 'move_info': False,
+ 'move_annotations': False,
+ 'move_thumbnail': False,
'keep_description': False,
'keep_info': False,
'keep_annotations': False,
@@ -624,6 +633,11 @@ class OptionsManager(object):
self.options_dict['write_annotations'] = False
self.options_dict['write_thumbnail'] = False
+ self.options_dict['move_description'] = False
+ self.options_dict['move_info'] = False
+ self.options_dict['move_annotations'] = False
+ self.options_dict['move_thumbnail'] = False
+
self.options_dict['keep_description'] = False
self.options_dict['keep_info'] = False
self.options_dict['keep_annotations'] = False
@@ -857,6 +871,10 @@ class OptionsParser(object):
# OptionHolder('min_filesize_unit', '', ''),
# OptionHolder('extra_cmd_string', '', ''),
# TARTUBE OPTIONS (not given an options.OptionHolder object)
+# OptionHolder('move_description', '', False),
+# OptionHolder('move_info', '', False),
+# OptionHolder('move_annotations', '', False),
+# OptionHolder('move_thumbnail', '', False),
# OptionHolder('keep_description', '', False),
# OptionHolder('keep_info', '', False),
# OptionHolder('keep_annotations', '', False),
diff --git a/tartube/po/messages.pot b/tartube/po/messages.pot
index c240e1e..c69c252 100644
--- a/tartube/po/messages.pot
+++ b/tartube/po/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-08-08 09:32+0100\n"
+"POT-Creation-Date: 2020-09-30 13:36+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -17,569 +17,598 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
-#: .././mainapp.py:2277
+#: .././mainapp.py:807
+msgid ""
+"Failed to convert a thumbnail from .webp to .jpg. No more conversions will "
+"be attempted until you install FFmpeg on your system, or (if FFmpeg is "
+"already installed) you set the correct FFmpeg path. To attempt more "
+"conversions, restart Tartube. To stop these messages, disable thumbnail "
+"conversions"
+msgstr ""
+
+#: .././mainapp.py:2330
msgid ""
"Tartube can't create the folder in which its configuration file is saved"
msgstr ""
-#: .././mainapp.py:2476
-#, python-brace-format
-msgid ""
-"Tartube is assuming that Gtk v{0}.{1}.{2} is broken; some minor cosmetic "
-"features are disabled"
-msgstr ""
-
-#: .././mainapp.py:2516
-msgid "The Tartube database file was not loaded, but is no longer protected"
-msgstr ""
-
-#: .././mainapp.py:2519
-msgid "Restart Tartube to load it"
-msgstr ""
-
-#: .././mainapp.py:2528
+#: .././mainapp.py:2542
msgid "Because of an error, file load/save has been disabled"
msgstr ""
-#: .././mainapp.py:2538
+#: .././mainapp.py:2551
msgid "Because of the error, file load/save has been disabled"
msgstr ""
-#: .././mainapp.py:2569
+#: .././mainapp.py:2586
msgid ""
"youtube-dl must be installed before you can use Tartube. Do you want to "
"install youtube-dl now?"
msgstr ""
-#: .././mainapp.py:2624
+#: .././mainapp.py:2617 .././mainwin.py:20148
+msgid ""
+"Without FFmpeg, Tartube cannot download high-resolution videos. If you have "
+"not already installed FFmpeg, then we recommend that you install it now."
+msgstr ""
+
+#: .././mainapp.py:2673
msgid "There is a download operation in progress."
msgstr ""
-#: .././mainapp.py:2626
+#: .././mainapp.py:2675
msgid "There is an update operation in progress."
msgstr ""
-#: .././mainapp.py:2628
+#: .././mainapp.py:2677
msgid "There is a refresh operation in progress."
msgstr ""
-#: .././mainapp.py:2630
+#: .././mainapp.py:2679
msgid "There is an info operation in progress."
msgstr ""
-#: .././mainapp.py:2632
+#: .././mainapp.py:2681
msgid "There is a tidy operation in progress."
msgstr ""
-#: .././mainapp.py:2637
+#: .././mainapp.py:2683
+msgid "There is a process operation in progress."
+msgstr ""
+
+#: .././mainapp.py:2688
msgid "Are you sure you want to quit Tartube?"
msgstr ""
-#: .././mainapp.py:2841
+#: .././mainapp.py:2899
msgid "Failed to load the Tartube config file (failed sanity check)"
msgstr ""
-#: .././mainapp.py:2864
+#: .././mainapp.py:2922
msgid "Failed to load the Tartube config file (file is locked)"
msgstr ""
-#: .././mainapp.py:2895
+#: .././mainapp.py:2953
msgid "Failed to load the Tartube config file (JSON load failure)"
msgstr ""
-#: .././mainapp.py:2913
+#: .././mainapp.py:2971
msgid "Failed to load the Tartube config file (file is invalid)"
msgstr ""
-#: .././mainapp.py:2931
+#: .././mainapp.py:2989
msgid ""
"Failed to load the Tartube config file (file cannot be read by this version)"
msgstr ""
-#: .././mainapp.py:2946
+#: .././mainapp.py:3004
msgid "Failed to load the Tartube config file (missing file type)"
msgstr ""
-#: .././mainapp.py:3545
+#: .././mainapp.py:3650
msgid "Failed to save the Tartube config file (failed sanity check)"
msgstr ""
-#: .././mainapp.py:3802
+#: .././mainapp.py:3928
msgid "Failed to save the Tartube config file (file is locked)"
msgstr ""
-#: .././mainapp.py:3804 .././mainapp.py:3844 .././mainapp.py:4861
-#: .././mainapp.py:4917 .././mainapp.py:4923
+#: .././mainapp.py:3930 .././mainapp.py:3970 .././mainapp.py:5022
+#: .././mainapp.py:5078 .././mainapp.py:5084
msgid "File load/save has been disabled"
msgstr ""
-#: .././mainapp.py:3823
+#: .././mainapp.py:3949
msgid "Failed to save the Tartube config file (file already in use)"
msgstr ""
-#: .././mainapp.py:3843
+#: .././mainapp.py:3969
msgid "Failed to save the Tartube config file"
msgstr ""
-#: .././mainapp.py:3892 .././mainapp.py:3910 .././mainapp.py:3940
+#: .././mainapp.py:4046 .././mainapp.py:4062 .././mainapp.py:4092
msgid "Failed to load the Tartube database file"
msgstr ""
-#: .././mainapp.py:3955
+#: .././mainapp.py:4107
msgid "The Tartube database file is invalid"
msgstr ""
-#: .././mainapp.py:3971
+#: .././mainapp.py:4123
msgid "Database file can't be read by this version of Tartube"
msgstr ""
-#: .././mainapp.py:4278
+#: .././mainapp.py:4430
msgid "Tartube is applying an essential database update"
msgstr ""
-#: .././mainapp.py:4280
+#: .././mainapp.py:4432
msgid "This might take a few minutes, so please be patient"
msgstr ""
-#: .././mainapp.py:4855 .././mainapp.py:4913 .././mainapp.py:4922
+#: .././mainapp.py:5016 .././mainapp.py:5074 .././mainapp.py:5083
msgid "Failed to save the Tartube database file"
msgstr ""
-#: .././mainapp.py:4858
+#: .././mainapp.py:5019
msgid "(Could not make a backup copy of the existing file)"
msgstr ""
-#: .././mainapp.py:4894
+#: .././mainapp.py:5055
msgid "Failed to save the Tartube database file (file already in use)"
msgstr ""
-#: .././mainapp.py:4915
+#: .././mainapp.py:5076
msgid "A backup of the previous file can be found at:"
msgstr ""
-#: .././mainapp.py:5140 .././mainapp.py:5150
+#: .././mainapp.py:5283 .././mainapp.py:5293
msgid "Database file created"
msgstr ""
-#: .././mainapp.py:5201 .././mainapp.py:5253
+#: .././mainapp.py:5461 .././mainapp.py:5513
#, python-brace-format
msgid ""
"Tartube database '{0}' can't be loaded - another instance of Tartube may be "
"using it. If not, you can fix this problem by deleting the lockfile '{1}'"
msgstr ""
-#: .././mainapp.py:5424
+#: .././mainapp.py:5684
msgid "Tartube's database can't be checked while an operation is in progress"
msgstr ""
-#: .././mainapp.py:5621
+#: .././mainapp.py:5881
msgid "Database check complete, no inconsistencies found"
msgstr ""
-#: .././mainapp.py:5648
+#: .././mainapp.py:5908
msgid "Database check complete, problems found:"
msgstr ""
-#: .././mainapp.py:5651
+#: .././mainapp.py:5911
msgid ""
"Do you want to repair these problems? (The database will be fixed, but no "
"files will be deleted)"
msgstr ""
-#: .././mainapp.py:5796
+#: .././mainapp.py:6056
msgid "Database inconsistencies repaired"
msgstr ""
-#: .././mainapp.py:6438
+#: .././mainapp.py:6863
msgid "The user declined to specify a data folder for Tartube"
msgstr ""
-#: .././mainapp.py:6538 .././config.py:10074
+#: .././mainapp.py:6963 .././config.py:10543
msgid "Please select Tartube's data folder"
msgstr ""
-#: .././mainapp.py:6664
+#: .././mainapp.py:7143
msgid ""
"A download operation cannot start if one or more configuration windows are "
"still open"
msgstr ""
-#: .././mainapp.py:6688 .././mainapp.py:6710
+#: .././mainapp.py:7167 .././mainapp.py:7189
#, python-brace-format
msgid "You only have {0} / {1} Mb remaining on your device"
msgstr ""
-#: .././mainapp.py:6713 .././mainapp.py:11657 .././mainapp.py:11773
-#: .././mainapp.py:11947 .././mainwin.py:14237
+#: .././mainapp.py:7192 .././mainapp.py:12461 .././mainapp.py:12577
+#: .././mainapp.py:12751 .././mainwin.py:14443
msgid "Are you sure you want to continue?"
msgstr ""
-#: .././mainapp.py:6794
+#: .././mainapp.py:7273
msgid "There is nothing to check!"
msgstr ""
-#: .././mainapp.py:6796
+#: .././mainapp.py:7275
msgid "There is nothing to download!"
msgstr ""
-#: .././mainapp.py:7006
+#: .././mainapp.py:7487
msgid "Download operation complete"
msgstr ""
-#: .././mainapp.py:7008
+#: .././mainapp.py:7489
msgid "Download operation halted"
msgstr ""
-#: .././mainapp.py:7011 .././mainapp.py:7478 .././mainapp.py:7924
+#: .././mainapp.py:7492 .././mainapp.py:8003 .././mainapp.py:8456
+#: .././mainapp.py:8866
msgid "Time taken:"
msgstr ""
-#: .././mainapp.py:7069
+#: .././mainapp.py:7549
msgid ""
"An update operation cannot start if one or more configuration windows are "
"still open"
msgstr ""
-#: .././mainapp.py:7182
+#: .././mainapp.py:7673
msgid "Installation failed"
msgstr ""
-#: .././mainapp.py:7184
+#: .././mainapp.py:7675
msgid "Installation complete"
msgstr ""
-#: .././mainapp.py:7188
+#: .././mainapp.py:7679
msgid "Update operation failed"
msgstr ""
-#: .././mainapp.py:7190
+#: .././mainapp.py:7681
msgid "Update operation halted"
msgstr ""
-#: .././mainapp.py:7192
+#: .././mainapp.py:7683
msgid "Update operation complete"
msgstr ""
-#: .././mainapp.py:7193
-msgid "youtube-dl version:"
+#: .././mainapp.py:7685
+msgid "version:"
msgstr ""
-#: .././mainapp.py:7197
+#: .././mainapp.py:7689
msgid "(unknown)"
msgstr ""
-#: .././mainapp.py:7271
+#: .././mainapp.py:7701
+msgid "Do you want to install FFmpeg now?"
+msgstr ""
+
+#: .././mainapp.py:7703
+msgid ""
+"(You should click Yes, even if you think FFmpeg is already installed on your "
+"system)"
+msgstr ""
+
+#: .././mainapp.py:7796
msgid ""
"A refresh operation cannot start if one or more configuration windows are "
"still open"
msgstr ""
-#: .././mainapp.py:7284
+#: .././mainapp.py:7809
msgid ""
"During a refresh operation, Tartube analyses its data folder, looking for "
"videos that haven't yet been added to its database"
msgstr ""
-#: .././mainapp.py:7288
+#: .././mainapp.py:7813
msgid ""
"You only need to perform a refresh operation if you have manually copied "
"videos into Tartube's data folder"
msgstr ""
-#: .././mainapp.py:7295
+#: .././mainapp.py:7820
msgid ""
"Before starting a refresh operation, you should click the 'Check all' button "
"in the main window"
msgstr ""
-#: .././mainapp.py:7302
+#: .././mainapp.py:7827
msgid ""
"Before starting a refresh operation, you should right-click the channel and "
"select 'Check channel'"
msgstr ""
-#: .././mainapp.py:7309
+#: .././mainapp.py:7834
msgid ""
"Before starting a refresh operation, you should right-click the playlist and "
"select 'Check playlist'"
msgstr ""
-#: .././mainapp.py:7316
+#: .././mainapp.py:7841
msgid ""
"Before starting a refresh operation, you should right-click the folder and "
"select 'Check folder'"
msgstr ""
-#: .././mainapp.py:7321
+#: .././mainapp.py:7846
msgid "Are you sure you want to proceed with the refresh operation?"
msgstr ""
-#: .././mainapp.py:7473
+#: .././mainapp.py:7998
msgid "Refresh operation complete"
msgstr ""
-#: .././mainapp.py:7475
+#: .././mainapp.py:8000
msgid "Refresh operation halted"
msgstr ""
-#: .././mainapp.py:7575
+#: .././mainapp.py:8100
msgid ""
"An info operation cannot start if one or more configuration windows are "
"still open"
msgstr ""
-#: .././mainapp.py:7688
+#: .././mainapp.py:8212
msgid "Operation failed"
msgstr ""
-#: .././mainapp.py:7690 .././downloads.py:362
+#: .././mainapp.py:8214 .././downloads.py:362
msgid "Operation complete"
msgstr ""
-#: .././mainapp.py:7692
+#: .././mainapp.py:8216
msgid "Click the Output Tab to see the results"
msgstr ""
-#: .././mainapp.py:7790
+#: .././mainapp.py:8323
msgid ""
"A tidy operation cannot start if one or more configuration windows are still "
"open"
msgstr ""
-#: .././mainapp.py:7919
+#: .././mainapp.py:8451
msgid "Tidy operation complete"
msgstr ""
-#: .././mainapp.py:7921
+#: .././mainapp.py:8453
msgid "Tidy operation halted"
msgstr ""
-#: .././mainapp.py:8061 .././mainwin.py:14661
+#: .././mainapp.py:8591 .././mainwin.py:14867
msgid "Livestream has started"
msgstr ""
-#: .././mainapp.py:9316 .././mainapp.py:9492
+#: .././mainapp.py:8720
+msgid ""
+"A process operation cannot start if one or more configuration windows are "
+"still open"
+msgstr ""
+
+#: .././mainapp.py:8861
+msgid "Process operation complete"
+msgstr ""
+
+#: .././mainapp.py:8863
+msgid "Process operation halted"
+msgstr ""
+
+#: .././mainapp.py:10102 .././mainapp.py:10277
msgid "Cannot move anything to:"
msgstr ""
-#: .././mainapp.py:9318 .././mainapp.py:9494
+#: .././mainapp.py:10104 .././mainapp.py:10279
msgid ""
"because a file or folder with the same name already exists (although "
"Tartube's database doesn't know anything about it)"
msgstr ""
-#: .././mainapp.py:9322
+#: .././mainapp.py:10108
msgid ""
"You probably created that file/folder accidentally, in which case you should "
"delete it manually before trying again"
msgstr ""
-#: .././mainapp.py:9336 .././mainapp.py:9512
+#: .././mainapp.py:10122 .././mainapp.py:10297
msgid "Are you sure you want to move this channel:"
msgstr ""
-#: .././mainapp.py:9338 .././mainapp.py:9514
+#: .././mainapp.py:10124 .././mainapp.py:10299
msgid "Are you sure you want to move this playlist:"
msgstr ""
-#: .././mainapp.py:9340 .././mainapp.py:9516
+#: .././mainapp.py:10126 .././mainapp.py:10301
msgid "Are you sure you want to move this folder:"
msgstr ""
-#: .././mainapp.py:9345
+#: .././mainapp.py:10131
msgid ""
"This procedure will move all downloaded files to the top level of Tartube's "
"data folder"
msgstr ""
-#: .././mainapp.py:9446
+#: .././mainapp.py:10231
msgid "Channels, playlists and folders can only be dragged into a folder"
msgstr ""
-#: .././mainapp.py:9459
+#: .././mainapp.py:10244
#, python-brace-format
msgid "The fixed folder '{0}' cannot be moved (but it can still be hidden)"
msgstr ""
-#: .././mainapp.py:9472
+#: .././mainapp.py:10257
#, python-brace-format
msgid "The folder '{0}' can only contain videos"
msgstr ""
-#: .././mainapp.py:9499
+#: .././mainapp.py:10284
msgid ""
"You probably created that file/folder accidentally, in which case, you "
"should delete it manually before trying again"
msgstr ""
-#: .././mainapp.py:9518
+#: .././mainapp.py:10303
msgid "into this folder:"
msgstr ""
-#: .././mainapp.py:9522
+#: .././mainapp.py:10307
msgid "This procedure will move all downloaded files to the new location"
msgstr ""
-#: .././mainapp.py:9528
+#: .././mainapp.py:10313
msgid ""
"WARNING: The destination folder is marked as temporary, so everything inside "
"it will be DELETED when Tartube restarts!"
msgstr ""
-#: .././mainapp.py:9918
+#: .././mainapp.py:10714
msgid ""
"Are you SURE you want to delete files? This procedure cannot be reversed!"
msgstr ""
-#: .././mainapp.py:11641 .././mainapp.py:11757 .././mainapp.py:11931
+#: .././mainapp.py:12445 .././mainapp.py:12561 .././mainapp.py:12735
#, python-brace-format
msgid "The channel contains {0} item(s), so this action may take a while"
msgstr ""
-#: .././mainapp.py:11647 .././mainapp.py:11763 .././mainapp.py:11937
+#: .././mainapp.py:12451 .././mainapp.py:12567 .././mainapp.py:12741
#, python-brace-format
msgid "The playlist contains {0} item(s), so this action may take a while"
msgstr ""
-#: .././mainapp.py:11653 .././mainapp.py:11769 .././mainapp.py:11943
+#: .././mainapp.py:12457 .././mainapp.py:12573 .././mainapp.py:12747
#, python-brace-format
msgid "The folder contains {0} item(s), so this action may take a while"
msgstr ""
-#: .././mainapp.py:12011 .././mainapp.py:14653 .././mainapp.py:14785
-#: .././mainapp.py:14916
+#: .././mainapp.py:12815 .././mainapp.py:15563 .././mainapp.py:15695
+#: .././mainapp.py:15826
#, python-brace-format
msgid "The name '{0}' is not allowed"
msgstr ""
-#: .././mainapp.py:12020
+#: .././mainapp.py:12824
#, python-brace-format
msgid "The name '{0}' is already in use"
msgstr ""
-#: .././mainapp.py:12033
+#: .././mainapp.py:12837
#, python-brace-format
msgid "Failed to rename '{0}'"
msgstr ""
-#: .././mainapp.py:12351
+#: .././mainapp.py:13155
msgid "Select where to save the database export"
msgstr ""
-#: .././mainapp.py:12480
+#: .././mainapp.py:13284
msgid "There is nothing to export!"
msgstr ""
-#: .././mainapp.py:12513 .././mainapp.py:12571
-msgid "Failed to save the database export file"
+#: .././mainapp.py:13324 .././mainapp.py:13390
+msgid "Failed to save the database export file:"
msgstr ""
-#: .././mainapp.py:12578
+#: .././mainapp.py:13398
msgid "Database export file saved to:"
msgstr ""
-#: .././mainapp.py:12615
+#: .././mainapp.py:13435
msgid "Select the database export"
msgstr ""
-#: .././mainapp.py:12640 .././mainapp.py:12654
+#: .././mainapp.py:13460 .././mainapp.py:13474
msgid "Failed to load the database export file"
msgstr ""
-#: .././mainapp.py:12671
+#: .././mainapp.py:13491
msgid "The database export file is invalid"
msgstr ""
-#: .././mainapp.py:12682
+#: .././mainapp.py:13502
msgid "The database export file is invalid (or empty)"
msgstr ""
-#: .././mainapp.py:12726
+#: .././mainapp.py:13546
msgid "Nothing was imported from the database export file"
msgstr ""
#. Show a confirmation
-#: .././mainapp.py:12740
+#: .././mainapp.py:13560
msgid "Imported:"
msgstr ""
-#: .././mainapp.py:12741
+#: .././mainapp.py:13561
msgid "Videos:"
msgstr ""
-#: .././mainapp.py:12742
+#: .././mainapp.py:13562
msgid "Channels:"
msgstr ""
-#: .././mainapp.py:12743
+#: .././mainapp.py:13563
msgid "Playlists:"
msgstr ""
-#: .././mainapp.py:12744
+#: .././mainapp.py:13564
msgid "Folders:"
msgstr ""
-#: .././mainapp.py:13105
+#: .././mainapp.py:13948
msgid ""
"The video file is missing from Tartube's data folder (try downloading the "
"video again!)"
msgstr ""
-#: .././mainapp.py:13800
+#: .././mainapp.py:14708
msgid "Please select a destination folder"
msgstr ""
-#: .././mainapp.py:13974
+#: .././mainapp.py:14882
msgid "No video(s) have been downloaded"
msgstr ""
#. Prompt for confirmation
-#: .././mainapp.py:14064
+#: .././mainapp.py:14972
msgid "Are you sure you want to remove the selected item(s)?"
msgstr ""
-#: .././mainapp.py:14644
+#: .././mainapp.py:15554
msgid "You must give the channel a name"
msgstr ""
-#: .././mainapp.py:14662 .././mainapp.py:14925
+#: .././mainapp.py:15572 .././mainapp.py:15835
msgid "You must enter a valid URL"
msgstr ""
-#: .././mainapp.py:14777
+#: .././mainapp.py:15687
msgid "You must give the folder a name"
msgstr ""
-#: .././mainapp.py:14907
+#: .././mainapp.py:15817
msgid "You must give the playlist a name"
msgstr ""
-#: .././mainapp.py:15062 .././mainwin.py:14132
+#: .././mainapp.py:15972 .././mainwin.py:14338
msgid "The following videos are duplicates:"
msgstr ""
-#: .././mainapp.py:15126
+#: .././mainapp.py:16036
msgid "There were no livestream alerts to cancel"
msgstr ""
-#: .././mainapp.py:15128
+#: .././mainapp.py:16038
msgid "Livestream alerts for 1 video were cancelled"
msgstr ""
-#: .././mainapp.py:15131
+#: .././mainapp.py:16041
#, python-brace-format
msgid "Livestream alerts for {0} videos were cancelled"
msgstr ""
-#: .././mainapp.py:15432
+#: .././mainapp.py:16342
msgid "Data saved"
msgstr ""
-#: .././mainapp.py:15462
+#: .././mainapp.py:16372
msgid "Database saved"
msgstr ""
-#: .././mainapp.py:15686 .././mainwin.py:11087
+#: .././mainapp.py:16627 .././mainwin.py:11127
msgid ""
"Files cannot be recovered, after being deleted. Are you sure you want to "
"continue?"
@@ -588,250 +617,254 @@ msgstr ""
#. Because livestream operations run silently in the background, when
#. the user goes to the trouble of clicking a menu item in the
#. main window's menu, tell them why nothing is happening
-#: .././mainapp.py:15726
+#: .././mainapp.py:16667
msgid "Cannot update existing livestreams because"
msgstr ""
-#: .././mainapp.py:15728
+#: .././mainapp.py:16669
msgid "there is another operation running"
msgstr ""
-#: .././mainapp.py:15730
+#: .././mainapp.py:16671
msgid "they are currently being updated"
msgstr ""
-#: .././mainapp.py:15732
+#: .././mainapp.py:16673
msgid "one or more configuration windows are open"
msgstr ""
-#: .././mainapp.py:15734
+#: .././mainapp.py:16675
msgid "there are no livestreams to update"
msgstr ""
-#: .././mainapp.py:15808
+#: .././mainapp.py:16749
msgid "There is already a channel with that name"
msgstr ""
-#: .././mainapp.py:15810
+#: .././mainapp.py:16751
msgid "There is already a playlist with that name"
msgstr ""
-#: .././mainapp.py:15812
+#: .././mainapp.py:16753
msgid "There is already a folder with that name"
msgstr ""
-#: .././mainapp.py:15815
+#: .././mainapp.py:16756
msgid "(so please choose a different name)"
msgstr ""
-#: .././mainwin.py:715
+#: .././mainwin.py:719
msgid "Tartube cannot start because it cannot find its icons folder"
msgstr ""
#. File column
-#: .././mainwin.py:805
+#: .././mainwin.py:809
msgid "_File"
msgstr ""
-#: .././mainwin.py:812
+#: .././mainwin.py:816
msgid "_Database preferences..."
msgstr ""
-#: .././mainwin.py:821
+#: .././mainwin.py:825
msgid "_Save database"
msgstr ""
-#: .././mainwin.py:827
+#: .././mainwin.py:831
msgid "Save _all"
msgstr ""
-#: .././mainwin.py:836
+#: .././mainwin.py:840
msgid "_Close to tray"
msgstr ""
#. Quit
-#: .././mainwin.py:841 .././mainwin.py:17368
+#: .././mainwin.py:845 .././mainwin.py:17587
msgid "_Quit"
msgstr ""
#. Edit column
-#: .././mainwin.py:846
+#: .././mainwin.py:850
msgid "_Edit"
msgstr ""
-#: .././mainwin.py:853
+#: .././mainwin.py:857
msgid "_System preferences..."
msgstr ""
-#: .././mainwin.py:859
+#: .././mainwin.py:863
msgid "_General download options..."
msgstr ""
#. Media column
-#: .././mainwin.py:865
+#: .././mainwin.py:869
msgid "_Media"
msgstr ""
-#: .././mainwin.py:872
+#: .././mainwin.py:876
msgid "Add _videos..."
msgstr ""
-#: .././mainwin.py:878
+#: .././mainwin.py:882
msgid "Add _channel..."
msgstr ""
-#: .././mainwin.py:884
+#: .././mainwin.py:888
msgid "Add _playlist..."
msgstr ""
-#: .././mainwin.py:890
+#: .././mainwin.py:894
msgid "Add _folder..."
msgstr ""
-#: .././mainwin.py:899
+#: .././mainwin.py:903
msgid "_Export from database"
msgstr ""
-#: .././mainwin.py:907
+#: .././mainwin.py:911
msgid "_JSON export file"
msgstr ""
-#: .././mainwin.py:913
+#: .././mainwin.py:917
msgid "Plain _text export file"
msgstr ""
-#: .././mainwin.py:919
+#: .././mainwin.py:923
msgid "_Import into database"
msgstr ""
-#: .././mainwin.py:928
+#: .././mainwin.py:932
msgid "_Switch between views"
msgstr ""
-#: .././mainwin.py:933
+#: .././mainwin.py:937
msgid "Show _hidden folders"
msgstr ""
-#: .././mainwin.py:943
+#: .././mainwin.py:950
msgid "_Add test media"
msgstr ""
+#: .././mainwin.py:958
+msgid "_Run test code"
+msgstr ""
+
#. Operations column
#. Add this tab...
-#: .././mainwin.py:949 .././config.py:7993
+#: .././mainwin.py:964 .././config.py:8246
msgid "_Operations"
msgstr ""
#. Check all
-#: .././mainwin.py:956 .././mainwin.py:17339
+#: .././mainwin.py:971 .././mainwin.py:17558
msgid "_Check all"
msgstr ""
#. Download all
-#: .././mainwin.py:962 .././mainwin.py:17346
+#: .././mainwin.py:977 .././mainwin.py:17565
msgid "_Download all"
msgstr ""
-#: .././mainwin.py:967
+#: .././mainwin.py:982
msgid "C_ustom download all"
msgstr ""
-#: .././mainwin.py:975
+#: .././mainwin.py:990
msgid "_Refresh database..."
msgstr ""
-#: .././mainwin.py:984
-msgid "Update _youtube-dl"
+#: .././mainwin.py:1000
+msgid "U_pdate"
msgstr ""
-#: .././mainwin.py:990
-msgid "_Test youtube-dl..."
+#: .././mainwin.py:1006
+msgid "_Test"
msgstr ""
-#: .././mainwin.py:999
+#: .././mainwin.py:1015
msgid "_Install FFmpeg"
msgstr ""
-#: .././mainwin.py:1010
+#: .././mainwin.py:1026
msgid "Tidy up _files..."
msgstr ""
-#: .././mainwin.py:1021 .././mainwin.py:17357
+#: .././mainwin.py:1037 .././mainwin.py:17576
msgid "_Stop current operation"
msgstr ""
#. Livestreams column
-#: .././mainwin.py:1028 .././config.py:8263
+#: .././mainwin.py:1044 .././config.py:8545
msgid "_Livestreams"
msgstr ""
-#: .././mainwin.py:1035
+#: .././mainwin.py:1051
msgid "_Livestream preferences..."
msgstr ""
-#: .././mainwin.py:1044
+#: .././mainwin.py:1060
msgid "_Update existing livestreams"
msgstr ""
-#: .././mainwin.py:1049
+#: .././mainwin.py:1065
msgid "_Cancel all livestream alerts"
msgstr ""
#. Help column
-#: .././mainwin.py:1054
+#: .././mainwin.py:1070
msgid "_Help"
msgstr ""
-#: .././mainwin.py:1060
+#: .././mainwin.py:1076
msgid "_About..."
msgstr ""
-#: .././mainwin.py:1065
+#: .././mainwin.py:1081
msgid "Go to _website"
msgstr ""
-#: .././mainwin.py:1071
+#: .././mainwin.py:1087
msgid "Send _feedback"
msgstr ""
-#: .././mainwin.py:1108
+#: .././mainwin.py:1124
msgid "Videos"
msgstr ""
-#: .././mainwin.py:1118
+#: .././mainwin.py:1134
msgid "Add new video(s)"
msgstr ""
-#: .././mainwin.py:1127
+#: .././mainwin.py:1143
msgid "Channel"
msgstr ""
-#: .././mainwin.py:1137
+#: .././mainwin.py:1153
msgid "Add a new channel"
msgstr ""
-#: .././mainwin.py:1148
+#: .././mainwin.py:1164
msgid "Playlist"
msgstr ""
-#: .././mainwin.py:1158
+#: .././mainwin.py:1174
msgid "Add a new playlist"
msgstr ""
-#: .././mainwin.py:1169
+#: .././mainwin.py:1185
msgid "Folder"
msgstr ""
-#: .././mainwin.py:1179
+#: .././mainwin.py:1195
msgid "Add a new folder"
msgstr ""
-#: .././mainwin.py:1193
+#: .././mainwin.py:1209
msgid "Check"
msgstr ""
-#: .././mainwin.py:1204 .././mainwin.py:1436 .././mainwin.py:3027
-#: .././mainwin.py:3197
+#: .././mainwin.py:1220 .././mainwin.py:1429 .././mainwin.py:3044
+#: .././mainwin.py:3216
msgid "Check all videos, channels, playlists and folders"
msgstr ""
@@ -840,21 +873,21 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:1214 .././mainwin.py:15718 .././mainwin.py:15726
-#: .././mainwin.py:15977 .././mainwin.py:15989 .././mainwin.py:16705
+#: .././mainwin.py:1230 .././mainwin.py:15927 .././mainwin.py:15935
+#: .././mainwin.py:16190 .././mainwin.py:16202 .././mainwin.py:16919
msgid "Download"
msgstr ""
-#: .././mainwin.py:1225 .././mainwin.py:1444 .././mainwin.py:3035
-#: .././mainwin.py:3203
+#: .././mainwin.py:1241 .././mainwin.py:1437 .././mainwin.py:3052
+#: .././mainwin.py:3222
msgid "Download all videos, channels, playlists and folders"
msgstr ""
-#: .././mainwin.py:1240
+#: .././mainwin.py:1256
msgid "Stop"
msgstr ""
-#: .././mainwin.py:1252
+#: .././mainwin.py:1268
msgid "Stop the current operation"
msgstr ""
@@ -864,176 +897,168 @@ msgstr ""
#. produces no error)
#. selection = treeview.get_selection()
#. selection.set_mode(Gtk.SelectionMode.MULTIPLE)
-#: .././mainwin.py:1264 .././config.py:6748
+#: .././mainwin.py:1280 .././config.py:6922
msgid "Switch"
msgstr ""
-#: .././mainwin.py:1275
+#: .././mainwin.py:1291
msgid "Switch between simple and complex views"
msgstr ""
-#: .././mainwin.py:1289 .././config.py:8403
-msgid "Test"
-msgstr ""
-
-#: .././mainwin.py:1300
-msgid "Add test media data objects"
-msgstr ""
-
-#: .././mainwin.py:1313
+#: .././mainwin.py:1306
msgid "Quit"
msgstr ""
-#: .././mainwin.py:1323
+#: .././mainwin.py:1316
msgid "Close Tartube"
msgstr ""
-#: .././mainwin.py:1345
+#: .././mainwin.py:1338
msgid "_Videos"
msgstr ""
-#: .././mainwin.py:1353
+#: .././mainwin.py:1346
msgid "_Progress"
msgstr ""
-#: .././mainwin.py:1361
+#: .././mainwin.py:1354
msgid "_Classic Mode"
msgstr ""
-#: .././mainwin.py:1369
+#: .././mainwin.py:1362
msgid "_Output"
msgstr ""
-#: .././mainwin.py:1378 .././config.py:5398 .././config.py:5750
+#: .././mainwin.py:1371 .././config.py:5442 .././config.py:5794
msgid "_Errors / Warnings"
msgstr ""
-#: .././mainwin.py:1434 .././mainwin.py:3025 .././mainwin.py:3194
+#: .././mainwin.py:1427 .././mainwin.py:3042 .././mainwin.py:3213
msgid "Check all"
msgstr ""
-#: .././mainwin.py:1442 .././mainwin.py:2482 .././mainwin.py:3033
+#: .././mainwin.py:1435 .././mainwin.py:2493 .././mainwin.py:3050
msgid "Download all"
msgstr ""
-#: .././mainwin.py:1499
+#: .././mainwin.py:1492
msgid "Page"
msgstr ""
-#: .././mainwin.py:1511
+#: .././mainwin.py:1504
msgid "Set visible page"
msgstr ""
-#: .././mainwin.py:1535 .././mainwin.py:1843 .././mainwin.py:1904
-#: .././mainwin.py:2336
+#: .././mainwin.py:1528 .././mainwin.py:1836 .././mainwin.py:1904
+#: .././mainwin.py:2340
msgid "Size"
msgstr ""
-#: .././mainwin.py:1546
+#: .././mainwin.py:1539
msgid "Set page size"
msgstr ""
-#: .././mainwin.py:1567
+#: .././mainwin.py:1560
msgid "Go to first page"
msgstr ""
-#: .././mainwin.py:1582
+#: .././mainwin.py:1575
msgid "Go to previous page"
msgstr ""
-#: .././mainwin.py:1599
+#: .././mainwin.py:1592
msgid "Go to next page"
msgstr ""
-#: .././mainwin.py:1614
+#: .././mainwin.py:1607
msgid "Go to last page"
msgstr ""
-#: .././mainwin.py:1629
+#: .././mainwin.py:1622
msgid "Scroll up"
msgstr ""
-#: .././mainwin.py:1644
+#: .././mainwin.py:1637
msgid "Scroll down"
msgstr ""
-#: .././mainwin.py:1662 .././mainwin.py:3438
+#: .././mainwin.py:1655 .././mainwin.py:3455
msgid "Show filter options"
msgstr ""
-#: .././mainwin.py:1675
+#: .././mainwin.py:1668
msgid "Sort by"
msgstr ""
-#: .././mainwin.py:1690 .././mainwin.py:3510
+#: .././mainwin.py:1683 .././mainwin.py:3527
msgid "Sort alphabetically"
msgstr ""
-#: .././mainwin.py:1700
+#: .././mainwin.py:1693
msgid "Filter"
msgstr ""
-#: .././mainwin.py:1709
+#: .././mainwin.py:1702
msgid "Enter search text"
msgstr ""
-#: .././mainwin.py:1714
+#: .././mainwin.py:1707
msgid "Regex"
msgstr ""
-#: .././mainwin.py:1722
+#: .././mainwin.py:1715
msgid "Select if search text is a regex"
msgstr ""
-#: .././mainwin.py:1739
+#: .././mainwin.py:1732
msgid "Filter videos"
msgstr ""
-#: .././mainwin.py:1756
+#: .././mainwin.py:1749
msgid "Cancel filter"
msgstr ""
-#: .././mainwin.py:1767
+#: .././mainwin.py:1760
msgid "Find date"
msgstr ""
-#: .././mainwin.py:1781
+#: .././mainwin.py:1774
msgid "Find videos by date"
msgstr ""
-#: .././mainwin.py:1836
+#: .././mainwin.py:1829
msgid "TRANSLATOR'S NOTE: Ext is short for a file extension, e.g. .EXE"
msgstr ""
-#: .././mainwin.py:1841 .././mainwin.py:2334
+#: .././mainwin.py:1834 .././mainwin.py:2338
msgid "Source"
msgstr ""
-#: .././mainwin.py:1841 .././mainwin.py:2334
+#: .././mainwin.py:1834 .././mainwin.py:2338
msgid "Status"
msgstr ""
-#: .././mainwin.py:1842 .././mainwin.py:2335
+#: .././mainwin.py:1835 .././mainwin.py:2339
msgid "Incoming file"
msgstr ""
-#: .././mainwin.py:1842 .././mainwin.py:2335
+#: .././mainwin.py:1835 .././mainwin.py:2339
msgid "Ext"
msgstr ""
-#: .././mainwin.py:1842 .././mainwin.py:2335
+#: .././mainwin.py:1835 .././mainwin.py:2339
msgid "Speed"
msgstr ""
-#: .././mainwin.py:1842 .././mainwin.py:2335
+#: .././mainwin.py:1835 .././mainwin.py:2339
msgid "ETA"
msgstr ""
-#: .././mainwin.py:1904 .././config.py:5662
+#: .././mainwin.py:1904 .././config.py:5706
msgid "New videos"
msgstr ""
-#: .././mainwin.py:1904 .././config.py:5175
+#: .././mainwin.py:1904 .././config.py:5219
msgid "Duration"
msgstr ""
@@ -1041,7 +1066,7 @@ msgstr ""
msgid "Date"
msgstr ""
-#: .././mainwin.py:1905 .././config.py:5146
+#: .././mainwin.py:1905 .././config.py:5190
msgid "File"
msgstr ""
@@ -1049,42 +1074,42 @@ msgstr ""
msgid "Downloaded to"
msgstr ""
-#: .././mainwin.py:1961
+#: .././mainwin.py:1965
msgid "Max downloads"
msgstr ""
-#: .././mainwin.py:1984
+#: .././mainwin.py:1988
msgid "D/L speed (KiB/s)"
msgstr ""
-#: .././mainwin.py:2010 .././config.py:2402
+#: .././mainwin.py:2014 .././config.py:2404
msgid "Video resolution"
msgstr ""
-#: .././mainwin.py:2045
+#: .././mainwin.py:2049
msgid "Hide rows when they are finished"
msgstr ""
-#: .././mainwin.py:2058
+#: .././mainwin.py:2062
msgid "Add newest videos to the top of the list"
msgstr ""
-#: .././mainwin.py:2117
+#: .././mainwin.py:2121
msgid "This tab emulates the classic youtube-dl-gui interface"
msgstr ""
-#: .././mainwin.py:2125
+#: .././mainwin.py:2129
msgid "Videos downloaded here are not added to Tartube's database"
msgstr ""
-#: .././mainwin.py:2147
+#: .././mainwin.py:2151
msgid "Open the Classic Mode menu"
msgstr ""
#. Second row - a textview for entering URLs. If automatic copy/paste is
#. enabled, URLs are automatically copied into this textview
#. --------------------------------------------------------------------
-#: .././mainwin.py:2154
+#: .././mainwin.py:2158
msgid "Enter URLs below"
msgstr ""
@@ -1094,955 +1119,967 @@ msgstr ""
#. the specified destination and format
#. --------------------------------------------------------------------
#. Destination directory
-#: .././mainwin.py:2193
+#: .././mainwin.py:2197
msgid "Destination:"
msgstr ""
-#: .././mainwin.py:2230
+#: .././mainwin.py:2234
msgid "Add a new destination folder"
msgstr ""
-#: .././mainwin.py:2249
+#: .././mainwin.py:2253
msgid "Open the destination folder"
msgstr ""
#. Video/audio format
-#: .././mainwin.py:2254
+#: .././mainwin.py:2258
msgid "Format:"
msgstr ""
-#: .././mainwin.py:2257
+#: .././mainwin.py:2261
msgid "Default"
msgstr ""
-#: .././mainwin.py:2257 .././mainwin.py:13380
+#: .././mainwin.py:2261 .././mainwin.py:13586
msgid "Video:"
msgstr ""
-#: .././mainwin.py:2261 .././mainwin.py:13380
+#: .././mainwin.py:2265 .././mainwin.py:13586
msgid "Audio:"
msgstr ""
-#: .././mainwin.py:2291
+#: .././mainwin.py:2295
msgid "Add URLs"
msgstr ""
-#: .././mainwin.py:2297
+#: .././mainwin.py:2301
msgid "Add these URLs"
msgstr ""
-#: .././mainwin.py:2380
+#: .././mainwin.py:2391
msgid "Remove from list"
msgstr ""
-#: .././mainwin.py:2403
+#: .././mainwin.py:2414
msgid "Play video"
msgstr ""
#. Signal connect below
-#: .././mainwin.py:2419 .././config.py:2755 .././config.py:6785
+#: .././mainwin.py:2430 .././config.py:2799 .././config.py:6959
msgid "Move up"
msgstr ""
#. Signal connect below
#. signal connect appears below
-#: .././mainwin.py:2440 .././config.py:2759 .././config.py:6793
+#: .././mainwin.py:2451 .././config.py:2803 .././config.py:6967
msgid "Move down"
msgstr ""
-#: .././mainwin.py:2456
+#: .././mainwin.py:2467
msgid "Re-download"
msgstr ""
-#: .././mainwin.py:2479
+#: .././mainwin.py:2490
msgid "Stop download"
msgstr ""
-#: .././mainwin.py:2489
+#: .././mainwin.py:2500
msgid "Download the URLs above"
msgstr ""
-#: .././mainwin.py:2552
+#: .././mainwin.py:2563
msgid "Time"
msgstr ""
-#: .././mainwin.py:2552
+#: .././mainwin.py:2563
msgid "Type"
msgstr ""
-#: .././mainwin.py:2552
+#: .././mainwin.py:2563
msgid "Message"
msgstr ""
-#: .././mainwin.py:2586
+#: .././mainwin.py:2597
msgid "Show Tartube errors"
msgstr ""
-#: .././mainwin.py:2599
+#: .././mainwin.py:2610
msgid "Show Tartube warnings"
msgstr ""
-#: .././mainwin.py:2612
+#: .././mainwin.py:2623
msgid "Show server errors"
msgstr ""
-#: .././mainwin.py:2630
+#: .././mainwin.py:2641
msgid "Show server warnings"
msgstr ""
-#: .././mainwin.py:2642
+#: .././mainwin.py:2653
msgid "Clear list"
msgstr ""
-#: .././mainwin.py:2953 .././mainwin.py:2981
+#: .././mainwin.py:2966 .././mainwin.py:2996
msgid "Checking..."
msgstr ""
-#: .././mainwin.py:2955 .././mainwin.py:2983
+#: .././mainwin.py:2968 .././mainwin.py:2998
msgid "Downloading..."
msgstr ""
-#: .././mainwin.py:2957 .././mainwin.py:2985
+#: .././mainwin.py:2970 .././mainwin.py:3000
msgid "Refreshing..."
msgstr ""
-#: .././mainwin.py:2959 .././mainwin.py:2987
+#: .././mainwin.py:2972 .././mainwin.py:3002
msgid "Tidying..."
msgstr ""
-#: .././mainwin.py:3173
+#: .././mainwin.py:2974
+msgid "FFmpeg processing..."
+msgstr ""
+
+#: .././mainwin.py:3004
+msgid "FFmpeg Processing..."
+msgstr ""
+
+#: .././mainwin.py:3192
msgid "Installing"
msgstr ""
-#: .././mainwin.py:3176
+#: .././mainwin.py:3195
msgid "Updating"
msgstr ""
-#: .././mainwin.py:3179 .././mainwin.py:3182
+#: .././mainwin.py:3198 .././mainwin.py:3201
msgid "Fetching"
msgstr ""
-#: .././mainwin.py:3185
+#: .././mainwin.py:3204
msgid "Testing"
msgstr ""
-#: .././mainwin.py:3461
+#: .././mainwin.py:3478
msgid "Hide filter options"
msgstr ""
-#: .././mainwin.py:3526
+#: .././mainwin.py:3543
msgid "Sort by date"
msgstr ""
-#: .././mainwin.py:3752
+#: .././mainwin.py:3769
msgid "_Check channel"
msgstr ""
-#: .././mainwin.py:3754
+#: .././mainwin.py:3771
msgid "_Check playlist"
msgstr ""
-#: .././mainwin.py:3756
+#: .././mainwin.py:3773
msgid "_Check folder"
msgstr ""
-#: .././mainwin.py:3773
+#: .././mainwin.py:3790
msgid "_Download channel"
msgstr ""
-#: .././mainwin.py:3775
+#: .././mainwin.py:3792
msgid "_Download playlist"
msgstr ""
-#: .././mainwin.py:3777
+#: .././mainwin.py:3794
msgid "_Download folder"
msgstr ""
-#: .././mainwin.py:3794
+#: .././mainwin.py:3811
msgid "C_ustom download channel"
msgstr ""
-#: .././mainwin.py:3796
+#: .././mainwin.py:3813
msgid "C_ustom download playlist"
msgstr ""
-#: .././mainwin.py:3798
+#: .././mainwin.py:3815
msgid "C_ustom download folder"
msgstr ""
-#: .././mainwin.py:3843
+#: .././mainwin.py:3860
msgid "_Empty folder"
msgstr ""
-#: .././mainwin.py:3855
+#: .././mainwin.py:3872
msgid "_All contents"
msgstr ""
-#: .././mainwin.py:3873
+#: .././mainwin.py:3890
msgid "_Remove videos"
msgstr ""
-#: .././mainwin.py:3885
+#: .././mainwin.py:3902
msgid "_Just folder videos"
msgstr ""
-#: .././mainwin.py:3891
+#: .././mainwin.py:3908
msgid "Channel co_ntents"
msgstr ""
-#: .././mainwin.py:3893
+#: .././mainwin.py:3910
msgid "Playlist co_ntents"
msgstr ""
-#: .././mainwin.py:3895
+#: .././mainwin.py:3912
msgid "Folder co_ntents"
msgstr ""
-#: .././mainwin.py:3907
+#: .././mainwin.py:3924
msgid "_Move to top level"
msgstr ""
-#: .././mainwin.py:3924
+#: .././mainwin.py:3941
msgid "_Convert to playlist"
msgstr ""
-#: .././mainwin.py:3926
+#: .././mainwin.py:3943
msgid "_Convert to channel"
msgstr ""
-#: .././mainwin.py:3948
+#: .././mainwin.py:3965
msgid "_Hide folder"
msgstr ""
-#: .././mainwin.py:3958
+#: .././mainwin.py:3975
msgid "_Rename channel..."
msgstr ""
-#: .././mainwin.py:3960
+#: .././mainwin.py:3977
msgid "_Rename playlist..."
msgstr ""
-#: .././mainwin.py:3962
+#: .././mainwin.py:3979
msgid "_Rename folder..."
msgstr ""
-#: .././mainwin.py:3979
+#: .././mainwin.py:3996
msgid "Set _nickname..."
msgstr ""
-#: .././mainwin.py:3994
+#: .././mainwin.py:4011
msgid "Set _URL..."
msgstr ""
-#: .././mainwin.py:4006
+#: .././mainwin.py:4023
msgid "Set _download destination..."
msgstr ""
-#: .././mainwin.py:4022
+#: .././mainwin.py:4039
msgid "_Export channel..."
msgstr ""
-#: .././mainwin.py:4024
+#: .././mainwin.py:4041
msgid "_Export playlist..."
msgstr ""
-#: .././mainwin.py:4026
+#: .././mainwin.py:4043
msgid "_Export folder..."
msgstr ""
-#: .././mainwin.py:4039
+#: .././mainwin.py:4056
msgid "Re_fresh channel"
msgstr ""
-#: .././mainwin.py:4041
+#: .././mainwin.py:4058
msgid "Re_fresh playlist"
msgstr ""
-#: .././mainwin.py:4043
+#: .././mainwin.py:4060
msgid "Re_fresh folder"
msgstr ""
-#: .././mainwin.py:4060
+#: .././mainwin.py:4077
msgid "_Tidy up channel"
msgstr ""
-#: .././mainwin.py:4062
+#: .././mainwin.py:4079
msgid "_Tidy up playlist"
msgstr ""
-#: .././mainwin.py:4064
+#: .././mainwin.py:4081
msgid "_Tidy up folder"
msgstr ""
-#: .././mainwin.py:4081 .././mainwin.py:4870 .././mainwin.py:5842
+#: .././mainwin.py:4098 .././mainwin.py:4903 .././mainwin.py:5904
msgid "Add to _Classic Mode tab"
msgstr ""
-#: .././mainwin.py:4094
+#: .././mainwin.py:4111
msgid "Channel _actions"
msgstr ""
-#: .././mainwin.py:4096
+#: .././mainwin.py:4113
msgid "Playlist _actions"
msgstr ""
-#: .././mainwin.py:4098
+#: .././mainwin.py:4115
msgid "Folder _actions"
msgstr ""
-#: .././mainwin.py:4118 .././mainwin.py:4432
+#: .././mainwin.py:4135 .././mainwin.py:4450
msgid "_Apply download options..."
msgstr ""
-#: .././mainwin.py:4136 .././mainwin.py:4446
+#: .././mainwin.py:4153 .././mainwin.py:4464
msgid "_Remove download options"
msgstr ""
-#: .././mainwin.py:4152 .././mainwin.py:4458
+#: .././mainwin.py:4169 .././mainwin.py:4476
msgid "_Edit download options..."
msgstr ""
-#: .././mainwin.py:4168
+#: .././mainwin.py:4185
msgid "_Show system command"
msgstr ""
-#: .././mainwin.py:4181
+#: .././mainwin.py:4198
msgid "_Disable checking/downloading"
msgstr ""
-#: .././mainwin.py:4193
+#: .././mainwin.py:4210
msgid "_Just disable downloading"
msgstr ""
-#: .././mainwin.py:4218 .././mainwin.py:4517
+#: .././mainwin.py:4235 .././mainwin.py:4535
msgid "D_ownloads"
msgstr ""
-#: .././mainwin.py:4226
+#: .././mainwin.py:4243
msgid "Channel _properties..."
msgstr ""
-#: .././mainwin.py:4228
+#: .././mainwin.py:4245
msgid "Playlist _properties..."
msgstr ""
-#: .././mainwin.py:4230
+#: .././mainwin.py:4247
msgid "Folder _properties..."
msgstr ""
-#: .././mainwin.py:4246
+#: .././mainwin.py:4263
msgid "_Default location"
msgstr ""
-#: .././mainwin.py:4259
+#: .././mainwin.py:4276
msgid "_Actual location"
msgstr ""
-#: .././mainwin.py:4271
+#: .././mainwin.py:4288
msgid "_Show"
msgstr ""
-#: .././mainwin.py:4280
+#: .././mainwin.py:4297
msgid "D_elete channel"
msgstr ""
-#: .././mainwin.py:4282
+#: .././mainwin.py:4299
msgid "D_elete playlist"
msgstr ""
-#: .././mainwin.py:4284
+#: .././mainwin.py:4301
msgid "D_elete folder"
msgstr ""
-#: .././mainwin.py:4343
+#: .././mainwin.py:4361
msgid "_Check video"
msgstr ""
-#: .././mainwin.py:4365
+#: .././mainwin.py:4383
msgid "_Download video"
msgstr ""
-#: .././mainwin.py:4386
+#: .././mainwin.py:4404
msgid "Re-_download this video"
msgstr ""
-#: .././mainwin.py:4399
+#: .././mainwin.py:4417
msgid "C_ustom download video"
msgstr ""
-#: .././mainwin.py:4474
+#: .././mainwin.py:4492
msgid "Show system _command"
msgstr ""
-#: .././mainwin.py:4484
+#: .././mainwin.py:4502
msgid "_Test system command"
msgstr ""
-#: .././mainwin.py:4499
+#: .././mainwin.py:4517
msgid "_Disable downloads"
msgstr ""
-#: .././mainwin.py:4529
+#: .././mainwin.py:4541 .././mainwin.py:4930 .././mainwin.py:5404
+msgid "_Process with FFmpeg..."
+msgstr ""
+
+#: .././mainwin.py:4561
msgid "Video is _archived"
msgstr ""
-#: .././mainwin.py:4542
+#: .././mainwin.py:4574
msgid "Video is _bookmarked"
msgstr ""
-#: .././mainwin.py:4553
+#: .././mainwin.py:4585
msgid "Video is _favourite"
msgstr ""
-#: .././mainwin.py:4564
+#: .././mainwin.py:4596
msgid "Video is _missing"
msgstr ""
-#: .././mainwin.py:4580
+#: .././mainwin.py:4612
msgid "Video is _new"
msgstr ""
-#: .././mainwin.py:4593
+#: .././mainwin.py:4625
msgid "Video is in _waiting list"
msgstr ""
-#: .././mainwin.py:4604
+#: .././mainwin.py:4636
msgid "_Mark video"
msgstr ""
-#: .././mainwin.py:4615
+#: .././mainwin.py:4647
msgid "_Location"
msgstr ""
-#: .././mainwin.py:4625
+#: .././mainwin.py:4657
msgid "_Properties..."
msgstr ""
-#: .././mainwin.py:4637
+#: .././mainwin.py:4669
msgid "_Show video"
msgstr ""
-#: .././mainwin.py:4646
+#: .././mainwin.py:4678
msgid "Available _formats"
msgstr ""
-#: .././mainwin.py:4656
+#: .././mainwin.py:4688
msgid "Available _subtitles"
msgstr ""
-#: .././mainwin.py:4666
+#: .././mainwin.py:4698
msgid "_Fetch"
msgstr ""
#. Delete video
-#: .././mainwin.py:4677
+#: .././mainwin.py:4709
msgid "D_elete video"
msgstr ""
#. Check/download videos
-#: .././mainwin.py:4772
+#: .././mainwin.py:4804
msgid "_Check videos"
msgstr ""
-#: .././mainwin.py:4791
+#: .././mainwin.py:4823
msgid "_Download videos"
msgstr ""
-#: .././mainwin.py:4810
+#: .././mainwin.py:4842
msgid "C_ustom download videos"
msgstr ""
-#: .././mainwin.py:4828
+#: .././mainwin.py:4860
msgid "D_ownload and watch"
msgstr ""
-#: .././mainwin.py:4845 .././mainwin.py:5758
+#: .././mainwin.py:4878 .././mainwin.py:5820
msgid "Watch in _player"
msgstr ""
-#: .././mainwin.py:4855 .././mainwin.py:5773 .././mainwin.py:5784
+#: .././mainwin.py:4888 .././mainwin.py:5835 .././mainwin.py:5846
msgid "Watch on _website"
msgstr ""
-#: .././mainwin.py:4886 .././mainwin.py:5956
+#: .././mainwin.py:4919 .././mainwin.py:6018
msgid "_Mark for download"
msgstr ""
-#: .././mainwin.py:4898 .././mainwin.py:5967
+#: .././mainwin.py:4944 .././mainwin.py:6029
msgid "_Download"
msgstr ""
-#: .././mainwin.py:4908
+#: .././mainwin.py:4954
msgid "_Download and watch"
msgstr ""
-#: .././mainwin.py:4919 .././mainwin.py:5987
+#: .././mainwin.py:4965 .././mainwin.py:6049
msgid "_Temporary"
msgstr ""
-#: .././mainwin.py:4937
+#: .././mainwin.py:4984
msgid "_Archived"
msgstr ""
-#: .././mainwin.py:4950
+#: .././mainwin.py:4997
msgid "Not a_rchived"
msgstr ""
-#: .././mainwin.py:4966
+#: .././mainwin.py:5013
msgid "_Bookmarked"
msgstr ""
-#: .././mainwin.py:4979
+#: .././mainwin.py:5026
msgid "Not b_ookmarked"
msgstr ""
-#: .././mainwin.py:4995
+#: .././mainwin.py:5042
msgid "_Favourite"
msgstr ""
-#: .././mainwin.py:5008
+#: .././mainwin.py:5055
msgid "Not fa_vourite"
msgstr ""
-#: .././mainwin.py:5024
+#: .././mainwin.py:5071
msgid "_Missing"
msgstr ""
-#: .././mainwin.py:5037
+#: .././mainwin.py:5084
msgid "Not m_issing"
msgstr ""
-#: .././mainwin.py:5053
+#: .././mainwin.py:5100
msgid "_New"
msgstr ""
-#: .././mainwin.py:5066
+#: .././mainwin.py:5113
msgid "Not n_ew"
msgstr ""
-#: .././mainwin.py:5082
+#: .././mainwin.py:5129
msgid "In _waiting list"
msgstr ""
-#: .././mainwin.py:5095
+#: .././mainwin.py:5142
msgid "Not in w_aiting list"
msgstr ""
-#: .././mainwin.py:5108
+#: .././mainwin.py:5155
msgid "_Mark videos"
msgstr ""
-#: .././mainwin.py:5117
+#: .././mainwin.py:5164
msgid "Show p_roperties..."
msgstr ""
#. Delete videos
-#: .././mainwin.py:5132
+#: .././mainwin.py:5179
msgid "D_elete videos"
msgstr ""
#. Stop check/download
-#: .././mainwin.py:5197
+#: .././mainwin.py:5244
msgid "_Stop now"
msgstr ""
-#: .././mainwin.py:5211
+#: .././mainwin.py:5258
msgid "Stop after this _video"
msgstr ""
-#: .././mainwin.py:5226
+#: .././mainwin.py:5273
msgid "Stop after these v_ideos"
msgstr ""
-#: .././mainwin.py:5241
+#: .././mainwin.py:5288
msgid "Download _next"
msgstr ""
-#: .././mainwin.py:5253
+#: .././mainwin.py:5300
msgid "Download _last"
msgstr ""
-#: .././mainwin.py:5276
+#: .././mainwin.py:5323
msgid "Watch on _YouTube"
msgstr ""
-#: .././mainwin.py:5286
+#: .././mainwin.py:5333
msgid "Watch on _HookTube"
msgstr ""
-#: .././mainwin.py:5296
+#: .././mainwin.py:5343
msgid "Watch on _Invidious"
msgstr ""
-#: .././mainwin.py:5308
+#: .././mainwin.py:5355
msgid "Watch on _Website"
msgstr ""
#. Delete video
-#: .././mainwin.py:5360
+#: .././mainwin.py:5421
msgid "_Delete video"
msgstr ""
-#: .././mainwin.py:5392 .././mainwin.py:18036 .././mainwin.py:18531
-#: .././mainwin.py:18884
+#: .././mainwin.py:5453 .././mainwin.py:18257 .././mainwin.py:18752
+#: .././mainwin.py:19105
msgid "Enable automatic copy/paste"
msgstr ""
-#: .././mainwin.py:5394
+#: .././mainwin.py:5455
msgid "Disable automatic copy/paste"
msgstr ""
-#: .././mainwin.py:5410
+#: .././mainwin.py:5471
msgid "Use _classic download options"
msgstr ""
-#: .././mainwin.py:5423
+#: .././mainwin.py:5484
msgid "Use _general download options"
msgstr ""
-#: .././mainwin.py:5434
+#: .././mainwin.py:5495
msgid "_Edit classic download options"
msgstr ""
-#: .././mainwin.py:5450
-msgid "Update youtube-dl"
+#: .././mainwin.py:5511 .././mainwin.py:21825
+msgid "Update"
msgstr ""
#. Get URL
-#: .././mainwin.py:5504
+#: .././mainwin.py:5565
msgid "Get _URL"
msgstr ""
#. Get command
-#: .././mainwin.py:5513
+#: .././mainwin.py:5574
msgid "Get _command"
msgstr ""
-#: .././mainwin.py:5523
+#: .././mainwin.py:5584
msgid "_Open destination"
msgstr ""
-#: .././mainwin.py:5564
+#: .././mainwin.py:5625
msgid "Mark as _archived"
msgstr ""
-#: .././mainwin.py:5575
+#: .././mainwin.py:5636
msgid "Mark as not a_rchived"
msgstr ""
-#: .././mainwin.py:5589
+#: .././mainwin.py:5650
msgid "Mark as _bookmarked"
msgstr ""
-#: .././mainwin.py:5601
+#: .././mainwin.py:5662
msgid "Mark as not b_ookmarked"
msgstr ""
-#: .././mainwin.py:5614
+#: .././mainwin.py:5675
msgid "Mark as _favourite"
msgstr ""
-#: .././mainwin.py:5627
+#: .././mainwin.py:5688
msgid "Mark as not fa_vourite"
msgstr ""
-#: .././mainwin.py:5641
+#: .././mainwin.py:5702
msgid "Mark as _missing"
msgstr ""
-#: .././mainwin.py:5654
+#: .././mainwin.py:5715
msgid "Mark as not m_issing"
msgstr ""
-#: .././mainwin.py:5671
+#: .././mainwin.py:5732
msgid "Mark as _new"
msgstr ""
-#: .././mainwin.py:5683
+#: .././mainwin.py:5744
msgid "Mark as not n_ew"
msgstr ""
-#: .././mainwin.py:5697
+#: .././mainwin.py:5758
msgid "Mark as in _waiting list"
msgstr ""
-#: .././mainwin.py:5709
+#: .././mainwin.py:5770
msgid "Mark as not in wai_ting list"
msgstr ""
-#: .././mainwin.py:5741 .././mainwin.py:5977
+#: .././mainwin.py:5802 .././mainwin.py:6039
msgid "Download and _watch"
msgstr ""
-#: .././mainwin.py:5798
+#: .././mainwin.py:5860
msgid "_YouTube"
msgstr ""
-#: .././mainwin.py:5808
+#: .././mainwin.py:5870
msgid "_HookTube"
msgstr ""
-#: .././mainwin.py:5818
+#: .././mainwin.py:5880
msgid "_Invidious"
msgstr ""
-#: .././mainwin.py:5828
+#: .././mainwin.py:5890
msgid "TRANSLATOR'S NOTE: Watch on YouTube, Watch on HookTube, etc"
msgstr ""
-#: .././mainwin.py:5833
+#: .././mainwin.py:5895
msgid "W_atch on"
msgstr ""
-#: .././mainwin.py:5862
+#: .././mainwin.py:5924
msgid "Auto _notify"
msgstr ""
-#: .././mainwin.py:5878
+#: .././mainwin.py:5940
msgid "Auto _sound alarm"
msgstr ""
-#: .././mainwin.py:5893
+#: .././mainwin.py:5955
msgid "Auto _open"
msgstr ""
-#: .././mainwin.py:5906
+#: .././mainwin.py:5968
msgid "_Download on start"
msgstr ""
-#: .././mainwin.py:5919
+#: .././mainwin.py:5981
msgid "Download on _stop"
msgstr ""
-#: .././mainwin.py:5935
+#: .././mainwin.py:5997
msgid "Not a _livestream"
msgstr ""
-#: .././mainwin.py:5945 .././config.py:5285
+#: .././mainwin.py:6007 .././config.py:5329
msgid "_Livestream"
msgstr ""
-#: .././mainwin.py:6788
+#: .././mainwin.py:6850
msgid ""
"TRANSLATOR'S NOTE: V = number of videos B = (number of videos) bookmarked D "
"= downloaded F = favourite L = live/livestream M = missing N = new W = in "
"waiting list E = (number of) errors W = warnings"
msgstr ""
-#: .././mainwin.py:6795
+#: .././mainwin.py:6857
msgid "V:"
msgstr ""
-#: .././mainwin.py:6796
+#: .././mainwin.py:6858
msgid "B:"
msgstr ""
-#: .././mainwin.py:6797
+#: .././mainwin.py:6859
msgid "D:"
msgstr ""
-#: .././mainwin.py:6798
+#: .././mainwin.py:6860
msgid "F:"
msgstr ""
-#: .././mainwin.py:6799
+#: .././mainwin.py:6861
msgid "L:"
msgstr ""
-#: .././mainwin.py:6800
+#: .././mainwin.py:6862
msgid "M:"
msgstr ""
-#: .././mainwin.py:6801
+#: .././mainwin.py:6863
msgid "N:"
msgstr ""
-#: .././mainwin.py:6802 .././mainwin.py:6813
+#: .././mainwin.py:6864 .././mainwin.py:6875
msgid "W:"
msgstr ""
-#: .././mainwin.py:6812
+#: .././mainwin.py:6874
msgid "E:"
msgstr ""
-#: .././mainwin.py:7838 .././mainwin.py:8518
+#: .././mainwin.py:7895 .././mainwin.py:8546
msgid "Waiting"
msgstr ""
-#: .././mainwin.py:8978
+#: .././mainwin.py:9003
msgid ""
"TRANSLATOR'S NOTE: Thread means a computer processor thread. If you're not "
"sure how to translate it, just use 'Page #', as in Page #1, Page #2, etc"
msgstr ""
-#: .././mainwin.py:8985
+#: .././mainwin.py:9010
msgid "Thread"
msgstr ""
-#: .././mainwin.py:8988
+#: .././mainwin.py:9013
msgid "_Summary"
msgstr ""
-#: .././mainwin.py:9516
+#: .././mainwin.py:9556
msgid "Tartube error"
msgstr ""
-#: .././mainwin.py:9569
+#: .././mainwin.py:9609
msgid "Tartube warning"
msgstr ""
-#: .././mainwin.py:9602
+#: .././mainwin.py:9642
msgid "_Errors"
msgstr ""
-#: .././mainwin.py:9606
+#: .././mainwin.py:9646
msgid "Warnings"
msgstr ""
-#: .././mainwin.py:10896
+#: .././mainwin.py:10936
msgid "The URL is not valid"
msgstr ""
-#: .././mainwin.py:14219
+#: .././mainwin.py:14425
#, python-brace-format
msgid "The channel contains {0} items, so this action may take a while"
msgstr ""
-#: .././mainwin.py:14226
+#: .././mainwin.py:14432
#, python-brace-format
msgid "The playlist contains {0} items, so this action may take a while"
msgstr ""
-#: .././mainwin.py:14233
+#: .././mainwin.py:14439
#, python-brace-format
msgid "The folder contains {0} items, so this action may take a while"
msgstr ""
-#: .././mainwin.py:14614 .././mainwin.py:15532
+#: .././mainwin.py:14820 .././mainwin.py:15741
msgid "Originally from:"
msgstr ""
-#: .././mainwin.py:14627 .././mainwin.py:15545
+#: .././mainwin.py:14833 .././mainwin.py:15754
msgid "From channel:"
msgstr ""
-#: .././mainwin.py:14629 .././mainwin.py:15547
+#: .././mainwin.py:14835 .././mainwin.py:15756
msgid "From playlist:"
msgstr ""
-#: .././mainwin.py:14631 .././mainwin.py:15549
+#: .././mainwin.py:14837 .././mainwin.py:15758
msgid "From folder:"
msgstr ""
-#: .././mainwin.py:14657
+#: .././mainwin.py:14863
msgid "Livestream has not started yet"
msgstr ""
-#: .././mainwin.py:14666 .././mainwin.py:14672 .././mainwin.py:15596
-#: .././mainwin.py:15603
+#: .././mainwin.py:14872 .././mainwin.py:14878 .././mainwin.py:15805
+#: .././mainwin.py:15812
msgid "Duration:"
msgstr ""
-#: .././mainwin.py:14672 .././mainwin.py:14678 .././mainwin.py:14687
-#: .././mainwin.py:15603 .././mainwin.py:15610 .././mainwin.py:15620
-#: .././media.py:319 .././media.py:329 .././media.py:1546 .././media.py:1552
-#: .././media.py:1562
+#: .././mainwin.py:14878 .././mainwin.py:14884 .././mainwin.py:14893
+#: .././mainwin.py:15812 .././mainwin.py:15819 .././mainwin.py:15829
+#: .././media.py:320 .././media.py:330 .././media.py:1552 .././media.py:1558
+#: .././media.py:1568
msgid "unknown"
msgstr ""
-#: .././mainwin.py:14676 .././mainwin.py:14678 .././mainwin.py:15607
-#: .././mainwin.py:15609
+#: .././mainwin.py:14882 .././mainwin.py:14884 .././mainwin.py:15816
+#: .././mainwin.py:15818
msgid "Size:"
msgstr ""
-#: .././mainwin.py:14685 .././mainwin.py:14687 .././mainwin.py:15617
-#: .././mainwin.py:15619
+#: .././mainwin.py:14891 .././mainwin.py:14893 .././mainwin.py:15826
+#: .././mainwin.py:15828
msgid "Date:"
msgstr ""
-#: .././mainwin.py:15012
+#: .././mainwin.py:15218
msgid "Watch:"
msgstr ""
-#: .././mainwin.py:15081
+#: .././mainwin.py:15287
msgid "Temporary:"
msgstr ""
-#: .././mainwin.py:15124
+#: .././mainwin.py:15330
msgid "Marked:"
msgstr ""
-#: .././mainwin.py:15504 .././mainwin.py:15566
+#: .././mainwin.py:15713 .././mainwin.py:15775
msgid "Show the full description"
msgstr ""
-#: .././mainwin.py:15505 .././mainwin.py:15567
+#: .././mainwin.py:15714 .././mainwin.py:15776
msgid "More"
msgstr ""
-#: .././mainwin.py:15517 .././mainwin.py:15575
+#: .././mainwin.py:15726 .././mainwin.py:15784
msgid "Show the short description"
msgstr ""
-#: .././mainwin.py:15518 .././mainwin.py:15576
+#: .././mainwin.py:15727 .././mainwin.py:15785
msgid "Less"
msgstr ""
-#: .././mainwin.py:15636
+#: .././mainwin.py:15845
msgid "Live:"
msgstr ""
-#: .././mainwin.py:15639 .././mainwin.py:15641 .././mainwin.py:15645
-#: .././mainwin.py:15883 .././mainwin.py:15885 .././mainwin.py:15889
-#: .././mainwin.py:16342
+#: .././mainwin.py:15848 .././mainwin.py:15850 .././mainwin.py:15854
+#: .././mainwin.py:16096 .././mainwin.py:16098 .././mainwin.py:16102
+#: .././mainwin.py:16555
msgid "Notify"
msgstr ""
-#: .././mainwin.py:15649 .././mainwin.py:15893
+#: .././mainwin.py:15858 .././mainwin.py:16106
msgid "When the livestream starts, notify the user"
msgstr ""
-#: .././mainwin.py:15660 .././mainwin.py:15662 .././mainwin.py:15899
-#: .././mainwin.py:15901 .././mainwin.py:16209
+#: .././mainwin.py:15869 .././mainwin.py:15871 .././mainwin.py:16112
+#: .././mainwin.py:16114 .././mainwin.py:16422
msgid "Alarm"
msgstr ""
-#: .././mainwin.py:15666 .././mainwin.py:15905
+#: .././mainwin.py:15875 .././mainwin.py:16118
msgid "When the livestream starts, sound an alarm"
msgstr ""
-#: .././mainwin.py:15671 .././mainwin.py:15673 .././mainwin.py:15911
-#: .././mainwin.py:15913 .././mainwin.py:16387
+#: .././mainwin.py:15880 .././mainwin.py:15882 .././mainwin.py:16124
+#: .././mainwin.py:16126 .././mainwin.py:16600
msgid "Open"
msgstr ""
-#: .././mainwin.py:15677 .././mainwin.py:15917
+#: .././mainwin.py:15886 .././mainwin.py:16130
msgid "When the livestream starts, open it"
msgstr ""
-#: .././mainwin.py:15682 .././mainwin.py:15684 .././mainwin.py:15923
-#: .././mainwin.py:15925 .././mainwin.py:16253
+#: .././mainwin.py:15891 .././mainwin.py:15893 .././mainwin.py:16136
+#: .././mainwin.py:16138 .././mainwin.py:16466
msgid "D/L on start"
msgstr ""
-#: .././mainwin.py:15688 .././mainwin.py:15929
+#: .././mainwin.py:15897 .././mainwin.py:16142
msgid "When the livestream starts, download it"
msgstr ""
-#: .././mainwin.py:15693 .././mainwin.py:15695 .././mainwin.py:15935
-#: .././mainwin.py:15937 .././mainwin.py:16298
+#: .././mainwin.py:15902 .././mainwin.py:15904 .././mainwin.py:16148
+#: .././mainwin.py:16150 .././mainwin.py:16511
msgid "D/L on stop"
msgstr ""
-#: .././mainwin.py:15699 .././mainwin.py:15941
+#: .././mainwin.py:15908 .././mainwin.py:16154
msgid "When the livestream stops, download it"
msgstr ""
-#: .././mainwin.py:15725
+#: .././mainwin.py:15934
msgid "Download this video"
msgstr ""
-#: .././mainwin.py:15736
+#: .././mainwin.py:15945
msgid "Watch in your media player"
msgstr ""
@@ -2050,37 +2087,37 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15737 .././mainwin.py:17036
+#: .././mainwin.py:15946 .././mainwin.py:17253
msgid "Player"
msgstr ""
-#: .././mainwin.py:15745
+#: .././mainwin.py:15955
msgid ""
"TRANSLATOR'S NOTE: If you want to use &, use & - if you want to use a "
"different word (e.g. French et), then just use that word"
msgstr ""
-#: .././mainwin.py:15753
+#: .././mainwin.py:15963
msgid "Download and watch in your media player"
msgstr ""
-#: .././mainwin.py:15754
+#: .././mainwin.py:15964
msgid "Download & watch"
msgstr ""
-#: .././mainwin.py:15761
+#: .././mainwin.py:15971
msgid "Not downloaded"
msgstr ""
-#: .././mainwin.py:15787
+#: .././mainwin.py:15997
msgid "Watch on YouTube"
msgstr ""
-#: .././mainwin.py:15788 .././mainwin.py:17081
+#: .././mainwin.py:15998 .././mainwin.py:17298
msgid "YouTube"
msgstr ""
-#: .././mainwin.py:15800
+#: .././mainwin.py:16010
msgid "Watch on HookTube"
msgstr ""
@@ -2088,11 +2125,11 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15801 .././mainwin.py:16846
+#: .././mainwin.py:16011 .././mainwin.py:17062
msgid "HookTube"
msgstr ""
-#: .././mainwin.py:15810
+#: .././mainwin.py:16023
msgid "Watch on Invidious"
msgstr ""
@@ -2100,7 +2137,7 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15811 .././mainwin.py:16890
+#: .././mainwin.py:16024 .././mainwin.py:17106
msgid "Invidious"
msgstr ""
@@ -2108,24 +2145,24 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15829 .././mainwin.py:16935
+#: .././mainwin.py:16042 .././mainwin.py:17151
msgid "Other"
msgstr ""
-#: .././mainwin.py:15849
+#: .././mainwin.py:16062
msgid "Watch on website"
msgstr ""
-#: .././mainwin.py:15850 .././mainwin.py:17083
+#: .././mainwin.py:16063 .././mainwin.py:17300
msgid "Website"
msgstr ""
#. Links not clickable
-#: .././mainwin.py:15861
+#: .././mainwin.py:16074
msgid "No link"
msgstr ""
-#: .././mainwin.py:15970
+#: .././mainwin.py:16183
msgid "Download to a temporary folder later"
msgstr ""
@@ -2133,15 +2170,15 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15971 .././mainwin.py:15988 .././mainwin.py:16802
+#: .././mainwin.py:16184 .././mainwin.py:16201 .././mainwin.py:17018
msgid "Mark for download"
msgstr ""
-#: .././mainwin.py:15976
+#: .././mainwin.py:16189
msgid "Download to a temporary folder"
msgstr ""
-#: .././mainwin.py:15982
+#: .././mainwin.py:16195
msgid "Download to a temporary folder, then watch"
msgstr ""
@@ -2149,12 +2186,12 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:15983 .././mainwin.py:15990 .././mainwin.py:16759
+#: .././mainwin.py:16196 .././mainwin.py:16203 .././mainwin.py:16974
msgid "D/L and watch"
msgstr ""
#. Archived/not archived
-#: .././mainwin.py:16014
+#: .././mainwin.py:16227
msgid "Prevent automatic deletion of the video"
msgstr ""
@@ -2162,21 +2199,21 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16018 .././mainwin.py:16022 .././mainwin.py:16431
+#: .././mainwin.py:16231 .././mainwin.py:16235 .././mainwin.py:16644
msgid "Archived"
msgstr ""
#. Bookmarked/not bookmarked
-#: .././mainwin.py:16027
+#: .././mainwin.py:16240
msgid "Show video in Bookmarks folder"
msgstr ""
-#: .././mainwin.py:16031 .././mainwin.py:16035
+#: .././mainwin.py:16244 .././mainwin.py:16248
msgid "Bookmarked"
msgstr ""
#. Favourite/not favourite
-#: .././mainwin.py:16040
+#: .././mainwin.py:16253
msgid "Show in Favourite Videos folder"
msgstr ""
@@ -2184,12 +2221,12 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16044 .././mainwin.py:16048 .././mainwin.py:16521
+#: .././mainwin.py:16257 .././mainwin.py:16261 .././mainwin.py:16734
msgid "Favourite"
msgstr ""
#. Missing/not missing
-#: .././mainwin.py:16052
+#: .././mainwin.py:16265
msgid "Mark video as removed by creator"
msgstr ""
@@ -2197,12 +2234,12 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16056 .././mainwin.py:16060 .././mainwin.py:16566
+#: .././mainwin.py:16269 .././mainwin.py:16273 .././mainwin.py:16779
msgid "Missing"
msgstr ""
#. New/not new
-#: .././mainwin.py:16065
+#: .././mainwin.py:16278
msgid "Mark video as never watched"
msgstr ""
@@ -2210,36 +2247,36 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16069 .././mainwin.py:16073 .././mainwin.py:16604
+#: .././mainwin.py:16282 .././mainwin.py:16286 .././mainwin.py:16817
msgid "New"
msgstr ""
#. In waiting list/not in waiting list
-#: .././mainwin.py:16078
+#: .././mainwin.py:16291
msgid "Show in Waiting Videos folder"
msgstr ""
-#: .././mainwin.py:16081
+#: .././mainwin.py:16294
msgid "In waiting list"
msgstr ""
-#: .././mainwin.py:16085
+#: .././mainwin.py:16298
msgid "In Waiting list"
msgstr ""
-#: .././mainwin.py:16204
+#: .././mainwin.py:16417
msgid "Undo alarm"
msgstr ""
-#: .././mainwin.py:16248 .././mainwin.py:16293
+#: .././mainwin.py:16461 .././mainwin.py:16506
msgid "Don't D/L"
msgstr ""
-#: .././mainwin.py:16337
+#: .././mainwin.py:16550
msgid "Undo notify"
msgstr ""
-#: .././mainwin.py:16382
+#: .././mainwin.py:16595
msgid "Undo open"
msgstr ""
@@ -2247,7 +2284,7 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16476
+#: .././mainwin.py:16689
msgid "Not bookmarked"
msgstr ""
@@ -2255,2815 +2292,2991 @@ msgstr ""
#. this function returns. Workaround is to make the label unclickable,
#. then use a Glib timer to restore it (after some small fraction of a
#. second)
-#: .././mainwin.py:16649
+#: .././mainwin.py:16862
msgid "Not in waiting list"
msgstr ""
-#: .././mainwin.py:17713
+#: .././mainwin.py:17934
msgid "Tartube failed to start because:"
msgstr ""
-#: .././mainwin.py:17722
+#: .././mainwin.py:17943
msgid "If you don't know how to resolve this error, please contact the authors"
msgstr ""
-#: .././mainwin.py:17727
+#: .././mainwin.py:17948
msgid "here"
msgstr ""
#. 'OK' button
-#: .././mainwin.py:17730 .././mainwin.py:20016 .././config.py:426
-#: .././config.py:1602
+#: .././mainwin.py:17951 .././mainwin.py:20423 .././config.py:428
+#: .././config.py:1604
msgid "OK"
msgstr ""
-#: .././mainwin.py:17781 .././mainwin.py:20793 .././mainwin.py:20888
+#: .././mainwin.py:18002 .././mainwin.py:21402 .././mainwin.py:21497
msgid "Welcome to Tartube!"
msgstr ""
-#: .././mainwin.py:17913
+#: .././mainwin.py:18134
msgid "Add channel"
msgstr ""
-#: .././mainwin.py:17932
+#: .././mainwin.py:18153
msgid "Enter the channel name"
msgstr ""
-#: .././mainwin.py:17937
+#: .././mainwin.py:18158
msgid "(Use the channel's real name or a customised name)"
msgstr ""
-#: .././mainwin.py:17945
+#: .././mainwin.py:18166
msgid "Copy and paste a link to the channel"
msgstr ""
-#: .././mainwin.py:17992
+#: .././mainwin.py:18213
msgid "(Optional) Add this channel inside a folder"
msgstr ""
-#: .././mainwin.py:18022
+#: .././mainwin.py:18243
msgid "I want to download videos from this channel automatically"
msgstr ""
-#: .././mainwin.py:18029 .././mainwin.py:18316 .././mainwin.py:18524
+#: .././mainwin.py:18250 .././mainwin.py:18537 .././mainwin.py:18745
msgid "Don't download anything, just check for new videos"
msgstr ""
-#: .././mainwin.py:18217
+#: .././mainwin.py:18438
msgid "Add folder"
msgstr ""
-#: .././mainwin.py:18236
+#: .././mainwin.py:18457
msgid "Enter the folder name"
msgstr ""
-#: .././mainwin.py:18279
+#: .././mainwin.py:18500
msgid "(Optional) Add this folder inside another folder"
msgstr ""
-#: .././mainwin.py:18310
+#: .././mainwin.py:18531
msgid "I want to download videos from this folder automatically"
msgstr ""
-#: .././mainwin.py:18408
+#: .././mainwin.py:18629
msgid "Add playlist"
msgstr ""
-#: .././mainwin.py:18427
+#: .././mainwin.py:18648
msgid "Enter the playlist name"
msgstr ""
-#: .././mainwin.py:18432
+#: .././mainwin.py:18653
msgid "(Use the playlist's real name or a customised name)"
msgstr ""
-#: .././mainwin.py:18440
+#: .././mainwin.py:18661
msgid "Copy and paste a link to the playlist"
msgstr ""
-#: .././mainwin.py:18487
+#: .././mainwin.py:18708
msgid "(Optional) Add this playlist inside a folder"
msgstr ""
-#: .././mainwin.py:18517
+#: .././mainwin.py:18738
msgid "I want to download videos from this playlist automatically"
msgstr ""
-#: .././mainwin.py:18714
+#: .././mainwin.py:18935
msgid "Add videos"
msgstr ""
-#: .././mainwin.py:18733
+#: .././mainwin.py:18954
msgid "Copy and paste the links to one or more videos"
msgstr ""
-#: .././mainwin.py:18739
+#: .././mainwin.py:18960
msgid "Links containing multiple videos will be converted to a channel"
msgstr ""
-#: .././mainwin.py:18746
+#: .././mainwin.py:18967
msgid "Links containing multiple videos will be converted to a playlist"
msgstr ""
-#: .././mainwin.py:18753
+#: .././mainwin.py:18974
msgid "Links containing multiple videos will be downloaded separately"
msgstr ""
-#: .././mainwin.py:18760
+#: .././mainwin.py:18981
msgid "Links containing multiple videos will not be downloaded at all"
msgstr ""
-#: .././mainwin.py:18842
+#: .././mainwin.py:19063
msgid "Add the videos to this folder"
msgstr ""
-#: .././mainwin.py:18872
+#: .././mainwin.py:19093
msgid "I want to download these videos automatically"
msgstr ""
-#: .././mainwin.py:18878
+#: .././mainwin.py:19099
msgid "Don't download anything, just check the videos"
msgstr ""
-#: .././mainwin.py:19043
+#: .././mainwin.py:19264
msgid "Select a date"
msgstr ""
-#: .././mainwin.py:19149
+#: .././mainwin.py:19370
msgid "Delete channel"
msgstr ""
-#: .././mainwin.py:19151
+#: .././mainwin.py:19372
msgid "Delete playlist"
msgstr ""
-#: .././mainwin.py:19153
+#: .././mainwin.py:19374
msgid "Delete folder"
msgstr ""
-#: .././mainwin.py:19156
+#: .././mainwin.py:19377
msgid "Empty channel"
msgstr ""
-#: .././mainwin.py:19158
+#: .././mainwin.py:19379
msgid "Empty playlist"
msgstr ""
-#: .././mainwin.py:19160
+#: .././mainwin.py:19381
msgid "Empty folder"
msgstr ""
-#: .././mainwin.py:19194
+#: .././mainwin.py:19415
msgid "This channel does not contain any videos"
msgstr ""
-#: .././mainwin.py:19196
+#: .././mainwin.py:19417
msgid "This playlist does not contain any videos"
msgstr ""
-#: .././mainwin.py:19198
+#: .././mainwin.py:19419
msgid "This folder doesn't contain anything"
msgstr ""
-#: .././mainwin.py:19204
+#: .././mainwin.py:19425
msgid "(but there might be some files in Tartube's data folder)"
msgstr ""
-#: .././mainwin.py:19217
+#: .././mainwin.py:19438
msgid "This channel contains:"
msgstr ""
-#: .././mainwin.py:19219
+#: .././mainwin.py:19440
msgid "This playlist contains:"
msgstr ""
-#: .././mainwin.py:19221
+#: .././mainwin.py:19442
msgid "This folder contains:"
msgstr ""
-#: .././mainwin.py:19228
+#: .././mainwin.py:19449
msgid "1 folder"
msgstr ""
-#: .././mainwin.py:19230
+#: .././mainwin.py:19451
#, python-brace-format
msgid "{0} folders"
msgstr ""
-#: .././mainwin.py:19237
+#: .././mainwin.py:19458
msgid "1 channel"
msgstr ""
-#: .././mainwin.py:19239
+#: .././mainwin.py:19460
#, python-brace-format
msgid "{0} channels"
msgstr ""
-#: .././mainwin.py:19246
+#: .././mainwin.py:19467
msgid "1 playlist"
msgstr ""
-#: .././mainwin.py:19248
+#: .././mainwin.py:19469
#, python-brace-format
msgid "{0} playlists"
msgstr ""
-#: .././mainwin.py:19255 .././mainwin.py:19680
+#: .././mainwin.py:19476 .././mainwin.py:19901
msgid "1 video"
msgstr ""
-#: .././mainwin.py:19257 .././mainwin.py:19683
+#: .././mainwin.py:19478 .././mainwin.py:19904
#, python-brace-format
msgid "{0} videos"
msgstr ""
-#: .././mainwin.py:19270
+#: .././mainwin.py:19491
msgid ""
"Do you want to delete the channel from Tartube's data folder, or do you just "
"want to remove the channel from this list?"
msgstr ""
-#: .././mainwin.py:19276
+#: .././mainwin.py:19497
msgid ""
"Do you want to delete the playlist from Tartube's data folder, or do you "
"just want to remove the playlist from this list?"
msgstr ""
-#: .././mainwin.py:19282
+#: .././mainwin.py:19503
msgid ""
"Do you want to delete the folder from Tartube's data folder, or do you just "
"want to remove the folder from this list?"
msgstr ""
-#: .././mainwin.py:19291
+#: .././mainwin.py:19512
msgid ""
"Do you want to empty the channel in Tartube's data folder, or do you just "
"want to empty the channel in this list?"
msgstr ""
-#: .././mainwin.py:19297
+#: .././mainwin.py:19518
msgid ""
"Do you want to empty the playlist in Tartube's data folder, or do you just "
"want to empty the playlist in this list?"
msgstr ""
-#: .././mainwin.py:19303
+#: .././mainwin.py:19524
msgid ""
"Do you want to empty the folder in Tartube's data folder, or do you just "
"want to empty the folder in this list?"
msgstr ""
-#: .././mainwin.py:19320
+#: .././mainwin.py:19541
msgid "Just remove the channel from this list"
msgstr ""
-#: .././mainwin.py:19322
+#: .././mainwin.py:19543
msgid "Just remove the playlist from this list"
msgstr ""
-#: .././mainwin.py:19324
+#: .././mainwin.py:19545
msgid "Just remove the folder from this list"
msgstr ""
-#: .././mainwin.py:19329
+#: .././mainwin.py:19550
msgid "Just empty the channel in this list"
msgstr ""
-#: .././mainwin.py:19331
+#: .././mainwin.py:19552
msgid "Just empty the playlist in this list"
msgstr ""
-#: .././mainwin.py:19333
+#: .././mainwin.py:19554
msgid "Just empty the folder in this list"
msgstr ""
-#: .././mainwin.py:19339
+#: .././mainwin.py:19560
msgid "Delete all files"
msgstr ""
-#: .././mainwin.py:19391
+#: .././mainwin.py:19612
msgid "Export from database"
msgstr ""
-#: .././mainwin.py:19415
+#: .././mainwin.py:19636
msgid ""
"Tartube is ready to export a partial summary of its database, containing a "
"list of videos, channels, playlists and/or folders (but not including the "
"videos themselves)"
msgstr ""
-#: .././mainwin.py:19422
+#: .././mainwin.py:19643
msgid ""
"Tartube is ready to export a summary of its database, containing a list of "
"videos, channels, playlists and/or folders (but not including the videos "
"themselves)"
msgstr ""
-#: .././mainwin.py:19438
+#: .././mainwin.py:19659
msgid "Choose what should be included:"
msgstr ""
-#: .././mainwin.py:19446
+#: .././mainwin.py:19667
msgid "Include lists of videos"
msgstr ""
-#: .././mainwin.py:19451
+#: .././mainwin.py:19672
msgid "Include channels"
msgstr ""
-#: .././mainwin.py:19456
+#: .././mainwin.py:19677
msgid "Include playlists"
msgstr ""
-#: .././mainwin.py:19461
+#: .././mainwin.py:19682
msgid "Preserve folder structure"
msgstr ""
-#: .././mainwin.py:19469
+#: .././mainwin.py:19690
msgid "Export as plain text"
msgstr ""
-#: .././mainwin.py:19555
+#: .././mainwin.py:19776
msgid "Import into database"
msgstr ""
-#: .././mainwin.py:19578
+#: .././mainwin.py:19799
msgid "Choose which items to import"
msgstr ""
-#: .././mainwin.py:19599
+#: .././mainwin.py:19820
msgid "Import"
msgstr ""
-#: .././mainwin.py:19615
+#: .././mainwin.py:19836
msgid "Name"
msgstr ""
-#: .././mainwin.py:19635
+#: .././mainwin.py:19856
msgid "Import videos"
msgstr ""
-#: .././mainwin.py:19640
+#: .././mainwin.py:19861
msgid "Merge channels/playlists/folders"
msgstr ""
#. Bottom strip
-#: .././mainwin.py:19643 .././mainwin.py:21615
+#: .././mainwin.py:19864 .././mainwin.py:22242
msgid "Select all"
msgstr ""
-#: .././mainwin.py:19648
+#: .././mainwin.py:19869
msgid "Unselect all"
msgstr ""
-#: .././mainwin.py:19910
+#: .././mainwin.py:20122
+msgid "Install youtube-dl and FFmpeg"
+msgstr ""
+
+#: .././mainwin.py:20141
+msgid ""
+"Tartube could not auto-detect youtube-dl on your system. youtube-dl must be "
+"installed before you can use Tartube."
+msgstr ""
+
+#: .././mainwin.py:20163
+msgid "I have now installed youtube-dl, please detect its location"
+msgstr ""
+
+#: .././mainwin.py:20175
+msgid ""
+"I have now installed youtube-dl, please open the preferences window so I can "
+"set its location manually"
+msgstr ""
+
+#: .././mainwin.py:20317
msgid "Mount drive"
msgstr ""
-#: .././mainwin.py:19934
+#: .././mainwin.py:20341
msgid "The Tartube data folder is set to:"
msgstr ""
-#: .././mainwin.py:19947
+#: .././mainwin.py:20354
msgid "...but this folder doesn't exist"
msgstr ""
-#: .././mainwin.py:19950
+#: .././mainwin.py:20357
msgid "...but Tartube cannot write to this folder"
msgstr ""
-#: .././mainwin.py:19960
+#: .././mainwin.py:20367
msgid "I have mounted the drive, please try again"
msgstr ""
-#: .././mainwin.py:19966
+#: .././mainwin.py:20373
msgid "Use this data folder:"
msgstr ""
-#: .././mainwin.py:19993
+#: .././mainwin.py:20400
msgid "Select a different data folder"
msgstr ""
-#: .././mainwin.py:19999
+#: .././mainwin.py:20406
msgid "Use the default data folder"
msgstr ""
-#: .././mainwin.py:20005
+#: .././mainwin.py:20412
msgid "Shut down Tartube"
msgstr ""
#. 'Cancel' button
-#: .././mainwin.py:20012 .././config.py:435
+#: .././mainwin.py:20419 .././config.py:437
msgid "Cancel"
msgstr ""
-#: .././mainwin.py:20138
+#: .././mainwin.py:20545
msgid "The folder still doesn't exist. Please try a different option"
msgstr ""
-#: .././mainwin.py:20205
+#: .././mainwin.py:20620
+msgid "Process videos with FFmpeg"
+msgstr ""
+
+#: .././mainwin.py:20642
+msgid "Process 1 video with the following options:"
+msgstr ""
+
+#: .././mainwin.py:20644
+#, python-brace-format
+msgid "Process {0} videos with the following options:"
+msgstr ""
+
+#: .././mainwin.py:20652
+msgid "Reset all"
+msgstr ""
+
+#: .././mainwin.py:20660
+msgid "Add to end of filename:"
+msgstr ""
+
+#: .././mainwin.py:20670
+msgid "If regex matches filename:"
+msgstr ""
+
+#: .././mainwin.py:20678
+msgid "...then apply substitution:"
+msgstr ""
+
+#: .././mainwin.py:20686
+msgid "Change file extension:"
+msgstr ""
+
+#: .././mainwin.py:20696
+msgid "FFmpeg command-line options:"
+msgstr ""
+
+#: .././mainwin.py:20718
+msgid "If the video has a new name/extension, delete the original"
+msgstr ""
+
+#: .././mainwin.py:20727
+msgid "Remember these options for the next time"
+msgstr ""
+
+#: .././mainwin.py:20806
msgid "Stale lockfile"
msgstr ""
-#: .././mainwin.py:20242
+#: .././mainwin.py:20843
msgid ""
-"Failed to load the Tartube database file, because another instance of "
-"Tartube seems to be using it"
+"Failed to load the Tartube database file, because another copy of Tartube "
+"seems to be using it"
msgstr ""
-#: .././mainwin.py:20249
+#: .././mainwin.py:20850
+msgid "Do you want to load it anyway?"
+msgstr ""
+
+#: .././mainwin.py:20856
msgid ""
-"If you are SURE that this is the only instance of Tartube running on your "
-"system. click 'Yes' to remove the protection (and then restart Tartube)"
+"(Only click 'Yes' if you are sure that other copies of Tartube are not using "
+"the database right now)"
msgstr ""
-#: .././mainwin.py:20254
-msgid "If you are not sure, then click 'No'"
+#: .././mainwin.py:20868
+msgid "Yes, load the file"
msgstr ""
-#: .././mainwin.py:20262
-msgid "Yes, I'm sure"
+#: .././mainwin.py:20875
+msgid "No, just shut down Tartube"
msgstr ""
-#: .././mainwin.py:20269
-msgid "No, I'm not sure"
+#: .././mainwin.py:20877
+msgid "No, don't load the file"
msgstr ""
-#: .././mainwin.py:20363
+#: .././mainwin.py:20972
msgid "Rename channel"
msgstr ""
-#: .././mainwin.py:20365
+#: .././mainwin.py:20974
msgid "Rename playlist"
msgstr ""
-#: .././mainwin.py:20367
+#: .././mainwin.py:20976
msgid "Rename folder"
msgstr ""
-#: .././mainwin.py:20391
+#: .././mainwin.py:21000
msgid "Set the new name for the channel:"
msgstr ""
-#: .././mainwin.py:20393
+#: .././mainwin.py:21002
msgid "Set the new name for the playlist:"
msgstr ""
-#: .././mainwin.py:20395
+#: .././mainwin.py:21004
msgid "Set the new name for the folder:"
msgstr ""
-#: .././mainwin.py:20401
+#: .././mainwin.py:21010
msgid "N.B. This procedure will modify your filesystem!\n"
msgstr ""
-#: .././mainwin.py:20462
+#: .././mainwin.py:21071
msgid "Set download destination"
msgstr ""
-#: .././mainwin.py:20487
+#: .././mainwin.py:21096
msgid ""
"This channel can store its videos in its own system folder, or it can store "
"them in a different system folder"
msgstr ""
-#: .././mainwin.py:20492
+#: .././mainwin.py:21101
msgid ""
"This playlist can store its videos in its own system folder, or it can store "
"them in a different folder"
msgstr ""
-#: .././mainwin.py:20497
+#: .././mainwin.py:21106
msgid ""
"This folder can store its videos in its own system folder, or it can store "
"them in a different system folder"
msgstr ""
-#: .././mainwin.py:20505
+#: .././mainwin.py:21114
msgid "Choose a different system folder if:"
msgstr ""
-#: .././mainwin.py:20508
+#: .././mainwin.py:21117
msgid ""
"1. You want to add a channel and its playlists, without downloading the same "
"video twice"
msgstr ""
-#: .././mainwin.py:20515
+#: .././mainwin.py:21124
msgid ""
"2. A video creator has channels on both YouTube and BitChute, and you want "
"to add both without downloading the same video twice"
msgstr ""
-#: .././mainwin.py:20528
+#: .././mainwin.py:21137
msgid "Use this channel's own folder"
msgstr ""
-#: .././mainwin.py:20530
+#: .././mainwin.py:21139
msgid "Use this playlist's own folder"
msgstr ""
-#: .././mainwin.py:20532
+#: .././mainwin.py:21141
msgid "Use this folder's own system folder"
msgstr ""
-#: .././mainwin.py:20823
+#: .././mainwin.py:21432
msgid "Tartube's data folder will be:"
msgstr ""
-#: .././mainwin.py:20838
+#: .././mainwin.py:21447
msgid "Use this folder"
msgstr ""
-#: .././mainwin.py:20843
+#: .././mainwin.py:21452
msgid "Choose a different folder"
msgstr ""
-#: .././mainwin.py:20919
+#: .././mainwin.py:21528
msgid "Click OK to create a folder in which Tartube can store its videos"
msgstr ""
-#: .././mainwin.py:20926
+#: .././mainwin.py:21535
msgid ""
"If you have used Tartube before, you can select an existing folder instead "
"of creating a new one"
msgstr ""
-#: .././mainwin.py:20981
+#: .././mainwin.py:21590
msgid "Set nickname"
msgstr ""
-#: .././mainwin.py:21006
+#: .././mainwin.py:21615
#, python-brace-format
msgid ""
"Set a nickname for the channel '{0}' (or leave it blank to reset the "
"nickname)"
msgstr ""
-#: .././mainwin.py:21011
+#: .././mainwin.py:21620
#, python-brace-format
msgid ""
"Set a nickname for the playlist '{0}' (or leave it blank to reset the "
"nickname)"
msgstr ""
-#: .././mainwin.py:21016
+#: .././mainwin.py:21625
#, python-brace-format
msgid ""
"Set a nickname for the folder '{0}' (or leave it blank to reset the nickname)"
msgstr ""
-#: .././mainwin.py:21079
+#: .././mainwin.py:21688
msgid "Set URL"
msgstr ""
-#: .././mainwin.py:21104
+#: .././mainwin.py:21713
#, python-brace-format
msgid "Update the URL for the channel '{0}'"
msgstr ""
-#: .././mainwin.py:21108
+#: .././mainwin.py:21717
#, python-brace-format
msgid "Update the URL for the playlist '{0}'"
msgstr ""
-#: .././mainwin.py:21172
+#: .././mainwin.py:21781
msgid "Show system command"
msgstr ""
-#: .././mainwin.py:21216
-msgid "Update"
-msgstr ""
-
-#: .././mainwin.py:21225
+#: .././mainwin.py:21834
msgid "Copy to clipboard"
msgstr ""
-#: .././mainwin.py:21399
-msgid "Test youtube-dl"
+#: .././mainwin.py:22008 .././config.py:8685
+msgid "Test"
msgstr ""
-#: .././mainwin.py:21419
+#: .././mainwin.py:22028
msgid "URL of the video to download (optional)"
msgstr ""
-#: .././mainwin.py:21430
-msgid "youtube-dl command line options (optional)"
+#: .././mainwin.py:22039
+msgid "Command line options (optional)"
msgstr ""
-#: .././mainwin.py:21509
+#: .././mainwin.py:22120
msgid "Tidy up files"
msgstr ""
-#: .././mainwin.py:21511
+#: .././mainwin.py:22122
msgid "Tidy up channel"
msgstr ""
-#: .././mainwin.py:21513
+#: .././mainwin.py:22124
msgid "Tidy up playlist"
msgstr ""
-#: .././mainwin.py:21515
+#: .././mainwin.py:22126
msgid "Tidy up folder"
msgstr ""
-#: .././mainwin.py:21544
+#: .././mainwin.py:22155
msgid "Check that videos are not corrupted"
msgstr ""
-#: .././mainwin.py:21549
+#: .././mainwin.py:22160
msgid "Delete corrupted video files"
msgstr ""
-#: .././mainwin.py:21559
+#: .././mainwin.py:22170
msgid "Check that videos do/don't exist"
msgstr ""
-#: .././mainwin.py:21566
+#: .././mainwin.py:22177
msgid ""
"Delete downloaded video files (doesn't remove videos from Tartube's database)"
msgstr ""
-#: .././mainwin.py:21578
+#: .././mainwin.py:22189
msgid "Also delete all video/audio files with the same name"
msgstr ""
-#: .././mainwin.py:21587
-msgid "Delete all description files"
+#: .././mainwin.py:22197
+msgid "Delete all archive files"
msgstr ""
-#: .././mainwin.py:21591
-msgid "Delete all metadata (JSON) files"
+#: .././mainwin.py:22202
+msgid "Move thumbnails into own folder"
msgstr ""
-#: .././mainwin.py:21595
-msgid "Delete all annotation files"
-msgstr ""
-
-#: .././mainwin.py:21599
+#: .././mainwin.py:22207
msgid "Delete all thumbnail files"
msgstr ""
-#: .././mainwin.py:21607
-msgid "Delete .webp/malformed .jpg files"
+#: .././mainwin.py:22213
+msgid "Convert .webp thumbnails to .jpg using FFmpeg"
msgstr ""
-#: .././mainwin.py:21611
-msgid "Delete all youtube-dl archive files"
+#: .././mainwin.py:22222
+msgid "Move other metadata files into own folder"
msgstr ""
-#: .././mainwin.py:21620
+#: .././mainwin.py:22230
+msgid "Delete all description files"
+msgstr ""
+
+#: .././mainwin.py:22234
+msgid "Delete all metadata (JSON) files"
+msgstr ""
+
+#: .././mainwin.py:22238
+msgid "Delete all annotation files"
+msgstr ""
+
+#. (signal_connect appears below)
+#: .././mainwin.py:22247
msgid "Select none"
msgstr ""
#. 'Reset' button
#. (signal_connect appears below)
-#: .././config.py:408 .././config.py:8910
+#: .././config.py:410 .././config.py:9331 .././config.py:9372
msgid "Reset"
msgstr ""
-#: .././config.py:412
+#: .././config.py:414
msgid "Reset changes without closing the window"
msgstr ""
#. 'Apply' button
-#: .././config.py:417
+#: .././config.py:419
msgid "Apply"
msgstr ""
-#: .././config.py:421
+#: .././config.py:423
msgid "Apply changes without closing the window"
msgstr ""
-#: .././config.py:429
+#: .././config.py:431
msgid "Apply changes"
msgstr ""
-#: .././config.py:438
+#: .././config.py:440
msgid "Cancel changes"
msgstr ""
-#: .././config.py:1279
+#: .././config.py:1281
msgid "Listed as"
msgstr ""
-#: .././config.py:1291
+#: .././config.py:1293
msgid "Contained in"
msgstr ""
-#: .././config.py:1350
+#: .././config.py:1352
msgid "Channel URL"
msgstr ""
-#: .././config.py:1352
+#: .././config.py:1354
msgid "Playlist URL"
msgstr ""
-#: .././config.py:1354 .././config.py:2370
+#: .././config.py:1356 .././config.py:2372
msgid "Video URL"
msgstr ""
-#: .././config.py:1384
+#: .././config.py:1386
msgid "Download to"
msgstr ""
-#: .././config.py:1423
+#: .././config.py:1425
msgid "Location"
msgstr ""
-#: .././config.py:1444
+#: .././config.py:1446
msgid "Download _options"
msgstr ""
-#: .././config.py:1448 .././config.py:1968 .././config.py:2967
-#: .././config.py:3006
+#: .././config.py:1450 .././config.py:1970 .././config.py:3011
+#: .././config.py:3050
msgid "Download options"
msgstr ""
-#: .././config.py:1452
+#: .././config.py:1454
msgid "Apply download options"
msgstr ""
-#: .././config.py:1459
+#: .././config.py:1461
msgid "Edit download options"
msgstr ""
-#: .././config.py:1466
+#: .././config.py:1468
msgid "Remove download options"
msgstr ""
-#: .././config.py:1605
+#: .././config.py:1607
msgid "Close this window"
msgstr ""
#. Add this tab...
-#: .././config.py:2155 .././config.py:5134 .././config.py:5593
-#: .././config.py:5952 .././config.py:6192
+#: .././config.py:2157 .././config.py:5178 .././config.py:5637
+#: .././config.py:5996 .././config.py:6250
msgid "_General"
msgstr ""
-#: .././config.py:2161
+#: .././config.py:2163
msgid "General options"
msgstr ""
-#: .././config.py:2172
+#: .././config.py:2174
msgid "These options have been applied to:"
msgstr ""
-#: .././config.py:2178
+#: .././config.py:2180
msgid "All channels, playlists and folders"
msgstr ""
-#: .././config.py:2213
-msgid ""
-"Extra youtube-dl command line options (e.g. --help; do not use -o or --"
-"output)"
-msgstr ""
-
-#: .././config.py:2241
-msgid "Hide advanced download options"
+#: .././config.py:2215
+msgid "Extra command line options (e.g. --help; do not use -o or --output)"
msgstr ""
#: .././config.py:2243
+msgid "Hide advanced download options"
+msgstr ""
+
+#: .././config.py:2245
msgid "Show advanced download options"
msgstr ""
-#: .././config.py:2253
+#: .././config.py:2255
msgid "Import general download options into this window"
msgstr ""
-#: .././config.py:2268
+#: .././config.py:2270
msgid "Completely reset all download options to their default values"
msgstr ""
#. Add this tab...
-#: .././config.py:2282
+#: .././config.py:2284
msgid "_Files"
msgstr ""
-#: .././config.py:2302
+#: .././config.py:2304
msgid "File _names"
msgstr ""
-#: .././config.py:2310
+#: .././config.py:2312
msgid "File name options"
msgstr ""
-#: .././config.py:2315
+#: .././config.py:2317
msgid "Format for video file names"
msgstr ""
-#: .././config.py:2339
-msgid "youtube-dl file output template"
+#: .././config.py:2341
+msgid "File output template"
msgstr ""
-#: .././config.py:2359
+#: .././config.py:2361
msgid "Add to template:"
msgstr ""
-#: .././config.py:2364 .././config.py:5023
+#: .././config.py:2366 .././config.py:5067
msgid "Video properties"
msgstr ""
-#: .././config.py:2366
+#: .././config.py:2368
msgid "Video ID"
msgstr ""
-#: .././config.py:2367
+#: .././config.py:2369
msgid "Video title"
msgstr ""
-#: .././config.py:2368
+#: .././config.py:2370
msgid "Alternative video ID"
msgstr ""
-#: .././config.py:2369
+#: .././config.py:2371
msgid "Secondary video title"
msgstr ""
-#: .././config.py:2371
+#: .././config.py:2373
msgid "Video filename extension"
msgstr ""
-#: .././config.py:2372
+#: .././config.py:2374
msgid "Video licence"
msgstr ""
-#: .././config.py:2373
+#: .././config.py:2375
msgid "Age restriction (years)"
msgstr ""
-#: .././config.py:2374
+#: .././config.py:2376
msgid "Is a livestream"
msgstr ""
-#: .././config.py:2375
+#: .././config.py:2377
msgid "Autonumber videos, starting at 0"
msgstr ""
-#: .././config.py:2377
+#: .././config.py:2379
msgid "Creator/uploader"
msgstr ""
-#: .././config.py:2379 .././config.py:2380
+#: .././config.py:2381 .././config.py:2382
msgid "Full name of video uploader"
msgstr ""
-#: .././config.py:2381
+#: .././config.py:2383
msgid "Nickname/ID of video uploader"
msgstr ""
-#: .././config.py:2382
+#: .././config.py:2384
msgid "Channel name"
msgstr ""
-#: .././config.py:2383
+#: .././config.py:2385
msgid "Channel ID"
msgstr ""
-#: .././config.py:2384
+#: .././config.py:2386
msgid "Playlist name"
msgstr ""
-#: .././config.py:2385
+#: .././config.py:2387
msgid "Playlist ID"
msgstr ""
-#: .././config.py:2386
+#: .././config.py:2388
msgid "Video index in playlist"
msgstr ""
-#: .././config.py:2388
+#: .././config.py:2390
msgid "Date/time/location"
msgstr ""
-#: .././config.py:2390
+#: .././config.py:2392
msgid "Release date (YYYYMMDD)"
msgstr ""
-#: .././config.py:2391
+#: .././config.py:2393
msgid "Release time (UNIX timestamp)"
msgstr ""
-#: .././config.py:2392
+#: .././config.py:2394
msgid "Upload data (YYYYMMDD)"
msgstr ""
-#: .././config.py:2393
+#: .././config.py:2395
msgid "Video length (seconds)"
msgstr ""
-#: .././config.py:2394
+#: .././config.py:2396
msgid "Filming location"
msgstr ""
-#: .././config.py:2396 .././config.py:2398
+#: .././config.py:2398 .././config.py:2400
msgid "Video format"
msgstr ""
-#: .././config.py:2399
-msgid "youtube-dl format code"
+#: .././config.py:2401
+msgid "Video format code"
msgstr ""
-#: .././config.py:2400
+#: .././config.py:2402
msgid "Video width"
msgstr ""
-#: .././config.py:2401
+#: .././config.py:2403
msgid "Video height"
msgstr ""
-#: .././config.py:2403
+#: .././config.py:2405
msgid "Video frame rate"
msgstr ""
-#: .././config.py:2404
+#: .././config.py:2406
msgid "Average video/audio bitrate (KiB/s)"
msgstr ""
-#: .././config.py:2405
+#: .././config.py:2407
msgid "Average video bitrate (KiB/s)"
msgstr ""
-#: .././config.py:2406
+#: .././config.py:2408
msgid "Average audio bitrate (KiB/s)"
msgstr ""
-#: .././config.py:2408
+#: .././config.py:2410
msgid "Ratings/comments"
msgstr ""
-#: .././config.py:2410
+#: .././config.py:2412
msgid "Number of views"
msgstr ""
-#: .././config.py:2411
+#: .././config.py:2413
msgid "Number of positive ratings"
msgstr ""
-#: .././config.py:2412
+#: .././config.py:2414
msgid "Number of negative ratings"
msgstr ""
-#: .././config.py:2413
+#: .././config.py:2415
msgid "Average rating"
msgstr ""
-#: .././config.py:2414
+#: .././config.py:2416
msgid "Number of reposts"
msgstr ""
-#: .././config.py:2415
+#: .././config.py:2417
msgid "Number of comments"
msgstr ""
-#: .././config.py:2451
+#: .././config.py:2453
msgid "Add"
msgstr ""
#. Add this tab...
-#: .././config.py:2479 .././config.py:6549
+#: .././config.py:2481 .././config.py:6723
msgid "_Filesystem"
msgstr ""
-#: .././config.py:2489
+#: .././config.py:2491
msgid "Filesystem options"
msgstr ""
-#: .././config.py:2494
+#: .././config.py:2496
msgid "Restrict filenames to ASCII characters"
msgstr ""
-#: .././config.py:2500
+#: .././config.py:2502
msgid "Use the server's file modification time"
msgstr ""
-#: .././config.py:2507
+#: .././config.py:2509
msgid "Filesystem overrides"
msgstr ""
-#: .././config.py:2512
+#: .././config.py:2514
msgid "Download all videos into this folder"
msgstr ""
-#: .././config.py:2566
-msgid "_Write files"
+#: .././config.py:2568
+msgid "_Write/move files"
msgstr ""
-#: .././config.py:2572
-msgid "Write other file options"
+#: .././config.py:2576
+msgid "File write options"
msgstr ""
-#: .././config.py:2577
+#: .././config.py:2581
msgid "Write video's description to a .description file"
msgstr ""
-#: .././config.py:2583
+#: .././config.py:2587
msgid "Write video's metadata to an .info.json file"
msgstr ""
-#: .././config.py:2589
+#: .././config.py:2594
msgid "Write video's annotations to an .annotations.xml file"
msgstr ""
-#: .././config.py:2595
-msgid "Write the video's thumbnail to the same folder"
+#: .././config.py:2602
+msgid ""
+"Annotations are not downloaded when checking videos/channels/playlists/"
+"folders"
msgstr ""
#: .././config.py:2609
+msgid "Write the video's thumbnail to the same folder"
+msgstr ""
+
+#: .././config.py:2616
+msgid "File move options"
+msgstr ""
+
+#: .././config.py:2621
+msgid "Move video's description file into a sub-folder"
+msgstr ""
+
+#: .././config.py:2627
+msgid "Write video's metadata file into a sub-folder"
+msgstr ""
+
+#: .././config.py:2633
+msgid "Write video's annotations file into a sub-folder"
+msgstr ""
+
+#: .././config.py:2639
+msgid "Write the video's thumbnail into a sub-folder"
+msgstr ""
+
+#: .././config.py:2653
msgid "_Keep files"
msgstr ""
-#: .././config.py:2615
+#: .././config.py:2659
msgid "Options during real (not simulated) downloads"
msgstr ""
-#: .././config.py:2621 .././config.py:2652
+#: .././config.py:2665 .././config.py:2696
msgid "Keep the description file after Tartube shuts down"
msgstr ""
-#: .././config.py:2627 .././config.py:2658
+#: .././config.py:2671 .././config.py:2702
msgid "Keep the metadata file after Tartube shuts down"
msgstr ""
-#: .././config.py:2633 .././config.py:2664
+#: .././config.py:2677 .././config.py:2708
msgid "Keep the annotations file after Tartube shuts down"
msgstr ""
-#: .././config.py:2639 .././config.py:2670
+#: .././config.py:2683 .././config.py:2714
msgid "Keep the thumbnail file after Tartube shuts down"
msgstr ""
-#: .././config.py:2646
+#: .././config.py:2690
msgid "Options during simulated (not real) downloads"
msgstr ""
#. Add this tab...
-#: .././config.py:2684
+#: .././config.py:2728
msgid "F_ormats"
msgstr ""
-#: .././config.py:2703
+#: .././config.py:2747
msgid "_Preferred"
msgstr ""
-#: .././config.py:2711
+#: .././config.py:2755
msgid "Preferred format options"
msgstr ""
-#: .././config.py:2717
+#: .././config.py:2761
msgid "Recognised video/audio formats"
msgstr ""
-#: .././config.py:2728
+#: .././config.py:2772
msgid "Add format"
msgstr ""
-#: .././config.py:2734
+#: .././config.py:2778
msgid "List of preferred formats"
msgstr ""
-#: .././config.py:2751
+#: .././config.py:2795
msgid "Remove format"
msgstr ""
-#: .././config.py:2804
+#: .././config.py:2848
msgid "If a merge is required after post-processing, output to this format:"
msgstr ""
#. Add this tab...
-#: .././config.py:2830 .././config.py:3524
+#: .././config.py:2874 .././config.py:3568
msgid "_Advanced"
msgstr ""
-#: .././config.py:2839
+#: .././config.py:2883
msgid "Multiple format options"
msgstr ""
-#: .././config.py:2848
+#: .././config.py:2892
msgid ""
-"Multiple formats will not be downloaded, because youtube-dl is creating an "
-"archive file"
+"Multiple formats will not be downloaded, because an archive file will be "
+"created"
msgstr ""
-#: .././config.py:2851
+#: .././config.py:2895
msgid "The archive file can be disabled in the System Preferences window"
msgstr ""
-#: .././config.py:2860
+#: .././config.py:2904
msgid ""
"For each video, download the first available format from the preferred list"
msgstr ""
-#: .././config.py:2874
+#: .././config.py:2918
msgid ""
"From the preferred list, download the first format that's available for all "
"videos"
msgstr ""
-#: .././config.py:2888
+#: .././config.py:2932
msgid "For each video, download all available formats from the preferred list"
msgstr ""
-#: .././config.py:2901
+#: .././config.py:2945
msgid "Download all available formats for all videos"
msgstr ""
-#: .././config.py:2934
+#: .././config.py:2978
msgid "Other format options"
msgstr ""
-#: .././config.py:2939
+#: .././config.py:2983
msgid "Prefer free video formats, unless one is specified above"
msgstr ""
-#: .././config.py:2945
+#: .././config.py:2989
msgid "Do not download DASH-related data for YouTube videos"
msgstr ""
#. Add this tab...
-#: .././config.py:2961 .././config.py:2980 .././config.py:8017
+#: .././config.py:3005 .././config.py:3024 .././config.py:8270
msgid "_Downloads"
msgstr ""
-#: .././config.py:3023
+#: .././config.py:3067
msgid "_Playlists"
msgstr ""
-#: .././config.py:3038
+#: .././config.py:3082
msgid "_Size limits"
msgstr ""
-#: .././config.py:3052
+#: .././config.py:3096
msgid "_Dates"
msgstr ""
-#: .././config.py:3064
+#: .././config.py:3108
msgid "_Views"
msgstr ""
-#: .././config.py:3077
+#: .././config.py:3121
msgid "_Filtering"
msgstr ""
-#: .././config.py:3091
+#: .././config.py:3135
msgid "_External"
msgstr ""
-#: .././config.py:3103
+#: .././config.py:3147
msgid "_Sound only"
msgstr ""
-#: .././config.py:3108
+#: .././config.py:3152
msgid "Sound only options"
msgstr ""
-#: .././config.py:3114
+#: .././config.py:3158
msgid ""
"Download each video, extract the sound, and then discard the original videos"
msgstr ""
-#: .././config.py:3119
+#: .././config.py:3163
msgid "(requires that FFmpeg or AVConv is installed on your system)"
msgstr ""
-#: .././config.py:3129
+#: .././config.py:3173
msgid "Use this audio format:"
msgstr ""
-#: .././config.py:3144
+#: .././config.py:3188
msgid "Use this audio quality:"
msgstr ""
-#: .././config.py:3150 .././config.py:3223
+#: .././config.py:3194 .././config.py:3267
msgid "High"
msgstr ""
-#: .././config.py:3151 .././config.py:3224
+#: .././config.py:3195 .././config.py:3268
msgid "Medium"
msgstr ""
-#: .././config.py:3152 .././config.py:3225
+#: .././config.py:3196 .././config.py:3269
msgid "Low"
msgstr ""
-#: .././config.py:3170
-msgid "_Post-process"
+#: .././config.py:3214
+msgid "_Post-processing"
msgstr ""
-#: .././config.py:3176 .././config.py:3493
+#: .././config.py:3220 .././config.py:3537
msgid "Post-processing options"
msgstr ""
-#: .././config.py:3182
+#: .././config.py:3226
msgid "Post-process video files to convert them to audio-only files"
msgstr ""
-#: .././config.py:3189
-msgid "Prefer avconv over ffmpeg"
+#: .././config.py:3233
+msgid "Prefer AVConv over FFmpeg"
msgstr ""
-#: .././config.py:3197
-msgid "Prefer ffmpeg over avconv (default)"
+#: .././config.py:3241
+msgid "Prefer FFmpeg over AVConv (default)"
msgstr ""
-#: .././config.py:3205
+#: .././config.py:3249
msgid "Audio format of the post-processed file"
msgstr ""
-#: .././config.py:3218
+#: .././config.py:3262
msgid "Audio quality of the post-processed file"
msgstr ""
-#: .././config.py:3235
+#: .././config.py:3279
msgid "Encode video to another format, if necessary"
msgstr ""
-#: .././config.py:3247
+#: .././config.py:3291
msgid "Arguments to pass to post-processor"
msgstr ""
-#: .././config.py:3257
+#: .././config.py:3301
msgid "Keep original file after processing it"
msgstr ""
-#: .././config.py:3264
+#: .././config.py:3308
msgid "Merge subtitles file with video (.mp4 only)"
msgstr ""
-#: .././config.py:3275
+#: .././config.py:3319
msgid "Embed thumbnail in audio file as cover art"
msgstr ""
-#: .././config.py:3281
+#: .././config.py:3325
msgid "Write metadata to the video file"
msgstr ""
-#: .././config.py:3287
+#: .././config.py:3331
msgid "Automatically correct known faults of the file"
msgstr ""
-#: .././config.py:3293
+#: .././config.py:3337
msgid "Do nothing"
msgstr ""
-#: .././config.py:3294
+#: .././config.py:3338
msgid "Warn, but do nothing"
msgstr ""
-#: .././config.py:3295
+#: .././config.py:3339
msgid "Fix if possible, otherwise warn"
msgstr ""
#. Add this tab...
-#: .././config.py:3312
+#: .././config.py:3356
msgid "S_ubtitles"
msgstr ""
-#: .././config.py:3329
+#: .././config.py:3373
msgid "_Options"
msgstr ""
-#: .././config.py:3333
+#: .././config.py:3377
msgid "Subtitles options"
msgstr ""
-#: .././config.py:3339
+#: .././config.py:3383
msgid "Don't download the subtitles file"
msgstr ""
-#: .././config.py:3350
+#: .././config.py:3394
msgid "Download the automatic subtitles file (YouTube only)"
msgstr ""
-#: .././config.py:3362
+#: .././config.py:3406
msgid "Download all available subtitles files"
msgstr ""
-#: .././config.py:3374
+#: .././config.py:3418
msgid "Download subtitles file for these languages:"
msgstr ""
-#: .././config.py:3397
+#: .././config.py:3441
msgid "Add language"
msgstr ""
-#: .././config.py:3410
+#: .././config.py:3454
msgid "Remove language"
msgstr ""
-#: .././config.py:3468
+#: .././config.py:3512
msgid "_More options"
msgstr ""
-#: .././config.py:3474
+#: .././config.py:3518
msgid "Subtitle format options"
msgstr ""
-#: .././config.py:3480
+#: .././config.py:3524
msgid "Preferred subtitle format(s), e.g. 'srt', 'vtt', 'srt/ass/vtt/lrc/best'"
msgstr ""
-#: .././config.py:3498
+#: .././config.py:3542
msgid "Applies to .mp4 videos only; requires FFmpeg/AVConv"
msgstr ""
-#: .././config.py:3505
+#: .././config.py:3549
msgid "During post-processing, merge subtitles file with video"
msgstr ""
-#: .././config.py:3544
+#: .././config.py:3588
msgid "_Authentication"
msgstr ""
-#: .././config.py:3552
+#: .././config.py:3596
msgid "Authentication options"
msgstr ""
-#: .././config.py:3557
+#: .././config.py:3601
msgid "Username with which to log in"
msgstr ""
-#: .././config.py:3567
+#: .././config.py:3611
msgid "Password with which to log in"
msgstr ""
-#: .././config.py:3577
+#: .././config.py:3621
msgid "Password required for this URL"
msgstr ""
-#: .././config.py:3587
+#: .././config.py:3631
msgid "Two-factor authentication code"
msgstr ""
-#: .././config.py:3597
+#: .././config.py:3641
msgid "Use .netrc authentication data"
msgstr ""
-#: .././config.py:3610
+#: .././config.py:3654
msgid "_Network"
msgstr ""
-#: .././config.py:3616
+#: .././config.py:3660
msgid "Network options"
msgstr ""
-#: .././config.py:3621
+#: .././config.py:3665
msgid "Use this HTTP/HTTPS proxy"
msgstr ""
-#: .././config.py:3631
+#: .././config.py:3675
msgid "Time to wait for socket connection, before giving up"
msgstr ""
-#: .././config.py:3641
+#: .././config.py:3685
msgid "Bind with this Client-side IP address"
msgstr ""
-#: .././config.py:3651
+#: .././config.py:3695
msgid "Connect using IPv4 only"
msgstr ""
-#: .././config.py:3657
+#: .././config.py:3701
msgid "Connect using IPv6 only"
msgstr ""
-#: .././config.py:3671
+#: .././config.py:3715
msgid "_Geo-restriction"
msgstr ""
-#: .././config.py:3679
+#: .././config.py:3723
msgid "Geo-restriction options"
msgstr ""
-#: .././config.py:3684
+#: .././config.py:3728
msgid "Use this proxy to verify IP address"
msgstr ""
-#: .././config.py:3694
+#: .././config.py:3738
msgid "Bypass using fake X-Forwarded-For HTTP header"
msgstr ""
-#: .././config.py:3700
+#: .././config.py:3744
msgid "Don't bypass using fake HTTP header"
msgstr ""
-#: .././config.py:3706
+#: .././config.py:3750
msgid "Bypass geo-restriction with ISO 3166-2 country code"
msgstr ""
-#: .././config.py:3716
+#: .././config.py:3760
msgid "Bypass with explicit IP block in CIDR notation"
msgstr ""
-#: .././config.py:3739
+#: .././config.py:3783
msgid "Workaround options"
msgstr ""
-#: .././config.py:3744
-msgid "Custom user agent for youtube-dl"
+#: .././config.py:3788
+msgid "Custom user agent"
msgstr ""
-#: .././config.py:3754
+#: .././config.py:3798
msgid "Custom referer if video access has restricted domain"
msgstr ""
-#: .././config.py:3764
+#: .././config.py:3808
msgid "Force this encoding (experimental)"
msgstr ""
-#: .././config.py:3774
+#: .././config.py:3818
msgid "Suppress HTTPS certificate validation"
msgstr ""
-#: .././config.py:3781
+#: .././config.py:3825
msgid ""
"Use an unencrypted connection to retrieve information about videos (YouTube "
"only)"
msgstr ""
-#: .././config.py:3862
+#: .././config.py:3906
msgid "Prefer HLS (HTTP Live Streaming)"
msgstr ""
-#: .././config.py:3868
+#: .././config.py:3912
msgid "Prefer FFMpeg over native HLS downloader"
msgstr ""
-#: .././config.py:3874
+#: .././config.py:3918
msgid "Include advertisements (experimental feature)"
msgstr ""
-#: .././config.py:3880
+#: .././config.py:3924
msgid "Ignore errors and continue the download operation"
msgstr ""
-#: .././config.py:3886
+#: .././config.py:3930
msgid "Number of retries"
msgstr ""
-#: .././config.py:3906
+#: .././config.py:3950
msgid "Download videos suitable for this age"
msgstr ""
-#: .././config.py:3926
+#: .././config.py:3970
msgid "Playlist options"
msgstr ""
-#: .././config.py:3932
+#: .././config.py:3976
msgid ""
-"youtube-dl treats channels and playlists the same way, so these options can "
-"be used with both"
+"Channels and playlists are handled in the same way, so these options can be "
+"used with both"
msgstr ""
-#: .././config.py:3939
+#: .././config.py:3983
msgid "Start downloading playlist from index"
msgstr ""
-#: .././config.py:3950
+#: .././config.py:3994
msgid "Stop downloading playlist at index"
msgstr ""
-#: .././config.py:3961
+#: .././config.py:4005
msgid "Abort operation after downloading this many videos"
msgstr ""
-#: .././config.py:3972
+#: .././config.py:4016
msgid "Abort downloading the playlist if an error occurs"
msgstr ""
-#: .././config.py:3978
+#: .././config.py:4022
msgid "Download playlist in reverse order"
msgstr ""
-#: .././config.py:3984
+#: .././config.py:4028
msgid "Download playlist in random order"
msgstr ""
-#: .././config.py:3999
+#: .././config.py:4043
msgid "Video size limit options"
msgstr ""
-#: .././config.py:4004
+#: .././config.py:4048
msgid "Minimum file size for video downloads"
msgstr ""
-#: .././config.py:4021
+#: .././config.py:4065
msgid "Maximum file size for video downloads"
msgstr ""
-#: .././config.py:4048
+#: .././config.py:4092
msgid "Video date options"
msgstr ""
-#: .././config.py:4053
+#: .././config.py:4097
msgid "Only videos uploaded on this date"
msgstr ""
-#: .././config.py:4063 .././config.py:4083 .././config.py:4103
-#: .././config.py:8906
+#: .././config.py:4107 .././config.py:4127 .././config.py:4147
+#: .././config.py:9327 .././config.py:9368
msgid "Set"
msgstr ""
-#: .././config.py:4073
+#: .././config.py:4117
msgid "Only videos uploaded before this date"
msgstr ""
-#: .././config.py:4093
+#: .././config.py:4137
msgid "Only videos uploaded after this date"
msgstr ""
-#: .././config.py:4123
+#: .././config.py:4167
msgid "Video views options"
msgstr ""
-#: .././config.py:4128
+#: .././config.py:4172
msgid "Minimum number of views"
msgstr ""
-#: .././config.py:4139
+#: .././config.py:4183
msgid "Maximum number of views"
msgstr ""
-#: .././config.py:4164
+#: .././config.py:4208
msgid "Video filtering options"
msgstr ""
-#: .././config.py:4169
+#: .././config.py:4213
msgid "Download only matching titles (regex or caseless substring)"
msgstr ""
-#: .././config.py:4180
+#: .././config.py:4224
msgid "Don't download only matching titles (regex or caseless substring)"
msgstr ""
-#: .././config.py:4192
+#: .././config.py:4236
msgid "Generic video filter, for example:"
msgstr ""
-#: .././config.py:4212
+#: .././config.py:4256
msgid "External downloader options"
msgstr ""
-#: .././config.py:4217
+#: .././config.py:4261
msgid "Use this external downloader"
msgstr ""
-#: .././config.py:4234
+#: .././config.py:4278
msgid "Arguments to pass to external downloader"
msgstr ""
-#: .././config.py:4307 .././config.py:4733
+#: .././config.py:4351 .././config.py:4777
msgid "This procedure cannot be reversed. Are you sure you want to continue?"
msgstr ""
-#: .././config.py:4569
+#: .././config.py:4613
msgid ""
"This option won't work unless the format is also added to the list of "
"preferred formats above"
msgstr ""
-#: .././config.py:4793
+#: .././config.py:4837
msgid "When the window is re-opened, some download options will be hidden"
msgstr ""
-#: .././config.py:4802
+#: .././config.py:4846
msgid "Show advanced download options (when window re-opens)"
msgstr ""
-#: .././config.py:4815
+#: .././config.py:4859
msgid "When the window is re-opened, all download options will be visible"
msgstr ""
-#: .././config.py:4824
+#: .././config.py:4868
msgid "Hide advanced download options (when window re-opens)"
msgstr ""
-#: .././config.py:5137 .././config.py:5596 .././config.py:5955
+#: .././config.py:5181 .././config.py:5640 .././config.py:5999
msgid "General properties"
msgstr ""
-#: .././config.py:5168
+#: .././config.py:5212
msgid "Always simulate download of this video"
msgstr ""
-#: .././config.py:5191
+#: .././config.py:5235
msgid "Video has been downloaded"
msgstr ""
-#: .././config.py:5198
+#: .././config.py:5242
msgid "File size"
msgstr ""
-#: .././config.py:5212
+#: .././config.py:5256
msgid "Video is marked as unwatched"
msgstr ""
-#: .././config.py:5219
+#: .././config.py:5263
msgid "Upload time"
msgstr ""
-#: .././config.py:5233
+#: .././config.py:5277
msgid "Video is archived"
msgstr ""
-#: .././config.py:5240
+#: .././config.py:5284
msgid "Video is bookmarked"
msgstr ""
-#: .././config.py:5247
+#: .././config.py:5291
msgid "Receive time"
msgstr ""
-#: .././config.py:5261
+#: .././config.py:5305
msgid "Video is favourite"
msgstr ""
-#: .././config.py:5268
+#: .././config.py:5312
msgid "Video is in waiting list"
msgstr ""
-#: .././config.py:5291
+#: .././config.py:5335
msgid "Livestream properties"
msgstr ""
-#: .././config.py:5296
+#: .././config.py:5340
msgid "Livestream status"
msgstr ""
-#: .././config.py:5307
+#: .././config.py:5351
msgid "Waiting to start"
msgstr ""
-#: .././config.py:5309
+#: .././config.py:5353
msgid "Stream has started"
msgstr ""
-#: .././config.py:5311
+#: .././config.py:5355
msgid "Not a livestream"
msgstr ""
-#: .././config.py:5318
+#: .././config.py:5362
msgid "When the livestream starts, show a desktop notification"
msgstr ""
-#: .././config.py:5327
+#: .././config.py:5371
msgid "When the livestream starts, play an alarm"
msgstr ""
-#: .././config.py:5337
+#: .././config.py:5381
msgid "When the livestream starts, open it in the system's web browser"
msgstr ""
-#: .././config.py:5349
+#: .././config.py:5393
msgid "When the livestream starts, begin downloading it immediately"
msgstr ""
-#: .././config.py:5360 .././config.py:8436
+#: .././config.py:5404 .././config.py:8718
msgid "When a livestream stops, download it (overwriting any earlier file)"
msgstr ""
-#: .././config.py:5376
+#: .././config.py:5420
msgid "_Description"
msgstr ""
-#: .././config.py:5380
+#: .././config.py:5424
msgid "Video description"
msgstr ""
-#: .././config.py:5401 .././config.py:5753
+#: .././config.py:5445 .././config.py:5797
msgid "Errors / Warnings"
msgstr ""
-#: .././config.py:5407
+#: .././config.py:5451
msgid "Error messages produced the last time this video was checked/downloaded"
msgstr ""
-#: .././config.py:5422
+#: .././config.py:5466
msgid ""
"Warning messages produced the last time this video was checked/downloaded"
msgstr ""
-#: .././config.py:5478
+#: .././config.py:5522
msgid "Channel properties"
msgstr ""
-#: .././config.py:5481
+#: .././config.py:5525
msgid "Playlist properties"
msgstr ""
-#: .././config.py:5614
+#: .././config.py:5658
msgid "Always simulate download of videos in this channel"
msgstr ""
-#: .././config.py:5616
+#: .././config.py:5660
msgid "Always simulate download of videos in this playlist"
msgstr ""
-#: .././config.py:5626
+#: .././config.py:5670
msgid "Disable checking/downloading for this channel"
msgstr ""
-#: .././config.py:5628
+#: .././config.py:5672
msgid "Disable checking/downloading for this playlist"
msgstr ""
-#: .././config.py:5638
+#: .././config.py:5682
msgid "This channel is marked as a favourite"
msgstr ""
-#: .././config.py:5640
+#: .././config.py:5684
msgid "This playlist is marked as a favourite"
msgstr ""
-#: .././config.py:5650
+#: .././config.py:5694
msgid "Total videos"
msgstr ""
-#: .././config.py:5674
+#: .././config.py:5718
msgid "Favourite videos"
msgstr ""
-#: .././config.py:5686
+#: .././config.py:5730
msgid "Downloaded videos"
msgstr ""
-#: .././config.py:5708
+#: .././config.py:5752
msgid "_RSS feed"
msgstr ""
-#: .././config.py:5711
+#: .././config.py:5755
msgid "RSS feed"
msgstr ""
-#: .././config.py:5717
+#: .././config.py:5761
msgid ""
"If Tartube cannot detect the channel's RSS feed, you can enter the URL here"
msgstr ""
-#: .././config.py:5722
+#: .././config.py:5766
msgid ""
"If Tartube cannot detect the playlist's RSS feed, you can enter the URL here"
msgstr ""
-#: .././config.py:5727
+#: .././config.py:5771
msgid "(The feed is used to detect livestreams on compatible websites)"
msgstr ""
-#: .././config.py:5759
+#: .././config.py:5803
msgid ""
"Error messages produced the last time this channel was checked/downloaded"
msgstr ""
-#: .././config.py:5764
+#: .././config.py:5808
msgid ""
"Error messages produced the last time this playlist was checked/downloaded"
msgstr ""
-#: .././config.py:5782
+#: .././config.py:5826
msgid ""
"Warning messages produced the last time this channel was checked/downloaded"
msgstr ""
-#: .././config.py:5787
+#: .././config.py:5831
msgid ""
"Warning messages produced the last time this playlist was checked/downloaded"
msgstr ""
-#: .././config.py:5844
+#: .././config.py:5888
msgid "Folder properties"
msgstr ""
-#: .././config.py:5972
+#: .././config.py:6016
msgid "Always simulate download of videos"
msgstr ""
-#: .././config.py:5979
+#: .././config.py:6023
msgid "Disable checking/downloading for this folder"
msgstr ""
-#: .././config.py:5986
+#: .././config.py:6030
msgid "This folder is marked as a favourite"
msgstr ""
-#: .././config.py:5993
+#: .././config.py:6037
msgid "This folder is hidden"
msgstr ""
-#: .././config.py:6000
+#: .././config.py:6044
msgid "This folder can't be deleted by the user"
msgstr ""
-#: .././config.py:6007
+#: .././config.py:6051
msgid "This is a system-controlled folder"
msgstr ""
-#: .././config.py:6014
+#: .././config.py:6058
msgid "Only videos can be added to this folder"
msgstr ""
-#: .././config.py:6021
+#: .././config.py:6065
msgid "All contents deleted when Tartube shuts down"
msgstr ""
-#: .././config.py:6074
+#: .././config.py:6119
msgid "System preferences"
msgstr ""
-#: .././config.py:6211
+#: .././config.py:6270
msgid "_Language"
msgstr ""
-#: .././config.py:6216
+#: .././config.py:6275
msgid "Language preferences"
msgstr ""
-#: .././config.py:6221
+#: .././config.py:6280
msgid "Language"
msgstr ""
-#: .././config.py:6257
+#: .././config.py:6316
msgid "_Stability"
msgstr ""
-#: .././config.py:6267
+#: .././config.py:6326
msgid "Gtk library"
msgstr ""
-#: .././config.py:6272
+#: .././config.py:6331
msgid "Current version of the system's Gtk library"
msgstr ""
-#: .././config.py:6287
+#: .././config.py:6346
msgid "Gtk stability"
msgstr ""
-#: .././config.py:6326
+#: .././config.py:6385
msgid ""
"Tartube uses the Gtk graphics library. This library is notoriously "
"unreliable and may even causes crashes."
msgstr ""
-#: .././config.py:6333
+#: .././config.py:6392
msgid ""
"By default, some cosmetic features are disabled (for example, in the Videos "
"tab, the list of videos is not updated until the end of a download "
"operation)."
msgstr ""
-#: .././config.py:6341
+#: .././config.py:6400
msgid ""
"If you think that your system Gtk has been fixed (or if you want to test Gtk "
"stability), you can re-enable the cosmetic features."
msgstr ""
-#: .././config.py:6351
+#: .././config.py:6410
msgid "Disable some cosmetic features to prevent crashes and other issues"
msgstr ""
-#: .././config.py:6369
+#: .././config.py:6428
msgid "_Modules"
msgstr ""
-#: .././config.py:6374
+#: .././config.py:6433
msgid "Module availability"
msgstr ""
-#: .././config.py:6380
+#: .././config.py:6439
msgid "feedparser module is available (required for detecting livestreams)"
msgstr ""
-#: .././config.py:6390
+#: .././config.py:6449
msgid "moviepy module is available (finds the length of videos, if unknown)"
msgstr ""
-#: .././config.py:6400
+#: .././config.py:6459
msgid "playsound module is available (sound an alarm when a livestream starts)"
msgstr ""
-#: .././config.py:6410
+#: .././config.py:6469
msgid ""
"XDG module is available (saves the config file in the standard location)"
msgstr ""
-#: .././config.py:6420
+#: .././config.py:6479
+msgid ""
+"Notify module is available (shows desktop notifications; Linux/*BSD only)"
+msgstr ""
+
+#: .././config.py:6489
msgid "Module preferences"
msgstr ""
-#: .././config.py:6426
+#: .././config.py:6495
msgid ""
"Use 'moviepy' module to get a video's duration, if not known (may be slow)"
msgstr ""
-#: .././config.py:6438
+#: .././config.py:6507
msgid "Timeout applied when moviepy checks a video file"
msgstr ""
-#: .././config.py:6463
+#: .././config.py:6532
msgid "_Video matching"
msgstr ""
-#: .././config.py:6471
+#: .././config.py:6540
msgid "Video matching preferences"
msgstr ""
-#: .././config.py:6476
+#: .././config.py:6545
msgid "When matching videos on the filesystem:"
msgstr ""
-#: .././config.py:6482
+#: .././config.py:6551
msgid "The video names must match exactly"
msgstr ""
-#: .././config.py:6489
+#: .././config.py:6558
msgid "The first # characters must match exactly"
msgstr ""
-#: .././config.py:6503
+#: .././config.py:6572
msgid "Ignore the last # characters; the remaining name must match exactly"
msgstr ""
-#: .././config.py:6572
+#: .././config.py:6618
+msgid "_Debugging"
+msgstr ""
+
+#: .././config.py:6626
+msgid "Debugging preferences"
+msgstr ""
+
+#: .././config.py:6632
+msgid ""
+"Debug messages are only visible in the terminal window. These settings are "
+"not saved"
+msgstr ""
+
+#: .././config.py:6639
+msgid "Enable application debug messages (code in mainapp.py)"
+msgstr ""
+
+#: .././config.py:6648 .././config.py:6668
+msgid "...but don't show timer debug messages"
+msgstr ""
+
+#: .././config.py:6659
+msgid "Enable main winddow debug messages (code in mainwin.py)"
+msgstr ""
+
+#: .././config.py:6679
+msgid "Enabled downloader debug messages (code in downloads.py)"
+msgstr ""
+
+#: .././config.py:6746
msgid "_Device"
msgstr ""
-#: .././config.py:6577
+#: .././config.py:6751
msgid "Device preferences"
msgstr ""
-#: .././config.py:6582
+#: .././config.py:6756
msgid "Size of device (in Mb)"
msgstr ""
-#: .././config.py:6594
+#: .././config.py:6768
msgid "Free space on device (in Mb)"
msgstr ""
-#: .././config.py:6606
+#: .././config.py:6780
msgid "Warn user if disk space is less than"
msgstr ""
-#: .././config.py:6624
+#: .././config.py:6798
msgid "Halt downloads if disk space is less than"
msgstr ""
-#: .././config.py:6663
+#: .././config.py:6837
msgid "Configuration preferences"
msgstr ""
-#: .././config.py:6668
+#: .././config.py:6842
msgid "Tartube configuration file loaded from:"
msgstr ""
-#: .././config.py:6696
+#: .././config.py:6870
msgid "D_atabase"
msgstr ""
-#: .././config.py:6702
+#: .././config.py:6876
msgid "Database preferences"
msgstr ""
-#: .././config.py:6707
+#: .././config.py:6881
msgid "Tartube data folder"
msgstr ""
-#: .././config.py:6719
+#: .././config.py:6893
msgid "Change"
msgstr ""
-#: .././config.py:6721
+#: .././config.py:6895
msgid "Change to a different data folder"
msgstr ""
-#: .././config.py:6729
+#: .././config.py:6903
msgid "Recent data folders"
msgstr ""
-#: .././config.py:6750
+#: .././config.py:6924
msgid "Switch to the selected data folder"
msgstr ""
-#: .././config.py:6760
+#: .././config.py:6934
msgid "Forget"
msgstr ""
-#: .././config.py:6763
+#: .././config.py:6937
msgid "Remove the selected data folder from the list"
msgstr ""
-#: .././config.py:6772
+#: .././config.py:6946
msgid "Forget all"
msgstr ""
-#: .././config.py:6775
+#: .././config.py:6949
msgid "Forget every folder in this list (except the current one)"
msgstr ""
-#: .././config.py:6788
+#: .././config.py:6962
msgid "Move the selected folder up the list"
msgstr ""
-#: .././config.py:6796
+#: .././config.py:6970
msgid "Move the selected folder down the list"
msgstr ""
-#: .././config.py:6824
+#: .././config.py:6998
msgid ""
"On startup, load the first database on the list (not the most recently-use "
"one)"
msgstr ""
-#: .././config.py:6834
+#: .././config.py:7008
msgid "If one database is in use, try to load others"
msgstr ""
-#: .././config.py:6842
+#: .././config.py:7016
msgid "Add new data directories to this list"
msgstr ""
-#: .././config.py:6881
+#: .././config.py:7055
msgid "DB _Errors"
msgstr ""
-#: .././config.py:6889
+#: .././config.py:7063
msgid "Database error preferences"
msgstr ""
-#: .././config.py:6894
+#: .././config.py:7068
msgid "Check Tartube's database for inconsistencies, and fix them"
msgstr ""
-#: .././config.py:6898
+#: .././config.py:7072
msgid "Check DB"
msgstr ""
-#: .././config.py:6913
+#: .././config.py:7087
msgid "_Backups"
msgstr ""
-#: .././config.py:6917
+#: .././config.py:7091
msgid "Backup preferences"
msgstr ""
-#: .././config.py:6922
+#: .././config.py:7096
msgid ""
"When saving a database file, Tartube makes a backup copy of it (in case "
"something goes wrong)"
msgstr ""
-#: .././config.py:6931
+#: .././config.py:7105
msgid "Delete the backup file as soon as the save procedure is finished"
msgstr ""
-#: .././config.py:6941
+#: .././config.py:7115
msgid "Keep the backup file, replacing any previous backup file"
msgstr ""
-#: .././config.py:6952
+#: .././config.py:7126
msgid ""
"Make a new backup file once per day, after the day's first save procedure"
msgstr ""
-#: .././config.py:6963
+#: .././config.py:7137
msgid "Make a new backup file for every save procedure"
msgstr ""
-#: .././config.py:7004
+#: .././config.py:7178
msgid "_Video deletion"
msgstr ""
-#: .././config.py:7012
+#: .././config.py:7186
msgid "Automatic video deletion preferences"
msgstr ""
-#: .././config.py:7017
+#: .././config.py:7191
msgid "Automatically delete downloaded videos after this many days"
msgstr ""
-#: .././config.py:7031
+#: .././config.py:7205
msgid "...but only delete videos which have been watched"
msgstr ""
-#: .././config.py:7062
+#: .././config.py:7236
msgid "_Temporary folders"
msgstr ""
-#: .././config.py:7068
+#: .././config.py:7242
msgid "Temporary folder preferences"
msgstr ""
-#: .././config.py:7073
+#: .././config.py:7247
msgid "Empty temporary folders when Tartube shuts down"
msgstr ""
-#: .././config.py:7082
+#: .././config.py:7256
msgid "(N.B. Temporary folders are always emptied when Tartube starts up)"
msgstr ""
-#: .././config.py:7090
+#: .././config.py:7264
msgid "Open temporary folders (on the desktop) when Tartube shuts down"
msgstr ""
#. Add this tab...
-#: .././config.py:7116
+#: .././config.py:7290
msgid "_Windows"
msgstr ""
-#: .././config.py:7138
+#: .././config.py:7312
msgid "_Main window"
msgstr ""
-#: .././config.py:7144
+#: .././config.py:7318
msgid "Main window preferences"
msgstr ""
-#: .././config.py:7149
+#: .././config.py:7323
msgid "Remember the size of the main window when shutting down"
msgstr ""
-#: .././config.py:7157
+#: .././config.py:7331
msgid "Don't show the main window toolbar"
msgstr ""
-#: .././config.py:7165
+#: .././config.py:7339
msgid "Don't show labels in the main window toolbar"
msgstr ""
-#: .././config.py:7182
+#: .././config.py:7356
msgid "Show tooltips for videos, channels, playlists and folders"
msgstr ""
-#: .././config.py:7191
+#: .././config.py:7365
msgid ""
"Replace stock icons with custom icons (in case stock icons are not visible)"
msgstr ""
-#: .././config.py:7202
+#: .././config.py:7376
msgid "Show smaller icons in the Video Index (left side of the Videos Tab)"
msgstr ""
-#: .././config.py:7213
+#: .././config.py:7387
msgid ""
"In the Video Index, show detailed statistics about the videos in each "
"channel / playlist / folder"
msgstr ""
-#: .././config.py:7224
+#: .././config.py:7398
msgid ""
"After clicking on a folder, automatically expand/collapse the tree around it"
msgstr ""
-#: .././config.py:7235
+#: .././config.py:7409
msgid "Expand the whole tree, not just the level beneath the clicked folder"
msgstr ""
-#: .././config.py:7256
+#: .././config.py:7430
msgid "Disable the 'Download all' buttons in the toolbar and the Videos Tab"
msgstr ""
-#: .././config.py:7273
+#: .././config.py:7447
msgid "_Tabs"
msgstr ""
-#: .././config.py:7277
+#: .././config.py:7451
msgid "Tab preferences"
msgstr ""
-#: .././config.py:7283
+#: .././config.py:7457
msgid ""
"In the Videos Tab, show 'today' and 'yesterday' as the date, when possible"
msgstr ""
-#: .././config.py:7294
+#: .././config.py:7468
msgid "In the Progress Tab, hide finished videos / channels / playlists"
msgstr ""
-#: .././config.py:7303
+#: .././config.py:7477
msgid "In the Progress Tab, show results in reverse order"
msgstr ""
-#: .././config.py:7311
+#: .././config.py:7485
msgid "When Tartube starts, automatically open the Classic Mode tab"
msgstr ""
-#: .././config.py:7323
+#: .././config.py:7497
msgid "In the Errors/Warnings Tab, don't reset the tab text when it is clicked"
msgstr ""
-#: .././config.py:7341
+#: .././config.py:7515
msgid "_System tray"
msgstr ""
-#: .././config.py:7347
+#: .././config.py:7521
msgid "System tray preferences"
msgstr ""
-#: .././config.py:7352
+#: .././config.py:7526
msgid "Show icon in system tray"
msgstr ""
-#: .././config.py:7361
+#: .././config.py:7535
msgid "Close to the tray, rather than closing the application"
msgstr ""
-#: .././config.py:7387
+#: .././config.py:7561
msgid "_Dialogues"
msgstr ""
-#: .././config.py:7393
+#: .././config.py:7567
msgid "Dialogue window preferences"
msgstr ""
-#: .././config.py:7398
+#: .././config.py:7572
msgid "When adding channels/playlists, keep the dialogue window open"
msgstr ""
-#: .././config.py:7408
+#: .././config.py:7582
msgid "When the dialogue window opens, add URLs from the system clipboard"
msgstr ""
-#: .././config.py:7436
+#: .././config.py:7610
msgid "_Errors/Warnings"
msgstr ""
-#: .././config.py:7444
+#: .././config.py:7618
msgid "Errors/Warnings tab preferences"
msgstr ""
-#: .././config.py:7449
+#: .././config.py:7623
msgid "Show Tartube error messages"
msgstr ""
-#: .././config.py:7457
+#: .././config.py:7631
msgid "Show Tartube warning messages"
msgstr ""
-#: .././config.py:7465
+#: .././config.py:7639
msgid "Show server error messages"
msgstr ""
-#: .././config.py:7476
+#: .././config.py:7650
msgid "Show server warning messages"
msgstr ""
-#: .././config.py:7488
-msgid "youtube-dl error/warning preferences"
+#: .././config.py:7662
+msgid "Downloader error/warning preferences"
msgstr ""
-#: .././config.py:7493
-msgid ""
-"TRANSLATOR'S NOTE: These youtube-dl error messages are always in English"
+#: .././config.py:7667
+msgid "TRANSLATOR'S NOTE: These error messages are always in English"
msgstr ""
-#: .././config.py:7498
+#: .././config.py:7671
msgid "Ignore 'Child process exited with non-zero code' errors"
msgstr ""
-#: .././config.py:7507
+#: .././config.py:7680
msgid "Ignore 'Unable to download video data: HTTP Error 404' errors"
msgstr ""
-#: .././config.py:7516
+#: .././config.py:7689
msgid "Ignore 'Did not get any data blocks' errors"
msgstr ""
-#: .././config.py:7525
+#: .././config.py:7698
msgid "Ignore 'Requested formats are incompatible for merge' warnings"
msgstr ""
-#: .././config.py:7534
+#: .././config.py:7707
msgid "Ignore 'No video formats found' errors"
msgstr ""
-#: .././config.py:7542
+#: .././config.py:7715
msgid "Ignore 'There are no annotations to write' warnings"
msgstr ""
-#: .././config.py:7550
+#: .././config.py:7723
msgid "Ignore 'Video doesn't have subtitles' warnings"
msgstr ""
-#: .././config.py:7566
+#: .././config.py:7739
msgid "_Websites"
msgstr ""
-#: .././config.py:7574
+#: .././config.py:7747
msgid "YouTube error/warning preferences"
msgstr ""
-#: .././config.py:7579
+#: .././config.py:7752
msgid "Ignore YouTube copyright errors"
msgstr ""
-#: .././config.py:7587
+#: .././config.py:7760
msgid "Ignore YouTube age-restriction errors"
msgstr ""
-#: .././config.py:7595
+#: .././config.py:7768
msgid "Ignore YouTube deletion by uploader errors"
msgstr ""
-#: .././config.py:7604
+#: .././config.py:7777
msgid "General preferences"
msgstr ""
-#: .././config.py:7610
+#: .././config.py:7783
msgid ""
"Ignore any errors/warnings which match lines in this list (applies to all "
"websites)"
msgstr ""
-#: .././config.py:7623
+#: .././config.py:7796
msgid "These are ordinary strings"
msgstr ""
-#: .././config.py:7630
+#: .././config.py:7803
msgid "These are regular expressions (regexes)"
msgstr ""
#. Add this tab...
-#: .././config.py:7659
+#: .././config.py:7832
msgid "_Scheduling"
msgstr ""
-#: .././config.py:7676
+#: .././config.py:7849
msgid "_Start"
msgstr ""
-#: .././config.py:7682
+#: .././config.py:7855
msgid "Scheduled start preferences"
msgstr ""
-#: .././config.py:7687
-msgid "Automatic 'Download all' operations"
-msgstr ""
-
-#: .././config.py:7693 .././config.py:7754
-msgid "Disabled"
-msgstr ""
-
-#: .././config.py:7694 .././config.py:7755
-msgid "Performed when Tartube starts"
-msgstr ""
-
-#: .././config.py:7695 .././config.py:7756
-msgid "Performed at regular intervals"
-msgstr ""
-
-#: .././config.py:7715 .././config.py:7776
-msgid "Time (in hours) between operations"
-msgstr ""
-
-#: .././config.py:7748
+#: .././config.py:7861
msgid "Automatic 'Check all' operations"
msgstr ""
-#: .././config.py:7810
-msgid "After an automatic 'Download/Check all' operation, shut down Tartube"
+#: .././config.py:7867 .././config.py:7929 .././config.py:7991
+msgid "Disabled"
msgstr ""
-#: .././config.py:7849
+#: .././config.py:7868 .././config.py:7930 .././config.py:7992
+msgid "Performed when Tartube starts"
+msgstr ""
+
+#: .././config.py:7869 .././config.py:7931 .././config.py:7993
+msgid "Performed at regular intervals"
+msgstr ""
+
+#: .././config.py:7889 .././config.py:7951 .././config.py:8013
+msgid "Time (in hours) between operations"
+msgstr ""
+
+#: .././config.py:7923
+msgid "Automatic 'Download all' operations"
+msgstr ""
+
+#: .././config.py:7985
+msgid "Automatic custom 'Download all' operations"
+msgstr ""
+
+#: .././config.py:8047
+msgid "After an automatic operation, shut down Tartube"
+msgstr ""
+
+#: .././config.py:8102
msgid "S_top"
msgstr ""
-#: .././config.py:7855
+#: .././config.py:8108
msgid "Scheduled stop preferences"
msgstr ""
-#: .././config.py:7860
+#: .././config.py:8113
msgid "Stop all download operations after this much time"
msgstr ""
-#: .././config.py:7908
+#: .././config.py:8161
msgid "Stop all download operations after this many videos"
msgstr ""
-#: .././config.py:7935
+#: .././config.py:8188
msgid "Stop all download operations after this much disk space"
msgstr ""
-#: .././config.py:7978
+#: .././config.py:8231
msgid ""
"N.B. Disk space is estimated. This setting does not apply to simulated "
"downloads"
msgstr ""
-#: .././config.py:8023
+#: .././config.py:8276
msgid "Download operation preferences"
msgstr ""
-#: .././config.py:8029
-msgid "Automatically update youtube-dl before every download operation"
+#: .././config.py:8282
+msgid "Automatically update downloader before every download operation"
msgstr ""
-#: .././config.py:8041
-msgid ""
-"Automatically save files at the end of a download/update/refresh operation"
+#: .././config.py:8294
+msgid "Automatically save files at the end of all operations"
msgstr ""
-#: .././config.py:8052
+#: .././config.py:8304
msgid ""
"When applying download options to something, clone the general download "
"options"
msgstr ""
-#: .././config.py:8063
+#: .././config.py:8315
msgid "For simulated downloads, don't check a video in a folder more than once"
msgstr ""
-#: .././config.py:8080
+#: .././config.py:8326
+msgid "Invidious mirror"
+msgstr ""
+
+#: .././config.py:8332
+msgid "To find an updated list of Invidious mirrors, use any search engine!"
+msgstr ""
+
+#: .././config.py:8345 .././config.py:8486
+msgid "Type the exact text that replaces youtube.com e.g."
+msgstr ""
+
+#: .././config.py:8362
msgid "_Custom"
msgstr ""
-#: .././config.py:8085
+#: .././config.py:8367
msgid "Custom download preferences"
msgstr ""
-#: .././config.py:8091
+#: .././config.py:8373
msgid ""
"In custom downloads, download each video independently of its channel or "
"playlist"
msgstr ""
-#: .././config.py:8102
+#: .././config.py:8384
msgid ""
"In custom downloads, apply a delay after each video/channel/playlist is "
"download"
msgstr ""
-#: .././config.py:8112
+#: .././config.py:8394
msgid "Maximum delay to apply (in minutes)"
msgstr ""
-#: .././config.py:8129
+#: .././config.py:8411
msgid "Minimum delay to apply (in minutes; randomises the actual delay)"
msgstr ""
-#: .././config.py:8152
+#: .././config.py:8434
msgid "In custom downloads, obtain a YouTube video from the original website"
msgstr ""
-#: .././config.py:8162
+#: .././config.py:8444
msgid "In custom downloads, obtain the video from HookTube rather than YouTube"
msgstr ""
-#: .././config.py:8174
+#: .././config.py:8456
msgid ""
"In custom downloads, obtain the video from Invidious rather than YouTube"
msgstr ""
-#: .././config.py:8186
+#: .././config.py:8468
msgid ""
"In custom downloads, obtain the video from the YouTube front-end specified "
"below"
msgstr ""
-#: .././config.py:8206
-msgid ""
-"Type the exact text that replaces youtube.com e.g. "
-"i>hooktube.com"
-msgstr ""
-
-#: .././config.py:8272
+#: .././config.py:8554
msgid "Livestream preferences (compatible websites only)"
msgstr ""
-#: .././config.py:8278
+#: .././config.py:8560
msgid "Detect livestreams announced within this many days"
msgstr ""
-#: .././config.py:8293
+#: .././config.py:8575
msgid "How often to check the status of livestreams (in minutes)"
msgstr ""
-#: .././config.py:8338
+#: .././config.py:8620
msgid "Video Catalogue options"
msgstr ""
-#: .././config.py:8343
+#: .././config.py:8625
msgid "Show livestreams with a different background colour"
msgstr ""
-#: .././config.py:8356
+#: .././config.py:8638
msgid "Livestream actions (can be toggled for individual videos)"
msgstr ""
-#: .././config.py:8363
+#: .././config.py:8645
msgid "(currently disabled on MS Windows)"
msgstr ""
-#: .././config.py:8368
+#: .././config.py:8650
msgid "When a livestream starts, show a desktop notification"
msgstr ""
-#: .././config.py:8382
+#: .././config.py:8664
msgid "When a livestream starts, sound an alarm"
msgstr ""
-#: .././config.py:8405
+#: .././config.py:8687
msgid "Plays the selected sound effect"
msgstr ""
-#: .././config.py:8412
+#: .././config.py:8694
msgid "When a livestream starts, open it in the system's web browser"
msgstr ""
-#: .././config.py:8424
+#: .././config.py:8706
msgid "When a livestream starts, begin downloading it immediately"
msgstr ""
-#: .././config.py:8457
+#: .././config.py:8739
msgid "_Notifications"
msgstr ""
-#: .././config.py:8463
+#: .././config.py:8745
msgid "Desktop notification preferences"
msgstr ""
-#: .././config.py:8470
-msgid ""
-"Show a dialogue window at the end of a download/update/refresh/info/tidy "
-"operation"
+#: .././config.py:8752
+msgid "Show a dialogue window at the end of an operation"
msgstr ""
-#: .././config.py:8480
-msgid ""
-"Show a desktop notification at the end of a download/update/refresh/info/"
-"tidy operation"
+#: .././config.py:8777
+msgid "Don't notify the user at the end of an operation"
msgstr ""
-#: .././config.py:8494
-msgid ""
-"Don't notify the user at the end of a download/update/refresh/info/tidy "
-"operation"
-msgstr ""
-
-#: .././config.py:8529
+#: .././config.py:8811
msgid "_URL flexibility"
msgstr ""
-#: .././config.py:8535
+#: .././config.py:8817
msgid "URL flexibility preferences"
msgstr ""
-#: .././config.py:8542
+#: .././config.py:8824
msgid ""
"If a video's URL represents a channel/playlist, not a video, don't download "
"it"
msgstr ""
-#: .././config.py:8551
+#: .././config.py:8833
msgid "...or, download multiple videos into the containing folder"
msgstr ""
-#: .././config.py:8561
+#: .././config.py:8843
msgid "...or, create a new channel, and download the videos into that"
msgstr ""
-#: .././config.py:8572
+#: .././config.py:8854
msgid "...or, create a new playlist, and download the videos into that"
msgstr ""
-#: .././config.py:8611
+#: .././config.py:8893
msgid "_Performance"
msgstr ""
-#: .././config.py:8619
+#: .././config.py:8901
msgid "Performance limits"
msgstr ""
-#: .././config.py:8624
+#: .././config.py:8906
msgid "Limit simultaneous downloads to"
msgstr ""
-#: .././config.py:8642
+#: .././config.py:8924
msgid "Limit download speed to"
msgstr ""
-#: .././config.py:8668
+#: .././config.py:8950
msgid "Overriding video format options, limit video resolution to"
msgstr ""
-#: .././config.py:8690
+#: .././config.py:8972
msgid "Time-saving preferences"
msgstr ""
-#: .././config.py:8696
+#: .././config.py:8978
msgid ""
"Stop checking/downloading a channel/playlist when it starts sending videos "
"we already have"
msgstr ""
-#: .././config.py:8707
+#: .././config.py:8989
msgid "Stop after this many videos (when checking)"
msgstr ""
-#: .././config.py:8722
+#: .././config.py:9004
msgid "Stop after this many videos (when downloading)"
msgstr ""
-#: .././config.py:8771
+#: .././config.py:9054
msgid "_File paths"
msgstr ""
-#: .././config.py:8778
+#: .././config.py:9061
msgid "youtube-dl file paths"
msgstr ""
-#: .././config.py:8784
-msgid "youtube-dl executable (system-dependent)"
+#: .././config.py:9067
+msgid "Path to youtube-dl executable"
msgstr ""
-#: .././config.py:8797
-msgid "Default path to youtube-dl executable"
-msgstr ""
-
-#: .././config.py:8810
-msgid "Actual path to use"
-msgstr ""
-
-#: .././config.py:8816
+#. (signal_connect appears below)
+#: .././config.py:9073 .././config.py:9335 .././config.py:9376
+#: .././config.py:13316
msgid "Use default path"
msgstr ""
-#: .././config.py:8821
+#: .././config.py:9078 .././config.py:13328
msgid "Use local path"
msgstr ""
-#: .././config.py:8829
+#: .././config.py:9086 .././config.py:13340
msgid "Use PyPI path"
msgstr ""
-#: .././config.py:8856
-msgid "Shell command for update operations"
+#: .././config.py:9111
+msgid "Command for update operations"
msgstr ""
-#: .././config.py:8890
+#: .././config.py:9146
+msgid "youtube-dl forks"
+msgstr ""
+
+#: .././config.py:9151
+msgid "Use this fork of youtube-dl"
+msgstr ""
+
+#: .././config.py:9166
+msgid ""
+"If you specify a fork (e.g. youtube-dlc), it must be very similar to the "
+"original youtube-dl\n"
+"To use the original youtube-dl, leave the box empty"
+msgstr ""
+
+#: .././config.py:9182
msgid "_Preferences"
msgstr ""
-#: .././config.py:8897
-msgid "Post-processing preferences"
-msgstr ""
-
-#: .././config.py:8902
-msgid "Path to the ffmpeg/avconv binary"
-msgstr ""
-
-#: .././config.py:8925
-msgid "Install from main menu"
-msgstr ""
-
-#: .././config.py:8935
+#: .././config.py:9189
msgid "Missing video preferences"
msgstr ""
-#: .././config.py:8941
+#: .././config.py:9195
msgid ""
"Add videos which have been removed from a channel/playlist to the Missing "
"Videos folder"
msgstr ""
-#: .././config.py:8952
+#: .././config.py:9206
msgid "Only add videos that were uploaded within this many days"
msgstr ""
-#: .././config.py:8993
+#: .././config.py:9247
msgid "Other preferences"
msgstr ""
-#: .././config.py:8999
+#: .././config.py:9253
msgid ""
-"Allow youtube-dl to create its own archive file (so deleted videos are not "
+"Allow downloader to create its own archive file (so deleted videos are not "
"re-downloaded)"
msgstr ""
-#: .././config.py:9010
+#: .././config.py:9264
msgid ""
"Also create an archive file when downloading from the Classic Mode tab (not "
"recommended)"
msgstr ""
-#: .././config.py:9021
+#: .././config.py:9275
msgid "When checking videos, apply a 60-second timeout"
msgstr ""
+#: .././config.py:9285
+msgid ""
+"Convert .webp thumbnails into .jpg thumbnails (using FFmpeg) after "
+"downloading them"
+msgstr ""
+
+#: .././config.py:9303
+msgid "_FFmpeg / AVConv"
+msgstr ""
+
+#: .././config.py:9311
+msgid "Post-processing preferences"
+msgstr ""
+
+#: .././config.py:9316
+msgid ""
+"You only need to set these paths if Tartube cannot find FFmpeg / AVConv "
+"automatically"
+msgstr ""
+
+#: .././config.py:9323
+msgid "Path to the FFmpeg executable"
+msgstr ""
+
+#: .././config.py:9350
+msgid "Install from main menu"
+msgstr ""
+
+#: .././config.py:9364
+msgid "Path to the AVConv executable"
+msgstr ""
+
+#: .././config.py:9391
+msgid "Not supported on MS Windows"
+msgstr ""
+
#. Add this tab...
-#: .././config.py:9038
+#: .././config.py:9414
msgid "Out_put"
msgstr ""
-#: .././config.py:9057
+#: .././config.py:9433
msgid "_Output Tab"
msgstr ""
-#: .././config.py:9063
+#: .././config.py:9439
msgid "Output Tab preferences"
msgstr ""
-#: .././config.py:9068
-msgid "Display youtube-dl system commands in the Output Tab"
+#: .././config.py:9444
+msgid "Display downloader system commands in the Output Tab"
msgstr ""
-#: .././config.py:9077
-msgid "Display output from youtube-dl's STDOUT in the Output Tab"
+#: .././config.py:9453
+msgid "Display output from downloader's STDOUT in the Output Tab"
msgstr ""
-#: .././config.py:9086 .././config.py:9216
+#: .././config.py:9462 .././config.py:9603
msgid "...but don't write each video's JSON data"
msgstr ""
-#: .././config.py:9097 .././config.py:9227
+#: .././config.py:9473 .././config.py:9614
msgid "...but don't write each video's download progress"
msgstr ""
-#: .././config.py:9116
-msgid "Display output from youtube-dl's STDERR in the Output Tab"
+#: .././config.py:9492
+msgid "Display output from downloader's STDERR in the Output Tab"
msgstr ""
-#: .././config.py:9125
+#: .././config.py:9501
msgid "Empty pages in the Output Tab at the start of every operation"
msgstr ""
-#: .././config.py:9135
+#: .././config.py:9511
msgid ""
"Show a summary of active threads (changes are applied when Tartube restarts)"
msgstr ""
-#: .././config.py:9147
+#: .././config.py:9523
+msgid "During an update operation, automatically switch to the Output tab"
+msgstr ""
+
+#: .././config.py:9534
msgid "During a refresh operation, show all matching videos in the Output Tab"
msgstr ""
-#: .././config.py:9158
+#: .././config.py:9545
msgid "...also show all non-matching videos"
msgstr ""
-#: .././config.py:9187
+#: .././config.py:9574
msgid "_Terminal window"
msgstr ""
-#: .././config.py:9193
+#: .././config.py:9580
msgid "Terminal window preferences"
msgstr ""
-#: .././config.py:9198
-msgid "Write youtube-dl system commands to the terminal window"
+#: .././config.py:9585
+msgid "Write downloader system commands to the terminal window"
msgstr ""
-#: .././config.py:9207
-msgid "Write output from youtube-dl's STDOUT to the terminal window"
+#: .././config.py:9594
+msgid "Write output from downloader's STDOUT to the terminal window"
msgstr ""
-#: .././config.py:9249
-msgid "Write output from youtube-dl's STDERR to the terminal window"
+#: .././config.py:9636
+msgid "Write output from downloader's STDERR to the terminal window"
msgstr ""
-#: .././config.py:9268
+#: .././config.py:9655
msgid "_Both"
msgstr ""
-#: .././config.py:9273
+#: .././config.py:9660
msgid ""
"Special preferences (applies to both the Output Tab and the terminal window)"
msgstr ""
-#: .././config.py:9280
-msgid "Write verbose output (youtube-dl debugging mode)"
+#: .././config.py:9667
+msgid "Write verbose output (downloader debugging mode)"
msgstr ""
-#: .././config.py:10105
+#: .././config.py:10574
msgid "Are you sure you want to create a new database at this location?"
msgstr ""
-#: .././config.py:10212
+#: .././config.py:10681
msgid "Are you sure you want to forget this database?"
msgstr ""
-#: .././config.py:10247
+#: .././config.py:10716
msgid "Are you sure you want to forget all databases except the current one?"
msgstr ""
-#: .././config.py:10451
+#: .././config.py:10920
msgid "No database exists at this location:"
msgstr ""
-#: .././config.py:10453
+#: .././config.py:10922
msgid "Do you want to create a new one?"
msgstr ""
-#: .././config.py:10907 .././config.py:11197 .././config.py:12011
+#: .././config.py:11432 .././config.py:11737 .././config.py:12606
msgid "The new setting will be applied when Tartube restarts"
msgstr ""
-#: .././config.py:11950
+#: .././config.py:12508
+msgid "Please select the AVConv executable"
+msgstr ""
+
+#: .././config.py:12545
msgid "Please select the FFmpeg executable"
msgstr ""
-#: .././config.py:12561
+#: .././config.py:13235
msgid "Database file not loaded"
msgstr ""
-#: .././config.py:12596
+#: .././config.py:13255
+msgid "Did not try to load the database file"
+msgstr ""
+
+#: .././config.py:13280
msgid "Database file loaded"
msgstr ""
@@ -5132,17 +5345,17 @@ msgstr ""
msgid "Download did not start"
msgstr ""
-#: .././downloads.py:2420 .././info.py:352 .././updates.py:293
-#: .././updates.py:451
+#: .././downloads.py:2420 .././info.py:342 .././updates.py:277
+#: .././updates.py:445
msgid "Child process exited with non-zero code: {}"
msgstr ""
-#: .././downloads.py:2534 .././downloads.py:3331
+#: .././downloads.py:2534 .././downloads.py:3448
msgid ""
"This video has a URL that points to a channel or a playlist, not a video"
msgstr ""
-#: .././downloads.py:3223
+#: .././downloads.py:3340
msgid "Simulated download of:"
msgstr ""
@@ -5171,476 +5384,494 @@ msgid "years"
msgstr ""
#. System folder names
-#: .././formats.py:777
+#: .././formats.py:779
msgid "All Videos"
msgstr ""
-#: .././formats.py:778
+#: .././formats.py:780
msgid "Bookmarks"
msgstr ""
-#: .././formats.py:779
+#: .././formats.py:781
msgid "Favourite Videos"
msgstr ""
-#: .././formats.py:780
+#: .././formats.py:782
msgid "Livestreams"
msgstr ""
-#: .././formats.py:781
+#: .././formats.py:783
msgid "Missing Videos"
msgstr ""
-#: .././formats.py:782
+#: .././formats.py:784
msgid "New Videos"
msgstr ""
-#: .././formats.py:783
+#: .././formats.py:785
msgid "Waiting Videos"
msgstr ""
-#: .././formats.py:784
+#: .././formats.py:786
msgid "Temporary Videos"
msgstr ""
-#: .././formats.py:785
+#: .././formats.py:787
msgid "Unsorted Videos"
msgstr ""
-#: .././formats.py:790
+#: .././formats.py:792
msgid "Update using default youtube-dl path"
msgstr ""
-#: .././formats.py:792
+#: .././formats.py:794
msgid "Update using local youtube-dl path"
msgstr ""
-#: .././formats.py:794
+#: .././formats.py:796
msgid "Update using pip"
msgstr ""
-#: .././formats.py:796
+#: .././formats.py:798
msgid "Update using pip (omit --user option)"
msgstr ""
-#: .././formats.py:798
+#: .././formats.py:800
msgid "Update using pip3"
msgstr ""
-#: .././formats.py:800
+#: .././formats.py:802
msgid "Update using pip3 (omit --user option)"
msgstr ""
-#: .././formats.py:802
+#: .././formats.py:804
msgid "Update using pip3 (recommended)"
msgstr ""
-#: .././formats.py:804
+#: .././formats.py:806
msgid "Update using PyPI youtube-dl path"
msgstr ""
-#: .././formats.py:806
+#: .././formats.py:808
msgid "Windows 32-bit update (recommended)"
msgstr ""
-#: .././formats.py:808
+#: .././formats.py:810
msgid "Windows 64-bit update (recommended)"
msgstr ""
-#: .././formats.py:810
+#: .././formats.py:812
msgid "youtube-dl updates are disabled"
msgstr ""
#. Download operation stages
-#: .././formats.py:814
+#: .././formats.py:816
msgid "Queued"
msgstr ""
-#: .././formats.py:815
+#: .././formats.py:817
msgid "Active"
msgstr ""
-#: .././formats.py:816
+#: .././formats.py:818
msgid "Paused"
msgstr ""
#. (not actually used)
-#: .././formats.py:817
+#: .././formats.py:819
msgid "Completed"
msgstr ""
#. (not actually used)
#. Sub-stages of the 'Error' stage
-#: .././formats.py:818 .././formats.py:829
+#: .././formats.py:820 .././formats.py:831
msgid "Error"
msgstr ""
#. Sub-stages of the 'Active' stage
-#: .././formats.py:820
+#: .././formats.py:822
msgid "Pre-processing"
msgstr ""
-#: .././formats.py:821
+#: .././formats.py:823
msgid "Downloading"
msgstr ""
-#: .././formats.py:822
+#: .././formats.py:824
msgid "Post-processing"
msgstr ""
-#: .././formats.py:823
+#: .././formats.py:825
msgid "Checking"
msgstr ""
#. Sub-stages of the 'Completed' stage
-#: .././formats.py:825
+#: .././formats.py:827
msgid "Finished"
msgstr ""
-#: .././formats.py:826
+#: .././formats.py:828
msgid "Warning"
msgstr ""
-#: .././formats.py:827
+#: .././formats.py:829
msgid "Already downloaded"
msgstr ""
#. (not actually used)
-#: .././formats.py:830
+#: .././formats.py:832
msgid "Stopped"
msgstr ""
-#: .././formats.py:831
+#: .././formats.py:833
msgid "Filesize abort"
msgstr ""
-#: .././formats.py:841
+#: .././formats.py:843
msgid ""
"TRANSLATOR'S NOTE: ID refers to a video's unique ID on the website, e.g. on "
"YouTube \"CS9OO0S5w2k\""
msgstr ""
-#: .././formats.py:849
+#: .././formats.py:851
msgid "Custom"
msgstr ""
-#: .././formats.py:850
+#: .././formats.py:852
msgid "ID"
msgstr ""
-#: .././formats.py:851
+#: .././formats.py:853
msgid "Title"
msgstr ""
-#: .././formats.py:852
+#: .././formats.py:854
msgid "Quality"
msgstr ""
-#: .././formats.py:853
+#: .././formats.py:855
msgid "Autonumber"
msgstr ""
-#: .././formats.py:865
+#: .././formats.py:867
msgid "Any format"
msgstr ""
-#: .././info.py:186
-msgid "Starting info operation, testing youtube-dl with specified options"
+#: .././info.py:176
+msgid "Starting info operation, testing downloader with specified options"
msgstr ""
-#: .././info.py:195
+#: .././info.py:185
#, python-brace-format
msgid "Starting info operation, fetching list of video/audio formats for '{0}'"
msgstr ""
-#: .././info.py:202
+#: .././info.py:192
#, python-brace-format
msgid "Starting info operation, fetching list of subtitles for '{0}'"
msgstr ""
-#: .././info.py:343
-msgid "youtube-dl process did not start"
+#: .././info.py:333
+msgid "System process did not start"
msgstr ""
-#: .././info.py:368
+#: .././info.py:358
msgid "Info operation finished"
msgstr ""
#. (The code in self.run() will spot that the child process did not
#. start)
-#: .././info.py:421 .././updates.py:193
+#: .././info.py:408 .././updates.py:180
msgid "Child process did not start"
msgstr ""
-#: .././media.py:314
+#: .././media.py:315
msgid "TRANSLATOR'S NOTE: Source = video/channel/playlist URL"
msgstr ""
#. When the download operation is launched from the Classic Mode
#. tab, there is less to display
-#: .././media.py:317 .././media.py:1544 .././media.py:1560
+#: .././media.py:318 .././media.py:1550 .././media.py:1566
msgid "Source:"
msgstr ""
-#: .././media.py:325
+#: .././media.py:326
msgid "Location:"
msgstr ""
-#: .././media.py:336
+#: .././media.py:337
msgid "Download destination:"
msgstr ""
-#: .././media.py:1515
+#: .././media.py:1521
msgid ""
"TRANSLATOR'S NOTE: WAITING = livestream not started, LIVE = livestream "
"started"
msgstr ""
-#: .././media.py:1520
+#: .././media.py:1526
msgid "WAITING"
msgstr ""
-#: .././media.py:1522
+#: .././media.py:1528
msgid "LIVE"
msgstr ""
-#: .././media.py:1532 .././refresh.py:272 .././refresh.py:540
+#: .././media.py:1538 .././refresh.py:259 .././refresh.py:528
msgid "Channel:"
msgstr ""
-#: .././media.py:1534 .././refresh.py:274 .././refresh.py:542
+#: .././media.py:1540 .././refresh.py:261 .././refresh.py:530
msgid "Playlist:"
msgstr ""
-#: .././media.py:1536 .././refresh.py:276 .././refresh.py:544
+#: .././media.py:1542 .././refresh.py:263 .././refresh.py:532
msgid "Folder:"
msgstr ""
-#: .././media.py:1541
+#: .././media.py:1547
msgid "TRANSLATOR'S NOTE 2: Source = video/channel/playlist URL"
msgstr ""
-#: .././media.py:1550 .././media.py:1567
+#: .././media.py:1556 .././media.py:1573
msgid "File:"
msgstr ""
-#: .././media.py:2042
+#: .././media.py:2240
msgid "Today"
msgstr ""
-#: .././media.py:2044
+#: .././media.py:2242
msgid "Yesterday"
msgstr ""
-#: .././refresh.py:149
+#: .././refresh.py:139
msgid "Starting refresh operation, analysing whole database"
msgstr ""
-#: .././refresh.py:158
+#: .././refresh.py:148
msgid "Starting refresh operation, analysing '{}'"
msgstr ""
-#: .././refresh.py:202
+#: .././refresh.py:192
msgid "Refresh operation finished"
msgstr ""
-#: .././refresh.py:207
+#: .././refresh.py:197
msgid "Number of video files analysed:"
msgstr ""
-#: .././refresh.py:213
+#: .././refresh.py:203
msgid "Video files already in the database:"
msgstr ""
-#: .././refresh.py:219
+#: .././refresh.py:209
msgid "New videos found and added to the database:"
msgstr ""
-#: .././refresh.py:385 .././tidy.py:518
+#: .././refresh.py:376 .././tidy.py:556
msgid "Checking:"
msgstr ""
-#: .././refresh.py:419 .././refresh.py:592
+#: .././refresh.py:410 .././refresh.py:584
msgid "Match:"
msgstr ""
-#: .././refresh.py:437
+#: .././refresh.py:428
msgid "Non-match:"
msgstr ""
-#: .././refresh.py:485
+#: .././refresh.py:476
msgid "New video:"
msgstr ""
-#: .././refresh.py:491 .././refresh.py:598
+#: .././refresh.py:482 .././refresh.py:590
msgid "Total videos:"
msgstr ""
-#: .././refresh.py:492 .././refresh.py:599
+#: .././refresh.py:483 .././refresh.py:591
msgid "matched:"
msgstr ""
-#: .././refresh.py:493
+#: .././refresh.py:484
msgid "new:"
msgstr ""
-#: .././refresh.py:574
+#: .././refresh.py:566
msgid "Missing:"
msgstr ""
-#: .././refresh.py:600
+#: .././refresh.py:592
msgid "missing:"
msgstr ""
-#: .././tidy.py:226
+#: .././tidy.py:230
msgid "Starting tidy operation, tidying up whole data directory"
msgstr ""
-#: .././tidy.py:235
+#: .././tidy.py:239
#, python-brace-format
msgid "Starting tidy operation, tidying up '{0}'"
msgstr ""
-#: .././tidy.py:241 .././tidy.py:253 .././tidy.py:263 .././tidy.py:273
-#: .././tidy.py:285 .././tidy.py:295 .././tidy.py:305 .././tidy.py:315
-#: .././tidy.py:325 .././tidy.py:335 .././tidy.py:345
+#: .././tidy.py:245 .././tidy.py:257 .././tidy.py:267 .././tidy.py:277
+#: .././tidy.py:289 .././tidy.py:299 .././tidy.py:309 .././tidy.py:319
+#: .././tidy.py:329 .././tidy.py:339 .././tidy.py:350 .././tidy.py:360
+#: .././tidy.py:370
msgid "YES"
msgstr ""
-#: .././tidy.py:243 .././tidy.py:255 .././tidy.py:265 .././tidy.py:275
-#: .././tidy.py:287 .././tidy.py:297 .././tidy.py:307 .././tidy.py:317
-#: .././tidy.py:327 .././tidy.py:337 .././tidy.py:347
+#: .././tidy.py:247 .././tidy.py:259 .././tidy.py:269 .././tidy.py:279
+#: .././tidy.py:291 .././tidy.py:301 .././tidy.py:311 .././tidy.py:321
+#: .././tidy.py:331 .././tidy.py:341 .././tidy.py:352 .././tidy.py:362
+#: .././tidy.py:372
msgid "NO"
msgstr ""
-#: .././tidy.py:247
+#: .././tidy.py:251
msgid "Check videos are not corrupted:"
msgstr ""
-#: .././tidy.py:259
+#: .././tidy.py:263
msgid "Delete corrupted videos:"
msgstr ""
-#: .././tidy.py:269
+#: .././tidy.py:273
msgid "Check videos do/don't exist:"
msgstr ""
-#: .././tidy.py:279
+#: .././tidy.py:283
msgid "Delete all video files:"
msgstr ""
-#: .././tidy.py:291
+#: .././tidy.py:295
msgid "Delete other video/audio files:"
msgstr ""
-#: .././tidy.py:301
-msgid "Delete all description files:"
+#: .././tidy.py:305
+msgid "Delete downloader archive files:"
msgstr ""
-#: .././tidy.py:311
-msgid "Delete all metadata (JSON) files:"
+#: .././tidy.py:315
+msgid "Move thumbnails into own folder:"
msgstr ""
-#: .././tidy.py:321
-msgid "Delete all annotation files:"
-msgstr ""
-
-#: .././tidy.py:331
+#: .././tidy.py:325
msgid "Delete all thumbnail files:"
msgstr ""
-#: .././tidy.py:341
-msgid "Delete .webp/malformed .jpg files:"
+#: .././tidy.py:335
+msgid "Convert .webp thumbnails to .jpg:"
msgstr ""
-#: .././tidy.py:351
-msgid "Delete youtube-dl archive files:"
+#: .././tidy.py:345
+msgid "Move other metadata files into own folder:"
msgstr ""
-#: .././tidy.py:387
+#: .././tidy.py:356
+msgid "Delete all description files:"
+msgstr ""
+
+#: .././tidy.py:366
+msgid "Delete all metadata (JSON) files:"
+msgstr ""
+
+#: .././tidy.py:376
+msgid "Delete all annotation files:"
+msgstr ""
+
+#: .././tidy.py:412
msgid "Tidy operation finished"
msgstr ""
-#: .././tidy.py:394
+#: .././tidy.py:419
msgid "Corrupted videos found:"
msgstr ""
-#: .././tidy.py:400
+#: .././tidy.py:425
msgid "Corrupted videos deleted:"
msgstr ""
-#: .././tidy.py:408
+#: .././tidy.py:433
msgid "New video files detected:"
msgstr ""
-#: .././tidy.py:414
+#: .././tidy.py:439
msgid "Missing video files detected:"
msgstr ""
-#: .././tidy.py:422
+#: .././tidy.py:447
msgid "Non-corrupted video files deleted:"
msgstr ""
-#: .././tidy.py:428
+#: .././tidy.py:453
msgid "Other video/audio files deleted:"
msgstr ""
-#: .././tidy.py:436
-msgid "Description files deleted:"
+#: .././tidy.py:461
+msgid "Downloader archive files deleted:"
msgstr ""
-#: .././tidy.py:444
-msgid "Metadata (JSON) files deleted:"
+#: .././tidy.py:469
+msgid "Thumbnail files moved:"
msgstr ""
-#: .././tidy.py:452
-msgid "Annotation files deleted:"
-msgstr ""
-
-#: .././tidy.py:460
+#: .././tidy.py:477
msgid "Thumbnail files deleted:"
msgstr ""
-#: .././tidy.py:468
-msgid ".webp/malformed .jpg files deleted:"
+#: .././tidy.py:485
+msgid ".webp thumbnails converted to .jpg:"
msgstr ""
-#: .././tidy.py:476
-msgid "youtube-dl archive files deleted:"
+#: .././tidy.py:493
+msgid "Other metadata files moved:"
msgstr ""
-#: .././tidy.py:606
+#: .././tidy.py:501
+msgid "Description files deleted:"
+msgstr ""
+
+#: .././tidy.py:509
+msgid "Metadata (JSON) files deleted:"
+msgstr ""
+
+#: .././tidy.py:517
+msgid "Annotation files deleted:"
+msgstr ""
+
+#: .././tidy.py:647
msgid "Deleted (possibly) corrupted video file:"
msgstr ""
-#: .././tidy.py:621 .././tidy.py:1073
+#: .././tidy.py:662 .././tidy.py:1273
msgid "Video file might be corrupt:"
msgstr ""
-#: .././tidy.py:665
+#: .././tidy.py:703
msgid "Video file exists:"
msgstr ""
-#: .././tidy.py:683
+#: .././tidy.py:721
msgid "Video file doesn't exist:"
msgstr ""
-#: .././updates.py:215
+#: .././updates.py:199
msgid "Starting update operation, installing FFmpeg"
msgstr ""
-#: .././updates.py:289
+#: .././updates.py:273
msgid "FFmpeg installation did not start"
msgstr ""
-#: .././updates.py:306 .././updates.py:467
+#: .././updates.py:290 .././updates.py:461
msgid "Update operation finished"
msgstr ""
-#: .././updates.py:335
-msgid "Starting update operation, installing/updating youtube-dl"
+#: .././updates.py:317
+msgid "Starting update operation, installing/updating "
msgstr ""
-#: .././updates.py:442
-msgid "youtube-dl update did not start"
+#: .././updates.py:436
+msgid "Update did not start"
msgstr ""
diff --git a/tartube/process.py b/tartube/process.py
new file mode 100644
index 0000000..2f4255d
--- /dev/null
+++ b/tartube/process.py
@@ -0,0 +1,304 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2019-2020 A S Lewis
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see .
+
+
+"""Process operation classes."""
+
+
+# Import Gtk modules
+import gi
+from gi.repository import GObject
+
+
+# Import other modules
+import os
+import re
+import threading
+import time
+
+
+# Import our modules
+import media
+import utils
+# Use same gettext translations
+from mainapp import _
+
+
+# Classes
+
+
+class ProcessManager(threading.Thread):
+
+ """Called by mainapp.TartubeApp.process_manager_start().
+
+ Python class to manage the process operation, in which media.Video objects
+ are sent to FFmpeg for post-processing.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application
+
+ option_string (str): A string of FFmpeg options (usually a copy of
+ mainapp.TartubeApp.ffmpeg_option_string)
+
+ add_string (str): Text to add to the end of every filename (usually a
+ copy of mainapp.TartubeApp.ffmpeg_add_string)
+
+ regex_string, substitute_string (str): A regex substitution to apply to
+ every filename (usually a copy of
+ mainapp.TartubeApp.ffmpeg_regex_string and
+ .ffmpeg_substitute_string); ignored if regex_string is an empty
+ string, not ignored if substitute_string is an empty string
+
+ ext_string (str): The replacement file extension to use (usually a copy
+ of mainapp.TartubeApp.ffmpeg_ext_string); ignored if an empty
+ string
+
+ delete_flag (bool): True if the old video file should be deleted (and
+ media.Video IVs updated) if FFmpeg's output file has a different
+ name (for example, if the file extension has changed); False
+ otherwise
+
+ video_list (list): A list of media.Video objects to be passed to FFmpeg
+
+ """
+
+
+ # Standard class methods
+
+
+ def __init__(self, app_obj, option_string, add_string, regex_string,
+ substitute_string, ext_string, delete_flag, video_list):
+
+ super(ProcessManager, self).__init__()
+
+ # IV list - class objects
+ # -----------------------
+ # The mainapp.TartubeApp object
+ self.app_obj = app_obj
+ # A list of media.Video objects to be passed to FFmpeg
+ self.video_list = video_list
+
+
+ # IV list - other
+ # ---------------
+ # Flag set to False if self.stop_process_operation() is called, which
+ # halts the operation immediately
+ self.running_flag = True
+
+ # The time at which the process operation began (in seconds since
+ # epoch)
+ self.start_time = int(time.time())
+ # The time at which the process operation completed (in seconds since
+ # epoch)
+ self.stop_time = None
+ # The time (in seconds) between iterations of the loop in self.run()
+ self.sleep_time = 0.25
+
+ # The number of media.Video objects processed so far...
+ self.job_count = 0
+ # ...and the total number to process (these numbers are displayed in
+ # the progress bar in the Videos tab)
+ self.job_total = len(video_list)
+
+ # A string of FFmpeg options (usually a copy of
+ # mainapp.TartubeApp.ffmpeg_option_string)
+ self.option_string = option_string
+ # That string, converted to a list of options by self.run
+ self.option_list = []
+
+ # Text to add to the end of every filename (usually a copy of
+ # mainapp.TartubeApp.ffmpeg_add_string)
+ self.add_string = add_string
+ # A regex substitution to apply to every filename (usually a copy of
+ # mainapp.TartubeApp.ffmpeg_regex_string and
+ # .ffmpeg_substitute_string); ignored if regex_string is an
+ # empty string, not ignored if substitute_string is an empty string
+ self.regex_string = regex_string
+ self.substitute_string = substitute_string
+ # The replacement file extension to use (usually a copy of
+ # mainapp.TartubeApp.ffmpeg_ext_string); ignored if an empty string
+ self.ext_string = ext_string
+ # Flag set to True if the old video file should be deleted (and
+ # media.Video IVs updated) if FFmpeg's output file has a different
+ # name (for example, if the file extension has changed); False
+ # otherwise
+ self.delete_flag = delete_flag
+
+ # Code
+ # ----
+
+ # Prepare a list of FFmpeg options, from the option string specified by
+ # the user
+ self.option_list = utils.parse_ytdl_options(self.option_string)
+
+ # Let's get this party started!
+ self.start()
+
+
+ # Public class methods
+
+
+ def run(self):
+
+ """Called as a result of self.__init__().
+
+ Calls FFmpegManager.run_ffmpeg for every media.Video object in the
+ list.
+
+ Then informs the main application that the process operation is
+ complete.
+ """
+
+ # Show information about the process operation in the Output Tab
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ _('Starting process operation'),
+ )
+
+ # Process each video in turn
+ while self.running_flag and self.video_list:
+
+ self.process_video(self.video_list.pop(0))
+
+ # Pause a moment, before the next iteration of the loop (don't want
+ # to hog resources)
+ time.sleep(self.sleep_time)
+
+ # Operation complete. Set the stop time
+ self.stop_time = int(time.time())
+
+ # Show a confirmation in the Output Tab
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ _('Process operation finished'),
+ )
+
+ # Let the timer run for a few more seconds to prevent Gtk errors (for
+ # systems with Gtk < 3.24)
+ GObject.timeout_add(
+ 0,
+ self.app_obj.process_manager_halt_timer,
+ )
+
+
+ def process_video(self, video_obj):
+
+ """Called by self.run().
+
+ Sends a single video to FFmpeg for post-processing.
+
+ Args:
+
+ video_obj (media.Video): The video to be sent to FFmpeg
+
+ """
+
+ # Get the path to the video file, which might be in the directory of
+ # its parent channel/playlist/folder, or in a different directory
+ # altogether
+ input_path = video_obj.get_actual_path(self.app_obj)
+ # Set the output path; the same as the input path, unless the user has
+ # requested changes
+ output_file, output_ext = os.path.splitext(input_path)
+
+ if self.add_string != '':
+ output_file += self.add_string
+
+ if self.substitute_string != '':
+ output_file = re.sub(
+ self.regex_string,
+ self.substitute_string,
+ output_file,
+ )
+
+ if self.ext_string != '':
+ output_ext = self.ext_string
+
+ output_path = output_file + output_ext
+
+ # Update the main window's progress bar
+ self.job_count += 1
+ GObject.timeout_add(
+ 0,
+ self.app_obj.main_win_obj.update_progress_bar,
+ video_obj.name,
+ self.job_count,
+ self.job_total,
+ )
+
+ # Update our progress in the Output Tab
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Video') + ' ' + str(self.job_count) + '/' \
+ + str(self.job_total) + ': ' + video_obj.name,
+ )
+
+ # Show the system command we're about to execute...
+ test_list = self.app_obj.ffmpeg_manager_obj.run_ffmpeg(
+ input_path,
+ output_path,
+ self.option_list,
+ True,
+ )
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Input:') + ' ' + ' '.join(test_list[1]),
+ )
+
+ # ...and then send the command to FFmpeg for processing, which returns
+ # a list in the form (success_flag, optional_message)
+ result_list = self.app_obj.ffmpeg_manager_obj.run_ffmpeg(
+ input_path,
+ output_path,
+ self.option_list,
+ )
+
+ if not result_list or not result_list[0]:
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Output: FAILED:') + ' ' + result_list[1],
+ )
+
+ else:
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Output:') + ' ' + output_path,
+ )
+
+ # Delete the original video file, and update media.Video IVs, if
+ # required
+ if self.delete_flag \
+ and os.path.isfile(input_path) \
+ and os.path.isfile(output_path) \
+ and input_path != output_path:
+ os.remove(input_path)
+ video_obj.set_file(output_file, output_ext)
+
+
+ def stop_process_operation(self):
+
+ """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(),
+ .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item().
+
+ Stops the process operation.
+ """
+
+ self.running_flag = False
diff --git a/tartube/refresh.py b/tartube/refresh.py
index eebff31..2bd5034 100644
--- a/tartube/refresh.py
+++ b/tartube/refresh.py
@@ -39,10 +39,6 @@ import utils
from mainapp import _
-# Debugging flag (calls utils.debug_time at the start of every function)
-DEBUG_FUNC_FLAG = False
-
-
# Classes
@@ -69,9 +65,6 @@ class RefreshManager(threading.Thread):
def __init__(self, app_obj, init_obj=None):
- if DEBUG_FUNC_FLAG:
- utils.debug_time('rop 73 __init__')
-
super(RefreshManager, self).__init__()
# IV list - class objects
@@ -139,9 +132,6 @@ class RefreshManager(threading.Thread):
complete.
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('rop 143 run')
-
# Show information about the refresh operation in the Output Tab
if not self.init_obj:
self.app_obj.main_win_obj.output_tab_write_stdout(
@@ -248,9 +238,6 @@ class RefreshManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('rop 252 refresh_from_default_destination')
-
# Update the main window's progress bar
self.job_count += 1
GObject.timeout_add(
@@ -519,9 +506,6 @@ class RefreshManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('rop 519 refresh_from_actual_destination')
-
# Update the main window's progress bar
self.job_count += 1
GObject.timeout_add(
@@ -617,7 +601,4 @@ class RefreshManager(threading.Thread):
Stops the refresh operation.
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('rop 613 stop_refresh_operation')
-
self.running_flag = False
diff --git a/tartube/tartube b/tartube/tartube
index 811c795..74a957a 100644
--- a/tartube/tartube
+++ b/tartube/tartube
@@ -42,8 +42,8 @@ import mainapp
# 'Global' variables
__packagename__ = 'tartube'
-__version__ = '2.1.080'
-__date__ = '13 Aug 2020'
+__version__ = '2.2.0'
+__date__ = '30 Sep 2020'
__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis'
__license__ = """
Copyright \xa9 2019-2020 A S Lewis.
diff --git a/tartube/testing.py b/tartube/testing.py
index b4d6c92..7840885 100644
--- a/tartube/testing.py
+++ b/tartube/testing.py
@@ -29,7 +29,7 @@
# Import our modules
-# ...
+import media
# Functions
@@ -39,11 +39,11 @@ def add_test_media(app_obj):
"""Called by mainapp.TartubeApp.on_menu_test().
- Add a set of media data objects for testing. This function can only be
+ Adds a set of media data objects for testing. This function can only be
called if the debugging flags are set.
- Enable/disable various media objects by changing the 0s and 1s in the code
- below.
+ Enables/disables various media objects by changing the 0s and 1s in the
+ code below.
The videos, channels and playlists listed here have been chosen because
they are short. They have no connection to the Tartube developers.
@@ -135,3 +135,108 @@ def add_test_media(app_obj):
)
app_obj.main_win_obj.video_index_add_row(folder4)
+
+def run_test_code(app_obj):
+
+ """Called by mainapp.TartubeApp.on_menu_test_code().
+
+ Executes some arbitrary test code, and returns a result.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application
+
+ """
+
+ # ... insert code here ...
+
+ # ...
+
+ # ... insert code here ...
+
+ return "Hello world"
+
+
+def setup_screenshots(app_obj):
+
+ """Call this function from testing.run_test_code, when required.
+
+ Sets up four fake channels, with fake videos, in order to take screenshots
+ for the README.
+
+ """
+
+ folder = app_obj.add_folder(
+ 'Comedy',
+ None, # No parent
+ )
+ app_obj.main_win_obj.video_index_add_row(folder)
+
+ folder2 = app_obj.add_folder(
+ 'Music',
+ None, # No parent
+ )
+ app_obj.main_win_obj.video_index_add_row(folder2)
+
+
+ channel_list = [
+ 'PewDiePie',
+ 'https://www.youtube.com/user/PewDiePie/videos',
+ 4205,
+ 'Comedy',
+ 'T-Series',
+ 'https://www.youtube.com/aashiqui2/videos',
+ 14705,
+ 'Music',
+ 'The Beatles',
+ 'https://www.youtube.com/c/TheBeatles/videos',
+ 219,
+ 'Music',
+ 'Luke TheNotable',
+ 'https://www.youtube.com/c/LukeTheNotable/videos',
+# 486,
+ 476,
+ 'Comedy',
+ ]
+
+ while channel_list:
+
+ channel_name = channel_list.pop(0)
+ channel_url = channel_list.pop(0)
+ video_count = channel_list.pop(0)
+ folder_name = channel_list.pop(0)
+ folder_dbid = app_obj.media_name_dict[folder_name]
+ folder_obj = app_obj.media_reg_dict[folder_dbid]
+
+ channel_obj = app_obj.add_channel(
+ channel_name,
+ folder_obj,
+ channel_url,
+ None,
+ )
+ app_obj.main_win_obj.video_index_add_row(channel_obj)
+
+ for i in range(0, video_count):
+ video_obj = app_obj.add_video(
+ channel_obj,
+ 'https://www.youtube.com/', # Fake URL
+ )
+ video_obj.name = video_obj.nickname = 'Fake video'
+ video_obj.upload_time = 1
+ video_obj.receive_time = 1
+
+ app_obj.mark_video_downloaded(video_obj, True)
+
+
+def setup_screenshots2(app_obj):
+
+ """Call this function from testing.run_test_code, when required.
+
+ Makes sure all videos are marked as downloaded.
+ """
+
+ for media_data_obj in app_obj.media_reg_dict.values():
+ if isinstance(media_data_obj, media.Video) \
+ and not media_data_obj.dl_flag:
+ app_obj.mark_video_downloaded(media_data_obj, True)
+
diff --git a/tartube/tidy.py b/tartube/tidy.py
index 3b9979a..c2c8929 100644
--- a/tartube/tidy.py
+++ b/tartube/tidy.py
@@ -33,6 +33,7 @@ except:
import os
import re
+import shutil
import threading
import time
@@ -45,10 +46,6 @@ import utils
from mainapp import _
-# Debugging flag (calls utils.debug_time at the start of every function)
-DEBUG_FUNC_FLAG = False
-
-
# Classes
@@ -86,20 +83,26 @@ class TidyManager(threading.Thread):
should be deleted (as artefacts of post-processing with FFmpeg
or AVConv)
+ del_archive_flag: True if all youtube-dl archive files should be
+ deleted
+
+ move_thumb_flag: True if all thumbnail files should be moved into a
+ subdirectory
+
+ del_thumb_flag: True if all thumbnail files should be deleted
+
+ convert_webp_flag: True if all .webp thumbnail files should be
+ converted to .jpg
+
+ move_data_flag: True if description, metadata (JSON) and annotation
+ files should be moved into a subdirectory
+
del_descrip_flag: True if all description files should be deleted
del_json_flag: True if all metadata (JSON) files should be deleted
del_xml_flag: True if all annotation files should be deleted
- del_thumb_flag: True if all thumbnail files should be deleted
-
- del_webp_flag: True if all thumbnail files in .webp or malformed
- .jpg format should be deleted (see comments below)
-
- del_archive_flag: True if all youtube-dl archive files should be
- deleted
-
"""
@@ -108,9 +111,6 @@ class TidyManager(threading.Thread):
def __init__(self, app_obj, choices_dict):
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 112 __init__')
-
super(TidyManager, self).__init__()
# IV list - class objects
@@ -151,23 +151,24 @@ class TidyManager(threading.Thread):
# True if all video/audio files with the same name should be deleted
# (as artefacts of post-processing with FFmpeg or AVConv)
self.del_others_flag = choices_dict['del_others_flag']
+ # True if all youtube-dl archive files should be deleted
+ self.del_archive_flag = choices_dict['del_archive_flag']
+ # True if all thumbnail files should be moved into a subdirectory
+ self.move_thumb_flag = choices_dict['move_thumb_flag']
+ # True if all thumbnail files should be deleted
+ self.del_thumb_flag = choices_dict['del_thumb_flag']
+ # True if all .webp thumbnail files should be converted to .jpg.
+ # Requires mainapp.TartubeApp.ffmpeg_fail_flag set to False
+ self.convert_webp_flag = choices_dict['convert_webp_flag']
+ # True if description, metadata (JSON) and annotation files should be
+ # moved into a subdirectory
+ self.move_data_flag = choices_dict['move_data_flag']
# True if all description files should be deleted
self.del_descrip_flag = choices_dict['del_descrip_flag']
# True if all metadata (JSON) files should be deleted
self.del_json_flag = choices_dict['del_json_flag']
# True if all annotation files should be deleted
self.del_xml_flag = choices_dict['del_xml_flag']
- # True if all thumbnail files should be deleted
- self.del_thumb_flag = choices_dict['del_thumb_flag']
- # v2.1.027. In June 2020, YouTube started serving .webp thumbnails.
- # At the time of writing, Gtk can't display them. A youtube-dl fix is
- # expected, which will convert .webp thumbnails to .jpg; in
- # anticipation of that, we add an option to remove .webp files
- # True if all thumbnail files in .webp or malformed .jpg format should
- # be deleted
- self.del_webp_flag = choices_dict['del_webp_flag']
- # True if all youtube-dl archive files should be deleted
- self.del_archive_flag = choices_dict['del_archive_flag']
# The number of media data objects whose directories have been tidied
# so far...
@@ -183,17 +184,23 @@ class TidyManager(threading.Thread):
self.video_no_exist_count = 0
self.video_deleted_count = 0
self.other_deleted_count = 0
+ self.archive_deleted_count = 0
+ self.thumb_moved_count = 0
+ self.thumb_deleted_count = 0
+ self.webp_converted_count = 0
+ self.data_moved_count = 0
self.descrip_deleted_count = 0
self.json_deleted_count = 0
self.xml_deleted_count = 0
- self.thumb_deleted_count = 0
- self.webp_deleted_count = 0
- self.archive_deleted_count = 0
# Code
# ----
+ # Do not convert .webp thumbnails, if not allowed
+ if self.app_obj.ffmpeg_fail_flag:
+ self.convert_webp_flag = False
+
# Let's get this party started!
self.start()
@@ -216,9 +223,6 @@ class TidyManager(threading.Thread):
complete.
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 220 run')
-
# Show information about the tidy operation in the Output Tab
if not self.init_obj:
self.app_obj.main_win_obj.output_tab_write_stdout(
@@ -291,6 +295,57 @@ class TidyManager(threading.Thread):
' ' + _('Delete other video/audio files:') + ' ' + text,
)
+ if self.del_archive_flag:
+ text = _('YES')
+ else:
+ text = _('NO')
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Delete downloader archive files:') + ' ' + text,
+ )
+
+ if self.move_thumb_flag:
+ text = _('YES')
+ else:
+ text = _('NO')
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Move thumbnails into own folder:') + ' ' + text,
+ )
+
+ if self.del_thumb_flag:
+ text = _('YES')
+ else:
+ text = _('NO')
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Delete all thumbnail files:') + ' ' + text,
+ )
+
+ if self.convert_webp_flag:
+ text = _('YES')
+ else:
+ text = _('NO')
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Convert .webp thumbnails to .jpg:') + ' ' + text,
+ )
+
+ if self.move_data_flag:
+ text = _('YES')
+ else:
+ text = _('NO')
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Move other metadata files into own folder:') \
+ + ' ' + text,
+ )
+
if self.del_descrip_flag:
text = _('YES')
else:
@@ -321,36 +376,6 @@ class TidyManager(threading.Thread):
' ' + _('Delete all annotation files:') + ' ' + text,
)
- if self.del_thumb_flag:
- text = _('YES')
- else:
- text = _('NO')
-
- self.app_obj.main_win_obj.output_tab_write_stdout(
- 1,
- ' ' + _('Delete all thumbnail files:') + ' ' + text,
- )
-
- if self.del_webp_flag:
- text = _('YES')
- else:
- text = _('NO')
-
- self.app_obj.main_win_obj.output_tab_write_stdout(
- 1,
- ' ' + _('Delete .webp/malformed .jpg files:') + ' ' + text,
- )
-
- if self.del_archive_flag:
- text = _('YES')
- else:
- text = _('NO')
-
- self.app_obj.main_win_obj.output_tab_write_stdout(
- 1,
- ' ' + _('Delete youtube-dl archive files:') + ' ' + text,
- )
-
# Compile a list of channels, playlists and folders to tidy up (each
# one has their own sub-directory inside Tartube's data directory)
obj_list = []
@@ -429,6 +454,46 @@ class TidyManager(threading.Thread):
+ str(self.other_deleted_count),
)
+ if self.del_archive_flag:
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Downloader archive files deleted:') + ' ' \
+ + str(self.archive_deleted_count),
+ )
+
+ if self.move_thumb_flag:
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Thumbnail files moved:') + ' ' \
+ + str(self.thumb_moved_count),
+ )
+
+ if self.del_thumb_flag:
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Thumbnail files deleted:') + ' ' \
+ + str(self.thumb_deleted_count),
+ )
+
+ if self.convert_webp_flag:
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('.webp thumbnails converted to .jpg:') + ' ' \
+ + str(self.webp_converted_count),
+ )
+
+ if self.move_data_flag:
+
+ self.app_obj.main_win_obj.output_tab_write_stdout(
+ 1,
+ ' ' + _('Other metadata files moved:') + ' ' \
+ + str(self.data_moved_count),
+ )
+
if self.del_descrip_flag:
self.app_obj.main_win_obj.output_tab_write_stdout(
@@ -453,30 +518,6 @@ class TidyManager(threading.Thread):
+ str(self.xml_deleted_count),
)
- if self.del_thumb_flag:
-
- self.app_obj.main_win_obj.output_tab_write_stdout(
- 1,
- ' ' + _('Thumbnail files deleted:') + ' ' \
- + str(self.thumb_deleted_count),
- )
-
- if self.del_webp_flag:
-
- self.app_obj.main_win_obj.output_tab_write_stdout(
- 1,
- ' ' + _('.webp/malformed .jpg files deleted:') + ' ' \
- + str(self.webp_deleted_count),
- )
-
- if self.del_archive_flag:
-
- self.app_obj.main_win_obj.output_tab_write_stdout(
- 1,
- ' ' + _('youtube-dl archive files deleted:') + ' ' \
- + str(self.archive_deleted_count),
- )
-
# Let the timer run for a few more seconds to prevent Gtk errors (for
# systems with Gtk < 3.24)
GObject.timeout_add(
@@ -498,9 +539,6 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 502 tidy_directory')
-
# Update the main window's progress bar
self.job_count += 1
GObject.timeout_add(
@@ -527,6 +565,21 @@ class TidyManager(threading.Thread):
if self.del_video_flag:
self.delete_video(media_data_obj)
+ if self.del_archive_flag:
+ self.delete_archive(media_data_obj)
+
+ if self.move_thumb_flag:
+ self.move_thumb(media_data_obj)
+
+ if self.del_thumb_flag:
+ self.delete_thumb(media_data_obj)
+
+ if self.convert_webp_flag:
+ self.convert_webp(media_data_obj)
+
+ if self.move_data_flag:
+ self.move_data(media_data_obj)
+
if self.del_descrip_flag:
self.delete_descrip(media_data_obj)
@@ -536,15 +589,6 @@ class TidyManager(threading.Thread):
if self.del_xml_flag:
self.delete_xml(media_data_obj)
- if self.del_thumb_flag:
- self.delete_thumb(media_data_obj)
-
- if self.del_webp_flag:
- self.delete_webp(media_data_obj)
-
- if self.del_archive_flag:
- self.delete_archive(media_data_obj)
-
def check_video_corrupt(self, media_data_obj):
@@ -560,9 +604,6 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 564 check_video_corrupt')
-
for video_obj in media_data_obj.compile_all_videos( [] ):
if video_obj.file_name is not None \
@@ -638,9 +679,6 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 642 check_videos_exist')
-
for video_obj in media_data_obj.compile_all_videos( [] ):
if video_obj.file_name is not None:
@@ -699,9 +737,6 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 703 delete_video')
-
ext_list = formats.VIDEO_FORMAT_LIST.copy()
ext_list.extend(formats.AUDIO_FORMAT_LIST)
@@ -784,12 +819,12 @@ class TidyManager(threading.Thread):
self.other_deleted_count += 1
- def delete_descrip(self, media_data_obj):
+ def delete_archive(self, media_data_obj):
"""Called by self.tidy_directory().
- Checks all child videos of the specified media data object. If the
- associated description file exists, delete it.
+ Checks the specified media data object's directory. If a youtube-dl
+ archive file is found there, delete it.
Args:
@@ -798,43 +833,26 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 802 delete_descrip')
+ archive_path = os.path.abspath(
+ os.path.join(
+ media_data_obj.get_default_dir(self.app_obj),
+ 'ytdl-archive.txt',
+ ),
+ )
- for video_obj in media_data_obj.compile_all_videos( [] ):
+ if os.path.isfile(archive_path):
- if video_obj.file_name is not None:
-
- descrip_path = video_obj.get_actual_path_by_ext(
- self.app_obj,
- '.description',
- )
-
- # If the video's parent container has an alternative download
- # destination set, we must check the corresponding media
- # data object. If the latter also has a media.Video object
- # matching this video, then this function returns None and
- # nothing is deleted
- descrip_path = self.check_video_in_actual_dir(
- media_data_obj,
- video_obj,
- descrip_path,
- )
-
- if descrip_path is not None \
- and os.path.isfile(descrip_path):
-
- # Delete the description file
- os.remove(descrip_path)
- self.descrip_deleted_count += 1
+ # Delete the archive file
+ os.remove(archive_path)
+ self.archive_deleted_count += 1
- def delete_json(self, media_data_obj):
+ def move_thumb(self, media_data_obj):
"""Called by self.tidy_directory().
Checks all child videos of the specified media data object. If the
- associated metadata (JSON) file exists, delete it.
+ associated thumbnail file exists, moves it into its own sub-directory.
Args:
@@ -843,80 +861,53 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 847 delete_json')
-
for video_obj in media_data_obj.compile_all_videos( [] ):
if video_obj.file_name is not None:
- json_path = video_obj.get_actual_path_by_ext(
+ # Thumbnails might be in one of four locations. If the
+ # thumbnail has already been moved into /.thumbs, then of
+ # course we don't move it again (and this function returns an
+ # empty list)
+ path_list = utils.find_thumbnail_restricted(
self.app_obj,
- '.info.json',
- )
-
- # If the video's parent container has an alternative download
- # destination set, we must check the corresponding media
- # data object. If the latter also has a media.Video object
- # matching this video, then this function returns None and
- # nothing is deleted
- json_path = self.check_video_in_actual_dir(
- media_data_obj,
video_obj,
- json_path,
)
- if json_path is not None \
- and os.path.isfile(json_path):
+ if path_list:
- # Delete the metadata file
- os.remove(json_path)
- self.json_deleted_count += 1
+ main_path = os.path.abspath(
+ os.path.join(
+ path_list[0], path_list[1],
+ ),
+ )
+ subdir = os.path.abspath(
+ os.path.join(
+ path_list[0], self.app_obj.thumbs_sub_dir,
+ ),
+ )
- def delete_xml(self, media_data_obj):
+ subdir_path = os.path.abspath(
+ os.path.join(
+ path_list[0],
+ self.app_obj.thumbs_sub_dir,
+ path_list[1],
+ ),
+ )
- """Called by self.tidy_directory().
+ if os.path.isfile(main_path) \
+ and not os.path.isfile(subdir_path):
- Checks all child videos of the specified media data object. If the
- associated annotation file exists, delete it.
+ try:
+ if not os.path.isdir(subdir):
+ os.makedirs(subdir)
- Args:
+ shutil.move(main_path, subdir_path)
+ self.thumb_moved_count += 1
- media_data_obj (media.Channel, media.Playlist or media.Folder):
- The media data object whose directory must be tidied up
-
- """
-
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 892 delete_xml')
-
- for video_obj in media_data_obj.compile_all_videos( [] ):
-
- if video_obj.file_name is not None:
-
- xml_path = video_obj.get_actual_path_by_ext(
- self.app_obj,
- '.annotations.xml',
- )
-
- # If the video's parent container has an alternative download
- # destination set, we must check the corresponding media
- # data object. If the latter also has a media.Video object
- # matching this video, then this function returns None and
- # nothing is deleted
- xml_path = self.check_video_in_actual_dir(
- media_data_obj,
- video_obj,
- xml_path,
- )
-
- if xml_path is not None \
- and os.path.isfile(xml_path):
-
- # Delete the annotation file
- os.remove(xml_path)
- self.xml_deleted_count += 1
+ except:
+ pass
def delete_thumb(self, media_data_obj):
@@ -933,14 +924,11 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 937 delete_thumb')
-
for video_obj in media_data_obj.compile_all_videos( [] ):
if video_obj.file_name is not None:
- # Thumbnails might be in one of two locations
+ # Thumbnails might be in one of four locations
thumb_path = utils.find_thumbnail(self.app_obj, video_obj)
# If the video's parent container has an alternative download
@@ -964,13 +952,13 @@ class TidyManager(threading.Thread):
self.thumb_deleted_count += 1
- def delete_webp(self, media_data_obj):
+ def convert_webp(self, media_data_obj):
"""Called by self.tidy_directory().
Checks all child videos of the specified media data object. If the
associated thumbnail file in a .webp or malformed .jpg format exists,
- delete it
+ convert it to .jpg.
Args:
@@ -979,14 +967,11 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 983 delete_webp')
-
for video_obj in media_data_obj.compile_all_videos( [] ):
if video_obj.file_name is not None:
- # Thumbnails might be in one of two locations
+ # Thumbnails might be in one of four locations
thumb_path = utils.find_thumbnail_webp(self.app_obj, video_obj)
# If the video's parent container has an alternative download
@@ -1005,17 +990,26 @@ class TidyManager(threading.Thread):
if thumb_path is not None \
and os.path.isfile(thumb_path):
- # Delete the thumbnail file
- os.remove(thumb_path)
- self.webp_deleted_count += 1
+ # Convert to .jpg
+ if not self.app_obj.ffmpeg_manager_obj.convert_webp(
+ thumb_path
+ ):
+ # FFmpeg is probably not installed; don't try any more
+ # conversions
+ self.convert_webp_flag = False
+ self.app_obj.set_ffmpeg_fail_flag(True)
+
+ else:
+
+ self.webp_converted_count += 1
- def delete_archive(self, media_data_obj):
+ def move_data(self, media_data_obj):
"""Called by self.tidy_directory().
- Checks the specified media data object's directory. If a youtube-dl
- archive file is found there, delete it.
+ Checks all child videos of the specified media data object. If the
+ associated thumbnail file exists, moves it into its own sub-directory.
Args:
@@ -1024,21 +1018,230 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 1028 delete_archive')
+ for video_obj in media_data_obj.compile_all_videos( [] ):
- archive_path = os.path.abspath(
- os.path.join(
- media_data_obj.get_default_dir(self.app_obj),
- 'ytdl-archive.txt',
- ),
- )
+ if video_obj.file_name is not None:
- if os.path.isfile(archive_path):
+ # Description/JSON/annotations files might be in one of four
+ # locations. If the file has already been moved into /.data,
+ # then of course we don't move it again
+ for ext in ['.description', '.info.json', '.annotations.xml']:
- # Delete the archive file
- os.remove(archive_path)
- self.archive_deleted_count += 1
+ main_path = video_obj.get_actual_path_by_ext(
+ self.app_obj,
+ ext,
+ )
+
+ subdir = os.path.abspath(
+ os.path.join(
+ video_obj.parent_obj.get_actual_dir(self.app_obj),
+ self.app_obj.metadata_sub_dir,
+ ),
+ )
+
+ subdir_path \
+ = video_obj.get_actual_path_in_subdirectory_by_ext(
+ self.app_obj,
+ ext,
+ )
+
+ if os.path.isfile(main_path) \
+ and not os.path.isfile(subdir_path):
+
+ try:
+ if not os.path.isdir(subdir):
+ os.makedirs(subdir)
+
+ # (os.rename sometimes fails on external hard
+ # drives; this is safer)
+ shutil.move(main_path, subdir_path)
+ self.data_moved_count += 1
+
+ except:
+ pass
+
+
+ def delete_descrip(self, media_data_obj):
+
+ """Called by self.tidy_directory().
+
+ Checks all child videos of the specified media data object. If the
+ associated description file exists, delete it.
+
+ Args:
+
+ media_data_obj (media.Channel, media.Playlist or media.Folder):
+ The media data object whose directory must be tidied up
+
+ """
+
+ for video_obj in media_data_obj.compile_all_videos( [] ):
+
+ if video_obj.file_name is not None:
+
+ main_path = video_obj.get_actual_path_by_ext(
+ self.app_obj,
+ '.description',
+ )
+
+ # If the video's parent container has an alternative download
+ # destination set, we must check the corresponding media
+ # data object. If the latter also has a media.Video object
+ # matching this video, then this function returns None and
+ # nothing is deleted
+ main_path = self.check_video_in_actual_dir(
+ media_data_obj,
+ video_obj,
+ main_path,
+ )
+
+ if main_path is not None \
+ and os.path.isfile(main_path):
+
+ # Delete the description file
+ os.remove(main_path)
+ self.descrip_deleted_count += 1
+
+ # (Repeat for a file that might be in the sub-directory
+ # '.data')
+ subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext(
+ self.app_obj,
+ '.description',
+ )
+
+ subdir_path = self.check_video_in_actual_dir(
+ subdir_path,
+ video_obj,
+ subdir_path,
+ )
+
+ if subdir_path is not None \
+ and os.path.isfile(subdir_path):
+
+ os.remove(subdir_path)
+ self.descrip_deleted_count += 1
+
+
+ def delete_json(self, media_data_obj):
+
+ """Called by self.tidy_directory().
+
+ Checks all child videos of the specified media data object. If the
+ associated metadata (JSON) file exists, delete it.
+
+ Args:
+
+ media_data_obj (media.Channel, media.Playlist or media.Folder):
+ The media data object whose directory must be tidied up
+
+ """
+
+ for video_obj in media_data_obj.compile_all_videos( [] ):
+
+ if video_obj.file_name is not None:
+
+ main_path = video_obj.get_actual_path_by_ext(
+ self.app_obj,
+ '.info.json',
+ )
+
+ # If the video's parent container has an alternative download
+ # destination set, we must check the corresponding media
+ # data object. If the latter also has a media.Video object
+ # matching this video, then this function returns None and
+ # nothing is deleted
+ main_path = self.check_video_in_actual_dir(
+ media_data_obj,
+ video_obj,
+ main_path,
+ )
+
+ if main_path is not None \
+ and os.path.isfile(main_path):
+
+ # Delete the metadata file
+ os.remove(main_path)
+ self.json_deleted_count += 1
+
+ # (Repeat for a file that might be in the sub-directory
+ # '.data')
+ subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext(
+ self.app_obj,
+ '.info.json',
+ )
+
+ subdir_path = self.check_video_in_actual_dir(
+ media_data_obj,
+ video_obj,
+ subdir_path,
+ )
+
+ if subdir_path is not None \
+ and os.path.isfile(subdir_path):
+
+ os.remove(subdir_path)
+ self.json_deleted_count += 1
+
+
+ def delete_xml(self, media_data_obj):
+
+ """Called by self.tidy_directory().
+
+ Checks all child videos of the specified media data object. If the
+ associated annotation file exists, delete it.
+
+ Args:
+
+ media_data_obj (media.Channel, media.Playlist or media.Folder):
+ The media data object whose directory must be tidied up
+
+ """
+
+ for video_obj in media_data_obj.compile_all_videos( [] ):
+
+ if video_obj.file_name is not None:
+
+ main_path = video_obj.get_actual_path_by_ext(
+ self.app_obj,
+ '.annotations.xml',
+ )
+
+ # If the video's parent container has an alternative download
+ # destination set, we must check the corresponding media
+ # data object. If the latter also has a media.Video object
+ # matching this video, then this function returns None and
+ # nothing is deleted
+ main_path = self.check_video_in_actual_dir(
+ media_data_obj,
+ video_obj,
+ main_path,
+ )
+
+ if main_path is not None \
+ and os.path.isfile(main_path):
+
+ # Delete the annotation file
+ os.remove(main_path)
+ self.xml_deleted_count += 1
+
+ # (Repeat for a file that might be in the sub-directory
+ # '.data')
+ subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext(
+ self.app_obj,
+ '.annotations.xml',
+ )
+
+ subdir_path = self.check_video_in_actual_dir(
+ media_data_obj,
+ video_obj,
+ subdir_path,
+ )
+
+ if subdir_path is not None \
+ and os.path.isfile(subdir_path):
+
+ os.remove(subdir_path)
+ self.xml_deleted_count += 1
def call_moviepy(self, video_obj, video_path):
@@ -1059,9 +1262,6 @@ class TidyManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 1063 call_moviepy')
-
try:
clip = moviepy.editor.VideoFileClip(video_path)
@@ -1075,7 +1275,7 @@ class TidyManager(threading.Thread):
)
- def check_video_in_actual_dir(self, container_obj, video_obj, file_path):
+ def check_video_in_actual_dir(self, container_obj, video_obj, delete_path):
"""Called by self.delete_video(), .delete_descrip(), .delete_json(),
.delete_xml() and .delete_thumb().
@@ -1083,8 +1283,8 @@ class TidyManager(threading.Thread):
If the video's parent container has an alternative download destination
set, we must check the corresponding media data object. If the latter
also has a media.Video object matching this video, then this function
- returns None and nothing is deleted. Otherwise, the specified file_path
- is returned, so it can be deleted.
+ returns None and nothing is deleted. Otherwise, the specified
+ delete_path is returned, so it can be deleted.
Args:
@@ -1094,23 +1294,20 @@ class TidyManager(threading.Thread):
video_obj (media.Video): A video contained in that channel,
playlist or folder
- file_path (str): The path to a file which the calling function
+ delete_path (str): The path to a file which the calling function
wants to delete
Returns:
- The specified file_path if it can be deleted, or None if it should
- not be deleted
+ The specified delete_path if it can be deleted, or None if it
+ should not be deleted
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 1108 check_video_in_actual_dir')
-
if container_obj.dbid == container_obj.master_dbid:
# No alternative download destination to check
- return file_path
+ return delete_path
else:
@@ -1129,7 +1326,7 @@ class TidyManager(threading.Thread):
# There are no videos with the same name, so the file can be
# deleted
- return file_path
+ return delete_path
def stop_tidy_operation(self):
@@ -1140,7 +1337,4 @@ class TidyManager(threading.Thread):
Stops the tidy operation.
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('top 1144 stop_tidy_operation')
-
self.running_flag = False
diff --git a/tartube/updates.py b/tartube/updates.py
index d7d490e..f2714f0 100644
--- a/tartube/updates.py
+++ b/tartube/updates.py
@@ -44,10 +44,6 @@ import utils
from mainapp import _
-# Debugging flag (calls utils.debug_time at the start of every function)
-DEBUG_FUNC_FLAG = False
-
-
# Classes
@@ -79,9 +75,6 @@ class UpdateManager(threading.Thread):
def __init__(self, app_obj, update_type):
- if DEBUG_FUNC_FLAG:
- utils.debug_time('uop 83 __init__')
-
super(UpdateManager, self).__init__()
# IV list - class objects
@@ -139,9 +132,6 @@ class UpdateManager(threading.Thread):
Initiates the download.
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('uop 143 run')
-
if self.update_type == 'ffmpeg':
self.install_ffmpeg()
else:
@@ -163,9 +153,6 @@ class UpdateManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('uop 167 create_child_process')
-
info = preexec = None
if os.name == 'nt':
@@ -206,9 +193,6 @@ class UpdateManager(threading.Thread):
application with the result of the update (success or failure).
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('uop 210 install_ffmpeg')
-
# Show information about the update operation in the Output Tab
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
@@ -326,13 +310,11 @@ class UpdateManager(threading.Thread):
application with the result of the update (success or failure).
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('uop 330 install_ytdl')
-
# Show information about the update operation in the Output Tab
+ downloader = self.app_obj.get_downloader()
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
- _('Starting update operation, installing/updating youtube-dl'),
+ _('Starting update operation, installing/updating ' + downloader),
)
# Prepare the system command
@@ -344,18 +326,25 @@ class UpdateManager(threading.Thread):
cmd_list \
= self.app_obj.ytdl_update_dict[self.app_obj.ytdl_update_current]
- # Convert a path beginning with ~ (not on MS Windows)
- if os.name != 'nt':
- cmd_list[0] = re.sub('^\~', os.path.expanduser('~'), cmd_list[0])
+ mod_list = []
+ for arg in cmd_list:
+
+ # Substitute in the fork, if one is specified
+ arg = self.app_obj.check_downloader(arg)
+ # Convert a path beginning with ~ (not on MS Windows)
+ if os.name != 'nt':
+ arg = re.sub('^\~', os.path.expanduser('~'), arg)
+
+ mod_list.append(arg)
# Create a new child process using that command
- self.create_child_process(cmd_list)
+ self.create_child_process(mod_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),
+ space.join(mod_list),
)
# So that we can read from the child process STDOUT and STDERR, attach
@@ -390,9 +379,14 @@ class UpdateManager(threading.Thread):
# "The script youtube-dl is installed in '...' which is not
# on PATH. Consider adding this directory to PATH..."
if re.search('It looks like you installed', stdout) \
- or re.search('The script youtube-dl is installed', stdout):
+ or re.search(
+ 'The script ' + downloader + ' is installed',
+ stdout,
+ ):
self.stderr_list.append(stdout)
+
else:
+
# Try to intercept the new version number for
# youtube-dl
self.intercept_version_from_stdout(stdout)
@@ -439,7 +433,7 @@ class UpdateManager(threading.Thread):
# situations)
if self.child_process is None:
- msg = _('youtube-dl update did not start')
+ msg = _('Update did not start')
self.stderr_list.append(msg)
self.app_obj.main_win_obj.output_tab_write_stdout(
1,
@@ -488,9 +482,6 @@ class UpdateManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('uop 489 intercept_version_from_stdout')
-
substring = re.search(
'Requirement already up\-to\-date.*\(([\d\.]+)\)\s*$',
stdout,
@@ -525,9 +516,6 @@ class UpdateManager(threading.Thread):
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('uop 526 is_child_process_alive')
-
if self.child_process is None:
return False
@@ -544,9 +532,6 @@ class UpdateManager(threading.Thread):
Terminates the child process.
"""
- if DEBUG_FUNC_FLAG:
- utils.debug_time('uop 545 stop_update_operation')
-
if self.is_child_process_alive():
if os.name == 'nt':
diff --git a/tartube/utils.py b/tartube/utils.py
index c93a982..cebcc66 100644
--- a/tartube/utils.py
+++ b/tartube/utils.py
@@ -445,7 +445,7 @@ def convert_youtube_id_to_rss(media_type, youtube_id):
youtube_id (str): The YouTube channel or playlist ID
- Return values:
+ Returns:
The full URL for the RSS feed
@@ -485,12 +485,12 @@ def convert_youtube_to_hooktube(url):
return url
-def convert_youtube_to_invidious(url):
+def convert_youtube_to_invidious(app_obj, url):
"""Can be called by anything.
Converts a YouTube weblink to an Invidious weblink (but doesn't modify
- links to other sites.
+ links to other sites).
Args:
@@ -502,11 +502,12 @@ def convert_youtube_to_invidious(url):
"""
- if re.search(r'^https?:\/\/(www)+\.youtube\.com', url):
+ if re.search(r'^https?:\/\/(www)+\.youtube\.com', url) \
+ and re.search('\w+\.\w+', app_obj.custom_invidious_mirror):
url = re.sub(
r'youtube\.com',
- 'invidio.us',
+ app_obj.custom_invidious_mirror,
url,
# Substitute first occurence only
1,
@@ -775,31 +776,87 @@ def find_thumbnail(app_obj, video_obj, temp_dir_flag=False):
for ext in formats.IMAGE_FORMAT_LIST:
# Look in Tartube's permanent data directory
- path = video_obj.get_actual_path_by_ext(app_obj, ext)
-
- if os.path.isfile(path):
- return path
+ normal_path = video_obj.check_actual_path_by_ext(app_obj, ext)
+ if normal_path is not None:
+ return normal_path
elif temp_dir_flag:
# Look in temporary data directory
data_dir_len = len(app_obj.downloads_dir)
- temp_path = app_obj.temp_dl_dir + path[data_dir_len:]
+ temp_path = video_obj.get_actual_path_by_ext(app_obj, ext)
+ temp_path = app_obj.temp_dl_dir + temp_path[data_dir_len:]
if os.path.isfile(temp_path):
return temp_path
+ # Catch YouTube .jpg thumbnails, in the form .jpg?...
+ normal_path = video_obj.get_actual_path_by_ext(app_obj, '.jpg*')
+ for glob_path in glob.glob(normal_path):
+ if os.path.isfile(glob_path):
+ return glob_path
+
+ if temp_dir_flag:
+
+ temp_path = video_obj.get_actual_path_by_ext(app_obj, '.jpg*')
+ temp_path = app_obj.temp_dl_dir + temp_path[data_dir_len:]
+
+ for glob_path in glob.glob(temp_path):
+ if os.path.isfile(glob_path):
+ return glob_path
+
+
+ # No matching thumbnail found
return None
+def find_thumbnail_restricted(app_obj, video_obj):
+
+ """Called by mainapp.TartubeApp.update_video_when_file_found().
+
+ Modified version of utils.find_thumbnail().
+
+ Returns the path of the thumbnail in the same directory as its video. The
+ path is returned as a list, so the calling code can convert it into the
+ equivalent path in the '.thumbs' subdirectory.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application
+
+ video_obj (media.Video): The video object handling the downloaded video
+
+ Returns:
+
+ return_list (list): A list whose items, when combined, will be the full
+ path to the thumbnail file. If no thumbnail file was found, an
+ empty list is returned
+
+ """
+
+ for ext in formats.IMAGE_FORMAT_LIST:
+
+ actual_dir = video_obj.parent_obj.get_actual_dir(app_obj)
+ test_path = os.path.abspath(
+ os.path.join(
+ actual_dir,
+ video_obj.file_name + ext,
+ ),
+ )
+
+ if os.path.isfile(test_path):
+ return [ actual_dir, video_obj.file_name + ext ]
+
+ # No matching thumbnail found
+ return []
+
+
def find_thumbnail_webp(app_obj, video_obj):
- """Called by tidy.TidyManager.delete_webp().
+ """Can be called by anything.
- In June 2020, YouTube started serving .webp thumbnails. At the time of
- writing (v2.1.027), Gtk can't display them. A youtube-dl fix is expected,
- which will convert .webp thumbnails to .jpg; in anticipation of that, we
- add an option to remove .webp files.
+ In June 2020, YouTube started serving .webp thumbnails. Gtk cannot display
+ them, so Tartube typically converts themto .jpg.
This is a modified version of utils.find_thumbnail(), which looks for
thumbnails in the .webp or malformed .jpg format, and return the path to
@@ -817,16 +874,36 @@ def find_thumbnail_webp(app_obj, video_obj):
"""
- path = video_obj.get_actual_path_by_ext(app_obj, '.webp')
- if os.path.isfile(path):
- return path
+ for ext in ('.webp', '.jpg'):
- # malformed .jpg thumbnail files have an extension .jpg?sqp=-XXX, where XXX
- # is a large number of random characters
- path = video_obj.get_actual_path_by_ext(app_obj, '.jpg?*')
- for actual_path in glob.glob(path):
- if os.path.isfile(actual_path):
- return actual_path
+ main_path = video_obj.get_actual_path_by_ext(app_obj, ext)
+ if os.path.isfile(main_path) \
+ and app_obj.ffmpeg_manager_obj.is_webp(main_path):
+ return main_path
+
+ # The extension may be followed by additional characters, e.g.
+ # .jpg?sqp=-XXX (as well as several other patterns)
+ for actual_path in glob.glob(main_path + '*'):
+ if os.path.isfile(actual_path) \
+ and app_obj.ffmpeg_manager_obj.is_webp(actual_path):
+ return actual_path
+
+ subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext(
+ app_obj,
+ ext,
+ )
+
+ if os.path.isfile(subdir_path) \
+ and app_obj.ffmpeg_manager_obj.is_webp(subdir_path):
+ return subdir_path
+
+ for actual_path in glob.glob(subdir_path + '*'):
+ if os.path.isfile(actual_path) \
+ and app_obj.ffmpeg_manager_obj.is_webp(actual_path):
+ return actual_path
+
+ # No webp thumbnail found
+ return None
def format_bytes(num_bytes):
@@ -912,11 +989,8 @@ divert_mode=None):
# If actually downloading videos, use (or create) an archive file so that,
# if the user deletes the videos, youtube-dl won't try to download them
# again
- # We don't use an archive file when:
- # 1. Downloading into a system folder
- # 2. Checking for missing videos
- if not missing_video_check_flag \
- and (
+ # We don't use an archive file when downloading into a system folder
+ if (
not dl_classic_flag and app_obj.allow_ytdl_archive_flag \
or dl_classic_flag and app_obj.classic_ytdl_archive_flag
):
@@ -958,9 +1032,17 @@ divert_mode=None):
# 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:
+ # If both paths have been set, prefer ffmpeg, unless the 'prefer_avconv'
+ # download option had been specified
+ if '--prefer-avconv' in options_list and app_obj.avconv_path is not None:
+ options_list.append('--ffmpeg-location')
+ options_list.append(app_obj.avconv_path)
+ elif app_obj.ffmpeg_path is not None:
options_list.append('--ffmpeg-location')
options_list.append(app_obj.ffmpeg_path)
+ elif app_obj.avconv_path is not None:
+ options_list.append('--ffmpeg-location')
+ options_list.append(app_obj.avconv_path)
# Convert a YouTube URL to an alternative YouTube front-end, if required
source = media_data_obj.source
@@ -968,14 +1050,14 @@ divert_mode=None):
if divert_mode == 'hooktube':
source = convert_youtube_to_hooktube(source)
elif divert_mode == 'invidious':
- source = convert_youtube_to_invidious(source)
+ source = convert_youtube_to_invidious(app_obj, source)
elif divert_mode == 'custom' \
and app_obj.custom_dl_divert_website is not None \
and len(app_obj.custom_dl_divert_website) > 2:
source = convert_youtube_to_other(app_obj, source)
# Convert a path beginning with ~ (not on MS Windows)
- ytdl_path = app_obj.ytdl_path
+ ytdl_path = app_obj.check_downloader(app_obj.ytdl_path)
if os.name != 'nt':
ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path)
@@ -1062,6 +1144,100 @@ def is_youtube(url):
return False
+def move_metadata_to_subdir(app_obj, video_obj, ext):
+
+ """Can be called by anything.
+
+ Moves a description, JSON or annotations file from the same directory as
+ its video, into the subdirectory '.data'.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application
+
+ video_obj (media.Video): The file's parent video
+
+ ext (str): The file extension, which will be one of '.description',
+ '.info.json' or '.annotations.xml'
+
+ """
+
+ main_path = video_obj.get_actual_path_by_ext(app_obj, '.description')
+ subdir = os.path.abspath(
+ os.path.join(
+ video_obj.parent_obj.get_actual_dir(app_obj),
+ app_obj.metadata_sub_dir,
+ ),
+ )
+
+ subdir_path = video_obj.get_actual_path_in_subdirectory_by_ext(
+ app_obj,
+ '.description',
+ )
+
+ if os.path.isfile(main_path) and not os.path.isfile(subdir_path):
+
+ try:
+ if not os.path.isdir(subdir):
+ os.makedirs(subdir)
+
+ # (os.rename sometimes fails on external hard drives; this
+ # is safer)
+ shutil.move(main_path, subdir_path)
+
+ except:
+ pass
+
+
+def move_thumbnail_to_subdir(app_obj, video_obj):
+
+ """Can be called by anything.
+
+ Moves a thumbnail file from the same directory as its video, into the
+ subdirectory '.thumbs'.
+
+ Args:
+
+ app_obj (mainapp.TartubeApp): The main application
+
+ video_obj (media.Video): The file's parent video
+
+ """
+
+ path_list = utils.find_thumbnail_restricted(app_obj, video_obj)
+ if path_list:
+
+ main_path = os.path.abspath(
+ os.path.join(
+ path_list[0], path_list[1],
+ ),
+ )
+
+ subdir = os.path.abspath(
+ os.path.join(
+ path_list[0], app_obj.thumbs_sub_dir,
+ ),
+ )
+
+ subdir_path = os.path.abspath(
+ os.path.join(
+ path_list[0], app_obj.thumbs_sub_dir, path_list[1],
+ ),
+ )
+
+ if os.path.isfile(main_path) \
+ and not os.path.isfile(subdir_path):
+
+ try:
+ if not os.path.isdir(subdir):
+ os.makedirs(subdir)
+
+ shutil.move(main_path, subdir_path)
+
+ except:
+ pass
+
+
def open_file(uri):
"""Can be called by anything.
@@ -1086,9 +1262,15 @@ def parse_ytdl_options(options_string):
"""Called by options.OptionsParser.parse() or info.InfoManager.run().
+ Also called by process.ProcessManager.__init__, to parse FFmpeg command-
+ line options on the same basis.
+
Parses the 'extra_cmd_string' option, which can contain arguments inside
double quotes "..." (arguments that can therefore contain whitespace)
+ If options_string contains newline characters, then it terminates an
+ argument, closing newline character or not.
+
Args:
options_string (str): A string containing various youtube-dl
@@ -1100,31 +1282,33 @@ def parse_ytdl_options(options_string):
"""
- # Set a flag for an item beginning with double quotes, and reset it for an
- # item ending in double quotes
- quote_flag = False
- # Temporary list to hold such quoted arguments
- quote_list = []
# Add options, one at a time, to a list
return_list = []
- return_string = ''
- for item in options_string.split():
+ for line in options_string.splitlines():
- quote_flag = (quote_flag or item[0] == "\"")
+ # Set a flag for an item beginning with double quotes, and reset it for
+ # an item ending in double quotes
+ quote_flag = False
+ # Temporary list to hold such quoted arguments
+ quote_list = []
- if quote_flag:
- quote_list.append(item)
- else:
- return_list.append(item)
+ for item in line.split():
- if quote_flag and item[-1] == "\"":
+ quote_flag = (quote_flag or item[0] == "\"")
- # Special case mode is over
- return_list.append(" ".join(quote_list)[1:-1])
+ if quote_flag:
+ quote_list.append(item)
+ else:
+ return_list.append(item)
- quote_flag = False
- quote_list = []
+ if quote_flag and item[-1] == "\"":
+
+ # Special case mode is over
+ return_list.append(" ".join(quote_list)[1:-1])
+
+ quote_flag = False
+ quote_list = []
return return_list
@@ -1177,6 +1361,39 @@ def strip_whitespace(string):
return string
+def strip_whitespace_multiline(string):
+
+ """Can be called by anything.
+
+ An extended version of utils.strip_whitepspace.
+
+ Divides a string into lines, removes empty lines, removes any leading/
+ trailing whitespace from each line, then combines the lines back into a
+ single string (with lines separated by newline characters).
+
+ Args:
+
+ string (str): The string to convert
+
+ Returns:
+
+ The converted string
+
+ """
+
+ line_list = string.splitlines()
+ mod_list = []
+
+ for line in line_list:
+ line = re.sub(r'^\s+', '', line)
+ line = re.sub(r'\s+$', '', line)
+
+ if re.search('\S', line):
+ mod_list.append(line)
+
+ return "\n".join(mod_list)
+
+
def tidy_up_container_name(string, max_length):
"""Called by mainapp.TartubeApp.on_menu_add_channel(),