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. 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. 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(),