diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 37d6d86a..00000000 --- a/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# This file copied from -# https://python-packaging.readthedocs.io/en/latest/minimal.html?highlight=gitignore - -# Compiled python modules. -*.pyc - -# Setuptools distribution folder. -/dist/ - -# Python egg metadata, regenerated from source files by setuptools. -/*.egg-info diff --git a/CHANGES b/CHANGES index 3f2556af..73ee2515 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,68 @@ +v2.1.0 (7 May 2020) +------------------------------------------------------------------------------- + +MAJOR NEW FEATURES +- For everyone who wants a simpler way to download videos, a new Classic Mode + Tab has been added, emulating the look and feel of youtube-dl-gui. Videos + downloaded in this tab can be downloaded to any location, and are not added + to Tartube's database +- Tartube can now detect livestreams, and alert you when they start. This + feature is EXPERIMENTAL, has only been tested on YouTube, and may not be + reliable. It does not work at all on the 32-bit MS Windows installer +- Added internationalisation. If you can contribute a translation to this + project, please see the ../docs/translate.rst file. As a proof of concept, + Tartube can now be used with either British or American spelling +- You can now specify unlimited numbers of video/audio formats (the limit was + previously three). You can also specify that youtube-dl should try to + download each video in each of your preferred formats, rather than in one + or all available formats (as was the case before) + +MINOR NEW FEATURES +- Made minor improvements to the look of various dialogue windows +- Slightly improved the functionality of buttons in the system preference + window's database tab +- If a database can't be loaded (but an alternative database can), an + explanatory messages is now added to the Errors/Warnings Tab. If an + alternative database can't be loaded (or only one database has been added + to Tartube's list), then the dialogue window seen by the user is now + slightly more helpful +- Some startup errors, which were invisible to users not running Tartube from a + terminal window, now produce a visible dialogue window +- The code to force renamig of channels/playlists/folders (when required) has + been adjusted so it creates a name like 'Folder_3' rather than + 'Folder_2_2_2_2' +- Instructions for Tartube installations have been updated generally. The + MS Windows batch/shell files used to start Tartube have been tweaked + +MAJOR FIXES +- The MS Windows installers have been updated to use Python 3.8. This may fix + some stability issues for a few users +- For systems with a broken Gtk library (or if the user has disabled minor + cosmetic features anyway), the list of videos in the Videos Tab is no + longer updated during a download operation. This should resolve some + lingering stability issues. (You can manually update the list by selecting + a different channel/playlist/folder, then selecting the original one again) +- In the Video Catalogue, new videos are sometimes added to the last page, + rather than to the first one. Rewrote the code yet again to resolve this + issue; hopefully this should be the last rewrite +- The Python setup file now explicitly mentions more dependencies; this should + assist with installation issues for PyPI, DEB and RPM packages +- The RPM package, which did not work at all on Tartube v2.0.016, is now + working again + +MINOR FIXES +- In the general download options window, in the Files tab, there was a + duplicate set of template options. Remove the duplicate set, replacing it + with a larger set of options +- Info/Tidy operations complained that they couldn't start while an edit/ + preference window is open, and then started anyway. Fixed +- In the Progress List, a video's name is updated as soon as it is known + (before, the name was only updated in the Results List) +- The start of a download operation is now (slightly) quicker, because the + setup code is no longer called (incorrectly) for every single video +- Download operations scheduled to begin when Tartube starts now begin after + a few seconds, rather than immediately (for aesthetic reasons) + v2.0.016 (10 Apr 2020) ------------------------------------------------------------------------------- diff --git a/README.rst b/README.rst index 2b656960..9265b682 100644 --- a/README.rst +++ b/README.rst @@ -5,8 +5,6 @@ Tartube - The Easy Way To Watch And Download Videos Works with YouTube, BitChute, and hundreds of other websites ------------------------------------------------------------ -Attention `Linux Format `__ readers! There are easier ways to install **Tartube**. Go to the main `downloads page `__ or see below. - .. image:: screenshots/tartube.png :alt: Tartube screenshot @@ -35,23 +33,31 @@ Problems can be reported at `our GitHub page `__ for a full list) - You can fetch information about those videos, channels and playlists, without actually downloading anything -- **Tartube** will organise your videos into convenient folders +- **Tartube** will organise your videos into convenient folders (if that's what you want) +- **Tartube** can alert you when livestreams are starting (**YouTube** only) - If creators upload their videos to more than one website (**YouTube** and **BitChute**, for example), you can download videos from both sites without creating duplicates - Certain popular websites manipulate search results, repeatedly 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 - **Tartube** is free and open-source software +2.1 What's new in version 2.1.0 +------------------------------- + +- For everyone who wants a simpler way to download videos, a new Classic Mode, emulating the look and feel of `youtube-dl-gui `__ - 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 + 3 Downloads =========== -Latest version: **v2.0.016 (10 Apr 2020)** +Latest version: **v2.1.0 (7 May 2020)** -- `MS Windows (32-bit) installer `__ from Sourceforge -- `MS Windows (64-bit) installer `__ 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 (32-bit) installer `__ from Sourceforge +- `MS Windows (64-bit) installer `__ 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 - `Gentoo ebuild (available in src_prepare-overlay) `__ from Gitlab -- `Source code `__ from Sourceforge +- `Source code `__ from Sourceforge - `Source code `__ and `support `__ from GitHub There are also 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. @@ -65,7 +71,10 @@ There are also DEB/RPM packages marked STRICT. In these packages, updates to **y - Download, install and run **Tartube**, using the links above - 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 **FFmeg**. 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: + - Go to the `YouTube website `__, and find your favourite channel - In **Tartube**, click the **Add a new channel** button (or from the menu, click **Media > Add channel...** ) - In the dialogue window, add the name of the channel and the address (URL) @@ -82,6 +91,9 @@ There are also DEB/RPM packages marked STRICT. In these packages, updates to **y - Run **Tartube** - When prompted, choose a directory where **Tartube** can store videos - Install **youtube-dl** by clicking **Operations > Update youtube-dl** + +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: + - Go to the `YouTube website `__, and find your favourite channel - In **Tartube**, click the **Add a new channel** button (or from the menu, click **Media > Add channel...** ) - In the dialogue window, add the name of the channel and the address (URL) @@ -96,7 +108,7 @@ There are also DEB/RPM packages marked STRICT. In these packages, updates to **y 5.1 Installation - MS Windows ----------------------------- -MS Windows users should use the installer `available at the **Tartube** website `__. The installer contains everything you need to run **Tartube**. You must be using Windows Vista or above; the installer will not work on Windows XP. +MS Windows users should use the installer `available at the Tartube website `__. The installer contains everything you need to run **Tartube**. You must be using Windows Vista or above; the installer will not work on Windows XP. If you want to use **FFmpeg**, see `6.4 Setting the location of FFmpeg / AVConv`_. @@ -105,7 +117,7 @@ From v1.4, the installer includes a copy of `AtomicParsley `__. You need the file that looks something like **msys2-x86_64-yyyymmdd.exe** @@ -130,15 +142,19 @@ Some users report that **Tartube** will install but won't run. This problem shou **pacman -S mingw-w64-x86_64-gtk3** - **pacman -S mingw-w64-x86_64-gsettings-desktop-schemas** + **pacman -S mingw-w64-x86_64-gsettings-desktop-schemas** + + **pip3 install feedparser** + + **pip3 install playsound** - Download the **Tartube** source code from Sourceforge, using the links above - Extract it into the folder **C:\\msys64\\home\\YOURNAME**, creating a folder called **C:\\msys64\\home\\YOURNAME\\tartube** -- Now, to run **Tartube**, type these commands in the MINGW64 terminal: +- Now, to run **Tartube**, type these commands in the MINGW64 terminal (don't forget to use *forward* slashes): - **cd tartube** + **cd /home/YOURNAME/tartube** - **python3 tartube** + **python3 tartube/tartube** 5.2 Installation - MacOS ------------------------ @@ -213,7 +229,9 @@ For any other method of installation, the following dependencies are required: These dependencies are optional, but recommended: - `Python pip `__ - keeping youtube-dl up to date is much simpler when pip is installed +- `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 - `AtomicParsley `__ - required for embedding thumbnails in audio files @@ -255,7 +273,7 @@ After installing dependencies (see above): * `6.12 Other download options`_ * `6.13 Custom downloads`_ * `6.13.1 Independent downloads`_ -* `6.13.2 Diverting to HookTube/Invidious`_ +* `6.13.2 Diverting to HookTube / Invidious`_ * `6.13.3 Delays between downloads`_ * `6.14 Watching videos`_ * `6.15 Filtering and finding videos`_ @@ -273,6 +291,12 @@ After installing dependencies (see above): * `6.19.3 Multiple Tartubes`_ * `6.19.4 Exporting/importing the database`_ * `6.20 Converting to audio`_ +* `6.21 Classic Mode`_ +* `6.22 Livestreams`_ +* `6.22.1 Detecting livestreams`_ +* `6.22.2 Customising livestreams`_ +* `6.22.3 Livestream notifications`_ +* `6.22.4 Compatible websites`_ 6.1 Choose where to save videos ------------------------------- @@ -357,6 +381,7 @@ When you start **Tartube**, there are seven folders already visible. You can't r - The **All Videos** folder shows every video in **Tartube**'s database, whether it has been downloaded or not - The **Bookmarks** folder shows videos you've bookmarked, because they're interesting or important (see `6.16.1 Bookmarked videos`_ ) - The **Favourite Videos** folder shows videos in a channel, playlist or folder that you've marked as a favourite (see `6.16.2 Favourite channels, playlists and folders`_ ) +- The **Livestreams** folder shows livestreams. Videos are automatically removed from this folder (but not from other folders) when the livestream is finished - The **New Videos** folder shows videos that have been downloaded, but not yet watched - The **Waiting Videos** folder shows videos that you want to watch soon. When you watch the video, it's automatically removed from the folder (but not from **Tartube**'s database) - Videos saved to the **Temporary Videos** folder will be deleted when **Tartube** next starts @@ -365,6 +390,8 @@ When you start **Tartube**, there are seven folders already visible. You can't r 6.6 Adding videos ----------------- +*If you want a simpler way to download videos, see* `6.21 Classic Mode`_. + You can add individual videos by clicking the **'Videos'** button near the top of the window. A dialogue window will appear. .. image:: screenshots/example7.png @@ -485,12 +512,12 @@ In the new window, click the **'OK'** button. The options are applied to *everyt .. image:: screenshots/example15.png :alt: Download options applied to the Music folder -Now, suppose you want to add a *different* set of download options, but only for the **Village People** channel. +Now, suppose you want to add a *different* set of download options, but only for the channel **The Beatles**. - Right-click the channel, and select **Apply download options...** - In the new window, click the **'OK'** button -The previous set of download options still applies to everything in the **Music** folder, *except* the **Village People** channel. +The previous set of download options still applies to everything in the **Music** folder, *except* the channel **The Beatles**. .. image:: screenshots/example16.png :alt: Download options applied to the Village People channel @@ -498,7 +525,7 @@ The previous set of download options still applies to everything in the **Music* 6.13 Custom downloads --------------------- -By default, **Tartube** downloads videos as quickly as possible using each video's original address (URL). +By default, **Tartube** downloads videos as quickly as possible using each video's original web address (URL). A **Custom download** enables you to modify this behaviour, if desired. It's important to note that a custom download behaves exactly like a regular download until you specify the new behaviour. @@ -518,8 +545,8 @@ If you need to download videos directly, for any reason, you can: - Click **In custom downloads, download each video independently of its channel or playlist** to select it - You can now start the custom download -6.13.2 Diverting to HookTube/Invidious -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +6.13.2 Diverting to HookTube / Invidious +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If **Tartube** can't download a video from YouTube, it's sometimes possible to obtain it from an alternative website instead. @@ -551,7 +578,7 @@ If you've downloaded a video, you can watch it by clicking the word **Player**. .. image:: screenshots/example17.png :alt: Watching a video -If you haven't downloaded the video yet, you can watch it online by clicking the word **YouTube** or **Website**. (One or the other will be visible). +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 the **HookTube** and/or **Invidious** websites. @@ -794,29 +821,170 @@ N.B. Many video websites, such as **YouTube**, allow you to download the audio ( - Click the **Add format >>>** button to add it to the list - Click the **OK** button at the bottom of the window to apply your changes +6.21 Classic Mode +----------------- + +**Tartube** compiles a database of the videos, channels and playlists it has downloaded. + +If you want something simpler, then you can click the **Classic Mode** Tab for an interface that looks just like `youtube-dl-gui `__. + +.. image:: screenshots/example20.png + :alt: The Classic Mode Tab + +- Copy and paste the URLs of videos, channels and/or playlists into the box at the top +- Click the **...** button to select a directory (folder). All the videos are downloaded into this directory +- Select a video or audio format, or leave the **Default** setting enabled +- Click the **Add URLs** button +- If you like, you can add more videos/channels/playlists, using a different directory and/or a different format +- When you're ready, click the **Download all** button + +**Tartube** doesn't add any of these videos to its database. When you restart **Tartube**, all of the URLs will be gone. However, the videos themselves will still be on your hard drive. + +Because the videos aren't in a database, you can move them anywhere you want (once you've finished downloading them). + +**PROTIP:** If you *only* use this tab, you can tell **Tartube** to open it automatically. Click **Edit > System preferences... > Windows > Main Window** and select **When Tartube starts, automatically open the Classic Mode Tab**. + +6.22 Livestreams +---------------- + +Since v2.1.0, **Tartube** has been able to detect livestreams, and to notify you when they start. + +This feature is EXPERIMENTAL, has only been tested on **YouTube**, and may not work as intended. + +Livestream detection does not work at all on 32-bit MS Windows. + +6.22.1 Detecting livestreams +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Tartube** searches for livestreams whenever you check or download channels and playlists. + +Livestreams are easy to spot. A livestream that hasn't started yet has a red background. A livestream that's streaming now has a green background. (Livestreams that have stopped broadcasting have a normal background.) + +.. image:: screenshots/example21.png + :alt: The main window with livestreams visible + +Every few minutes, **Tartube** checks whether a livestream has started or stopped. This happens automatically in the background; there is no need for you to do anything. + +6.22.2 Customising livestreams +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can modify how often livestreams are checked (and whether they are checked at all). Click **Livestreams > Livestream preferences...**. + +.. image:: screenshots/example22.png + :alt: Livestream preferences + +For technical reasons, there are practical limits to what **Tartube** can detect. On busy channels, **Tartube** may not be able to detect livestreams that were announced some time ago. Even if you change the number of days from 7 to a very large number, there is no guarantee that **Tartube** will detect everything. (If you change the value to 0, **Tartube** will only detect livestreams that are listed before any ordinary videos.) + +By default, **Tartube** checks a livestream every three minutes, waiting for it to start (or stop). Decreasing this period might not be a good idea; it's possible that the website will think you are spamming. + +If you keep missing the start of your favourite livetreams, pester the creators until they add a short countdown. If you want to force a check, in the main window click **Livestreams > Update existing livestreams**. + +A **Tartube** installation includes a number of sound effects. You can choose the one you want to use as an alarm. If you want to add your own sound effects, find the directory (folder) where Tartube is installed, copy the new **.mp3** or **.wav** files into **../sounds**, and restart **Tartube.** + +6.22.3 Livestream notifications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tartube can notify you when a livestream starts. (**Desktop notifications** do not work on MS Windows yet.) + +The preferences window shows the actions **Tartube** takes by default. These preference are applied to a livestream as soon as it is detected. + +Most users will prefer to leave the checkboxes unselected, and instead set up notifications only for the livestreams they want to see. + +.. image:: screenshots/example23.png + :alt: Some example livestreams + +- Click **Notify** to show a desktop notification when the stream starts (does not work on MS Windows) +- Click **Alarm** to sound an alarm when the stream starts +- Click **Open** to open the stream in your web browser as soon as it starts +- If you think the stream might be removed from the website, you can click **D/L on start** or **D/L on stop**. If you click both of them, **Tartube** will download the video twice. (Think of the first one as a backup, in case the second download doesn't succeed.) + +To disable any of these actions, simply click the same label again. + +**NOTE:** At the time of writing (April 2020), youtube-dl cannot download livestreams while they are broadcasting. Hopefully this is a **youtube-dl** issue that will be fixed in due course. + +6.22.4 Compatible websites +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Tartube**'s livestream detection has only been tested on **YouTube**. It's possible that it might work on other websites, if they behave in the same way. Here is how to set it up. + +Firstly, find the RSS feed for the channel or playlist. You may have to use a search engine to find out how to do that. (For **YouTube** channels/playlists, **Tartube** finds the feed for you automatically.) + +Secondly, right-click the channel and select **Show > Channel properties...** (alternatively, right-click a playlist and select **Show > Playlist properties...** + +Now click the **RSS feed** tab. Enter the address (URL) of the RSS feed in the box. Click the **OK** button to close the window. + 7 Frequently-Asked Questions ============================ -**Q: I can't install Tartube / I can't run Tartube / Tartube doesn't work properly / Tartube keeps crashing!** +* `7.1 Tartube won't install/won't run/doesn't work`_ +* `7.2 Tartube crashes a lot`_ +* `7.3 "Download did not start" error`_ +* `7.4 Can't download a video`_ +* `7.5 Downloads never finish`_ +* `7.6 Videos are missing after a crash`_ +* `7.7 'Check all' button takes too long`_ +* `7.8 'Download all' button takes too long`_ +* `7.9 Videos downloaded to inconvenient location`_ +* `7.10 Tartube database file is getting in the way`_ +* `7.11 Duplicate video names`_ +* `7.12 Convert video to audio`_ +* `7.13 Too many folders in the main window`_ +* `7.14 Not enough videos in the main window`_ +* `7.15 Toolbar is too small`_ +* `7.16 YouTube name/password not accepted`_ +* `7.17 Georestriction workarounds don't work`_ +* `7.18 MS Windows installer is too big`_ +* `7.19 Tartube can't detect livestreams`_ +* `7.20 Livestream start time not visible`_ +* `7.21 Livestream is already finished`_ +* `7.22 Can't hear livestream alarms`_ +* `7.23 British spelling`_ +* `7.24 No puedo hablar inglés`_ + +7.1 Tartube won't install/won't run/doesn't work +------------------------------------------------ + +*Q: I can't install Tartube / I can't run Tartube / Tartube doesn't work properly!* + +A: Please report any problems to the authors at our `Github page `__. + +A: Tartube is known to fail on Windows 7 systems that have not been updated for some time. The solution is to install `this patch from Microsoft `__. The simplest way to install the patch is to let Windows update itself, as normal. + +A: On Linux, if the DEB or RPM package doesn't work, try installing via PyPI. + +7.2 Tartube crashes a lot +------------------------- + +*Q: I can install and run Tartube, but it keeps crashing!* + +A: Tartube uses the Gtk graphics library. This library is notoriously unreliable and may even cause crashes. -A: Please report any problems to the authors at our `Github page `__ +If stability is a problem, you can disable some minor cosmetic features. **Tartube**'s functionality is not affected. You can do anything, even when the cosmetic features are disabled. -A: Crashes are usually caused by the Gtk graphics library. Depending on the version of the library installed on your system, **Tartube** may restrict some minor cosmetic features, or not, in an effort to avoid such crashes. +- Click **Edit > System preferences... > General > Stability** +- Click **Assume that Gtk is broken, and disable those features anyway** to select it -If crashes are a problem, you can force **Tartube** to restrict those cosmetic features, regardless of your current Gtk library. +Another option is to reduce the number of simultaneous downloads. (On crash-prone systems, two simultaneous downloads seems to be safe, but four is rather less safe.) -- Click **Edit > System preferences... > General > Modules** -- Click **Assume that Gtk is broken, and disable some minor features** to select it +- In the main window, click the **Progress** Tab +- At the bottom of the tab, click the **Max downloads** checkbutton to select it, and reduce the number of simultaneous downloads to 1 or 2 +- (It's not necessary to reduce the download speed; this has no effect on stability) -**Q: When I try to download videos, nothing happens! In the Errors/Warnings tab, I can see "Download did not start"!** +7.3 "Download did not start" error +---------------------------------- + +*Q: When I try to download videos, nothing happens! In the Errors/Warnings tab, I can see "Download did not start"!* A: See `6.3 Setting youtube-dl's location`_ -**Q: I can't download my favourite video!** +7.4 Can't download a video +-------------------------- + +*Q: I can't download my favourite video!* A: Make sure **youtube-dl** is updated; see `6.2 Check youtube-dl is updated`_ -A: Before submitting a `bug report `__, find out whether **Tartube** is responsible for the problem, or not. You can do this by opening a terminal window, and typing something like this: +Before submitting a `bug report `__, find out whether **Tartube** is responsible for the problem, or not. You can do this by opening a terminal window, and typing something like this: **youtube-dl ** @@ -831,7 +999,22 @@ Because most people don't like typing, **Tartube** offers a shortcut. - Click the **Output** Tab to watch the test as it progresses - When the test is finished, a temporary directory (folder) opens, containing anything that **youtube-dl** was able to download -**Q: After I downloaded some videos, Tartube crashed, and now all my videos are missing!** +7.5 Downloads never finish +-------------------------- + +*Q: I clicked the 'Download all' button and it starts, but never finishes!* + +A: This generally indicates an error in the Python, Gtk and/or **Tartube** code. If you're running **Tartube** from a terminal window, you should be able to see the error, which you can report on `our GitHub page `__. + +There are two things you can try in the meantime: + +- Click **Edit > System preferences... > General > Modules**, and select the **Assume that Gtk is broken, and disable some features** box +- Click **Edit > System preferences... > Filesystem > DB Errors**, and then click the **Check** button + +7.6 Videos are missing after a crash +------------------------------------ + +*Q: After I downloaded some videos, Tartube crashed, and now all my videos are missing!* A: **Tartube** creates a backup copy of its database, before trying to save a new copy. In the unlikely event of a failure, you can replace the broken database file with the backup file. @@ -849,7 +1032,10 @@ A: **Tartube** creates a backup copy of its database, before trying to save a ne Note that **Tartube** does not create backup copies of the videos you've downloaded. That is your responsibility! -**Q: I clicked the 'Check all' button, but the operation takes so long! It only found two new videos!** +7.7 'Check all' button takes too long +------------------------------------- + +*Q: I clicked the 'Check all' button, but the operation takes so long! It only found two new videos!* A: By default, the underlying **youtube-dl** software checks an entire channel, even if it contains hundreds of videos. @@ -857,19 +1043,25 @@ You can drastically reduce the time this takes by telling **Tartube** to stop ch This works well on sites like YouTube, which send information about videos in the order they were uploaded, newest first. We can't guarantee it will work on every site. -- Click **Edit > System preferences... > Operations > Time-saving** +- Click **Edit > System preferences... > Operations > Performance** - Select the checkbox **Stop checking/downloading a channel/playlist when it starts sending vidoes we already have** - In the **Stop after this many videos (when checking)** box, enter the value 3 - In the **Stop after this many videos (when downloading)** box, enter the value 3 - Click **OK** to close the window -**Q: I clicked the 'Download all' button, but the operation takes so long! It only downloaded two new videos!** +7.8 'Download all' button takes too long +---------------------------------------- + +*Q: I clicked the 'Download all' button, but the operation takes so long! It only downloaded two new videos!* A: **youtube-dl** can create an archive file especially for the purpose of speeding up downloads, when some of your channels and playlists have no new videos to download, but when others do. To enable this functionality, click **Edit > System preferences... > youtube-dl > Allow youtube-dl to create its own archive**. The functionality is enabled by default. -**Q: Tartube always downloads its channels and playlists into ../tartube-data/downloads. Why doesn't it just download directly into ../tartube-data?** +7.9 Videos downloaded to inconvenient location +---------------------------------------------- + +*Q: Tartube always downloads its channels and playlists into ../tartube-data/downloads. Why doesn't it just download directly into ../tartube-data?* A: This was implemented in v1.4.0. If you installed an earlier version of **Tartube**, you don't need to take any action; **Tartube** can cope with both the old and new file structures. @@ -882,7 +1074,10 @@ If you installed an earlier version of **Tartube**, and if you want to move your - Delete the empty **downloads** directory - You can now restart **Tartube** -**Q: Tartube stores its database file in the same place as its videos. Why can't I store them in different places?** +7.10 Tartube database file is getting in the way +------------------------------------------------ + +*Q: Tartube stores its database file in the same place as its videos. Why can't I store them in different places?* A: This question has been asked by several people who were storing their videos on some remote filesystem (perhaps in the so-called 'cloud'). They found that the videos could be downloaded to that remote location, but that Tartube couldn't save its database file there. @@ -894,11 +1089,28 @@ At the moment, the answer is "**Tartube** is working fine, fix your own computer - If you want to move your videos from one location to another, it's easy - just move a single directory (folder) and everything it contains. There is no need to reconfigure anything; just tell **Tartube** where to find the new directory (folder) - Splitting up the data folder and the database file would require a lot of code to be rewritten, and this would probably introduce lots of new bugs -**Q: I want to convert the video files to audio files!** +7.11 Duplicate video names +-------------------------- + +*Q: I downloaded a channel, but some of the videos in the channel have the same name. Tartube only downloads one of them!* + +A: Tartube can save the video files using a multitude of different filename formats. Video names might be identical, but the video IDs are unique, so you can add the ID to the filename. + +- Click **Edit > General download options... > Files > File names** +- In the box **Format for video file names**, select **Title + ID** +- Click **OK** to close the window + +7.12 Convert video to audio +--------------------------- + +*Q: I want to convert the video files to audio files!* A: See `6.20 Converting to audio`_ -**Q: The main window is full of folders I never use! I can't see my own channels, playlists and folders!** +7.13 Too many folders in the main window +---------------------------------------- + +*Q: The main window is full of folders I never use! I can't see my own channels, playlists and folders!* A: Right-click the folders you don't want to see, and select **Folder actions > Hide folder**. To reverse this step, in the main menu click **Media > Show hidden folders** @@ -906,17 +1118,26 @@ A: In the main menu, click **Edit > System preferences... > Windows > Main windo A: If you have many channels and playlists, create a folder, and then drag-and-drop the channels/playlists into it -**Q: I want to see all the videos on a single page, not spread over several pages!** +7.14 Not enough videos in the main window +----------------------------------------- + +*Q: I want to see all the videos on a single page, not spread over several pages!* A: At the bottom of the **Tartube** window, set the page size to zero, and press **ENTER**. -**Q: The toolbar is too small! There isn't enough room for all the buttons!** +7.15 Toolbar is too small +------------------------- + +*Q: The toolbar is too small! There isn't enough room for all the buttons!* A: Click **Edit > System preferences... > Windows > Main window > Don't show labels in the toolbar**. MS Windows users can already see a toolbar without labels. -**Q: I added my YouTube username and password, but I am still seeing authentification errors!** +7.16 YouTube name/password not accepted +--------------------------------------- + +*Q: I added my YouTube username and password, but I am still seeing authentification errors!* A: The questioner is talking about the settings in **Edit > General download options... > Advanced**. @@ -930,7 +1151,21 @@ Having created the file, in the same edit window, click the **General** tab. In See also the **Tartube** thread `here `__. -**Q: Why is the Windows installer so big?** +7.17 Georestriction workarounds don't work +------------------------------------------ + +*Q: I want to download a video, but it's blocked in my region. I set the geostriction workarounds, but I still can't download the video!* + +A: **youtube-dl** provides some options for bypassing region-blocking. These options are visible by clicking **Edit > General download options...**, then click the **Show advanced download options** button if it's visible, then click the tabs **Advanced > Geo-restriction**. + +Unfortunately, although these options exist, websites are not compelled to respect them. **YouTube**, in particular, will completely ignore them. + +In many cases, the only remedy is to pay for a subscription to a `VPN `__. + +7.18 MS Windows installer is too big +------------------------------------ + +*Q: Why is the Windows installer so big?* A: **Tartube** is a Linux application. The installer for MS Windows contains not just **Tartube** itself, but a copy of Python and a whole bunch of essential graphics libraries, all of them ported to MS Windows. @@ -948,6 +1183,64 @@ The NSIS scripts used to create the installers can be found here: The scripts contain full instructions, so you should be able to create your own installer, and compare it with the official one. +7.19 Tartube can't detect livestreams +------------------------------------- + +*Q: Tartube can't detect upcoming livestreams at all!* + +A: Livestream detection is experimental, has only been tested on **YouTube**, and may not be reliable. It does not work at all on 32-bit MS Windows. See `6.22 Livestreams`_. + +A: Click **Edit > System preferences... General > Modules**. + +If the `Python feedparser module `__ is not available, you can install it via PyPI. On Linux/BSD, the command to use is something like: + +**pip3 install feedparser** + +The Tartube installer for 64-bit MS Windows already contains a copy of **feedparser**, so there is no need to install it again. + +7.20 Livestream start time not visible +-------------------------------------- + +*Q: Why doesn't **Tartube** show the start time for livestreams?* + +A: Popular video websites like **YouTube** do not provide that information. + +7.21 Livestream is already finished +----------------------------------- + +*Q: Tartube is showing a livestream that finished hours/days/centuries ago!* + +A: Right-click the video and select **Livestream > Not a livestream**. + +7.22 Can't hear livestream alarms +--------------------------------- + +*Q: I set an alarm for an upcoming livestream, but I didn't hear anything!* + +A: Obviously you have already checked that your speakers are turned on, so now click **Edit > System preferences... General > Modules**. + +If the `Python playsound module `__ is not available, you can install it via PyPI. On Linux/BSD, the command to use is something like: + +**pip3 install playsound** + +The Tartube installer for 64-bit MS Windows already contains a copy of **playsound**, so there is no need to install it again. + +7.23 British spelling +--------------------- + +*Q: These British spellings are getting on my nerves!* + +A: Click **Edit > System preferences...**. Click the drop-down box and select American English, and then restart **Tartube** + +7.24 No puedo hablar inglés +--------------------------- + +*Q: ¡No puedo usar YouTube porque no hablo inglés!* + +A: Necesitamos más traductores. + +If you would like to contribute a translation of this project, please read `this document `__. + 8 Contributing ============== diff --git a/docs/empty.md b/docs/empty.md deleted file mode 100644 index a3e0248d..00000000 --- a/docs/empty.md +++ /dev/null @@ -1 +0,0 @@ -#Tartube \ No newline at end of file diff --git a/docs/translate.rst b/docs/translate.rst new file mode 100644 index 00000000..94e8950d --- /dev/null +++ b/docs/translate.rst @@ -0,0 +1,203 @@ +==================== +Tartube translations +==================== + +You want to contribute a translation to this project? Well, that's just great! + +The simple way +-------------- + +1. Get a copy of the file `../tartube/po/messages.pot `__ +2. Open it in a text editor +3. Read the notes below +4. Translate everything +5. Send the modified file to the authors, via `our GitHub page `__, and we'll take care of the rest + +The technical way +----------------- + +1. Fork the original `GitHub archive `__ +2. Clone the fork onto your system +3. Create a new directory/folder using the correct locale, in the form _, e.g. **../tartube/locale/en_GB**, **../tartube/locale/es_ES** +4. Copy the file `../tartube/po/messages.pot `__ into that directory, and rename it as **base.po** +5. Open the copy in a text editor +6. Read the notes below +7. Translate everything +8. Push the modified code back to your fork +9. Submit a pull request `here `__. + +Notes +----- + +Header +====== + +The lines at the top must be changed from this:: + + # SOME DESCRIPTIVE TITLE. + # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER + # This file is distributed under the same license as the PACKAGE package. + +...to this:: + + # Tartube + # Copyright (C) 2019-2020 A S Lewis + # This file is distributed under the same license as the Tartube package. + +The Project-Id-Version must be changed from this:: + + "Project-Id-Version: PACKAGE VERSION\n" + +...to the current Tartube version, for example:: + + "Project-Id-Version: 2.1\n" + +The Language-Team field must be changed from this:: + + "Language-Team: LANGUAGE \n" + +...to this, again using the _ format, for example: + + "Language-Team: es_ES \n" + +The Content-Type field must be changed from this:: + + "Content-Type: text/plain; charset=CHARSET\n" + +...to this:: + + "Content-Type: text/plain; charset=UTF-8\n" + +Credits +======= + +Change both of the following lines to claim credit. If you don't want to add an email address, then don't:: + + # FIRST AUTHOR , YEAR. + "Last-Translator: FULL NAME \n" + +For example:: + + # FIRST AUTHOR Victor Hugo , 2020. + "Last-Translator: Victor Hugo \n" + +If you use an email address, it could also be added to the Language-Team field. + +Translations +============ + +The rest of the document consists of pairs of strings. (A string is a single piece of text, enclosed within double quotes.) + +The string labelled **msgid** is the original English text. The string labelled **msgstr** is an empty string that contains the translation. + +For example, change this:: + + msgid "Channel" + msgstr "" + +...to this:: + + msgid "Channel" + msgstr "Canal" + +If you don't add a translation, then the original English is used. (This can be useful for words which are the same in both languages.) + +Some pieces of text are spread across several lines, like this:: + + msgid "" + "The video file is missing from Tartube's data folder (try downloading the " + "video again!)" + msgstr "" + +The two strings are added to each other, producing a single string. You can do the same, if you want. (It doesn't matter how many strings you use). + +Multiple strings are combined without extra space characters. You should add them yourself, as in the example below:: + + msgid "" + "The video file is missing from Tartube's data folder (try downloading the " + "video again!)" + msgstr "" + "¡No puedo usar " + "YouTube " + "porque no hablo inglés!" + +Please preserve capitalisation and punctuation:: + + msgid "Help!" + msgstr "¡Ayuda!" + + msgid "HELP!" + msgstr "¡AYUDA!" + + msgid "help!" + msgstr "¡ayuda!" + +One exception to this rule is underline/underscore characters. These denote keyboard shortcuts. Don't add the underline/underscore character to your translation:: + + msgid "_Channel" + msgstr "Canal" + +Comments +======== + +Sometimes the programme that generates the **messages.pot** file adds extra comments. You can ignore any line that starts with a # character. These lines were generated by a computer, not by a human. + +Clarifications +============== + +We've added a few clarifications to help you, for example this one:: + + msgid "TRANSLATOR'S NOTE: Ext is short for a file extension, e.g. .EXE" + msgstr "" + +You don't need to translate the clarification. Nothing uses it and no-one will see it. + +If you're not sure how something should be translated, let's discuss it on `our GitHub page `__. + +Substitutions +============= + +Some strings contain {0}, {1}, {2} and so on. These are substituted for something else. + + msgid "Give the {0} to the {1}, please" + +Your translation must include the literal {0}, {1}, {2} and so on. + + msgstr "blah blah blah {0} blah blah {1} blah blah" + +If your translation uses a different word order, then treat the substrings like a word. + + msgstr "Give to the {1} the {0}, please" + +Directories/folders +=================== + +Earlier version of Tartube used *directory* on Linux systems, and *folder* on MS Windows. To make translations simpler, we have removed this distinction. Everything is not a *folder*. + +Downloads +========= + +You have probably noticed two buttons in Tartube's main window: **Check all** and **Download all**. + +The first one fetches a list of videos from websites, but doesn't download the videos. The second one fetches a list of videos from websites AND downloads the videos. + +Throughout **messages.pot**, the word *check* is used with this meaning. You can decided for yourself how to translate it. + +Operations +========== + +Throughout **messages.pot**, the word *operation* has a fixed meaning. When Tartube is busy doing something, many buttons don't work (are greyed out). + +For example, click the **Download all** button, and it is greyed out until the downloads are finished. + +There are five operations. You can decide for yourself, the best way to translate them. + +**Download operation**: downloads videos, or just fetches a list of videos. The **Check all** and **Download all** buttons both start a **download operation** + +**Update operation**: installs FFmpeg, or installs youtube-dl, or updates youtube-dl to the latest version + +**Refresh operation**: searches a directory/folder on the user's filesystem. If videos are found, those videos are added to Tartube's database + +**Info operation**: fetches a list of the available video/audio formats for a video, or fetches a list of available subtitles for a video. Also used to test youtube-dl + +**Tidy operation**: tidies up the directory/folder where Tartube stores its videos. Checks that fils are not missing, not corrupted, and so on diff --git a/icons/COPYING b/icons/COPYING index ca9d99be..98595806 100644 --- a/icons/COPYING +++ b/icons/COPYING @@ -29,11 +29,12 @@ Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. All files in the ../locale directory -Author: Mr. Hopnguyen -Source: https://www.iconfinder.com/icons/2634450/ +These files are in the public domain. -This work is licensed under the Creative Commons Attribution 3.0 Unported -License. To view a copy of this license, visit -http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative -Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. +All files in the ../external directory + +Author: MrS0m30n3 +Source: https://github.com/MrS0m30n3/youtube-dl-gui/blob/master/youtube_dl_gui +/data/icons/hicolor/32x32/apps/youtube-dl-gui.png +This is free and unencumbered software released into the public domain. diff --git a/icons/external/youtube-dl-gui.png b/icons/external/youtube-dl-gui.png new file mode 100644 index 00000000..629f924c Binary files /dev/null and b/icons/external/youtube-dl-gui.png differ diff --git a/icons/locale/flag_en_GB.png b/icons/locale/flag_en_GB.png new file mode 100644 index 00000000..829dd2e7 Binary files /dev/null and b/icons/locale/flag_en_GB.png differ diff --git a/icons/locale/flag_en_US.png b/icons/locale/flag_en_US.png new file mode 100644 index 00000000..760e4d59 Binary files /dev/null and b/icons/locale/flag_en_US.png differ diff --git a/icons/locale/flag_uk.png b/icons/locale/flag_uk.png deleted file mode 100644 index 49d82e3a..00000000 Binary files a/icons/locale/flag_uk.png and /dev/null differ diff --git a/icons/small/stream_live.png b/icons/small/stream_live.png new file mode 100644 index 00000000..9bb42d13 Binary files /dev/null and b/icons/small/stream_live.png differ diff --git a/icons/small/stream_wait.png b/icons/small/stream_wait.png new file mode 100644 index 00000000..f0846716 Binary files /dev/null and b/icons/small/stream_wait.png differ diff --git a/locale/en_US/LC_MESSAGES/base.mo b/locale/en_US/LC_MESSAGES/base.mo new file mode 100644 index 00000000..f07da2f3 Binary files /dev/null and b/locale/en_US/LC_MESSAGES/base.mo differ diff --git a/locale/en_US/LC_MESSAGES/base.po b/locale/en_US/LC_MESSAGES/base.po new file mode 100644 index 00000000..fc4159ca --- /dev/null +++ b/locale/en_US/LC_MESSAGES/base.po @@ -0,0 +1,5493 @@ +# Tartube +# Copyright (C) 2019-2020 A S Lewis +# This file is distributed under the same license as the Tartube package. +# FIRST AUTHOR A S Lewis , 2020. +msgid "" +msgstr "" +"Project-Id-Version: 2.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-07 07:01+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: A S Lewis \n" +"Language-Team: en_US\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" + +#: .././mainapp.py:2225 +msgid "" +"Tartube can't create the folder in which its configuration file is saved" +msgstr "" + +#: .././mainapp.py:2267 +msgid "The user declined to specify a data folder for Tartube" +msgstr "" + +#: .././mainapp.py:2456 +#, python-brace-format +msgid "" +"Gtk v{0}.{1}.{2} is broken, which may cause problems when running Tartube. " +"If possible, please update it to at least Gtk v3.24" +msgstr "" + +#: .././mainapp.py:2470 +#, python-brace-format +msgid "" +"Tartube is assuming that Gtk v{0}.{1}.{2} is broken; some minor cosmetic " +"features are disabled" +msgstr "" + +#: .././mainapp.py:2510 +msgid "The Tartube database file was not loaded, but is no longer protected" +msgstr "" + +#: .././mainapp.py:2513 +msgid "Restart Tartube to load it" +msgstr "" + +#: .././mainapp.py:2522 +msgid "Because of an error, file load/save has been disabled" +msgstr "" + +#: .././mainapp.py:2532 +msgid "Because of the error, file load/save has been disabled" +msgstr "" + +#: .././mainapp.py:2563 +msgid "" +"youtube-dl must be installed before you can use Tartube. Do you want to " +"install youtube-dl now?" +msgstr "" + +#: .././mainapp.py:2618 +msgid "There is a download operation in progress." +msgstr "" + +#: .././mainapp.py:2620 +msgid "There is an update operation in progress." +msgstr "" + +#: .././mainapp.py:2622 +msgid "There is a refresh operation in progress." +msgstr "" + +#: .././mainapp.py:2624 +msgid "There is an info operation in progress." +msgstr "" + +#: .././mainapp.py:2626 +msgid "There is a tidy operation in progress." +msgstr "" + +#: .././mainapp.py:2631 +msgid "Are you sure you want to quit Tartube?" +msgstr "" + +#: .././mainapp.py:2828 +msgid "Failed to load the Tartube config file (failed sanity check)" +msgstr "" + +#: .././mainapp.py:2850 +msgid "Failed to load the Tartube config file (file is locked)" +msgstr "" + +#: .././mainapp.py:2866 +msgid "Failed to load the Tartube config file (JSON load failure)" +msgstr "" + +#: .././mainapp.py:2882 +msgid "Failed to load the Tartube config file (file is invalid)" +msgstr "" + +#: .././mainapp.py:2899 +msgid "" +"Failed to load the Tartube config file (file cannot be read by this version)" +msgstr "" + +#: .././mainapp.py:2913 +msgid "Failed to load the Tartube config file (missing file type)" +msgstr "" + +#: .././mainapp.py:3472 +msgid "Failed to save the Tartube config file (failed sanity check)" +msgstr "" + +#: .././mainapp.py:3718 +msgid "Failed to save the Tartube config file (file is locked)" +msgstr "" + +#: .././mainapp.py:3720 .././mainapp.py:3760 .././mainapp.py:4690 +#: .././mainapp.py:4746 .././mainapp.py:4752 +msgid "File load/save has been disabled" +msgstr "" + +#: .././mainapp.py:3739 +msgid "Failed to save the Tartube config file (file already in use)" +msgstr "" + +#: .././mainapp.py:3759 +msgid "Failed to save the Tartube config file" +msgstr "" + +#: .././mainapp.py:3808 .././mainapp.py:3826 .././mainapp.py:3856 +msgid "Failed to load the Tartube database file" +msgstr "" + +#: .././mainapp.py:3871 +msgid "The Tartube database file is invalid" +msgstr "" + +#: .././mainapp.py:3887 +msgid "Database file can't be read by this version of Tartube" +msgstr "" + +#: .././mainapp.py:4187 +msgid "Tartube is applying an essential database update" +msgstr "" + +#: .././mainapp.py:4189 +msgid "This might take a few minutes, so please be patient" +msgstr "" + +#: .././mainapp.py:4684 .././mainapp.py:4742 .././mainapp.py:4751 +msgid "Failed to save the Tartube database file" +msgstr "" + +#: .././mainapp.py:4687 +msgid "(Could not make a backup copy of the existing file)" +msgstr "" + +#: .././mainapp.py:4723 +msgid "Failed to save the Tartube database file (file already in use)" +msgstr "" + +#: .././mainapp.py:4744 +msgid "A backup of the previous file can be found at:" +msgstr "" + +#: .././mainapp.py:4969 .././mainapp.py:4979 +msgid "Database file created" +msgstr "" + +#: .././mainapp.py:5032 .././mainapp.py:5084 +#, 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:5247 +msgid "Tartube's database can't be checked while an operation is in progress" +msgstr "" + +#: .././mainapp.py:5431 +msgid "Database check complete, no inconsistencies found" +msgstr "" + +#: .././mainapp.py:5445 +msgid "Database check complete, problems found:" +msgstr "" + +#: .././mainapp.py:5448 +msgid "" +"Do you want to repair these problems? (The database will be fixed, but no " +"files will be deleted)" +msgstr "" + +#: .././mainapp.py:5588 +msgid "Database inconsistencies repaired" +msgstr "" + +#: .././mainapp.py:6229 .././config.py:9731 +msgid "Please select Tartube's data folder" +msgstr "" + +#: .././mainapp.py:6355 +msgid "" +"A download operation cannot start if one or more configuration windows are " +"still open" +msgstr "" + +#: .././mainapp.py:6379 .././mainapp.py:6401 +#, python-brace-format +msgid "You only have {0} / {1} Mb remaining on your device" +msgstr "" + +#: .././mainapp.py:6404 .././mainapp.py:11069 .././mainapp.py:11234 +#: .././mainwin.py:13433 +msgid "Are you sure you want to continue?" +msgstr "" + +#: .././mainapp.py:6485 +msgid "There is nothing to check!" +msgstr "" + +#: .././mainapp.py:6487 +msgid "There is nothing to download!" +msgstr "" + +#: .././mainapp.py:6698 +msgid "Download operation complete" +msgstr "" + +#: .././mainapp.py:6700 +msgid "Download operation halted" +msgstr "" + +#: .././mainapp.py:6703 .././mainapp.py:7170 .././mainapp.py:7616 +msgid "Time taken:" +msgstr "" + +#: .././mainapp.py:6761 +msgid "" +"An update operation cannot start if one or more configuration windows are " +"still open" +msgstr "" + +#: .././mainapp.py:6874 +msgid "Installation failed" +msgstr "" + +#: .././mainapp.py:6876 +msgid "Installation complete" +msgstr "" + +#: .././mainapp.py:6880 +msgid "Update operation failed" +msgstr "" + +#: .././mainapp.py:6882 +msgid "Update operation halted" +msgstr "" + +#: .././mainapp.py:6884 +msgid "Update operation complete" +msgstr "" + +#: .././mainapp.py:6885 +msgid "youtube-dl version:" +msgstr "" + +#: .././mainapp.py:6889 +msgid "(unknown)" +msgstr "" + +#: .././mainapp.py:6963 +msgid "" +"A refresh operation cannot start if one or more configuration windows are " +"still open" +msgstr "" + +#: .././mainapp.py:6976 +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:6980 +msgid "" +"You only need to perform a refresh operation if you have manually copied " +"videos into Tartube's data folder" +msgstr "" + +#: .././mainapp.py:6987 +msgid "" +"Before starting a refresh operation, you should click the 'Check all' button " +"in the main window" +msgstr "" + +#: .././mainapp.py:6994 +msgid "" +"Before starting a refresh operation, you should right-click the channel and " +"select 'Check channel'" +msgstr "" + +#: .././mainapp.py:7001 +msgid "" +"Before starting a refresh operation, you should right-click the playlist and " +"select 'Check playlist'" +msgstr "" + +#: .././mainapp.py:7008 +msgid "" +"Before starting a refresh operation, you should right-click the folder and " +"select 'Check folder'" +msgstr "" + +#: .././mainapp.py:7013 +msgid "Are you sure you want to proceed with the refresh operation?" +msgstr "" + +#: .././mainapp.py:7165 +msgid "Refresh operation complete" +msgstr "" + +#: .././mainapp.py:7167 +msgid "Refresh operation halted" +msgstr "" + +#: .././mainapp.py:7267 +msgid "" +"An info operation cannot start if one or more configuration windows are " +"still open" +msgstr "" + +#: .././mainapp.py:7380 +msgid "Operation failed" +msgstr "" + +#: .././mainapp.py:7382 .././downloads.py:357 +msgid "Operation complete" +msgstr "" + +#: .././mainapp.py:7384 +msgid "Click the Output Tab to see the results" +msgstr "" + +#: .././mainapp.py:7482 +msgid "" +"A tidy operation cannot start if one or more configuration windows are still " +"open" +msgstr "" + +#: .././mainapp.py:7611 +msgid "Tidy operation complete" +msgstr "" + +#: .././mainapp.py:7613 +msgid "Tidy operation halted" +msgstr "" + +#: .././mainapp.py:7741 .././mainwin.py:13843 +msgid "Livestream has started" +msgstr "" + +#: .././mainapp.py:8995 .././mainapp.py:9171 +msgid "Cannot move anything to:" +msgstr "" + +#: .././mainapp.py:8997 .././mainapp.py:9173 +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:9001 +msgid "" +"You probably created that file/folder accidentally, in which case you should " +"delete it manually before trying again" +msgstr "" + +#: .././mainapp.py:9015 .././mainapp.py:9191 +msgid "Are you sure you want to move this channel:" +msgstr "" + +#: .././mainapp.py:9017 .././mainapp.py:9193 +msgid "Are you sure you want to move this playlist:" +msgstr "" + +#: .././mainapp.py:9019 .././mainapp.py:9195 +msgid "Are you sure you want to move this folder:" +msgstr "" + +#: .././mainapp.py:9024 +msgid "" +"This procedure will move all downloaded files to the top level of Tartube's " +"data folder" +msgstr "" + +#: .././mainapp.py:9125 +msgid "Channels, playlists and folders can only be dragged into a folder" +msgstr "" + +#: .././mainapp.py:9138 +#, python-brace-format +msgid "The fixed folder '{0}' cannot be moved (but it can still be hidden)" +msgstr "" + +#: .././mainapp.py:9151 +#, python-brace-format +msgid "The folder '{0}' can only contain videos" +msgstr "" + +#: .././mainapp.py:9178 +msgid "" +"You probably created that file/folder accidentally, in which case, you " +"should delete it manually before trying again" +msgstr "" + +#: .././mainapp.py:9197 +msgid "into this folder:" +msgstr "" + +#: .././mainapp.py:9201 +msgid "This procedure will move all downloaded files to the new location" +msgstr "" + +#: .././mainapp.py:9207 +msgid "" +"WARNING: The destination folder is marked as temporary, so everything inside " +"it will be DELETED when Tartube restarts!" +msgstr "" + +#: .././mainapp.py:9589 +msgid "" +"Are you SURE you want to delete files? This procedure cannot be reversed!" +msgstr "" + +#: .././mainapp.py:11053 .././mainapp.py:11218 +#, python-brace-format +msgid "The channel contains {0} item(s), so this action may take a while" +msgstr "" + +#: .././mainapp.py:11059 .././mainapp.py:11224 +#, python-brace-format +msgid "The playlist contains {0} item(s), so this action may take a while" +msgstr "" + +#: .././mainapp.py:11065 .././mainapp.py:11230 +#, python-brace-format +msgid "The folder contains {0} item(s), so this action may take a while" +msgstr "" + +#: .././mainapp.py:11298 .././mainapp.py:13839 .././mainapp.py:13971 +#: .././mainapp.py:14102 +#, python-brace-format +msgid "The name '{0}' is not allowed" +msgstr "" + +#: .././mainapp.py:11307 +#, python-brace-format +msgid "The name '{0}' is already in use" +msgstr "" + +#: .././mainapp.py:11320 +#, python-brace-format +msgid "Failed to rename '{0}'" +msgstr "" + +#: .././mainapp.py:11576 +msgid "Select where to save the database export" +msgstr "" + +#: .././mainapp.py:11705 +msgid "There is nothing to export!" +msgstr "" + +#: .././mainapp.py:11738 .././mainapp.py:11796 +msgid "Failed to save the database export file" +msgstr "" + +#: .././mainapp.py:11803 +msgid "Database export file saved to:" +msgstr "" + +#: .././mainapp.py:11840 +msgid "Select the database export" +msgstr "" + +#: .././mainapp.py:11865 .././mainapp.py:11879 +msgid "Failed to load the database export file" +msgstr "" + +#: .././mainapp.py:11896 +msgid "The database export file is invalid" +msgstr "" + +#: .././mainapp.py:11907 +msgid "The database export file is invalid (or empty)" +msgstr "" + +#: .././mainapp.py:11951 +msgid "Nothing was imported from the database export file" +msgstr "" + +#. Show a confirmation +#: .././mainapp.py:11965 +msgid "Imported:" +msgstr "" + +#: .././mainapp.py:11966 +msgid "Videos:" +msgstr "" + +#: .././mainapp.py:11967 +msgid "Channels:" +msgstr "" + +#: .././mainapp.py:11968 +msgid "Playlists:" +msgstr "" + +#: .././mainapp.py:11969 +msgid "Folders:" +msgstr "" + +#: .././mainapp.py:12330 +msgid "" +"The video file is missing from Tartube's data folder (try downloading the " +"video again!)" +msgstr "" + +#: .././mainapp.py:13027 +msgid "Please select a destination folder" +msgstr "" + +#: .././mainapp.py:13160 +msgid "No video(s) have been downloaded" +msgstr "" + +#. Prompt for confirmation +#: .././mainapp.py:13250 +msgid "Are you sure you want to remove the selected item(s)?" +msgstr "" + +#: .././mainapp.py:13830 +msgid "You must give the channel a name" +msgstr "" + +#: .././mainapp.py:13848 .././mainapp.py:14111 +msgid "You must enter a valid URL" +msgstr "" + +#: .././mainapp.py:13963 +msgid "You must give the folder a name" +msgstr "" + +#: .././mainapp.py:14093 +msgid "You must give the playlist a name" +msgstr "" + +#: .././mainapp.py:14248 .././mainwin.py:13328 +msgid "The following videos are duplicates:" +msgstr "" + +#: .././mainapp.py:14312 +msgid "There were no livestream alerts to cancel" +msgstr "" + +#: .././mainapp.py:14314 +msgid "Livestream alerts for 1 video were cancelled" +msgstr "Livestream alerts for 1 video were canceled" + +#: .././mainapp.py:14317 +#, python-brace-format +msgid "Livestream alerts for {0} videos were cancelled" +msgstr "Livestream alerts for {0} videos were canceled" + +#: .././mainapp.py:14618 +msgid "Data saved" +msgstr "" + +#: .././mainapp.py:14648 +msgid "Database saved" +msgstr "" + +#: .././mainapp.py:14869 .././mainwin.py:10597 +msgid "" +"Files cannot be recovered, after being deleted. Are you sure you want to " +"continue?" +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:14909 +msgid "Cannot update existing livestreams because" +msgstr "" + +#: .././mainapp.py:14911 +msgid "there is another operation running" +msgstr "" + +#: .././mainapp.py:14913 +msgid "they are currently being updated" +msgstr "" + +#: .././mainapp.py:14915 +msgid "one or more configuration windows are open" +msgstr "" + +#: .././mainapp.py:14917 +msgid "there are no livestreams to update" +msgstr "" + +#: .././mainapp.py:14991 +msgid "There is already a channel with that name" +msgstr "" + +#: .././mainapp.py:14993 +msgid "There is already a playlist with that name" +msgstr "" + +#: .././mainapp.py:14995 +msgid "There is already a folder with that name" +msgstr "" + +#: .././mainapp.py:14998 +msgid "(so please choose a different name)" +msgstr "" + +#: .././mainwin.py:709 +msgid "Tartube cannot start because it cannot find its icons folder" +msgstr "" + +#. File column +#: .././mainwin.py:799 +msgid "_File" +msgstr "" + +#: .././mainwin.py:806 +msgid "_Database preferences..." +msgstr "" + +#: .././mainwin.py:815 +msgid "_Save database" +msgstr "" + +#: .././mainwin.py:821 +msgid "Save _all" +msgstr "" + +#: .././mainwin.py:830 +msgid "_Close to tray" +msgstr "" + +#. Quit +#: .././mainwin.py:835 .././mainwin.py:16379 +msgid "_Quit" +msgstr "" + +#. Edit column +#: .././mainwin.py:840 +msgid "_Edit" +msgstr "" + +#: .././mainwin.py:847 +msgid "_System preferences..." +msgstr "" + +#: .././mainwin.py:853 +msgid "_General download options..." +msgstr "" + +#. Media column +#: .././mainwin.py:859 +msgid "_Media" +msgstr "" + +#: .././mainwin.py:866 +msgid "Add _videos..." +msgstr "" + +#: .././mainwin.py:872 +msgid "Add _channel..." +msgstr "" + +#: .././mainwin.py:878 +msgid "Add _playlist..." +msgstr "" + +#: .././mainwin.py:884 +msgid "Add _folder..." +msgstr "" + +#: .././mainwin.py:893 +msgid "_Export from database" +msgstr "" + +#: .././mainwin.py:901 +msgid "_JSON export file" +msgstr "" + +#: .././mainwin.py:907 +msgid "Plain _text export file" +msgstr "" + +#: .././mainwin.py:913 +msgid "_Import into database" +msgstr "" + +#: .././mainwin.py:922 +msgid "_Switch between views" +msgstr "" + +#: .././mainwin.py:927 +msgid "Show _hidden folders" +msgstr "" + +#: .././mainwin.py:937 +msgid "_Add test media" +msgstr "" + +#. Operations column +#. Add this tab... +#: .././mainwin.py:943 .././config.py:7862 +msgid "_Operations" +msgstr "" + +#. Check all +#: .././mainwin.py:950 .././mainwin.py:16350 +msgid "_Check all" +msgstr "" + +#. Download all +#: .././mainwin.py:956 .././mainwin.py:16357 +msgid "_Download all" +msgstr "" + +#: .././mainwin.py:961 +msgid "C_ustom download all" +msgstr "" + +#: .././mainwin.py:969 +msgid "_Refresh database..." +msgstr "" + +#: .././mainwin.py:978 +msgid "Update _youtube-dl" +msgstr "" + +#: .././mainwin.py:984 +msgid "_Test youtube-dl..." +msgstr "" + +#: .././mainwin.py:993 +msgid "_Install FFmpeg" +msgstr "" + +#: .././mainwin.py:1004 +msgid "Tidy up _files..." +msgstr "" + +#: .././mainwin.py:1015 .././mainwin.py:16368 +msgid "_Stop current operation" +msgstr "" + +#. Livestreams column +#: .././mainwin.py:1022 .././config.py:8093 +msgid "_Livestreams" +msgstr "" + +#: .././mainwin.py:1029 +msgid "_Livestream preferences..." +msgstr "" + +#: .././mainwin.py:1038 +msgid "_Update existing livestreams" +msgstr "" + +#: .././mainwin.py:1043 +msgid "_Cancel all livestream alerts" +msgstr "" + +#. Help column +#: .././mainwin.py:1048 +msgid "_Help" +msgstr "" + +#: .././mainwin.py:1054 +msgid "_About..." +msgstr "" + +#: .././mainwin.py:1059 +msgid "Go to _website" +msgstr "" + +#: .././mainwin.py:1065 +msgid "Send _feedback" +msgstr "" + +#: .././mainwin.py:1101 +msgid "Videos" +msgstr "" + +#: .././mainwin.py:1111 +msgid "Add new video(s)" +msgstr "" + +#: .././mainwin.py:1120 +msgid "Channel" +msgstr "" + +#: .././mainwin.py:1130 +msgid "Add a new channel" +msgstr "" + +#: .././mainwin.py:1141 +msgid "Playlist" +msgstr "" + +#: .././mainwin.py:1151 +msgid "Add a new playlist" +msgstr "" + +#: .././mainwin.py:1162 +msgid "Folder" +msgstr "" + +#: .././mainwin.py:1172 +msgid "Add a new folder" +msgstr "" + +#: .././mainwin.py:1186 +msgid "Check" +msgstr "" + +#: .././mainwin.py:1197 .././mainwin.py:1429 .././mainwin.py:2898 +#: .././mainwin.py:3068 +msgid "Check all videos, channels, playlists and folders" +msgstr "" + +#. Link not clickable +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:1207 .././mainwin.py:14863 .././mainwin.py:14871 +#: .././mainwin.py:15094 .././mainwin.py:15106 .././mainwin.py:15764 +msgid "Download" +msgstr "" + +#: .././mainwin.py:1218 .././mainwin.py:1437 .././mainwin.py:2906 +#: .././mainwin.py:3074 +msgid "Download all videos, channels, playlists and folders" +msgstr "" + +#: .././mainwin.py:1233 +msgid "Stop" +msgstr "" + +#: .././mainwin.py:1245 +msgid "Stop the current operation" +msgstr "" + +#. (signal_connect appears below) +#. v2.0.079 These lines produce a Gtk error, for no obvious reason (the +#. equivalent code in mainwin.MainWin.setup_classic_mode_tab() +#. produces no error) +#. selection = treeview.get_selection() +#. selection.set_mode(Gtk.SelectionMode.MULTIPLE) +#: .././mainwin.py:1257 .././config.py:6694 +msgid "Switch" +msgstr "" + +#: .././mainwin.py:1268 +msgid "Switch between simple and complex views" +msgstr "" + +#: .././mainwin.py:1282 .././config.py:8233 +msgid "Test" +msgstr "" + +#: .././mainwin.py:1293 +msgid "Add test media data objects" +msgstr "" + +#: .././mainwin.py:1306 +msgid "Quit" +msgstr "" + +#: .././mainwin.py:1316 +msgid "Close Tartube" +msgstr "" + +#: .././mainwin.py:1338 +msgid "_Videos" +msgstr "" + +#: .././mainwin.py:1346 +msgid "_Progress" +msgstr "" + +#: .././mainwin.py:1354 +msgid "_Classic Mode" +msgstr "" + +#: .././mainwin.py:1362 +msgid "_Output" +msgstr "" + +#: .././mainwin.py:1371 .././config.py:5361 .././config.py:5713 +msgid "_Errors / Warnings" +msgstr "" + +#: .././mainwin.py:1427 .././mainwin.py:2896 .././mainwin.py:3065 +msgid "Check all" +msgstr "" + +#: .././mainwin.py:1435 .././mainwin.py:2355 .././mainwin.py:2904 +msgid "Download all" +msgstr "" + +#: .././mainwin.py:1492 +msgid "Page" +msgstr "" + +#: .././mainwin.py:1504 +msgid "Set visible page" +msgstr "" + +#: .././mainwin.py:1528 .././mainwin.py:1762 .././mainwin.py:1823 +#: .././mainwin.py:2249 +msgid "Size" +msgstr "" + +#: .././mainwin.py:1539 +msgid "Set page size" +msgstr "" + +#: .././mainwin.py:1552 +msgid "Go to first page" +msgstr "" + +#: .././mainwin.py:1561 +msgid "Go to previous page" +msgstr "" + +#: .././mainwin.py:1570 +msgid "Go to next page" +msgstr "" + +#: .././mainwin.py:1579 +msgid "Go to last page" +msgstr "" + +#: .././mainwin.py:1588 +msgid "Scroll up" +msgstr "" + +#: .././mainwin.py:1597 +msgid "Scroll down" +msgstr "" + +#: .././mainwin.py:1607 .././mainwin.py:3302 +msgid "Show filter options" +msgstr "" + +#: .././mainwin.py:1620 +msgid "Sort by" +msgstr "" + +#: .././mainwin.py:1627 .././mainwin.py:3359 +msgid "Sort alphabetically" +msgstr "" + +#: .././mainwin.py:1637 +msgid "Filter" +msgstr "" + +#: .././mainwin.py:1646 +msgid "Enter search text" +msgstr "" + +#: .././mainwin.py:1651 +msgid "Regex" +msgstr "" + +#: .././mainwin.py:1659 +msgid "Select if search text is a regex" +msgstr "" + +#: .././mainwin.py:1670 +msgid "Filter videos" +msgstr "" + +#: .././mainwin.py:1681 +msgid "Cancel filter" +msgstr "" + +#: .././mainwin.py:1692 +msgid "Find date" +msgstr "" + +#: .././mainwin.py:1700 +msgid "Find videos by date" +msgstr "" + +#: .././mainwin.py:1755 +msgid "TRANSLATOR'S NOTE: Ext is short for a file extension, e.g. .EXE" +msgstr "" + +#: .././mainwin.py:1760 .././mainwin.py:2247 +msgid "Source" +msgstr "" + +#: .././mainwin.py:1760 .././mainwin.py:2247 +msgid "Status" +msgstr "" + +#: .././mainwin.py:1761 .././mainwin.py:2248 +msgid "Incoming file" +msgstr "" + +#: .././mainwin.py:1761 .././mainwin.py:2248 +msgid "Ext" +msgstr "" + +#: .././mainwin.py:1761 .././mainwin.py:2248 +msgid "Speed" +msgstr "" + +#: .././mainwin.py:1761 .././mainwin.py:2248 +msgid "ETA" +msgstr "" + +#: .././mainwin.py:1823 .././config.py:5625 +msgid "New videos" +msgstr "" + +#: .././mainwin.py:1823 .././config.py:5138 +msgid "Duration" +msgstr "" + +#: .././mainwin.py:1824 +msgid "Date" +msgstr "" + +#: .././mainwin.py:1824 .././config.py:5109 +msgid "File" +msgstr "" + +#: .././mainwin.py:1824 +msgid "Downloaded to" +msgstr "" + +#: .././mainwin.py:1880 +msgid "Max downloads" +msgstr "" + +#: .././mainwin.py:1903 +msgid "D/L speed (KiB/s)" +msgstr "" + +#: .././mainwin.py:1929 .././config.py:2402 +msgid "Video resolution" +msgstr "" + +#: .././mainwin.py:1964 +msgid "Hide rows when they are finished" +msgstr "" + +#: .././mainwin.py:1977 +msgid "Add newest videos to the top of the list" +msgstr "" + +#: .././mainwin.py:2037 +msgid "This tab emulates the classic youtube-dl-gui interface" +msgstr "" + +#: .././mainwin.py:2045 +msgid "Videos downloaded here are not added to Tartube's database" +msgstr "" + +#: .././mainwin.py:2059 +msgid "General download options" +msgstr "" + +#: .././mainwin.py:2076 +msgid "Update youtube-dl" +msgstr "" + +#: .././mainwin.py:2088 .././mainwin.py:8569 .././mainwin.py:17047 +#: .././mainwin.py:17542 .././mainwin.py:17895 +msgid "Enable automatic copy/paste" +msgstr "" + +#. Second row - a textview for entering URLs. If automatic copy/paste is +#. enabled, URLs are automatically copied into this textview +#. -------------------------------------------------------------------- +#: .././mainwin.py:2095 +msgid "Enter URLs below" +msgstr "" + +#. Third row - widgets to set the download destination and video/audio +#. format. The user clicks the 'Add URLs' button to create dummy +#. media.Video objects for each URL. Each object is associated with +#. the specified destination and format +#. -------------------------------------------------------------------- +#. Destination directory +#: .././mainwin.py:2134 +msgid "Destination:" +msgstr "" + +#: .././mainwin.py:2162 +msgid "Add a new destination folder" +msgstr "" + +#. Video/audio format +#: .././mainwin.py:2167 +msgid "Format:" +msgstr "" + +#: .././mainwin.py:2170 +msgid "Default" +msgstr "" + +#: .././mainwin.py:2170 .././mainwin.py:12732 +msgid "Video:" +msgstr "" + +#: .././mainwin.py:2174 .././mainwin.py:12732 +msgid "Audio:" +msgstr "" + +#: .././mainwin.py:2204 +msgid "Add URLs" +msgstr "" + +#: .././mainwin.py:2210 +msgid "Add these URLs" +msgstr "" + +#: .././mainwin.py:2287 +msgid "Remove from list" +msgstr "" + +#: .././mainwin.py:2302 +msgid "Play video" +msgstr "" + +#. Signal connect below +#: .././mainwin.py:2312 .././config.py:2755 .././config.py:6731 +msgid "Move up" +msgstr "" + +#. Signal connect below +#. signal connect appears below +#: .././mainwin.py:2327 .././config.py:2759 .././config.py:6739 +msgid "Move down" +msgstr "" + +#: .././mainwin.py:2337 +msgid "Re-download" +msgstr "" + +#: .././mainwin.py:2352 +msgid "Stop download" +msgstr "" + +#: .././mainwin.py:2362 +msgid "Download the URLs above" +msgstr "" + +#: .././mainwin.py:2425 +msgid "Time" +msgstr "" + +#: .././mainwin.py:2425 +msgid "Type" +msgstr "" + +#: .././mainwin.py:2425 +msgid "Message" +msgstr "" + +#: .././mainwin.py:2459 +msgid "Show Tartube errors" +msgstr "" + +#: .././mainwin.py:2472 +msgid "Show Tartube warnings" +msgstr "" + +#: .././mainwin.py:2485 +msgid "Show server errors" +msgstr "" + +#: .././mainwin.py:2503 +msgid "Show server warnings" +msgstr "" + +#: .././mainwin.py:2515 +msgid "Clear list" +msgstr "" + +#: .././mainwin.py:2824 .././mainwin.py:2852 +msgid "Checking..." +msgstr "" + +#: .././mainwin.py:2826 .././mainwin.py:2854 +msgid "Downloading..." +msgstr "" + +#: .././mainwin.py:2828 .././mainwin.py:2856 +msgid "Refreshing..." +msgstr "" + +#: .././mainwin.py:2830 .././mainwin.py:2858 +msgid "Tidying..." +msgstr "" + +#: .././mainwin.py:3044 +msgid "Installing" +msgstr "" + +#: .././mainwin.py:3047 +msgid "Updating" +msgstr "" + +#: .././mainwin.py:3050 .././mainwin.py:3053 +msgid "Fetching" +msgstr "" + +#: .././mainwin.py:3056 +msgid "Testing" +msgstr "" + +#: .././mainwin.py:3318 +msgid "Hide filter options" +msgstr "" + +#: .././mainwin.py:3367 +msgid "Sort by date" +msgstr "" + +#: .././mainwin.py:3590 +msgid "_Check channel" +msgstr "" + +#: .././mainwin.py:3592 +msgid "_Check playlist" +msgstr "" + +#: .././mainwin.py:3594 +msgid "_Check folder" +msgstr "" + +#: .././mainwin.py:3611 +msgid "_Download channel" +msgstr "" + +#: .././mainwin.py:3613 +msgid "_Download playlist" +msgstr "" + +#: .././mainwin.py:3615 +msgid "_Download folder" +msgstr "" + +#: .././mainwin.py:3632 +msgid "C_ustom download channel" +msgstr "" + +#: .././mainwin.py:3634 +msgid "C_ustom download playlist" +msgstr "" + +#: .././mainwin.py:3636 +msgid "C_ustom download folder" +msgstr "" + +#: .././mainwin.py:3681 +msgid "_Empty folder" +msgstr "" + +#: .././mainwin.py:3693 +msgid "_All contents" +msgstr "" + +#: .././mainwin.py:3711 +msgid "_Remove videos" +msgstr "" + +#: .././mainwin.py:3723 +msgid "_Just folder videos" +msgstr "" + +#: .././mainwin.py:3729 +msgid "Channel co_ntents" +msgstr "" + +#: .././mainwin.py:3731 +msgid "Playlist co_ntents" +msgstr "" + +#: .././mainwin.py:3733 +msgid "Folder co_ntents" +msgstr "" + +#: .././mainwin.py:3745 +msgid "_Move to top level" +msgstr "" + +#: .././mainwin.py:3762 +msgid "_Convert to playlist" +msgstr "" + +#: .././mainwin.py:3764 +msgid "_Convert to channel" +msgstr "" + +#: .././mainwin.py:3786 +msgid "_Hide folder" +msgstr "" + +#: .././mainwin.py:3796 +msgid "_Rename channel..." +msgstr "" + +#: .././mainwin.py:3798 +msgid "_Rename playlist..." +msgstr "" + +#: .././mainwin.py:3800 +msgid "_Rename folder..." +msgstr "" + +#: .././mainwin.py:3817 +msgid "Set _nickname..." +msgstr "" + +#: .././mainwin.py:3830 +msgid "Set _download destination..." +msgstr "" + +#: .././mainwin.py:3846 +msgid "_Export channel..." +msgstr "" + +#: .././mainwin.py:3848 +msgid "_Export playlist..." +msgstr "" + +#: .././mainwin.py:3850 +msgid "_Export folder..." +msgstr "" + +#: .././mainwin.py:3863 +msgid "Re_fresh channel" +msgstr "" + +#: .././mainwin.py:3865 +msgid "Re_fresh playlist" +msgstr "" + +#: .././mainwin.py:3867 +msgid "Re_fresh folder" +msgstr "" + +#: .././mainwin.py:3884 +msgid "_Tidy up channel" +msgstr "" + +#: .././mainwin.py:3886 +msgid "_Tidy up playlist" +msgstr "" + +#: .././mainwin.py:3888 +msgid "_Tidy up folder" +msgstr "" + +#: .././mainwin.py:3905 +msgid "Channel _actions" +msgstr "" + +#: .././mainwin.py:3907 +msgid "Playlist _actions" +msgstr "" + +#: .././mainwin.py:3909 +msgid "Folder _actions" +msgstr "" + +#: .././mainwin.py:3929 .././mainwin.py:4243 +msgid "_Apply download options..." +msgstr "" + +#: .././mainwin.py:3947 .././mainwin.py:4257 +msgid "_Remove download options" +msgstr "" + +#: .././mainwin.py:3963 .././mainwin.py:4269 +msgid "_Edit download options..." +msgstr "" + +#: .././mainwin.py:3979 +msgid "_Show system command" +msgstr "" + +#: .././mainwin.py:3992 +msgid "_Disable checking/downloading" +msgstr "" + +#: .././mainwin.py:4004 +msgid "_Just disable downloading" +msgstr "" + +#: .././mainwin.py:4029 .././mainwin.py:4328 +msgid "D_ownloads" +msgstr "" + +#: .././mainwin.py:4037 +msgid "Channel _properties..." +msgstr "" + +#: .././mainwin.py:4039 +msgid "Playlist _properties..." +msgstr "" + +#: .././mainwin.py:4041 +msgid "Folder _properties..." +msgstr "" + +#: .././mainwin.py:4057 +msgid "_Default location" +msgstr "" + +#: .././mainwin.py:4070 +msgid "_Actual location" +msgstr "" + +#: .././mainwin.py:4082 +msgid "_Show" +msgstr "" + +#: .././mainwin.py:4091 +msgid "D_elete channel" +msgstr "" + +#: .././mainwin.py:4093 +msgid "D_elete playlist" +msgstr "" + +#: .././mainwin.py:4095 +msgid "D_elete folder" +msgstr "" + +#: .././mainwin.py:4154 +msgid "_Check video" +msgstr "" + +#: .././mainwin.py:4177 +msgid "_Download video" +msgstr "" + +#: .././mainwin.py:4197 +msgid "Re-_download this video" +msgstr "" + +#: .././mainwin.py:4210 +msgid "C_ustom download video" +msgstr "" + +#: .././mainwin.py:4285 +msgid "Show system _command" +msgstr "" + +#: .././mainwin.py:4295 +msgid "_Test system command" +msgstr "" + +#: .././mainwin.py:4310 +msgid "_Disable downloads" +msgstr "" + +#: .././mainwin.py:4340 +msgid "Video is _archived" +msgstr "" + +#: .././mainwin.py:4353 +msgid "Video is _bookmarked" +msgstr "" + +#: .././mainwin.py:4364 +msgid "Video is _favourite" +msgstr "" + +#: .././mainwin.py:4375 +msgid "Video is _new" +msgstr "" + +#: .././mainwin.py:4388 +msgid "Video is in _waiting list" +msgstr "" + +#: .././mainwin.py:4399 +msgid "_Mark video" +msgstr "" + +#: .././mainwin.py:4410 +msgid "_Location" +msgstr "" + +#: .././mainwin.py:4420 +msgid "_Properties..." +msgstr "" + +#: .././mainwin.py:4432 +msgid "_Show video" +msgstr "" + +#: .././mainwin.py:4441 +msgid "Available _formats" +msgstr "" + +#: .././mainwin.py:4451 +msgid "Available _subtitles" +msgstr "" + +#: .././mainwin.py:4461 +msgid "_Fetch" +msgstr "" + +#. Delete video +#: .././mainwin.py:4472 +msgid "D_elete video" +msgstr "" + +#. Check/download videos +#: .././mainwin.py:4559 +msgid "_Check videos" +msgstr "" + +#: .././mainwin.py:4579 +msgid "_Download videos" +msgstr "" + +#: .././mainwin.py:4598 +msgid "C_ustom download videos" +msgstr "" + +#: .././mainwin.py:4616 +msgid "D_ownload and watch" +msgstr "" + +#: .././mainwin.py:4633 .././mainwin.py:5376 +msgid "Watch in _player" +msgstr "" + +#: .././mainwin.py:4643 .././mainwin.py:5391 .././mainwin.py:5402 +msgid "Watch on _website" +msgstr "" + +#: .././mainwin.py:4661 .././mainwin.py:5559 +msgid "_Mark for download" +msgstr "" + +#: .././mainwin.py:4673 .././mainwin.py:5570 +msgid "_Download" +msgstr "" + +#: .././mainwin.py:4683 +msgid "_Download and watch" +msgstr "" + +#: .././mainwin.py:4694 .././mainwin.py:5590 +msgid "_Temporary" +msgstr "" + +#: .././mainwin.py:4712 +msgid "_Archived" +msgstr "" + +#: .././mainwin.py:4725 +msgid "Not a_rchived" +msgstr "" + +#: .././mainwin.py:4741 +msgid "_Bookmarked" +msgstr "" + +#: .././mainwin.py:4754 +msgid "Not b_ookmarked" +msgstr "" + +#: .././mainwin.py:4770 +msgid "_Favourite" +msgstr "_Favorite" + +#: .././mainwin.py:4783 +msgid "Not fa_vourite" +msgstr "Not fa_vorite" + +#: .././mainwin.py:4799 +msgid "_New" +msgstr "" + +#: .././mainwin.py:4812 +msgid "Not n_ew" +msgstr "" + +#: .././mainwin.py:4828 +msgid "In _waiting list" +msgstr "" + +#: .././mainwin.py:4841 +msgid "Not in w_aiting list" +msgstr "" + +#: .././mainwin.py:4854 +msgid "_Mark videos" +msgstr "" + +#: .././mainwin.py:4863 +msgid "Show p_roperties..." +msgstr "" + +#. Delete videos +#: .././mainwin.py:4878 +msgid "D_elete videos" +msgstr "" + +#. Stop check/download +#: .././mainwin.py:4943 +msgid "_Stop now" +msgstr "" + +#: .././mainwin.py:4957 +msgid "Stop after this _video" +msgstr "" + +#: .././mainwin.py:4972 +msgid "Stop after these v_ideos" +msgstr "" + +#: .././mainwin.py:4987 +msgid "Download _next" +msgstr "" + +#: .././mainwin.py:4999 +msgid "Download _last" +msgstr "" + +#: .././mainwin.py:5022 +msgid "Watch on _YouTube" +msgstr "" + +#: .././mainwin.py:5032 +msgid "Watch on _HookTube" +msgstr "" + +#: .././mainwin.py:5042 +msgid "Watch on _Invidious" +msgstr "" + +#: .././mainwin.py:5054 +msgid "Watch on _Website" +msgstr "" + +#. Delete video +#: .././mainwin.py:5106 +msgid "_Delete video" +msgstr "" + +#. Get URL +#: .././mainwin.py:5153 +msgid "Get _URL" +msgstr "" + +#. Get command +#: .././mainwin.py:5162 +msgid "Get _command" +msgstr "" + +#: .././mainwin.py:5172 +msgid "_Open destination" +msgstr "" + +#: .././mainwin.py:5213 +msgid "Mark as _archived" +msgstr "" + +#: .././mainwin.py:5224 +msgid "Mark as not a_rchived" +msgstr "" + +#: .././mainwin.py:5238 +msgid "Mark as _bookmarked" +msgstr "" + +#: .././mainwin.py:5250 +msgid "Mark as not b_ookmarked" +msgstr "" + +#: .././mainwin.py:5263 +msgid "Mark as _favourite" +msgstr "Mark as _favorite" + +#: .././mainwin.py:5276 +msgid "Mark as not fa_vourite" +msgstr "Mark as not fa_vorite" + +#: .././mainwin.py:5289 +msgid "Mark as _new" +msgstr "" + +#: .././mainwin.py:5301 +msgid "Mark as not n_ew" +msgstr "" + +#: .././mainwin.py:5315 +msgid "Mark as in _waiting list" +msgstr "" + +#: .././mainwin.py:5327 +msgid "Mark as not in wai_ting list" +msgstr "" + +#: .././mainwin.py:5359 .././mainwin.py:5580 +msgid "Download and _watch" +msgstr "" + +#: .././mainwin.py:5416 +msgid "_YouTube" +msgstr "" + +#: .././mainwin.py:5426 +msgid "_HookTube" +msgstr "" + +#: .././mainwin.py:5436 +msgid "_Invidious" +msgstr "" + +#: .././mainwin.py:5446 +msgid "TRANSLATOR'S NOTE: Watch on YouTube, Watch on HookTube, etc" +msgstr "" + +#: .././mainwin.py:5451 +msgid "W_atch on" +msgstr "" + +#: .././mainwin.py:5465 +msgid "Auto _notify" +msgstr "" + +#: .././mainwin.py:5481 +msgid "Auto _sound alarm" +msgstr "" + +#: .././mainwin.py:5496 +msgid "Auto _open" +msgstr "" + +#: .././mainwin.py:5509 +msgid "_Download on start" +msgstr "" + +#: .././mainwin.py:5522 +msgid "Download on _stop" +msgstr "" + +#: .././mainwin.py:5538 +msgid "Not a _livestream" +msgstr "" + +#: .././mainwin.py:5548 .././config.py:5248 +msgid "_Livestream" +msgstr "" + +#: .././mainwin.py:6394 +msgid "" +"TRANSLATOR'S NOTE: V = number of videos B = (number of videos) bookmarked D " +"= downloaded F = favourite L = live/livestream N = new W = in waiting list E " +"= (number of) errors W = warnings" +msgstr "" + +#: .././mainwin.py:6401 +msgid "V:" +msgstr "" + +#: .././mainwin.py:6402 +msgid "B:" +msgstr "" + +#: .././mainwin.py:6403 +msgid "D:" +msgstr "" + +#: .././mainwin.py:6404 +msgid "F:" +msgstr "" + +#: .././mainwin.py:6405 +msgid "L:" +msgstr "" + +#: .././mainwin.py:6406 +msgid "N:" +msgstr "" + +#: .././mainwin.py:6407 .././mainwin.py:6418 +msgid "W:" +msgstr "" + +#: .././mainwin.py:6417 +msgid "E:" +msgstr "" + +#: .././mainwin.py:7444 .././mainwin.py:8122 +msgid "Waiting" +msgstr "" + +#: .././mainwin.py:8546 +msgid "Disable automatic copy/paste" +msgstr "" + +#: .././mainwin.py:8637 +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:8644 +msgid "Thread" +msgstr "" + +#: .././mainwin.py:8647 +msgid "_Summary" +msgstr "" + +#: .././mainwin.py:9175 +msgid "Tartube error" +msgstr "" + +#: .././mainwin.py:9228 +msgid "Tartube warning" +msgstr "" + +#: .././mainwin.py:9261 +msgid "_Errors" +msgstr "" + +#: .././mainwin.py:9265 +msgid "Warnings" +msgstr "" + +#: .././mainwin.py:13415 +#, python-brace-format +msgid "The channel contains {0} items, so this action may take a while" +msgstr "" + +#: .././mainwin.py:13422 +#, python-brace-format +msgid "The playlist contains {0} items, so this action may take a while" +msgstr "" + +#: .././mainwin.py:13429 +#, python-brace-format +msgid "The folder contains {0} items, so this action may take a while" +msgstr "" + +#: .././mainwin.py:13809 .././mainwin.py:14690 +msgid "From channel:" +msgstr "" + +#: .././mainwin.py:13811 .././mainwin.py:14692 +msgid "From playlist:" +msgstr "" + +#: .././mainwin.py:13813 .././mainwin.py:14694 +msgid "From folder:" +msgstr "" + +#: .././mainwin.py:13839 +msgid "Livestream has not started yet" +msgstr "" + +#: .././mainwin.py:13848 .././mainwin.py:13854 .././mainwin.py:14741 +#: .././mainwin.py:14748 +msgid "Duration:" +msgstr "" + +#: .././mainwin.py:13854 .././mainwin.py:13860 .././mainwin.py:13869 +#: .././mainwin.py:14748 .././mainwin.py:14755 .././mainwin.py:14765 +#: .././media.py:316 .././media.py:326 .././media.py:1510 .././media.py:1516 +#: .././media.py:1526 +msgid "unknown" +msgstr "" + +#: .././mainwin.py:13858 .././mainwin.py:13860 .././mainwin.py:14752 +#: .././mainwin.py:14754 +msgid "Size:" +msgstr "" + +#: .././mainwin.py:13867 .././mainwin.py:13869 .././mainwin.py:14762 +#: .././mainwin.py:14764 +msgid "Date:" +msgstr "" + +#: .././mainwin.py:14192 +msgid "Watch:" +msgstr "" + +#: .././mainwin.py:14248 +msgid "Temporary:" +msgstr "" + +#: .././mainwin.py:14291 +msgid "Marked:" +msgstr "" + +#: .././mainwin.py:14663 .././mainwin.py:14711 +msgid "Show the full description" +msgstr "" + +#: .././mainwin.py:14664 .././mainwin.py:14712 +msgid "More" +msgstr "" + +#: .././mainwin.py:14676 .././mainwin.py:14720 +msgid "Show the short description" +msgstr "" + +#: .././mainwin.py:14677 .././mainwin.py:14721 +msgid "Less" +msgstr "" + +#: .././mainwin.py:14781 +msgid "Live:" +msgstr "" + +#: .././mainwin.py:14784 .././mainwin.py:14786 .././mainwin.py:14790 +#: .././mainwin.py:15000 .././mainwin.py:15002 .././mainwin.py:15006 +#: .././mainwin.py:15446 +msgid "Notify" +msgstr "" + +#: .././mainwin.py:14794 .././mainwin.py:15010 +msgid "When the livestream starts, notify the user" +msgstr "" + +#: .././mainwin.py:14805 .././mainwin.py:14807 .././mainwin.py:15016 +#: .././mainwin.py:15018 .././mainwin.py:15313 +msgid "Alarm" +msgstr "" + +#: .././mainwin.py:14811 .././mainwin.py:15022 +msgid "When the livestream starts, sound an alarm" +msgstr "" + +#: .././mainwin.py:14816 .././mainwin.py:14818 .././mainwin.py:15028 +#: .././mainwin.py:15030 .././mainwin.py:15491 +msgid "Open" +msgstr "" + +#: .././mainwin.py:14822 .././mainwin.py:15034 +msgid "When the livestream starts, open it" +msgstr "" + +#: .././mainwin.py:14827 .././mainwin.py:14829 .././mainwin.py:15040 +#: .././mainwin.py:15042 .././mainwin.py:15357 +msgid "D/L on start" +msgstr "" + +#: .././mainwin.py:14833 .././mainwin.py:15046 +msgid "When the livestream starts, download it" +msgstr "" + +#: .././mainwin.py:14838 .././mainwin.py:14840 .././mainwin.py:15052 +#: .././mainwin.py:15054 .././mainwin.py:15402 +msgid "D/L on stop" +msgstr "" + +#: .././mainwin.py:14844 .././mainwin.py:15058 +msgid "When the livestream stops, download it" +msgstr "" + +#: .././mainwin.py:14870 +msgid "Download this video" +msgstr "" + +#: .././mainwin.py:14881 +msgid "Watch in your media player" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:14882 .././mainwin.py:16049 +msgid "Player" +msgstr "" + +#: .././mainwin.py:14890 +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:14898 +msgid "Download and watch in your media player" +msgstr "" + +#: .././mainwin.py:14899 +msgid "Download & watch" +msgstr "" + +#: .././mainwin.py:14906 +msgid "Not downloaded" +msgstr "" + +#: .././mainwin.py:14930 +msgid "Watch on YouTube" +msgstr "" + +#: .././mainwin.py:14931 .././mainwin.py:16094 +msgid "YouTube" +msgstr "" + +#: .././mainwin.py:14943 +msgid "Watch on HookTube" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:14944 .././mainwin.py:15905 +msgid "HookTube" +msgstr "" + +#: .././mainwin.py:14953 +msgid "Watch on Invidious" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:14954 .././mainwin.py:15949 +msgid "Invidious" +msgstr "" + +#: .././mainwin.py:14968 +msgid "Watch on website" +msgstr "" + +#: .././mainwin.py:14969 .././mainwin.py:16096 +msgid "Website" +msgstr "" + +#. Links not clickable +#: .././mainwin.py:14979 +msgid "No link" +msgstr "" + +#: .././mainwin.py:15087 +msgid "Download to a temporary folder later" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15088 .././mainwin.py:15105 .././mainwin.py:15861 +msgid "Mark for download" +msgstr "" + +#: .././mainwin.py:15093 +msgid "Download to a temporary folder" +msgstr "" + +#: .././mainwin.py:15099 +msgid "Download to a temporary folder, then watch" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15100 .././mainwin.py:15107 .././mainwin.py:15818 +msgid "D/L and watch" +msgstr "" + +#. Archived/not archived +#: .././mainwin.py:15131 +msgid "Prevent automatic deletion of the video" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15135 .././mainwin.py:15139 .././mainwin.py:15535 +msgid "Archived" +msgstr "" + +#. Bookmarked/not bookmarked +#: .././mainwin.py:15144 +msgid "Show video in Bookmarks folder" +msgstr "" + +#: .././mainwin.py:15148 .././mainwin.py:15152 +msgid "Bookmarked" +msgstr "" + +#. Favourite/not favourite +#: .././mainwin.py:15157 +msgid "Show in Favourite Videos folder" +msgstr "Show in Favorite Videos folder" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15161 .././mainwin.py:15165 .././mainwin.py:15625 +msgid "Favourite" +msgstr "Favorite" + +#. New/not new +#: .././mainwin.py:15169 +msgid "Mark video as never watched" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15173 .././mainwin.py:15177 .././mainwin.py:15663 +msgid "New" +msgstr "" + +#. In waiting list/not in waiting list +#: .././mainwin.py:15182 +msgid "Show in Waiting Videos folder" +msgstr "" + +#: .././mainwin.py:15185 +msgid "In waiting list" +msgstr "" + +#: .././mainwin.py:15189 +msgid "In Waiting list" +msgstr "" + +#: .././mainwin.py:15308 +msgid "Undo alarm" +msgstr "" + +#: .././mainwin.py:15352 .././mainwin.py:15397 +msgid "Don't D/L" +msgstr "" + +#: .././mainwin.py:15441 +msgid "Undo notify" +msgstr "" + +#: .././mainwin.py:15486 +msgid "Undo open" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15580 +msgid "Not bookmarked" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15708 +msgid "Not in waiting list" +msgstr "" + +#: .././mainwin.py:16724 +msgid "Tartube failed to start because:" +msgstr "" + +#: .././mainwin.py:16733 +msgid "If you don't know how to resolve this error, please contact the authors" +msgstr "" + +#: .././mainwin.py:16738 +msgid "here" +msgstr "" + +#. 'OK' button +#: .././mainwin.py:16741 .././mainwin.py:19027 .././config.py:426 +#: .././config.py:1602 +msgid "OK" +msgstr "" + +#: .././mainwin.py:16792 .././mainwin.py:19804 .././mainwin.py:19899 +msgid "Welcome to Tartube!" +msgstr "" + +#: .././mainwin.py:16924 +msgid "Add channel" +msgstr "" + +#: .././mainwin.py:16943 +msgid "Enter the channel name" +msgstr "" + +#: .././mainwin.py:16948 +msgid "(Use the channel's real name or a customised name)" +msgstr "(Use the channel's real name or a customized name)" + +#: .././mainwin.py:16956 +msgid "Copy and paste a link to the channel" +msgstr "" + +#: .././mainwin.py:17003 +msgid "(Optional) Add this channel inside a folder" +msgstr "" + +#: .././mainwin.py:17033 +msgid "I want to download videos from this channel automatically" +msgstr "" + +#: .././mainwin.py:17040 .././mainwin.py:17327 .././mainwin.py:17535 +msgid "Don't download anything, just check for new videos" +msgstr "" + +#: .././mainwin.py:17228 +msgid "Add folder" +msgstr "" + +#: .././mainwin.py:17247 +msgid "Enter the folder name" +msgstr "" + +#: .././mainwin.py:17290 +msgid "(Optional) Add this folder inside another folder" +msgstr "" + +#: .././mainwin.py:17321 +msgid "I want to download videos from this folder automatically" +msgstr "" + +#: .././mainwin.py:17419 +msgid "Add playlist" +msgstr "" + +#: .././mainwin.py:17438 +msgid "Enter the playlist name" +msgstr "" + +#: .././mainwin.py:17443 +msgid "(Use the playlist's real name or a customised name)" +msgstr "(Use the playlist's real name or a customized name)" + +#: .././mainwin.py:17451 +msgid "Copy and paste a link to the playlist" +msgstr "" + +#: .././mainwin.py:17498 +msgid "(Optional) Add this playlist inside a folder" +msgstr "" + +#: .././mainwin.py:17528 +msgid "I want to download videos from this playlist automatically" +msgstr "" + +#: .././mainwin.py:17725 +msgid "Add videos" +msgstr "" + +#: .././mainwin.py:17744 +msgid "Copy and paste the links to one or more videos" +msgstr "" + +#: .././mainwin.py:17750 +msgid "Links containing multiple videos will be converted to a channel" +msgstr "" + +#: .././mainwin.py:17757 +msgid "Links containing multiple videos will be converted to a playlist" +msgstr "" + +#: .././mainwin.py:17764 +msgid "Links containing multiple videos will be downloaded separately" +msgstr "" + +#: .././mainwin.py:17771 +msgid "Links containing multiple videos will not be downloaded at all" +msgstr "" + +#: .././mainwin.py:17853 +msgid "Add the videos to this folder" +msgstr "" + +#: .././mainwin.py:17883 +msgid "I want to download these videos automatically" +msgstr "" + +#: .././mainwin.py:17889 +msgid "Don't download anything, just check the videos" +msgstr "" + +#: .././mainwin.py:18054 +msgid "Select a date" +msgstr "" + +#: .././mainwin.py:18160 +msgid "Delete channel" +msgstr "" + +#: .././mainwin.py:18162 +msgid "Delete playlist" +msgstr "" + +#: .././mainwin.py:18164 +msgid "Delete folder" +msgstr "" + +#: .././mainwin.py:18167 +msgid "Empty channel" +msgstr "" + +#: .././mainwin.py:18169 +msgid "Empty playlist" +msgstr "" + +#: .././mainwin.py:18171 +msgid "Empty folder" +msgstr "" + +#: .././mainwin.py:18205 +msgid "This channel does not contain any videos" +msgstr "" + +#: .././mainwin.py:18207 +msgid "This playlist does not contain any videos" +msgstr "" + +#: .././mainwin.py:18209 +msgid "This folder doesn't contain anything" +msgstr "" + +#: .././mainwin.py:18215 +msgid "(but there might be some files in Tartube's data folder)" +msgstr "" + +#: .././mainwin.py:18228 +msgid "This channel contains:" +msgstr "" + +#: .././mainwin.py:18230 +msgid "This playlist contains:" +msgstr "" + +#: .././mainwin.py:18232 +msgid "This folder contains:" +msgstr "" + +#: .././mainwin.py:18239 +msgid "1 folder" +msgstr "" + +#: .././mainwin.py:18241 +#, python-brace-format +msgid "{0} folders" +msgstr "" + +#: .././mainwin.py:18248 +msgid "1 channel" +msgstr "" + +#: .././mainwin.py:18250 +#, python-brace-format +msgid "{0} channels" +msgstr "" + +#: .././mainwin.py:18257 +msgid "1 playlist" +msgstr "" + +#: .././mainwin.py:18259 +#, python-brace-format +msgid "{0} playlists" +msgstr "" + +#: .././mainwin.py:18266 .././mainwin.py:18691 +msgid "1 video" +msgstr "" + +#: .././mainwin.py:18268 .././mainwin.py:18694 +#, python-brace-format +msgid "{0} videos" +msgstr "" + +#: .././mainwin.py:18281 +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:18287 +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:18293 +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:18302 +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:18308 +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:18314 +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:18331 +msgid "Just remove the channel from this list" +msgstr "" + +#: .././mainwin.py:18333 +msgid "Just remove the playlist from this list" +msgstr "" + +#: .././mainwin.py:18335 +msgid "Just remove the folder from this list" +msgstr "" + +#: .././mainwin.py:18340 +msgid "Just empty the channel in this list" +msgstr "" + +#: .././mainwin.py:18342 +msgid "Just empty the playlist in this list" +msgstr "" + +#: .././mainwin.py:18344 +msgid "Just empty the folder in this list" +msgstr "" + +#: .././mainwin.py:18350 +msgid "Delete all files" +msgstr "" + +#: .././mainwin.py:18402 +msgid "Export from database" +msgstr "" + +#: .././mainwin.py:18426 +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:18433 +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:18449 +msgid "Choose what should be included:" +msgstr "" + +#: .././mainwin.py:18457 +msgid "Include lists of videos" +msgstr "" + +#: .././mainwin.py:18462 +msgid "Include channels" +msgstr "" + +#: .././mainwin.py:18467 +msgid "Include playlists" +msgstr "" + +#: .././mainwin.py:18472 +msgid "Preserve folder structure" +msgstr "" + +#: .././mainwin.py:18480 +msgid "Export as plain text" +msgstr "" + +#: .././mainwin.py:18566 +msgid "Import into database" +msgstr "" + +#: .././mainwin.py:18589 +msgid "Choose which items to import" +msgstr "" + +#: .././mainwin.py:18610 +msgid "Import" +msgstr "" + +#: .././mainwin.py:18626 +msgid "Name" +msgstr "" + +#: .././mainwin.py:18646 +msgid "Import videos" +msgstr "" + +#: .././mainwin.py:18651 +msgid "Merge channels/playlists/folders" +msgstr "" + +#. Bottom strip +#: .././mainwin.py:18654 .././mainwin.py:20527 +msgid "Select all" +msgstr "" + +#: .././mainwin.py:18659 +msgid "Unselect all" +msgstr "" + +#: .././mainwin.py:18921 +msgid "Mount drive" +msgstr "" + +#: .././mainwin.py:18945 +msgid "The Tartube data folder is set to:" +msgstr "" + +#: .././mainwin.py:18958 +msgid "...but this folder doesn't exist" +msgstr "" + +#: .././mainwin.py:18961 +msgid "...but Tartube cannot write to this folder" +msgstr "" + +#: .././mainwin.py:18971 +msgid "I have mounted the drive, please try again" +msgstr "" + +#: .././mainwin.py:18977 +msgid "Use this data folder:" +msgstr "" + +#: .././mainwin.py:19004 +msgid "Select a different data folder" +msgstr "" + +#: .././mainwin.py:19010 +msgid "Use the default data folder" +msgstr "" + +#: .././mainwin.py:19016 +msgid "Shut down Tartube" +msgstr "" + +#. 'Cancel' button +#: .././mainwin.py:19023 .././config.py:435 +msgid "Cancel" +msgstr "" + +#: .././mainwin.py:19149 +msgid "The folder still doesn't exist. Please try a different option" +msgstr "" + +#: .././mainwin.py:19216 +msgid "Stale lockfile" +msgstr "" + +#: .././mainwin.py:19253 +msgid "" +"Failed to load the Tartube database file, because another instance of " +"Tartube seems to be using it" +msgstr "" + +#: .././mainwin.py:19260 +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)" +msgstr "" + +#: .././mainwin.py:19265 +msgid "If you are not sure, then click 'No'" +msgstr "" + +#: .././mainwin.py:19273 +msgid "Yes, I'm sure" +msgstr "" + +#: .././mainwin.py:19280 +msgid "No, I'm not sure" +msgstr "" + +#: .././mainwin.py:19374 +msgid "Rename channel" +msgstr "" + +#: .././mainwin.py:19376 +msgid "Rename playlist" +msgstr "" + +#: .././mainwin.py:19378 +msgid "Rename folder" +msgstr "" + +#: .././mainwin.py:19402 +msgid "Set the new name for the channel:" +msgstr "" + +#: .././mainwin.py:19404 +msgid "Set the new name for the playlist:" +msgstr "" + +#: .././mainwin.py:19406 +msgid "Set the new name for the folder:" +msgstr "" + +#: .././mainwin.py:19412 +msgid "N.B. This procedure will modify your filesystem!\n" +msgstr "" + +#: .././mainwin.py:19473 +msgid "Set download destination" +msgstr "" + +#: .././mainwin.py:19498 +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:19503 +msgid "" +"This playlist can store its videos in its own system folder, or it can store " +"them in a different folder" +msgstr "" + +#: .././mainwin.py:19508 +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:19516 +msgid "Choose a different system folder if:" +msgstr "" + +#: .././mainwin.py:19519 +msgid "" +"1. You want to add a channel and its playlists, without downloading the same " +"video twice" +msgstr "" + +#: .././mainwin.py:19526 +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:19539 +msgid "Use this channel's own folder" +msgstr "" + +#: .././mainwin.py:19541 +msgid "Use this playlist's own folder" +msgstr "" + +#: .././mainwin.py:19543 +msgid "Use this folder's own system folder" +msgstr "" + +#: .././mainwin.py:19834 +msgid "Tartube's data folder will be:" +msgstr "" + +#: .././mainwin.py:19849 +msgid "Use this folder" +msgstr "" + +#: .././mainwin.py:19854 +msgid "Choose a different folder" +msgstr "" + +#: .././mainwin.py:19930 +msgid "Click OK to create a folder in which Tartube can store its videos" +msgstr "" + +#: .././mainwin.py:19937 +msgid "" +"If you have used Tartube before, you can select an existing folder instead " +"of creating a new one" +msgstr "" + +#: .././mainwin.py:19992 +msgid "Set nickname" +msgstr "" + +#: .././mainwin.py:20017 +#, python-brace-format +msgid "" +"Set a nickname for the channel '{0}' (or leave it blank to reset the " +"nickname)" +msgstr "" + +#: .././mainwin.py:20022 +#, python-brace-format +msgid "" +"Set a nickname for the playlist '{0}' (or leave it blank to reset the " +"nickname)" +msgstr "" + +#: .././mainwin.py:20027 +#, python-brace-format +msgid "" +"Set a nickname for the folder '{0}' (or leave it blank to reset the nickname)" +msgstr "" + +#: .././mainwin.py:20093 +msgid "Show system command" +msgstr "" + +#: .././mainwin.py:20137 +msgid "Update" +msgstr "" + +#: .././mainwin.py:20146 +msgid "Copy to clipboard" +msgstr "" + +#: .././mainwin.py:20320 +msgid "Test youtube-dl" +msgstr "" + +#: .././mainwin.py:20340 +msgid "URL of the video to download (optional)" +msgstr "" + +#: .././mainwin.py:20351 +msgid "youtube-dl command line options (optional)" +msgstr "" + +#: .././mainwin.py:20429 +msgid "Tidy up files" +msgstr "" + +#: .././mainwin.py:20431 +msgid "Tidy up channel" +msgstr "" + +#: .././mainwin.py:20433 +msgid "Tidy up playlist" +msgstr "" + +#: .././mainwin.py:20435 +msgid "Tidy up folder" +msgstr "" + +#: .././mainwin.py:20464 +msgid "Check that videos are not corrupted" +msgstr "" + +#: .././mainwin.py:20469 +msgid "Delete corrupted video files" +msgstr "" + +#: .././mainwin.py:20479 +msgid "Check that videos do/don't exist" +msgstr "" + +#: .././mainwin.py:20486 +msgid "" +"Delete downloaded video files (doesn't remove videos from Tartube's database)" +msgstr "" + +#: .././mainwin.py:20498 +msgid "Also delete all video/audio files with the same name" +msgstr "" + +#: .././mainwin.py:20507 +msgid "Delete all description files" +msgstr "" + +#: .././mainwin.py:20511 +msgid "Delete all metadata (JSON) files" +msgstr "" + +#: .././mainwin.py:20515 +msgid "Delete all annotation files" +msgstr "" + +#: .././mainwin.py:20519 +msgid "Delete all thumbnail files" +msgstr "" + +#: .././mainwin.py:20523 +msgid "Delete all youtube-dl archive files" +msgstr "" + +#: .././mainwin.py:20532 +msgid "Select none" +msgstr "" + +#. 'Reset' button +#: .././config.py:408 .././config.py:8714 +msgid "Reset" +msgstr "" + +#: .././config.py:412 +msgid "Reset changes without closing the window" +msgstr "" + +#. 'Apply' button +#: .././config.py:417 +msgid "Apply" +msgstr "" + +#: .././config.py:421 +msgid "Apply changes without closing the window" +msgstr "" + +#: .././config.py:429 +msgid "Apply changes" +msgstr "" + +#: .././config.py:438 +msgid "Cancel changes" +msgstr "" + +#: .././config.py:1279 +msgid "Listed as" +msgstr "" + +#: .././config.py:1291 +msgid "Contained in" +msgstr "" + +#: .././config.py:1350 +msgid "Channel URL" +msgstr "" + +#: .././config.py:1352 +msgid "Playlist URL" +msgstr "" + +#: .././config.py:1354 .././config.py:2370 +msgid "Video URL" +msgstr "" + +#: .././config.py:1384 +msgid "Download to" +msgstr "" + +#: .././config.py:1423 +msgid "Location" +msgstr "" + +#: .././config.py:1444 +msgid "Download _options" +msgstr "" + +#: .././config.py:1448 .././config.py:1968 .././config.py:2964 +#: .././config.py:3003 +msgid "Download options" +msgstr "" + +#: .././config.py:1452 +msgid "Apply download options" +msgstr "" + +#: .././config.py:1459 +msgid "Edit download options" +msgstr "" + +#: .././config.py:1466 +msgid "Remove download options" +msgstr "" + +#: .././config.py:1605 +msgid "Close this window" +msgstr "" + +#. Add this tab... +#: .././config.py:2156 .././config.py:5097 .././config.py:5556 +#: .././config.py:5915 .././config.py:6155 +msgid "_General" +msgstr "" + +#: .././config.py:2162 +msgid "General options" +msgstr "" + +#: .././config.py:2173 +msgid "These options have been applied to:" +msgstr "" + +#: .././config.py:2179 +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" +msgstr "" + +#: .././config.py:2243 +msgid "Show advanced download options" +msgstr "" + +#: .././config.py:2253 +msgid "Import general download options into this window" +msgstr "" + +#: .././config.py:2268 +msgid "Completely reset all download options to their default values" +msgstr "" + +#. Add this tab... +#: .././config.py:2282 +msgid "_Files" +msgstr "" + +#: .././config.py:2302 +msgid "File _names" +msgstr "" + +#: .././config.py:2310 +msgid "File name options" +msgstr "" + +#: .././config.py:2315 +msgid "Format for video file names" +msgstr "" + +#: .././config.py:2339 +msgid "youtube-dl file output template" +msgstr "" + +#: .././config.py:2359 +msgid "Add to template:" +msgstr "" + +#: .././config.py:2364 .././config.py:4986 +msgid "Video properties" +msgstr "" + +#: .././config.py:2366 +msgid "Video ID" +msgstr "" + +#: .././config.py:2367 +msgid "Video title" +msgstr "" + +#: .././config.py:2368 +msgid "Alternative video ID" +msgstr "" + +#: .././config.py:2369 +msgid "Secondary video title" +msgstr "" + +#: .././config.py:2371 +msgid "Video filename extension" +msgstr "" + +#: .././config.py:2372 +msgid "Video licence" +msgstr "Video license" + +#: .././config.py:2373 +msgid "Age restriction (years)" +msgstr "" + +#: .././config.py:2374 +msgid "Is a livestream" +msgstr "" + +#: .././config.py:2375 +msgid "Autonumber videos, starting at 0" +msgstr "" + +#: .././config.py:2377 +msgid "Creator/uploader" +msgstr "" + +#: .././config.py:2379 .././config.py:2380 +msgid "Full name of video uploader" +msgstr "" + +#: .././config.py:2381 +msgid "Nickname/ID of video uploader" +msgstr "" + +#: .././config.py:2382 +msgid "Channel name" +msgstr "" + +#: .././config.py:2383 +msgid "Channel ID" +msgstr "" + +#: .././config.py:2384 +msgid "Playlist name" +msgstr "" + +#: .././config.py:2385 +msgid "Playlist ID" +msgstr "" + +#: .././config.py:2386 +msgid "Video index in playlist" +msgstr "" + +#: .././config.py:2388 +msgid "Date/time/location" +msgstr "" + +#: .././config.py:2390 +msgid "Release date (YYYYMMDD)" +msgstr "" + +#: .././config.py:2391 +msgid "Release time (UNIX timestamp)" +msgstr "" + +#: .././config.py:2392 +msgid "Upload data (YYYYMMDD)" +msgstr "" + +#: .././config.py:2393 +msgid "Video length (seconds)" +msgstr "" + +#: .././config.py:2394 +msgid "Filming location" +msgstr "" + +#: .././config.py:2396 .././config.py:2398 +msgid "Video format" +msgstr "" + +#: .././config.py:2399 +msgid "youtube-dl format code" +msgstr "" + +#: .././config.py:2400 +msgid "Video width" +msgstr "" + +#: .././config.py:2401 +msgid "Video height" +msgstr "" + +#: .././config.py:2403 +msgid "Video frame rate" +msgstr "" + +#: .././config.py:2404 +msgid "Average video/audio bitrate (KiB/s)" +msgstr "" + +#: .././config.py:2405 +msgid "Average video bitrate (KiB/s)" +msgstr "" + +#: .././config.py:2406 +msgid "Average audio bitrate (KiB/s)" +msgstr "" + +#: .././config.py:2408 +msgid "Ratings/comments" +msgstr "" + +#: .././config.py:2410 +msgid "Number of views" +msgstr "" + +#: .././config.py:2411 +msgid "Number of positive ratings" +msgstr "" + +#: .././config.py:2412 +msgid "Number of negative ratings" +msgstr "" + +#: .././config.py:2413 +msgid "Average rating" +msgstr "" + +#: .././config.py:2414 +msgid "Number of reposts" +msgstr "" + +#: .././config.py:2415 +msgid "Number of comments" +msgstr "" + +#: .././config.py:2451 +msgid "Add" +msgstr "" + +#. Add this tab... +#: .././config.py:2479 .././config.py:6495 +msgid "_Filesystem" +msgstr "" + +#: .././config.py:2489 +msgid "Filesystem options" +msgstr "" + +#: .././config.py:2494 +msgid "Restrict filenames to ASCII characters" +msgstr "" + +#: .././config.py:2500 +msgid "Use the server's file modification time" +msgstr "" + +#: .././config.py:2507 +msgid "Filesystem overrides" +msgstr "" + +#: .././config.py:2512 +msgid "Download all videos into this folder" +msgstr "" + +#: .././config.py:2566 +msgid "_Write files" +msgstr "" + +#: .././config.py:2572 +msgid "Write other file options" +msgstr "" + +#: .././config.py:2577 +msgid "Write video's description to a .description file" +msgstr "" + +#: .././config.py:2583 +msgid "Write video's metadata to an .info.json file" +msgstr "" + +#: .././config.py:2589 +msgid "Write video's annotations to an .annotations.xml file" +msgstr "" + +#: .././config.py:2595 +msgid "Write the video's thumbnail to the same folder" +msgstr "" + +#: .././config.py:2609 +msgid "_Keep files" +msgstr "" + +#: .././config.py:2615 +msgid "Options during real (not simulated) downloads" +msgstr "" + +#: .././config.py:2621 .././config.py:2652 +msgid "Keep the description file after Tartube shuts down" +msgstr "" + +#: .././config.py:2627 .././config.py:2658 +msgid "Keep the metadata file after Tartube shuts down" +msgstr "" + +#: .././config.py:2633 .././config.py:2664 +msgid "Keep the annotations file after Tartube shuts down" +msgstr "" + +#: .././config.py:2639 .././config.py:2670 +msgid "Keep the thumbnail file after Tartube shuts down" +msgstr "" + +#: .././config.py:2646 +msgid "Options during simulated (not real) downloads" +msgstr "" + +#. Add this tab... +#: .././config.py:2684 +msgid "F_ormats" +msgstr "" + +#: .././config.py:2703 +msgid "_Preferred" +msgstr "" + +#: .././config.py:2711 +msgid "Preferred format options" +msgstr "" + +#: .././config.py:2717 +msgid "Recognised video/audio formats" +msgstr "" + +#: .././config.py:2728 +msgid "Add format" +msgstr "" + +#: .././config.py:2734 +msgid "List of preferred formats" +msgstr "" + +#: .././config.py:2751 +msgid "Remove format" +msgstr "" + +#. Add this tab... +#: .././config.py:2811 .././config.py:3521 +msgid "_Advanced" +msgstr "" + +#: .././config.py:2820 +msgid "Multiple format options" +msgstr "" + +#: .././config.py:2829 +msgid "" +"Multiple formats will not be downloaded, because youtube-dl is creating an " +"archive file" +msgstr "" + +#: .././config.py:2832 +msgid "The archive file can be disabled in the System Preferences window" +msgstr "" + +#: .././config.py:2841 +msgid "" +"For each video, download the first available format from the preferred list" +msgstr "" + +#: .././config.py:2855 +msgid "" +"From the preferred list, download the first format that's available for all " +"videos" +msgstr "" + +#: .././config.py:2869 +msgid "For each video, download all available formats from the preferred list" +msgstr "" + +#: .././config.py:2882 +msgid "Download all available formats for all videos" +msgstr "" + +#: .././config.py:2915 +msgid "Other format options" +msgstr "" + +#: .././config.py:2920 +msgid "Prefer free video formats, unless one is specified above" +msgstr "" + +#: .././config.py:2926 +msgid "Do not download DASH-related data for YouTube videos" +msgstr "" + +#: .././config.py:2933 +msgid "If a merge is required after post-processing, output to this format" +msgstr "" + +#. Add this tab... +#: .././config.py:2958 .././config.py:2977 .././config.py:7886 +msgid "_Downloads" +msgstr "" + +#: .././config.py:3020 +msgid "_Playlists" +msgstr "" + +#: .././config.py:3035 +msgid "_Size limits" +msgstr "" + +#: .././config.py:3049 +msgid "_Dates" +msgstr "" + +#: .././config.py:3061 +msgid "_Views" +msgstr "" + +#: .././config.py:3074 +msgid "_Filtering" +msgstr "" + +#: .././config.py:3088 +msgid "_External" +msgstr "" + +#: .././config.py:3100 +msgid "_Sound only" +msgstr "" + +#: .././config.py:3105 +msgid "Sound only options" +msgstr "" + +#: .././config.py:3111 +msgid "" +"Download each video, extract the sound, and then discard the original videos" +msgstr "" + +#: .././config.py:3116 +msgid "(requires that FFmpeg or AVConv is installed on your system)" +msgstr "" + +#: .././config.py:3126 +msgid "Use this audio format:" +msgstr "" + +#: .././config.py:3141 +msgid "Use this audio quality:" +msgstr "" + +#: .././config.py:3147 .././config.py:3220 +msgid "High" +msgstr "" + +#: .././config.py:3148 .././config.py:3221 +msgid "Medium" +msgstr "" + +#: .././config.py:3149 .././config.py:3222 +msgid "Low" +msgstr "" + +#: .././config.py:3167 +msgid "_Post-process" +msgstr "" + +#: .././config.py:3173 .././config.py:3490 +msgid "Post-processing options" +msgstr "" + +#: .././config.py:3179 +msgid "Post-process video files to convert them to audio-only files" +msgstr "" + +#: .././config.py:3186 +msgid "Prefer avconv over ffmpeg" +msgstr "" + +#: .././config.py:3194 +msgid "Prefer ffmpeg over avconv (default)" +msgstr "" + +#: .././config.py:3202 +msgid "Audio format of the post-processed file" +msgstr "" + +#: .././config.py:3215 +msgid "Audio quality of the post-processed file" +msgstr "" + +#: .././config.py:3232 +msgid "Encode video to another format, if necessary" +msgstr "" + +#: .././config.py:3244 +msgid "Arguments to pass to post-processor" +msgstr "" + +#: .././config.py:3254 +msgid "Keep original file after processing it" +msgstr "" + +#: .././config.py:3261 +msgid "Merge subtitles file with video (.mp4 only)" +msgstr "" + +#: .././config.py:3272 +msgid "Embed thumbnail in audio file as cover art" +msgstr "" + +#: .././config.py:3278 +msgid "Write metadata to the video file" +msgstr "" + +#: .././config.py:3284 +msgid "Automatically correct known faults of the file" +msgstr "" + +#: .././config.py:3290 +msgid "Do nothing" +msgstr "" + +#: .././config.py:3291 +msgid "Warn, but do nothing" +msgstr "" + +#: .././config.py:3292 +msgid "Fix if possible, otherwise warn" +msgstr "" + +#. Add this tab... +#: .././config.py:3309 +msgid "S_ubtitles" +msgstr "" + +#: .././config.py:3326 +msgid "_Options" +msgstr "" + +#: .././config.py:3330 +msgid "Subtitles options" +msgstr "" + +#: .././config.py:3336 +msgid "Don't download the subtitles file" +msgstr "" + +#: .././config.py:3347 +msgid "Download the automatic subtitles file (YouTube only)" +msgstr "" + +#: .././config.py:3359 +msgid "Download all available subtitles files" +msgstr "" + +#: .././config.py:3371 +msgid "Download subtitles file for these languages:" +msgstr "" + +#: .././config.py:3394 +msgid "Add language" +msgstr "" + +#: .././config.py:3407 +msgid "Remove language" +msgstr "" + +#: .././config.py:3465 +msgid "_More options" +msgstr "" + +#: .././config.py:3471 +msgid "Subtitle format options" +msgstr "" + +#: .././config.py:3477 +msgid "Preferred subtitle format(s), e.g. 'srt', 'vtt', 'srt/ass/vtt/lrc/best'" +msgstr "" + +#: .././config.py:3495 +msgid "Applies to .mp4 videos only; requires FFmpeg/AVConv" +msgstr "" + +#: .././config.py:3502 +msgid "During post-processing, merge subtitles file with video" +msgstr "" + +#: .././config.py:3541 +msgid "_Authentication" +msgstr "" + +#: .././config.py:3549 +msgid "Authentication options" +msgstr "" + +#: .././config.py:3554 +msgid "Username with which to log in" +msgstr "" + +#: .././config.py:3564 +msgid "Password with which to log in" +msgstr "" + +#: .././config.py:3574 +msgid "Password required for this URL" +msgstr "" + +#: .././config.py:3584 +msgid "Two-factor authentication code" +msgstr "" + +#: .././config.py:3594 +msgid "Use .netrc authentication data" +msgstr "" + +#: .././config.py:3607 +msgid "_Network" +msgstr "" + +#: .././config.py:3613 +msgid "Network options" +msgstr "" + +#: .././config.py:3618 +msgid "Use this HTTP/HTTPS proxy" +msgstr "" + +#: .././config.py:3628 +msgid "Time to wait for socket connection, before giving up" +msgstr "" + +#: .././config.py:3638 +msgid "Bind with this Client-side IP address" +msgstr "" + +#: .././config.py:3648 +msgid "Connect using IPv4 only" +msgstr "" + +#: .././config.py:3654 +msgid "Connect using IPv6 only" +msgstr "" + +#: .././config.py:3668 +msgid "_Geo-restriction" +msgstr "" + +#: .././config.py:3676 +msgid "Geo-restriction options" +msgstr "" + +#: .././config.py:3681 +msgid "Use this proxy to verify IP address" +msgstr "" + +#: .././config.py:3691 +msgid "Bypass using fake X-Forwarded-For HTTP header" +msgstr "" + +#: .././config.py:3697 +msgid "Don't bypass using fake HTTP header" +msgstr "" + +#: .././config.py:3703 +msgid "Bypass geo-restriction with ISO 3166-2 country code" +msgstr "" + +#: .././config.py:3713 +msgid "Bypass with explicit IP block in CIDR notation" +msgstr "" + +#: .././config.py:3736 +msgid "Workaround options" +msgstr "" + +#: .././config.py:3741 +msgid "Custom user agent for youtube-dl" +msgstr "" + +#: .././config.py:3751 +msgid "Custom referer if video access has restricted domain" +msgstr "" + +#: .././config.py:3761 +msgid "Force this encoding (experimental)" +msgstr "" + +#: .././config.py:3771 +msgid "Suppress HTTPS certificate validation" +msgstr "" + +#: .././config.py:3778 +msgid "" +"Use an unencrypted connection to retrieve information about videos (YouTube " +"only)" +msgstr "" + +#: .././config.py:3859 +msgid "Prefer HLS (HTTP Live Streaming)" +msgstr "" + +#: .././config.py:3865 +msgid "Prefer FFMpeg over native HLS downloader" +msgstr "" + +#: .././config.py:3871 +msgid "Include advertisements (experimental feature)" +msgstr "" + +#: .././config.py:3877 +msgid "Ignore errors and continue the download operation" +msgstr "" + +#: .././config.py:3883 +msgid "Number of retries" +msgstr "" + +#: .././config.py:3903 +msgid "Download videos suitable for this age" +msgstr "" + +#: .././config.py:3923 +msgid "Playlist options" +msgstr "" + +#: .././config.py:3929 +msgid "" +"youtube-dl treats channels and playlists the same way, so these options can " +"be used with both" +msgstr "" + +#: .././config.py:3936 +msgid "Start downloading playlist from index" +msgstr "" + +#: .././config.py:3947 +msgid "Stop downloading playlist at index" +msgstr "" + +#: .././config.py:3958 +msgid "Abort operation after downloading this many videos" +msgstr "" + +#: .././config.py:3969 +msgid "Abort downloading the playlist if an error occurs" +msgstr "" + +#: .././config.py:3975 +msgid "Download playlist in reverse order" +msgstr "" + +#: .././config.py:3981 +msgid "Download playlist in random order" +msgstr "" + +#: .././config.py:3996 +msgid "Video size limit options" +msgstr "" + +#: .././config.py:4001 +msgid "Minimum file size for video downloads" +msgstr "" + +#: .././config.py:4018 +msgid "Maximum file size for video downloads" +msgstr "" + +#: .././config.py:4045 +msgid "Video date options" +msgstr "" + +#: .././config.py:4050 +msgid "Only videos uploaded on this date" +msgstr "" + +#: .././config.py:4060 .././config.py:4080 .././config.py:4100 +#: .././config.py:8710 +msgid "Set" +msgstr "" + +#: .././config.py:4070 +msgid "Only videos uploaded before this date" +msgstr "" + +#: .././config.py:4090 +msgid "Only videos uploaded after this date" +msgstr "" + +#: .././config.py:4120 +msgid "Video views options" +msgstr "" + +#: .././config.py:4125 +msgid "Minimum number of views" +msgstr "" + +#: .././config.py:4136 +msgid "Maximum number of views" +msgstr "" + +#: .././config.py:4161 +msgid "Video filtering options" +msgstr "" + +#: .././config.py:4166 +msgid "Download only matching titles (regex or caseless substring)" +msgstr "" + +#: .././config.py:4177 +msgid "Don't download only matching titles (regex or caseless substring)" +msgstr "" + +#: .././config.py:4189 +msgid "Generic video filter, for example:" +msgstr "" + +#: .././config.py:4209 +msgid "External downloader options" +msgstr "" + +#: .././config.py:4214 +msgid "Use this external downloader" +msgstr "" + +#: .././config.py:4231 +msgid "Arguments to pass to external downloader" +msgstr "" + +#: .././config.py:4304 .././config.py:4696 +msgid "This procedure cannot be reversed. Are you sure you want to continue?" +msgstr "" + +#: .././config.py:4756 +msgid "When the window is re-opened, some download options will be hidden" +msgstr "" + +#: .././config.py:4765 +msgid "Show advanced download options (when window re-opens)" +msgstr "" + +#: .././config.py:4778 +msgid "When the window is re-opened, all download options will be visible" +msgstr "" + +#: .././config.py:4787 +msgid "Hide advanced download options (when window re-opens)" +msgstr "" + +#: .././config.py:5100 .././config.py:5559 .././config.py:5918 +msgid "General properties" +msgstr "" + +#: .././config.py:5131 +msgid "Always simulate download of this video" +msgstr "" + +#: .././config.py:5154 +msgid "Video has been downloaded" +msgstr "" + +#: .././config.py:5161 +msgid "File size" +msgstr "" + +#: .././config.py:5175 +msgid "Video is marked as unwatched" +msgstr "" + +#: .././config.py:5182 +msgid "Upload time" +msgstr "" + +#: .././config.py:5196 +msgid "Video is archived" +msgstr "" + +#: .././config.py:5203 +msgid "Video is bookmarked" +msgstr "" + +#: .././config.py:5210 +msgid "Receive time" +msgstr "" + +#: .././config.py:5224 +msgid "Video is favourite" +msgstr "Video is favorite" + +#: .././config.py:5231 +msgid "Video is in waiting list" +msgstr "" + +#: .././config.py:5254 +msgid "Livestream properties" +msgstr "" + +#: .././config.py:5259 +msgid "Livestream status" +msgstr "" + +#: .././config.py:5270 +msgid "Waiting to start" +msgstr "" + +#: .././config.py:5272 +msgid "Stream has started" +msgstr "" + +#: .././config.py:5274 +msgid "Not a livestream" +msgstr "" + +#: .././config.py:5281 +msgid "When the livestream starts, show a desktop notification" +msgstr "" + +#: .././config.py:5290 +msgid "When the livestream starts, play an alarm" +msgstr "" + +#: .././config.py:5300 +msgid "When the livestream starts, open it in the system's web browser" +msgstr "" + +#: .././config.py:5312 +msgid "When the livestream starts, begin downloading it immediately" +msgstr "" + +#: .././config.py:5323 .././config.py:8266 +msgid "When a livestream stops, download it (overwriting any earlier file)" +msgstr "" + +#: .././config.py:5339 +msgid "_Description" +msgstr "" + +#: .././config.py:5343 +msgid "Video description" +msgstr "" + +#: .././config.py:5364 .././config.py:5716 +msgid "Errors / Warnings" +msgstr "" + +#: .././config.py:5370 +msgid "Error messages produced the last time this video was checked/downloaded" +msgstr "" + +#: .././config.py:5385 +msgid "" +"Warning messages produced the last time this video was checked/downloaded" +msgstr "" + +#: .././config.py:5441 +msgid "Channel properties" +msgstr "" + +#: .././config.py:5444 +msgid "Playlist properties" +msgstr "" + +#: .././config.py:5577 +msgid "Always simulate download of videos in this channel" +msgstr "" + +#: .././config.py:5579 +msgid "Always simulate download of videos in this playlist" +msgstr "" + +#: .././config.py:5589 +msgid "Disable checking/downloading for this channel" +msgstr "" + +#: .././config.py:5591 +msgid "Disable checking/downloading for this playlist" +msgstr "" + +#: .././config.py:5601 +msgid "This channel is marked as a favourite" +msgstr "This channel is marked as a favorite" + +#: .././config.py:5603 +msgid "This playlist is marked as a favourite" +msgstr "This playlist is marked as a favorite" + +#: .././config.py:5613 +msgid "Total videos" +msgstr "" + +#: .././config.py:5637 +msgid "Favourite videos" +msgstr "Favorite videos" + +#: .././config.py:5649 +msgid "Downloaded videos" +msgstr "" + +#: .././config.py:5671 +msgid "_RSS feed" +msgstr "" + +#: .././config.py:5674 +msgid "RSS feed" +msgstr "" + +#: .././config.py:5680 +msgid "" +"If Tartube cannot detect the channel's RSS feed, you can enter the URL here" +msgstr "" + +#: .././config.py:5685 +msgid "" +"If Tartube cannot detect the playlist's RSS feed, you can enter the URL here" +msgstr "" + +#: .././config.py:5690 +msgid "(The feed is used to detect livestreams on compatible websites)" +msgstr "" + +#: .././config.py:5722 +msgid "" +"Error messages produced the last time this channel was checked/downloaded" +msgstr "" + +#: .././config.py:5727 +msgid "" +"Error messages produced the last time this playlist was checked/downloaded" +msgstr "" + +#: .././config.py:5745 +msgid "" +"Warning messages produced the last time this channel was checked/downloaded" +msgstr "" + +#: .././config.py:5750 +msgid "" +"Warning messages produced the last time this playlist was checked/downloaded" +msgstr "" + +#: .././config.py:5807 +msgid "Folder properties" +msgstr "" + +#: .././config.py:5935 +msgid "Always simulate download of videos" +msgstr "" + +#: .././config.py:5942 +msgid "Disable checking/downloading for this folder" +msgstr "" + +#: .././config.py:5949 +msgid "This folder is marked as a favourite" +msgstr "This folder is marked as a favorite" + +#: .././config.py:5956 +msgid "This folder is hidden" +msgstr "" + +#: .././config.py:5963 +msgid "This folder can't be deleted by the user" +msgstr "" + +#: .././config.py:5970 +msgid "This is a system-controlled folder" +msgstr "" + +#: .././config.py:5977 +msgid "Only videos can be added to this folder" +msgstr "" + +#: .././config.py:5984 +msgid "All contents deleted when Tartube shuts down" +msgstr "" + +#: .././config.py:6037 +msgid "System preferences" +msgstr "" + +#: .././config.py:6174 +msgid "_Language" +msgstr "" + +#: .././config.py:6179 +msgid "Language preferences" +msgstr "" + +#: .././config.py:6184 +msgid "Language" +msgstr "" + +#: .././config.py:6220 +msgid "_Stability" +msgstr "" + +#: .././config.py:6230 +msgid "Gtk library" +msgstr "" + +#: .././config.py:6235 +msgid "Current version of the system's Gtk library" +msgstr "" + +#: .././config.py:6250 +msgid "Gtk stability" +msgstr "" + +#: .././config.py:6266 +msgid "" +"Tartube uses the Gtk graphics library. This library is notoriously " +"unreliable and may even causes crashes." +msgstr "" + +#: .././config.py:6273 +msgid "" +"If stability is a problem, you can disable some minor cosmetic features." +msgstr "" + +#: .././config.py:6280 +msgid "" +"Tartube's functionality is not affected. You can do anything, even when " +"cosmetic features are disabled." +msgstr "" + +#: .././config.py:6289 +msgid "" +"Some features are disabled because this version of the library is broken" +msgstr "" + +#: .././config.py:6299 +msgid "Assume that Gtk is broken, and disable those features anyway" +msgstr "" + +#: .././config.py:6315 +msgid "_Modules" +msgstr "" + +#: .././config.py:6320 +msgid "Module availability" +msgstr "" + +#: .././config.py:6326 +msgid "feedparser module is available (required for detecting livestreams)" +msgstr "" + +#: .././config.py:6336 +msgid "moviepy module is available (finds the length of videos, if unknown)" +msgstr "" + +#: .././config.py:6346 +msgid "playsound module is available (sound an alarm when a livestream starts)" +msgstr "" + +#: .././config.py:6356 +msgid "" +"XDG module is available (saves the config file in the standard location)" +msgstr "" + +#: .././config.py:6366 +msgid "Module preferences" +msgstr "" + +#: .././config.py:6372 +msgid "" +"Use 'moviepy' module to get a video's duration, if not known (may be slow)" +msgstr "" + +#: .././config.py:6384 +msgid "Timeout applied when moviepy checks a video file" +msgstr "" + +#: .././config.py:6409 +msgid "_Video matching" +msgstr "" + +#: .././config.py:6417 +msgid "Video matching preferences" +msgstr "" + +#: .././config.py:6422 +msgid "When matching videos on the filesystem:" +msgstr "" + +#: .././config.py:6428 +msgid "The video names must match exactly" +msgstr "" + +#: .././config.py:6435 +msgid "The first # characters must match exactly" +msgstr "" + +#: .././config.py:6449 +msgid "Ignore the last # characters; the remaining name must match exactly" +msgstr "" + +#: .././config.py:6518 +msgid "_Device" +msgstr "" + +#: .././config.py:6523 +msgid "Device preferences" +msgstr "" + +#: .././config.py:6528 +msgid "Size of device (in Mb)" +msgstr "" + +#: .././config.py:6540 +msgid "Free space on device (in Mb)" +msgstr "" + +#: .././config.py:6552 +msgid "Warn user if disk space is less than" +msgstr "" + +#: .././config.py:6570 +msgid "Halt downloads if disk space is less than" +msgstr "" + +#: .././config.py:6609 +msgid "Configuration preferences" +msgstr "" + +#: .././config.py:6614 +msgid "Tartube configuration file loaded from:" +msgstr "" + +#: .././config.py:6642 +msgid "D_atabase" +msgstr "" + +#: .././config.py:6648 +msgid "Database preferences" +msgstr "" + +#: .././config.py:6653 +msgid "Tartube data folder" +msgstr "" + +#: .././config.py:6665 +msgid "Change" +msgstr "" + +#: .././config.py:6667 +msgid "Change to a different data folder" +msgstr "" + +#: .././config.py:6675 +msgid "Recent data folders" +msgstr "" + +#: .././config.py:6696 +msgid "Switch to the selected data folder" +msgstr "" + +#: .././config.py:6706 +msgid "Forget" +msgstr "" + +#: .././config.py:6709 +msgid "Remove the selected data folder from the list" +msgstr "" + +#: .././config.py:6718 +msgid "Forget all" +msgstr "" + +#: .././config.py:6721 +msgid "Forget every folder in this list (except the current one)" +msgstr "" + +#: .././config.py:6734 +msgid "Move the selected folder up the list" +msgstr "" + +#: .././config.py:6742 +msgid "Move the selected folder down the list" +msgstr "" + +#: .././config.py:6770 +msgid "" +"On startup, load the first database on the list (not the most recently-use " +"one)" +msgstr "" + +#: .././config.py:6780 +msgid "If one database is in use, try to load others" +msgstr "" + +#: .././config.py:6788 +msgid "Add new data directories to this list" +msgstr "" + +#: .././config.py:6827 +msgid "DB _Errors" +msgstr "" + +#: .././config.py:6835 +msgid "Database error preferences" +msgstr "" + +#: .././config.py:6840 +msgid "Check Tartube's database for inconsistencies, and fix them" +msgstr "" + +#: .././config.py:6844 +msgid "Check DB" +msgstr "" + +#: .././config.py:6859 +msgid "_Backups" +msgstr "" + +#: .././config.py:6863 +msgid "Backup preferences" +msgstr "" + +#: .././config.py:6868 +msgid "" +"When saving a database file, Tartube makes a backup copy of it (in case " +"something goes wrong)" +msgstr "" + +#: .././config.py:6877 +msgid "Delete the backup file as soon as the save procedure is finished" +msgstr "" + +#: .././config.py:6887 +msgid "Keep the backup file, replacing any previous backup file" +msgstr "" + +#: .././config.py:6898 +msgid "" +"Make a new backup file once per day, after the day's first save procedure" +msgstr "" + +#: .././config.py:6909 +msgid "Make a new backup file for every save procedure" +msgstr "" + +#: .././config.py:6950 +msgid "_Video deletion" +msgstr "" + +#: .././config.py:6958 +msgid "Automatic video deletion preferences" +msgstr "" + +#: .././config.py:6963 +msgid "Automatically delete downloaded videos after this many days" +msgstr "" + +#: .././config.py:6977 +msgid "...but only delete videos which have been watched" +msgstr "" + +#: .././config.py:7008 +msgid "_Temporary folders" +msgstr "" + +#: .././config.py:7014 +msgid "Temporary folder preferences" +msgstr "" + +#: .././config.py:7019 +msgid "Empty temporary folders when Tartube shuts down" +msgstr "" + +#: .././config.py:7028 +msgid "(N.B. Temporary folders are always emptied when Tartube starts up)" +msgstr "" + +#: .././config.py:7036 +msgid "Open temporary folders (on the desktop) when Tartube shuts down" +msgstr "" + +#. Add this tab... +#: .././config.py:7062 +msgid "_Windows" +msgstr "" + +#: .././config.py:7084 +msgid "_Main window" +msgstr "" + +#: .././config.py:7090 +msgid "Main window preferences" +msgstr "" + +#: .././config.py:7095 +msgid "Remember the size of the main window when shutting down" +msgstr "" + +#: .././config.py:7103 +msgid "Don't show labels in the toolbar" +msgstr "" + +#: .././config.py:7111 +msgid "Show tooltips for videos, channels, playlists and folders" +msgstr "" + +#: .././config.py:7120 +msgid "Show smaller icons in the Video Index (left side of the Videos Tab)" +msgstr "" + +#: .././config.py:7131 +msgid "" +"In the Video Index, show detailed statistics about the videos in each " +"channel / playlist / folder" +msgstr "" + +#: .././config.py:7142 +msgid "" +"After clicking on a folder, automatically expand/collapse the tree around it" +msgstr "" + +#: .././config.py:7153 +msgid "Expand the whole tree, not just the level beneath the clicked folder" +msgstr "" + +#: .././config.py:7174 +msgid "Disable the 'Download all' buttons in the toolbar and the Videos Tab" +msgstr "" + +#: .././config.py:7184 +msgid "When Tartube starts, automatically open the Classic Mode tab" +msgstr "" + +#: .././config.py:7202 +msgid "_Tabs" +msgstr "" + +#: .././config.py:7206 +msgid "Tab preferences" +msgstr "" + +#: .././config.py:7212 +msgid "" +"In the Videos Tab, show 'today' and 'yesterday' as the date, when possible" +msgstr "" + +#: .././config.py:7223 +msgid "In the Progress Tab, hide finished videos / channels / playlists" +msgstr "" + +#: .././config.py:7232 +msgid "In the Progress Tab, show results in reverse order" +msgstr "" + +#: .././config.py:7241 +msgid "In the Errors/Warnings Tab, don't reset the tab text when it is clicked" +msgstr "" + +#: .././config.py:7259 +msgid "_System tray" +msgstr "" + +#: .././config.py:7265 +msgid "System tray preferences" +msgstr "" + +#: .././config.py:7270 +msgid "Show icon in system tray" +msgstr "" + +#: .././config.py:7279 +msgid "Close to the tray, rather than closing the application" +msgstr "" + +#: .././config.py:7305 +msgid "_Dialogues" +msgstr "" + +#: .././config.py:7311 +msgid "Dialogue window preferences" +msgstr "" + +#: .././config.py:7316 +msgid "When adding channels/playlists, keep the dialogue window open" +msgstr "" + +#: .././config.py:7326 +msgid "When the dialogue window opens, add URLs from the system clipboard" +msgstr "" + +#: .././config.py:7354 +msgid "_Errors/Warnings" +msgstr "" + +#: .././config.py:7362 +msgid "Errors/Warnings tab preferences" +msgstr "" + +#: .././config.py:7367 +msgid "Show Tartube error messages" +msgstr "" + +#: .././config.py:7375 +msgid "Show Tartube warning messages" +msgstr "" + +#: .././config.py:7383 +msgid "Show server error messages" +msgstr "" + +#: .././config.py:7394 +msgid "Show server warning messages" +msgstr "" + +#: .././config.py:7406 +msgid "youtube-dl error/warning preferences" +msgstr "" + +#: .././config.py:7411 +msgid "" +"TRANSLATOR'S NOTE: These youtube-dl error messages are always in English" +msgstr "" + +#: .././config.py:7416 +msgid "Ignore 'Child process exited with non-zero code' errors" +msgstr "" + +#: .././config.py:7425 +msgid "Ignore 'Unable to download video data: HTTP Error 404' errors" +msgstr "" + +#: .././config.py:7434 +msgid "Ignore 'Did not get any data blocks' errors" +msgstr "" + +#: .././config.py:7443 +msgid "Ignore 'Requested formats are incompatible for merge' warnings" +msgstr "" + +#: .././config.py:7452 +msgid "Ignore 'No video formats found' errors" +msgstr "" + +#: .././config.py:7460 +msgid "Ignore 'There are no annotations to write' warnings" +msgstr "" + +#: .././config.py:7468 +msgid "Ignore 'Video doesn't have subtitles' warnings" +msgstr "" + +#: .././config.py:7484 +msgid "_Websites" +msgstr "" + +#: .././config.py:7492 +msgid "YouTube error/warning preferences" +msgstr "" + +#: .././config.py:7497 +msgid "Ignore YouTube copyright errors" +msgstr "" + +#: .././config.py:7505 +msgid "Ignore YouTube age-restriction errors" +msgstr "" + +#: .././config.py:7513 +msgid "Ignore YouTube deletion by uploader errors" +msgstr "" + +#: .././config.py:7522 +msgid "General preferences" +msgstr "" + +#: .././config.py:7528 +msgid "" +"Ignore any errors/warnings which match lines in this list (applies to all " +"websites)" +msgstr "" + +#: .././config.py:7541 +msgid "These are ordinary strings" +msgstr "" + +#: .././config.py:7548 +msgid "These are regular expressions (regexes)" +msgstr "" + +#. Add this tab... +#: .././config.py:7577 +msgid "_Scheduling" +msgstr "" + +#: .././config.py:7594 +msgid "_Start" +msgstr "" + +#: .././config.py:7600 +msgid "Scheduled start preferences" +msgstr "" + +#: .././config.py:7605 +msgid "Automatic 'Download all' operations" +msgstr "" + +#: .././config.py:7611 .././config.py:7652 +msgid "Disabled" +msgstr "" + +#: .././config.py:7612 .././config.py:7653 +msgid "Performed when Tartube starts" +msgstr "" + +#: .././config.py:7613 .././config.py:7654 +msgid "Performed at regular intervals" +msgstr "" + +#: .././config.py:7633 .././config.py:7674 +msgid "Time (in hours) between operations" +msgstr "" + +#: .././config.py:7646 +msgid "Automatic 'Check all' operations" +msgstr "" + +#: .././config.py:7688 +msgid "After an automatic 'Download/Check all' operation, shut down Tartube" +msgstr "" + +#: .././config.py:7718 +msgid "S_top" +msgstr "" + +#: .././config.py:7724 +msgid "Scheduled stop preferences" +msgstr "" + +#: .././config.py:7729 +msgid "Stop all download operations after this much time" +msgstr "" + +#: .././config.py:7777 +msgid "Stop all download operations after this many videos" +msgstr "" + +#: .././config.py:7804 +msgid "Stop all download operations after this much disk space" +msgstr "" + +#: .././config.py:7847 +msgid "" +"N.B. Disk space is estimated. This setting does not apply to simulated " +"downloads" +msgstr "" + +#: .././config.py:7892 +msgid "Download operation preferences" +msgstr "" + +#: .././config.py:7898 +msgid "Automatically update youtube-dl before every download operation" +msgstr "" + +#: .././config.py:7910 +msgid "" +"Automatically save files at the end of a download/update/refresh operation" +msgstr "" + +#: .././config.py:7921 +msgid "" +"When applying download options to something, clone the general download " +"options" +msgstr "" + +#: .././config.py:7932 +msgid "For simulated downloads, don't check a video in a folder more than once" +msgstr "" + +#: .././config.py:7949 +msgid "_Custom" +msgstr "" + +#: .././config.py:7954 +msgid "Custom download preferences" +msgstr "" + +#: .././config.py:7960 +msgid "" +"In custom downloads, download each video independently of its channel or " +"playlist" +msgstr "" + +#: .././config.py:7972 +msgid "In custom downloads, obtain a YouTube video from the original website" +msgstr "" + +#: .././config.py:7982 +msgid "In custom downloads, obtain the video from HookTube rather than YouTube" +msgstr "" + +#: .././config.py:7994 +msgid "" +"In custom downloads, obtain the video from Invidious rather than YouTube" +msgstr "" + +#: .././config.py:8005 +msgid "" +"In custom downloads, apply a delay after each video/channel/playlist is " +"download" +msgstr "" + +#: .././config.py:8015 +msgid "Maximum delay to apply (in minutes)" +msgstr "" + +#: .././config.py:8032 +msgid "Minimum delay to apply (in minutes; randomises the actual delay)" +msgstr "Minimum delay to apply (in minutes; randomizes the actual delay)" + +#: .././config.py:8102 +msgid "Livestream preferences (compatible websites only)" +msgstr "" + +#: .././config.py:8108 +msgid "Detect livestreams announced within this many days" +msgstr "" + +#: .././config.py:8123 +msgid "How often to check the status of livestreams (in minutes)" +msgstr "" + +#: .././config.py:8168 +msgid "Video Catalogue options" +msgstr "Video Catalog options" + +#: .././config.py:8173 +msgid "Show livestreams with a different background colour" +msgstr "Show livestreams with a different background color" + +#: .././config.py:8186 +msgid "Livestream actions (can be toggled for individual videos)" +msgstr "" + +#: .././config.py:8193 +msgid "(currently disabled on MS Windows)" +msgstr "" + +#: .././config.py:8198 +msgid "When a livestream starts, show a desktop notification" +msgstr "" + +#: .././config.py:8212 +msgid "When a livestream starts, sound an alarm" +msgstr "" + +#: .././config.py:8235 +msgid "Plays the selected sound effect" +msgstr "" + +#: .././config.py:8242 +msgid "When a livestream starts, open it in the system's web browser" +msgstr "" + +#: .././config.py:8254 +msgid "When a livestream starts, begin downloading it immediately" +msgstr "" + +#: .././config.py:8287 +msgid "_Notifications" +msgstr "" + +#: .././config.py:8293 +msgid "Desktop notification preferences" +msgstr "" + +#: .././config.py:8300 +msgid "" +"Show a dialogue window at the end of a download/update/refresh/info/tidy " +"operation" +msgstr "" + +#: .././config.py:8310 +msgid "" +"Show a desktop notification at the end of a download/update/refresh/info/" +"tidy operation" +msgstr "" + +#: .././config.py:8324 +msgid "" +"Don't notify the user at the end of a download/update/refresh/info/tidy " +"operation" +msgstr "" + +#: .././config.py:8359 +msgid "_URL flexibility" +msgstr "" + +#: .././config.py:8365 +msgid "URL flexibility preferences" +msgstr "" + +#: .././config.py:8372 +msgid "" +"If a video's URL represents a channel/playlist, not a video, don't download " +"it" +msgstr "" + +#: .././config.py:8381 +msgid "...or, download multiple videos into the containing folder" +msgstr "" + +#: .././config.py:8391 +msgid "...or, create a new channel, and download the videos into that" +msgstr "" + +#: .././config.py:8402 +msgid "...or, create a new playlist, and download the videos into that" +msgstr "" + +#: .././config.py:8441 +msgid "_Performance" +msgstr "" + +#: .././config.py:8449 +msgid "Performance limits" +msgstr "" + +#: .././config.py:8454 +msgid "Limit simultaneous downloads to" +msgstr "" + +#: .././config.py:8472 +msgid "Limit download speed to" +msgstr "" + +#: .././config.py:8498 +msgid "Overriding video format options, limit video resolution to" +msgstr "" + +#: .././config.py:8520 +msgid "Time-saving preferences" +msgstr "" + +#: .././config.py:8526 +msgid "" +"Stop checking/downloading a channel/playlist when it starts sending videos " +"we already have" +msgstr "" + +#: .././config.py:8537 +msgid "Stop after this many videos (when checking)" +msgstr "" + +#: .././config.py:8552 +msgid "Stop after this many videos (when downloading)" +msgstr "" + +#: .././config.py:8587 +msgid "youtube-dl preferences" +msgstr "" + +#: .././config.py:8593 +msgid "youtube-dl executable (system-dependent)" +msgstr "" + +#: .././config.py:8606 +msgid "Default path to youtube-dl executable" +msgstr "" + +#: .././config.py:8619 +msgid "Actual path to use" +msgstr "" + +#: .././config.py:8625 +msgid "Use default path" +msgstr "" + +#: .././config.py:8630 +msgid "Use local path" +msgstr "" + +#: .././config.py:8638 +msgid "Use PyPI path" +msgstr "" + +#: .././config.py:8665 +msgid "Shell command for update operations" +msgstr "" + +#: .././config.py:8692 +msgid "Post-processing preferences" +msgstr "" + +#: .././config.py:8697 +msgid "Path to the ffmpeg/avconv binary" +msgstr "" + +#: .././config.py:8720 +msgid "Install from main menu" +msgstr "" + +#: .././config.py:8726 +msgid "Other preferences" +msgstr "" + +#: .././config.py:8732 +msgid "" +"Allow youtube-dl to create its own archive file (so deleted videos are not " +"re-downloaded)" +msgstr "" + +#: .././config.py:8743 +msgid "" +"When checking videos, apply a 60-second timeout while fetching JSON data" +msgstr "" + +#. Add this tab... +#: .././config.py:8761 +msgid "Out_put" +msgstr "" + +#: .././config.py:8780 +msgid "_Output Tab" +msgstr "" + +#: .././config.py:8786 +msgid "Output Tab preferences" +msgstr "" + +#: .././config.py:8791 +msgid "Display youtube-dl system commands in the Output Tab" +msgstr "" + +#: .././config.py:8800 +msgid "Display output from youtube-dl's STDOUT in the Output Tab" +msgstr "" + +#: .././config.py:8809 .././config.py:8939 +msgid "...but don't write each video's JSON data" +msgstr "" + +#: .././config.py:8820 .././config.py:8950 +msgid "...but don't write each video's download progress" +msgstr "" + +#: .././config.py:8839 +msgid "Display output from youtube-dl's STDERR in the Output Tab" +msgstr "" + +#: .././config.py:8848 +msgid "Empty pages in the Output Tab at the start of every operation" +msgstr "" + +#: .././config.py:8858 +msgid "" +"Show a summary of active threads (changes are applied when Tartube restarts)" +msgstr "" + +#: .././config.py:8870 +msgid "During a refresh operation, show all matching videos in the Output Tab" +msgstr "" + +#: .././config.py:8881 +msgid "...also show all non-matching videos" +msgstr "" + +#: .././config.py:8910 +msgid "_Terminal window" +msgstr "" + +#: .././config.py:8916 +msgid "Terminal window preferences" +msgstr "" + +#: .././config.py:8921 +msgid "Write youtube-dl system commands to the terminal window" +msgstr "" + +#: .././config.py:8930 +msgid "Write output from youtube-dl's STDOUT to the terminal window" +msgstr "" + +#: .././config.py:8972 +msgid "Write output from youtube-dl's STDERR to the terminal window" +msgstr "" + +#: .././config.py:8991 +msgid "_Both" +msgstr "" + +#: .././config.py:8996 +msgid "" +"Special preferences (applies to both the Output Tab and the terminal window)" +msgstr "" + +#: .././config.py:9003 +msgid "Write verbose output (youtube-dl debugging mode)" +msgstr "" + +#: .././config.py:9762 +msgid "Are you sure you want to create a new database at this location?" +msgstr "" + +#: .././config.py:9869 +msgid "Are you sure you want to forget this database?" +msgstr "" + +#: .././config.py:9904 +msgid "Are you sure you want to forget all databases except the current one?" +msgstr "" + +#: .././config.py:10108 +msgid "No database exists at this location:" +msgstr "" + +#: .././config.py:10110 +msgid "Do you want to create a new one?" +msgstr "" + +#: .././config.py:10800 +msgid "The new setting will be applied when Tartube restarts" +msgstr "" + +#: .././config.py:11476 +msgid "Please select the FFmpeg executable" +msgstr "" + +#: .././config.py:12060 +msgid "Database file not loaded" +msgstr "" + +#: .././config.py:12095 +msgid "Database file loaded" +msgstr "" + +#: .././downloads.py:221 +msgid "D/L Manager:" +msgstr "" + +#: .././downloads.py:225 +msgid "Starting download operation" +msgstr "" + +#: .././downloads.py:253 +msgid "Workers: available:" +msgstr "" + +#: .././downloads.py:254 +msgid "total:" +msgstr "" + +#: .././downloads.py:284 +msgid "All threads finished" +msgstr "" + +#: .././downloads.py:306 .././downloads.py:874 .././downloads.py:925 +#: .././downloads.py:935 .././downloads.py:946 +msgid "Thread #" +msgstr "" + +#: .././downloads.py:307 +msgid "Downloading:" +msgstr "" + +#: .././downloads.py:334 +msgid "Downloads complete (or stopped)" +msgstr "" + +#: .././downloads.py:340 +msgid "Halting all workers" +msgstr "" + +#: .././downloads.py:349 +msgid "Join and collect threads" +msgstr "" + +#: .././downloads.py:875 +msgid "Assigned job:" +msgstr "" + +#: .././downloads.py:926 +msgid "Checking RSS feed" +msgstr "" + +#: .././downloads.py:936 +msgid "Job complete" +msgstr "" + +#: .././downloads.py:947 +msgid "Worker now available again" +msgstr "" + +#: .././downloads.py:1369 +msgid "Cannot download videos in a private folder" +msgstr "" + +#: .././downloads.py:2337 +msgid "Download did not start" +msgstr "" + +#: .././downloads.py:2345 .././info.py:352 .././updates.py:293 +#: .././updates.py:448 +msgid "Child process exited with non-zero code: {}" +msgstr "" + +#: .././downloads.py:2414 .././downloads.py:3198 +msgid "" +"This video has a URL that points to a channel or a playlist, not a video" +msgstr "" + +#: .././downloads.py:3090 +msgid "Simulated download of:" +msgstr "" + +#: .././formats.py:66 +msgid "seconds" +msgstr "" + +#: .././formats.py:67 +msgid "minutes" +msgstr "" + +#: .././formats.py:68 +msgid "hours" +msgstr "" + +#: .././formats.py:69 +msgid "days" +msgstr "" + +#: .././formats.py:70 +msgid "weeks" +msgstr "" + +#: .././formats.py:71 +msgid "years" +msgstr "" + +#. System folder names +#: .././formats.py:748 +msgid "All Videos" +msgstr "" + +#: .././formats.py:749 +msgid "Bookmarks" +msgstr "" + +#: .././formats.py:750 +msgid "Favourite Videos" +msgstr "Favorite Videos" + +#: .././formats.py:751 +msgid "Livestreams" +msgstr "" + +#: .././formats.py:752 +msgid "New Videos" +msgstr "" + +#: .././formats.py:753 +msgid "Waiting Videos" +msgstr "" + +#: .././formats.py:754 +msgid "Temporary Videos" +msgstr "" + +#: .././formats.py:755 +msgid "Unsorted Videos" +msgstr "" + +#: .././formats.py:760 +msgid "Update using default youtube-dl path" +msgstr "" + +#: .././formats.py:762 +msgid "Update using local youtube-dl path" +msgstr "" + +#: .././formats.py:764 +msgid "Update using pip" +msgstr "" + +#: .././formats.py:766 +msgid "Update using pip (omit --user option)" +msgstr "" + +#: .././formats.py:768 +msgid "Update using pip3" +msgstr "" + +#: .././formats.py:770 +msgid "Update using pip3 (omit --user option)" +msgstr "" + +#: .././formats.py:772 +msgid "Update using pip3 (recommended)" +msgstr "" + +#: .././formats.py:774 +msgid "Update using PyPI youtube-dl path" +msgstr "" + +#: .././formats.py:776 +msgid "Windows 32-bit update (recommended)" +msgstr "" + +#: .././formats.py:778 +msgid "Windows 64-bit update (recommended)" +msgstr "" + +#: .././formats.py:780 +msgid "youtube-dl updates are disabled" +msgstr "" + +#. Download operation stages +#: .././formats.py:784 +msgid "Queued" +msgstr "" + +#: .././formats.py:785 +msgid "Active" +msgstr "" + +#: .././formats.py:786 +msgid "Paused" +msgstr "" + +#. (not actually used) +#: .././formats.py:787 +msgid "Completed" +msgstr "" + +#. (not actually used) +#. Sub-stages of the 'Error' stage +#: .././formats.py:788 .././formats.py:799 +msgid "Error" +msgstr "" + +#. Sub-stages of the 'Active' stage +#: .././formats.py:790 +msgid "Pre-processing" +msgstr "" + +#: .././formats.py:791 +msgid "Downloading" +msgstr "" + +#: .././formats.py:792 +msgid "Post-processing" +msgstr "" + +#: .././formats.py:793 +msgid "Checking" +msgstr "" + +#. Sub-stages of the 'Completed' stage +#: .././formats.py:795 +msgid "Finished" +msgstr "" + +#: .././formats.py:796 +msgid "Warning" +msgstr "" + +#: .././formats.py:797 +msgid "Already downloaded" +msgstr "" + +#. (not actually used) +#: .././formats.py:800 +msgid "Stopped" +msgstr "" + +#: .././formats.py:801 +msgid "Filesize abort" +msgstr "" + +#: .././formats.py:811 +msgid "" +"TRANSLATOR'S NOTE: ID refers to a video's unique ID on the website, e.g. on " +"YouTube \"CS9OO0S5w2k\"" +msgstr "" + +#: .././formats.py:819 +msgid "Custom" +msgstr "" + +#: .././formats.py:820 +msgid "ID" +msgstr "" + +#: .././formats.py:821 +msgid "Title" +msgstr "" + +#: .././formats.py:822 +msgid "Quality" +msgstr "" + +#: .././formats.py:823 +msgid "Autonumber" +msgstr "" + +#: .././formats.py:835 +msgid "Any format" +msgstr "" + +#: .././info.py:186 +msgid "Starting info operation, testing youtube-dl with specified options" +msgstr "" + +#: .././info.py:195 +#, python-brace-format +msgid "Starting info operation, fetching list of video/audio formats for '{0}'" +msgstr "" + +#: .././info.py:202 +#, python-brace-format +msgid "Starting info operation, fetching list of subtitles for '{0}'" +msgstr "" + +#: .././info.py:343 +msgid "youtube-dl process did not start" +msgstr "" + +#: .././info.py:368 +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 +msgid "Child process did not start" +msgstr "" + +#: .././media.py:311 +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:314 .././media.py:1508 .././media.py:1524 +msgid "Source:" +msgstr "" + +#: .././media.py:322 +msgid "Location:" +msgstr "" + +#: .././media.py:333 +msgid "Download destination:" +msgstr "" + +#: .././media.py:1479 +msgid "" +"TRANSLATOR'S NOTE: WAITING = livestream not started, LIVE = livestream " +"started" +msgstr "" + +#: .././media.py:1484 +msgid "WAITING" +msgstr "" + +#: .././media.py:1486 +msgid "LIVE" +msgstr "" + +#: .././media.py:1496 .././refresh.py:272 .././refresh.py:540 +msgid "Channel:" +msgstr "" + +#: .././media.py:1498 .././refresh.py:274 .././refresh.py:542 +msgid "Playlist:" +msgstr "" + +#: .././media.py:1500 .././refresh.py:276 .././refresh.py:544 +msgid "Folder:" +msgstr "" + +#: .././media.py:1505 +msgid "TRANSLATOR'S NOTE 2: Source = video/channel/playlist URL" +msgstr "" + +#: .././media.py:1514 .././media.py:1531 +msgid "File:" +msgstr "" + +#: .././media.py:1965 +msgid "Today" +msgstr "" + +#: .././media.py:1967 +msgid "Yesterday" +msgstr "" + +#: .././refresh.py:149 +msgid "Starting refresh operation, analysing whole database" +msgstr "Starting refresh operation, analyzing whole database" + +#: .././refresh.py:158 +msgid "Starting refresh operation, analysing '{}'" +msgstr "Starting refresh operation, analyzing '{}'" + +#: .././refresh.py:202 +msgid "Refresh operation finished" +msgstr "" + +#: .././refresh.py:207 +msgid "Number of video files analysed:" +msgstr "Number of video files analyzed:" + +#: .././refresh.py:213 +msgid "Video files already in the database:" +msgstr "" + +#: .././refresh.py:219 +msgid "New videos found and added to the database:" +msgstr "" + +#: .././refresh.py:385 .././tidy.py:489 +msgid "Checking:" +msgstr "" + +#: .././refresh.py:419 .././refresh.py:592 +msgid "Match:" +msgstr "" + +#: .././refresh.py:437 +msgid "Non-match:" +msgstr "" + +#: .././refresh.py:485 +msgid "New video:" +msgstr "" + +#: .././refresh.py:491 .././refresh.py:598 +msgid "Total videos:" +msgstr "" + +#: .././refresh.py:492 .././refresh.py:599 +msgid "matched:" +msgstr "" + +#: .././refresh.py:493 +msgid "new:" +msgstr "" + +#: .././refresh.py:574 +msgid "Missing:" +msgstr "" + +#: .././refresh.py:600 +msgid "missing:" +msgstr "" + +#: .././tidy.py:215 +msgid "Starting tidy operation, tidying up whole data directory" +msgstr "" + +#: .././tidy.py:224 +#, python-brace-format +msgid "Starting tidy operation, tidying up '{0}'" +msgstr "" + +#: .././tidy.py:230 .././tidy.py:242 .././tidy.py:252 .././tidy.py:262 +#: .././tidy.py:274 .././tidy.py:284 .././tidy.py:294 .././tidy.py:304 +#: .././tidy.py:314 .././tidy.py:324 +msgid "YES" +msgstr "" + +#: .././tidy.py:232 .././tidy.py:244 .././tidy.py:254 .././tidy.py:264 +#: .././tidy.py:276 .././tidy.py:286 .././tidy.py:296 .././tidy.py:306 +#: .././tidy.py:316 .././tidy.py:326 +msgid "NO" +msgstr "" + +#: .././tidy.py:236 +msgid "Check videos are not corrupted:" +msgstr "" + +#: .././tidy.py:248 +msgid "Delete corrupted videos:" +msgstr "" + +#: .././tidy.py:258 +msgid "Check videos do/don't exist:" +msgstr "" + +#: .././tidy.py:268 +msgid "Delete all video files:" +msgstr "" + +#: .././tidy.py:280 +msgid "Delete other video/audio files:" +msgstr "" + +#: .././tidy.py:290 +msgid "Delete all description files:" +msgstr "" + +#: .././tidy.py:300 +msgid "Delete all metadata (JSON) files:" +msgstr "" + +#: .././tidy.py:310 +msgid "Delete all annotation files:" +msgstr "" + +#: .././tidy.py:320 +msgid "Delete all thumbnail files:" +msgstr "" + +#: .././tidy.py:330 +msgid "Delete youtube-dl archive files:" +msgstr "" + +#: .././tidy.py:366 +msgid "Tidy operation finished" +msgstr "" + +#: .././tidy.py:373 +msgid "Corrupted videos found:" +msgstr "" + +#: .././tidy.py:379 +msgid "Corrupted videos deleted:" +msgstr "" + +#: .././tidy.py:387 +msgid "New video files detected:" +msgstr "" + +#: .././tidy.py:393 +msgid "Missing video files detected:" +msgstr "" + +#: .././tidy.py:401 +msgid "Non-corrupted video files deleted:" +msgstr "" + +#: .././tidy.py:407 +msgid "Other video/audio files deleted:" +msgstr "" + +#: .././tidy.py:415 +msgid "Description files deleted:" +msgstr "" + +#: .././tidy.py:423 +msgid "Metadata (JSON) files deleted:" +msgstr "" + +#: .././tidy.py:431 +msgid "Annotation files deleted:" +msgstr "" + +#: .././tidy.py:439 +msgid "Thumbnail files deleted:" +msgstr "" + +#: .././tidy.py:447 +msgid "youtube-dl archive files deleted:" +msgstr "" + +#: .././tidy.py:574 +msgid "Deleted (possibly) corrupted video file:" +msgstr "" + +#: .././tidy.py:589 .././tidy.py:995 +msgid "Video file might be corrupt:" +msgstr "" + +#: .././tidy.py:633 +msgid "Video file exists:" +msgstr "" + +#: .././tidy.py:651 +msgid "Video file doesn't exist:" +msgstr "" + +#: .././updates.py:215 +msgid "Starting update operation, installing FFmpeg" +msgstr "" + +#: .././updates.py:289 +msgid "FFmpeg installation did not start" +msgstr "" + +#: .././updates.py:306 .././updates.py:464 +msgid "Update operation finished" +msgstr "" + +#: .././updates.py:335 +msgid "Starting update operation, installing/updating youtube-dl" +msgstr "" + +#: .././updates.py:439 +msgid "youtube-dl update did not start" +msgstr "" diff --git a/nsis/tartube_install_32bit.nsi b/nsis/tartube_install_32bit.nsi index c3e03943..2cb3b1e6 100644 --- a/nsis/tartube_install_32bit.nsi +++ b/nsis/tartube_install_32bit.nsi @@ -1,4 +1,4 @@ -# Tartube v2.0.016 installer script for MS Windows +# Tartube v2.1.0 installer script for MS Windows # # Copyright (C) 2019-2020 A S Lewis # @@ -32,6 +32,7 @@ # - Run the file to install MSYS2. We suggest that you create a directory # called C:\testme, and then let MSYS2 install itself inside that # directory, i.e. C:\testme\msys32 +# # - Run the mingw32 terminal, i.e. # # C:\testme\msys32\mingw32.exe @@ -43,6 +44,7 @@ # # - Usually, the terminal window tells you to close it. Do that, and then # open a new mingw32 terminal window +# # - In the new window, type these commands # # pacman -Su @@ -58,19 +60,6 @@ # # gtk3-demo # -# - Now download the Tartube source code from -# -# https://sourceforge.net/projects/tartube/ -# -# - Extract it, and copy the whole 'tartube' folder to -# -# C:\testme\msys32\home\YOURNAME -# -# - Note that, throughout this guide, YOURNAME should be substituted for your -# actual Windows username. For example, the copied folder might be -# -# C:\testme\msys32\home\alice\tartube -# # - The C:\testme folder now contains about 2GB of data. If you like, you can # use all of it (which would create an installer of about 600MB). In most # cases, though, you will probably want to remove everything that's not @@ -81,9 +70,11 @@ # C:\testme\msys32\dev # C:\testme\msys32\etc # C:\testme\msys32\home +# C:\testme\msys32\mingw32.exe # C:\testme\msys32\minwg32\bin # C:\testme\msys32\minwg32\bin\gdbus* # C:\testme\msys32\minwg32\bin\gdk* +# C:\testme\msys32\minwg32\bin\gettext* # C:\testme\msys32\minwg32\bin\gio* # C:\testme\msys32\minwg32\bin\glib* # C:\testme\msys32\minwg32\bin\gobject* @@ -111,32 +102,34 @@ # C:\testme\msys32\minwg32\include\readline # C:\testme\msys32\minwg32\include\tk8.6 # C:\testme\msys32\minwg32\lib\gdk-pixbuf-2.0 +# C:\testme\msys32\minwg32\lib\gettext # C:\testme\msys32\minwg32\lib\girepository-1.0 # C:\testme\msys32\minwg32\lib\glib-2.0 # C:\testme\msys32\minwg32\lib\gtk-3.0 -# C:\testme\msys32\minwg32\lib\python3.7\collections -# C:\testme\msys32\minwg32\lib\python3.7\ctypes -# C:\testme\msys32\minwg32\lib\python3.7\distutils -# C:\testme\msys32\minwg32\lib\python3.7\email -# C:\testme\msys32\minwg32\lib\python3.7\encodings -# C:\testme\msys32\minwg32\lib\python3.7\ensurepip -# C:\testme\msys32\minwg32\lib\python3.7\html -# C:\testme\msys32\minwg32\lib\python3.7\http -# C:\testme\msys32\minwg32\lib\python3.7\importlib -# C:\testme\msys32\minwg32\lib\python3.7\json -# C:\testme\msys32\minwg32\lib\python3.7\lib2to3 -# C:\testme\msys32\minwg32\lib\python3.7\lib-dynload -# C:\testme\msys32\minwg32\lib\python3.7\logging -# C:\testme\msys32\minwg32\lib\python3.7\msilib -# C:\testme\msys32\minwg32\lib\python3.7\multiprocessing -# C:\testme\msys32\minwg32\lib\python3.7\site-packages -# C:\testme\msys32\minwg32\lib\python3.7\sqlite3 -# C:\testme\msys32\minwg32\lib\python3.7\urllib -# C:\testme\msys32\minwg32\lib\python3.7\xml -# C:\testme\msys32\minwg32\lib\python3.7\xmlrpc -# C:\testme\msys32\minwg32\lib\python3.7\*.py +# C:\testme\msys32\minwg32\lib\python3.8\collections +# C:\testme\msys32\minwg32\lib\python3.8\ctypes +# C:\testme\msys32\minwg32\lib\python3.8\distutils +# C:\testme\msys32\minwg32\lib\python3.8\email +# C:\testme\msys32\minwg32\lib\python3.8\encodings +# C:\testme\msys32\minwg32\lib\python3.8\ensurepip +# C:\testme\msys32\minwg32\lib\python3.8\html +# C:\testme\msys32\minwg32\lib\python3.8\http +# C:\testme\msys32\minwg32\lib\python3.8\importlib +# C:\testme\msys32\minwg32\lib\python3.8\json +# C:\testme\msys32\minwg32\lib\python3.8\lib2to3 +# C:\testme\msys32\minwg32\lib\python3.8\lib-dynload +# C:\testme\msys32\minwg32\lib\python3.8\logging +# C:\testme\msys32\minwg32\lib\python3.8\msilib +# C:\testme\msys32\minwg32\lib\python3.8\multiprocessing +# C:\testme\msys32\minwg32\lib\python3.8\site-packages +# C:\testme\msys32\minwg32\lib\python3.8\sqlite3 +# C:\testme\msys32\minwg32\lib\python3.8\urllib +# C:\testme\msys32\minwg32\lib\python3.8\xml +# C:\testme\msys32\minwg32\lib\python3.8\xmlrpc +# C:\testme\msys32\minwg32\lib\python3.8\*.py # C:\testme\msys32\minwg32\lib\thread2.8.4 # C:\testme\msys32\minwg32\lib\tk8.6 +# C:\testme\msys32\minwg32\share\gettext # C:\testme\msys32\minwg32\share\gir-1.0 # C:\testme\msys32\minwg32\share\glib-2.0 # C:\testme\msys32\minwg32\share\gtk-3.0 @@ -149,6 +142,7 @@ # C:\testme\msys32\tmp # C:\testme\msys32\usr\bin\bash # C:\testme\msys32\usr\bin\chmod +# C:\testme\msys32\usr\bin\cut # C:\testme\msys32\usr\bin\cygpath # C:\testme\msys32\usr\bin\cygwin-console-helper # C:\testme\msys32\usr\bin\dir @@ -189,9 +183,10 @@ # C:\testme\msys32\usr\bin\pac* # C:\testme\msys32\usr\bin\test # C:\testme\msys32\usr\bin\tzset +# C:\testme\msys32\usr\lib\gettext # C:\testme\msys32\usr\lib\gio # C:\testme\msys32\usr\lib\openssl -# C:\testme\msys32\usr\lib\python3.7 +# C:\testme\msys32\usr\lib\python3.8 # C:\testme\msys32\usr\share\cygwin # C:\testme\msys32\usr\share\glib-2.0 # C:\testme\msys32\usr\share\mintty @@ -201,17 +196,41 @@ # C:\testme\msys32\usr\ssl # C:\testme\msys32\var\lib\pacman # -# - You can optionally install AtomicParsley at this location: +# - The followng optional dependencies are required for fetching livestreams. +# If you decide to install them (it's recommended that you do), run the +# mingw32 terminal again, if it's not still open +# +# C:\testme\msys32\mingw32.exe +# +# - In the terminal window, type +# +# pip3 install feedparser +# pip3 install playsound +# +# - AtomicParsley, if you want it, can be copied to this location: +# # C:\testme\msys32\usr\bin # -# - Now go into the C:\testme\msys32\home\YOURNAME\tartube\nsis folder, and -# MOVE all the windows batch files into the folder above, i.e. into -# C:\testme\msys32\home\YOURNAME\tartube +# - Now download the Tartube source code from +# +# https://sourceforge.net/projects/tartube/ +# +# - Extract it, and copy the whole 'tartube' folder to +# +# C:\testme\msys32\home\YOURNAME +# +# - Note that YOURNAME should be substituted for your actual Windows +# username. For example, the copied folder might be +# +# C:\testme\msys32\home\alice\tartube +# # - Next, COPY all the remaining files in # C:\testme\msys32\home\YOURNAME\tartube\nsis to C:\testme +# # - Create the installer by compiling the NSIS script, # C:\testme\tartube_install_32bit.nsi (the quickest way to do this is # by right-clicking the file and selecting 'Compile NSIS script file') +# # - When NSIS is finished, the installer appears in C:\testme # Header files @@ -225,7 +244,7 @@ ;Name and file Name "Tartube" - OutFile "install-tartube-2.0.016-32bit.exe" + OutFile "install-tartube-2.1.0-32bit.exe" ;Default installation folder InstallDir "$LOCALAPPDATA\Tartube" @@ -328,7 +347,7 @@ Section "Tartube" SecClient # "Publisher" "A S Lewis" # WriteRegStr HKLM \ # "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "DisplayVersion" "2.0.016" +# "DisplayVersion" "2.1.0" # Create uninstaller WriteUninstaller "$INSTDIR\Uninstall.exe" diff --git a/nsis/tartube_install_64bit.nsi b/nsis/tartube_install_64bit.nsi index 5f5ae30e..0995a5c2 100644 --- a/nsis/tartube_install_64bit.nsi +++ b/nsis/tartube_install_64bit.nsi @@ -1,4 +1,4 @@ -# Tartube v2.0.016 installer script for MS Windows +# Tartube v2.1.0 installer script for MS Windows # # Copyright (C) 2019-2020 A S Lewis # @@ -44,6 +44,7 @@ # # - Usually, the terminal window tells you to close it. Do that, and then # open a new mingw64 terminal window +# # - In the new window, type these commands # # pacman -Su @@ -59,19 +60,6 @@ # # gtk3-demo # -# - Now download the Tartube source code from -# -# https://sourceforge.net/projects/tartube/ -# -# - Extract it, and copy the whole 'tartube' folder to -# -# C:\testme\msys64\home\YOURNAME -# -# - Note that, throughout this guide, YOURNAME should be substituted for your -# actual Windows username. For example, the copied folder might be -# -# C:\testme\msys64\home\alice\tartube -# # - The C:\testme folder now contains about 2GB of data. If you like, you can # use all of it (which would create an installer of about 600MB). In most # cases, though, you will probably want to remove everything that's not @@ -82,9 +70,11 @@ # C:\testme\msys64\dev # C:\testme\msys64\etc # C:\testme\msys64\home +# C:\testme\msys64\mingw64.exe # C:\testme\msys64\mingw64\bin # C:\testme\msys64\mingw64\bin\gdbus* # C:\testme\msys64\mingw64\bin\gdk* +# C:\testme\msys64\mingw64\bin\gettext* # C:\testme\msys64\mingw64\bin\gio* # C:\testme\msys64\mingw64\bin\glib* # C:\testme\msys64\mingw64\bin\gobject* @@ -108,36 +98,38 @@ # C:\testme\msys64\mingw64\include\openssl # C:\testme\msys64\mingw64\include\pycairo # C:\testme\msys64\mingw64\include\pygobject-3.0 -# C:\testme\msys64\mingw64\include\python3.7 +# C:\testme\msys64\mingw64\include\python3.8 # C:\testme\msys64\mingw64\include\readline # C:\testme\msys64\mingw64\include\tk8.6 # C:\testme\msys64\mingw64\lib\gdk-pixbuf-2.0 +# C:\testme\msys64\mingw64\lib\gettext # C:\testme\msys64\mingw64\lib\girepository-1.0 # C:\testme\msys64\mingw64\lib\glib-2.0 # C:\testme\msys64\mingw64\lib\gtk-3.0 -# C:\testme\msys64\mingw64\lib\python3.7\collections -# C:\testme\msys64\mingw64\lib\python3.7\ctypes -# C:\testme\msys64\mingw64\lib\python3.7\distutils -# C:\testme\msys64\mingw64\lib\python3.7\email -# C:\testme\msys64\mingw64\lib\python3.7\encodings -# C:\testme\msys64\mingw64\lib\python3.7\ensurepip -# C:\testme\msys64\mingw64\lib\python3.7\html -# C:\testme\msys64\mingw64\lib\python3.7\http -# C:\testme\msys64\mingw64\lib\python3.7\importlib -# C:\testme\msys64\mingw64\lib\python3.7\json -# C:\testme\msys64\mingw64\lib\python3.7\lib2to3 -# C:\testme\msys64\mingw64\lib\python3.7\lib-dynload -# C:\testme\msys64\mingw64\lib\python3.7\logging -# C:\testme\msys64\mingw64\lib\python3.7\msilib -# C:\testme\msys64\mingw64\lib\python3.7\multiprocessing -# C:\testme\msys64\mingw64\lib\python3.7\site-packages -# C:\testme\msys64\mingw64\lib\python3.7\sqlite3 -# C:\testme\msys64\mingw64\lib\python3.7\urllib -# C:\testme\msys64\mingw64\lib\python3.7\xml -# C:\testme\msys64\mingw64\lib\python3.7\xmlrpc -# C:\testme\msys64\mingw64\lib\python3.7\*.py +# C:\testme\msys64\mingw64\lib\python3.8\collections +# C:\testme\msys64\mingw64\lib\python3.8\ctypes +# C:\testme\msys64\mingw64\lib\python3.8\distutils +# C:\testme\msys64\mingw64\lib\python3.8\email +# C:\testme\msys64\mingw64\lib\python3.8\encodings +# C:\testme\msys64\mingw64\lib\python3.8\ensurepip +# C:\testme\msys64\mingw64\lib\python3.8\html +# C:\testme\msys64\mingw64\lib\python3.8\http +# C:\testme\msys64\mingw64\lib\python3.8\importlib +# C:\testme\msys64\mingw64\lib\python3.8\json +# C:\testme\msys64\mingw64\lib\python3.8\lib2to3 +# C:\testme\msys64\mingw64\lib\python3.8\lib-dynload +# C:\testme\msys64\mingw64\lib\python3.8\logging +# C:\testme\msys64\mingw64\lib\python3.8\msilib +# C:\testme\msys64\mingw64\lib\python3.8\multiprocessing +# C:\testme\msys64\mingw64\lib\python3.8\site-packages +# C:\testme\msys64\mingw64\lib\python3.8\sqlite3 +# C:\testme\msys64\mingw64\lib\python3.8\urllib +# C:\testme\msys64\mingw64\lib\python3.8\xml +# C:\testme\msys64\mingw64\lib\python3.8\xmlrpc +# C:\testme\msys64\mingw64\lib\python3.8\*.py # C:\testme\msys64\mingw64\lib\thread2.8.4 # C:\testme\msys64\mingw64\lib\tk8.6 +# C:\testme\msys64\mingw64\share\gettext # C:\testme\msys64\mingw64\share\gir-1.0 # C:\testme\msys64\mingw64\share\glib-2.0 # C:\testme\msys64\mingw64\share\gtk-3.0 @@ -150,6 +142,7 @@ # C:\testme\msys64\tmp # C:\testme\msys64\usr\bin\bash # C:\testme\msys64\usr\bin\chmod +# C:\testme\msys64\usr\bin\cut # C:\testme\msys64\usr\bin\cygpath # C:\testme\msys64\usr\bin\cygwin-console-helper # C:\testme\msys64\usr\bin\dir @@ -190,9 +183,10 @@ # C:\testme\msys64\usr\bin\pac* # C:\testme\msys64\usr\bin\test # C:\testme\msys64\usr\bin\tzset +# C:\testme\msys64\usr\lib\gettext # C:\testme\msys64\usr\lib\gio # C:\testme\msys64\usr\lib\openssl -# C:\testme\msys64\usr\lib\python3.7 +# C:\testme\msys64\usr\lib\python3.8 # C:\testme\msys64\usr\share\cygwin # C:\testme\msys64\usr\share\glib-2.0 # C:\testme\msys64\usr\share\mintty @@ -202,17 +196,41 @@ # C:\testme\msys64\usr\ssl # C:\testme\msys64\var\lib\pacman # -# - You can optionally install AtomicParsley at this location: +# - The followng optional dependencies are required for fetching livestreams. +# If you decide to install them (it's recommended that you do), run the +# mingw64 terminal again, if it's not still open +# +# C:\testme\msys64\mingw64.exe +# +# - In the terminal window, type +# +# pip3 install feedparser +# pip3 install playsound +# +# - AtomicParsley, if you want it, can be copied to this location: +# # C:\testme\msys64\usr\bin # -# - Now go into the C:\testme\msys64\home\YOURNAME\tartube\nsis folder, and -# MOVE all the windows batch files into the folder above, i.e. into -# C:\testme\msys64\home\YOURNAME\tartube -# - Next, COPY all the remaining files in +# - Now download the Tartube source code from +# +# https://sourceforge.net/projects/tartube/ +# +# - Extract it, and copy the whole 'tartube' folder to +# +# C:\testme\msys64\home\YOURNAME +# +# - Note that YOURNAME should be substituted for your actual Windows +# username. For example, the copied folder might be +# +# C:\testme\msys64\home\alice\tartube +# +# - Next, copy all of the files in # C:\testme\msys64\home\YOURNAME\tartube\nsis to C:\testme +# # - Create the installer by compiling the NSIS script, # C:\testme\tartube_install_64bit.nsi (the quickest way to do this is # by right-clicking the file and selecting 'Compile NSIS script file') +# # - When NSIS is finished, the installer appears in C:\testme # Header files @@ -226,7 +244,7 @@ ;Name and file Name "Tartube" - OutFile "install-tartube-2.0.016-64bit.exe" + OutFile "install-tartube-2.1.0-64bit.exe" ;Default installation folder InstallDir "$LOCALAPPDATA\Tartube" @@ -329,7 +347,7 @@ Section "Tartube" SecClient # "Publisher" "A S Lewis" # WriteRegStr HKLM \ # "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "DisplayVersion" "2.0.016" +# "DisplayVersion" "2.1.0" # Create uninstaller WriteUninstaller "$INSTDIR\Uninstall.exe" diff --git a/pack/bin/tartube b/pack/bin/tartube index 101d1d32..2f8ac08f 100755 --- a/pack/bin/tartube +++ b/pack/bin/tartube @@ -42,9 +42,8 @@ import mainapp # 'Global' variables __packagename__ = 'tartube' -__prettyname__ = 'Tartube' -__version__ = '2.0.016' -__date__ = '10 Apr 2020' +__version__ = '2.1.0' +__date__ = '7 May 2020' __copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' __license__ = """ Copyright \xa9 2019-2020 A S Lewis. @@ -69,6 +68,7 @@ __description__ = 'A front-end GUI for youtube-dl,\n' \ + 'and written in Python 3 / Gtk 3' __website__ = 'http://tartube.sourceforge.io' __app_id__ = 'io.sourceforge.tartube' +__website_bugs__ = 'https://github.com/axcore/tartube' # There are three executables; a default one, and two others used in Debian/RPM # packaging (of which this is one). The executables are identical, except for # the values of these variables diff --git a/pack/bin_strict/tartube b/pack/bin_strict/tartube index bf2e0bef..a3579aa2 100755 --- a/pack/bin_strict/tartube +++ b/pack/bin_strict/tartube @@ -42,9 +42,8 @@ import mainapp # 'Global' variables __packagename__ = 'tartube' -__prettyname__ = 'Tartube' -__version__ = '2.0.016' -__date__ = '10 Apr 2020' +__version__ = '2.1.0' +__date__ = '7 May 2020' __copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' __license__ = """ Copyright \xa9 2019-2020 A S Lewis. @@ -69,6 +68,7 @@ __description__ = 'A front-end GUI for youtube-dl,\n' \ + 'and written in Python 3 / Gtk 3' __website__ = 'http://tartube.sourceforge.io' __app_id__ = 'io.sourceforge.tartube' +__website_bugs__ = 'https://github.com/axcore/tartube' # There are three executables; a default one, and two others used in Debian/RPM # packaging (of which this is one). The executables are identical, except for # the values of these variables diff --git a/pack/tartube.1 b/pack/tartube.1 index b910edd8..50553051 100644 --- a/pack/tartube.1 +++ b/pack/tartube.1 @@ -1,4 +1,4 @@ -.TH man 1 "10 Apr 2020" "2.0.016" "tartube man page" +.TH man 1 "7 May 2020" "2.1.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 77d7e65e..6154a839 100644 --- a/pack/tartube.desktop +++ b/pack/tartube.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Name=Tartube -Version=2.0.016 +Version=2.1.0 Exec=tartube Icon=tartube Type=Application diff --git a/screenshots/example11.png b/screenshots/example11.png index 5bd2bb04..e66d9af8 100644 Binary files a/screenshots/example11.png and b/screenshots/example11.png differ diff --git a/screenshots/example12.png b/screenshots/example12.png index 95c0dfb5..2b208926 100644 Binary files a/screenshots/example12.png and b/screenshots/example12.png differ diff --git a/screenshots/example13.png b/screenshots/example13.png index 0dc5c7c9..ec2a57ee 100644 Binary files a/screenshots/example13.png and b/screenshots/example13.png differ diff --git a/screenshots/example14.png b/screenshots/example14.png index b0ddb0c9..41d171e8 100644 Binary files a/screenshots/example14.png and b/screenshots/example14.png differ diff --git a/screenshots/example15.png b/screenshots/example15.png index f7267c12..d4c35af0 100644 Binary files a/screenshots/example15.png and b/screenshots/example15.png differ diff --git a/screenshots/example16.png b/screenshots/example16.png index ac14abfe..48a8f2a9 100644 Binary files a/screenshots/example16.png and b/screenshots/example16.png differ diff --git a/screenshots/example17.png b/screenshots/example17.png index ad35e1c0..48cd30cd 100644 Binary files a/screenshots/example17.png and b/screenshots/example17.png differ diff --git a/screenshots/example20.png b/screenshots/example20.png new file mode 100644 index 00000000..11d5766f Binary files /dev/null and b/screenshots/example20.png differ diff --git a/screenshots/example21.png b/screenshots/example21.png new file mode 100644 index 00000000..978f377f Binary files /dev/null and b/screenshots/example21.png differ diff --git a/screenshots/example22.png b/screenshots/example22.png new file mode 100644 index 00000000..7a238689 Binary files /dev/null and b/screenshots/example22.png differ diff --git a/screenshots/example23.png b/screenshots/example23.png new file mode 100644 index 00000000..d2f12a38 Binary files /dev/null and b/screenshots/example23.png differ diff --git a/screenshots/example3.png b/screenshots/example3.png index 3e70e7ca..18f13d7b 100644 Binary files a/screenshots/example3.png and b/screenshots/example3.png differ diff --git a/screenshots/example4.png b/screenshots/example4.png index 866ed86d..bcae0209 100644 Binary files a/screenshots/example4.png and b/screenshots/example4.png differ diff --git a/screenshots/example6.png b/screenshots/example6.png index 557673d7..0f854f71 100644 Binary files a/screenshots/example6.png and b/screenshots/example6.png differ diff --git a/screenshots/example7.png b/screenshots/example7.png index cd9811aa..23b41ae2 100644 Binary files a/screenshots/example7.png and b/screenshots/example7.png differ diff --git a/screenshots/example8.png b/screenshots/example8.png index c253fe8d..5e2fb549 100644 Binary files a/screenshots/example8.png and b/screenshots/example8.png differ diff --git a/screenshots/example9.png b/screenshots/example9.png index bb46880f..efde87e0 100644 Binary files a/screenshots/example9.png and b/screenshots/example9.png differ diff --git a/screenshots/tartube.png b/screenshots/tartube.png index e082f4d3..252905d3 100644 Binary files a/screenshots/tartube.png and b/screenshots/tartube.png differ diff --git a/setup.py b/setup.py index 404c9384..6c682f77 100755 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ pkg_strict_value = os.environ.get( pkg_strict_var, None ) script_exec = os.path.join('tartube', 'tartube') icon_path = '/tartube/icons/' +sound_path = '/tartube/sounds/' pkg_flag = False if pkg_strict_value is not None: @@ -110,8 +111,9 @@ # Apply changes if either environment variable was specified if pkg_flag: - # Icons must be copied into the right place + # Icons/sounds must be copied into the right place icon_path = '/usr/share/tartube/icons/' + sound_path = '/usr/share/tartube/sounds/' # Use a shorter long description, as the standard one tends to cause errors long_description = alt_description # Add a desktop file @@ -122,7 +124,7 @@ param_list.append(('share/man/man1', ['pack/tartube.1'])) # For PyPI installations and Debian/RPM packaging, copy everything in ../icons -# into a suitable location +# and ../sounds into a suitable location subdir_list = [ 'dialogue', 'large', @@ -137,10 +139,13 @@ for path in glob.glob('icons/' + subdir + '/*'): param_list.append((icon_path + subdir + '/', [path])) +for path in glob.glob('sounds/*'): + param_list.append((icon_path + '/', [path])) + # Setup setuptools.setup( name='tartube', - version='2.0.016', + version='2.1.0', description='GUI front-end for youtube-dl', long_description=long_description, long_description_content_type='text/plain', @@ -167,7 +172,8 @@ ), include_package_data=True, python_requires='>=3.0, <4', - install_requires=['requests'], +# install_requires=['requests'], + install_requires=['feedparser', 'gi', 'playsound', 'requests'], scripts=[script_exec], project_urls={ 'Bug Reports': 'https://github.com/axcore/tartube/issues', diff --git a/sounds/COPYING b/sounds/COPYING new file mode 100644 index 00000000..4060891b --- /dev/null +++ b/sounds/COPYING @@ -0,0 +1,129 @@ +COPYING + +All files in this directory were obtained from soundbible.com + +File: ahem.mp3 +Source: http://soundbible.com/758-Throat-Clearing.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: beep.mp3 +Source: http://soundbible.com/1598-Electronic-Chime.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: belch.mp3 +Source: http://soundbible.com/1579-Belch.html +Licence: Public domain +Author: Kevan + +File: bell.mp3 +Source: http://soundbible.com/2190-Front-Desk-Bell.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: boxing.mp3 +Source: http://soundbible.com/1559-Boxing-Arena-Sound.html +Licence: Attribution 3.0 +Author: Samantha Enrico + +File: call.mp3 +Source: http://soundbible.com/1795-Electrical-Sweep.html +Licence: Public domain +Author: Sweeper + +File: chime.mp3 +Source: http://soundbible.com/1599-Store-Door-Chime.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: cow.mp3 +Source: http://soundbible.com/1143-Cow-And-Bell.html +Licence: Public domain +Author: (unknown) + +File: cowbell.mp3 +Source: http://soundbible.com/1781-Metal-Clang.html +Licence: Attribution 3.0 +Author: battlestar10 + +File: cuckoo.mp3 +Source: http://soundbible.com/1261-Cuckoo-Clock.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: dixie.mp3 +Source: http://soundbible.com/2179-Dixie-Horn.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: doorbell.mp3 +Source: http://soundbible.com/165-Door-Bell.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: gong.mp3 +Source: http://soundbible.com/2148-Chinese-Gong.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: hello.mp3 +Source: http://soundbible.com/678-Hello.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: honk.mp3 +Source: http://soundbible.com/1695-Train-Honk-Horn-2x.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: horn.mp3 +Source: http://soundbible.com/583-Horn-Honk.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: party.mp3 +Source: http://soundbible.com/1817-Party-Horn.html +Licence: Attribution 3.0 +Author: Mike Koenig + +File: phone1.mp3 +Source: http://soundbible.com/2154-Text-Message-Alert-1.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: phone2.mp3 +Source: http://soundbible.com/2155-Text-Message-Alert-2.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: phone3.mp3 +Source: http://soundbible.com/2156-Text-Message-Alert-3.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: phone4.mp3 +Source: http://soundbible.com/2157-Text-Message-Alert-4.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: phone5.mp3 +Source: http://soundbible.com/2158-Text-Message-Alert-5.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: ring.mp3 +Source: http://soundbible.com/2189-Cartoon-Phone-Ring.html +Licence: Attribution 3.0 +Author: Daniel Simon + +File: suspense.mp3 +Source: http://soundbible.com/2046-Incoming-Suspense.html +Licence: Attribution 3.0 +Author: Maximilien + +File: teaspoon.mp3 +Source: http://soundbible.com/1967-Clinking-Teaspoon.html +Licence: Attribution 3.0 +Author: Simon Craggs + diff --git a/sounds/ahem.mp3 b/sounds/ahem.mp3 new file mode 100644 index 00000000..735d87b1 Binary files /dev/null and b/sounds/ahem.mp3 differ diff --git a/sounds/beep.mp3 b/sounds/beep.mp3 new file mode 100644 index 00000000..bfbd9bb8 Binary files /dev/null and b/sounds/beep.mp3 differ diff --git a/sounds/belch.mp3 b/sounds/belch.mp3 new file mode 100644 index 00000000..a87b7ee4 Binary files /dev/null and b/sounds/belch.mp3 differ diff --git a/sounds/bell.mp3 b/sounds/bell.mp3 new file mode 100644 index 00000000..d2f8a528 Binary files /dev/null and b/sounds/bell.mp3 differ diff --git a/sounds/boxing.mp3 b/sounds/boxing.mp3 new file mode 100644 index 00000000..cbb9ff85 Binary files /dev/null and b/sounds/boxing.mp3 differ diff --git a/sounds/call.mp3 b/sounds/call.mp3 new file mode 100644 index 00000000..f9bf4f8f Binary files /dev/null and b/sounds/call.mp3 differ diff --git a/sounds/chime.mp3 b/sounds/chime.mp3 new file mode 100644 index 00000000..fedb4d99 Binary files /dev/null and b/sounds/chime.mp3 differ diff --git a/sounds/cow.mp3 b/sounds/cow.mp3 new file mode 100644 index 00000000..4463dfbf Binary files /dev/null and b/sounds/cow.mp3 differ diff --git a/sounds/cowbell.mp3 b/sounds/cowbell.mp3 new file mode 100644 index 00000000..879ce6d3 Binary files /dev/null and b/sounds/cowbell.mp3 differ diff --git a/sounds/cuckoo.mp3 b/sounds/cuckoo.mp3 new file mode 100644 index 00000000..70a52bb7 Binary files /dev/null and b/sounds/cuckoo.mp3 differ diff --git a/sounds/dixie.mp3 b/sounds/dixie.mp3 new file mode 100644 index 00000000..afe09994 Binary files /dev/null and b/sounds/dixie.mp3 differ diff --git a/sounds/doorbell.mp3 b/sounds/doorbell.mp3 new file mode 100644 index 00000000..d01d5992 Binary files /dev/null and b/sounds/doorbell.mp3 differ diff --git a/sounds/gong.mp3 b/sounds/gong.mp3 new file mode 100644 index 00000000..008fb679 Binary files /dev/null and b/sounds/gong.mp3 differ diff --git a/sounds/hello.mp3 b/sounds/hello.mp3 new file mode 100644 index 00000000..8bc82f8d Binary files /dev/null and b/sounds/hello.mp3 differ diff --git a/sounds/honk.mp3 b/sounds/honk.mp3 new file mode 100644 index 00000000..8dc12ce8 Binary files /dev/null and b/sounds/honk.mp3 differ diff --git a/sounds/horn.mp3 b/sounds/horn.mp3 new file mode 100644 index 00000000..e707ea28 Binary files /dev/null and b/sounds/horn.mp3 differ diff --git a/sounds/party.mp3 b/sounds/party.mp3 new file mode 100644 index 00000000..b6ebf687 Binary files /dev/null and b/sounds/party.mp3 differ diff --git a/sounds/phone1.mp3 b/sounds/phone1.mp3 new file mode 100644 index 00000000..7751f7b5 Binary files /dev/null and b/sounds/phone1.mp3 differ diff --git a/sounds/phone2.mp3 b/sounds/phone2.mp3 new file mode 100644 index 00000000..e5638cf8 Binary files /dev/null and b/sounds/phone2.mp3 differ diff --git a/sounds/phone3.mp3 b/sounds/phone3.mp3 new file mode 100644 index 00000000..b9e02f48 Binary files /dev/null and b/sounds/phone3.mp3 differ diff --git a/sounds/phone4.mp3 b/sounds/phone4.mp3 new file mode 100644 index 00000000..ee7b8b90 Binary files /dev/null and b/sounds/phone4.mp3 differ diff --git a/sounds/phone5.mp3 b/sounds/phone5.mp3 new file mode 100644 index 00000000..b5ed30bb Binary files /dev/null and b/sounds/phone5.mp3 differ diff --git a/sounds/ring.mp3 b/sounds/ring.mp3 new file mode 100644 index 00000000..a6e73e87 Binary files /dev/null and b/sounds/ring.mp3 differ diff --git a/sounds/suspense.mp3 b/sounds/suspense.mp3 new file mode 100644 index 00000000..4d16e6db Binary files /dev/null and b/sounds/suspense.mp3 differ diff --git a/sounds/teaspoon.mp3 b/sounds/teaspoon.mp3 new file mode 100644 index 00000000..8725560f Binary files /dev/null and b/sounds/teaspoon.mp3 differ diff --git a/tartube/config.py b/tartube/config.py index 8a5c6711..65c4be3d 100644 --- a/tartube/config.py +++ b/tartube/config.py @@ -38,6 +38,8 @@ import media import re import utils +# Use same gettext translations +from mainapp import _ # Classes @@ -273,6 +275,39 @@ def close(self, also_self): # (Add widgets) + def add_image(self, grid, image_path, x, y, wid, hei): + + """Called by various functions in the child edit window. + + Adds a Gtk.Image to the tab's Gtk.Grid. + + Args: + + grid (Gtk.Grid): The grid on which this widget will be placed + + image_path (str): Full path to the image file to load + + x, y, wid, hei (int): Position on the grid at which the widget is + placed + + Returns: + + The Gtk.Frame containing the image + + """ + + frame = Gtk.Frame() + grid.attach(frame, x, y, wid, hei) + + image = Gtk.Image() + frame.add(image) + image.set_from_pixbuf( + self.app_obj.file_manager_obj.load_to_pixbuf(image_path), + ) + + return frame + + def add_treeview(self, grid, x, y, wid, hei): """Called by various functions in the child preference/edit window. @@ -370,37 +405,37 @@ def setup_button_strip(self): if self.multi_button_flag: # 'Reset' button - self.reset_button = Gtk.Button('Reset') + self.reset_button = Gtk.Button(_('Reset')) hbox.pack_start(self.reset_button, False, False, self.spacing_size) self.reset_button.get_child().set_width_chars(10) self.reset_button.set_tooltip_text( - 'Reset changes without closing the window', + _('Reset changes without closing the window'), ); self.reset_button.connect('clicked', self.on_button_reset_clicked) # 'Apply' button - self.apply_button = Gtk.Button('Apply') + self.apply_button = Gtk.Button(_('Apply')) hbox.pack_start(self.apply_button, False, False, self.spacing_size) self.apply_button.get_child().set_width_chars(10) self.apply_button.set_tooltip_text( - 'Apply changes without closing the window', + _('Apply changes without closing the window'), ); self.apply_button.connect('clicked', self.on_button_apply_clicked) # 'OK' button - self.ok_button = Gtk.Button('OK') + self.ok_button = Gtk.Button(_('OK')) hbox.pack_end(self.ok_button, False, False, self.spacing_size) self.ok_button.get_child().set_width_chars(10) - self.ok_button.set_tooltip_text('Apply changes'); + self.ok_button.set_tooltip_text(_('Apply changes')); self.ok_button.connect('clicked', self.on_button_ok_clicked) if self.multi_button_flag: # 'Cancel' button - self.cancel_button = Gtk.Button('Cancel') + self.cancel_button = Gtk.Button(_('Cancel')) hbox.pack_end(self.cancel_button, False, False, self.spacing_size) self.cancel_button.get_child().set_width_chars(10) - self.cancel_button.set_tooltip_text('Cancel changes'); + self.cancel_button.set_tooltip_text(_('Cancel changes')); self.cancel_button.connect( 'clicked', self.on_button_cancel_clicked, @@ -692,37 +727,7 @@ def add_entry(self, grid, prop, x, y, wid, hei): return entry - def add_image(self, grid, image_path, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.Image to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - image_path (str): Full path to the image file to load - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The Gtk.Frame containing the image - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - image = Gtk.Image() - frame.add(image) - image.set_from_pixbuf( - self.app_obj.file_manager_obj.load_to_pixbuf(image_path), - ) - - return frame +# def add_image # Inherited from GenericConfigWin def add_label(self, grid, text, x, y, wid, hei): @@ -1271,7 +1276,7 @@ def add_container_properties(self, grid): entry2.set_editable(False) label = self.add_label(grid, - 'Listed as', + _('Listed as'), 0, 2, 1, 1, ) label.set_hexpand(False) @@ -1283,7 +1288,7 @@ def add_container_properties(self, grid): entry3.set_editable(False) label2 = self.add_label(grid, - 'Contained in', + _('Contained in'), 0, 3, 1, 1, ) label2.set_hexpand(False) @@ -1340,17 +1345,25 @@ def add_source_properties(self, grid): """ - label2 = self.add_label(grid, - utils.upper_case_first(self.media_type) + ' URL', + media_type = self.edit_obj.get_type() + if media_type == 'channel': + string = _('Channel URL') + elif media_type == 'playlist': + string = _('Playlist URL') + else: + string = _('Video URL') + + label = self.add_label(grid, + string, 0, 4, 1, 1, ) - label2.set_hexpand(False) + label.set_hexpand(False) - entry5 = self.add_entry(grid, + entry = self.add_entry(grid, 'source', - 1, 4, 2, 1, + 2, 4, 1, 1, ) - entry5.set_editable(False) + entry.set_editable(False) def add_destination_properties(self, grid): @@ -1367,65 +1380,57 @@ def add_destination_properties(self, grid): """ - # To avoid messing up the neat format of the rows above, add another - # grid, and put the next set of widgets inside it - grid2 = Gtk.Grid() - grid.attach(grid2, 0, 5, 3, 1) - grid2.set_vexpand(False) - grid2.set_column_spacing(self.spacing_size) - grid2.set_row_spacing(self.spacing_size) - - label3 = self.add_label(grid2, - 'Videos downloaded to', - 0, 0, 1, 1, + label = self.add_label(grid, + _('Download to'), + 0, 5, 1, 1, ) - label3.set_hexpand(False) + label.set_hexpand(False) main_win_obj = self.app_obj.main_win_obj dest_obj = self.app_obj.media_reg_dict[self.edit_obj.master_dbid] if isinstance(dest_obj, media.Channel): - icon_path3 = main_win_obj.icon_dict['channel_small'] + icon_path = main_win_obj.icon_dict['channel_small'] elif isinstance(dest_obj, media.Playlist): - icon_path3 = main_win_obj.icon_dict['playlist_small'] + icon_path = main_win_obj.icon_dict['playlist_small'] else: if dest_obj.priv_flag: - icon_path3 = main_win_obj.icon_dict['folder_red_small'] + icon_path = main_win_obj.icon_dict['folder_red_small'] elif dest_obj.temp_flag: - icon_path3 = main_win_obj.icon_dict['folder_blue_small'] + icon_path = main_win_obj.icon_dict['folder_blue_small'] elif dest_obj.fixed_flag: - icon_path3 = main_win_obj.icon_dict['folder_green_small'] + icon_path = main_win_obj.icon_dict['folder_green_small'] else: - icon_path3 = main_win_obj.icon_dict['folder_small'] + icon_path = main_win_obj.icon_dict['folder_small'] - frame3 = self.add_image(grid2, - icon_path3, - 1, 0, 1, 1, + frame = self.add_image(grid, + icon_path, + 1, 5, 1, 1, ) - frame3.set_size_request( + frame.set_size_request( 16 + (self.spacing_size * 2), -1, ) - entry6 = self.add_entry(grid2, + entry = self.add_entry(grid, None, - 2, 0, 1, 1, + 2, 5, 1, 1, ) - entry6.set_editable(False) - entry6.set_text(dest_obj.name) + entry.set_editable(False) + entry.set_text(dest_obj.name) - label5 = self.add_label(grid2, - 'Location on filesystem', - 0, 1, 1, 1, + label2 = self.add_label(grid, + _('Location'), + 0, 6, 1, 1, ) - label5.set_hexpand(False) + label2.set_hexpand(False) - entry7 = self.add_entry(grid2, + entry2 = self.add_entry(grid, None, - 1, 1, 2, 1, + 2, 6, 1, 1, ) - entry7.set_editable(False) - entry7.set_text(self.edit_obj.get_default_dir(self.app_obj)) + entry2.set_editable(False) + entry2.set_text(self.edit_obj.get_default_dir(self.app_obj)) def setup_download_options_tab(self): @@ -1436,29 +1441,29 @@ def setup_download_options_tab(self): Sets up the 'Download options' tab. """ - tab, grid = self.add_notebook_tab('Download _options') + tab, grid = self.add_notebook_tab(_('Download _options')) # Download options self.add_label(grid, - 'Download options', + '' + _('Download options') + '', 0, 0, 2, 1, ) - self.apply_options_button = Gtk.Button('Apply download options') + self.apply_options_button = Gtk.Button(_('Apply download options')) grid.attach(self.apply_options_button, 0, 1, 1, 1) self.apply_options_button.connect( 'clicked', self.on_button_apply_options_clicked, ) - self.edit_button = Gtk.Button('Edit download options') + self.edit_button = Gtk.Button(_('Edit download options')) grid.attach(self.edit_button, 1, 1, 1, 1) self.edit_button.connect( 'clicked', self.on_button_edit_options_clicked, ) - self.remove_button = Gtk.Button('Remove download options') + self.remove_button = Gtk.Button(_('Remove download options')) grid.attach(self.remove_button, 1, 2, 1, 1) self.remove_button.connect( 'clicked', @@ -1594,10 +1599,10 @@ def setup_button_strip(self): self.grid.attach(hbox, 0, 2, 1, 1) # 'OK' button - self.ok_button = Gtk.Button('OK') + self.ok_button = Gtk.Button(_('OK')) hbox.pack_end(self.ok_button, False, False, self.spacing_size) self.ok_button.get_child().set_width_chars(10) - self.ok_button.set_tooltip_text('Close this window'); + self.ok_button.set_tooltip_text(_('Close this window')); self.ok_button.connect('clicked', self.on_button_ok_clicked) @@ -1758,6 +1763,9 @@ def add_entry(self, grid, text, edit_flag, x, y, wid, hei): return entry +# def add_image # Inherited from GenericConfigWin + + def add_label(self, grid, text, x, y, wid, hei): """Called by various functions in the child preference window. @@ -1957,7 +1965,7 @@ class OptionsEditWin(GenericEditWin): def __init__(self, app_obj, edit_obj, media_data_obj=None): - Gtk.Window.__init__(self, title='Download options') + Gtk.Window.__init__(self, title=_('Download options')) # IV list - class objects # ----------------------- @@ -2074,7 +2082,7 @@ def apply_changes(self): # necessary self.edit_obj.rearrange_formats() # ...then redraw the textview in the Formats tab - self.redraw_formats_list() + self.formats_tab_redraw_list() def retrieve_val(self, name): @@ -2113,35 +2121,6 @@ def retrieve_val(self, name): ) - def redraw_formats_list(self): - - """Called by self.setup_formats_tab() and then again by - self.apply_changes(). - - Update the Gtk.ListStore containing the user's preferrerd video/audio - formats. - """ - - self.formats_liststore.clear() - - # There are three video format options, any or all of which might be - # set - val1 = self.retrieve_val('video_format') - val2 = self.retrieve_val('second_video_format') - val3 = self.retrieve_val('third_video_format') - - # (Need to reverse formats.VIDEO_OPTION_DICT for quick lookup) - rev_dict = {} - for key in formats.VIDEO_OPTION_DICT: - rev_dict[formats.VIDEO_OPTION_DICT[key]] = key - - if val1 != '0': - self.formats_liststore.append([rev_dict[val1]]) - if val2 != '0': - self.formats_liststore.append([rev_dict[val2]]) - if val3 != '0': - self.formats_liststore.append([rev_dict[val3]]) - # (Setup tabs) @@ -2174,13 +2153,13 @@ def setup_general_tab(self): Sets up the 'General' tab. """ - tab, grid = self.add_notebook_tab('_General') + tab, grid = self.add_notebook_tab(_('_General')) if self.media_data_obj: - parent_type = self.media_data_obj.get_type() + media_type = self.media_data_obj.get_type() self.add_label(grid, - 'General options', + '' + _('General options') + '', 0, 0, 2, 1, ) @@ -2191,19 +2170,26 @@ def setup_general_tab(self): if self.media_data_obj is None: - label.set_text('These options have been applied to:') + label.set_text(_('These options have been applied to:')) entry = self.add_entry(grid, None, 0, 2, 2, 1, ) - entry.set_text('All channels, playlists and folders') + entry.set_text(_('All channels, playlists and folders')) else: - label.set_text( - 'These options have been applied to the ' + parent_type + ':', - ) + if media_type == 'video': + string = 'These options have been applied to the video:' + if media_type == 'channel': + string = 'These options have been applied to the channel:' + elif media_type == 'playlist': + string = 'These options have been applied to the playlist:' + else: + string = 'These options have been applied to the folder:' + + label.set_text(string) entry = self.add_entry(grid, None, @@ -2223,8 +2209,10 @@ def setup_general_tab(self): entry2.set_text(self.media_data_obj.name) self.add_label(grid, + _( 'Extra youtube-dl command line options (e.g. --help; do not use' \ + ' -o or --output)', + ), 0, 3, 2, 1, ) @@ -2250,9 +2238,9 @@ def setup_general_tab(self): button = Gtk.Button() grid.attach(button, 1, 5, 1, 1) if not self.app_obj.simple_options_flag: - button.set_label('Hide advanced download options') + button.set_label(_('Hide advanced download options')) else: - button.set_label('Show advanced download options') + button.set_label(_('Show advanced download options')) button.connect('clicked', self.on_simple_options_clicked) frame2 = self.add_pixbuf(grid, @@ -2262,7 +2250,7 @@ def setup_general_tab(self): frame2.set_hexpand(False) button2 = Gtk.Button( - 'Import general download options into this window', + _('Import general download options into this window'), ) grid.attach(button2, 1, 6, 1, 1) button2.connect('clicked', self.on_clone_options_clicked) @@ -2277,7 +2265,7 @@ def setup_general_tab(self): frame3.set_hexpand(False) button3 = Gtk.Button( - 'Completely reset all download options to their default values', + _('Completely reset all download options to their default values'), ) grid.attach(button3, 1, 7, 1, 1) button3.connect('clicked', self.on_reset_options_clicked) @@ -2291,7 +2279,7 @@ def setup_files_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('_Files', 0) + tab, grid = self.add_notebook_tab(_('_Files'), 0) # ...and an inner notebook... inner_notebook = self.add_inner_notebook(grid) @@ -2310,18 +2298,21 @@ def setup_files_names_tab(self, inner_notebook): Sets up the 'File names' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('File _names', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('File _names'), + inner_notebook, + ) - grid_width = 5 + grid_width = 4 # File name options self.add_label(grid, - 'File name options', + '' + _('File name options') + '', 0, 0, grid_width, 1, ) self.add_label(grid, - 'Format for video file names', + _('Format for video file names'), 0, 1, grid_width, 1, ) @@ -2345,7 +2336,7 @@ def setup_files_names_tab(self, inner_notebook): # Signal connect appears below self.add_label(grid, - 'youtube-dl file output template', + _('youtube-dl file output template'), 0, 3, grid_width, 1, ) @@ -2356,120 +2347,121 @@ def setup_files_names_tab(self, inner_notebook): entry.set_text(current_template) # Signal connect appears below + # Signal connects from above + combo.connect('changed', self.on_file_tab_combo_changed, entry) + entry.connect('changed', self.on_file_tab_entry_changed) + + # Add widgets to a list, so we can sensitise them when a custom + # template is selected, and desensitise them the rest of the time + self.template_widget_list = [entry] + self.add_label(grid, - 'Add to template:', + _('Add to template:'), 0, 5, 1, 1, ) - store2 = Gtk.ListStore(str) - for string in ( - 'ID', - 'Title', - 'Ext', - 'Uploader', - 'Resolution', - 'Autonumber', - ): - store2.append( [string] ) + master_list = [ + _('Video properties'), + [ + 'id', _('Video ID'), + 'title', _('Video title'), + 'display_id', _('Alternative video ID'), + 'alt_title', _('Secondary video title'), + 'url', _('Video URL'), + 'ext', _('Video filename extension'), + 'license', _('Video licence'), + 'age_limit', _('Age restriction (years)'), + 'is_live', _('Is a livestream'), + 'autonumber', _('Autonumber videos, starting at 0'), + ], + _('Creator/uploader'), + [ + 'uploader', _('Full name of video uploader'), + 'uploader_id', _('Full name of video uploader'), + 'creator', _('Nickname/ID of video uploader'), + 'channel', _('Channel name'), + 'channel_id', _('Channel ID'), + 'playlist', _('Playlist name'), + 'playlist_id', _('Playlist ID'), + 'playlist_index', _('Video index in playlist'), + ], + _('Date/time/location'), + [ + 'release_date', _('Release date (YYYYMMDD)'), + 'timestamp', _('Release time (UNIX timestamp)'), + 'upload_date', _('Upload data (YYYYMMDD)'), + 'duration', _('Video length (seconds)'), + 'location', _('Filming location'), + ], + _('Video format'), + [ + 'format', _('Video format'), + 'format_id', _('youtube-dl format code'), + 'width', _('Video width'), + 'height', _('Video height'), + 'resolution', _('Video resolution'), + 'fps', _('Video frame rate'), + 'tbr', _('Average video/audio bitrate (KiB/s)'), + 'vbr', _('Average video bitrate (KiB/s)'), + 'abr', _('Average audio bitrate (KiB/s)'), + ], + _('Ratings/comments'), + [ + 'view_count', _('Number of views'), + 'like_count', _('Number of positive ratings'), + 'dislike_count', _('Number of negative ratings'), + 'average_rating', _('Average rating'), + 'repost_count', _('Number of reposts'), + 'comment_count', _('Number of comments'), + ], + ] - combo2 = Gtk.ComboBox.new_with_model(store2) - grid.attach(combo2, 1, 5, 1, 1) - renderer_text2 = Gtk.CellRendererText() - combo2.pack_start(renderer_text2, True) - combo2.add_attribute(renderer_text2, "text", 0) - combo2.set_entry_text_column(0) - combo2.set_active(0) - - button2 = Gtk.Button('Add') - grid.attach(button2, 2, 5, 1, 1) - # Signal connect appears below + row_num = 4 + while master_list: - store3 = Gtk.ListStore(str) - for string in ( - 'View Count', - 'Like Count', - 'Dislike Count', - 'Comment Count', - 'Average Rating', - 'Age Limit', - 'Width', - 'Height', - 'Extractor', - ): - store3.append( [string] ) - - combo3 = Gtk.ComboBox.new_with_model(store3) - grid.attach(combo3, 3, 5, 1, 1) - renderer_text3 = Gtk.CellRendererText() - combo3.pack_start(renderer_text3, True) - combo3.add_attribute(renderer_text3, "text", 0) - combo3.set_entry_text_column(0) - combo3.set_active(0) - - button3 = Gtk.Button('Add') - grid.attach(button3, 4, 5, 1, 1) - # Signal connect appears below + row_num += 1 - store4 = Gtk.ListStore(str) - for string in ( - 'View Count', - 'Like Count', - 'Dislike Count', - 'Comment Count', - 'Average Rating', - 'Age Limit', - 'Width', - 'Height', - 'Extractor', - ): - store4.append( [string] ) - - combo4 = Gtk.ComboBox.new_with_model(store4) - grid.attach(combo4, 1, 6, 1, 1) - renderer_text4 = Gtk.CellRendererText() - combo4.pack_start(renderer_text4, True) - combo4.add_attribute(renderer_text4, "text", 0) - combo4.set_entry_text_column(0) - combo4.set_active(0) - - button4 = Gtk.Button('Add') - grid.attach(button4, 2, 6, 1, 1) - # Signal connect appears below + this_title = master_list.pop(0) + this_store_list = master_list.pop(0) - # Signal connects from above - combo.connect('changed', self.on_file_tab_combo_changed, entry) - entry.connect('changed', self.on_file_tab_entry_changed) - button2.connect( - 'clicked', - self.on_file_tab_button_clicked, - entry, - combo2, - ) - button3.connect( - 'clicked', - self.on_file_tab_button_clicked, - entry, - combo3, - ) - button4.connect( - 'clicked', - self.on_file_tab_button_clicked, - entry, - combo4, - ) + this_store = Gtk.ListStore(str) + # (The dictionary is used by self.on_file_tab_button_clicked() to + # translate the visible string into the string youtube-dl uses) + this_store_dict = {} + while this_store_list: + item = this_store_list.pop(0) + mod_item = this_store_list.pop(0) - # Add widgets to a list, so we can sensitise them when a custom - # template is selected, and desensitise them the rest of the time - self.template_widget_list = [ - entry, - combo2, - combo3, - combo4, - button2, - button3, - button4, - ] + this_store_dict[mod_item] = item + this_store.append( [mod_item] ) + + self.add_label(grid, + this_title, + 1, row_num, 1, 1, + ) + + this_combo = Gtk.ComboBox.new_with_model(this_store) + grid.attach(this_combo, 2, row_num, 1, 1) + this_renderer_text = Gtk.CellRendererText() + this_combo.pack_start(this_renderer_text, True) + this_combo.add_attribute(this_renderer_text, "text", 0) + this_combo.set_entry_text_column(0) + this_combo.set_active(0) + + this_button = Gtk.Button(_('Add')) + grid.attach(this_button, 3, row_num, 1, 1) + this_button.connect( + 'clicked', + self.on_file_tab_button_clicked, + entry, + this_combo, + this_store_dict, + ) + self.template_widget_list.append(this_combo) + self.template_widget_list.append(this_button) + + # (De)sensitise widgets in self.template_widget_list if current_format == 0: self.file_tab_sensitise_widgets(True) else: @@ -2483,7 +2475,10 @@ def setup_files_filesystem_tab(self, inner_notebook): Sets up the 'Filesystem' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Filesystem', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('_Filesystem'), + inner_notebook, + ) grid_width = 2 @@ -2491,30 +2486,30 @@ def setup_files_filesystem_tab(self, inner_notebook): if not self.app_obj.simple_options_flag: self.add_label(grid, - 'Filesystem options', + '' + _('Filesystem options') + '', 0, 0, grid_width, 1, ) self.add_checkbutton(grid, - 'Restrict filenames to using ASCII characters', + _('Restrict filenames to ASCII characters'), 'restrict_filenames', 0, 1, grid_width, 1, ) self.add_checkbutton(grid, - 'Set the file modification time from the server', + _('Use the server\'s file modification time'), 'nomtime', 0, 2, grid_width, 1, ) # Filesystem overrides self.add_label(grid, - 'Filesystem overrides', + '' + _('Filesystem overrides') + '', 0, 3, grid_width, 1, ) checkbutton = self.add_checkbutton(grid, - 'Download all videos into this folder', + _('Download all videos into this folder'), None, 0, 4, 1, 1, ) @@ -2567,34 +2562,37 @@ def setup_files_write_files_tab(self, inner_notebook): Sets up the 'Write files' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Write files', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('_Write files'), + inner_notebook, + ) # Write other files options self.add_label(grid, - 'Write other file options', + '' + _('Write other file options') + '', 0, 0, 1, 1, ) self.add_checkbutton(grid, - 'Write video\'s description to a .description file', + _('Write video\'s description to a .description file'), 'write_description', 0, 1, 1, 1, ) self.add_checkbutton(grid, - 'Write video\'s metadata to an .info.json file', + _('Write video\'s metadata to an .info.json file'), 'write_info', 0, 2, 1, 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, ) self.add_checkbutton(grid, - 'Write the video\'s thumbnail to the same folder', + _('Write the video\'s thumbnail to the same folder'), 'write_thumbnail', 0, 4, 1, 1, ) @@ -2607,66 +2605,69 @@ def setup_files_keep_files_tab(self, inner_notebook): Sets up the 'Write files' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Keep files', inner_notebook) - - script = __main__.__prettyname__ + tab, grid = self.add_inner_notebook_tab( + _('_Keep files'), + inner_notebook, + ) # Options during real (not simulated) downloads self.add_label(grid, - 'Options during real (not simulated) downloads', + '' + _('Options during real (not simulated) downloads') \ + + '', 0, 0, 1, 1, ) self.add_checkbutton(grid, - 'Keep the description file after ' + script + ' shuts down', + _('Keep the description file after Tartube shuts down'), 'keep_description', 0, 1, 1, 1, ) self.add_checkbutton(grid, - 'Keep the metadata file after ' + script + ' shuts down', + _('Keep the metadata file after Tartube shuts down'), 'keep_info', 0, 2, 1, 1, ) self.add_checkbutton(grid, - 'Keep the annotations file after ' + script + ' shuts down', + _('Keep the annotations file after Tartube shuts down'), 'keep_annotations', 0, 3, 1, 1, ) self.add_checkbutton(grid, - 'Keep the thumbnail file after ' + script + ' shuts down', + _('Keep the thumbnail file after Tartube shuts down'), 'keep_thumbnail', 0, 4, 1, 1, ) # Options during simulated (not real) downloads self.add_label(grid, - 'Options during simulated (not real) downloads', + '' + _('Options during simulated (not real) downloads') \ + + '', 0, 5, 1, 1, ) self.add_checkbutton(grid, - 'Keep the description file after ' + script + ' shuts down', + _('Keep the description file after Tartube shuts down'), 'sim_keep_description', 0, 6, 1, 1, ) self.add_checkbutton(grid, - 'Keep the metadata file after ' + script + ' shuts down', + _('Keep the metadata file after Tartube shuts down'), 'sim_keep_info', 0, 7, 1, 1, ) self.add_checkbutton(grid, - 'Keep the annotations file after ' + script + ' shuts down', + _('Keep the annotations file after Tartube shuts down'), 'sim_keep_annotations', 0, 8, 1, 1, ) self.add_checkbutton(grid, - 'Keep the thumbnail file after ' + script + ' shuts down', + _('Keep the thumbnail file after Tartube shuts down'), 'sim_keep_thumbnail', 0, 9, 1, 1, ) @@ -2679,47 +2680,63 @@ def setup_formats_tab(self): Sets up the 'Formats' tab. """ - tab, grid = self.add_notebook_tab('F_ormats') + # Add this tab... + tab, grid = self.add_notebook_tab(_('F_ormats'), 0) + + # ...and an inner notebook... + inner_notebook = self.add_inner_notebook(grid) + + # ...with its own tabs + self.setup_formats_preferred_tab(inner_notebook) + if not self.app_obj.simple_options_flag: + self.setup_formats_advanced_tab(inner_notebook) + + + def setup_formats_preferred_tab(self, inner_notebook): + + """Called by self.setup_formats_tab(). + + Sets up the 'Preferred' inner notebook tab. + """ + + tab, grid = self.add_inner_notebook_tab( + _('_Preferred'), + inner_notebook, + ) + grid_width = 4 - grid.set_column_homogeneous(True) - # Format options + # Preferred format options self.add_label(grid, - 'Format options', + '' + _('Preferred format options') + '', 0, 0, 4, 1, ) - self.add_checkbutton(grid, - 'Download all available video formats', - 'all_formats', - 0, 1, grid_width, 1, - ) - # Left column label = self.add_label(grid, - 'Available video/audio formats', - 0, 2, 2, 1, + _('Recognised video/audio formats'), + 0, 1, 2, 1, ) treeview, liststore = self.add_treeview(grid, - 0, 3, 2, 1, + 0, 2, 2, 1, ) for key in formats.VIDEO_OPTION_LIST: liststore.append([key]) - button = Gtk.Button('Add format >>>') - grid.attach(button, 0, 4, 2, 1) + button = Gtk.Button(_('Add format') + ' >>>') + grid.attach(button, 0, 3, 2, 1) # Signal connect below # Right column label2 = self.add_label(grid, - 'Preference list (up to three formats)', - 2, 2, 2, 1, + _('List of preferred formats'), + 2, 1, 2, 1, ) treeview2, self.formats_liststore = self.add_treeview(grid, - 2, 3, 2, 1, + 2, 2, 2, 1, ) # (Need to reverse formats.VIDEO_OPTION_DICT for quick lookup) @@ -2729,18 +2746,18 @@ def setup_formats_tab(self): # There are three video format options, any or all of which might be # set - self.redraw_formats_list() + self.formats_tab_redraw_list() - button2 = Gtk.Button('<<< Remove format') - grid.attach(button2, 2, 4, 2, 1) + button2 = Gtk.Button('<<< ' + _('Remove format')) + grid.attach(button2, 2, 3, 2, 1) # Signal connect below - button3 = Gtk.Button('^ Move up') - grid.attach(button3, 2, 5, 1, 1) + button3 = Gtk.Button('^ ' + _('Move up')) + grid.attach(button3, 2, 4, 1, 1) # Signal connect below - button4 = Gtk.Button('v Move down') - grid.attach(button4, 3, 5, 1, 1) + button4 = Gtk.Button('v ' + _('Move down')) + grid.attach(button4, 3, 4, 1, 1) # Signal connect below # Signal connects from above @@ -2782,40 +2799,155 @@ def setup_formats_tab(self): button3.set_sensitive(False) button4.set_sensitive(False) - if format_count == 3: - button.set_sensitive(False) - # Now add other widgets - if not self.app_obj.simple_options_flag: + def setup_formats_advanced_tab(self, inner_notebook): - self.add_checkbutton(grid, - 'Prefer free video formats, unless one is specified above', - 'prefer_free_formats', - 0, 6, grid_width, 1, - ) + """Called by self.setup_formats_tab(). - self.add_checkbutton(grid, - 'Do not download DASH-related data on YouTube videos', - 'yt_skip_dash', - 0, 7, grid_width, 1, - ) + Sets up the 'Advanced' inner notebook tab. + """ - self.add_label(grid, - 'Output to this format, if merge required', - 0, 8, 2, 1, - ) + tab, grid = self.add_inner_notebook_tab( + _('_Advanced'), + inner_notebook, + ) - combo_list = ['', 'flv', 'mkv', 'mp4', 'ogg', 'webm'] - self.add_combo(grid, - combo_list, - 'merge_output_format', - 2, 8, 2, 1, - ) + grid_width = 2 + extra_row = 0 + # Multiple format options + self.add_label(grid, + '' + _('Multiple format options') + '', + 0, 0, grid_width, 1, + ) - def setup_downloads_tab(self): + if self.app_obj.allow_ytdl_archive_flag: - """Called by self.setup_tabs(). + extra_row = 1 + self.add_label(grid, + '' + _( + 'Multiple formats will not be downloaded, because' \ + + ' youtube-dl is creating an archive file' + ) + '\n' + _( + 'The archive file can be disabled in the System' \ + ' Preferences window', + ) + '', + 0, 1, grid_width, 1, + ) + + radiobutton = self.add_radiobutton(grid, + None, + _( + 'For each video, download the first available format from the' \ + + ' preferred list', + ), + None, + None, + 0, (1 + extra_row), grid_width, 1, + ) + if self.retrieve_val('video_format_mode') == 'single': + radiobutton.set_active(True) + # Signal connect appears below + + radiobutton2 = self.add_radiobutton(grid, + radiobutton, + _( + 'From the preferred list, download the first format that\'s' \ + + ' available for all videos', + ), + None, + None, + 0, (2 + extra_row), grid_width, 1, + ) + if self.retrieve_val('video_format_mode') == 'single_agree': + radiobutton2.set_active(True) + # Signal connect appears below + + radiobutton3 = self.add_radiobutton(grid, + radiobutton2, + _( + 'For each video, download all available formats from the' \ + + ' preferred list', + ), + None, + None, + 0, (3 + extra_row), grid_width, 1, + ) + if self.retrieve_val('video_format_mode') == 'multiple': + radiobutton3.set_active(True) + # Signal connect appears below + + radiobutton4 = self.add_radiobutton(grid, + radiobutton2, + _('Download all available formats for all videos'), + None, + None, + 0, (4 + extra_row), grid_width, 1, + ) + if self.retrieve_val('video_format_mode') == 'all': + radiobutton4.set_active(True) + # Signal connect appears below + + # Signal connects from above + radiobutton.connect( + 'toggled', + self.on_video_format_mode_toggled, + 'single', + ) + radiobutton2.connect( + 'toggled', + self.on_video_format_mode_toggled, + 'single_agree', + ) + radiobutton3.connect( + 'toggled', + self.on_video_format_mode_toggled, + 'multiple', + ) + radiobutton4.connect( + 'toggled', + self.on_video_format_mode_toggled, + 'all', + ) + + # Other format options + self.add_label(grid, + '' + _('Other format options') + '', + 0, (5 + extra_row), grid_width, 1, + ) + + self.add_checkbutton(grid, + _('Prefer free video formats, unless one is specified above'), + 'prefer_free_formats', + 0, (6 + extra_row), grid_width, 1, + ) + + self.add_checkbutton(grid, + _('Do not download DASH-related data for YouTube videos'), + 'yt_skip_dash', + 0, (7 + extra_row), grid_width, 1, + ) + + self.add_label(grid, + _( + 'If a merge is required after post-processing, output to this' \ + + ' format', + ), + 0, (8 + extra_row), 1, 1, + ) + + combo_list = ['', 'flv', 'mkv', 'mp4', 'ogg', 'webm'] + combo = self.add_combo(grid, + combo_list, + 'merge_output_format', + 1, (8 + extra_row), 1, 1, + ) + combo.set_hexpand(True) + + + def setup_downloads_tab(self): + + """Called by self.setup_tabs(). Sets up the 'Downloads' tab. """ @@ -2823,13 +2955,13 @@ def setup_downloads_tab(self): # Simple options only if self.app_obj.simple_options_flag: - tab, grid = self.add_notebook_tab('_Downloads') + tab, grid = self.add_notebook_tab(_('_Downloads')) row_count = 0 # Download options self.add_label(grid, - 'Download options', + '' + _('Download options') + '', 0, row_count, 1, 1, ) @@ -2842,7 +2974,7 @@ def setup_downloads_tab(self): else: # Add this tab... - tab, grid = self.add_notebook_tab('_Downloads', 0) + tab, grid = self.add_notebook_tab(_('_Downloads'), 0) # ...and an inner notebook... inner_notebook = self.add_inner_notebook(grid) @@ -2868,7 +3000,7 @@ def setup_downloads_general_tab(self, inner_notebook): # Download options self.add_label(grid, - 'Download options', + '' + _('Download options') + '', 0, 0, 1, 1, ) @@ -2884,7 +3016,10 @@ def setup_downloads_playlists_tab(self, inner_notebook): Sets up the 'Playlists' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Playlists', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('_Playlists'), + inner_notebook, + ) row_count = self.downloads_playlist_widgets(grid, 0) @@ -2896,7 +3031,10 @@ def setup_downloads_size_limits_tab(self, inner_notebook): Sets up the 'Size limits' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Size limits', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('_Size limits'), + inner_notebook, + ) row_count = self.downloads_size_limit_widgets(grid, 0) @@ -2908,7 +3046,7 @@ def setup_downloads_dates_tab(self, inner_notebook): Sets up the 'Dates' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Dates', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Dates'), inner_notebook) row_count = self.downloads_date_widgets(grid, 0) @@ -2920,7 +3058,7 @@ def setup_downloads_views_tab(self, inner_notebook): Sets up the 'Views' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Views', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Views'), inner_notebook) row_count = self.downloads_views_widgets(grid, 0) @@ -2932,7 +3070,10 @@ def setup_downloads_filtering_tab(self, inner_notebook): Sets up the 'Filtering' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Filtering', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('_Filtering'), + inner_notebook, + ) row_count = self.downloads_filtering_widgets(grid, 0) @@ -2944,7 +3085,7 @@ def setup_downloads_external_tab(self, inner_notebook): Sets up the 'External' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_External', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_External'), inner_notebook) row_count = self.downloads_external_widgets(grid, 0) @@ -2956,21 +3097,24 @@ def setup_sound_only_tab(self): Sets up the 'Sound Only' tab. """ - tab, grid = self.add_notebook_tab('_Sound only') + tab, grid = self.add_notebook_tab(_('_Sound only')) grid_width = 4 # Sound only options self.add_label(grid, - 'Sound only options', + '' + _('Sound only options') + '', 0, 0, grid_width, 1, ) # (The MS Windows installer includes FFmpeg) - text = 'Download each video, extract the sound, and then discard the' \ - + ' original videos' + text = _( + 'Download each video, extract the sound, and then discard the' \ + + ' original videos', + ) if os.name != 'nt': - text += '\n(requires that FFmpeg or AVConv is installed on your' \ - + ' system)' + text += '\n' + _( + '(requires that FFmpeg or AVConv is installed on your system)' + ) self.add_checkbutton(grid, text, @@ -2979,7 +3123,7 @@ def setup_sound_only_tab(self): ) label = self.add_label(grid, - 'Use this audio format: ', + _('Use this audio format:') + ' ', 0, 2, 1, 1, ) label.set_hexpand(False) @@ -2994,15 +3138,15 @@ def setup_sound_only_tab(self): combo.set_hexpand(True) label2 = self.add_label(grid, - 'Use this audio quality: ', + _('Use this audio quality:') + ' ', 2, 2, 1, 1, ) label2.set_hexpand(False) combo2_list = [ - ['High', '0'], - ['Medium', '5'], - ['Low', '9'], + [_('High'), '0'], + [_('Medium'), '5'], + [_('Low'), '9'], ] combo2 = self.add_combo_with_data(grid, @@ -3020,24 +3164,26 @@ def setup_post_process_tab(self): Sets up the 'Post-processing' tab. """ - tab, grid = self.add_notebook_tab('_Post-process') + tab, grid = self.add_notebook_tab(_('_Post-process')) grid_width = 2 grid.set_column_homogeneous(True) # Post-processing options self.add_label(grid, - 'Post-processing options', + '' + _('Post-processing options') + '', 0, 0, grid_width, 1, ) self.add_checkbutton(grid, + _( 'Post-process video files to convert them to audio-only files', + ), 'extract_audio', 0, 1, grid_width, 1, ) button = self.add_checkbutton(grid, - 'Prefer avconv over ffmpeg', + _('Prefer avconv over ffmpeg'), 'prefer_avconv', 0, 2, 1, 1, ) @@ -3045,7 +3191,7 @@ def setup_post_process_tab(self): 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, ) @@ -3053,7 +3199,7 @@ def setup_post_process_tab(self): button2.set_sensitive(False) self.add_label(grid, - 'Audio format of the post-processed file', + _('Audio format of the post-processed file'), 0, 3, 1, 1, ) @@ -3066,14 +3212,14 @@ def setup_post_process_tab(self): ) self.add_label(grid, - 'Audio quality of the post-processed file', + _('Audio quality of the post-processed file'), 0, 4, 1, 1, ) combo2_list = [ - ['High', '0'], - ['Medium', '5'], - ['Low', '9'], + [_('High'), '0'], + [_('Medium'), '5'], + [_('Low'), '9'], ] self.add_combo_with_data(grid, @@ -3083,7 +3229,7 @@ def setup_post_process_tab(self): ) self.add_label(grid, - 'Encode video to another format, if necessary', + _('Encode video to another format, if necessary'), 0, 5, 1, 1, ) @@ -3095,7 +3241,7 @@ def setup_post_process_tab(self): ) self.add_label(grid, - 'Arguments to pass to postprocessor', + _('Arguments to pass to post-processor'), 0, 6, 1, 1, ) @@ -3105,14 +3251,14 @@ def setup_post_process_tab(self): ) self.add_checkbutton(grid, - 'Keep video file after processing it', + _('Keep original file after processing it'), 'keep_video', 0, 7, 1, 1, ) # (This option can also be modified in the Post-process tab) self.embed_checkbutton = self.add_checkbutton(grid, - 'Merge subtitles file with video (.mp4 only)', + _('Merge subtitles file with video (.mp4 only)'), None, 1, 7, 1, 1, ) @@ -3123,24 +3269,29 @@ def setup_post_process_tab(self): ) self.add_checkbutton(grid, - 'Embed thumbnail in audio file as cover art', + _('Embed thumbnail in audio file as cover art'), 'embed_thumbnail', 0, 8, 1, 1, ) self.add_checkbutton(grid, - 'Write metadata to the video file', + _('Write metadata to the video file'), 'add_metadata', 1, 8, 1, 1, ) self.add_label(grid, - 'Automatically correct known faults of the file', + _('Automatically correct known faults of the file'), 0, 9, 1, 1, ) - combo_list4 = ['', 'never', 'warn', 'detect_or_warn'] - self.add_combo(grid, + combo_list4 = [ + ['', ''], + [_('Do nothing'), 'never'], + [_('Warn, but do nothing'), 'warn'], + [_('Fix if possible, otherwise warn'), 'detect_or_warn'], + ] + self.add_combo_with_data(grid, combo_list4, 'fixup_policy', 1, 9, 1, 1, @@ -3155,7 +3306,7 @@ def setup_subtitles_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('S_ubtitles', 0) + tab, grid = self.add_notebook_tab(_('S_ubtitles'), 0) # ...and an inner notebook... inner_notebook = self.add_inner_notebook(grid) @@ -3172,17 +3323,17 @@ def setup_subtitles_options_tab(self, inner_notebook): Sets up the 'Options' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Options', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Options'), inner_notebook) # Subtitles options self.add_label(grid, - 'Subtitles options', + '' + _('Subtitles options') + '', 0, 0, 2, 1, ) radiobutton = self.add_radiobutton(grid, None, - 'Don\'t download the subtitles file', + _('Don\'t download the subtitles file'), None, None, 0, 1, 2, 1, @@ -3193,7 +3344,7 @@ def setup_subtitles_options_tab(self, inner_notebook): radiobutton2 = self.add_radiobutton(grid, radiobutton, - 'Download the automatic subtitles file (YouTube only)', + _('Download the automatic subtitles file (YouTube only)'), None, None, 0, 2, 2, 1, @@ -3205,7 +3356,7 @@ def setup_subtitles_options_tab(self, inner_notebook): radiobutton3 = self.add_radiobutton(grid, radiobutton2, - 'Download all available subtitles files', + _('Download all available subtitles files'), None, None, 0, 3, 2, 1, @@ -3217,7 +3368,7 @@ def setup_subtitles_options_tab(self, inner_notebook): radiobutton4 = self.add_radiobutton(grid, radiobutton3, - 'Download subtitles file for these languages:', + _('Download subtitles file for these languages:'), None, None, 0, 4, 2, 1, @@ -3240,7 +3391,7 @@ def setup_subtitles_options_tab(self, inner_notebook): val = formats.LANGUAGE_CODE_DICT[key] rev_dict[val] = key - button = Gtk.Button('Add language >>>') + button = Gtk.Button(_('Add language') + ' >>>') grid.attach(button, 0, 6, 1, 1) # Signal connect below @@ -3253,7 +3404,7 @@ def setup_subtitles_options_tab(self, inner_notebook): for lang_code in lang_list: liststore2.append([rev_dict[lang_code]]) - button2 = Gtk.Button('<<< Remove language') + button2 = Gtk.Button('<<< ' + _('Remove language')) grid.attach(button2, 1, 6, 1, 1) # Signal connect below @@ -3311,19 +3462,21 @@ def setup_subtitles_more_options_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_More options', + _('_More options'), inner_notebook, ) # Subtitle format options self.add_label(grid, - 'Subtitle format options', + '' + _('Subtitle format options') + '', 0, 0, 1, 1, ) self.add_label(grid, + _( 'Preferred subtitle format(s), e.g. \'srt\', \'vtt\',' \ + ' \'srt/ass/vtt/lrc/best\'', + ), 0, 1, 1, 1, ) @@ -3334,18 +3487,19 @@ def setup_subtitles_more_options_tab(self, inner_notebook): # Post-processing options self.add_label(grid, - 'Post-processing options', + '' + _('Post-processing options') + '', 0, 3, 1, 1, ) self.add_label(grid, - 'Applies to .mp4 videos only; requires FFmpeg/AVConv', + '' + _('Applies to .mp4 videos only; requires FFmpeg/AVConv') \ + + '', 0, 4, 1, 1, ) # (This option can also be modified in the Post-process tab) self.embed_checkbutton2 = self.add_checkbutton(grid, - 'During post-processing, merge subtitles file with video', + _('During post-processing, merge subtitles file with video'), None, 0, 5, 1, 1, ) @@ -3364,40 +3518,40 @@ def setup_advanced_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('_Advanced', 0) + tab, grid = self.add_notebook_tab(_('_Advanced'), 0) # ...and an inner notebook... inner_notebook = self.add_inner_notebook(grid) # ...with its own tabs - self.setup_advanced_authentification_tab(inner_notebook) + self.setup_advanced_authentication_tab(inner_notebook) self.setup_advanced_network_tab(inner_notebook) self.setup_advanced_georestrict_tab(inner_notebook) self.setup_advanced_workaround_tab(inner_notebook) - def setup_advanced_authentification_tab(self, inner_notebook): + def setup_advanced_authentication_tab(self, inner_notebook): """Called by self.setup_advanced_tab(). - Sets up the 'Authentification' inner notebook tab. + Sets up the 'Authentication' inner notebook tab. """ tab, grid = self.add_inner_notebook_tab( - '_Authentification', + _('_Authentication'), inner_notebook, ) grid_width = 2 - # Authentification options + # Authentication options self.add_label(grid, - 'Authentification options', + '' + _('Authentication options') + '', 0, 0, grid_width, 1, ) self.add_label(grid, - 'Username with which to log in', + _('Username with which to log in'), 0, 1, 1, 1, ) @@ -3407,7 +3561,7 @@ def setup_advanced_authentification_tab(self, inner_notebook): ) self.add_label(grid, - 'Password with which to log in', + _('Password with which to log in'), 0, 2, 1, 1, ) @@ -3417,7 +3571,7 @@ def setup_advanced_authentification_tab(self, inner_notebook): ) self.add_label(grid, - 'Password required for this URL', + _('Password required for this URL'), 0, 3, 1, 1, ) @@ -3427,7 +3581,7 @@ def setup_advanced_authentification_tab(self, inner_notebook): ) self.add_label(grid, - 'Two-factor authentication code', + _('Two-factor authentication code'), 0, 4, 1, 1, ) @@ -3437,7 +3591,7 @@ def setup_advanced_authentification_tab(self, inner_notebook): ) self.add_checkbutton(grid, - 'Use .netrc authentication data', + _('Use .netrc authentication data'), 'force_ipv4', 0, 5, grid_width, 1, ) @@ -3450,18 +3604,18 @@ def setup_advanced_network_tab(self, inner_notebook): Sets up the 'Network' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Network', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Network'), inner_notebook) grid_width = 2 # Network options self.add_label(grid, - 'Network options', + '' + _('Network options') + '', 0, 6, grid_width, 1, ) self.add_label(grid, - 'Use this HTTP/HTTPS proxy', + _('Use this HTTP/HTTPS proxy'), 0, 7, 1, 1, ) @@ -3471,7 +3625,7 @@ def setup_advanced_network_tab(self, inner_notebook): ) self.add_label(grid, - 'Time to wait for socket connection, before giving up', + _('Time to wait for socket connection, before giving up'), 0, 8, 1, 1, ) @@ -3481,7 +3635,7 @@ def setup_advanced_network_tab(self, inner_notebook): ) self.add_label(grid, - 'Client-side IP address to which to bind', + _('Bind with this Client-side IP address'), 0, 9, 1, 1, ) @@ -3491,13 +3645,13 @@ def setup_advanced_network_tab(self, inner_notebook): ) self.add_checkbutton(grid, - 'Make all connections via IPv4', + _('Connect using IPv4 only'), 'force_ipv4', 0, 10, 1, 1, ) self.add_checkbutton(grid, - 'Make all connections via IPv6', + _('Connect using IPv6 only'), 'force_ipv6', 1, 10, 1, 1, ) @@ -3511,7 +3665,7 @@ def setup_advanced_georestrict_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Geo-restriction', + _('_Geo-restriction'), inner_notebook, ) @@ -3519,12 +3673,12 @@ def setup_advanced_georestrict_tab(self, inner_notebook): # Geo-restriction options self.add_label(grid, - 'Geo-restriction options', + '' + _('Geo-restriction options') + '', 0, 11, grid_width, 1, ) self.add_label(grid, - 'Use this proxy to verify IP address', + _('Use this proxy to verify IP address'), 0, 12, 1, 1, ) @@ -3534,19 +3688,19 @@ def setup_advanced_georestrict_tab(self, inner_notebook): ) self.add_checkbutton(grid, - 'Bypass via fake X-Forwarded-For HTTP header', + _('Bypass using fake X-Forwarded-For HTTP header'), 'geo_bypass', 0, 13, 1, 1, ) self.add_checkbutton(grid, - 'Don\'t bypass via fake HTTP header', + _('Don\'t bypass using fake HTTP header'), 'no_geo_bypass', 1, 13, 1, 1, ) self.add_label(grid, - 'Bypass geo-restriction with ISO 3166-2 country code', + _('Bypass geo-restriction with ISO 3166-2 country code'), 0, 14, 1, 1, ) @@ -3556,7 +3710,7 @@ def setup_advanced_georestrict_tab(self, inner_notebook): ) self.add_label(grid, - 'Bypass with explicit IP block in CIDR notation', + _('Bypass with explicit IP block in CIDR notation'), 0, 15, 1, 1, ) @@ -3579,12 +3733,12 @@ def setup_advanced_workaround_tab(self, inner_notebook): # Workaround options self.add_label(grid, - 'Workaround options', + '' + _('Workaround options') + '', 0, 16, grid_width, 1, ) self.add_label(grid, - 'Custom user agent for youtube-dl', + _('Custom user agent for youtube-dl'), 0, 17, 1, 1, ) @@ -3594,7 +3748,7 @@ def setup_advanced_workaround_tab(self, inner_notebook): ) self.add_label(grid, - 'Custom referer if video access has restricted domain', + _('Custom referer if video access has restricted domain'), 0, 18, 1, 1, ) @@ -3604,7 +3758,7 @@ def setup_advanced_workaround_tab(self, inner_notebook): ) self.add_label(grid, - 'Force this encoding (experimental)', + _('Force this encoding (experimental)'), 0, 19, 1, 1, ) @@ -3614,14 +3768,16 @@ def setup_advanced_workaround_tab(self, inner_notebook): ) self.add_checkbutton(grid, - 'Suppress HTTPS certificate validation', + _('Suppress HTTPS certificate validation'), 'no_check_certificate', 0, 20, grid_width, 1, ) self.add_checkbutton(grid, + _( 'Use an unencrypted connection to retrieve information about' \ + ' videos (YouTube only)', + ), 'prefer_insecure', 0, 21, grid_width, 1, ) @@ -3662,14 +3818,32 @@ def formats_tab_count_formats(self): """ - if self.retrieve_val('video_format') == '0': - return 0 - elif self.retrieve_val('second_video_format') == '0': - return 1 - elif self.retrieve_val('third_video_format') == '0': - return 2 - else: - return 3 + format_list = self.retrieve_val('video_format_list') + + return len(format_list) + + + def formats_tab_redraw_list(self): + + """Called by self.setup_formats_tab() and then again by + self.apply_changes(). + + Update the Gtk.ListStore containing the user's preferrerd video/audio + formats. + """ + + # Empty the treeview + self.formats_liststore.clear() + + # (Need to reverse formats.VIDEO_OPTION_DICT for quick lookup) + rev_dict = {} + for key in formats.VIDEO_OPTION_DICT: + rev_dict[formats.VIDEO_OPTION_DICT[key]] = key + + # Refill the treeview + format_list = self.retrieve_val('video_format_list') + for item in format_list: + self.formats_liststore.append([rev_dict[item]]) # (Tab support functions - Downloads tab) @@ -3682,31 +3856,31 @@ def downloads_general_widgets(self, grid, row_count): grid_width = 3 self.add_checkbutton(grid, - 'Prefer HLS (HTTP Live Streaming)', + _('Prefer HLS (HTTP Live Streaming)'), 'native_hls', 0, row_count, grid_width, 1, ) self.add_checkbutton(grid, - 'Prefer FFMpeg over native HLS downloader', + _('Prefer FFMpeg over native HLS downloader'), 'hls_prefer_ffmpeg', 0, (row_count + 1), grid_width, 1, ) self.add_checkbutton(grid, - 'Include advertisements (experimental feature)', + _('Include advertisements (experimental feature)'), 'include_ads', 0, (row_count + 2), grid_width, 1, ) self.add_checkbutton(grid, - 'Ignore errors and continue the download operation', + _('Ignore errors and continue the download operation'), 'ignore_errors', 0, (row_count + 3), grid_width, 1, ) self.add_label(grid, - 'Number of retries', + _('Number of retries'), 0, (row_count + 4), 1, 1, ) @@ -3726,7 +3900,7 @@ def downloads_age_widgets(self, grid, row_count): grid_width = 3 self.add_label(grid, - 'Download videos suitable for this age', + _('Download videos suitable for this age'), 0, row_count, 1, 1, ) @@ -3746,18 +3920,20 @@ def downloads_playlist_widgets(self, grid, row_count): # Playlist options self.add_label(grid, - 'Playlist options', + '' + _('Playlist options') + '', 0, row_count, grid_width, 1, ) self.add_label(grid, - 'youtube-dl treats channels and playlists the same way, so' \ - + ' these options can be used with both', + '' + _( + 'youtube-dl treats channels and playlists the same way, so' \ + + ' these options can be used with both', + ) + '', 0, (row_count + 1), grid_width, 1, ) self.add_label(grid, - 'Start downloading playlist from index', + _('Start downloading playlist from index'), 0, (row_count + 2), 1, 1, ) @@ -3768,7 +3944,7 @@ def downloads_playlist_widgets(self, grid, row_count): ) self.add_label(grid, - 'Stop downloading playlist at index', + _('Stop downloading playlist at index'), 0, (row_count + 3), 1, 1, ) @@ -3779,7 +3955,7 @@ def downloads_playlist_widgets(self, grid, row_count): ) self.add_label(grid, - 'Abort operation after downloading this many videos', + _('Abort operation after downloading this many videos'), 0, (row_count + 4), 1, 1, ) @@ -3790,19 +3966,19 @@ def downloads_playlist_widgets(self, grid, row_count): ) self.add_checkbutton(grid, - 'Abort downloading the playlist if an error occurs', + _('Abort downloading the playlist if an error occurs'), 'abort_on_error', 0, (row_count + 5), grid_width, 1, ) self.add_checkbutton(grid, - 'Download playlist in reverse order', + _('Download playlist in reverse order'), 'playlist_reverse', 0, (row_count + 6), grid_width, 1, ) self.add_checkbutton(grid, - 'Download playlist in random order', + _('Download playlist in random order'), 'playlist_random', 0, (row_count + 7), grid_width, 1, ) @@ -3817,12 +3993,12 @@ def downloads_size_limit_widgets(self, grid, row_count): grid_width = 3 self.add_label(grid, - 'Video size limit options', + '' + _('Video size limit options') + '', 0, row_count, grid_width, 1, ) self.add_label(grid, - 'Minimum file size for video downloads', + _('Minimum file size for video downloads'), 0, (row_count + 1), (grid_width - 2), 1, ) @@ -3839,7 +4015,7 @@ def downloads_size_limit_widgets(self, grid, row_count): ) self.add_label(grid, - 'Maximum file size for video downloads', + _('Maximum file size for video downloads'), 0, (row_count + 2), (grid_width - 2), 1, ) @@ -3866,12 +4042,12 @@ def downloads_date_widgets(self, grid, row_count): # Video date options self.add_label(grid, - 'Video date options', + '' + _('Video date options') + '', 0, row_count, grid_width, 1, ) self.add_label(grid, - 'Only videos uploaded on this date', + _('Only videos uploaded on this date'), 0, (row_count + 1), (grid_width - 2), 1, ) @@ -3881,7 +4057,7 @@ def downloads_date_widgets(self, grid, row_count): ) entry.set_editable(False) - button = Gtk.Button('Set') + button = Gtk.Button(_('Set')) grid.attach(button, (grid_width - 1), (row_count + 1), 1, 1) button.connect( 'clicked', @@ -3891,7 +4067,7 @@ def downloads_date_widgets(self, grid, row_count): ) self.add_label(grid, - 'Only videos uploaded before this date', + _('Only videos uploaded before this date'), 0, (row_count + 2), (grid_width - 2), 1, ) @@ -3901,7 +4077,7 @@ def downloads_date_widgets(self, grid, row_count): ) entry2.set_editable(False) - button2 = Gtk.Button('Set') + button2 = Gtk.Button(_('Set')) grid.attach(button2, (grid_width - 1), (row_count + 2), 1, 1) button2.connect( 'clicked', @@ -3911,7 +4087,7 @@ def downloads_date_widgets(self, grid, row_count): ) self.add_label(grid, - 'Only videos uploaded after this date', + _('Only videos uploaded after this date'), 0, (row_count + 3), (grid_width - 2), 1, ) @@ -3921,7 +4097,7 @@ def downloads_date_widgets(self, grid, row_count): ) entry3.set_editable(False) - button3 = Gtk.Button('Set') + button3 = Gtk.Button(_('Set')) grid.attach(button3, (grid_width - 1), (row_count + 3), 1, 1) button3.connect( 'clicked', @@ -3941,12 +4117,12 @@ def downloads_views_widgets(self, grid, row_count): # Video views options self.add_label(grid, - 'Video views options', + '' + _('Video views options') + '', 0, row_count, grid_width, 1, ) self.add_label(grid, - 'Minimum number of views', + _('Minimum number of views'), 0, (row_count + 1), (grid_width - 2), 1, ) @@ -3957,7 +4133,7 @@ def downloads_views_widgets(self, grid, row_count): ) self.add_label(grid, - 'Maximum number of views', + _('Maximum number of views'), 0, (row_count + 2), (grid_width - 2), 1, ) @@ -3982,12 +4158,12 @@ def downloads_filtering_widgets(self, grid, row_count): grid_width = 3 self.add_label(grid, - 'Video filtering options', + '' + _('Video filtering options') + '', 0, row_count, grid_width, 1, ) self.add_label(grid, - 'Download only matching titles (regex or caseless substring)', + _('Download only matching titles (regex or caseless substring)'), 0, (row_count + 1), grid_width, 1, ) @@ -3997,8 +4173,10 @@ def downloads_filtering_widgets(self, grid, row_count): ) self.add_label(grid, + _( 'Don\'t download only matching titles (regex or caseless' \ + ' substring)', + ), 0, (row_count + 3), grid_width, 1, ) @@ -4008,7 +4186,7 @@ def downloads_filtering_widgets(self, grid, row_count): ) self.add_label(grid, - 'Generic video filter, for example: like_count > 100', + _('Generic video filter, for example:') + ' like_count > 100', 0, (row_count + 5), grid_width, 1, ) @@ -4028,12 +4206,12 @@ def downloads_external_widgets(self, grid, row_count): # External downloader options self.add_label(grid, - 'External downloader options', + '' + _('External downloader options') + '', 0, row_count, grid_width, 1, ) self.add_label(grid, - 'Use this external downloader', + _('Use this external downloader'), 0, (row_count + 1), 1, 1, ) @@ -4050,7 +4228,7 @@ def downloads_external_widgets(self, grid, row_count): combo.set_hexpand(True) self.add_label(grid, - 'Arguments to pass to external downloader', + _('Arguments to pass to external downloader'), 0, (row_count + 2), grid_width, 1, ) @@ -4122,8 +4300,10 @@ def on_clone_options_clicked(self, button): # data object (this function can't be called for the General Options # Manager) self.app_obj.dialogue_manager_obj.show_msg_dialogue( - 'This procedure cannot be reversed.' \ - + ' Are you sure you want to continue?', + _( + 'This procedure cannot be reversed. Are you sure you want to' \ + + ' continue?', + ), 'question', 'yes-no', self, # Parent window is this window @@ -4228,7 +4408,7 @@ def on_fixed_folder_toggled(self, checkbutton, combo): combo.set_sensitive(True) - def on_file_tab_button_clicked(self, button, entry, combo): + def on_file_tab_button_clicked(self, button, entry, combo, trans_dict): """Called by callback in self.setup_files_names_tab(). @@ -4241,14 +4421,16 @@ def on_file_tab_button_clicked(self, button, entry, combo): combo (Gtk.ComboBox): Another widget to be modified by this function + trans_dict (dict): Converts a translated string into the string + used by youtube-dl + """ tree_iter = combo.get_active_iter() model = combo.get_model() - label = model[tree_iter][0] + label = trans_dict[model[tree_iter][0]] # (Code adapted from youtube-dl-gui's GeneralTab._on_template) - label = label.lower().replace(' ', '_') if label == "ext": prefix = '.' else: @@ -4338,27 +4520,13 @@ def on_formats_tab_add_clicked(self, add_button, remove_button, \ # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18' extract_code = formats.VIDEO_OPTION_DICT[name] - # There are three video format options; set the first one whose value - # is not already 0 - val1 = self.retrieve_val('video_format') - val2 = self.retrieve_val('second_video_format') - val3 = self.retrieve_val('third_video_format') - # Check the user's choice of format hasn't already been added - if extract_code == val1 or extract_code == val2 \ - or extract_code == val3: + # Update the option + format_list = self.retrieve_val('video_format_list') + if extract_code in format_list: return - - if val1 == '0': - self.edit_dict['video_format'] = extract_code - elif val2 == '0': - self.edit_dict['second_video_format'] = extract_code - elif val3 == '0': - self.edit_dict['third_video_format'] = extract_code - add_button.set_sensitive(False) else: - # 'add_button' should be desensitised, but if clicked, just ignore - # it - return + format_list.append(extract_code) + self.edit_dict['video_format_list'] = format_list # Update the other treeview, adding the format to it (and don't modify # this treeview) @@ -4397,36 +4565,24 @@ def on_formats_tab_down_clicked(self, down_button, treeview): # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18' extract_code = formats.VIDEO_OPTION_DICT[name] - # There are three video format options; the selected one might be any - # of them - val1 = self.retrieve_val('video_format') - val2 = self.retrieve_val('second_video_format') - val3 = self.retrieve_val('third_video_format') - - if extract_code == val3: - # Can't move the last item down - return - - else: + # Update the option + format_list = self.retrieve_val('video_format_list') + if extract_code in format_list: - if extract_code == val2: - self.edit_dict['second_video_format'] = val3 - self.edit_dict['third_video_format'] = val2 + index = format_list.index(extract_code) + if index < (len(format_list) - 1): + format_list.remove(extract_code) + format_list.insert((index + 1), extract_code) - elif extract_code == val1: - self.edit_dict['video_format'] = val2 - self.edit_dict['second_video_format'] = val1 + self.edit_dict['video_format_list'] = format_list - else: - # This should not be possible - return - - this_path = path_list[0] - next_path = this_path[0]+1 - model.move_after( - model.get_iter(this_path), - model.get_iter(next_path), - ) + # Update the other treeview + this_path = path_list[0] + next_path = this_path[0]+1 + model.move_after( + model.get_iter(this_path), + model.get_iter(next_path), + ) def on_formats_tab_remove_clicked(self, remove_button, add_button, \ @@ -4459,36 +4615,24 @@ def on_formats_tab_remove_clicked(self, remove_button, add_button, \ # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18' extract_code = formats.VIDEO_OPTION_DICT[name] - # There are three video format options; the selected one might be any - # of them - val1 = self.retrieve_val('video_format') - val2 = self.retrieve_val('second_video_format') - val3 = self.retrieve_val('third_video_format') - - if extract_code == val1: - self.edit_dict['video_format'] = val2 - self.edit_dict['second_video_format'] = val3 - self.edit_dict['third_video_format'] = '0' - elif extract_code == val2: - self.edit_dict['second_video_format'] = val3 - self.edit_dict['third_video_format'] = '0' - elif extract_code == val3: - self.edit_dict['third_video_format'] = '0' - else: - # This should not be possible - return + # Update the option + format_list = self.retrieve_val('video_format_list') + if extract_code in format_list: + format_list.remove(extract_code) - # Update the right-hand side treeview - model.remove(iter) + self.edit_dict['video_format_list'] = format_list - # Update other widgets, as required - add_button.set_sensitive(True) - if self.retrieve_val('video_format') == '0': + # Update the right-hand side treeview + model.remove(iter) - # No formats left to remove - remove_button.set_sensitive(False) - up_button.set_sensitive(False) - down_button.set_sensitive(False) + # Update other widgets, as required + add_button.set_sensitive(True) + if not format_list: + + # No formats left to remove + remove_button.set_sensitive(False) + up_button.set_sensitive(False) + down_button.set_sensitive(False) def on_formats_tab_up_clicked(self, up_button, treeview): @@ -4518,36 +4662,24 @@ def on_formats_tab_up_clicked(self, up_button, treeview): # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18' extract_code = formats.VIDEO_OPTION_DICT[name] - # There are three video format options; the selected one might be any - # of them - val1 = self.retrieve_val('video_format') - val2 = self.retrieve_val('second_video_format') - val3 = self.retrieve_val('third_video_format') - - if extract_code == val1: - # Can't move the first item up - return - - else: + # Update the option + format_list = self.retrieve_val('video_format_list') + if extract_code in format_list: - if extract_code == val2: - self.edit_dict['video_format'] = val2 - self.edit_dict['second_video_format'] = val1 + index = format_list.index(extract_code) + if index > 0: + format_list.remove(extract_code) + format_list.insert((index - 1), extract_code) - elif extract_code == val3: - self.edit_dict['second_video_format'] = val3 - self.edit_dict['third_video_format'] = val2 + self.edit_dict['video_format_list'] = format_list - else: - # This should not be possible - return - - this_path = path_list[0] - prev_path = this_path[0]-1 - model.move_before( - model.get_iter(this_path), - model.get_iter(prev_path), - ) + # Update the other treeview + this_path = path_list[0] + prev_path = this_path[0]-1 + model.move_before( + model.get_iter(this_path), + model.get_iter(prev_path), + ) def on_reset_options_clicked(self, button): @@ -4560,8 +4692,10 @@ def on_reset_options_clicked(self, button): """ - msg = 'This procedure cannot be reversed.' \ - + ' Are you sure you want to continue?', + msg = _( + 'This procedure cannot be reversed. Are you sure you want to' \ + + ' continue?', + ) if self.media_data_obj is None: @@ -4618,15 +4752,17 @@ def on_simple_options_clicked(self, button): # them, so wait for the window to close and be re-opened, # before switching between simple/advanced options self.app_obj.dialogue_manager_obj.show_msg_dialogue( + _( 'When the window is re-opened, some download options' \ + ' will be hidden', + ), 'info', 'ok', self, # Parent window is this window ) button.set_label( - 'Show advanced download options (when window re-opens)', + _('Show advanced download options (when window re-opens)'), ) else: @@ -4638,15 +4774,17 @@ def on_simple_options_clicked(self, button): else: self.app_obj.dialogue_manager_obj.show_msg_dialogue( + _( 'When the window is re-opened, all download options' \ - + ' will bevisible', + + ' will be visible', + ), 'info', 'ok', self, # Parent window is this window ) button.set_label( - 'Hide advanced download options (when window re-opens)', + _('Hide advanced download options (when window re-opens)'), ) @@ -4809,6 +4947,22 @@ def on_subtitles_toggled(self, radiobutton, button, button2, prop): button2.set_sensitive(True) + def on_video_format_mode_toggled(self, radiobutton, value): + + """Called by callback in self.setup_formats_advanced_tab(). + + Args: + + radiobutton (Gtk.RadioButton): The widget clicked + + prop (str): The attribute in self.edit_dict to modify + + """ + + if radiobutton.get_active(): + self.edit_dict['video_format_mode'] = value + + class VideoEditWin(GenericEditWin): """Python class for an 'edit window' to modify values in a media.Video @@ -4829,7 +4983,7 @@ class VideoEditWin(GenericEditWin): def __init__(self, app_obj, edit_obj): - Gtk.Window.__init__(self, title='Video properties') + Gtk.Window.__init__(self, title=_('Video properties')) # IV list - class objects # ----------------------- @@ -4928,6 +5082,7 @@ def setup_tabs(self): self.setup_general_tab() self.setup_download_options_tab() + self.setup_livestream_tab() self.setup_descrip_tab() self.setup_errors_warnings_tab() @@ -4939,142 +5094,141 @@ def setup_general_tab(self): Sets up the 'General' tab. """ - tab, grid = self.add_notebook_tab('_General') + tab, grid = self.add_notebook_tab(_('_General')) self.add_label(grid, - 'General properties', - 0, 0, 2, 1, + '' + _('General properties') + '', + 0, 0, 3, 1, ) # The first sets of widgets are shared by multiple edit windows self.add_container_properties(grid) self.add_source_properties(grid) - label3 = self.add_label(grid, - 'File', - 0, 5, 1, 1, + label = self.add_label(grid, + _('File'), + 0, 5, 2, 1, ) - label3.set_hexpand(False) + label.set_hexpand(False) - entry6 = self.add_entry(grid, + entry = self.add_entry(grid, None, - 1, 5, 2, 1, + 2, 5, 1, 1, ) - entry6.set_editable(False) + entry.set_editable(False) if self.edit_obj.file_name: - entry6.set_text(self.edit_obj.get_actual_path(self.app_obj)) + entry.set_text(self.edit_obj.get_actual_path(self.app_obj)) # To avoid messing up the neat format of the rows above, add another # grid, and put the next set of widgets inside it - grid3 = Gtk.Grid() - grid.attach(grid3, 0, 6, 3, 1) - grid3.set_vexpand(False) - grid3.set_border_width(self.spacing_size) - grid3.set_column_spacing(self.spacing_size) - grid3.set_row_spacing(self.spacing_size) - - checkbutton = self.add_checkbutton(grid3, - 'Always simulate download of this video', + grid2 = Gtk.Grid() + grid.attach(grid2, 0, 6, 3, 1) + grid2.set_vexpand(False) + grid2.set_column_spacing(self.spacing_size) + grid2.set_row_spacing(self.spacing_size) + + checkbutton = self.add_checkbutton(grid2, + _('Always simulate download of this video'), 'dl_sim_flag', 0, 0, 2, 1, ) checkbutton.set_sensitive(False) - label4 = self.add_label(grid3, - 'Duration', + label2 = self.add_label(grid2, + _('Duration'), 2, 0, 1, 1, ) - label4.set_hexpand(False) + label2.set_hexpand(False) - entry7 = self.add_entry(grid3, + entry2 = self.add_entry(grid2, None, 3, 0, 1, 1, ) - entry7.set_editable(False) + entry2.set_editable(False) if self.edit_obj.duration is not None: - entry7.set_text( + entry2.set_text( utils.convert_seconds_to_string(self.edit_obj.duration), ) - checkbutton2 = self.add_checkbutton(grid3, - 'Video has been downloaded', + checkbutton2 = self.add_checkbutton(grid2, + _('Video has been downloaded'), 'dl_flag', 0, 1, 2, 1, ) checkbutton2.set_sensitive(False) - label5 = self.add_label(grid3, - 'File size', + label3 = self.add_label(grid2, + _('File size'), 2, 1, 1, 1, ) - label5.set_hexpand(False) + label3.set_hexpand(False) - entry8 = self.add_entry(grid3, + entry3 = self.add_entry(grid2, None, 3, 1, 1, 1, ) - entry8.set_editable(False) + entry3.set_editable(False) if self.edit_obj.file_size is not None: - entry8.set_text(self.edit_obj.get_file_size_string()) + entry3.set_text(self.edit_obj.get_file_size_string()) - checkbutton3 = self.add_checkbutton(grid3, - 'Video is marked as unwatched', + checkbutton3 = self.add_checkbutton(grid2, + _('Video is marked as unwatched'), 'new_flag', 0, 2, 2, 1, ) checkbutton3.set_sensitive(False) - label6 = self.add_label(grid3, - 'Upload time', + label4 = self.add_label(grid2, + _('Upload time'), 2, 2, 1, 1, ) - label6.set_hexpand(False) + label4.set_hexpand(False) - entry9 = self.add_entry(grid3, + entry4 = self.add_entry(grid2, None, 3, 2, 1, 1, ) - entry9.set_editable(False) + entry4.set_editable(False) if self.edit_obj.upload_time is not None: - entry9.set_text(self.edit_obj.get_upload_time_string()) + entry4.set_text(self.edit_obj.get_upload_time_string()) - checkbutton4 = self.add_checkbutton(grid3, - 'Video is archived', + checkbutton4 = self.add_checkbutton(grid2, + _('Video is archived'), 'archive_flag', 0, 3, 1, 1, ) checkbutton4.set_sensitive(False) - checkbutton5 = self.add_checkbutton(grid3, - 'Video is bookmarked', + checkbutton5 = self.add_checkbutton(grid2, + _('Video is bookmarked'), 'bookmark_flag', 1, 3, 1, 1, ) checkbutton5.set_sensitive(False) - label7 = self.add_label(grid3, - 'Receive time', + label5 = self.add_label(grid2, + _('Receive time'), 2, 3, 1, 1, ) - label7.set_hexpand(False) + label5.set_hexpand(False) - entry10 = self.add_entry(grid3, + entry5 = self.add_entry(grid2, None, 3, 3, 1, 1, ) - entry10.set_editable(False) + entry5.set_editable(False) if self.edit_obj.receive_time is not None: - entry10.set_text(self.edit_obj.get_receive_time_string()) + entry5.set_text(self.edit_obj.get_receive_time_string()) - checkbutton6 = self.add_checkbutton(grid3, - 'Video is favourite', + checkbutton6 = self.add_checkbutton(grid2, + _('Video is favourite'), 'fav_flag', 0, 4, 1, 1, ) checkbutton6.set_sensitive(False) - checkbutton7 = self.add_checkbutton(grid3, - 'Video is in waiting list', + checkbutton7 = self.add_checkbutton(grid2, + _('Video is in waiting list'), 'waiting_flag', 1, 4, 1, 1, ) @@ -5084,6 +5238,97 @@ def setup_general_tab(self): # def setup_download_options_tab(): # Inherited from GenericConfigWin + def setup_livestream_tab(self): + + """Called by self.setup_tabs(). + + Sets up the 'Livestream' tab. + """ + + tab, grid = self.add_notebook_tab(_('_Livestream')) + + grid_width = 2 + + # Livestream properties + self.add_label(grid, + '' + _('Livestream properties') + '', + 0, 0, grid_width, 1, + ) + + label = self.add_label(grid, + _('Livestream status'), + 0, 1, 1, 1, + ) + label.set_hexpand(False) + + entry = self.add_entry(grid, + None, + 1, 1, 1, 1, + ) + entry.set_editable(False) + if self.edit_obj.live_mode == 1: + entry.set_text(_('Waiting to start')) + elif self.edit_obj.live_mode == 2: + entry.set_text(_('Stream has started')) + else: + entry.set_text(_('Not a livestream')) + + if self.edit_obj.live_mode: + + checkbutton = Gtk.CheckButton() + grid.attach(checkbutton, 0, 2, grid_width, 1) + checkbutton.set_label( + _('When the livestream starts, show a desktop notification'), + ) + if self.edit_obj.dbid in self.app_obj.media_reg_auto_notify_dict: + checkbutton.set_active(True) + checkbutton.set_sensitive(False) + + checkbutton2 = Gtk.CheckButton() + grid.attach(checkbutton2, 0, 3, grid_width, 1) + checkbutton2.set_label( + _('When the livestream starts, play an alarm'), + ) + if self.edit_obj.dbid in self.app_obj.media_reg_auto_alarm_dict: + checkbutton2.set_active(True) + checkbutton2.set_sensitive(False) + + checkbutton3 = Gtk.CheckButton() + grid.attach(checkbutton3, 0, 4, grid_width, 1) + checkbutton3.set_label( + _( + 'When the livestream starts, open it in the system\'s web' \ + + ' browser', + ), + ) + if self.edit_obj.dbid in self.app_obj.media_reg_auto_open_dict: + checkbutton3.set_active(True) + checkbutton3.set_sensitive(False) + + checkbutton4 = Gtk.CheckButton() + grid.attach(checkbutton4, 0, 5, grid_width, 1) + checkbutton4.set_label( + _( + 'When the livestream starts, begin downloading it immediately', + ), + ) + if self.edit_obj.dbid in self.app_obj.media_reg_auto_dl_start_dict: + checkbutton4.set_active(True) + checkbutton4.set_sensitive(False) + + checkbutton5 = Gtk.CheckButton() + grid.attach(checkbutton5, 0, 6, grid_width, 1) + checkbutton5.set_label( + _( + 'When a livestream stops, download it (overwriting any' \ + + ' earlier file)', + ), + ) + if self.edit_obj.dbid in self.app_obj.media_reg_auto_dl_stop_dict: + checkbutton5.set_active(True) + checkbutton5.set_sensitive(False) + + def setup_descrip_tab(self): """Called by self.setup_tabs(). @@ -5091,10 +5336,11 @@ def setup_descrip_tab(self): Sets up the 'Description' tab. """ - tab, grid = self.add_notebook_tab('_Description') + tab, grid = self.add_notebook_tab(_('_Description')) + # Video description self.add_label(grid, - 'Video description', + '' + _('Video description') + '', 0, 0, 1, 1, ) @@ -5112,16 +5358,18 @@ def setup_errors_warnings_tab(self): Sets up the 'Errors / Warnings' tab. """ - tab, grid = self.add_notebook_tab('_Errors / Warnings') + tab, grid = self.add_notebook_tab(_('_Errors / Warnings')) self.add_label(grid, - 'Errors / Warnings', + '' + _('Errors / Warnings') + '', 0, 0, 1, 1, ) self.add_label(grid, - 'Error messages produced the last time this video was' \ - + ' checked/downloaded', + '' + _( + 'Error messages produced the last time this video was' \ + + ' checked/downloaded', + ) + '', 0, 1, 1, 1, ) @@ -5133,8 +5381,10 @@ def setup_errors_warnings_tab(self): textview.set_wrap_mode(Gtk.WrapMode.WORD) self.add_label(grid, - 'Warning messages produced the last time this video was' \ - + ' checked/downloaded', + '' + _( + 'Warning messages produced the last time this video was' \ + + ' checked/downloaded', + ) + '', 0, 3, 1, 1, ) @@ -5188,10 +5438,10 @@ def __init__(self, app_obj, edit_obj): if isinstance(edit_obj, media.Channel): media_type = 'channel' - win_title = 'Channel properties' + win_title = _('Channel properties') else: media_type = 'playlist' - win_title = 'Playlist properties' + win_title = _('Playlist properties') Gtk.Window.__init__(self, title=win_title) @@ -5292,6 +5542,7 @@ def setup_tabs(self): self.setup_general_tab() self.setup_download_options_tab() + self.setup_rss_feed_tab() self.setup_errors_warnings_tab() @@ -5302,10 +5553,10 @@ def setup_general_tab(self): Sets up the 'General' tab. """ - tab, grid = self.add_notebook_tab('_General') + tab, grid = self.add_notebook_tab(_('_General')) self.add_label(grid, - 'General properties', + '' + _('General properties') + '', 0, 0, 3, 1, ) @@ -5316,121 +5567,198 @@ def setup_general_tab(self): # To avoid messing up the neat format of the rows above, add another # grid, and put the next set of widgets inside it - grid3 = Gtk.Grid() - grid.attach(grid3, 0, 6, 3, 1) - grid3.set_vexpand(False) - grid3.set_column_spacing(self.spacing_size) - grid3.set_row_spacing(self.spacing_size) - - checkbutton = self.add_checkbutton(grid3, - 'Always simulate download of videos in this ' + self.media_type, + grid2 = Gtk.Grid() + grid.attach(grid2, 0, 7, 3, 1) + grid2.set_vexpand(False) + grid2.set_column_spacing(self.spacing_size) + grid2.set_row_spacing(self.spacing_size) + + if self.media_type == 'channel': + string = _('Always simulate download of videos in this channel') + else: + string = _('Always simulate download of videos in this playlist') + + checkbutton = self.add_checkbutton(grid2, + string, 'dl_sim_flag', 0, 0, 1, 1, ) checkbutton.set_sensitive(False) - checkbutton2 = self.add_checkbutton(grid3, - 'Disable checking/downloading for this ' + self.media_type, + if self.media_type == 'channel': + string = _('Disable checking/downloading for this channel') + else: + string = _('Disable checking/downloading for this playlist') + + checkbutton2 = self.add_checkbutton(grid2, + string, 'dl_disable_flag', 0, 1, 1, 1, ) checkbutton2.set_sensitive(False) - checkbutton3 = self.add_checkbutton(grid3, - 'This ' + self.media_type + ' is marked as a favourite', + if self.media_type == 'channel': + string = _('This channel is marked as a favourite') + else: + string = _('This playlist is marked as a favourite') + + checkbutton3 = self.add_checkbutton(grid2, + string, 'fav_flag', 0, 2, 1, 1, ) checkbutton3.set_sensitive(False) - self.add_label(grid3, - 'Total videos', + self.add_label(grid2, + _('Total videos'), 1, 0, 1, 1, ) - entry8 = self.add_entry(grid3, + entry = self.add_entry(grid2, 'vid_count', 2, 0, 1, 1, ) - entry8.set_editable(False) - entry8.set_width_chars(8) - entry8.set_hexpand(False) + entry.set_editable(False) + entry.set_width_chars(8) + entry.set_hexpand(False) - self.add_label(grid3, - 'New videos', + self.add_label(grid2, + _('New videos'), 1, 1, 1, 1, ) - entry9 = self.add_entry(grid3, + entry2 = self.add_entry(grid2, 'new_count', 2, 1, 1, 1, ) - entry9.set_editable(False) - entry9.set_width_chars(8) - entry9.set_hexpand(False) + entry2.set_editable(False) + entry2.set_width_chars(8) + entry2.set_hexpand(False) - self.add_label(grid3, - 'Favourite videos', + self.add_label(grid2, + _('Favourite videos'), 1, 2, 1, 1, ) - entry10 = self.add_entry(grid3, + entry3 = self.add_entry(grid2, 'fav_count', 2, 2, 1, 1, ) - entry10.set_editable(False) - entry10.set_width_chars(8) - entry10.set_hexpand(False) + entry3.set_editable(False) + entry3.set_width_chars(8) + entry3.set_hexpand(False) - self.add_label(grid3, - 'Downloaded videos', + self.add_label(grid2, + _('Downloaded videos'), 1, 3, 1, 1, ) - entry11 = self.add_entry(grid3, + entry4 = self.add_entry(grid2, 'dl_count', 2, 3, 1, 1, ) - entry11.set_editable(False) - entry11.set_width_chars(8) - entry11.set_hexpand(False) + entry4.set_editable(False) + entry4.set_width_chars(8) + entry4.set_hexpand(False) # def setup_download_options_tab(): # Inherited from GenericConfigWin - def setup_errors_warnings_tab(self): + def setup_rss_feed_tab(self): """Called by self.setup_tabs(). - Sets up the 'Errors / Warnings' tab. + Sets up the 'RSS feed' tab. """ - tab, grid = self.add_notebook_tab('_Errors / Warnings') + tab, grid = self.add_notebook_tab(_('_RSS feed')) self.add_label(grid, - 'Errors / Warnings', + '' + _('RSS feed') + '', 0, 0, 1, 1, ) + if self.media_type == 'channel': + string = _( + 'If Tartube cannot detect the channel\'s RSS feed, you' \ + + ' can enter the URL here', + ) + else: + string = _( + 'If Tartube cannot detect the playlist\'s RSS feed, you' \ + + ' can enter the URL here', + ) + + string2 = _( + '(The feed is used to detect livestreams on compatible websites)', + ) + self.add_label(grid, - 'Error messages produced the last time this ' \ - + self.media_type + ' was checked/downloaded', + '' + string + '\n' + string2 + '', 0, 1, 1, 1, ) - textview, textbuffer = self.add_textview(grid, - 'error_list', + entry = self.add_entry(grid, + 'rss', 0, 2, 1, 1, ) - textview.set_editable(False) - textview.set_wrap_mode(Gtk.WrapMode.WORD) + entry.set_editable(True) + entry.set_hexpand(True) - self.add_label(grid, - 'Warning messages produced the last time this ' \ - + self.media_type + ' was checked/downloaded', - 0, 3, 1, 1, - ) - textview2, textbuffer2 = self.add_textview(grid, - 'warning_list', - 0, 4, 1, 1, + def setup_errors_warnings_tab(self): + + """Called by self.setup_tabs(). + + Sets up the 'Errors / Warnings' tab. + """ + + tab, grid = self.add_notebook_tab(_('_Errors / Warnings')) + + self.add_label(grid, + '' + _('Errors / Warnings') + '', + 0, 0, 1, 1, + ) + + if self.media_type == 'channel': + string = _( + 'Error messages produced the last time this channel was' \ + + ' checked/downloaded', + ) + else: + string = _( + 'Error messages produced the last time this playlist was' \ + + ' checked/downloaded', + ) + + self.add_label(grid, + '' + string + '', + 0, 1, 1, 1, + ) + + textview, textbuffer = self.add_textview(grid, + 'error_list', + 0, 2, 1, 1, + ) + textview.set_editable(False) + textview.set_wrap_mode(Gtk.WrapMode.WORD) + + if self.media_type == 'channel': + string = _( + 'Warning messages produced the last time this channel was' \ + + ' checked/downloaded', + ) + else: + string = _( + 'Warning messages produced the last time this playlist was' \ + + ' checked/downloaded', + ) + + self.add_label(grid, + '' + string + '', + 0, 3, 1, 1, + ) + + textview2, textbuffer2 = self.add_textview(grid, + 'warning_list', + 0, 4, 1, 1, ) textview2.set_editable(False) textview2.set_wrap_mode(Gtk.WrapMode.WORD) @@ -5476,7 +5804,7 @@ class FolderEditWin(GenericEditWin): def __init__(self, app_obj, edit_obj): - Gtk.Window.__init__(self, title='Folder properties') + Gtk.Window.__init__(self, title=_('Folder properties')) # IV list - class objects # ----------------------- @@ -5584,11 +5912,11 @@ def setup_general_tab(self): Sets up the 'General' tab. """ - tab, grid = self.add_notebook_tab('_General') + tab, grid = self.add_notebook_tab(_('_General')) self.add_label(grid, - 'General properties', - 0, 0, 2, 1, + '' + _('General properties') + '', + 0, 0, 3, 1, ) # The first sets of widgets are shared by multiple edit windows @@ -5597,65 +5925,63 @@ def setup_general_tab(self): # To avoid messing up the neat format of the rows above, add another # grid, and put the next set of widgets inside it - grid3 = Gtk.Grid() - grid.attach(grid3, 0, 6, 3, 1) - grid3.set_vexpand(False) - grid3.set_border_width(self.spacing_size) - grid3.set_column_spacing(self.spacing_size) - grid3.set_row_spacing(self.spacing_size) - - checkbutton = self.add_checkbutton(grid3, - 'Always simulate download of videos', + grid2 = Gtk.Grid() + grid.attach(grid2, 0, 7, 3, 1) + grid2.set_border_width(self.spacing_size) + grid2.set_column_spacing(self.spacing_size) + grid2.set_row_spacing(self.spacing_size) + + checkbutton = self.add_checkbutton(grid2, + _('Always simulate download of videos'), 'dl_sim_flag', 0, 0, 1, 1, ) checkbutton.set_sensitive(False) - checkbutton2 = self.add_checkbutton(grid3, - 'Disable checking/downloading for this folder', + checkbutton2 = self.add_checkbutton(grid2, + _('Disable checking/downloading for this folder'), 'dl_disable_flag', 0, 1, 1, 1, ) checkbutton2.set_sensitive(False) - checkbutton3 = self.add_checkbutton(grid3, - 'This folder is marked as a favourite', + checkbutton3 = self.add_checkbutton(grid2, + _('This folder is marked as a favourite'), 'fav_flag', 0, 2, 1, 1, ) checkbutton3.set_sensitive(False) - checkbutton4 = self.add_checkbutton(grid3, - 'This folder is hidden', + checkbutton4 = self.add_checkbutton(grid2, + _('This folder is hidden'), 'hidden_flag', 0, 3, 1, 1, ) checkbutton4.set_sensitive(False) - checkbutton5 = self.add_checkbutton(grid3, - 'This folder can\'t be deleted by the user', + checkbutton5 = self.add_checkbutton(grid2, + _('This folder can\'t be deleted by the user'), 'fixed_flag', 1, 0, 1, 1, ) checkbutton5.set_sensitive(False) - checkbutton6 = self.add_checkbutton(grid3, - 'This is a system-controlled folder', + checkbutton6 = self.add_checkbutton(grid2, + _('This is a system-controlled folder'), 'priv_flag', 1, 1, 1, 1, ) checkbutton6.set_sensitive(False) - checkbutton7 = self.add_checkbutton(grid3, - 'Only videos can be added to this folder', + checkbutton7 = self.add_checkbutton(grid2, + _('Only videos can be added to this folder'), 'restrict_flag', 1, 2, 1, 1, ) checkbutton7.set_sensitive(False) - checkbutton8 = self.add_checkbutton(grid3, - 'All contents deleted when ' + __main__.__prettyname__ \ - + ' shuts down', + checkbutton8 = self.add_checkbutton(grid2, + _('All contents deleted when Tartube shuts down'), 'temp_flag', 1, 3, 1, 1, ) @@ -5695,8 +6021,9 @@ class SystemPrefWin(GenericPrefWin): app_obj (mainapp.TartubeApp): The main application object - switch_db_flag (bool): If True, the tab containing the option to switch - Tartube's database is selected as soon as the window is opened + 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 """ @@ -5704,10 +6031,10 @@ class SystemPrefWin(GenericPrefWin): # Standard class methods - def __init__(self, app_obj, switch_db_flag=False): + def __init__(self, app_obj, init_mode=None): - Gtk.Window.__init__(self, title='System preferences') + Gtk.Window.__init__(self, title=_('System preferences')) # IV list - class objects # ----------------------- @@ -5729,7 +6056,9 @@ def __init__(self, app_obj, switch_db_flag=False): # (IVs used to handle widget changes in the 'Filesystem' tab) self.entry = None # Gtk.Entry self.entry2 = None # Gtk.Entry + # (IVs used to open the window at a particular tab) self.filesystem_inner_notebook = None # Gtk.Notebook + self.operations_inner_notebook = None # Gtk.Notebook # IV list - other @@ -5742,8 +6071,12 @@ def __init__(self, app_obj, switch_db_flag=False): # Set up the preference window self.setup() - if switch_db_flag: - self.select_switch_db_tab() + + if init_mode is not None: + if init_mode == 'db': + self.select_switch_db_tab() + elif init_mode == 'live': + self.select_livestream_tab() # Public class methods @@ -5779,6 +6112,18 @@ def select_switch_db_tab(self): self.filesystem_inner_notebook.set_current_page(1) + def select_livestream_tab(self): + + """Can be called by anything. + + Makes the visible tab the one on which the user can set livestream + options. + """ + + self.notebook.set_current_page(4) + self.operations_inner_notebook.set_current_page(2) + + # (Setup tabs) @@ -5807,13 +6152,14 @@ def setup_general_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('_General', 0) + tab, grid = self.add_notebook_tab(_('_General'), 0) # ...and an inner notebook... inner_notebook = self.add_inner_notebook(grid) # ...with its own tabs self.setup_general_language_tab(inner_notebook) + self.setup_general_stability_tab(inner_notebook) self.setup_general_modules_tab(inner_notebook) self.setup_general_video_matching_tab(inner_notebook) @@ -5825,25 +6171,27 @@ def setup_general_language_tab(self, inner_notebook): Sets up the 'Language' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Language', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Language'), inner_notebook) grid_width = 2 # Language preferences self.add_label(grid, - 'Language preferences', + '' + _('Language preferences') + '', 0, 0, grid_width, 1, ) label = self.add_label(grid, - 'Language', + _('Language'), 0, 1, 1, 1, ) label.set_hexpand(False) - # (This is a placeholder, to be replaced when we add translations) store = Gtk.ListStore(GdkPixbuf.Pixbuf, str) - pixbuf = self.app_obj.main_win_obj.pixbuf_dict['flag_uk'] - store.append( [pixbuf, 'English'] ) + for locale in formats.LOCALE_LIST: + pixbuf = self.app_obj.main_win_obj.pixbuf_dict['flag_' + locale] + store.append( + [ pixbuf, formats.LOCALE_DICT[locale] ], + ) combo = Gtk.ComboBox.new_with_model(store) grid.attach(combo, 1, 1, (grid_width - 1), 1) @@ -5857,27 +6205,34 @@ def setup_general_language_tab(self, inner_notebook): combo.pack_start(renderer_text, True) combo.add_attribute(renderer_text, 'text', 1) - combo.set_active(0) + combo.set_active(formats.LOCALE_LIST.index(self.app_obj.custom_locale)) + combo.connect('changed', self.on_locale_combo_changed, grid) - def setup_general_modules_tab(self, inner_notebook): + def setup_general_stability_tab(self, inner_notebook): """Called by self.setup_general_tab(). - Sets up the 'Modules' inner notebook tab. + Sets up the 'Stability' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Modules', inner_notebook) - grid_width = 3 + tab, grid = self.add_inner_notebook_tab( + _('_Stability'), + inner_notebook, + ) + + grid_width = 2 + label_length \ + = self.app_obj.main_win_obj.exceedingly_long_string_max_len - # Gtk support + # Gtk library self.add_label(grid, - 'Gtk support', + '' + _('Gtk library') + '', 0, 0, grid_width, 1, ) self.add_label(grid, - 'Current version of the system\'s Gtk library', + _('Current version of the system\'s Gtk library'), 0, 1, 1, 1 ) @@ -5886,68 +6241,148 @@ def setup_general_modules_tab(self, inner_notebook): + str(self.app_obj.gtk_version_minor) + '.' \ + str(self.app_obj.gtk_version_micro), False, - 1, 1, 2, 1, + 1, 1, 1, 1, ) entry.set_sensitive(False) + # Gtk stability + self.add_label(grid, + '' + _('Gtk stability') + '', + 0, 2, grid_width, 1, + ) + + frame = Gtk.Frame() + grid.attach(frame, 0, 3, grid_width, 1) + frame.set_border_width(self.spacing_size) + + vbox = Gtk.VBox() + frame.add(vbox) + + label = Gtk.Label() + vbox.pack_start(label, True, True, self.spacing_size) + label.set_markup( + utils.tidy_up_long_string( + _( + 'Tartube uses the Gtk graphics library. This library is' \ + + ' notoriously unreliable and may even causes crashes.', + ), + label_length, + ) + '\n\n' \ + + utils.tidy_up_long_string( + _( + 'If stability is a problem, you can disable some minor' \ + + ' cosmetic features.', + ), + label_length, + ) + '\n\n' \ + + utils.tidy_up_long_string( + _( + 'Tartube\'s functionality is not affected. You can do' \ + + ' anything, even when cosmetic features are disabled.', + ), + label_length, + ), + ) + checkbutton = self.add_checkbutton(grid, - 'Some (minor) features are disabled because this version of the' \ - + ' library is broken', + _( + 'Some features are disabled because this version of the library' \ + + ' is broken', + ), self.app_obj.gtk_broken_flag, False, # Can't be toggled by user - 0, 2, grid_width, 1, + 0, 4, grid_width, 1, ) checkbutton.set_hexpand(False) checkbutton2 = self.add_checkbutton(grid, - 'Assume that Gtk is broken, and disable some features', + _('Assume that Gtk is broken, and disable those features anyway'), self.app_obj.gtk_emulate_broken_flag, True, # Can be toggled by user - 0, 3, grid_width, 1, + 0, 5, grid_width, 1, ) checkbutton2.set_hexpand(False) checkbutton2.connect('toggled', self.on_gtk_emulate_button_toggled) + + def setup_general_modules_tab(self, inner_notebook): + + """Called by self.setup_general_tab(). + + Sets up the 'Modules' inner notebook tab. + """ + + tab, grid = self.add_inner_notebook_tab(_('_Modules'), inner_notebook) + grid_width = 2 + # Module availability self.add_label(grid, - 'Module availability', - 0, 4, grid_width, 1, + '' + _('Module availability') + '', + 0, 0, grid_width, 1, ) self.add_checkbutton(grid, - 'moviepy module is available', + _( + 'feedparser module is available (required for detecting' \ + + ' livestreams)', + ), + mainapp.HAVE_FEEDPARSER_FLAG, + False, # Can't be toggled by user + 0, 1, grid_width, 1, + ) + + self.add_checkbutton(grid, + _( + 'moviepy module is available (finds the length of videos, if' \ + + ' unknown)', + ), mainapp.HAVE_MOVIEPY_FLAG, False, # Can't be toggled by user - 0, 5, grid_width, 1, + 0, 2, grid_width, 1, ) self.add_checkbutton(grid, - 'XDG module is available', + _( + 'playsound module is available (sound an alarm when a livestream' \ + + ' starts)', + ), + mainapp.HAVE_PLAYSOUND_FLAG, + False, # Can't be toggled by user + 0, 3, grid_width, 1, + ) + + self.add_checkbutton(grid, + _( + 'XDG module is available (saves the config file in the standard' \ + + ' location)', + ), mainapp.HAVE_XDG_FLAG, False, # Can't be toggled by user - 0, 6, grid_width, 1, + 0, 4, grid_width, 1, ) # Module preferences self.add_label(grid, - 'Module preferences', - 0, 7, grid_width, 1, + '' + _('Module preferences') + '', + 0, 5, grid_width, 1, ) - checkbutton3 = self.add_checkbutton(grid, + checkbutton = self.add_checkbutton(grid, + _( 'Use \'moviepy\' module to get a video\'s duration, if not known' + ' (may be slow)', + ), self.app_obj.use_module_moviepy_flag, True, # Can be toggled by user - 0, 8, grid_width, 1, + 0, 6, grid_width, 1, ) - checkbutton3.connect('toggled', self.on_moviepy_button_toggled) + checkbutton.connect('toggled', self.on_moviepy_button_toggled) if not mainapp.HAVE_MOVIEPY_FLAG: - checkbutton3.set_sensitive(False) + checkbutton.set_sensitive(False) self.add_label(grid, - 'Timeout applied when moviepy checks a video file', - 0, 9, grid_width, 1, + _('Timeout applied when moviepy checks a video file'), + 0, 7, 1, 1, ) spinbutton = self.add_spinbutton(grid, @@ -5955,7 +6390,7 @@ def setup_general_modules_tab(self, inner_notebook): 60, 1, # Step self.app_obj.refresh_moviepy_timeout, - 1, 9, 2, 1, + 1, 7, 1, 1, ) spinbutton.connect( 'value-changed', @@ -5971,7 +6406,7 @@ def setup_general_video_matching_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Video matching', + _('_Video matching'), inner_notebook, ) @@ -5979,25 +6414,25 @@ def setup_general_video_matching_tab(self, inner_notebook): # Video matching preferences self.add_label(grid, - 'Video matching preferences', + '' + _('Video matching preferences') + '', 0, 0, grid_width, 1, ) self.add_label(grid, - 'When matching videos on the filesystem:', + _('When matching videos on the filesystem:'), 0, 1, grid_width, 1, ) self.radiobutton = self.add_radiobutton(grid, None, - 'The video names must match exactly', + _('The video names must match exactly'), 0, 2, grid_width, 1, ) # Signal connect appears below self.radiobutton2 = self.add_radiobutton(grid, self.radiobutton, - 'The first n characters must match exactly', + _('The first # characters must match exactly'), 0, 3, (grid_width - 1), 1, ) # Signal connect appears below @@ -6010,8 +6445,10 @@ def setup_general_video_matching_tab(self, inner_notebook): self.radiobutton3 = self.add_radiobutton(grid, self.radiobutton2, - 'Ignore the last n characters; the remaining name must match' \ + _( + 'Ignore the last # characters; the remaining name must match' \ + ' exactly', + ), 0, 4, (grid_width - 1), 1, ) # Signal connect appears below @@ -6055,7 +6492,7 @@ def setup_filesystem_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('_Filesystem', 0) + tab, grid = self.add_notebook_tab(_('_Filesystem'), 0) # ...and an inner notebook... self.filesystem_inner_notebook = self.add_inner_notebook(grid) @@ -6078,17 +6515,17 @@ def setup_filesystem_device_tab(self, inner_notebook): Sets up the 'Device' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Device', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Device'), inner_notebook) grid_width = 3 # Device preferences self.add_label(grid, - 'Device preferences', + '' + _('Device preferences') + '', 0, 0, grid_width, 1, ) self.add_label(grid, - 'Size of device (in Mb)', + _('Size of device (in Mb)'), 0, 3, 1, 1, ) @@ -6100,7 +6537,7 @@ def setup_filesystem_device_tab(self, inner_notebook): self.entry.set_sensitive(False) self.add_label(grid, - 'Free space on device (in Mb)', + _('Free space on device (in Mb)'), 0, 4, 1, 1, ) @@ -6112,7 +6549,7 @@ def setup_filesystem_device_tab(self, inner_notebook): self.entry2.set_sensitive(False) checkbutton = self.add_checkbutton(grid, - 'Warn user if disk space is below (Mb)', + _('Warn user if disk space is less than'), self.app_obj.disk_space_warn_flag, True, # Can be toggled by user 0, 5, 1, 1, @@ -6130,7 +6567,7 @@ def setup_filesystem_device_tab(self, inner_notebook): # (signal_connect appears below) checkbutton2 = self.add_checkbutton(grid, - 'Halt downloads if disk space is below (Mb)', + _('Halt downloads if disk space is less than'), self.app_obj.disk_space_stop_flag, True, # Can be toggled by user 0, 6, 1, 1, @@ -6169,12 +6606,12 @@ def setup_filesystem_device_tab(self, inner_notebook): # Configuration preferences self.add_label(grid, - 'Configuration preferences', + '' + _('Configuration preferences') + '', 0, 7, grid_width, 1, ) self.add_label(grid, - __main__.__prettyname__ + ' configuration file loaded from:', + _('Tartube configuration file loaded from:'), 0, 8, grid_width, 1, ) @@ -6202,24 +6639,18 @@ def setup_filesystem_database_tab(self, inner_notebook): Sets up the 'Database' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('D_atabase', inner_notebook) - grid_width = 3 + tab, grid = self.add_inner_notebook_tab(_('D_atabase'), inner_notebook) - if os.name == 'nt': - folder = 'folder' - folder_plural = 'folders' - else: - folder = 'directory' - folder_plural = 'directories' + grid_width = 3 # Database preferences self.add_label(grid, - 'Database preferences', + '' + _('Database preferences') + '', 0, 0, grid_width, 1, ) label = self.add_label(grid, - __main__.__prettyname__ + ' data ' + folder, + _('Tartube data folder'), 0, 2, 1, 1, ) label.set_hexpand(False) @@ -6231,9 +6662,9 @@ def setup_filesystem_database_tab(self, inner_notebook): ) entry.set_sensitive(False) - button = Gtk.Button('Change') + button = Gtk.Button(_('Change')) grid.attach(button, 2, 2, 1, 1) - button.set_tooltip_text('Change to a different data ' + folder) + button.set_tooltip_text(_('Change to a different data folder')) button.connect( 'clicked', self.on_data_dir_change_button_clicked, @@ -6241,7 +6672,7 @@ def setup_filesystem_database_tab(self, inner_notebook): ) label = self.add_label(grid, - 'Recent data ' + folder_plural, + _('Recent data folders'), 0, 3, 1, 1, ) label.set_hexpand(False) @@ -6254,9 +6685,15 @@ def setup_filesystem_database_tab(self, inner_notebook): liststore.append([item]) # (signal_connect appears below) - button2 = Gtk.Button('Switch') + # v2.0.079 These lines produce a Gtk error, for no obvious reason (the + # equivalent code in mainwin.MainWin.setup_classic_mode_tab() + # produces no error) +# selection = treeview.get_selection() +# selection.set_mode(Gtk.SelectionMode.MULTIPLE) + + button2 = Gtk.Button(_('Switch')) grid.attach(button2, 2, 3, 1, 1) - button2.set_tooltip_text('Switch to the selected data ' + folder) + button2.set_tooltip_text(_('Switch to the selected data folder')) button2.set_sensitive(False) button2.connect( 'clicked', @@ -6266,10 +6703,10 @@ def setup_filesystem_database_tab(self, inner_notebook): entry, ) - button3 = Gtk.Button('Forget') + button3 = Gtk.Button(_('Forget')) grid.attach(button3, 2, 4, 1, 1) button3.set_tooltip_text( - 'Remove the selected data ' + folder + ' from the list', + _('Remove the selected data folder from the list'), ) button3.set_sensitive(False) button3.connect( @@ -6278,11 +6715,10 @@ def setup_filesystem_database_tab(self, inner_notebook): treeview, ) - button4 = Gtk.Button('Forget all') + button4 = Gtk.Button(_('Forget all')) grid.attach(button4, 2, 5, 1, 1) button4.set_tooltip_text( - 'Forget every ' + folder + ' in this list (except the current' \ - + ' one)', + _('Forget every folder in this list (except the current one)'), ) if len(self.app_obj.data_dir_alt_list) <= 1: button4.set_sensitive(False) @@ -6292,30 +6728,36 @@ def setup_filesystem_database_tab(self, inner_notebook): treeview, ) - button5 = Gtk.Button('Move up') + button5 = Gtk.Button(_('Move up')) grid.attach(button5, 2, 6, 1, 1) button5.set_tooltip_text( - 'Move the selected ' + folder + ' up the list', + _('Move the selected folder up the list'), ) button5.set_sensitive(False) + # signal connect appears below + + button6 = Gtk.Button(_('Move down')) + grid.attach(button6, 2, 7, 1, 1) + button6.set_tooltip_text( + _('Move the selected folder down the list'), + ) + button6.set_sensitive(False) + # signal connect appers below + + # signal conencts from above button5.connect( 'clicked', self.on_data_dir_move_up_button_clicked, treeview, liststore, + button6, ) - - button6 = Gtk.Button('Move down') - grid.attach(button6, 2, 7, 1, 1) - button6.set_tooltip_text( - 'Move the selected ' + folder + ' down the list', - ) - button6.set_sensitive(False) button6.connect( 'clicked', self.on_data_dir_move_down_button_clicked, treeview, liststore, + button5, ) # (Add a second grid, so widget positioning on the first one isn't @@ -6324,8 +6766,10 @@ def setup_filesystem_database_tab(self, inner_notebook): grid.attach(grid2, 0, 8, grid_width, 1) checkbutton = self.add_checkbutton(grid2, + _( 'On startup, load the first database on the list (not the most' \ + ' recently-use one)', + ), self.app_obj.data_dir_use_first_flag, True, # Can be toggled by user 0, 0, 2, 1, @@ -6333,7 +6777,7 @@ def setup_filesystem_database_tab(self, inner_notebook): checkbutton.connect('toggled', self.on_use_first_button_toggled) checkbutton2 = self.add_checkbutton(grid2, - 'If one database is in use, try to load others', + _('If one database is in use, try to load others'), self.app_obj.data_dir_use_list_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -6341,7 +6785,7 @@ def setup_filesystem_database_tab(self, inner_notebook): checkbutton2.connect('toggled', self.on_use_list_button_toggled) checkbutton3 = self.add_checkbutton(grid2, - 'Add new data directories to this list', + _('Add new data directories to this list'), self.app_obj.data_dir_add_from_list_flag, True, # Can be toggled by user 1, 1, 1, 1, @@ -6379,22 +6823,25 @@ def setup_filesystem_db_errors_tab(self, inner_notebook): Sets up the 'DB Errors' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('DB _Errors', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('DB _Errors'), + inner_notebook, + ) + grid_width = 2 # Database error preferences self.add_label(grid, - 'Database error preferences', + '' + _('Database error preferences') + '', 0, 0, grid_width, 1, ) self.add_label(grid, - 'Check ' + __main__.__prettyname__ \ - + '\'s database for inconsistencies, and fix them', + _('Check Tartube\'s database for inconsistencies, and fix them'), 0, 1, 1, 1, ) - button = Gtk.Button('Check') + button = Gtk.Button(_('Check DB')) grid.attach(button, 1, 1, 1, 1) if self.app_obj.disable_load_save_flag: button.set_sensitive(False) @@ -6409,30 +6856,36 @@ def setup_filesystem_backups_tab(self, inner_notebook): Sets up the 'Backups' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Backups', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Backups'), inner_notebook) # Backup preferences self.add_label(grid, - 'Backup preferences', + '' + _('Backup preferences') + '', 0, 0, 1, 1, ) self.add_label(grid, - 'When saving a database file, ' + __main__.__prettyname__ \ - + ' makes a backup copy of it (in case something goes wrong)', + '' + _( + 'When saving a database file, Tartube makes a backup copy of' \ + + ' it (in case something goes wrong)', + ) + '', 0, 1, 1, 1, ) radiobutton = self.add_radiobutton(grid, None, + _( 'Delete the backup file as soon as the save procedure is' \ + ' finished', + ), 0, 2, 1, 1, ) # Signal connect appears below radiobutton2 = self.add_radiobutton(grid, radiobutton, + _( 'Keep the backup file, replacing any previous backup file', + ), 0, 3, 1, 1, ) if self.app_obj.db_backup_mode == 'single': @@ -6441,8 +6894,10 @@ def setup_filesystem_backups_tab(self, inner_notebook): radiobutton3 = self.add_radiobutton(grid, radiobutton2, + _( 'Make a new backup file once per day, after the day\'s first' \ + ' save procedure', + ), 0, 4, 1, 1, ) if self.app_obj.db_backup_mode == 'daily': @@ -6451,7 +6906,7 @@ def setup_filesystem_backups_tab(self, inner_notebook): radiobutton4 = self.add_radiobutton(grid, radiobutton3, - 'Make a new backup file for every save procedure', + _('Make a new backup file for every save procedure'), 0, 5, 1, 1, ) if self.app_obj.db_backup_mode == 'always': @@ -6492,7 +6947,7 @@ def setup_filesystem_video_deletion_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Video deletion', + _('_Video deletion'), inner_notebook, ) @@ -6500,12 +6955,12 @@ def setup_filesystem_video_deletion_tab(self, inner_notebook): # Automatic video deletion preferences self.add_label(grid, - 'Automatic video deletion preferences', + '' + _('Automatic video deletion preferences') + '', 0, 0, grid_width, 1, ) checkbutton = self.add_checkbutton(grid, - 'Automatically delete downloaded videos after this many days', + _('Automatically delete downloaded videos after this many days'), self.app_obj.auto_delete_flag, True, # Can be toggled by user 0, 1, (grid_width - 1), 1, @@ -6519,7 +6974,7 @@ def setup_filesystem_video_deletion_tab(self, inner_notebook): # Signal connect appears below checkbutton2 = self.add_checkbutton(grid, - '...but only delete videos which have been watched', + _('...but only delete videos which have been watched'), self.app_obj.auto_delete_watched_flag, True, # Can be toggled by user 0, 2, grid_width, 1, @@ -6550,19 +7005,18 @@ def setup_filesystem_temp_folders_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Temporary folders', + _('_Temporary folders'), inner_notebook, ) # Temporary folder preferences self.add_label(grid, - 'Temporary folder preferences', + '' + _('Temporary folder preferences') + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, - 'Empty temporary folders when ' + __main__.__prettyname__ \ - + ' shuts down', + _('Empty temporary folders when Tartube shuts down'), self.app_obj.delete_on_shutdown_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -6570,14 +7024,17 @@ def setup_filesystem_temp_folders_tab(self, inner_notebook): # signal_connect appears below self.add_label(grid, - '(N.B. Temporary folders are always emptied when ' \ - + __main__.__prettyname__ + ' starts up)', + '' + _( + '(N.B. Temporary folders are always emptied when Tartube' \ + + ' starts up)', + ) + '', 0, 2, 1, 1, ) checkbutton2 = self.add_checkbutton(grid, - 'Open temporary folders (on the desktop) when ' \ - + __main__.__prettyname__ + ' shuts down', + _( + 'Open temporary folders (on the desktop) when Tartube shuts down', + ), self.app_obj.open_temp_on_desktop_flag, True, # Can be toggled by user 0, 3, 1, 1, @@ -6602,15 +7059,16 @@ def setup_windows_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('_Windows', 0) + tab, grid = self.add_notebook_tab(_('_Windows'), 0) # ...and an inner notebook... inner_notebook = self.add_inner_notebook(grid) # ...with its own tabs self.setup_windows_main_window_tab(inner_notebook) + self.setup_windows_tabs_tab(inner_notebook) self.setup_windows_system_tray_tab(inner_notebook) - self.setup_windows_dialogue_windows_tab(inner_notebook) + self.setup_windows_dialogues_tab(inner_notebook) self.setup_windows_errors_warnings_tab(inner_notebook) self.setup_windows_websites_tab(inner_notebook) @@ -6622,16 +7080,19 @@ def setup_windows_main_window_tab(self, inner_notebook): Sets up the 'Main Window' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Main window', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('_Main window'), + inner_notebook, + ) # Main window preferences self.add_label(grid, - 'Main window preferences', + '' + _('Main window preferences') + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, - 'Remember the size of the main window when shutting down', + _('Remember the size of the main window when shutting down'), self.app_obj.main_win_save_size_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -6639,7 +7100,7 @@ def setup_windows_main_window_tab(self, inner_notebook): checkbutton.connect('toggled', self.on_remember_size_button_toggled) checkbutton2 = self.add_checkbutton(grid, - 'Don\'t show labels in the toolbar', + _('Don\'t show labels in the toolbar'), self.app_obj.toolbar_squeeze_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -6647,7 +7108,7 @@ def setup_windows_main_window_tab(self, inner_notebook): checkbutton2.connect('toggled', self.on_squeeze_button_toggled) checkbutton3 = self.add_checkbutton(grid, - 'Show tooltips for videos, channels, playlists and folders', + _('Show tooltips for videos, channels, playlists and folders'), self.app_obj.show_tooltips_flag, True, # Can be toggled by user 0, 3, 1, 1, @@ -6655,8 +7116,10 @@ def setup_windows_main_window_tab(self, inner_notebook): checkbutton3.connect('toggled', self.on_show_tooltips_toggled) checkbutton4 = self.add_checkbutton(grid, + _( 'Show smaller icons in the Video Index (left side of the' \ + ' Videos Tab)', + ), self.app_obj.show_small_icons_in_index, True, # Can be toggled by user 0, 4, 1, 1, @@ -6664,8 +7127,10 @@ def setup_windows_main_window_tab(self, inner_notebook): checkbutton4.connect('toggled', self.on_show_small_icons_toggled) checkbutton5 = self.add_checkbutton(grid, + _( 'In the Video Index, show detailed statistics about the videos' \ + ' in each channel / playlist / folder', + ), self.app_obj.complex_index_flag, True, # Can be toggled by user 0, 5, 1, 1, @@ -6673,8 +7138,10 @@ def setup_windows_main_window_tab(self, inner_notebook): checkbutton5.connect('toggled', self.on_complex_button_toggled) checkbutton6 = self.add_checkbutton(grid, + _( 'After clicking on a folder, automatically expand/collapse the' \ + ' tree around it', + ), self.app_obj.auto_expand_video_index_flag, True, # Can be toggled by user 0, 6, 1, 1, @@ -6682,8 +7149,10 @@ def setup_windows_main_window_tab(self, inner_notebook): # signal_connect appears below checkbutton7 = self.add_checkbutton(grid, + _( 'Expand the whole tree, not just the level beneath the clicked' \ + ' folder', + ), self.app_obj.full_expand_video_index_flag, True, # Can be toggled by user 0, 7, 1, 1, @@ -6701,14 +7170,27 @@ def setup_windows_main_window_tab(self, inner_notebook): checkbutton7.connect('toggled', self.on_expand_full_tree_toggled) checkbutton8 = self.add_checkbutton(grid, + _( 'Disable the \'Download all\' buttons in the toolbar and the' \ + ' Videos Tab', + ), self.app_obj.disable_dl_all_flag, True, # Can be toggled by user 0, 8, 1, 1, ) checkbutton8.connect('toggled', self.on_disable_dl_all_toggled) + checkbutton9 = self.add_checkbutton(grid, + _('When Tartube starts, automatically open the Classic Mode tab'), + self.app_obj.show_classic_tab_on_startup_flag, + True, # Can be toggled by user + 0, 9, 1, 1, + ) + checkbutton9.connect( + 'toggled', + self.on_show_classic_mode_button_toggled, + ) + def setup_windows_tabs_tab(self, inner_notebook): @@ -6717,17 +7199,19 @@ def setup_windows_tabs_tab(self, inner_notebook): Sets up the 'Tabs' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Tabs', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Tabs'), inner_notebook) # Tab preferences self.add_label(grid, - 'Tab preferences', + '' + _('Tab preferences') + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, + _( 'In the Videos Tab, show \'today\' and \'yesterday\' as the' \ + ' date, when possible', + ), self.app_obj.show_pretty_dates_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -6735,8 +7219,9 @@ def setup_windows_tabs_tab(self, inner_notebook): checkbutton.connect('toggled', self.on_pretty_date_button_toggled) checkbutton2 = self.add_checkbutton(grid, - 'In the Progress Tab, hide finished videos / channels' \ - + ' / playlists', + _( + 'In the Progress Tab, hide finished videos / channels / playlists', + ), self.app_obj.progress_list_hide_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -6744,7 +7229,7 @@ def setup_windows_tabs_tab(self, inner_notebook): checkbutton2.connect('toggled', self.on_hide_button_toggled) checkbutton3 = self.add_checkbutton(grid, - 'In the Progress Tab, show results in reverse order', + _('In the Progress Tab, show results in reverse order'), self.app_obj.results_list_reverse_flag, True, # Can be toggled by user 0, 3, 1, 1, @@ -6752,8 +7237,10 @@ def setup_windows_tabs_tab(self, inner_notebook): checkbutton3.connect('toggled', self.on_reverse_button_toggled) checkbutton4 = self.add_checkbutton(grid, - 'In the Errors/Warnings Tab, preserve message counts in the' \ - + ' tab label for longer', + _( + 'In the Errors/Warnings Tab, don\'t reset the tab text when' \ + + ' it is clicked', + ), self.app_obj.system_msg_keep_totals_flag, True, # Can be toggled by user 0, 4, 1, 1, @@ -6768,17 +7255,19 @@ def setup_windows_system_tray_tab(self, inner_notebook): Sets up the 'System tray' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_System tray', inner_notebook) - + tab, grid = self.add_inner_notebook_tab( + _('_System tray'), + inner_notebook, + ) # System tray preferences self.add_label(grid, - 'System tray preferences', + '' + _('System tray preferences') + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, - 'Show icon in system tray', + _('Show icon in system tray'), self.app_obj.show_status_icon_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -6787,7 +7276,7 @@ def setup_windows_system_tray_tab(self, inner_notebook): # signal connnect appears below checkbutton2 = self.add_checkbutton(grid, - 'Close to the tray, rather than closing the application', + _('Close to the tray, rather than closing the application'), self.app_obj.close_to_tray_flag, True, # Can be toggled by user 0, 12, 1, 1, @@ -6805,26 +7294,26 @@ def setup_windows_system_tray_tab(self, inner_notebook): ) - def setup_windows_dialogue_windows_tab(self, inner_notebook): + def setup_windows_dialogues_tab(self, inner_notebook): """Called by self.setup_windows_tab(). - Sets up the 'Dialogue windows' inner notebook tab. + Sets up the 'Dialogues' inner notebook tab. """ tab, grid = self.add_inner_notebook_tab( - '_Dialogue windows', + _('_Dialogues'), inner_notebook, ) # Dialogue window preferences self.add_label(grid, - 'Dialogue window preferences', + '' + _('Dialogue window preferences') + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, - 'When adding channels/playlists, keep the dialogue window open', + _('When adding channels/playlists, keep the dialogue window open'), self.app_obj.dialogue_keep_open_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -6833,8 +7322,10 @@ def setup_windows_dialogue_windows_tab(self, inner_notebook): # signal connnect appears below checkbutton2 = self.add_checkbutton(grid, - 'When the dialogue window opens, copy URLs from the system' \ + _( + 'When the dialogue window opens, add URLs from the system' \ + ' clipboard', + ), self.app_obj.dialogue_copy_clipboard_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -6860,7 +7351,7 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Errors/Warnings', + _('_Errors/Warnings'), inner_notebook, ) @@ -6868,12 +7359,12 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): # Errors/Warnings tab preferences self.add_label(grid, - 'Errors/Warnings tab preferences', + '' + _('Errors/Warnings tab preferences') + '', 0, 0, grid_width, 1, ) checkbutton = self.add_checkbutton(grid, - 'Show ' + __main__.__prettyname__ + ' error messages', + _('Show Tartube error messages'), self.app_obj.system_error_show_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -6881,7 +7372,7 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): checkbutton.connect('toggled', self.on_system_error_button_toggled) checkbutton2 = self.add_checkbutton(grid, - 'Show ' + __main__.__prettyname__ + ' warning messages', + _('Show Tartube warning messages'), self.app_obj.system_warning_show_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -6889,7 +7380,7 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): checkbutton2.connect('toggled', self.on_system_warning_button_toggled) checkbutton3 = self.add_checkbutton(grid, - 'Show server error messages', + _('Show server error messages'), self.app_obj.operation_error_show_flag, True, # Can be toggled by user 1, 1, 1, 1, @@ -6900,7 +7391,7 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): ) checkbutton4 = self.add_checkbutton(grid, - 'Show server warning messages', + _('Show server warning messages'), self.app_obj.operation_warning_show_flag, True, # Can be toggled by user 1, 2, 1, 1, @@ -6912,12 +7403,17 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): # youtube-dl error/warning preferences self.add_label(grid, - 'youtube-dl error/warning preferences', + '' + _('youtube-dl error/warning preferences') + '', 0, 3, 1, 1, ) + translate_note = _( + 'TRANSLATOR\'S NOTE: These youtube-dl error messages are always' \ + + ' in English', + ) + checkbutton5 = self.add_checkbutton(grid, - 'Ignore \'Child process exited with non-zero code\' errors', + _('Ignore \'Child process exited with non-zero code\' errors'), self.app_obj.ignore_child_process_exit_flag, True, # Can be toggled by user 0, 4, grid_width, 1, @@ -6925,7 +7421,9 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): checkbutton5.connect('toggled', self.on_child_process_button_toggled) checkbutton6 = self.add_checkbutton(grid, + _( 'Ignore \'Unable to download video data: HTTP Error 404\' errors', + ), self.app_obj.ignore_http_404_error_flag, True, # Can be toggled by user 0, 5, grid_width, 1, @@ -6933,7 +7431,7 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): checkbutton6.connect('toggled', self.on_http_404_button_toggled) checkbutton7 = self.add_checkbutton(grid, - 'Ignore \'Did not get any data blocks\' errors', + _('Ignore \'Did not get any data blocks\' errors'), self.app_obj.ignore_data_block_error_flag, True, # Can be toggled by user 0, 6, grid_width, 1, @@ -6941,7 +7439,9 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): checkbutton7.connect('toggled', self.on_data_block_button_toggled) checkbutton8 = self.add_checkbutton(grid, + _( 'Ignore \'Requested formats are incompatible for merge\' warnings', + ), self.app_obj.ignore_merge_warning_flag, True, # Can be toggled by user 0, 7, grid_width, 1, @@ -6949,7 +7449,7 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): checkbutton8.connect('toggled', self.on_merge_button_toggled) checkbutton9 = self.add_checkbutton(grid, - 'Ignore \'No video formats found\' errors', + _('Ignore \'No video formats found\' errors'), self.app_obj.ignore_missing_format_error_flag, True, # Can be toggled by user 0, 8, grid_width, 1, @@ -6957,7 +7457,7 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): checkbutton9.connect('toggled', self.on_missing_format_button_toggled) checkbutton10 = self.add_checkbutton(grid, - 'Ignore \'There are no annotations to write\' warnings', + _('Ignore \'There are no annotations to write\' warnings'), self.app_obj.ignore_no_annotations_flag, True, # Can be toggled by user 0, 9, grid_width, 1, @@ -6965,7 +7465,7 @@ def setup_windows_errors_warnings_tab(self, inner_notebook): checkbutton10.connect('toggled', self.on_no_annotations_button_toggled) checkbutton11 = self.add_checkbutton(grid, - 'Ignore \'Video doesn\'t have subtitles\' warnings', + _('Ignore \'Video doesn\'t have subtitles\' warnings'), self.app_obj.ignore_no_subtitles_flag, True, # Can be toggled by user 0, 10, grid_width, 1, @@ -6981,20 +7481,20 @@ def setup_windows_websites_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Websites', + _('_Websites'), inner_notebook, ) grid_width = 2 - # Youtube error/warning preferences + # YouTube error/warning preferences self.add_label(grid, - 'Youtube error/warning preferences', + '' + _('YouTube error/warning preferences') + '', 0, 0, grid_width, 1, ) checkbutton = self.add_checkbutton(grid, - 'Ignore YouTube copyright errors', + _('Ignore YouTube copyright errors'), self.app_obj.ignore_yt_copyright_flag, True, # Can be toggled by user 0, 1, grid_width, 1, @@ -7002,7 +7502,7 @@ def setup_windows_websites_tab(self, inner_notebook): checkbutton.connect('toggled', self.on_copyright_button_toggled) checkbutton2 = self.add_checkbutton(grid, - 'Ignore YouTube age-restriction errors', + _('Ignore YouTube age-restriction errors'), self.app_obj.ignore_yt_age_restrict_flag, True, # Can be toggled by user 0, 2, grid_width, 1, @@ -7010,7 +7510,7 @@ def setup_windows_websites_tab(self, inner_notebook): checkbutton2.connect('toggled', self.on_age_restrict_button_toggled) checkbutton3 = self.add_checkbutton(grid, - 'Ignore YouTube deletion by uploader errors', + _('Ignore YouTube deletion by uploader errors'), self.app_obj.ignore_yt_uploader_deleted_flag, True, # Can be toggled by user 0, 3, grid_width, 1, @@ -7019,13 +7519,15 @@ def setup_windows_websites_tab(self, inner_notebook): # Custom error/warning preferences self.add_label(grid, - 'General preferences', + '' + _('General preferences') + '', 0, 4, grid_width, 1, ) self.add_label(grid, - 'Ignore any errors/warnings which match lines in this list' \ - + ' (applies to all websites)', + '' + _( + 'Ignore any errors/warnings which match lines in this list' \ + + ' (applies to all websites)', + ) + '', 0, 5, grid_width, 1, ) @@ -7036,14 +7538,14 @@ def setup_windows_websites_tab(self, inner_notebook): radiobutton = self.add_radiobutton(grid, None, - 'These are ordinary strings', + _('These are ordinary strings'), 0, 7, 1, 1, ) # Signal connect appears below radiobutton2 = self.add_radiobutton(grid, radiobutton, - 'These are regular expressions (regexes)', + _('These are regular expressions (regexes)'), 1, 7, 1, 1, ) if self.app_obj.ignore_custom_regex_flag: @@ -7072,7 +7574,7 @@ def setup_scheduling_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('_Scheduling', 0) + tab, grid = self.add_notebook_tab(_('_Scheduling'), 0) # ...and an inner notebook... inner_notebook = self.add_inner_notebook(grid) @@ -7089,27 +7591,26 @@ def setup_scheduling_start_tab(self, inner_notebook): Sets up the 'Start' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Start', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Start'), inner_notebook) grid_width = 2 # Scheduled start preferences self.add_label(grid, - 'Scheduled start preferences', + '' + _('Scheduled start preferences') + '', 0, 0, grid_width, 1, ) self.add_label(grid, - 'Automatic \'Download all\' operations', + _('Automatic \'Download all\' operations'), 0, 1, 1, 1, ) store = Gtk.ListStore(str, str) - script = __main__.__prettyname__ - store.append( ['none', 'Disabled'] ) - store.append( ['start', 'Performed when ' + script + ' starts'] ) - store.append( ['scheduled', 'Performed at regular intervals'] ) + store.append( ['none', _('Disabled')] ) + store.append( ['start', _('Performed when Tartube starts')] ) + store.append( ['scheduled', _('Performed at regular intervals')] ) combo = Gtk.ComboBox.new_with_model(store) grid.attach(combo, 1, 1, 1, 1) @@ -7129,7 +7630,7 @@ def setup_scheduling_start_tab(self, inner_notebook): # Signal connect appears below self.add_label(grid, - 'Time (in hours) between operations', + _('Time (in hours) between operations'), 0, 2, 1, 1, ) @@ -7142,15 +7643,15 @@ def setup_scheduling_start_tab(self, inner_notebook): # Signal connect appears below self.add_label(grid, - 'Automatic \'Check all\' operations', + _('Automatic \'Check all\' operations'), 0, 3, 1, 1, ) store2 = Gtk.ListStore(str, str) - store2.append( ['none', 'Disabled'] ) - store2.append( ['start', 'Performed when ' + script + ' starts'] ) - store2.append( ['scheduled', 'Performed at regular intervals'] ) + store2.append( ['none', _('Disabled')] ) + store2.append( ['start', _('Performed when Tartube starts')] ) + store2.append( ['scheduled', _('Performed at regular intervals')] ) combo2 = Gtk.ComboBox.new_with_model(store2) grid.attach(combo2, 1, 3, 1, 1) @@ -7170,7 +7671,7 @@ def setup_scheduling_start_tab(self, inner_notebook): # Signal connect appears below self.add_label(grid, - 'Time (in hours) between operations', + _('Time (in hours) between operations'), 0, 4, 1, 1, ) @@ -7183,8 +7684,10 @@ def setup_scheduling_start_tab(self, inner_notebook): # Signal connect appears below checkbutton = self.add_checkbutton(grid, + _( 'After an automatic \'Download/Check all\' operation, shut down' \ - + script, + + ' Tartube', + ), self.app_obj.scheduled_shutdown_flag, True, # Can be toggled by user 0, 5, grid_width, 1, @@ -7212,18 +7715,18 @@ def setup_scheduling_stop_tab(self, inner_notebook): Sets up the 'Stop' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('S_top', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('S_top'), inner_notebook) grid_width = 3 # Scheduled stop preferences self.add_label(grid, - 'Scheduled stop preferences', + '' + _('Scheduled stop preferences') + '', 0, 0, grid_width, 1, ) checkbutton = self.add_checkbutton(grid, - 'Stop all download operations after this much time', + _('Stop all download operations after this much time'), self.app_obj.autostop_time_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -7237,11 +7740,17 @@ def setup_scheduling_stop_tab(self, inner_notebook): if not self.app_obj.autostop_time_flag: spinbutton.set_sensitive(False) - combo = self.add_combo(grid, - formats.TIME_METRIC_LIST, - None, - 2, 1, 1, 1, - ) + store = Gtk.ListStore(str, str) + for string in formats.TIME_METRIC_LIST: + store.append( [string, formats.TIME_METRIC_TRANS_DICT[string]] ) + + combo = Gtk.ComboBox.new_with_model(store) + grid.attach(combo, 2, 1, 1, 1) + + renderer_text = Gtk.CellRendererText() + combo.pack_start(renderer_text, True) + combo.add_attribute(renderer_text, 'text', 1) + combo.set_entry_text_column(1) combo.set_active( formats.TIME_METRIC_LIST.index( self.app_obj.autostop_time_unit, @@ -7260,12 +7769,12 @@ def setup_scheduling_stop_tab(self, inner_notebook): ) spinbutton.connect( 'value-changed', - self.on_autostop_time_spinbutton_toggled, + self.on_autostop_time_spinbutton_changed, ) combo.connect('changed', self.on_autostop_time_combo_changed) checkbutton2 = self.add_checkbutton(grid, - 'Stop all download operations after this many videos', + _('Stop all download operations after this many videos'), self.app_obj.autostop_videos_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -7288,11 +7797,11 @@ def setup_scheduling_stop_tab(self, inner_notebook): ) spinbutton2.connect( 'value-changed', - self.on_autostop_videos_spinbutton_toggled, + self.on_autostop_videos_spinbutton_changed, ) checkbutton3 = self.add_checkbutton(grid, - 'Stop all download operations after this much disk space', + _('Stop all download operations after this much disk space'), self.app_obj.autostop_size_flag, True, # Can be toggled by user 0, 3, 1, 1, @@ -7329,13 +7838,15 @@ def setup_scheduling_stop_tab(self, inner_notebook): ) spinbutton3.connect( 'value-changed', - self.on_autostop_size_spinbutton_toggled, + self.on_autostop_size_spinbutton_changed, ) combo3.connect('changed', self.on_autostop_size_combo_changed) self.add_label(grid, - 'NB Disk space is estimated, and does not apply to simulated' \ - + ' downloads (e.g. \'Check all\')', + '' + _( + 'N.B. Disk space is estimated. This setting does not apply' \ + + ' to simulated downloads', + ) + '', 0, 4, grid_width, 1, ) @@ -7348,18 +7859,20 @@ def setup_operations_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('_Operations', 0) + tab, grid = self.add_notebook_tab(_('_Operations'), 0) # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) + self.operations_inner_notebook = self.add_inner_notebook(grid) # ...with its own tabs - self.setup_operations_downloads_tab(inner_notebook) - self.setup_operations_custom_tab(inner_notebook) - self.setup_operations_notifications_tab(inner_notebook) - self.setup_operations_url_flexibility_tab(inner_notebook) - self.setup_operations_performance_tab(inner_notebook) - self.setup_operations_time_saving_tab(inner_notebook) + self.setup_operations_downloads_tab(self.operations_inner_notebook) + self.setup_operations_custom_tab(self.operations_inner_notebook) + self.setup_operations_livestreams_tab(self.operations_inner_notebook) + self.setup_operations_notifications_tab(self.operations_inner_notebook) + self.setup_operations_url_flexibility_tab( + self.operations_inner_notebook, + ) + self.setup_operations_performance_tab(self.operations_inner_notebook) def setup_operations_downloads_tab(self, inner_notebook): @@ -7369,16 +7882,21 @@ def setup_operations_downloads_tab(self, inner_notebook): Sets up the 'Downloads' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Downloads', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('_Downloads'), + inner_notebook, + ) # Download operation preferences self.add_label(grid, - 'Download operation preferences', + '' + _('Download operation preferences') + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, + _( 'Automatically update youtube-dl before every download operation', + ), self.app_obj.operation_auto_update_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -7388,8 +7906,10 @@ def setup_operations_downloads_tab(self, inner_notebook): checkbutton.set_sensitive(False) checkbutton2 = self.add_checkbutton(grid, + _( 'Automatically save files at the end of a download/update/' \ + 'refresh operation', + ), self.app_obj.operation_save_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -7397,8 +7917,10 @@ def setup_operations_downloads_tab(self, inner_notebook): checkbutton2.connect('toggled', self.on_save_button_toggled) checkbutton3 = self.add_checkbutton(grid, - 'When applying download options, automatically clone general' \ + _( + 'When applying download options to something, clone the general' \ + ' download options', + ), self.app_obj.auto_clone_options_flag, True, # Can be toggled by user 0, 3, 1, 1, @@ -7406,8 +7928,10 @@ def setup_operations_downloads_tab(self, inner_notebook): checkbutton3.connect('toggled', self.on_auto_clone_button_toggled) checkbutton4 = self.add_checkbutton(grid, + _( 'For simulated downloads, don\'t check a video in a folder' \ + ' more than once', + ), self.app_obj.operation_sim_shortcut_flag, True, # Can be toggled by user 0, 4, 1, 1, @@ -7422,18 +7946,20 @@ def setup_operations_custom_tab(self, inner_notebook): Sets up the 'Custom' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Custom', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Custom'), inner_notebook) grid_width = 2 # Custom download preferences self.add_label(grid, - 'Custom download preferences', + '' + _('Custom download preferences') + '', 0, 0, grid_width, 1, ) checkbutton = self.add_checkbutton(grid, + _( 'In custom downloads, download each video independently of its' \ + ' channel or playlist', + ), self.app_obj.custom_dl_by_video_flag, True, # Can be toggled by user 0, 1, grid_width, 1, @@ -7442,16 +7968,20 @@ def setup_operations_custom_tab(self, inner_notebook): radiobutton = self.add_radiobutton(grid, None, + _( 'In custom downloads, obtain a YouTube video from the original' \ - + 'website', + + ' website', + ), 0, 2, grid_width, 1, ) # Signal connect appears below radiobutton2 = self.add_radiobutton(grid, radiobutton, + _( 'In custom downloads, obtain the video from HookTube rather' \ + ' than YouTube', + ), 0, 3, grid_width, 1, ) if self.app_obj.custom_dl_divert_mode == 'hooktube': @@ -7460,8 +7990,10 @@ def setup_operations_custom_tab(self, inner_notebook): radiobutton3 = self.add_radiobutton(grid, radiobutton2, + _( 'In custom downloads, obtain the video from Invidious rather' \ + ' than YouTube', + ), 0, 4, grid_width, 1, ) if self.app_obj.custom_dl_divert_mode == 'invidious': @@ -7469,8 +8001,10 @@ def setup_operations_custom_tab(self, inner_notebook): # Signal connect appears below checkbutton2 = self.add_checkbutton(grid, + _( 'In custom downloads, apply a delay after each video/channel/' \ - + 'playlist download', + + 'playlist is download', + ), self.app_obj.custom_dl_delay_flag, True, # Can be toggled by user 0, 5, grid_width, 1, @@ -7478,7 +8012,7 @@ def setup_operations_custom_tab(self, inner_notebook): # signal_connect appears below self.add_label(grid, - 'Maximum delay to apply (in minutes)', + _('Maximum delay to apply (in minutes)'), 0, 6, 1, 1, ) @@ -7494,8 +8028,10 @@ def setup_operations_custom_tab(self, inner_notebook): spinbutton.set_sensitive(False) self.add_label(grid, + _( 'Minimum delay to apply (in minutes; randomises the actual' \ + ' delay)', + ), 0, 7, 1, 1, ) @@ -7546,6 +8082,200 @@ def setup_operations_custom_tab(self, inner_notebook): ) + def setup_operations_livestreams_tab(self, inner_notebook): + + """Called by self.setup_scheduling_tab(). + + Sets up the 'Streams' inner notebook tab. + """ + + tab, grid = self.add_inner_notebook_tab( + _('_Livestreams'), + inner_notebook, + ) + + grid_width = 3 + + # Livestream preferences + self.add_label(grid, + '' + _( + 'Livestream preferences (compatible websites only)', + ) + '', + 0, 0, grid_width, 1, + ) + + checkbutton = self.add_checkbutton(grid, + _('Detect livestreams announced within this many days'), + self.app_obj.enable_livestreams_flag, + True, # Can be toggled by user + 0, 1, 1, 1, + ) + # Signal connect appears below + spinbutton = self.add_spinbutton(grid, + 0, None, 1, self.app_obj.livestream_max_days, + 1, 1, 1, 1, + ) + if not self.app_obj.enable_livestreams_flag: + spinbutton.set_sensitive(False) + # Signal connect appears below + + checkbutton2 = self.add_checkbutton(grid, + _('How often to check the status of livestreams (in minutes)'), + self.app_obj.scheduled_livestream_flag, + True, # Can be toggled by user + 0, 2, 1, 1, + ) + if not self.app_obj.enable_livestreams_flag: + checkbutton2.set_sensitive(False) + # Signal connect appears below + + spinbutton2 = self.add_spinbutton(grid, + 1, None, 1, self.app_obj.scheduled_livestream_wait_mins, + 1, 2, 1, 1, + ) + if not self.app_obj.enable_livestreams_flag \ + or not self.app_obj.scheduled_livestream_flag: + spinbutton2.set_sensitive(False) + # Signal connect appears below + + # Signal connects from above + checkbutton.connect( + 'toggled', + self.on_enable_livestreams_button_toggled, + checkbutton2, + spinbutton, + spinbutton2, + ) + + spinbutton.connect( + 'value-changed', + self.on_livestream_max_days_spinbutton_changed, + ) + + checkbutton2.connect( + 'toggled', + self.on_scheduled_livestreams_button_toggled, + spinbutton2, + ) + + spinbutton2.connect( + 'value-changed', + self.on_scheduled_livestreams_spinbutton_changed, + ) + + # Video catalogue options + self.add_label(grid, + '' + _('Video Catalogue options') + '', + 0, 3, grid_width, 1, + ) + + checkbutton3 = self.add_checkbutton(grid, + _('Show livestreams with a different background colour'), + self.app_obj.livestream_use_colour_flag, + True, # Can be toggled by user + 0, 4, grid_width, 1, + ) + checkbutton3.connect( + 'toggled', + self.on_livestream_colour_button_toggled, + ) + + # Livestream actions + self.add_label(grid, + '' + _( + 'Livestream actions (can be toggled for individual videos)', + ) + '', + 0, 5, grid_width, 1, + ) + + # Currently disabled on MS Windows + if os.name == 'nt': + string = ' ' + _('(currently disabled on MS Windows)') + else: + string = '' + + checkbutton4 = self.add_checkbutton(grid, + _('When a livestream starts, show a desktop notification') \ + + string, + self.app_obj.livestream_auto_notify_flag, + True, # Can be toggled by user + 0, 6, grid_width, 1, + ) + checkbutton4.connect( + 'toggled', + self.on_livestream_auto_notify_button_toggled, + ) + if os.name == 'nt': + checkbutton4.set_sensitive(False) + + checkbutton5 = self.add_checkbutton(grid, + _('When a livestream starts, sound an alarm'), + self.app_obj.livestream_auto_alarm_flag, + True, # Can be toggled by user + 0, 7, 1, 1, + ) + if not mainapp.HAVE_PLAYSOUND_FLAG: + checkbutton5.set_sensitive(False) + checkbutton5.connect( + 'toggled', + self.on_livestream_auto_alarm_button_toggled, + ) + + combo = self.add_combo(grid, + self.app_obj.sound_list, + self.app_obj.sound_custom, + 1, 7, 1, 1, + ) + combo.connect('changed', self.on_sound_custom_changed) + if not mainapp.HAVE_PLAYSOUND_FLAG: + combo.set_sensitive(False) + + button = Gtk.Button(_('Test')) + grid.attach(button, 2, 7, 1, 1) + button.set_tooltip_text(_('Plays the selected sound effect')) + button.connect('clicked', self.on_test_sound_clicked, combo) + if not mainapp.HAVE_PLAYSOUND_FLAG: + button.set_sensitive(False) + + checkbutton6 = self.add_checkbutton(grid, + _( + 'When a livestream starts, open it in the system\'s web browser', + ), + self.app_obj.livestream_auto_open_flag, + True, # Can be toggled by user + 0, 8, grid_width, 1, + ) + checkbutton6.connect( + 'toggled', + self.on_livestream_auto_open_button_toggled, + ) + + checkbutton7 = self.add_checkbutton(grid, + _('When a livestream starts, begin downloading it immediately'), + self.app_obj.livestream_auto_dl_start_flag, + True, # Can be toggled by user + 0, 9, grid_width, 1, + ) + checkbutton7.connect( + 'toggled', + self.on_livestream_auto_dl_start_button_toggled, + ) + + checkbutton8 = self.add_checkbutton(grid, + _( + 'When a livestream stops, download it (overwriting any earlier' \ + + ' file)', + ), + self.app_obj.livestream_auto_dl_stop_flag, + True, # Can be toggled by user + 0, 10, grid_width, 1, + ) + checkbutton8.connect( + 'toggled', + self.on_livestream_auto_dl_stop_button_toggled, + ) + + def setup_operations_notifications_tab(self, inner_notebook): """Called by self.setup_operations_tab(). @@ -7554,28 +8284,32 @@ def setup_operations_notifications_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Notifications', + _('_Notifications'), inner_notebook, ) # Desktop notification preferences self.add_label(grid, - 'Desktop notification preferences', + '' + _('Desktop notification preferences') + '', 0, 0, 1, 1, ) radiobutton = self.add_radiobutton(grid, None, + _( 'Show a dialogue window at the end of a download/update/refresh/' \ + 'info/tidy operation', + ), 0, 1, 1, 1, ) # Signal connect appears below radiobutton2 = self.add_radiobutton(grid, radiobutton, + _( 'Show a desktop notification at the end of a download/update/' \ + 'refresh/info/tidy operation', + ), 0, 2, 1, 1, ) if self.app_obj.operation_dialogue_mode == 'desktop': @@ -7586,8 +8320,10 @@ def setup_operations_notifications_tab(self, inner_notebook): radiobutton3 = self.add_radiobutton(grid, radiobutton2, + _( 'Don\'t notify the user at the end of a download/update/refresh/' \ + 'info/tidy operation', + ), 0, 3, 1, 1, ) if self.app_obj.operation_dialogue_mode == 'default': @@ -7620,27 +8356,29 @@ def setup_operations_url_flexibility_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_URL flexibility', + _('_URL flexibility'), inner_notebook, ) # URL flexibility preferences self.add_label(grid, - 'URL flexibility preferences', + '' + _('URL flexibility preferences') + '', 0, 0, 1, 1, ) radiobutton = self.add_radiobutton(grid, None, + _( 'If a video\'s URL represents a channel/playlist, not a video,' \ + ' don\'t download it', + ), 0, 1, 1, 1, ) # Signal connect appears below radiobutton2 = self.add_radiobutton(grid, radiobutton, - '...or, download multiple videos into the containing folder', + _('...or, download multiple videos into the containing folder'), 0, 2, 1, 1, ) if self.app_obj.operation_convert_mode == 'multi': @@ -7649,7 +8387,9 @@ def setup_operations_url_flexibility_tab(self, inner_notebook): radiobutton3 = self.add_radiobutton(grid, radiobutton2, + _( '...or, create a new channel, and download the videos into that', + ), 0, 3, 1, 1, ) if self.app_obj.operation_convert_mode == 'channel': @@ -7658,7 +8398,9 @@ def setup_operations_url_flexibility_tab(self, inner_notebook): radiobutton4 = self.add_radiobutton(grid, radiobutton3, + _( '...or, create a new playlist, and download the videos into that', + ), 0, 4, 1, 1, ) if self.app_obj.operation_convert_mode == 'playlist': @@ -7696,7 +8438,7 @@ def setup_operations_performance_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Performance', + _('_Performance'), inner_notebook, ) @@ -7704,12 +8446,12 @@ def setup_operations_performance_tab(self, inner_notebook): # Performance limits self.add_label(grid, - 'Performance limits', + '' + _('Performance limits') + '', 0, 0, grid_width, 1, ) checkbutton = self.add_checkbutton(grid, - 'Limit simultaneous downloads to', + _('Limit simultaneous downloads to'), self.app_obj.num_worker_apply_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -7727,7 +8469,7 @@ def setup_operations_performance_tab(self, inner_notebook): spinbutton.connect('value-changed', self.on_worker_spinbutton_changed) checkbutton2 = self.add_checkbutton(grid, - 'Limit download speed to', + _('Limit download speed to'), self.app_obj.bandwidth_apply_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -7753,7 +8495,7 @@ def setup_operations_performance_tab(self, inner_notebook): ) checkbutton3 = self.add_checkbutton(grid, - 'Limit video resolution (overriding video format options) to', + _('Overriding video format options, limit video resolution to'), self.app_obj.video_res_apply_flag, True, # Can be toggled by user 0, 3, 1, 1, @@ -7773,46 +8515,33 @@ def setup_operations_performance_tab(self, inner_notebook): ) combo.connect('changed', self.on_video_res_combo_changed) - - def setup_operations_time_saving_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Time-saving' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Time-saving', - inner_notebook, - ) - - grid_width = 2 - # Time-saving preferences self.add_label(grid, - 'Time-saving preferences', - 0, 0, grid_width, 1, + '' + _('Time-saving preferences') + '', + 0, 4, grid_width, 1, ) - checkbutton = self.add_checkbutton(grid, + checkbutton4 = self.add_checkbutton(grid, + _( 'Stop checking/downloading a channel/playlist when it starts' \ + ' sending videos we already have', + ), self.app_obj.operation_limit_flag, True, # Can be toggled by user - 0, 1, grid_width, 1, + 0, 5, grid_width, 1, ) - checkbutton.set_hexpand(False) + checkbutton4.set_hexpand(False) # Signal connect appears below self.add_label(grid, - 'Stop after this many videos (when checking)', - 0, 2, 1, 1, + _('Stop after this many videos (when checking)'), + 0, 6, 1, 1, ) entry = self.add_entry(grid, self.app_obj.operation_check_limit, True, - 1, 2, 1, 1, + 1, 6, 1, 1, ) entry.set_width_chars(4) entry.connect('changed', self.on_check_limit_changed) @@ -7820,14 +8549,14 @@ def setup_operations_time_saving_tab(self, inner_notebook): entry.set_sensitive(False) self.add_label(grid, - 'Stop after this many videos (when downloading)', - 0, 3, 1, 1, + _('Stop after this many videos (when downloading)'), + 0, 7, 1, 1, ) entry2 = self.add_entry(grid, self.app_obj.operation_download_limit, True, - 1, 3, 1, 1, + 1, 7, 1, 1, ) entry2.set_width_chars(4) entry2.connect('changed', self.on_dl_limit_changed) @@ -7835,7 +8564,7 @@ def setup_operations_time_saving_tab(self, inner_notebook): entry2.set_sensitive(False) # Signal connect from above - checkbutton.connect( + checkbutton4.connect( 'toggled', self.on_limit_button_toggled, entry, @@ -7855,13 +8584,13 @@ def setup_ytdl_tab(self): # youtube-dl preferences self.add_label(grid, - 'youtube-dl preferences', + '' + _('youtube-dl preferences') + '', 0, 0, grid_width, 1, ) label = self.add_label(grid, - 'youtube-dl executable (system-dependant)', + _('youtube-dl executable (system-dependent)'), 0, 1, 1, 1, ) @@ -7874,7 +8603,7 @@ def setup_ytdl_tab(self): entry.set_editable(False) label2 = self.add_label(grid, - 'Default path to youtube-dl executable', + _('Default path to youtube-dl executable'), 0, 2, 1, 1, ) @@ -7887,18 +8616,18 @@ def setup_ytdl_tab(self): entry2.set_editable(False) label3 = self.add_label(grid, - 'Actual path to use', + _('Actual path to use'), 0, 3, 1, 1, ) combo_list = [ [ - 'Use default path (' + self.app_obj.ytdl_path_default \ + _('Use default path') + ' (' + self.app_obj.ytdl_path_default \ + ')', self.app_obj.ytdl_path_default, ], [ - 'Use local path (' + self.app_obj.ytdl_bin + ')', + _('Use local path') + ' (' + self.app_obj.ytdl_bin + ')', self.app_obj.ytdl_bin, ], ] @@ -7906,7 +8635,8 @@ def setup_ytdl_tab(self): combo_list.append( [ - 'Use PyPI path (' + self.app_obj.ytdl_path_pypi + ')', + _('Use PyPI path') + ' (' + self.app_obj.ytdl_path_pypi \ + + ')', self.app_obj.ytdl_path_pypi, ], ) @@ -7932,28 +8662,39 @@ def setup_ytdl_tab(self): combo.connect('changed', self.on_ytdl_path_combo_changed) label4 = self.add_label(grid, - 'Shell command for update operations', + _('Shell command for update operations'), 0, 4, 1, 1, ) - combo2 = self.add_combo(grid, - self.app_obj.ytdl_update_list, - self.app_obj.ytdl_update_current, - 1, 4, (grid_width - 1), 1, + store2 = Gtk.ListStore(str, str) + for item in self.app_obj.ytdl_update_list: + store2.append( [item, formats.YTDL_UPDATE_DICT[item]] ) + + combo2 = Gtk.ComboBox.new_with_model(store2) + grid.attach(combo2, 1, 4, (grid_width - 1), 1) + + renderer_text = Gtk.CellRendererText() + combo2.pack_start(renderer_text, True) + combo2.add_attribute(renderer_text, 'text', 1) + combo2.set_entry_text_column(1) + + combo2.set_active( + self.app_obj.ytdl_update_list.index( + self.app_obj.ytdl_update_current, + ), ) combo2.connect('changed', self.on_update_combo_changed) - if __main__.__pkg_strict_install_flag__: combo2.set_sensitive(False) # Post-processing preferences self.add_label(grid, - 'Post-processing preferences', + '' + _('Post-processing preferences') + '', 0, 5, grid_width, 1, ) self.add_label(grid, - 'Path to the ffmpeg/avconv binary', + _('Path to the ffmpeg/avconv binary'), 0, 6, 1, 1, ) @@ -7966,29 +8707,31 @@ def setup_ytdl_tab(self): entry3.set_editable(False) entry3.set_hexpand(True) - button = Gtk.Button('Set') + button = Gtk.Button(_('Set')) grid.attach(button, 2, 6, 1, 1) button.connect('clicked', self.on_set_ffmpeg_button_clicked, entry3) - button2 = Gtk.Button('Reset') + button2 = Gtk.Button(_('Reset')) grid.attach(button2, 3, 6, 1, 1) button2.connect('clicked', self.on_reset_ffmpeg_button_clicked, entry3) if os.name == 'nt': entry3.set_sensitive(False) - entry3.set_text('Install from main menu') + entry3.set_text(_('Install from main menu')) button.set_sensitive(False) button2.set_sensitive(False) # Other preferences self.add_label(grid, - 'Other preferences', + '' + _('Other preferences') + '', 0, 7, grid_width, 1, ) checkbutton = self.add_checkbutton(grid, + _( 'Allow youtube-dl 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, 8, grid_width, 1, @@ -7996,8 +8739,10 @@ def setup_ytdl_tab(self): checkbutton.connect('toggled', self.on_archive_button_toggled) checkbutton2 = self.add_checkbutton(grid, + _( 'When checking videos, apply a 60-second timeout while fetching' \ + ' JSON data', + ), self.app_obj.apply_json_timeout_flag, True, # Can be toggled by user 0, 9, grid_width, 1, @@ -8013,7 +8758,7 @@ def setup_output_tab(self): """ # Add this tab... - tab, grid = self.add_notebook_tab('Out_put', 0) + tab, grid = self.add_notebook_tab(_('Out_put'), 0) # ...and an inner notebook... inner_notebook = self.add_inner_notebook(grid) @@ -8031,16 +8776,19 @@ def setup_output_outputtab_tab(self, inner_notebook): Sets up the 'Output Tab' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Output Tab', inner_notebook) + tab, grid = self.add_inner_notebook_tab( + _('_Output Tab'), + inner_notebook, + ) # Output Tab preferences self.add_label(grid, - 'Output Tab preferences', + '' + _('Output Tab preferences') + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, - 'Display youtube-dl system commands in the Output Tab', + _('Display youtube-dl system commands in the Output Tab'), self.app_obj.ytdl_output_system_cmd_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -8049,7 +8797,7 @@ def setup_output_outputtab_tab(self, inner_notebook): 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 youtube-dl\'s STDOUT in the Output Tab'), self.app_obj.ytdl_output_stdout_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -8058,7 +8806,7 @@ def setup_output_outputtab_tab(self, inner_notebook): # Signal connect appears below checkbutton3 = self.add_checkbutton(grid, - '...but don\'t write each video\'s JSON data', + _('...but don\'t write each video\'s JSON data'), self.app_obj.ytdl_output_ignore_json_flag, True, # Can be toggled by user 0, 3, 1, 1, @@ -8069,7 +8817,7 @@ def setup_output_outputtab_tab(self, inner_notebook): checkbutton3.set_sensitive(False) checkbutton4 = self.add_checkbutton(grid, - '...but don\'t write each video\'s download progress', + _('...but don\'t write each video\'s download progress'), self.app_obj.ytdl_output_ignore_progress_flag, True, # Can be toggled by user 0, 4, 1, 1, @@ -8088,7 +8836,7 @@ def setup_output_outputtab_tab(self, inner_notebook): ) checkbutton5 = self.add_checkbutton(grid, - 'Display output from youtube-dl\'s STDERR in the Output Tab', + _('Display output from youtube-dl\'s STDERR in the Output Tab'), self.app_obj.ytdl_output_stderr_flag, True, # Can be toggled by user 0, 5, 1, 1, @@ -8097,7 +8845,7 @@ def setup_output_outputtab_tab(self, inner_notebook): checkbutton5.connect('toggled', self.on_output_stderr_button_toggled) checkbutton6 = self.add_checkbutton(grid, - 'Empty pages in the Output Tab at the start of every operation', + _('Empty pages in the Output Tab at the start of every operation'), self.app_obj.ytdl_output_start_empty_flag, True, # Can be toggled by user 0, 6, 1, 1, @@ -8106,8 +8854,10 @@ def setup_output_outputtab_tab(self, inner_notebook): checkbutton6.connect('toggled', self.on_output_empty_button_toggled) checkbutton7 = self.add_checkbutton(grid, - 'Show a summary of active threads (changes are applied when ' \ - + __main__.__prettyname__ + ' restarts', + _( + 'Show a summary of active threads (changes are applied when' \ + + ' Tartube restarts)', + ), self.app_obj.ytdl_output_show_summary_flag, True, # Can be toggled by user 0, 7, 1, 1, @@ -8116,8 +8866,10 @@ def setup_output_outputtab_tab(self, inner_notebook): checkbutton7.connect('toggled', self.on_output_summary_button_toggled) checkbutton8 = 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, @@ -8126,7 +8878,7 @@ def setup_output_outputtab_tab(self, inner_notebook): # Signal connect appears below checkbutton9 = self.add_checkbutton(grid, - '...also show all non-matching videos', + _('...also show all non-matching videos'), self.app_obj.refresh_output_verbose_flag, True, # Can be toggled by user 0, 9, 1, 1, @@ -8155,18 +8907,18 @@ def setup_output_terminal_window_tab(self, inner_notebook): """ tab, grid = self.add_inner_notebook_tab( - '_Terminal window', + _('_Terminal window'), inner_notebook, ) # Terminal window preferences self.add_label(grid, - 'Terminal window preferences', + '' + _('Terminal window preferences') + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, - 'Write youtube-dl system commands to the terminal window', + _('Write youtube-dl system commands to the terminal window'), self.app_obj.ytdl_write_system_cmd_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -8175,7 +8927,7 @@ def setup_output_terminal_window_tab(self, inner_notebook): 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 youtube-dl\'s STDOUT to the terminal window'), self.app_obj.ytdl_write_stdout_flag, True, # Can be toggled by user 0, 2, 1, 1, @@ -8184,7 +8936,7 @@ def setup_output_terminal_window_tab(self, inner_notebook): # Signal connect appears below checkbutton3 = self.add_checkbutton(grid, - '...but don\'t write each video\'s JSON data', + _('...but don\'t write each video\'s JSON data'), self.app_obj.ytdl_write_ignore_json_flag, True, # Can be toggled by user 0, 3, 1, 1, @@ -8195,7 +8947,7 @@ def setup_output_terminal_window_tab(self, inner_notebook): checkbutton3.set_sensitive(False) checkbutton4 = self.add_checkbutton(grid, - '...but don\'t write each video\'s download progress', + _('...but don\'t write each video\'s download progress'), self.app_obj.ytdl_write_ignore_progress_flag, True, # Can be toggled by user 0, 4, 1, 1, @@ -8217,7 +8969,7 @@ def setup_output_terminal_window_tab(self, inner_notebook): ) checkbutton5 = self.add_checkbutton(grid, - 'Write output from youtube-dl\'s STDERR to the terminal window', + _('Write output from youtube-dl\'s STDERR to the terminal window'), self.app_obj.ytdl_write_stderr_flag, True, # Can be toggled by user 0, 5, 1, 1, @@ -8236,17 +8988,19 @@ def setup_output_both_tab(self, inner_notebook): Sets up the 'Both' inner notebook tab. """ - tab, grid = self.add_inner_notebook_tab('_Both', inner_notebook) + tab, grid = self.add_inner_notebook_tab(_('_Both'), inner_notebook) # Special preferences self.add_label(grid, - 'Special preferences (applies to both the Output Tab and the' \ - + ' terminal window)', + '' + _( + 'Special preferences (applies to both the Output Tab and the' \ + + ' terminal window)', + ) + '', 0, 0, 1, 1, ) checkbutton = self.add_checkbutton(grid, - 'Write verbose output (youtube-dl debugging mode)', + _('Write verbose output (youtube-dl debugging mode)'), self.app_obj.ytdl_write_verbose_flag, True, # Can be toggled by user 0, 1, 1, 1, @@ -8455,7 +9209,7 @@ def on_autostop_size_combo_changed(self, combo): self.app_obj.set_autostop_size_unit(model[tree_iter][0]) - def on_autostop_size_spinbutton_toggled(self, spinbutton): + def on_autostop_size_spinbutton_changed(self, spinbutton): """Called from callback in self.setup_scheduling_stop_tab(). @@ -8518,7 +9272,7 @@ def on_autostop_time_combo_changed(self, combo): self.app_obj.set_autostop_time_unit(model[tree_iter][0]) - def on_autostop_time_spinbutton_toggled(self, spinbutton): + def on_autostop_time_spinbutton_changed(self, spinbutton): """Called from callback in self.setup_scheduling_stop_tab(). @@ -8559,7 +9313,7 @@ def on_autostop_videos_button_toggled(self, checkbutton, spinbutton): spinbutton.set_sensitive(False) - def on_autostop_videos_spinbutton_toggled(self, spinbutton): + def on_autostop_videos_spinbutton_changed(self, spinbutton): """Called from callback in self.setup_scheduling_stop_tab(). @@ -8714,7 +9468,7 @@ def on_child_process_button_toggled(self, checkbutton): def on_clipboard_button_toggled(self, checkbutton): - """Called from a callback in self.setup_windows_dialogue_windows_tab(). + """Called from a callback in self.setup_windows_dialogues_tab(). Enables/disables copying from the system clipboard in various dialogue windows. @@ -8974,7 +9728,7 @@ def on_data_dir_change_button_clicked(self, button, entry): """ dialogue_win = Gtk.FileChooserDialog( - 'Please select ' + __main__.__prettyname__ + '\'s data directory', + _('Please select Tartube\'s data folder'), self, Gtk.FileChooserAction.SELECT_FOLDER, ( @@ -9004,8 +9758,10 @@ def on_data_dir_change_button_clicked(self, button, entry): if not os.path.isfile(db_path): dialogue_manager_obj.show_msg_dialogue( - 'Are you sure you want to create a new database at this' \ - + ' location?\n\n' + new_path, + _( + 'Are you sure you want to create a new database at' \ + + ' this location?', + ) + '\n\n' + new_path, 'question', 'yes-no', self, # Parent window is this window @@ -9110,7 +9866,7 @@ def on_data_dir_forget_button_clicked(self, button, treeview): # Prompt the user for confirmation. If the user confirms, this window # is reset to update the treeview self.app_obj.dialogue_manager_obj.show_msg_dialogue( - 'Are you sure you want to forget this database?', + _('Are you sure you want to forget this database?'), 'question', 'yes-no', self, # Parent window is this window @@ -9144,8 +9900,10 @@ def on_data_dir_forget_all_button_clicked(self, button, treeview): # Prompt the user for confirmation. If the user confirms, this window # is reset to update the treeview self.app_obj.dialogue_manager_obj.show_msg_dialogue( + _( 'Are you sure you want to forget all databases except the' \ + ' current one?', + ), 'question', 'yes-no', self, # Parent window is this window @@ -9156,79 +9914,149 @@ def on_data_dir_forget_all_button_clicked(self, button, treeview): ) - def on_data_dir_move_up_button_clicked(self, button, treeview, liststore): + def on_data_dir_move_down_button_clicked(self, button, treeview, \ + liststore, button2): """Called from callback in self.setup_filesystem_database_tab(). - Moves the selected data directory up one position in the list of + Moves the selected data directory down one position in the list of alternative data directories. Args: - button (Gtk.Button): The widget that was clicked + button (Gtk.Button): The widget that was clicked (the down button) treeview (Gtk.TreeView): The widget in which a line was selected liststore (Gtk.ListStore): The treeview's liststore + button2 (Gtk.Button): The up button + """ selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: + (model, path_list) = selection.get_selected_rows() + if not path_list: # Nothing selected return - else: + # (Keeping track of the first/last selected items helps us to + # (de)sensitise buttons, in a moment) + first_item = None + last_item = None - data_dir = model[iter][0] + path_list.reverse() - # Update the IV - self.app_obj.reorder_db(data_dir, False) + for path in path_list: - # Update the liststore - liststore.clear() - for item in self.app_obj.data_dir_alt_list: - liststore.append([item]) + this_iter = model.get_iter(path) + last_item = model[this_iter][0] + if first_item is None: + first_item = model[this_iter][0] + if model.iter_next(this_iter): - def on_data_dir_move_down_button_clicked(self, button, treeview, \ - liststore): + liststore.move_after( + this_iter, + model.iter_next(this_iter), + ) + + else: + + # If the first item won't move up, then successive items will + # be moved above this one (which is not what we want) + break + + # Update the IV + dir_list = [] + for row in liststore: + dir_list.append(row[0]) + + self.app_obj.set_data_dir_alt_list(dir_list) + + # (De)sensitise the button(s), if required + if dir_list.index(first_item) == 0: + button2.set_sensitive(False) + else: + button2.set_sensitive(True) + + if dir_list.index(last_item) == (len(dir_list) - 1): + button.set_sensitive(False) + else: + button.set_sensitive(True) + + + def on_data_dir_move_up_button_clicked(self, button, treeview, liststore, + button2): """Called from callback in self.setup_filesystem_database_tab(). - Moves the selected data directory down one position in the list of + Moves the selected data directory up one position in the list of alternative data directories. Args: - button (Gtk.Button): The widget that was clicked + button (Gtk.Button): The widget that was clicked (the up button) treeview (Gtk.TreeView): The widget in which a line was selected liststore (Gtk.ListStore): The treeview's liststore + button2 (Gtk.Button): The down button + """ selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: + (model, path_list) = selection.get_selected_rows() + if not path_list: # Nothing selected return - else: + # (Keeping track of the first/last selected items helps us to + # (de)sensitise buttons, in a moment) + first_item = None + last_item = None - data_dir = model[iter][0] + # Move the selected items up + for path in path_list: + + this_iter = model.get_iter(path) + last_item = model[this_iter][0] + if first_item is None: + first_item = model[this_iter][0] + + if model.iter_previous(this_iter): + + liststore.move_before( + this_iter, + model.iter_previous(this_iter), + ) + + else: + + # If the first item won't move up, then successive items will + # be moved above this one (which is not what we want) + break + + # Update the IV + dir_list = [] + for row in liststore: + dir_list.append(row[0]) + + self.app_obj.set_data_dir_alt_list(dir_list) - # Update the IV - self.app_obj.reorder_db(data_dir, True) + # (De)sensitise the button(s), if required + if dir_list.index(first_item) == 0: + button.set_sensitive(False) + else: + button.set_sensitive(True) - # Update the liststore - liststore.clear() - for item in self.app_obj.data_dir_alt_list: - liststore.append([item]) + if dir_list.index(last_item) == (len(dir_list) - 1): + button2.set_sensitive(False) + else: + button2.set_sensitive(True) def on_data_dir_switch_button_clicked(self, button, button2, treeview, \ @@ -9276,8 +10104,11 @@ def on_data_dir_switch_button_clicked(self, button, button2, treeview, \ if not os.path.isfile(db_path): self.app_obj.dialogue_manager_obj.show_msg_dialogue( - 'No database exists at this location:\n\n' + data_dir \ - + '\n\nDo you want to create a new one?', + _( + 'No database exists at this location:', + ) + '\n\n' + data_dir + '\n\n' + _( + 'Do you want to create a new one?', + ), 'question', 'yes-no', self, # Parent window is this window @@ -9560,6 +10391,40 @@ def on_dl_wait_spinbutton_changed(self, spinbutton): self.app_obj.set_scheduled_dl_wait_hours(spinbutton.get_value()) + def on_enable_livestreams_button_toggled(self, checkbutton, checkbutton2, + spinbutton, spinbutton2): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Enables/disables livestream detection. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + checkbutton2 (Gtk.CheckButton): Another widget to sensitise/ + desensitise, according to the new value of the flag + + spinbutton, spinbutton2 (Gtk.SpinButton): Another widget to + sensitise/desensitise, according to the new value of the flag + + """ + + if checkbutton.get_active() \ + and not self.app_obj.enable_livestreams_flag: + self.app_obj.set_enable_livestreams_flag(True) + checkbutton2.set_sensitive(True) + spinbutton.set_sensitive(True) + spinbutton2.set_sensitive(True) + + elif not checkbutton.get_active() \ + and self.app_obj.enable_livestreams_flag: + self.app_obj.set_enable_livestreams_flag(False) + checkbutton2.set_sensitive(False) + spinbutton.set_sensitive(False) + spinbutton2.set_sensitive(False) + + def on_expand_tree_toggled(self, checkbutton, checkbutton2): """Called from callback in self.setup_windows_main_window_tab(). @@ -9609,7 +10474,7 @@ def on_expand_full_tree_toggled(self, checkbutton): def on_gtk_emulate_button_toggled(self, checkbutton): - """Called from callback in self.setup_general_modules_tab(). + """Called from callback in self.setup_general_stability_tab(). Enables/disables emulation of a broken Gtk library. @@ -9692,7 +10557,7 @@ def on_json_button_toggled(self, checkbutton): def on_keep_open_button_toggled(self, checkbutton, checkbutton2): - """Called from a callback in self.setup_windows_dialogue_windows_tab(). + """Called from a callback in self.setup_windows_dialogues_tab(). Enables/disables keeping the dialogue window open when adding channels/ playlists/folders. @@ -9745,6 +10610,202 @@ def on_limit_button_toggled(self, checkbutton, entry, entry2): entry2.set_sensitive(False) + def on_livestream_auto_alarm_button_toggled(self, checkbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Enables/disables sounding an alarm when a livestream starts. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + """ + + if checkbutton.get_active() \ + and not self.app_obj.livestream_auto_alarm_flag: + self.app_obj.set_livestream_auto_alarm_flag(True) + elif not checkbutton.get_active() \ + and self.app_obj.livestream_auto_alarm_flag: + self.app_obj.set_livestream_auto_alarm_flag(False) + + + def on_livestream_auto_dl_start_button_toggled(self, checkbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Enables/disables downloading a livestream as soon as it starts. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + """ + + if checkbutton.get_active() \ + and not self.app_obj.livestream_auto_dl_start_flag: + self.app_obj.set_livestream_auto_dl_start_flag(True) + elif not checkbutton.get_active() \ + and self.app_obj.livestream_auto_dl_start_flag: + self.app_obj.set_livestream_auto_dl_start_flag(False) + + + def on_livestream_auto_dl_stop_button_toggled(self, checkbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Enables/disables downloading a livestream as soon as it stops. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + """ + + if checkbutton.get_active() \ + and not self.app_obj.livestream_auto_dl_stop_flag: + self.app_obj.set_livestream_auto_dl_stop_flag(True) + elif not checkbutton.get_active() \ + and self.app_obj.livestream_auto_dl_stop_flag: + self.app_obj.set_livestream_auto_dl_stop_flag(False) + + + def on_livestream_auto_notify_button_toggled(self, checkbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Enables/disables desktop notifications when a livestream starts. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + """ + + if checkbutton.get_active() \ + and not self.app_obj.livestream_auto_notify_flag: + self.app_obj.set_livestream_auto_notify_flag(True) + elif not checkbutton.get_active() \ + and self.app_obj.livestream_auto_notify_flag: + self.app_obj.set_livestream_auto_notify_flag(False) + + + def on_livestream_auto_open_button_toggled(self, checkbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Enables/disables opening a livestream in the system's web browser when + it starts. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + """ + + if checkbutton.get_active() \ + and not self.app_obj.livestream_auto_open_flag: + self.app_obj.set_livestream_auto_open_flag(True) + elif not checkbutton.get_active() \ + and self.app_obj.livestream_auto_open_flag: + self.app_obj.set_livestream_auto_open_flag(False) + + + def on_livestream_colour_button_toggled(self, checkbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Enables/disables coloured backgrounds for livestream videos in the + Video Catalogue. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + """ + + if checkbutton.get_active() \ + and not self.app_obj.livestream_use_colour_flag: + self.app_obj.set_livestream_use_colour_flag(True) + elif not checkbutton.get_active() \ + and self.app_obj.livestream_use_colour_flag: + self.app_obj.set_livestream_use_colour_flag(False) + + + def on_livestream_max_days_spinbutton_changed(self, spinbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Sets the time (in days) at which Tartube stops looking for livestreams. + + Args: + + spinbutton (Gtk.SpinButton): The widget clicked + + """ + + self.app_obj.set_livestream_max_days( + spinbutton.get_value(), + ) + + + def on_locale_combo_changed(self, combo, grid): + + """Called from a callback in self.setup_general_language_tab(). + + Sets the custom locale for Tartube. + + Args: + + combo (Gtk.ComboBox): The widget clicked + + grid (Gtk.Grid): The grid on which this tab's widgets are + arranged + + """ + + tree_iter = combo.get_active_iter() + model = combo.get_model() + language = model[tree_iter][1] + + for key in formats.LOCALE_DICT: + if formats.LOCALE_DICT[key] == language: + + self.app_obj.set_custom_locale(key) + + # Add some more widgets to tell the user to restart Tartube. + # As the user might not know the language, show an icon as + # well as some text + # Use an extra grid to avoid messing up the layout of widgets + # above + grid2 = Gtk.Grid() + grid.attach(grid2, 0, 2, 2, 1) + grid2.set_border_width(self.spacing_size * 2) + grid2.set_column_spacing(self.spacing_size) + grid2.set_row_spacing(self.spacing_size) + + frame = self.add_image(grid2, + self.app_obj.main_win_obj.icon_dict['tool_quit_large'], + 0, 2, 1, 1, + ) + # (The frame looks cramped without this. The icon itself is + # 32x32) + frame.set_size_request( + 32 + (self.spacing_size * 2), + 32 + (self.spacing_size * 2), + ) + + self.add_label(grid2, + '' + _( + 'The new setting will be applied when Tartube' \ + + ' restarts', + ) + '', + 1, 2, 1, 1, + ) + + self.show_all() + + def on_match_button_toggled(self, radiobutton): """Called from callback in self.setup_general_video_matching_tab(). @@ -10331,6 +11392,50 @@ def on_save_button_toggled(self, checkbutton): self.app_obj.set_operation_save_flag(False) + def on_scheduled_livestreams_button_toggled(self, checkbutton, spinbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Enables starting the livestream task periodically to check videos + marked as livestreams. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + spinbutton (Gtk.SpinButton): Another widget to sensitise/ + desensitise, according to the new value of the flag + + """ + + if checkbutton.get_active() \ + and not self.app_obj.scheduled_livestream_flag: + self.app_obj.set_scheduled_livestream_flag(True) + spinbutton.set_sensitive(True) + + elif not checkbutton.get_active() \ + and self.app_obj.scheduled_livestream_flag: + self.app_obj.set_scheduled_livestream_flag(False) + spinbutton.set_sensitive(False) + + + def on_scheduled_livestreams_spinbutton_changed(self, spinbutton): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Sets the time (in minutes) between scheduled livestream operations. + + Args: + + spinbutton (Gtk.SpinButton): The widget clicked + + """ + + self.app_obj.set_scheduled_livestream_wait_mins( + spinbutton.get_value(), + ) + + def on_scheduled_stop_button_toggled(self, checkbutton): """Called from a callback in self.setup_scheduling_start_tab(). @@ -10368,7 +11473,7 @@ def on_set_ffmpeg_button_clicked(self, button, entry): """ dialogue_win = Gtk.FileChooserDialog( - 'Please select the ffmpeg executable', + _('Please select the FFmpeg executable'), self, Gtk.FileChooserAction.OPEN, ( @@ -10389,6 +11494,26 @@ def on_set_ffmpeg_button_clicked(self, button, entry): entry.set_text(self.app_obj.ffmpeg_path) + def on_show_classic_mode_button_toggled(self, checkbutton): + + """Called from a callback in self.setup_windows_main_window_tab(). + + Enables/disables automatically opening the Classic Mode tab on startup. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked + + """ + + if checkbutton.get_active() \ + and not self.app_obj.show_classic_tab_on_startup_flag: + self.app_obj.set_show_classic_tab_on_startup_flag(True) + elif not checkbutton.get_active() \ + and self.app_obj.show_classic_tab_on_startup_flag: + self.app_obj.set_show_classic_tab_on_startup_flag(False) + + def on_show_small_icons_toggled(self, checkbutton): """Called from callback in self.setup_windows_main_window_tab(). @@ -10455,6 +11580,23 @@ def on_show_tooltips_toggled(self, checkbutton): self.app_obj.set_show_tooltips_flag(False) + def on_sound_custom_changed(self, combo): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Sets the user's preferred sound effect for livestream alarms. + + Args: + + combo (Gtk.ComboBox): The widget clicked + + """ + + tree_iter = combo.get_active_iter() + model = combo.get_model() + self.app_obj.set_sound_custom(model[tree_iter][0]) + + def on_squeeze_button_toggled(self, checkbutton): """Called from callback in self.setup_windows_main_window_tab(). @@ -10656,6 +11798,24 @@ def on_terminal_system_button_toggled(self, checkbutton): self.app_obj.set_ytdl_write_system_cmd_flag(False) + def on_test_sound_clicked(self, button, combo): + + """Called from callback in self.setup_operations_livestreams_tab(). + + Plays the sound effect selected in the combobox. + + Args: + + button (Gtk.Button): The widget that was clicked + + combo (Gtk.ComboBox): The widget in which a sound effect is + selected. + + """ + + self.app_obj.play_sound() + + def on_update_combo_changed(self, combo): """Called from a callback in self.setup_ytdl_tab(). @@ -10897,7 +12057,7 @@ def try_switch_db(self, data_dir, button): else: dialogue_win = dialogue_manager_obj.show_msg_dialogue( - 'Database file not loaded', + _('Database file not loaded'), 'error', 'ok', self, # Parent window is this window @@ -10932,7 +12092,7 @@ def try_switch_db(self, data_dir, button): else: dialogue_manager_obj.show_msg_dialogue( - 'Database file loaded', + _('Database file loaded'), 'info', 'ok', self, # Parent window is this window diff --git a/tartube/dialogue.py b/tartube/dialogue.py index a54780bd..dad33fa8 100755 --- a/tartube/dialogue.py +++ b/tartube/dialogue.py @@ -28,6 +28,7 @@ # Import other modules import os +import re import threading @@ -99,9 +100,11 @@ def show_msg_dialogue(self, msg, msg_type, button_type, clicked the 'yes' or 'no' button). If specified, the keys are 0, 1 or more of the values 'ok', 'cancel', 'yes', 'no'. The corresponding values are the mainapp.TartubeApp function called - if the user clicks that button. The dictionary can also contain - the key 'data'. If it does, the corresponding value is passed - to the mainapp.TartubeApp function as an argument + if the user clicks that button. (f the value begins with + 'main_win_', then the rest of the value is the mainwin.MainWin + function called). The dictionary can also contain the key + 'data'. If it does, the corresponding value is passed to the + mainapp.TartubeApp function as an argument Returns: @@ -168,9 +171,10 @@ class MessageDialogue(Gtk.MessageDialog): the 'yes' or 'no' button). If specified, the keys are 0, 1 or more of the values 'ok', 'cancel', 'yes', 'no'. The corresponding values are the mainapp.TartubeApp function called if the user clicks that - button. The dictionary can also contain the key 'data'. If it does, - the corresponding value is passed to the mainapp.TartubeApp - function as an argument + button. (f the value begins with 'main_win_', then the rest of the + value is the mainwin.MainWin function called). The dictionary can + also contain the key 'data'. If it does, the corresponding value is + passed to the mainapp.TartubeApp function as an argument """ @@ -272,9 +276,11 @@ def on_clicked(self, widget, response, app_obj, response_dict): clicked the 'yes' or 'no' button). If specified, the keys are 0, 1 or more of the values 'ok', 'cancel', 'yes', 'no'. The corresponding values are the mainapp.TartubeApp function called - if the user clicks that button. The dictionary can also contain - the key 'data'. If it does, the corresponding value is passed - to the mainapp.TartubeApp function as an argument + if the user clicks that button. (f the value begins with + 'main_win_', then the rest of the value is the mainwin.MainWin + function called). The dictionary can also contain the key + 'data'. If it does, the corresponding value is passed to the + mainapp.TartubeApp function as an argument """ @@ -296,8 +302,18 @@ def on_clicked(self, widget, response, app_obj, response_dict): func = response_dict['no'] if func is not None: - # Call the specified mainapp.TartubeApp function - method = getattr(app_obj, func) + + # Is it a mainapp.TartubeApp function or a mainwin.MainWin + # function? + if re.search('^main_win_', func): + + # We will call the specified mainwin.MainWin function + method = getattr(app_obj.main_win_obj, func[9::]) + + else: + + # We will call the specified mainapp.TartubeApp function + method = getattr(app_obj, func) # If the dictionary contains a key called 'data', use its # corresponding value as an argument in the call diff --git a/tartube/downloads.py b/tartube/downloads.py index 72b4a1d4..8f120317 100755 --- a/tartube/downloads.py +++ b/tartube/downloads.py @@ -17,7 +17,7 @@ # this program. If not, see . -"""Download operation classes.""" +"""Download and livestream operation classes.""" # Import Gtk modules @@ -49,6 +49,11 @@ import media import options import utils +# Use same gettext translations +from mainapp import _ + +if mainapp.HAVE_FEEDPARSER_FLAG: + import feedparser # Debugging flag (calls utils.debug_time at the start of every function) @@ -95,7 +100,8 @@ class DownloadManager(threading.Thread): videos should be downloaded (or not) depending on each media data object's .dl_sim_flag IV. 'custom' is like 'real', but with additional options applied (specified by IVs like - mainapp.TartubeApp.custom_dl_by_video_flag) + self.custom_dl_by_video_flag). 'classic' if the Classic Mode Tab is + open, and the user has clicked the download button there download_list_obj(downloads.DownloadManager): An ordered list of media data objects to download, each one represented by a @@ -110,7 +116,7 @@ class DownloadManager(threading.Thread): def __init__(self, app_obj, operation_type, download_list_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('dld 113 __init__') + utils.debug_time('dld 119 __init__') super(DownloadManager, self).__init__() @@ -138,7 +144,9 @@ def __init__(self, app_obj, operation_type, download_list_obj): # without downloading anything. 'real' if videos should be downloaded # (or not) depending on each media data object's .dl_sim_flag IV. # 'custom' is like 'real', but with additional options applied - # (specified by IVs like mainapp.TartubeApp.custom_dl_by_video_flag) + # (specified by IVs like self.custom_dl_by_video_flag). 'classic' if + # the Classic Mode Tab is open, and the user has clicked the download + # button there self.operation_type = operation_type # The time at which the download operation began (in seconds since @@ -153,9 +161,14 @@ def __init__(self, app_obj, operation_type, download_list_obj): # Flag set to False if self.stop_download_operation() is called # The False value halts the main loop in self.run() self.running_flag = True + # Number of download jobs started (number of downloads.DownloadItem # objects which have been allocated to a worker) self.job_count = 0 + # The current downloads.DownloadItem being handled by self.run() + # (stored in this IV so that anything can update the main window's + # progress bar, at any time, by calling self.nudge_progress_bar() ) + self.current_item_obj = None # On-going counts of how many videos have been downloaded (real or # simulated), and how much disk space has been consumed (in bytes), @@ -203,11 +216,13 @@ def run(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 206 run') + utils.debug_time('dld 219 run') + + manager_string = _('D/L Manager:') + ' ' self.app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Manager: Starting download operation', + manager_string + _('Starting download operation'), ) # (Monitor changes to the number of workers, and number of available @@ -235,8 +250,8 @@ def run(self): local_worker_total_count = total_count self.app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Manager: Workers: available: ' \ - + str(available_count) + ', total: ' \ + manager_string + _('Workers: available:') + ' ' \ + + str(available_count) + ', ' + _('total:') + ' ' \ + str(total_count), ) @@ -251,68 +266,62 @@ def run(self): break # Fetch information about the next media data object to be - # downloaded - download_item_obj = self.download_list_obj.fetch_next_item() + # downloaded (and store it in an IV, so the main window's + # progress bar can be updated at any time, by any code) + self.current_item_obj = self.download_list_obj.fetch_next_item() # Exit this loop when there are no more downloads.DownloadItem # objects whose .status is formats.MAIN_STAGE_QUEUED, and when # all workers have finished their downloads # Otherwise, wait for an available downloads.DownloadWorker, and # then assign the next downloads.DownloadItem to it - if not download_item_obj: + if not self.current_item_obj: if self.check_workers_all_finished(): # Send a message to the Output Tab's summary page self.app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Manager: All threads finished', + manager_string + _('All threads finished'), ) break else: worker_obj = self.get_available_worker() - if worker_obj: - # If the worker has been marked as doomed (because the - # number of simultaneous downloads allowed has decreased) - # then we can destroy it now - if worker_obj.doomed_flag: - worker_obj.close() - self.remove_worker(worker_obj) + # If the worker has been marked as doomed (because the number + # of simultaneous downloads allowed has decreased) then we + # can destroy it now + if worker_obj and worker_obj.doomed_flag: - # Otherwise, initialise the worker's IVs for the next job - else: + worker_obj.close() + self.remove_worker(worker_obj) - # Send a message to the Output Tab's summary page - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - 'Thread #' + str(worker_obj.worker_id) \ - + ': Downloading \'' \ - + download_item_obj.media_data_obj.name + '\'', - ) + # Otherwise, initialise the worker's IVs for the next job + elif worker_obj: - # Initialise IVs - worker_obj.prepare_download(download_item_obj) - # Change the download stage for that - # downloads.DownloadItem - self.download_list_obj.change_item_stage( - download_item_obj.item_id, - formats.MAIN_STAGE_ACTIVE, - ) - # Update the main window's progress bar - self.job_count += 1 - # Throughout the downloads.py code, instead calling a - # mainapp.py or mainwin.py function directly (which - # is not thread-safe), set a Glib timeout to handle - # it - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - download_item_obj.media_data_obj.name, - self.job_count, - len(self.download_list_obj.download_item_list), - ) + # Send a message to the Output Tab's summary page + self.app_obj.main_win_obj.output_tab_write_stdout( + 0, + _('Thread #') + str(worker_obj.worker_id) \ + + ': ' + _('Downloading:') + ' \'' \ + + self.current_item_obj.media_data_obj.name + '\'', + ) + + # Initialise IVs + worker_obj.prepare_download(self.current_item_obj) + # Change the download stage for that downloads.DownloadItem + self.download_list_obj.change_item_stage( + self.current_item_obj.item_id, + formats.MAIN_STAGE_ACTIVE, + ) + # Update the main window's progress bar + self.job_count += 1 + # Throughout the downloads.py code, instead of calling a + # mainapp.py or mainwin.py function directly (which is + # not thread-safe), set a Glib timeout to handle it + if self.operation_type != 'classic': + self.nudge_progress_bar() # Pause a moment, before the next iteration of the loop (don't want # to hog resources) @@ -322,13 +331,13 @@ def run(self): # the Output Tab's summary page self.app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Manager: Downloads complete (or stopped)', + manager_string + _('Downloads complete (or stopped)'), ) # Close all the workers self.app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Manager: Halting all workers', + manager_string + _('Halting all workers'), ) for worker_obj in self.worker_list: @@ -337,7 +346,7 @@ def run(self): # Join and collect self.app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Manager: Join and collect threads', + manager_string + _('Join and collect threads'), ) for worker_obj in self.worker_list: @@ -345,18 +354,27 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Manager: Operation complete', + manager_string + _('Operation complete'), ) # Set the stop time self.stop_time = int(time.time()) - # Tell the Progress Tab to display any remaining download statistics - # immediately - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.progress_list_display_dl_stats, - ) + # Tell the Progress List (or Classic Progress List) to display any + # remaining download statistics immediately + if self.operation_type != 'classic': + + GObject.timeout_add( + 0, + self.app_obj.main_win_obj.progress_list_display_dl_stats, + ) + + else: + + GObject.timeout_add( + 0, + self.app_obj.main_win_obj.classic_mode_tab_display_dl_stats, + ) # Tell the Output Tab to display any remaining messages immediately GObject.timeout_add( @@ -381,13 +399,15 @@ def run(self): # Let the timer run for a few more seconds to allow those videos to be # marked as downloaded (we can stop before that, if all the videos # have been already marked) - if self.operation_type != 'sim': + if self.operation_type != 'sim' and self.operation_type != 'classic': GObject.timeout_add( 0, self.app_obj.download_manager_halt_timer, ) else: - # If we're only simulating downloads, we don't need to wait at all + # If we're only simulating downloads, and for download operations + # launched from the Classic Mode Tab, we don't need to wait at + # all GObject.timeout_add( 0, self.app_obj.download_manager_finished, @@ -414,7 +434,7 @@ def change_worker_count(self, number): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 417 change_worker_count') + utils.debug_time('dld 437 change_worker_count') # How many workers do we have already? current = len(self.worker_list) @@ -489,7 +509,7 @@ def check_master_slave(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 492 check_master_slave') + utils.debug_time('dld 512 check_master_slave') for worker_obj in self.worker_list: @@ -519,7 +539,7 @@ def check_workers_all_finished(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 522 check_workers_all_finished') + utils.debug_time('dld 542 check_workers_all_finished') for worker_obj in self.worker_list: if not worker_obj.available_flag: @@ -542,7 +562,7 @@ def get_available_worker(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 545 get_available_worker') + utils.debug_time('dld 565 get_available_worker') for worker_obj in self.worker_list: if worker_obj.available_flag: @@ -573,13 +593,39 @@ def mark_video_as_doomed(self, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 576 mark_video_as_doomed') + utils.debug_time('dld 596 mark_video_as_doomed') if isinstance(video_obj, media.Video) \ and not video_obj in self.doomed_video_list: self.doomed_video_list.append(video_obj) + def nudge_progress_bar (self): + + """Can be called by anything. + + Called by self.run() during the download operation. + + Also called by code in other files, just after that code adds a new + media data object to our download list. + + Updates the main window's progress bar. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 616 nudge_progress_bar') + + if self.current_item_obj: + + GObject.timeout_add( + 0, + self.app_obj.main_win_obj.update_progress_bar, + self.current_item_obj.media_data_obj.name, + self.job_count, + len(self.download_list_obj.download_item_list), + ) + + def register_video(self): """Called by VideoDownloader.confirm_new_video(), when a video is @@ -592,7 +638,7 @@ def register_video(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 595 register_video') + utils.debug_time('dld 641 register_video') self.total_video_count += 1 @@ -616,7 +662,7 @@ def register_video_size(self, size=None): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 619 register_video_size') + utils.debug_time('dld 665 register_video_size') # (In case the filesystem didn't detect the file size, for whatever # reason, we'll check for a None value) @@ -648,7 +694,7 @@ def remove_worker(self, worker_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 651 remove_worker') + utils.debug_time('dld 697 remove_worker') new_list = [] @@ -675,7 +721,7 @@ def stop_download_operation(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 678 stop_download_operation') + utils.debug_time('dld 724 stop_download_operation') self.running_flag = False @@ -691,7 +737,7 @@ def stop_download_operation_soon(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 694 stop_download_operation_soon') + utils.debug_time('dld 740 stop_download_operation_soon') self.download_list_obj.prevent_fetch_new_items() for worker_obj in self.worker_list: @@ -710,8 +756,12 @@ class DownloadWorker(threading.Thread): which handles a single download. The download manager runs on a loop, looking for available workers and, - when one is found, assigns them something to download. The worker - completes that download and then waits for another assignment. + when one is found, assigns them something to download. + + After the download is completely, the worker optionally checks a channel's + or a playlist's RSS feed, looking for livestreams. + + When all tasks are completed, the worker waits for another assignment. Args: @@ -727,7 +777,7 @@ class DownloadWorker(threading.Thread): def __init__(self, download_manager_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('dld 730 __init__') + utils.debug_time('dld 780 __init__') super(DownloadWorker, self).__init__() @@ -737,8 +787,11 @@ def __init__(self, download_manager_obj): self.download_manager_obj = download_manager_obj # The downloads.DownloadItem object for the current job self.download_item_obj = None - # The downloads.VideoDownloader object for the current job + # The downloads.VideoDownloader object for the current job (if it + # exists) self.video_downloader_obj = None + # The downloads.JSONFetcher object for the current job (if it exists) + self.json_fetcher_obj = None # The options.OptionsManager object for the current job self.options_manager_obj = None @@ -788,7 +841,7 @@ def run(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 791 run') + utils.debug_time('dld 844 run') # Import the main application (for convenience) app_obj = self.download_manager_obj.app_obj @@ -800,6 +853,9 @@ def run(self): # If this worker is currently assigned a job... if not self.available_flag: + # Import the media data object (for convenience) + media_data_obj = self.download_item_obj.media_data_obj + # youtube-dl-gui used a single instance of a # YoutubeDLDownloader object for each instance of a Worker # object. @@ -815,25 +871,14 @@ def run(self): # Send a message to the Output Tab's summary page app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Thread #' + str(self.worker_id) \ - + ': Assigned job \'' \ + _('Thread #') + str(self.worker_id) \ + + ': ' + _('Assigned job:') + ' \'' \ + self.download_item_obj.media_data_obj.name + '\'', ) # Then execute the assigned job return_code = self.video_downloader_obj.do_download() - # Send a message to the Output Tab's summary page - app_obj.main_win_obj.output_tab_write_stdout( - 0, - 'Thread #' + str(self.worker_id) \ - + ': Job complete \'' \ - + self.download_item_obj.media_data_obj.name + '\'', - ) - - # Import the media data object (for convenience) - media_data_obj = self.download_item_obj.media_data_obj - # If the downloads.VideoDownloader object collected any # youtube-dl error/warning messages, display them in the # Error List @@ -849,7 +894,8 @@ def run(self): # visible # Do that now (but don't if mainwin.ComplexCatalogueItem # objects aren't being used in the Video Catalogue) - if return_code == VideoDownloader.ERROR \ + if self.download_manager_obj.operation_type != 'classic' \ + and return_code == VideoDownloader.ERROR \ and isinstance(media_data_obj, media.Video) \ and app_obj.catalogue_mode != 'simple_hide_parent' \ and app_obj.catalogue_mode != 'simple_show_parent': @@ -862,14 +908,43 @@ def run(self): # Call the destructor function of VideoDownloader object self.video_downloader_obj.close() + # If possible, check the channel/playlist RSS feed for videos + # we don't already have, and mark them as livestreams + if self.running_flag \ + and mainapp.HAVE_FEEDPARSER_FLAG \ + and app_obj.enable_livestreams_flag \ + and ( + isinstance(media_data_obj, media.Channel) \ + or isinstance(media_data_obj, media.Playlist) + ) and media_data_obj.child_list \ + and media_data_obj.rss: + + # Send a message to the Output Tab's summary page + app_obj.main_win_obj.output_tab_write_stdout( + 0, + _('Thread #') + str(self.worker_id) \ + + ': ' + _('Checking RSS feed'), + ) + + # Check the RSS feed for the media data object + self.check_rss(media_data_obj) + + # Send a message to the Output Tab's summary page + app_obj.main_win_obj.output_tab_write_stdout( + 0, + _('Thread #') + str(self.worker_id) \ + + ': ' + _('Job complete') + ' \'' \ + + self.download_item_obj.media_data_obj.name + '\'', + ) + # This worker is now available for a new job self.available_flag = True # Send a message to the Output Tab's summary page app_obj.main_win_obj.output_tab_write_stdout( 0, - 'Thread #' + str(self.worker_id) \ - + ': Worker now available again', + _('Thread #') + str(self.worker_id) \ + + ': ' + _('Worker now available again'), ) # During custom downloads, apply a delay if one has been @@ -887,8 +962,6 @@ def run(self): else: delay = int(app_obj.custom_dl_delay_max * 60) - print('958 delay') - print(delay) time.sleep(delay) # Pause a moment, before the next iteration of the loop (don't want @@ -910,12 +983,178 @@ def close(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 913 close') + utils.debug_time('dld 986 close') self.running_flag = False + if self.video_downloader_obj: self.video_downloader_obj.stop() + if self.json_fetcher_obj: + self.json_fetcher_obj.stop() + + + def check_rss(self, container_obj): + + """Called by self.run(), after the VideoDownloader has finished. + + If possible, check the channel/playlist RSS feed for videos we don't + already have, and mark them as livestreams. + + This process works on YouTube (each media.Channel and media.Playlist + has the URL for its RSS feed set automatically). + + It might work on other compatible websites (the user must set the + channel's/playlist's RSS feed manually). + + On a compatible website, when youtube-dl fetches a list of videos in + the channel/playlist, it won't fetch any that are livestreams (either + waiting to start, or currently broadcasting). + + However, livestreams (both waiting and broadcasting) do appear in the + RSS feed. We can compare the RSS feed against the channel's/playlist's + list of child media.Video objects (which has just been updated), in + order to detect livestreams (with reasonably good accuracy). + + Args: + + container_obj (media.Channel, media.Playlist): The channel or + playlist which the VideoDownloader has just checked/downloaded. + (This function is not called for media.Folders or for + individual media.Video objects) + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 1029 check_rss') + + app_obj = self.download_manager_obj.app_obj + + # Livestreams are usually the first entry in the RSS feed, having not + # started yet (or being currently broadcast), but there's no + # gurantee of that + # In addition, although RSS feeds are normally quite short (with + # dozens of entries, not thousands), there is no guarantee of this + # mainapp.TartubeApp.livestream_max_days specifies how many days of + # videos we should check, looking for livestreams + # Implement this by stopping when an entry in the RSS feed matches a + # particular media.Video object + # (If we can't decide which video to match, the default to searching + # the whole RSS feed) + time_limit_video_obj = None + check_source_list = [] + + if app_obj.livestream_max_days: + + # Stop checking the RSS feed at the first matching video that's + # older than the specified time + # (Of course, the 'first video' must not itself be a livestream) + older_time = int( + time.time() - (app_obj.livestream_max_days * 86400), + ) + + for child_obj in container_obj.child_list: + if child_obj.source: + + # An entry in the RSS feed is a new livestream, if it + # doesn't match one of the videos in this list + # (We don't need to check each RSS entry against the + # entire contents of the channel/playlist - which might + # be thousands of videos - just those up to the time + # limit) + check_source_list.append(child_obj.source) + + # The time limit will apply to this video, when found + if not child_obj.live_mode \ + and child_obj.upload_time is not None \ + and child_obj.upload_time < older_time: + time_limit_video_obj = child_obj + break + + else: + + # Stop checking the RSS feed at the first matching video + for child_obj in container_obj.child_list: + if child_obj.source: + + check_source_list.append(child_obj.source) + if not child_obj.live_mode: + time_limit_video_obj = child_obj + break + + # Fetch the RSS feed + try: + feed_dict = feedparser.parse(container_obj.rss) + except: + return + + # Check each entry in the feed, stopping at the first one which matches + # the selected media.Video object + for entry_dict in feed_dict['entries']: + + if time_limit_video_obj \ + and entry_dict['link'] == time_limit_video_obj.source: + + # Found a matching media.Video object, so we can stop looking + # for livestreams now + break + + elif not entry_dict['link'] in check_source_list: + + # New livestream detected. Create a new JSONFetcher object to + # fetch its JSON data + # If the data is received, the livestream is live. If the data + # is not received, the livestream is waiting to go live + self.json_fetcher_obj = JSONFetcher( + self.download_manager_obj, + self, + container_obj, + entry_dict, + ) + + # Then execute the assigned job + self.json_fetcher_obj.do_fetch() + + # Call the destructor function of the JSONFetcher object + self.json_fetcher_obj.close() + self.json_fetcher_obj = None + +# # v2.0.063 removed - I think the code in downloads.LivestreamManager +# # and MiniJSONFetcher handles this acceptably +# # If the livestreamer cancels a livestream, before it goes live, our +# # only clue is that the video no longer appears in the RSS feed +# # Therefore, we're forced (reluctantly) to remove any media.Video +# # object which is marked as a livestream, but which is not in the +# # RSS feed +# # Compile a dictionary of media.Video objects marked as waiting +# # livestreams, whose parent channel/playlist is container_obj +# # (This is hopefully cheaper than checking every media.Video object +# # in container_obj, which might comprise thousands of videos) +# waiting_dict = {} +# # (The 1 argument specifies that we only want media.Video.live_mode = 1 +# # videos) +# video_list = container_obj.get_livestreams(app_obj, 1) +# for this_obj in video_list: +# if this_obj.source: +# waiting_dict[this_obj.source] = this_obj +# +# # Check that dictionary against the feed +# if waiting_dict: +# for entry_dict in feed_dict['entries']: +# if entry_dict['link'] in waiting_dict: +# del waiting_dict[entry_dict['link']] + +# # Delete any livestreams not found in the feed +# for delete_obj in waiting_dict.values(): + +# GObject.timeout_add( +# 0, +# self.app_obj.delete_video, +# delete_obj, +# True, # Delete files +# ) + pass + def prepare_download(self, download_item_obj): @@ -934,13 +1173,20 @@ def prepare_download(self, download_item_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 937 prepare_download') + utils.debug_time('dld 1176 prepare_download') self.download_item_obj = download_item_obj self.options_manager_obj = download_item_obj.options_manager_obj + + if self.download_manager_obj.operation_type == 'classic': + dl_classic_flag = True + else: + dl_classic_flag = False + self.options_list = self.download_manager_obj.options_parser_obj.parse( download_item_obj.media_data_obj, self.options_manager_obj, + dl_classic_flag, ) self.available_flag = False @@ -951,7 +1197,7 @@ def set_doomed_flag(self, flag): """Called by downloads.DownloadManager.change_worker_count().""" if DEBUG_FUNC_FLAG: - utils.debug_time('dld 954 set_doomed_flag') + utils.debug_time('dld 1200 set_doomed_flag') self.doomed_flag = flag @@ -983,16 +1229,29 @@ def data_callback(self, dl_stat_dict, last_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 986 data_callback') + utils.debug_time('dld 1232 data_callback') app_obj = self.download_manager_obj.app_obj - GObject.timeout_add( - 0, - app_obj.main_win_obj.progress_list_receive_dl_stats, - self.download_item_obj, - dl_stat_dict, - last_flag, - ) + + if self.download_manager_obj.operation_type != 'classic': + + GObject.timeout_add( + 0, + app_obj.main_win_obj.progress_list_receive_dl_stats, + self.download_item_obj, + dl_stat_dict, + last_flag, + ) + + else: + + GObject.timeout_add( + 0, + app_obj.main_win_obj.classic_mode_tab_receive_dl_stats, + self.download_item_obj, + dl_stat_dict, + last_flag, + ) class DownloadList(object): @@ -1019,13 +1278,17 @@ class DownloadList(object): videos should be downloaded (or not) depending on each media data object's .dl_sim_flag IV. 'custom' is like 'real', but with additional options applied (specified by IVs like - mainapp.TartubeApp.custom_dl_by_video_flag) + self.custom_dl_by_video_flag). 'classic' if the Classic Mode Tab is + open, and the user has clicked the download button there media_data_list (list): List of media.Video, media.Channel, media.Playlist and/or media.Folder objects. If not an empty list, only those media data objects and their descendants are checked/ downloaded. If an empty list, all media data objects are checked/ - downloaded + downloaded. If operation_type is 'classic', then the + media_data_list contains a list of dummy media.Video objects from a + previous call to this function. If an empty list, all dummy + media.Video objects are downloaded """ @@ -1036,7 +1299,7 @@ class DownloadList(object): def __init__(self, app_obj, operation_type, media_data_list): if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1039 __init__') + utils.debug_time('dld 1302 __init__') # IV list - class objects # ----------------------- @@ -1049,7 +1312,9 @@ def __init__(self, app_obj, operation_type, media_data_list): # without downloading anything. 'real' if videos should be downloaded # (or not) depending on each media data object's .dl_sim_flag IV. # 'custom' is like 'real', but with additional options applied - # (specified by IVs like mainapp.TartubeApp.custom_dl_by_video_flag) + # (specified by IVs like self.custom_dl_by_video_flag). 'classic' if + # the Classic Mode Tab is open, and the user has clicked the download + # button there self.operation_type = operation_type # Flag set to True in a call to self.prevent_fetch_new_items(), in # which case subsequent calls to self.fetch_next_item() return @@ -1062,6 +1327,8 @@ def __init__(self, app_obj, operation_type, media_data_list): # An ordered list of downloads.DownloadItem items, one for each # media.Video, media.Channel, media.Playlist or media.Folder object + # (including dummy media.Video objects used by download operations + # launched from the Classic Mode Tab) # This list stores each item's .item_id self.download_item_list = [] # Corresponding dictionary of downloads.DownloadItem items for quick @@ -1074,49 +1341,90 @@ def __init__(self, app_obj, operation_type, media_data_list): # Code # ---- - # For each media data object to be downloaded, created a - # downloads.DownloadItem object, and update the IVs above - if not media_data_list: + if self.operation_type != 'classic': + + # For each media data object to be downloaded, create a + # downloads.DownloadItem object, and update the IVs above + if not media_data_list: + + # Use all media data objects + for dbid in self.app_obj.media_top_level_list: + obj = self.app_obj.media_reg_dict[dbid] + self.create_item(obj) + + else: + + for media_data_obj in media_data_list: + + if isinstance(media_data_obj, media.Folder) \ + and media_data_obj.priv_flag: + + # Videos in a private folder's .child_list can't be + # downloaded (since they are also a child of a + # channel, playlist or a public folder) + GObject.timeout_add( + 0, + app_obj.system_error, + 301, + _('Cannot download videos in a private folder'), + ) + + else: - # Use all media data objects - for dbid in self.app_obj.media_top_level_list: - obj = self.app_obj.media_reg_dict[dbid] - self.create_item(obj) + # Use the specified media data object. The True value + # tells self.create_item() to download + # media_data_obj, even if it is a video in a channel + # or a playlist (which otherwise would be handled by + # downloading the channel/playlist) + self.create_item(media_data_obj, True) + + # Some media data objects have an alternate download destination, + # for example, a playlist ('slave') might download its videos + # into the directory used by a channel ('master') + # This can increase the length of the operation, because a 'slave' + # won't start until its 'master' is finished + # Make sure all designated 'masters' are handled before 'slaves' (a + # media data object can't be both a master and a slave) + self.reorder_master_slave() else: - for media_data_obj in media_data_list: + # The download operation was launched from the Classic Mode Tab. + # Each URL to be downloaded is represented by a dummy media.Video + # object (one which is not in the media data registry) + main_win_obj = self.app_obj.main_win_obj - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: + # The user may have rearranged rows in the Classic Mode Tab, so + # get a list of (all) dummy media.Videos in the rearranged order + # (It should be safe to assume that the Gtk.Liststore contains + # exactly the same number of rows, as dummy media.Video objects + # in mainwin.MainWin.classic_media_dict) + dbid_list = [] + for row in main_win_obj.classic_progress_liststore: + dbid_list.append(row[0]) - # Videos in a private folder's .child_list can't be - # downloaded (since they are also a child of a channel, - # playlist or a public folder) - GObject.timeout_add( - 0, - app_obj.system_error, - 301, - 'Cannot download videos in a private folder', - ) + # Compile a list of dummy media.Video objects in the correct order + obj_list = [] + if not media_data_list: - else: + # Use all of them + for dbid in dbid_list: + obj_list.append(main_win_obj.classic_media_dict[dbid]) + + else: + + # Use a subset of them + for dbid in dbid_list: + + dummy_obj = main_win_obj.classic_media_dict[dbid] + if dummy_obj in media_data_list: + obj_list.append(dummy_obj) - # Use the specified media data object. The True value tells - # self.create_item() to download media_data_obj, even if - # it is a video in a channel or a playlist (which - # otherwise would be handled by downloading the channel/ - # playlist) - self.create_item(media_data_obj, True) - # Some media data objects have an alternate download destination, for - # example, a playlist ('slave') might download its videos into the - # directory used by a channel ('master') - # This can increase the length of the operation, because a 'slave' - # won't start until its 'master' is finished - # Make sure all designated 'masters' are handled before 'slaves' (a - # media data object can't be both a master and a slave) - self.reorder_master_slave() + # For each dummy media.Video object, create a + # downloads.DownloadItem object, and update the IVs above + for dummy_obj in obj_list: + self.create_dummy_item(dummy_obj) # Public class methods @@ -1142,16 +1450,17 @@ def change_item_stage(self, item_id, new_stage): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1145 change_item_stage') + utils.debug_time('dld 1453 change_item_stage') self.download_item_dict[item_id].stage = new_stage def create_item(self, media_data_obj, init_flag=False): - """Called by self.__init__(), - mainapp.TartubeApp.download_watch_videos() or by this function - recursively. + """Called initially by self.__init__() (or by many other functions, + for example in mainapp.TartubeApp. + + Subsequently called by this function recursively. Creates a downloads.DownloadItem object for media data objects in the media data registry. @@ -1182,10 +1491,10 @@ def create_item(self, media_data_obj, init_flag=False): media_data_obj (media.Video, media.Channel, media.Playlist, media.Folder): A media data object - init_flag (bool): True when called by self.__init__, and False when - called by this function recursively. If True and media_data_obj - is a media.Video object, we download it even if its parent is a - channel or a playlist + init_flag (bool): False when called by this function recursively, + True when called (for the first time) by anything else. If True + and media_data_obj is a media.Video object, we download it even + if its parent is a channel or a playlist Returns: @@ -1196,7 +1505,7 @@ def create_item(self, media_data_obj, init_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1199 create_item') + utils.debug_time('dld 1508 create_item') # Get the options.OptionsManager object that applies to this media # data object @@ -1223,6 +1532,8 @@ def create_item(self, media_data_obj, init_flag=False): # their channel/playlist, if allowed) # Don't download videos in a folder, if this is a simulated download, # and the video has already been checked + # (Exception: do download videos in a folder if they're marked as + # livestreams, in case the livestream has finished) if isinstance(media_data_obj, media.Video): if media_data_obj.dl_flag \ @@ -1241,6 +1552,7 @@ def create_item(self, media_data_obj, init_flag=False): and self.operation_type == 'sim' \ and self.app_obj.operation_sim_shortcut_flag \ and media_data_obj.file_name \ + and not media_data_obj.live_mode \ and utils.find_thumbnail(self.app_obj, media_data_obj): return None @@ -1300,9 +1612,9 @@ def create_item(self, media_data_obj, init_flag=False): self.download_item_dict[download_item_obj.item_id] \ = download_item_obj - # If the media data object has children, call this function recursively + # If a media.Folder object has children, call this function recursively # for each of them - if not isinstance(media_data_obj, media.Video): + if isinstance(media_data_obj, media.Folder): for child_obj in media_data_obj.child_list: self.create_item(child_obj) @@ -1310,6 +1622,46 @@ def create_item(self, media_data_obj, init_flag=False): return download_item_obj + def create_dummy_item(self, media_data_obj): + + """Called by self.__init__() only, when the download operation was + launched from the Classic Mode Tab (this function is not called + recursively). + + Creates a downloads.DownloadItem object for each dummy media.Video + object. + + Adds the resulting downloads.DownloadItem object to this object's IVs. + + Args: + + media_data_obj (media.Video): A media data object + + Returns: + + The downloads.DownloadItem object created + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 1647 create_dummy_item') + + # Create a new download.DownloadItem object... + self.download_item_count += 1 + download_item_obj = DownloadItem( + media_data_obj.dbid, + media_data_obj, + self.app_obj.general_options_obj, + ) + + # ...and add it to our list + self.download_item_list.append(download_item_obj.item_id) + self.download_item_dict[download_item_obj.item_id] = download_item_obj + + # Procedure complete + return download_item_obj + + @synchronise(_SYNC_LOCK) def fetch_next_item(self): @@ -1325,7 +1677,7 @@ def fetch_next_item(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1328 fetch_next_item') + utils.debug_time('dld 1680 fetch_next_item') if not self.prevent_fetch_flag: @@ -1357,7 +1709,7 @@ def move_item_to_bottom(self, download_item_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1360 move_item_to_bottom') + utils.debug_time('dld 1712 move_item_to_bottom') # Move the item to the bottom (end) of the list if download_item_obj is None \ @@ -1388,7 +1740,7 @@ def move_item_to_top(self, download_item_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1391 move_item_to_top') + utils.debug_time('dld 1743 move_item_to_top') # Move the item to the top (beginning) of the list if download_item_obj is None \ @@ -1414,7 +1766,7 @@ def prevent_fetch_new_items(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1417 prevent_fetch_new_items') + utils.debug_time('dld 1769 prevent_fetch_new_items') self.prevent_fetch_flag = True @@ -1439,7 +1791,7 @@ def reorder_master_slave(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1442 reorder_master_slave') + utils.debug_time('dld 1794 reorder_master_slave') master_list = [] other_list = [] @@ -1473,7 +1825,9 @@ class DownloadItem(object): used to give each one a unique ID media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): A media data object to be downloaded + media.Folder): The media data object to be downloaded. When the + download operation was launched from the Classic Mode Tab, a dummy + media.Video object options_manager_obj (options.OptionsManager): The object which specifies download options for the media data object @@ -1487,11 +1841,12 @@ class DownloadItem(object): def __init__(self, item_id, media_data_obj, options_manager_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1490 __init__') + utils.debug_time('dld 1844 __init__') # IV list - class objects # ----------------------- - # The media data object to be downloaded + # The media data object to be downloaded. When the download operation + # was launched from the Classic Mode Tab, a dummy media.Video object self.media_data_obj = media_data_obj # The object which specifies download options for the media data object self.options_manager_obj = options_manager_obj @@ -1525,10 +1880,10 @@ class VideoDownloader(object): Args: - download_manager_obj (downloads.DownloadManager) - The download - manager object handling the entire download operation + download_manager_obj (downloads.DownloadManager): The download manager + object handling the entire download operation - download_worker_obj (downloads.DownloadWorker) - The parent download + download_worker_obj (downloads.DownloadWorker): The parent download worker object. The download manager uses multiple workers to implement simultaneous downloads. The download manager checks for free workers and, when it finds one, assigns it a @@ -1537,7 +1892,7 @@ class VideoDownloader(object): interface with youtube-dl, and waits for this object to return a return code - download_item_obj (downloads.DownloadItem) - The download item object + download_item_obj (downloads.DownloadItem): The download item object describing the URL from which youtube-dl should download video(s) Warnings: @@ -1579,7 +1934,7 @@ def __init__(self, download_manager_obj, download_worker_obj, \ download_item_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1582 __init__') + utils.debug_time('dld 1937 __init__') # IV list - class objects # ----------------------- @@ -1625,7 +1980,10 @@ def __init__(self, download_manager_obj, download_worker_obj, \ # Flag set to True if we are simulating downloads for this media data # object, or False if we actually downloading videos (set below) - self.dl_sim_flag = None + self.dl_sim_flag = False + # Flag set to True if this download operation was launched from the + # Classic Mode Tab, False if not (set below) + self.dl_classic_flag = False # Flag set to True by a call from any function to self.stop_soon() # After being set to True, this VideoDownloader should give up after @@ -1718,25 +2076,35 @@ def __init__(self, download_manager_obj, download_worker_obj, \ # download media_data_obj = self.download_item_obj.media_data_obj - # All media data objects can be marked as simulate downloads only. The - # setting applies not just to the media data object, but all of its + # All media data objects can be marked as simulate downloads only + # (except when the download operation was launched from the Classic + # Mode Tab) + # The setting applies not just to the media data object, but all of its # descendants - if self.download_manager_obj.operation_type == 'sim': - dl_sim_flag = True - else: - dl_sim_flag = media_data_obj.dl_sim_flag - parent_obj = media_data_obj.parent_obj + if self.download_manager_obj.operation_type != 'classic': + + if self.download_manager_obj.operation_type == 'sim': + dl_sim_flag = True + else: + dl_sim_flag = media_data_obj.dl_sim_flag + parent_obj = media_data_obj.parent_obj - while not dl_sim_flag and parent_obj is not None: - dl_sim_flag = parent_obj.dl_sim_flag - parent_obj = parent_obj.parent_obj + while not dl_sim_flag and parent_obj is not None: + dl_sim_flag = parent_obj.dl_sim_flag + parent_obj = parent_obj.parent_obj + + if dl_sim_flag: + self.dl_sim_flag = True + self.video_num = 0 + self.video_total = 0 + else: + self.dl_sim_flag = False + self.video_num = 1 + self.video_total = 1 - if dl_sim_flag: - self.dl_sim_flag = True - self.video_num = 0 - self.video_total = 0 else: - self.dl_sim_flag = False + + self.dl_classic_flag = True self.video_num = 1 self.video_total = 1 @@ -1760,7 +2128,7 @@ def do_download(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1763 do_download') + utils.debug_time('dld 2131 do_download') # Import the main application (for convenience) app_obj = self.download_manager_obj.app_obj @@ -1769,26 +2137,32 @@ def do_download(self): # any problems self.return_code = self.OK - # Reset the errors/warnings stored in the media data object, the last - # time it was checked/downloaded - self.download_item_obj.media_data_obj.reset_error_warning() + if not self.dl_classic_flag: - # If two channels/playlists/folders share a download destination, we - # don't want to download both of them at the same time - # If this media data obj shares a download destination with another - # downloads.DownloadWorker, wait until that download has finished - # before starting this one - if not isinstance(self.download_item_obj.media_data_obj, media.Video): + # Reset the errors/warnings stored in the media data object, the + # last time it was checked/downloaded + self.download_item_obj.media_data_obj.reset_error_warning() - while self.download_manager_obj.check_master_slave( + # If two channels/playlists/folders share a download destination, + # we don't want to download both of them at the same time + # If this media data obj shares a download destination with another + # downloads.DownloadWorker, wait until that download has finished + # before starting this one + if not isinstance( self.download_item_obj.media_data_obj, + media.Video, ): - time.sleep(self.long_sleep_time) + while self.download_manager_obj.check_master_slave( + self.download_item_obj.media_data_obj, + ): + time.sleep(self.long_sleep_time) # Prepare a system command... divert_mode = None - if self.download_manager_obj.operation_type == 'custom' \ + if not self.dl_classic_flag \ + and self.download_manager_obj.operation_type == 'custom' \ and isinstance(self.download_item_obj.media_data_obj, media.Video): + divert_mode = app_obj.custom_dl_divert_mode cmd_list = utils.generate_system_cmd( @@ -1796,6 +2170,7 @@ def do_download(self): self.download_item_obj.media_data_obj, self.download_worker_obj.options_list, self.dl_sim_flag, + self.dl_classic_flag, divert_mode, ) @@ -1915,7 +2290,6 @@ def do_download(self): if self.stop_now_flag: self.stop() - # The child process has finished while not self.stderr_queue.empty(): @@ -1960,7 +2334,7 @@ def do_download(self): if self.child_process is None: self.set_return_code(self.ERROR) self.download_item_obj.media_data_obj.set_error( - 'Download did not start', + _('Download did not start'), ) elif self.child_process.returncode > 0: @@ -1968,7 +2342,7 @@ def do_download(self): if not app_obj.ignore_child_process_exit_flag: self.download_item_obj.media_data_obj.set_error( - 'Child process exited with non-zero code: {}'.format( + _('Child process exited with non-zero code: {}').format( self.child_process.returncode, ) ) @@ -2010,8 +2384,14 @@ def check_dl_is_correct_type(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2013 check_dl_is_correct_type') + utils.debug_time('dld 2387 check_dl_is_correct_type') + + # Special case: if the download operation was launched from the + # Classic Mode Tab, there is no need to do anything + if self.dl_classic_flag: + return True + # Otherwise, import IVs (for convenience) app_obj = self.download_manager_obj.app_obj media_data_obj = self.download_item_obj.media_data_obj @@ -2030,9 +2410,10 @@ def check_dl_is_correct_type(self): # Stop downloading this URL self.stop() media_data_obj.set_error( - 'The video \'' + media_data_obj.name \ - + '\' has a source URL that points to a channel or a' \ - + ' playlist, not a video', + '\'' + media_data_obj.name + '\' ' + _( + 'This video has a URL that points to a channel or a' \ + + ' playlist, not a video', + ), ) # Don't allow self.confirm_sim_video() to be called @@ -2072,7 +2453,7 @@ def close(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2075 close') + utils.debug_time('dld 2456 close') # Tell the PipeReader objects to shut down, thus joining their threads self.stdout_reader.join() @@ -2101,9 +2482,24 @@ def confirm_new_video(self, dir_path, filename, extension): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2104 confirm_new_video') + utils.debug_time('dld 2485 confirm_new_video') + + # Special case: if the download operation was launched from the + # Classic Mode Tab, then we only need to update the dummy + # media.Video object + if self.dl_classic_flag: + + media_data_obj = self.download_item_obj.media_data_obj + media_data_obj.set_dummy_path( + os.path.abspath(os.path.join(dir_path, filename + extension)), + ) + + # Register the download with DownloadManager, so that download + # limits can be applied, if required + self.download_manager_obj.register_video() - if not self.video_num in self.video_check_dict: + # All other cases + elif not self.video_num in self.video_check_dict: app_obj = self.download_manager_obj.app_obj self.video_check_dict[self.video_num] = filename @@ -2182,13 +2578,27 @@ def confirm_old_video(self, dir_path, filename, extension): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2185 confirm_old_video') + utils.debug_time('dld 2581 confirm_old_video') # Create shortcut variables (for convenience) app_obj = self.download_manager_obj.app_obj media_data_obj = self.download_item_obj.media_data_obj - if isinstance(media_data_obj, media.Video): + # Special case: if the download operation was launched from the + # Classic Mode Tab, then we only need to update the dummy + # media.Video object + if self.dl_classic_flag: + + media_data_obj.set_dummy_path( + os.path.abspath(os.path.join(dir_path, filename, extension)), + ) + + # Register the download with DownloadManager, so that download + # limits can be applied, if required + self.download_manager_obj.register_video() + + # All other cases + elif isinstance(media_data_obj, media.Video): if not media_data_obj.dl_flag: @@ -2302,7 +2712,7 @@ def confirm_sim_video(self, json_dict): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2305 confirm_sim_video') + utils.debug_time('dld 2715 confirm_sim_video') # Import the main application (for convenience) app_obj = self.download_manager_obj.app_obj @@ -2367,6 +2777,14 @@ def confirm_sim_video(self, json_dict): else: playlist_index = None + if 'is_live' in json_dict: + if json_dict['is_live']: + live_flag = True + else: + live_flag = False + else: + live_flag = False + # Does an existing media.Video object match this video? media_data_obj = self.download_item_obj.media_data_obj video_obj = None @@ -2472,6 +2890,8 @@ def confirm_sim_video(self, json_dict): app_obj.fixed_bookmark_folder.sort_children() if video_obj.fav_flag: app_obj.fixed_fav_folder.sort_children() + if video_obj.live_mode: + app_obj.fixed_live_folder.sort_children() if video_obj.new_flag: app_obj.fixed_new_folder.sort_children() if video_obj.waiting_flag: @@ -2499,6 +2919,7 @@ def confirm_sim_video(self, json_dict): stop_flag = True else: + # This video must be displayed in the Results List, and counts # towards the limit (if any) specified by # mainapp.TartubeApp.autostop_videos_value @@ -2542,6 +2963,28 @@ def confirm_sim_video(self, json_dict): or isinstance(video_obj.parent_obj, media.Playlist): video_obj.set_index(playlist_index) + # Deal with livestreams + if video_obj.live_mode != 2 and live_flag: + + GObject.timeout_add( + 0, + app_obj.mark_video_live, + video_obj, + 2, # Livestream is broadcasting + True, # Don't update Video Index yet + True, # Don't update Video Catalogue yet + ) + + elif video_obj.live_mode != 0 and not live_flag: + + GObject.timeout_add( + 0, + app_obj.mark_video_live, + 0, # Livestream has finished + True, # Don't update Video Index yet + True, # Don't update Video Catalogue yet + ) + # 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 @@ -2644,7 +3087,7 @@ def confirm_sim_video(self, json_dict): if (app_obj.ytdl_output_stdout_flag): msg = '[' + video_obj.parent_obj.name \ - + '] ' + + '] <' + _('Simulated download of:') + ' \'' + filename + '\'>' if (app_obj.ytdl_output_stdout_flag): app_obj.main_win_obj.output_tab_write_stdout( @@ -2686,7 +3129,7 @@ def convert_video_to_container (self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2686 convert_video_to_container') + utils.debug_time('dld 3132 convert_video_to_container') app_obj = self.download_manager_obj.app_obj old_video_obj = self.download_item_obj.media_data_obj @@ -2751,9 +3194,10 @@ def convert_video_to_container (self): # stop downloading from this URL self.stop() media_data_obj.set_error( - 'The video \'' + media_data_obj.name \ - + '\' has a source URL that points to a channel or a' \ - + ' playlist, not a video', + '\'' + media_data_obj.name + '\' ' + _( + 'This video has a URL that points to a channel or a' \ + + ' playlist, not a video', + ), ) else: @@ -2785,7 +3229,7 @@ def convert_video_to_container (self): def create_child_process(self, cmd_list): """Called by self.do_download() immediately after the call to - self.get_system_cmd(). + utils.generate_system_cmd(). Based on YoutubeDLDownloader._create_process(). @@ -2804,7 +3248,7 @@ def create_child_process(self, cmd_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2804 create_child_process') + utils.debug_time('dld 3251 create_child_process') info = preexec = None if os.name == 'nt': @@ -2855,7 +3299,7 @@ def extract_filename(self, input_data): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2855 extract_filename') + utils.debug_time('dld 3302 extract_filename') path, fullname = os.path.split(input_data.strip("\"")) filename, extension = os.path.splitext(fullname) @@ -2905,7 +3349,10 @@ def extract_stdout_data(self, stdout): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2905 extract_stdout_data') + utils.debug_time('dld 3352 extract_stdout_data') + + # Import the media data object (for convenience) + media_data_obj = self.download_item_obj.media_data_obj # Initialise the dictionary with default key-value pairs for the main # window to display, to be overwritten (if possible) with new key- @@ -2986,7 +3433,8 @@ def extract_stdout_data(self, stdout): # If youtube-dl is about to download a channel or playlist into # a media.Video object, decide what to do to prevent it - self.check_dl_is_correct_type() + if not self.dl_classic_flag: + self.check_dl_is_correct_type() # Remove the 'and merged' part of the STDOUT message when using # FFmpeg to merge the formats @@ -3078,6 +3526,22 @@ def extract_stdout_data(self, stdout): self.confirm_new_video(path, filename, extension) + elif ( + isinstance(media_data_obj, media.Channel) + and not media_data_obj.rss \ + and stdout_list[0] == '[youtube:channel]' \ + ) or ( + isinstance(media_data_obj, media.Playlist) \ + and not media_data_obj.rss \ + and stdout_list[0] == '[youtube:playlist]' \ + and stdout_list[2] == 'Downloading' \ + and stdout_list[3] == 'webpage' + ): + # YouTube only: set the channel/playlist RSS feed, if not already + # set, first removing the final colon that should be there + youtube_id = re.sub('\:*$', '', stdout_list[1]) + media_data_obj.set_rss(youtube_id) + elif stdout_list[0][0] == '{': # JSON data, the result of a simulated download. Convert to a @@ -3100,10 +3564,11 @@ def extract_stdout_data(self, stdout): # If youtube-dl is about to download a channel or playlist # into a media.Video object, decide what to do to prevent + # that # The called function returns a True/False value, # specifically to allow this code block to call # self.confirm_sim_video when required - # v1.3.063 At this poitn, self.video_num can be None or 0 + # v1.3.063 At this point, self.video_num can be None or 0 # for a URL that's an individual video, but > 0 for a URL # that's actually a channel/playlist if not self.video_num \ @@ -3117,6 +3582,22 @@ def extract_stdout_data(self, stdout): dl_stat_dict['status'] = formats.ACTIVE_STAGE_CHECKING + # YouTube only: set the channel/playlist RSS feed, if not + # already set + if isinstance(media_data_obj, media.Channel) \ + and not media_data_obj.rss \ + and 'channel_id' in json_dict \ + and json_dict['channel_id'] \ + and utils.is_youtube(media_data_obj.source): + media_data_obj.set_rss(json_dict['channel_id']) + + elif isinstance(media_data_obj, media.Playlist) \ + and not media_data_obj.rss \ + and 'playlist_id' in json_dict \ + and json_dict['playlist_id'] \ + and utils.is_youtube(media_data_obj.source): + media_data_obj.set_rss(json_dict['playlist_id']) + elif stdout_list[0][0] != '[' or stdout_list[0] == '[debug]': # (Just ignore this output) @@ -3151,7 +3632,7 @@ def extract_stdout_status(self, dl_stat_dict): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3151 extract_stdout_status') + utils.debug_time('dld 3635 extract_stdout_status') if 'status' in dl_stat_dict: if dl_stat_dict['status'] == formats.COMPLETED_STAGE_ALREADY: @@ -3179,7 +3660,7 @@ def is_child_process_alive(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3179 is_child_process_alive') + utils.debug_time('dld 3663 is_child_process_alive') if self.child_process is None: return False @@ -3209,7 +3690,7 @@ def is_debug(self, stderr): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3209 is_debug') + utils.debug_time('dld 3693 is_debug') return stderr.split(' ')[0] == '[debug]' @@ -3233,9 +3714,10 @@ def is_ignorable(self, stderr): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3233 is_ignorable') + utils.debug_time('dld 3717 is_ignorable') app_obj = self.download_manager_obj.app_obj + media_data_obj = self.download_item_obj.media_data_obj if ( app_obj.ignore_http_404_error_flag \ @@ -3303,6 +3785,15 @@ def is_ignorable(self, stderr): stderr, ) ) + ) or ( + re.search(r'This video is unavailable', stderr) \ + and ( + ( + isinstance(media_data_obj, media.Video) \ + and media_data_obj.live_mode == 1 + ) or isinstance(media_data_obj, media.Channel) \ + or isinstance(media_data_obj, media.Playlist) + ) ): # This message is ignorable return True @@ -3342,7 +3833,7 @@ def is_warning(self, stderr): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3342 is_warning') + utils.debug_time('dld 3836 is_warning') return stderr.split(':')[0] == 'WARNING' @@ -3364,7 +3855,7 @@ def last_data_callback(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3364 last_data_callback') + utils.debug_time('dld 3858 last_data_callback') dl_stat_dict = {} @@ -3418,7 +3909,7 @@ def set_return_code(self, code): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3418 set_return_code') + utils.debug_time('dld 3912 set_return_code') if code >= self.return_code: self.return_code = code @@ -3429,7 +3920,7 @@ def set_temp_destination(self, path, filename, extension): """Called by self.extract_stdout_data().""" if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3429 set_temp_destination') + utils.debug_time('dld 3923 set_temp_destination') self.temp_path = path self.temp_filename = filename @@ -3441,7 +3932,7 @@ def reset_temp_destination(self): """Called by self.extract_stdout_data().""" if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3441 reset_temp_destination') + utils.debug_time('dld 3935 reset_temp_destination') self.temp_path = None self.temp_filename = None @@ -3458,7 +3949,7 @@ def stop(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3458 stop') + utils.debug_time('dld 3952 stop') if self.is_child_process_alive(): @@ -3488,11 +3979,962 @@ def stop_soon(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3488 stop_soon') + utils.debug_time('dld 3982 stop_soon') self.stop_soon_flag = True +class JSONFetcher(object): + + """Called by downloads.DownloadWorker.check_rss(). + + Python class to download JSON data for a video which is believed to be a + livestream, using youtube-dl. + + The video has been found in the channel's/playlist's RSS feed, but not by + youtube-dl, when the channel/playlist was last checked downloaded. + + If the data can be downloaded, we assume that the livestream is currently + broadcasting. If we get a 'This video is unavailable' error, we assume that + the livestream is waiting to start. + + This is the behaviour exhibited on YouTube. It might work on other + compatible websites, too, if the user has set manually set the URL for the + channel/playlist RSS feed. + + This class creates a system child process and uses the child process to + instruct youtube-dl to fetch the JSON data for the video. + + Reads from the child process STDOUT and STDERR, having set up a + downloads.PipeReader object to do so in an asynchronous way. + + If one of the two outcomes described above takes place, the media.Video + object's IVs are updated to mark it as a livestream. + + Args: + + download_manager_obj (downloads.DownloadManager): The download manager + object handling the entire download operation + + download_worker_obj (downloads.DownloadWorker): The parent download + worker object. The download manager uses multiple workers to + implement simultaneous downloads. The download manager checks for + free workers and, when it finds one, assigns it a + download.DownloadItem object. When the worker is assigned a + download item, it creates a new instance of this object for each + detected livestream, and waits for this object to complete its + task + + container_obj (media.Channel, media.Playlist): The channel/playlist + in which a livestream has been detected + + entry_dict (dict): A dictionary of values generated when reading the + RSS feed (provided by the Python feedparser module. The dictionary + represents available data for a single livestream video + + Warnings: + + The calling function is responsible for calling the close() method + when it's finished with this object, in order for this object to + properly close down. + + """ + + + # Standard class methods + + + def __init__(self, download_manager_obj, download_worker_obj, \ + container_obj, entry_dict): + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4051 __init__') + + # IV list - class objects + # ----------------------- + # The downloads.DownloadManager object handling the entire download + # operation + self.download_manager_obj = download_manager_obj + # The parent downloads.DownloadWorker object + self.download_worker_obj = download_worker_obj + # The media.Channel or media.Playlist object in which a livestream has + # been detected + self.container_obj = container_obj + + # This object reads from the child process STDOUT and STDERR in an + # asynchronous way + # Standard Python synchronised queue classes + self.stdout_queue = queue.Queue() + self.stderr_queue = queue.Queue() + # The downloads.PipeReader objects created to handle reading from the + # pipes + self.stdout_reader = PipeReader(self.stdout_queue) + self.stderr_reader = PipeReader(self.stderr_queue) + + # The child process created by self.create_child_process() + self.child_process = None + + + # IV list - other + # --------------- + # A dictionary of values generated when reading the RSS feed (provided + # by the Python feedparser module. The dictionary represents + # available data for a single livestream video + self.entry_dict = entry_dict + # Important data is extracted from the entry (below), and added to + # these IVs, ready for use + self.video_name = None + self.video_source = None + self.video_descrip = None + self.video_thumb_source = None + self.video_upload_time = None + + # The time (in seconds) between iterations of the loop in + # self.do_fetch() + self.sleep_time = 0.1 + + + # Code + # ---- + # Initialise IVs from the RSS feed entry for the livestream video + # (saves a bit of time later) + if 'title' in entry_dict: + self.video_name = entry_dict['title'] + + if 'link' in entry_dict: + self.video_source = entry_dict['link'] + + if 'summary' in entry_dict: + self.video_descrip = entry_dict['summary'] + + if 'media_thumbnail' in entry_dict \ + and entry_dict['media_thumbnail'] \ + and 'url' in entry_dict['media_thumbnail'][0]: + self.video_thumb_source = entry_dict['media_thumbnail'][0]['url'] + + if 'published_parsed' in entry_dict: + # A time.struct_time object; convert to Unix time, to match + # media.Video.upload_time + dt_obj = datetime.datetime.fromtimestamp( + time.mktime(entry_dict['published_parsed']), + ) + + self.video_upload_time = int(dt_obj.timestamp()) + + + # Public class methods + + + def do_fetch(self): + + """Called by downloads.DownloadWorker.check_rss(). + + Downloads JSON data for the livestream video whose URL is + self.video_source. + + If the data can be downloaded, we assume that the livestream is + currently broadcasting. If we get a 'This video is unavailable' error, + we assume that the livestream is waiting to start. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4141 do_fetch') + + # Import the main app (for convenience) + app_obj = self.download_manager_obj.app_obj + + # Convert a youtube-dl path beginning with ~ (not on MS Windows) + # (code copied from utils.generate_system_cmd() ) + ytdl_path = app_obj.ytdl_path + if os.name != 'nt': + ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path) + + # Generate the system command... + cmd_list = [ytdl_path] + ['--dump-json'] + [self.video_source] + # ...and create a new child process using that command + self.create_child_process(cmd_list) + + # So that we can read from the child process STDOUT and STDERR, attach + # a file descriptor to the PipeReader objects + if self.child_process is not None: + + self.stdout_reader.attach_file_descriptor( + self.child_process.stdout, + ) + + self.stderr_reader.attach_file_descriptor( + self.child_process.stderr, + ) + + # Wait for the process to finish + while self.is_child_process_alive(): + + # Pause a moment between each iteration of the loop (we don't want + # to hog system resources) + time.sleep(self.sleep_time) + + # Process has finished. If the JSON data has been received, indicating + # a livestream currently broadcasting, it's in STDOUT + new_video_flag = None + while not self.stdout_queue.empty(): + + stdout = self.stdout_queue.get_nowait().rstrip() + if stdout: + + # (Convert bytes to string) + stdout = stdout.decode() + if stdout[:1] == '{': + + # Broadcasting livestream detected; create a new + # media.Video object + GObject.timeout_add( + 0, + app_obj.create_livestream_from_download, + self.container_obj, + 2, # Livestream has started + self.video_name, + self.video_source, + self.video_descrip, + self.video_upload_time, + ) + + new_video_flag = True + + # If a 'This video is unavailable' error has been received, indicating + # a livestream waiting to start, it's in STDERR + if not new_video_flag: + + while not self.stderr_queue.empty(): + + stderr = self.stderr_queue.get_nowait().rstrip() + if stderr: + + # (Convert bytes to string) + stderr = stderr.decode() + if re.search('This video is unavailable', stderr): + + # Waiting livestream detected; create a new media.Video + # object + GObject.timeout_add( + 0, + app_obj.create_livestream_from_download, + self.container_obj, + 1, # Livestream waiting to start + self.video_name, + self.video_source, + self.video_descrip, + self.video_upload_time, + ) + + new_video_flag = True + + if new_video_flag: + + # Download the video's thumbnail, if possible + if self.video_thumb_source: + + # Get the thumbnail's extension... + remote_file, remote_ext = os.path.splitext( + self.video_thumb_source, + ) + + # ...and thus get the filename used by youtube-dl when storing + # the thumbnail locally (assuming that the video's name, and + # the filename when it is later downloaded, are the same) + local_thumb_path = os.path.abspath( + os.path.join( + self.container_obj.get_actual_dir(app_obj), + self.video_name + remote_ext, + ), + ) + + options_obj = self.download_worker_obj.options_manager_obj + if not options_obj.options_dict['sim_keep_thumbnail']: + local_thumb_path = utils.convert_path_to_temp( + app_obj, + local_thumb_path, + ) + + try: + request_obj = requests.get(self.video_thumb_source) + with open(local_thumb_path, 'wb') as outfile: + outfile.write(request_obj.content) + + except: + pass + + + def close(self): + + """Called by downloads.DownloadWorker.check_rss(). + + Destructor function for this object. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4275 close') + + # Tell the PipeReader objects to shut down, thus joining their threads + self.stdout_reader.join() + self.stderr_reader.join() + + + def create_child_process(self, cmd_list): + + """Called by self.do_fetch(). + + Based on YoutubeDLDownloader._create_process(). + + Executes the system command, creating a new child process which + executes youtube-dl. + + Args: + + cmd_list (list): Python list that contains the command to execute. + + Returns: + + True on success, False on an error + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4302 create_child_process') + + info = preexec = None + if os.name == 'nt': + # Hide the child process window that MS Windows helpfully creates + # for us + info = subprocess.STARTUPINFO() + info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + # Make this child process the process group leader, so that we can + # later kill the whole process group with os.killpg + preexec = os.setsid + + try: + self.child_process = subprocess.Popen( + cmd_list, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=preexec, + startupinfo=info, + ) + + return True + + except (ValueError, OSError) as error: + # (Errors are expected and frequent) + return False + + + def is_child_process_alive(self): + + """Called by self.do_fetch() and self.stop(). + + Based on YoutubeDLDownloader._proc_is_alive(). + + Called continuously during the self.do_fetch() loop to check whether + the child process has finished or not. + + Returns: + + True if the child process is alive, otherwise returns False. + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4347 is_child_process_alive') + + if self.child_process is None: + return False + + return self.child_process.poll() is None + + + def stop(self): + + """Called by DownloadWorker.close(). + + Terminates the child process. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4363 stop') + + if self.is_child_process_alive(): + + if os.name == 'nt': + # os.killpg is not available on MS Windows (see + # https://bugs.python.org/issue5115 ) + self.child_process.kill() + + # When we kill the child process on MS Windows the return code + # gets set to 1, so we want to reset the return code back to + # 0 + self.child_process.returncode = 0 + + else: + os.killpg(self.child_process.pid, signal.SIGKILL) + + +class LivestreamManager(threading.Thread): + + """Called by mainapp.TartubeApp.livestream_manager_start(). + + Python class to create a system child process, to check media.Video objects + already marked as livestreams, to see whether they have started or stopped + broadcasting. + + Reads from the child process STDOUT and STDERR, having set up a + downloads.PipeReader object to do so in an asynchronous way. + + Args: + + app_obj (mainapp.TartubeApp): The main application + + """ + + + # Standard class methods + + + def __init__(self, app_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4405 __init__') + + super(LivestreamManager, self).__init__() + + # IV list - class objects + # ----------------------- + # The mainapp.TartubeApp object + self.app_obj = app_obj + # The downloads.MiniJSONFetcher object used to check each media.Video + # object marked as a livestream + self.mini_fetcher_obj = None + + + # IV list - other + # --------------- + # A local list of media.Video objects marked as livestreams (in case + # the mainapp.TartubeApp IV changes during the course of this + # operation) + # Dictionary in the form: + # key = media data object's unique .dbid + # value = the media data object itself + self.video_dict = {} + # A subset of self.video_dict, containing only videos whose livestream + # status has changed from waiting to live + # Used by mainapp.TartubeApp.livestream_manager_finished() to update + # the Video Catalogue, create a desktop notification and/or open the + # livestream in the system's web browser + self.video_started_dict = {} + # A subset of self.video_dict, containing only videos whose livestream + # status has changed from live to finished + # Used by mainapp.TartubeApp.livestream_manager_stopped() to update + # the Video Catalogue + self.video_stopped_dict = {} + # A subset of self.video_dict, containing only videos which were + # currently broadcasting livestreams, but for which there is no + # JSON data (indicating that the video has been deleted, or is + # temporarily unavailable on the website) + # Used by mainapp.TartubeApp.livestream_manager_finished() to remove + # the video(s) from the Video Catalogue + self.video_missing_dict = {} + + # Flag set to False if self.stop_livestream_operation() is called + # The False value halts the loop in self.run() + self.running_flag = True + + # Code + # ---- + + # Let's get this party started! + self.start() + + + # Public class methods + + + def run(self): + + """Called as a result of self.__init__(). + + Initiates the download. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4468 run') + + # Generate a local list of media.Video objects marked as livestreams + # (in case the mainapp.TartubeApp IV changes during the course of + # this operation) + self.video_dict = self.app_obj.media_reg_live_dict.copy() + + for video_obj in self.video_dict.values(): + + if not self.running_flag: + break + + # For each media.Video in turn, try to fetch JSON data + # If the data is received, assume the livestream is live. If a + # 'This video is unavailable' error is received, the livestream + # is waiting to go live + self.mini_fetcher_obj = MiniJSONFetcher(self, video_obj) + + # Then execute the assigned job + self.mini_fetcher_obj.do_fetch() + + # Call the destructor function of the MiniJSONFetcher object + self.mini_fetcher_obj.close() + self.mini_fetcher_obj = None + + # Operation complete. If self.stop_livestream_operation() was called, + # then the mainapp.TartubeApp function has already been called + if self.running_flag: + self.running_flag = False + self.app_obj.livestream_manager_finished() + + + def mark_video_as_missing(self, video_obj): + + """Called by downloads.MiniJSONFetcher.do_fetch(). + + If a media.Video object marked as a livestream is missing in the + parent channel's/playlist's RSS feed, then add the video to an IV, so + that mainapp.TartubeApp.livestream_manager_finished() can take + appropriate action, when the livestream operation is finished. + + Args: + + video_obj (media.Video): The livestream video which was not found + in the RSS stream + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4517 mark_video_as_missing') + + self.video_missing_dict[video_obj.dbid] = video_obj + + + def mark_video_as_started(self, video_obj): + + """Called by downloads.MiniJSONFetcher.do_fetch(). + + If a media.Video object marked as a livestream has started + broadcasting, then add the video to an IV, so that + mainapp.TartubeApp.livestream_manager_finished() can take appropriate + action, when the livestream operation is finished. + + Args: + + video_obj (media.Video): The livestream video which has started + broadcasting + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4539 mark_video_as_started') + + self.video_started_dict[video_obj.dbid] = video_obj + + + def mark_video_as_stopped(self, video_obj): + + """Called by downloads.MiniJSONFetcher.do_fetch(). + + If a media.Video object marked as a livestream has stopped + broadcasting, then add the video to an IV, so that + mainapp.TartubeApp.livestream_manager_finished() can take appropriate + action, when the livestream operation is finished. + + Args: + + video_obj (media.Video): The livestream video which has stopped + broadcasting + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4561 mark_video_as_stopped') + + self.video_stopped_dict[video_obj.dbid] = video_obj + + + def stop_livestream_operation(self): + + """Can be called by anything. + + Based on downloads.DownloadManager.stop_downloads(). + + Stops the livestream operation. On the next iteration of self.run()'s + loop, the downloads.MiniJSONFetcher objects are cleaned up. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4577 stop_livestream_operation') + + self.running_flag = False + + # Halt the MiniJSONFetcher; it doesn't matter if it was in the middle + # of doing something + if self.mini_fetcher_obj: + self.mini_fetcher_obj.close() + self.mini_fetcher_obj = None + + # Call the mainapp.TartubeApp function to update everything (it's not + # called from self.run(), in this situation) + self.app_obj.livestream_manager_finished() + + +class MiniJSONFetcher(object): + + """Called by downloads.LivestreamManager.run(). + + A modified version of downlaods.JSONFetcher (the former is called by + downloads.DownloadWorker only; using a second Python class for the same + objective makes the code somewhat simpler). + + Python class to fetch JSON data for a livestream video, using youtube-dl. + + Creates a system child process and uses the child process to instruct + youtube-dl to fetch the JSON data for the video. + + Reads from the child process STDOUT and STDERR, having set up a + downloads.PipeReader object to do so in an asynchronous way. + + Args: + + livestream_manager_obj (downloads.LivestreamManager): The livestream + manager object handling the entire livestream operation + + video_obj (media.Video): The livestream video whose JSON data should be + fetched (the equivalent of right-clicking the video in the Video + Catalogue, and selecting 'Check this video') + + """ + + + # Standard class methods + + + def __init__(self, livestream_manager_obj, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4626 __init__') + + # IV list - class objects + # ----------------------- + # The downloads.LivestreamManager object handling the entire livestream + # operation + self.livestream_manager_obj = livestream_manager_obj + # The media.Video object for which new JSON data must be fetched + # (the equivalent of right-clicking the video in the Video Catalogue, + # and selecting 'Check this video') + self.video_obj = video_obj + + # This object reads from the child process STDOUT and STDERR in an + # asynchronous way + # Standard Python synchronised queue classes + self.stdout_queue = queue.Queue() + self.stderr_queue = queue.Queue() + # The downloads.PipeReader objects created to handle reading from the + # pipes + self.stdout_reader = PipeReader(self.stdout_queue) + self.stderr_reader = PipeReader(self.stderr_queue) + + # The child process created by self.create_child_process() + self.child_process = None + + + # IV list - other + # --------------- + # The time (in seconds) between iterations of the loop in + # self.do_fetch() + self.sleep_time = 0.1 + + + # Public class methods + + + def do_fetch(self): + + """Called by downloads.LivestreamManager.run(). + + Downloads JSON data for the livestream video, self.video_obj. + + If the data can be downloaded, we assume that the livestream is + currently broadcasting. If we get a 'This video is unavailable' error, + we assume that the livestream is waiting to start. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4674 do_fetch') + + # Import the main app (for convenience) + app_obj = self.livestream_manager_obj.app_obj + + # Convert a youtube-dl path beginning with ~ (not on MS Windows) + # (code copied from utils.generate_system_cmd() ) + ytdl_path = app_obj.ytdl_path + if os.name != 'nt': + ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path) + + # Generate the system command... + cmd_list = [ytdl_path] + ['--dump-json'] + [self.video_obj.source] + # ...and create a new child process using that command + self.create_child_process(cmd_list) + + # So that we can read from the child process STDOUT and STDERR, attach + # a file descriptor to the PipeReader objects + if self.child_process is not None: + + self.stdout_reader.attach_file_descriptor( + self.child_process.stdout, + ) + + self.stderr_reader.attach_file_descriptor( + self.child_process.stderr, + ) + + # Wait for the process to finish + while self.is_child_process_alive(): + + # Pause a moment between each iteration of the loop (we don't want + # to hog system resources) + time.sleep(self.sleep_time) + + # Process has finished. Check for JSON data, indicating that it's a + # 'live' livestream + while not self.stdout_queue.empty(): + + stdout = self.stdout_queue.get_nowait().rstrip() + if stdout: + + # (Convert bytes to string) + stdout = stdout.decode() + if stdout[:1] == '{': + + # Broadcasting livestream detected + json_dict = self.parse_json(stdout) + if self.video_obj.live_mode == 1: + + # Waiting livestream has gone live + GObject.timeout_add( + 0, + app_obj.mark_video_live, + self.video_obj, + 2, # Livestream is broadcasting + True, # Don't update Video Index yet + True, # Don't update Video Catalogue yet + ) + # (Mark this video as modified, so that + # mainapp.TartubeApp can update the Video Catalogue + # once the livestream operation has finished) + self.livestream_manager_obj.mark_video_as_started( + self.video_obj, + ) + + elif self.video_obj.live_mode == 2 \ + and not json_dict['is_live']: + + # Broadcasting livestream has finished + GObject.timeout_add( + 0, + app_obj.mark_video_live, + self.video_obj, + 0, # Not a livestream + True, # Don't update Video Index yet + True, # Don't update Video Catalogue yet + ) + self.livestream_manager_obj.mark_video_as_stopped( + self.video_obj, + ) + + # The video's name and description might change during the + # livestream; update them, if so + if 'title' in json_dict: + self.video_obj.set_nickname(json_dict['title']) + + if 'description' in json_dict: + self.video_obj.set_video_descrip( + json_dict['description'], + app_obj.main_win_obj.descrip_line_max_len, + ) + + # Check for errors, indicating that it's a 'waiting' livestream + while not self.stderr_queue.empty(): + + stderr = self.stderr_queue.get_nowait().rstrip() + if stderr: + + # (Convert bytes to string) + stderr = stderr.decode() + if re.search('This video is unavailable', stderr) \ + and self.video_obj.live_mode == 2: + + # The livestream broadcast has been deleted by its owner + # (or is not available on the website, possibly + # temporarily) + self.livestream_manager_obj.mark_video_as_missing( + self.video_obj, + ) + + + def close(self): + + """Called by downloads.LivestreamManager.run(). + + Destructor function for this object. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4794 close') + + # Tell the PipeReader objects to shut down, thus joining their threads + self.stdout_reader.join() + self.stderr_reader.join() + + + def create_child_process(self, cmd_list): + + """Called by self.do_fetch(). + + Based on YoutubeDLDownloader._create_process(). + + Executes the system command, creating a new child process which + executes youtube-dl. + + Args: + + cmd_list (list): Python list that contains the command to execute. + + Returns: + + True on success, False on an error + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4821 create_child_process') + + info = preexec = None + if os.name == 'nt': + # Hide the child process window that MS Windows helpfully creates + # for us + info = subprocess.STARTUPINFO() + info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + # Make this child process the process group leader, so that we can + # later kill the whole process group with os.killpg + preexec = os.setsid + + try: + self.child_process = subprocess.Popen( + cmd_list, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=preexec, + startupinfo=info, + ) + + return True + + except (ValueError, OSError) as error: + # (Errors are expected and frequent) + return False + + + def is_child_process_alive(self): + + """Called by self.do_fetch() and self.stop(). + + Based on YoutubeDLDownloader._proc_is_alive(). + + Called continuously during the self.do_fetch() loop to check whether + the child process has finished or not. + + Returns: + + True if the child process is alive, otherwise returns False. + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4866 is_child_process_alive') + + if self.child_process is None: + return False + + return self.child_process.poll() is None + + + def parse_json(self, stdout): + + """Called by self.do_fetch(). + + Code copied from downloads.VideoDownloader.extract_stdout_data(). + + Converts the receivd JSON data into a dictionary, and returns the + dictionary. + + Args: + + stdout (str): A string of JSON data as it was received from + youtube-dl (and starting with the character { ) + + Return values: + + The JSON data, converted into a Python dictionary + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4895 parse_json') + + # (Try/except to check for invalid JSON) + try: + return json.loads(stdout) + + except: + GObject.timeout_add( + 0, + app_obj.system_error, + 305, + 'Invalid JSON data received from server', + ) + + return {} + + + def stop(self): + + """Called by DownloadWorker.close(). + + Terminates the child process. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('dld 4920 stop') + + if self.is_child_process_alive(): + + if os.name == 'nt': + # os.killpg is not available on MS Windows (see + # https://bugs.python.org/issue5115 ) + self.child_process.kill() + + # When we kill the child process on MS Windows the return code + # gets set to 1, so we want to reset the return code back to + # 0 + self.child_process.returncode = 0 + + else: + os.killpg(self.child_process.pid, signal.SIGKILL) + + class PipeReader(threading.Thread): """Called by downloads.VideoDownloader.__init__(). @@ -3524,7 +4966,7 @@ class PipeReader(threading.Thread): def __init__(self, queue): if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3524 __init__') + utils.debug_time('dld 4969 __init__') super(PipeReader, self).__init__() @@ -3561,7 +5003,7 @@ def run(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3561 run') + utils.debug_time('dld 5006 run') # Use this flag so that the loop can ignore FFmpeg error messsages # (because the parent VideoDownloader object shouldn't use that as a @@ -3591,7 +5033,8 @@ def run(self): def attach_file_descriptor(self, filedesc): - """Called by downloads.VideoDownloader.do_download(). + """Called by downloads.VideoDownloader.do_download and comparable + functions. Sets the file descriptor for the child process STDOUT or STDERR. @@ -3602,7 +5045,7 @@ def attach_file_descriptor(self, filedesc): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3602 attach_file_descriptor') + utils.debug_time('dld 5048 attach_file_descriptor') self.file_descriptor = filedesc @@ -3621,7 +5064,7 @@ def join(self, timeout=None): """ if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3621 join') + utils.debug_time('dld 5067 join') self.running_flag = False super(PipeReader, self).join(timeout) diff --git a/tartube/formats.py b/tartube/formats.py index 52651ab1..bed16887 100755 --- a/tartube/formats.py +++ b/tartube/formats.py @@ -26,11 +26,30 @@ # Import other modules import datetime +import re # Import our modules -# ... +# Use same gettext translations +from mainapp import _ + + +# Supported locales: _ +locale_setup_list = [ + 'en_GB', 'English', + 'en_US', 'English (American)', +] +LOCALE_DEFAULT = locale_setup_list[0] +LOCALE_LIST = [] +LOCALE_DICT = {} + +while locale_setup_list: + key = locale_setup_list.pop(0) + value = locale_setup_list.pop(0) + + LOCALE_LIST.append(key) + LOCALE_DICT[key] = value # Some icons are different at Christmas today = datetime.date.today() @@ -42,47 +61,28 @@ else: xmas_flag = False -# Main stages of the download operation -MAIN_STAGE_QUEUED = 'Queued' -MAIN_STAGE_ACTIVE = 'Active' -MAIN_STAGE_PAUSED = 'Paused' # (not actually used) -MAIN_STAGE_COMPLETED = 'Completed' # (not actually used) -MAIN_STAGE_ERROR = 'Error' -# Sub-stages of the 'Active' stage -ACTIVE_STAGE_PRE_PROCESS = 'Pre-processing' -ACTIVE_STAGE_DOWNLOAD = 'Downloading' -ACTIVE_STAGE_POST_PROCESS = 'Post-processing' -ACTIVE_STAGE_CHECKING = 'Checking' -# Sub-stages of the 'Completed' stage -COMPLETED_STAGE_FINISHED = 'Finished' -COMPLETED_STAGE_WARNING = 'Warning' -COMPLETED_STAGE_ALREADY = 'Already downloaded' -# Sub-stages of the 'Error' stage -ERROR_STAGE_ERROR = 'Error' # (not actually used) -ERROR_STAGE_STOPPED = 'Stopped' -ERROR_STAGE_ABORT = 'Filesize abort' - - -# Standard dictionaries - +# Standard list and dictionaries time_metric_setup_list = [ - 'seconds', 1, - 'minutes', 60, - 'hours', int(60 * 60), - 'days', int(60 * 60 * 24), - 'weeks', int(60 * 60 * 24 * 7), - 'years', int(60 * 60 * 24 * 365), + 'seconds', _('seconds'), 1, + 'minutes', _('minutes'), 60, + 'hours', _('hours'), int(60 * 60), + 'days', _('days'), int(60 * 60 * 24), + 'weeks', _('weeks'), int(60 * 60 * 24 * 7), + 'years', _('years'), int(60 * 60 * 24 * 365), ] TIME_METRIC_LIST = [] TIME_METRIC_DICT = {} +TIME_METRIC_TRANS_DICT = {} while time_metric_setup_list: key = time_metric_setup_list.pop(0) + trans_key = time_metric_setup_list.pop(0) value = time_metric_setup_list.pop(0) TIME_METRIC_LIST.append(key) TIME_METRIC_DICT[key] = value + TIME_METRIC_TRANS_DICT[key] = trans_key KILO_SIZE = 1024.0 filesize_metric_setup_list = [ @@ -353,8 +353,8 @@ ['Yotta', 'y'], ] -# ISO 639-1 Language Codes language_setup_list = [ + # ISO 639-1 Language Codes # English is top of the list, because it's the default setting in # options.OptionsManager 'English', 'en', @@ -676,11 +676,17 @@ 'folder_red_small': 'folder_red.png', 'have_file_small': 'have_file.png', 'no_file_small': 'no_file.png', + 'stream_live_small': 'stream_live.png', + 'stream_wait_small': 'stream_wait.png', 'system_error_small': 'system_error.png', 'system_warning_small': 'system_warning.png', 'warning_small': 'warning.png', } +EXTERNAL_ICON_DICT = { + 'ytdl-gui': 'youtube-dl-gui.png', +} + if not xmas_flag: WIN_ICON_LIST = [ 'system_icon_16.png', @@ -703,3 +709,139 @@ 'system_icon_xmas_256.png', 'system_icon_xmas_512.png', ] + + +def do_translate(config_flag=False): + + """Function called for the first time below, setting various values. + + If mainapp.TartubeApp.load_config() changes the locale to something else, + called for a second time to update those values. + + Args: + + config_flag (bool): False for the initial call, True for the second + call from mainapp.TartubeApp.load_config() + + """ + + global FOLDER_ALL_VIDEOS, FOLDER_BOOKMARKS, FOLDER_FAVOURITE_VIDEOS, \ + FOLDER_LIVESTREAMS, FOLDER_NEW_VIDEOS, FOLDER_WAITING_VIDEOS, \ + FOLDER_TEMPORARY_VIDEOS, FOLDER_UNSORTED_VIDEOS + + global YTDL_UPDATE_DICT + + global MAIN_STAGE_QUEUED, MAIN_STAGE_ACTIVE, MAIN_STAGE_PAUSED, \ + MAIN_STAGE_COMPLETED, MAIN_STAGE_ERROR, ACTIVE_STAGE_PRE_PROCESS, \ + ACTIVE_STAGE_DOWNLOAD, ACTIVE_STAGE_POST_PROCESS, ACTIVE_STAGE_CHECKING, \ + COMPLETED_STAGE_FINISHED, COMPLETED_STAGE_WARNING, \ + COMPLETED_STAGE_ALREADY, ERROR_STAGE_ERROR, ERROR_STAGE_STOPPED, \ + ERROR_STAGE_ABORT + + global TIME_METRIC_TRANS_DICT + + global FILE_OUTPUT_NAME_DICT, FILE_OUTPUT_CONVERT_DICT + + global VIDEO_OPTION_LIST, VIDEO_OPTION_DICT + + # System folder names + FOLDER_ALL_VIDEOS = _('All Videos') + FOLDER_BOOKMARKS = _('Bookmarks') + FOLDER_FAVOURITE_VIDEOS = _('Favourite Videos') + FOLDER_LIVESTREAMS = _('Livestreams') + FOLDER_NEW_VIDEOS = _('New Videos') + FOLDER_WAITING_VIDEOS = _('Waiting Videos') + FOLDER_TEMPORARY_VIDEOS = _('Temporary Videos') + FOLDER_UNSORTED_VIDEOS = _('Unsorted Videos') + + # youtube-dl update shell commands + YTDL_UPDATE_DICT = { + 'ytdl_update_default_path': + _('Update using default youtube-dl path'), + 'ytdl_update_local_path': + _('Update using local youtube-dl path'), + 'ytdl_update_pip': + _('Update using pip'), + 'ytdl_update_pip_omit_user': + _('Update using pip (omit --user option)'), + 'ytdl_update_pip3': + _('Update using pip3'), + 'ytdl_update_pip3_omit_user': + _('Update using pip3 (omit --user option)'), + 'ytdl_update_pip3_recommend': + _('Update using pip3 (recommended)'), + 'ytdl_update_pypi_path': + _('Update using PyPI youtube-dl path'), + 'ytdl_update_win_32': + _('Windows 32-bit update (recommended)'), + 'ytdl_update_win_64': + _('Windows 64-bit update (recommended)'), + 'ytdl_update_disabled': + _('youtube-dl updates are disabled'), + } + + # Download operation stages + MAIN_STAGE_QUEUED = _('Queued') + MAIN_STAGE_ACTIVE = _('Active') + MAIN_STAGE_PAUSED = _('Paused') # (not actually used) + MAIN_STAGE_COMPLETED = _('Completed') # (not actually used) + MAIN_STAGE_ERROR = _('Error') + # Sub-stages of the 'Active' stage + ACTIVE_STAGE_PRE_PROCESS = _('Pre-processing') + ACTIVE_STAGE_DOWNLOAD = _('Downloading') + ACTIVE_STAGE_POST_PROCESS = _('Post-processing') + ACTIVE_STAGE_CHECKING = _('Checking') + # Sub-stages of the 'Completed' stage + COMPLETED_STAGE_FINISHED = _('Finished') + COMPLETED_STAGE_WARNING = _('Warning') + COMPLETED_STAGE_ALREADY = _('Already downloaded') + # Sub-stages of the 'Error' stage + ERROR_STAGE_ERROR = _('Error') # (not actually used) + ERROR_STAGE_STOPPED = _('Stopped') + ERROR_STAGE_ABORT = _('Filesize abort') + + if config_flag: + + for key in TIME_METRIC_TRANS_DICT: + TIME_METRIC_TRANS_DICT[key] = _(key) + + # File output templates use a combination of English words, each of + # which must be translated + translate_note = _( + 'TRANSLATOR\'S NOTE: ID refers to a video\'s unique ID on the' \ + + ' website, e.g. on YouTube "CS9OO0S5w2k"', + ) + + new_name_dict = {} + for key in FILE_OUTPUT_NAME_DICT.keys(): + + mod_value \ + = re.sub('Custom', _('Custom'), FILE_OUTPUT_NAME_DICT[key]) + mod_value = re.sub('ID', _('ID'), mod_value) + mod_value = re.sub('Title', _('Title'), mod_value) + mod_value = re.sub('Quality', _('Quality'), mod_value) + mod_value = re.sub('Autonumber', _('Autonumber'), mod_value) + + new_name_dict[key] = mod_value + + FILE_OUTPUT_NAME_DICT = new_name_dict + + # Video/audio formats. A number of them contain 'Any format', which + # must be translated + new_list = [] + new_dict = {} + for item in VIDEO_OPTION_LIST: + + mod_item = re.sub('Any format', _('Any format'), item) + new_list.append(mod_item) + new_dict[mod_item] = VIDEO_OPTION_DICT[item] + + VIDEO_OPTION_LIST = new_list + VIDEO_OPTION_DICT = new_dict + + # End of this function + return + + +# Call the function for the first time +do_translate() diff --git a/tartube/info.py b/tartube/info.py index 2548f263..df9862ac 100755 --- a/tartube/info.py +++ b/tartube/info.py @@ -38,6 +38,8 @@ # Import our modules import downloads import utils +# Use same gettext translations +from mainapp import _ # Debugging flag (calls utils.debug_time at the start of every function) @@ -97,7 +99,7 @@ def __init__(self, app_obj, info_type, media_data_obj, url_string, options_string): if DEBUG_FUNC_FLAG: - utils.debug_time('iop 100 __init__') + utils.debug_time('iop 102 __init__') super(InfoManager, self).__init__() @@ -175,20 +177,31 @@ def run(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('iop 178 run') + utils.debug_time('iop 180 run') # Show information about the info operation in the Output Tab - msg = 'Starting info operation, ' if self.info_type == 'test_ytdl': - msg += 'testing youtube-dl with specified options' + + msg = _( + 'Starting info operation, testing youtube-dl with specified' \ + + ' options', + ) else: + if self.info_type == 'formats': - msg += 'fetching list of video/audio formats' + + msg = _( + 'Starting info operation, fetching list of video/audio'\ + + ' formats for \'{0}\'', + ).format(self.video_obj.name) + else: - msg += 'fetching list of subtitles' - msg += ' for \'' + self.video_obj.name + '\'' + msg = _( + 'Starting info operation, fetching list of subtitles'\ + + ' for \'{0}\'', + ).format(self.video_obj.name) self.app_obj.main_win_obj.output_tab_write_stdout(1, msg) @@ -327,7 +340,7 @@ def run(self): # situations) if self.child_process is None: - msg = 'youtube-dl process did not start' + msg = _('youtube-dl process did not start') self.stderr_list.append(msg) self.app_obj.main_win_obj.output_tab_write_stdout( 1, @@ -336,7 +349,7 @@ def run(self): elif self.child_process.returncode > 0: - msg = 'Child process exited with non-zero code: {}'.format( + msg = _('Child process exited with non-zero code: {}').format( self.child_process.returncode, ) self.app_obj.main_win_obj.output_tab_write_stdout( @@ -352,7 +365,7 @@ def run(self): # Show a confirmation in the the Output Tab self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Info operation finished', + _('Info operation finished'), ) # Let the timer run for a few more seconds to prevent Gtk errors (for @@ -379,7 +392,7 @@ def create_child_process(self, cmd_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('iop 382 create_child_process') + utils.debug_time('iop 395 create_child_process') info = preexec = None @@ -405,7 +418,7 @@ def create_child_process(self, cmd_list): except (ValueError, OSError) as error: # (The code in self.run() will spot that the child process did not # start) - self.stderr_list.append('Child process did not start') + self.stderr_list.append(_('Child process did not start')) def is_child_process_alive(self): @@ -424,7 +437,7 @@ def is_child_process_alive(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('iop 427 is_child_process_alive') + utils.debug_time('iop 440 is_child_process_alive') if self.child_process is None: return False @@ -443,7 +456,7 @@ def stop_info_operation(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('iop 446 stop_info_operation') + utils.debug_time('iop 459 stop_info_operation') if self.is_child_process_alive(): diff --git a/tartube/mainapp.py b/tartube/mainapp.py index fee1ebc1..3f6750ae 100755 --- a/tartube/mainapp.py +++ b/tartube/mainapp.py @@ -39,14 +39,29 @@ import threading import time +import gettext +_ = gettext.gettext + # Import other Python modules +try: + import feedparser + HAVE_FEEDPARSER_FLAG = True +except: + HAVE_FEEDPARSER_FLAG = False + try: import moviepy.editor HAVE_MOVIEPY_FLAG = True except: HAVE_MOVIEPY_FLAG = False +try: + import playsound + HAVE_PLAYSOUND_FLAG = True +except: + HAVE_PLAYSOUND_FLAG = False + if os.name != 'nt': try: from xdg_tartube import XDG_CONFIG_HOME @@ -95,7 +110,7 @@ class TartubeApp(Gtk.Application): def __init__(self, *args, **kwargs): if DEBUG_FUNC_FLAG: - utils.debug_time('ap 98 __init__') + utils.debug_time('ap 113 __init__') super(TartubeApp, self).__init__( *args, @@ -118,9 +133,6 @@ def __init__(self, *args, **kwargs): # 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 - # Show an dialogue window with 'Tartube is already running!' if the - # user tries to open a second instance of Tartube - self.debug_warn_multiple_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 @@ -136,6 +148,11 @@ def __init__(self, *args, **kwargs): # ------------------------------------------- # The main window object, set as soon as it's created self.main_win_obj = None + # A fake main window object, temporarily created before the actual main + # window by self.start() (in certain situations) + # 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 # Gtk.StatusIcon) self.status_icon_obj = None @@ -177,9 +194,16 @@ def __init__(self, *args, **kwargs): # The current tidy.TidyManager object, if a tidy operation is in # progress (or None, if not) self.tidy_manager_obj = None + # A livestream operation is handled by a downloads.LivestreamManager + # object. It checks media.Video objects marked as livestreams, to + # see whether have started or stopped broadcasting + self.livestream_manager_obj = None # - # When any operation is in progress, the manager object is stored here + # 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) + # Livestream operations run silently in the background, and no + # functionality is disabled. Therefore, this IV remains set to None + # when the livestream operation is running self.current_manager_obj = None # # The file manager, files.FileManager, for loading thumbnail, icon @@ -216,6 +240,9 @@ def __init__(self, *args, **kwargs): # Instance variable (IV) list - other # ----------------------------------- + # Custom locale (matches one of the values in formats.LOCALE_LIST) + self.custom_locale = 'en_GB' + # Default window sizes (in pixels) self.main_win_width = 800 self.main_win_height = 600 @@ -348,7 +375,7 @@ def __init__(self, *args, **kwargs): os.path.join(self.script_dir, os.pardir), ) - # Tartube's data directory (platform-dependant), i.e. 'tartube-data' + # Tartube's data directory (platform-dependent), i.e. 'tartube-data' # Note that, using the MSWin installer, Cygwin gives file paths with # both / and \ separators. Throughout the code, we use # os.path.abspath to circumvent this problem @@ -460,6 +487,15 @@ def __init__(self, *args, **kwargs): ), ) + # The directory in which sound files are found, set in the call to + # self.find_sound_effects() + self.sound_dir = None + # List of sound files found in the ../sounds directory (e.g. + # 'beep.mp3') + self.sound_list = [] + # The user's preferred sound effect (for livestream alarms) + self.sound_custom = 'bell.mp3' + # Name of the Tartube config file self.config_file_name = 'settings.json' # The config file can be stored at one of two locations, depending on @@ -529,7 +565,25 @@ def __init__(self, *args, **kwargs): # loaded a database file, or wants to call .save_db to create one self.allow_db_save_flag = False - # The youtube-dl binary to use (platform-dependant) - 'youtube-dl' or + # Flag set to True if the Classic Mode Tab should be the visible one, + # when Tartube first starts (for the benefit of users who only want + # Classic Mode downloads) + self.show_classic_tab_on_startup_flag = False + # Users can add more destination directories to the combobox in the + # Classic Mode Tab. Tartube remembers those directories, up to the + # maximum number specified below + self.classic_dir_list = [ os.path.expanduser('~') ] + # The maximum size of the list. When a new directory is added by the + # user, it's moved to the top of the list. If the list is now too + # big, the last item is removed + self.classic_dir_max = 8 + # The most recently-selected destination directory. On startup, if this + # directory still exists in self.classic_dir_list, it is moved to the + # top (and so it appears as the first item in the combobox). This IV + # is then reset + self.classic_dir_previous = None + + # The youtube-dl binary to use (platform-dependent) - 'youtube-dl' or # 'youtube-dl.exe', depending on the platform. The default value is # set by self.start() self.ytdl_bin = None @@ -549,10 +603,35 @@ def __init__(self, *args, **kwargs): # 'youtube-dl' self.ytdl_path = None # The shell command to use during an update operation depends on how - # youtube-dl was installed. A dictionary containing some - # possibilities, populated by self.start() + # youtube-dl was installed + # Depending on the operatin system, Tartube provides some of these + # methods (listed here with the description visible to the user): + # + # 'ytdl_update_default_path' + # Update using default youtube-dl path + # 'ytdl_update_local_path' + # Update using local youtube-dl path + # 'ytdl_update_pip' + # Update using pip + # 'ytdl_update_pip_omit_user' + # Update using pip (omit --user option) + # 'ytdl_update_pip3' + # Update using pip3 + # 'ytdl_update_pip3_omit_user' + # Update using pip3 (omit --user option) + # 'ytdl_update_pip3_recommend' + # Update using pip3 (recommended) + # 'ytdl_update_pypi_path' + # Update using PyPI youtube-dl path + # 'ytdl_update_win_32', + # Windows 32-bit update (recommended) + # 'ytdl_update_win_64', + # Windows 64-bit update (recommended) + # 'ytdl_update_disabled' + # youtube-dl updates are disabled + # A dictionary containing some possibilities, populated by self.start() # Dictionary in the form - # key: description of the update method + # key: method name (one of those listed above) # value: list of words to use in the shell command self.ytdl_update_dict = {} # A list of keys from self.ytdl_update_dict in a standard order (so the @@ -782,8 +861,9 @@ def __init__(self, *args, **kwargs): # at which to stop waiting self.tidy_timer_check_time = None - # During any operation, a flag set to True if the operation was halted - # by the user, rather than being allowed to complete naturally + # 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 self.operation_halted_flag = False # During a download operation, a flag set to True if Tartube must shut # down when the operation is finished @@ -890,6 +970,31 @@ def __init__(self, *args, **kwargs): __main__.__packagename__, ] + # A subset of self.media_reg_dict, containing only media.Videos which + # are marked as livestreams (and which must therefore be checked by + # livestream operations) + self.media_reg_live_dict = {} + # A subset of self.media_reg_live_dict, containing only media.Videos + # which are waiting live streams. When the livestream goes live, a + # desktop notification is shown for them + self.media_reg_auto_notify_dict = {} + # A subset of self.media_reg_live_dict, containing only media.Videos + # which are waiting live streams. When the livestream goes live, an + # alarm is sounded for them + self.media_reg_auto_alarm_dict = {} + # A subset of self.media_reg_live_dict, containing only media.Videos + # which are waiting live streams. When the livestream goes live, the + # video is opened in the system's web browser + self.media_reg_auto_open_dict = {} + # A subset of self.media_reg_live_dict, containing only media.Videos + # which should be downloaded, as soon as they start (as soon as this + # is processed, the entry is removed from the dictionary) + self.media_reg_auto_dl_start_dict = {} + # A subset of self.media_reg_live_dict, containing only media.Videos + # which should be downloaded, as soon as they stop (as soon as this + # is processed, the entry is removed from the dictionary) + self.media_reg_auto_dl_stop_dict = {} + # Some media data objects are fixed (i.e. are created when Tartube # first starts, and cannot be deleted by the user). Shortcuts to # those objects @@ -900,6 +1005,9 @@ def __init__(self, *args, **kwargs): self.fixed_bookmark_folder = None # Private folder containing only favourite videos self.fixed_fav_folder = None + # Private folder containing only videos marked as (waiting or + # broadcasting) livestreams + self.fixed_live_folder = None # Private folder containing only new videos self.fixed_new_folder = None # Private folder containing only playlist videos (when the user @@ -913,6 +1021,10 @@ def __init__(self, *args, **kwargs): # Public folder that's used as the first one in the 'Add video' # dialogue window, in which the user can store any individual videos self.fixed_misc_folder = None + # The locale for which the fixed folders are named. When the database + # file is loaded, if this value no longer matches self.custom_locale, + # then the folder names are all updated for the new locale + self.fixed_folder_locale = self.custom_locale # A list of media.Video objects the user wants to watch, as soon as # they have been downloaded. Videos are added by a call to @@ -930,6 +1042,13 @@ def __init__(self, *args, **kwargs): # 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 @@ -941,12 +1060,79 @@ def __init__(self, *args, **kwargs): # The time (system time, in seconds) at which the last 'Check all' # operation started (regardless of whether it was scheduled or not) self.scheduled_check_last_time = 0 + # If self.scheduled_check_mode is 'start', on startup we wait a few + # seconds (for aesthetic reasons). The number of seconds to wait + self.scheduled_check_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_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') self.scheduled_shutdown_flag = False + # Flag set to True if Tartube should try to detect livestreams (on + # compatible websites only) + # This feature is only tested on YouTube. It might work on other + # websites, if the user has set the RSS feed for each channel/ + # playlist individually + # If enabled, the download operation checks a channel/playlist RSS for + # videos that weren't picked up by ytdl, and marks them as + # livestreams. If JSON data can't be downloaded from it, assume it's + # an upcoming livestream; otherwise assume the livestream is live + self.enable_livestreams_flag = True + # If enabled, Tartube will assume that the website lists videos in + # order of announcement time, and will stop checking the RSS feed + # when it finds videos which are at least this old (in days). If set + # to zero, Tartube stops checking the RSS feed when it finds the + # first non-livestream video + self.livestream_max_days = 7 + # Flag set to True if livestream videos in the Video Catalogue should + # be drawn with a coloured background, False if not + self.livestream_use_colour_flag = True + # Flag set to True if a desktop notification should be shown when a + # waiting livestream goes live (the setting can then be enabled/ + # disabled for each video individually in the Video Catalogue) + self.livestream_auto_notify_flag = False + # Flag set to True if a Tartube should play an alarm when a waiting + # livestream goes live (the setting can then be enabled/disabled for + # each video individually in the Video Catalogue) + self.livestream_auto_alarm_flag = False + # Flag set to True if a video should be opened in the system's web + # browser when it goes live (the setting can then be enabled/ + # disabled for each video individually in the Video Catalogue) + self.livestream_auto_open_flag = False + # Flag set to True if a video should be downloaded as soon as the + # livestream starts (media.Video.live_mode was 0/1, set to 2; the + # setting can then be enabled/disabled for each video individually in + # the Video Catalogue) + # The start of the download may be delayed if a download operation is + # already in progress + self.livestream_auto_dl_start_flag = False + # Flag set to True if a video should be downloaded as soon as the + # livestream stops (media.Video.live_mode was 2, set to 0; the + # setting can then be enabled/disabled for each video individually in + # the Video Catalogue) + # The start of the download may be delayed if a download operation is + # already in progress + # If both this flag and self.livestream_auto_dl_start_flag are set to + # True, then youtube-dl is instructed to overwrite the earlier file + # (NB As of April 2020, this is still not possible; as a temporary + # measure, the earlier file is renamed instead) + self.livestream_auto_dl_stop_flag = False + # The livestream operation can run periodically and checks the + # status of videos marked as livestreams + # Flag set to True if the livestream task should run periodically + self.scheduled_livestream_flag = True + # The time (in minutes) between scheduled livestream operations, if + # enabled (cannot be fractional, minimum value 1) + self.scheduled_livestream_wait_mins = 3 + # The time (system time, in seconds) at which the last livestream + # operation started + self.scheduled_livestream_last_time = 0 + # Flag set to True if a download operation should auto-stop after a # certain period of time (applies to both real and simulated # downloads) @@ -1044,10 +1230,12 @@ def __init__(self, *args, **kwargs): # a ytdl_archive.txt, recording every video ever downloaded in the # parent directory # This will prevent a successful re-downloading of the video. In - # response, the archive file is temporarily renamed, and the details - # are stored in these IVs - self.ytdl_archive_path = None - self.ytdl_archive_backup_path = None + # response, the archive file is temporarily renamed (in a call to + # self.set_backup_archive() ). The details are stored in these IVs, + # so the original file names can be restored at the end of the + # download operation (in a call to self.reset_backup_archive() ) + self.ytdl_archive_path_list = [] + self.ytdl_archive_backup_path_list = [] # Flag set to True if, when checking videos/channels/playlists, we # should timeout after 60 seconds (in case youtube-dl gets stuck # downloading the JSON data) @@ -1128,9 +1316,8 @@ def __init__(self, *args, **kwargs): # (e.g. '720p') self.video_res_default = '720p' # Flag set to True when this maximum video resolution is applied. When - # applied, it overrides the download options 'video_format', - # 'second_video_format' and 'third_video_format' (see the comments - # in options.OptionsManager) + # applied, it overrides the download option 'video_format_list' (see + # the comments in options.OptionsManager) self.video_res_apply_flag = False # The method of matching downloaded videos against existing @@ -1217,7 +1404,7 @@ def do_startup(self): """Gio.Application standard function.""" if DEBUG_FUNC_FLAG: - utils.debug_time('app 1215 do_startup') + utils.debug_time('app 1407 do_startup') GObject.threads_init() Gtk.Application.do_startup(self) @@ -1382,6 +1569,31 @@ def do_startup(self): ) self.add_action(stop_operation_menu_action) + # 'Livestreams' column + live_prefs_menu_action = Gio.SimpleAction.new( + 'live_prefs_menu', + None, + ) + live_prefs_menu_action.connect( + 'activate', + self.on_menu_live_preferences, + ) + self.add_action(live_prefs_menu_action) + + update_live_menu_action = Gio.SimpleAction.new( + 'update_live_menu', + None, + ) + update_live_menu_action.connect('activate', self.on_menu_update_live) + self.add_action(update_live_menu_action) + + cancel_live_menu_action = Gio.SimpleAction.new( + 'cancel_live_menu', + None, + ) + cancel_live_menu_action.connect('activate', self.on_menu_cancel_live) + self.add_action(cancel_live_menu_action) + # 'Help' column about_menu_action = Gio.SimpleAction.new('about_menu', None) about_menu_action.connect('activate', self.on_menu_about) @@ -1391,6 +1603,16 @@ def do_startup(self): go_website_menu_action.connect('activate', self.on_menu_go_website) self.add_action(go_website_menu_action) + send_feedback_menu_action = Gio.SimpleAction.new( + 'send_feedback_menu', + None, + ) + send_feedback_menu_action.connect( + 'activate', + self.on_menu_send_feedback, + ) + self.add_action(send_feedback_menu_action) + # Main toolbar actions # -------------------- @@ -1630,38 +1852,149 @@ def do_startup(self): ) self.add_action(download_all_button_action) + # Classic Mode Tab actions + # ------------------------ - def do_activate(self): + # Buttons - """Gio.Application standard function.""" + classic_options_button_action = Gio.SimpleAction.new( + 'classic_options_button', + None, + ) + classic_options_button_action.connect( + 'activate', + self.on_menu_general_options, + ) + self.add_action(classic_options_button_action) - if DEBUG_FUNC_FLAG: - utils.debug_time('app 1634 do_activate') + classic_update_ytdl_button_action = Gio.SimpleAction.new( + 'classic_update_ytdl_button', + None, + ) + classic_update_ytdl_button_action.connect( + 'activate', + self.on_menu_update_ytdl, + ) + self.add_action(classic_update_ytdl_button_action) - # Only allow a single main window (raise any existing main windows) - if not self.main_win_obj: - self.start() + classic_auto_copy_button_action = Gio.SimpleAction.new( + 'classic_auto_copy_button', + None, + ) + classic_auto_copy_button_action.connect( + 'activate', + self.on_button_classic_auto_copy, + ) + self.add_action(classic_auto_copy_button_action) - # Open the system preferences window, if the debugging flag is set - if self.debug_open_pref_win_flag: - config.SystemPrefWin(self) + classic_dest_dir_button_action = Gio.SimpleAction.new( + 'classic_dest_dir_button', + None, + ) + classic_dest_dir_button_action.connect( + 'activate', + self.on_button_classic_dest_dir, + ) + self.add_action(classic_dest_dir_button_action) - # Open the general download options window, if the debugging flag - # is set - if self.debug_open_options_win_flag: - config.OptionsEditWin(self, self.general_options_obj, None) + classic_add_urls_button_action = Gio.SimpleAction.new( + 'classic_add_urls_button', + None, + ) + classic_add_urls_button_action.connect( + 'activate', + self.on_button_classic_add_urls, + ) + self.add_action(classic_add_urls_button_action) - else: - self.main_win_obj.present() + classic_remove_button_action = Gio.SimpleAction.new( + 'classic_remove_button', + None, + ) + classic_remove_button_action.connect( + 'activate', + self.on_button_classic_remove, + ) + self.add_action(classic_remove_button_action) - # Show a warning dialogue window, if the debugging flag is set - if self.debug_warn_multiple_flag: + classic_play_button_action = Gio.SimpleAction.new( + 'classic_play_button', + None, + ) + classic_play_button_action.connect( + 'activate', + self.on_button_classic_play, + ) + self.add_action(classic_play_button_action) - self.dialogue_manager_obj.show_msg_dialogue( - __main__.__prettyname__ + ' is already running!', - 'warning', - 'ok', - ) + classic_move_up_button_action = Gio.SimpleAction.new( + 'classic_move_up_button', + None, + ) + classic_move_up_button_action.connect( + 'activate', + self.on_button_classic_move_up, + ) + self.add_action(classic_move_up_button_action) + + classic_move_down_button_action = Gio.SimpleAction.new( + 'classic_move_down_button', + None, + ) + classic_move_down_button_action.connect( + 'activate', + self.on_button_classic_move_down, + ) + self.add_action(classic_move_down_button_action) + + classic_redownload_button_action = Gio.SimpleAction.new( + 'classic_redownload_button', + None, + ) + classic_redownload_button_action.connect( + 'activate', + self.on_button_classic_redownload, + ) + self.add_action(classic_redownload_button_action) + + classic_stop_button_action = Gio.SimpleAction.new( + 'classic_stop_button', + None, + ) + classic_stop_button_action.connect( + 'activate', + self.on_button_classic_stop, + ) + self.add_action(classic_stop_button_action) + + classic_download_button_action = Gio.SimpleAction.new( + 'classic_download_button', + None, + ) + classic_download_button_action.connect( + 'activate', + self.on_button_classic_download, + ) + self.add_action(classic_download_button_action) + + + def do_activate(self): + + """Gio.Application standard function.""" + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 1986 do_activate') + + self.start() + + # Open the system preferences window, if the debugging flag is set + if self.debug_open_pref_win_flag: + config.SystemPrefWin(self) + + # Open the general download options window, if the debugging flag is + # set + if self.debug_open_options_win_flag: + config.OptionsEditWin(self, self.general_options_obj, None) def do_shutdown(self): @@ -1673,7 +2006,7 @@ def do_shutdown(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 1671 do_shutdown') + utils.debug_time('app 2009 do_shutdown') # Stop the GObject timers immediately if self.script_slow_timer_id: @@ -1707,6 +2040,11 @@ def do_shutdown(self): # If there is a lock on the database file, release it self.remove_db_lock_file() + # Destroy the fake main window used temporarily by self.start(), if it + # exists + if self.fake_main_win_obj: + self.fake_main_win_obj.destroy() + # Stop immediately Gtk.Application.do_shutdown(self) if os.name == 'nt': @@ -1729,90 +2067,67 @@ def start(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 1727 start') + utils.debug_time('app 2070 start') + + # Part 1 - Gtk stabilisation + # -------------------------- # Gtk v3.22.* produces numerous error/warning messages in the terminal # when the Video Index and Video Catalogue are updated. Whatever the # issues were, they appear to have been fixed by Gtk v3.24.* if self.gtk_version_major < 3 \ or (self.gtk_version_major == 3 and self.gtk_version_minor < 24): - self.gtk_broken_flag = True - # Create the main window - self.main_win_obj = mainwin.MainWin(self) - # Most main widgets are desensitised, until the database file has been - # loaded - self.main_win_obj.sensitise_widgets_if_database(False) - # If the debugging flag is set, move it to the top-left corner of the - # desktop - if self.debug_open_top_left_flag: - self.main_win_obj.move(0, 0) - # Make it visible - self.main_win_obj.show_all() - - # Prepare to add an icon to the system tray. The icon is made visible, - # if required, after the config file is loaded - self.status_icon_obj = mainwin.StatusIcon(self) - - # Start the dialogue manager (thread-safe code for Gtk message dialogue - # windows) - self.dialogue_manager_obj = dialogue.DialogueManager( - self, - self.main_win_obj, - ) - - # Give mainapp.TartubeApp IVs their initial values - - # Set the General Options Manager - self.general_options_obj = options.OptionsManager() + # Part 2 - Give mainapp.TartubeApp IVs their initial values + # --------------------------------------------------------- # Set youtube-dl path IVs if os.name == 'nt': if 'PROGRAMFILES(X86)' in os.environ: # 64-bit MS Windows - descrip = 'Windows 64-bit update (recommended)' - python_path = '..\\..\\..\\..\\mingw64\\bin\python3.exe' - pip_path = '..\\..\\..\\..\\mingw64\\bin\pip3-script.py' + recommended = 'ytdl_update_win_64' + python_path = '..\\..\\..\\mingw64\\bin\python3.exe' + pip_path = '..\\..\\..\\mingw64\\bin\pip3-script.py' else: # 32-bit MS Windows - descrip = 'Windows 32-bit update (recommended)' - python_path = '..\\..\\..\\..\\mingw32\\bin\python3.exe' - pip_path = '..\\..\\..\\..\\mingw32\\bin\pip3-script.py' + 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 = { - descrip: [ + recommended: [ python_path, pip_path, 'install', '--upgrade', 'youtube-dl', ], - 'Update using pip3': [ + 'ytdl_update_pip3': [ 'pip3', 'install', '--upgrade', 'youtube-dl', ], - 'Update using pip': [ + 'ytdl_update_pip': [ 'pip', 'install', '--upgrade', 'youtube-dl', ], - 'Update using default youtube-dl path': [ + 'ytdl_update_default_path': [ self.ytdl_path_default, '-U', ], - 'Update using local youtube-dl path': [ + 'ytdl_update_local_path': [ 'youtube-dl', '-U', ], } self.ytdl_update_list = [ - descrip, - 'Update using pip3', - 'Update using pip', - 'Update using default youtube-dl path', - 'Update using local youtube-dl path', + recommended, + 'ytdl_update_pip3', + 'ytdl_update_pip', + 'ytdl_update_default_path', + 'ytdl_update_local_path', ] - self.ytdl_update_current = descrip + self.ytdl_update_current = recommended elif __main__.__pkg_strict_install_flag__: @@ -1823,12 +2138,12 @@ def start(self): self.ytdl_path = self.ytdl_path_pypi self.ytdl_update_dict = { - 'youtube-dl updates are disabled': [], + 'ytdl_update_disabled': [], } self.ytdl_update_list = [ - 'youtube-dl updates are disabled', + 'ytdl_update_disabled', ] - self.ytdl_update_current = 'youtube-dl updates are disabled' + self.ytdl_update_current = 'ytdl_update_disabled' else: @@ -1843,38 +2158,47 @@ def start(self): self.ytdl_path = 'youtube-dl' self.ytdl_update_dict = { - 'Update using pip3 (recommended)': [ + 'ytdl_update_pip3_recommend': [ 'pip3', 'install', '--upgrade', '--user', 'youtube-dl', ], - 'Update using pip3 (omit --user option)': [ + 'ytdl_update_pip3_omit_user': [ 'pip3', 'install', '--upgrade', 'youtube-dl', ], - 'Update using pip': [ + 'ytdl_update_pip': [ 'pip', 'install', '--upgrade', '--user', 'youtube-dl', ], - 'Update using pip (omit --user option)': [ + 'ytdl_update_pip_omit_user': [ 'pip', 'install', '--upgrade', 'youtube-dl', ], - 'Update using default youtube-dl path': [ + 'ytdl_update_default_path': [ self.ytdl_path_default, '-U', ], - 'Update using local youtube-dl path': [ + 'ytdl_update_local_path': [ 'youtube-dl', '-U', ], - 'Update using PyPI youtube-dl path': [ + 'ytdl_update_pypi_path': [ self.ytdl_path_pypi, '-U', ], } self.ytdl_update_list = [ - 'Update using pip3 (recommended)', - 'Update using pip3 (omit --user option)', - 'Update using pip', - 'Update using pip (omit --user option)', - 'Update using default youtube-dl path', - 'Update using local youtube-dl path', - 'Update using PyPI youtube-dl path', + '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 = 'Update using pip3 (recommended)' + self.ytdl_update_current = 'ytdl_update_pip3_recommend' + + # Set the General Options Manager + self.general_options_obj = options.OptionsManager() + + # Compile a list of available sound effects + self.find_sound_effects() + + # Part 3 - Load the config file + # ----------------------------- # Make sure the directory containing the config file exists config_dir = None @@ -1892,16 +2216,19 @@ def start(self): if config_dir is not None and not self.make_directory(config_dir): - if os.name != 'nt': - folder = 'directory' - else: - folder = 'folder' - - self.disable_load_save( - __main__.__prettyname__ + ' can\'t create the ' + folder \ - + ' in which its configuration file is saved', + # Can't use an ordinary message dialogue without a parent window, + # and most users won't see a message in the terminal, so use a + # special window for this purpose + mainwin.StartErrorWin( + self, + _( + 'Tartube can\'t create the folder in which its configuration' \ + + ' file is saved', + ), ) + return + # If the config file exists, load it. If not, create it new_config_flag = False if ( @@ -1922,10 +2249,11 @@ def start(self): # New Tartube installation new_config_flag = True - # Show the status icon in the system tray (which would normally be - # done after the config file had been loaded) - if self.status_icon_obj and self.show_status_icon_flag: - self.status_icon_obj.show_icon() + # The main window hasn't been created yet, so create a temporary + # fake one (which never becomes visible) + # (Without a parent window, Gtk will complain at being asked to + # create dialogue windows) + self.fake_main_win_obj = mainwin.FakeMainWin(self) # On MS Windows, tell the user that they must set the location of # the data directory, self.data_dir. On other operating systems, @@ -1934,14 +2262,96 @@ def start(self): custom_flag = self.notify_user_of_data_dir() if custom_flag and not self.prompt_user_for_data_dir(): - # The user declined to specify a data directory, so shut down - # Tartube. Destroying the main window calls - # self.do_shutdown() - return self.main_win_obj.destroy() + self.disable_load_save( + _( + 'The user declined to specify a data folder for Tartube', + ), + ) - # All done; create the config file, whether Tartube's data - # directory has been changed, or not - self.save_config() + else: + + # All done; create the config file, whether Tartube's data + # directory has been changed, or not + self.save_config() + + # Destroy the fake main window + self.fake_main_win_obj.destroy() + self.fake_main_win_obj = None + + # If file load/save has been disabled, shut down after the special + # window is shown + if self.disable_load_save_flag: + + mainwin.StartErrorWin(self, self.disable_load_save_msg) + + return + + + # Part 4 - Set up the main window + # ------------------------------- + + # Create the main window + self.main_win_obj = mainwin.MainWin(self) + + # Set up widgets in the Video Catalogue toolbar + self.main_win_obj.update_show_filter_widgets() + self.main_win_obj.update_alpha_sort_widgets() + # If the flag it set, switch to the Classic Mode Tab + if self.show_classic_tab_on_startup_flag: + self.main_win_obj.notebook.set_current_page(2) + # Add the right number of pages to the Output Tab + self.main_win_obj.output_tab_setup_pages() + + # Most main widgets are desensitised, until the database file has been + # loaded + self.main_win_obj.sensitise_widgets_if_database(False) + # Disable tooltips, if necessary + if not self.show_tooltips_flag: + self.main_win_obj.disable_tooltips() + # Disable the 'Download all' button and related widgets, if necessary + if self.disable_dl_all_flag: + self.main_win_obj.disable_dl_all_buttons() + + # Resize the main window to match the previous size, if required (but + # don't bother if the previous size is the same as the standard one) + if self.main_win_save_size_flag \ + and ( + self.main_win_save_width != self.main_win_width + or self.main_win_save_height != self.main_win_height + or self.main_win_save_posn != self.paned_min_size + ): + self.main_win_obj.resize( + self.main_win_save_width, + self.main_win_save_height, + ) + + self.main_win_obj.videos_paned.set_position( + self.main_win_save_posn, + ) + + # If the debugging flag is set, move the window to the top-left corner + # of the desktop + if self.debug_open_top_left_flag: + self.main_win_obj.move(0, 0) + + # Make the main window visible + self.main_win_obj.show_all() + + # Prepare to add an icon to the system tray, making it visible only if + # required + self.status_icon_obj = mainwin.StatusIcon(self) + if self.show_status_icon_flag: + self.status_icon_obj.show_icon() + + # Start the dialogue manager (thread-safe code for Gtk message dialogue + # windows) + self.dialogue_manager_obj = dialogue.DialogueManager( + self, + self.main_win_obj, + ) + + # Part 5 - Load a database file + # ----------------------------- # Multiple instances of Tartube can share the same config file, but not # the same database file @@ -2025,7 +2435,7 @@ def start(self): # New database. First create fixed media data objects (media.Folder # objects) that can't be removed by the user (though they can be # hidden) - self.create_system_folders() + self.create_fixed_folders() # Populate the Video Index self.main_win_obj.video_index_populate() @@ -2034,36 +2444,52 @@ def start(self): self.allow_db_save_flag = True self.save_db() - # Now the config file has been loaded (or created), we can add the - # right number of pages to the Output Tab - self.main_win_obj.output_tab_setup_pages() + # Part 6 - Warn user about broken Gtk + # ----------------------------------- # If the system's Gtk is an early, broken version, display a system # warning if self.gtk_broken_flag: self.system_warning( - 126, - 'Gtk v' + str(self.gtk_version_major) + '.' \ - + str(self.gtk_version_minor) + '.' \ - + str(self.gtk_version_micro) \ - + ' is broken, which may cause problems when running ' \ - + __main__.__prettyname__ \ - + '. If possible, please update it to at least Gtk v3.24', + 101, + _( + 'Gtk v{0}.{1}.{2} is broken, which may cause problems when' \ + + ' running Tartube. If possible, please update it to at' \ + + ' least Gtk v3.24' + ).format( + str(self.gtk_version_major), + str(self.gtk_version_minor), + str(self.gtk_version_micro), + ), ) elif self.gtk_emulate_broken_flag: self.system_warning( - 140, - __main__.__prettyname__ + ' is assuming the Gtk v' \ - + str(self.gtk_version_major) - + ' is broken; some (minor) features are disabled', + 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 7 - 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: remove_flag = False + + # (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 Tab0 + self.main_win_obj.notebook.set_current_page(0) + if self.disable_load_save_lock_flag: dialogue_win = mainwin.RemoveLockFileDialogue( @@ -2080,10 +2506,11 @@ def start(self): self.disable_load_save_lock_flag = False self.file_error_dialogue( - 'The ' + __main__.__prettyname__ \ - + ' database file was not loaded, but is no' \ - + ' longer protected\n\nRestart ' \ - + __main__.__prettyname__ + ' to load it', + _( + 'The Tartube database file was not loaded, but is no'\ + + ' longer protected', + ) + '\n\n' \ + + _('Restart Tartube to load it'), ) if not remove_flag: @@ -2091,17 +2518,25 @@ def start(self): 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\nBecause of the' \ - + ' error, file load/save has been disabled', + self.disable_load_save_msg + '\n\n' \ + + _( + 'Because of the error, file load/save has been' \ + + ' disabled', + ) ) + # Part 8 - Start system timers + # ---------------------------- + # Start the script's GObject slow timer self.script_slow_timer_id = GObject.timeout_add( self.script_slow_timer_time, @@ -2114,6 +2549,9 @@ def start(self): self.script_fast_timer_callback, ) + # Part 9 - 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 @@ -2121,9 +2559,10 @@ def start(self): if new_config_flag and os.name == 'nt': self.dialogue_manager_obj.show_msg_dialogue( - 'youtube-dl must be installed before you can use ' \ - + __main__.__prettyname__ \ - + '. Do you want to install youtube-dl now?', + _( + '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 @@ -2135,18 +2574,17 @@ def start(self): ) # If a download operation (real or simulated) is scheduled to occur - # on startup, then initiate it + # on startup, then set the time at which + # self.script_fast_timer_callback() should initiate it elif self.scheduled_dl_mode == 'start': - self.download_manager_start( - 'real', # 'Download all' - True, # This function is the calling function - ) + + self.scheduled_dl_start_check_time \ + = time.time() + self.scheduled_dl_start_wait_time elif self.scheduled_check_mode == 'start': - self.download_manager_start( - 'sim', # 'Check all' - True, # This function is the calling function - ) + + self.scheduled_check_start_check_time \ + = time.time() + self.scheduled_check_start_wait_time def stop(self): @@ -2163,29 +2601,34 @@ def stop(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 2161 stop') + utils.debug_time('app 2604 stop') + + # If a (silent) livestream operation is in progress, we can stop it + # immediately + if self.livestream_manager_obj: + + 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 self.current_manager_obj: + elif self.current_manager_obj: if self.download_manager_obj: - string = 'a download' + string = _('There is a download operation in progress.') elif self.update_manager_obj: - string = 'an update' + string = _('There is an update operation in progress.') elif self.refresh_manager_obj: - string = 'a refresh' + string = _('There is a refresh operation in progress.') elif self.info_manager_obj: - string = 'an info' + string = _('There is an info operation in progress.') else: - string = 'a tidy' + string = _('There is a tidy operation in progress.') # If the user clicks 'yes', call self.stop_continue() to complete # the shutdown self.dialogue_manager_obj.show_msg_dialogue( - 'There is ' + string + ' operation in progress.' \ - + ' Are you sure you want to quit ' + __main__.__prettyname__ \ - + '?', + string + ' ' + _('Are you sure you want to quit Tartube?'), 'question', 'yes-no', None, # Parent window is main window @@ -2209,8 +2652,10 @@ def stop_continue(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 2207 stop_continue') + utils.debug_time('app 2655 stop_continue') + # (No need to check the livestream operation here - it was stopped in + # the call to self.stop() ) if self.download_manager_obj: self.download_manager_obj.stop_download_operation() @@ -2296,15 +2741,15 @@ def system_error(self, error_code, msg): Error codes for this function and for self.system_warning are currently assigned thus: - 100-199: mainapp.py (in use: 101-153) + 100-199: mainapp.py (in use: 101-158) 200-299: mainwin.py (in use: 201-248) - 300-399: downloads.py (in use: 301-304) + 300-399: downloads.py (in use: 301-305) 400-499: config.py (in use: 401-404) """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 2302 system_error') + utils.debug_time('app 2752 system_error') if self.main_win_obj and self.system_error_show_flag: self.main_win_obj.errors_list_add_system_error(error_code, msg) @@ -2330,7 +2775,7 @@ def system_warning(self, error_code, msg): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 2328 system_warning') + utils.debug_time('app 2778 system_warning') if self.main_win_obj and self.system_warning_show_flag: self.main_win_obj.errors_list_add_system_warning(error_code, msg) @@ -2351,7 +2796,10 @@ def load_config(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 2349 load_config') + utils.debug_time('app 2799 load_config') + + # Define global variables for this function + global _ # The config file can be stored at one of two locations, depending on # whether xdg is available, or not @@ -2374,6 +2822,13 @@ def load_config(self): if self.current_manager_obj \ or not os.path.isfile(config_file_path) \ or self.disable_load_save_flag: + + self.disable_load_save( + _( + 'Failed to load the Tartube config file (failed sanity check)', + ), + ) + return # In case a competing instance of Tartube is saving the same config @@ -2389,11 +2844,12 @@ def load_config(self): time.sleep(0.1) if os.path.isfile(lock_path): - self.disable_load_save() - self.file_error_dialogue( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' config file (file is locked)\n\nFile load/save' \ - + ' has been disabled', + + self.disable_load_save( + _( + 'Failed to load the Tartube config file (file is' \ + + ' locked)', + ), ) return @@ -2404,14 +2860,14 @@ def load_config(self): json_dict = json.load(infile) except: - # Loading failed. Prevent damage to backup files by disabling file - # load/save for the rest of this session + self.disable_load_save( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' config file', + _( + 'Failed to load the Tartube config file (JSON load failure)', + ), ) - return False + return # Do some basic checks on the loaded data if not json_dict \ @@ -2422,11 +2878,12 @@ def load_config(self): or json_dict['script_name'] != __main__.__packagename__: self.disable_load_save( - 'The ' + __main__.__prettyname__ \ - + ' config file is invalid (missing data)', + _( + 'Failed to load the Tartube config file (file is invalid)', + ), ) - return False + return # Convert a version, e.g. 1.234.567, into a simple number, e.g. # 1234567, that can be compared with other versions @@ -2436,12 +2893,15 @@ def load_config(self): # read) if version is None \ or version > self.convert_version(__main__.__version__): + self.disable_load_save( - 'Config file can\'t be read\nby this version of ' \ - + __main__.__prettyname__, + _( + 'Failed to load the Tartube config file (file cannot be read' \ + + ' by this version)', + ), ) - return False + return # Since v1.0.008, config files have identified their file type if version >= 1000008 \ @@ -2449,12 +2909,48 @@ def load_config(self): not 'file_type' in json_dict or json_dict['file_type'] != 'config' ): self.disable_load_save( - 'The ' + __main__.__prettyname__ \ - + ' config file is invalid (missing file type)', + _( + 'Failed to load the Tartube config file (missing file type)', + ), ) return False + # Set the locale + if version >= 2000081: # v2.0.081 + self.custom_locale = json_dict['custom_locale'] + + if self.custom_locale != formats.LOCALE_DEFAULT: + + if not self.custom_locale in formats.LOCALE_LIST: + # Invalid; use the default value + self.custom_locale = formats.LOCALE_DEFAULT + + else: + + LOCALE = gettext.translation( + 'base', + localedir='locale', + languages=[self.custom_locale], + ) + LOCALE.install() + + # (Apply to this file) + _ = LOCALE.gettext + # (Apply to other files) + mainwin._ = _ + config._ = _ + downloads._ = _ + formats._ = _ + info._ = _ + media._ = _ + refresh._ = _ + tidy._ = _ + updates._ = _ + # (Update download operation stages, e.g. + # formats.MAIN_STAGE_QUEUED + formats.do_translate(True) + # Set IVs to their new values if version >= 1004040: # v1.4.040 self.main_win_save_size_flag = json_dict['main_win_save_size_flag'] @@ -2487,31 +2983,22 @@ def load_config(self): self.show_status_icon_flag = json_dict['show_status_icon_flag'] self.close_to_tray_flag = json_dict['close_to_tray_flag'] - # (Setting the value of the Gtk widgets automatically sets the IVs) if version >= 1003129: # v1.3.129 - self.main_win_obj.hide_finished_checkbutton.set_active( - json_dict['progress_list_hide_flag'], - ) + self.progress_list_hide_flag = json_dict['progress_list_hide_flag'] if version >= 1000029: # v1.0.029 - self.main_win_obj.reverse_results_checkbutton.set_active( - json_dict['results_list_reverse_flag'], - ) + self.results_list_reverse_flag \ + = json_dict['results_list_reverse_flag'] if version >= 1003069: # v1.3.069 - self.main_win_obj.show_system_error_checkbutton.set_active( - json_dict['system_error_show_flag'], - ) + self.system_error_show_flag = json_dict['system_error_show_flag'] if version >= 6006: # v0.6.006 - self.main_win_obj.show_system_warning_checkbutton.set_active( - json_dict['system_warning_show_flag'], - ) + self.system_warning_show_flag \ + = json_dict['system_warning_show_flag'] if version >= 1003079: # v1.3.079 - self.main_win_obj.show_operation_error_checkbutton.set_active( - json_dict['operation_error_show_flag'], - ) - self.main_win_obj.show_operation_warning_checkbutton.set_active( - json_dict['operation_warning_show_flag'], - ) + self.operation_error_show_flag \ + = json_dict['operation_error_show_flag'] + self.operation_warning_show_flag \ + = json_dict['operation_warning_show_flag'] if version >= 1000007: # v1.0.007 self.system_msg_keep_totals_flag \ @@ -2528,44 +3015,21 @@ def load_config(self): else: self.data_dir_alt_list = [ self.data_dir ] + if version >= 2000069: # v2.0.069: + self.sound_custom = json_dict['sound_custom'] + if version >= 3014: # v0.3.014 self.db_backup_mode = json_dict['db_backup_mode'] - # (In version v0.5.027, the value of these IVs were overhauled. If - # loading from an earlier config file, replace those values with the - # new default values) - if version >= 5027: - self.ytdl_bin = json_dict['ytdl_bin'] - self.ytdl_path_default = json_dict['ytdl_path_default'] - self.ytdl_path = json_dict['ytdl_path'] - self.ytdl_update_dict = json_dict['ytdl_update_dict'] - self.ytdl_update_list = json_dict['ytdl_update_list'] - self.ytdl_update_current = json_dict['ytdl_update_current'] - # (In version v1.3.903, these IVs were modified a little, but not - # on MS Windows) - if os.name != 'nt' and version <= 1003090: # v1.3.090 - self.ytdl_update_dict['Update using pip3 (recommended)'] \ - = ['pip3', 'install', '--upgrade', '--user', 'youtube-dl'] - self.ytdl_update_dict['Update using pip3 (omit --user option)'] \ - = ['pip3', 'install', '--upgrade', 'youtube-dl'] - self.ytdl_update_dict['Update using pip'] \ - = ['pip', 'install', '--upgrade', '--user', 'youtube-dl'] - self.ytdl_update_dict['Update using pip (omit --user option)'] \ - = ['pip', 'install', '--upgrade', 'youtube-dl'] - self.ytdl_update_list = [ - 'Update using pip3 (recommended)', - 'Update using pip3 (omit --user option)', - 'Update using pip', - 'Update using pip (omit --user option)', - 'Update using default youtube-dl path', - 'Update using local youtube-dl path', - ] - # (In version v1.5.012, these IVs were modified a little, but not on - # MS Widnows) - if os.name != 'nt' and version <= 1005012: # v1.5.012 - self.ytdl_update_dict['Update using PyPI youtube-dl path'] \ - = [self.ytdl_path_pypi, '-U'] - self.ytdl_update_list.append('Update using PyPI youtube-dl path') + if version >= 2000029: # v2.0.029 + self.show_classic_tab_on_startup_flag \ + = json_dict['show_classic_tab_on_startup_flag'] + self.classic_dir_list = json_dict['classic_dir_list'] + self.classic_dir_previous = json_dict['classic_dir_previous'] + + # (In various versions between v0.5.027 and v2.0.097, the youtube + # update IVs were overhauled several times) + self.load_config_ytdl_update(version, json_dict) if version >= 1003074: # v1.3.074 self.ytdl_output_system_cmd_flag \ @@ -2656,6 +3120,35 @@ def load_config(self): self.scheduled_shutdown_flag \ = json_dict['scheduled_shutdown_flag'] + if version >= 2000037: # v2.0.037 + self.enable_livestreams_flag \ + = json_dict['enable_livestreams_flag'] + if version >= 2000047: # v2.0.047 + self.livestream_max_days = json_dict['livestream_max_days'] + self.livestream_use_colour_flag \ + = json_dict['livestream_use_colour_flag'] + if version >= 2000052: # v2.0.052 + self.livestream_auto_notify_flag \ + = json_dict['livestream_auto_notify_flag'] + if version >= 2000068: # v2.0.068 + self.livestream_auto_alarm_flag \ + = json_dict['livestream_auto_alarm_flag'] + if version >= 2000052: # v2.0.052 + self.livestream_auto_open_flag \ + = json_dict['livestream_auto_open_flag'] + if version >= 2000054: # v2.0.054 + self.livestream_auto_dl_start_flag \ + = json_dict['livestream_auto_dl_start_flag'] + self.livestream_auto_dl_stop_flag \ + = json_dict['livestream_auto_dl_stop_flag'] + if version >= 2000037: # v2.0.037 + self.scheduled_livestream_flag \ + = json_dict['scheduled_livestream_flag'] + self.scheduled_livestream_wait_mins \ + = json_dict['scheduled_livestream_wait_mins'] + self.scheduled_livestream_last_time \ + = json_dict['scheduled_livestream_last_time'] + if version >= 1003112: # v1.3.112 self.autostop_time_flag = json_dict['autostop_time_flag'] self.autostop_time_value = json_dict['autostop_time_value'] @@ -2737,28 +3230,15 @@ def load_config(self): self.ignore_custom_regex_flag \ = json_dict['ignore_custom_regex_flag'] - # (Setting the value of the Gtk widgets automatically sets the IVs) - self.main_win_obj.num_worker_spinbutton.set_value( - json_dict['num_worker_default'], - ) - self.main_win_obj.num_worker_checkbutton.set_active( - json_dict['num_worker_apply_flag'], - ) + self.num_worker_default = json_dict['num_worker_default'] + self.num_worker_apply_flag = json_dict['num_worker_apply_flag'] - self.main_win_obj.bandwidth_spinbutton.set_value( - json_dict['bandwidth_default'], - ) - self.main_win_obj.bandwidth_checkbutton.set_active( - json_dict['bandwidth_apply_flag'], - ) + self.bandwidth_default = json_dict['bandwidth_default'] + self.bandwidth_apply_flag = json_dict['bandwidth_apply_flag'] if version >= 1002011: # v1.2.011 - self.main_win_obj.set_video_res_limit( - json_dict['video_res_default'], - ) - self.main_win_obj.video_res_checkbutton.set_active( - json_dict['video_res_apply_flag'], - ) + self.video_res_default = json_dict['video_res_default'] + self.video_res_apply_flag = json_dict['video_res_apply_flag'] self.match_method = json_dict['match_method'] self.match_first_chars = json_dict['match_first_chars'] @@ -2811,50 +3291,141 @@ def load_config(self): os.path.join(self.data_dir, '.temp', 'ytdl-test'), ) - # ...and update various widgets + # If the most-recently selected directory, self.classic_dir_previous, + # still exists in self.classic_dir_list, move it to the top, so it's + # the first item displayed in the combo + if self.classic_dir_previous is not None \ + and self.classic_dir_previous in self.classic_dir_list: - # If the tray icon should be visible, make it visible - if self.show_status_icon_flag: - self.status_icon_obj.show_icon() + self.classic_dir_list.remove(self.classic_dir_previous) + self.classic_dir_list.insert(0, self.classic_dir_previous) - # If self.toolbar_squeeze_flag is set, redraw the main toolbar without - # labels - if self.toolbar_squeeze_flag: - self.main_win_obj.redraw_main_toolbar() + # In either case, we don't need to remember the previous session's + # destination directory any more + self.classic_dir_previous = None - # If self.show_tooltips_flag is not set, disable tooltips - if not self.show_tooltips_flag: - self.main_win_obj.disable_tooltips() - # If self.disable_dl_all_flag, disable the 'Download all' buttons - if self.disable_dl_all_flag: - self.main_win_obj.disable_dl_all_buttons() + def load_config_ytdl_update(self, version, json_dict): - # Update widgets in the Video Catalogue toolbar - self.main_win_obj.catalogue_size_entry.set_text( - str(self.catalogue_page_size), - ) + """"Called by self.load_config(). - self.main_win_obj.update_show_filter_widgets() - self.main_win_obj.update_alpha_sort_widgets() - self.main_win_obj.update_use_regex_widgets() + The IVs handling youtube-dl updates have been overhauled several + times. - # Resize the main window to match the previous size, if required (but - # don't bother if the previous size is the same as the standard one) - if self.main_win_save_size_flag \ - and ( - self.main_win_save_width != self.main_win_width - or self.main_win_save_height != self.main_win_height - or self.main_win_save_posn != self.paned_min_size - ): - self.main_win_obj.resize( - self.main_win_save_width, - self.main_win_save_height, - ) + To keep the layout of self.load_config() reasonable, this function is + called to import the IVs from the loaded config file, and update them + as appropriate. - self.main_win_obj.videos_paned.set_position( - self.main_win_save_posn, - ) + Args: + + version (int): The config file's Tartube version, converted to a + simple integer in a call to self.convert_version() + + json_dict: The data loaded from the config file + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 3329 load_config_ytdl_update') + + # (In version v0.5.027, the value of these IVs were overhauled. If + # loading from an earlier config file, replace those values with the + # new default values) + if version >= 5027: + self.ytdl_bin = json_dict['ytdl_bin'] + self.ytdl_path_default = json_dict['ytdl_path_default'] + self.ytdl_path = json_dict['ytdl_path'] + self.ytdl_update_dict = json_dict['ytdl_update_dict'] + self.ytdl_update_list = json_dict['ytdl_update_list'] + self.ytdl_update_current = json_dict['ytdl_update_current'] + + # (In version v1.3.903, these IVs were modified a little, but not + # on MS Windows) + if os.name != 'nt' and version <= 1003090: # v1.3.090 + self.ytdl_update_dict['Update using pip3 (recommended)'] \ + = ['pip3', 'install', '--upgrade', '--user', 'youtube-dl'] + self.ytdl_update_dict['Update using pip3 (omit --user option)'] \ + = ['pip3', 'install', '--upgrade', 'youtube-dl'] + self.ytdl_update_dict['Update using pip'] \ + = ['pip', 'install', '--upgrade', '--user', 'youtube-dl'] + self.ytdl_update_dict['Update using pip (omit --user option)'] \ + = ['pip', 'install', '--upgrade', 'youtube-dl'] + self.ytdl_update_list = [ + 'Update using pip3 (recommended)', + 'Update using pip3 (omit --user option)', + 'Update using pip', + 'Update using pip (omit --user option)', + 'Update using default youtube-dl path', + 'Update using local youtube-dl path', + ] + + # (In version v1.5.012, these IVs were modified a little, but not on + # MS Windows) + if os.name != 'nt' and version <= 1005012: # v1.5.012 + self.ytdl_update_dict['Update using PyPI youtube-dl path'] \ + = [self.ytdl_path_pypi, '-U'] + self.ytdl_update_list.append('Update using PyPI youtube-dl path') + + + # (In version v2.0.086, these IVs were completely overhauled on all + # operatin systems) + if version < 2000096: # v2.0.096 + + update_dict = { + 'Update using default youtube-dl path': + 'ytdl_update_default_path', + 'Update using local youtube-dl path': + 'ytdl_update_local_path', + 'Update using pip': + 'ytdl_update_pip', + 'Update using pip (omit --user option)': + 'ytdl_update_pip_omit_user', + 'Update using pip3': + 'ytdl_update_pip3', + 'Update using pip3 (omit --user option)': + 'ytdl_update_pip3_omit_user', + 'Update using pip3 (recommended)': + 'ytdl_update_pip3_recommend', + 'Update using PyPI youtube-dl path': + 'ytdl_update_pypi_path', + 'Windows 32-bit update (recommended)': + 'ytdl_update_win_32', + 'Windows 64-bit update (recommended)': + 'ytdl_update_win_64', + 'youtube-dl updates are disabled': + 'ytdl_update_disabled', + } + + ytdl_update_dict = {} + for key in self.ytdl_update_dict: + ytdl_update_dict[update_dict[key]] = self.ytdl_update_dict[key] + + self.ytdl_update_dict = ytdl_update_dict + + ytdl_update_list = [] + for item in self.ytdl_update_list: + ytdl_update_list.append(update_dict[item]) + + self.ytdl_update_list = ytdl_update_list + + self.ytdl_update_current = update_dict[self.ytdl_update_current] + + # (In version v2.0.109, the directory location used by tartube_mswin.sh + # was changed) + if version < 2000109 and os.name == 'nt': # v2.0.109 + + if 'PROGRAMFILES(X86)' in os.environ: + recommended = 'ytdl_update_win_64' + else: + recommended = 'ytdl_update_win_32' + + recommended_list = self.ytdl_update_dict[recommended] + mod_list = [] + + for item in recommended_list: + mod_list.append(re.sub(r'^..\\', '', item)) + + self.ytdl_update_dict[recommended] = mod_list def save_config(self): @@ -2869,7 +3440,7 @@ def save_config(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 2854 save_config') + utils.debug_time('app 3443 save_config') # The config file can be stored at one of two locations, depending on # whether xdg is available, or not @@ -2890,6 +3461,19 @@ def save_config(self): # Sanity check if self.current_manager_obj or self.disable_load_save_flag: + + # When called from self.start(), no main window object exists + # yet, and so Tartube will be shut down with this error message + # When called from anything else, throughout this function the + # response is different + if not self.main_win_obj: + self.disable_load_save( + _( + 'Failed to save the Tartube config file (failed sanity' \ + + ' check)', + ), + ) + return # Prepare values @@ -2899,7 +3483,7 @@ def save_config(self): # size for the 'Videos Tab' paned is the standard paned position; # the minimum size for the main window itself is half the standard # size - if self.main_win_save_size_flag: + if self.main_win_obj and self.main_win_save_size_flag: (width, height) = self.main_win_obj.get_size() posn = self.main_win_obj.videos_paned.get_position() @@ -2927,6 +3511,8 @@ def save_config(self): 'save_time': str(utc.strftime('%H:%M:%S')), 'file_type': 'config', # Data + 'custom_locale': self.custom_locale, + 'main_win_save_size_flag': self.main_win_save_size_flag, 'main_win_save_width': self.main_win_save_width, 'main_win_save_height': self.main_win_save_height, @@ -2960,8 +3546,15 @@ def save_config(self): 'data_dir_use_list_flag': self.data_dir_use_list_flag, 'data_dir_add_from_list_flag': self.data_dir_add_from_list_flag, + 'sound_custom': self.sound_custom, + 'db_backup_mode': self.db_backup_mode, + 'show_classic_tab_on_startup_flag': \ + self.show_classic_tab_on_startup_flag, + 'classic_dir_list': self.classic_dir_list, + 'classic_dir_previous': self.classic_dir_previous, + 'ytdl_bin': self.ytdl_bin, 'ytdl_path_default': self.ytdl_path_default, 'ytdl_path': self.ytdl_path, @@ -3021,6 +3614,22 @@ def save_config(self): 'scheduled_shutdown_flag': self.scheduled_shutdown_flag, + 'enable_livestreams_flag': \ + self.enable_livestreams_flag, + 'livestream_max_days': self.livestream_max_days, + 'livestream_use_colour_flag': self.livestream_use_colour_flag, + 'livestream_auto_notify_flag': self.livestream_auto_notify_flag, + 'livestream_auto_alarm_flag': self.livestream_auto_alarm_flag, + 'livestream_auto_open_flag': self.livestream_auto_open_flag, + 'livestream_auto_dl_start_flag': \ + self.livestream_auto_dl_start_flag, + 'livestream_auto_dl_stop_flag': self.livestream_auto_dl_stop_flag, + 'scheduled_livestream_flag': self.scheduled_livestream_flag, + 'scheduled_livestream_wait_mins': \ + self.scheduled_livestream_wait_mins, + 'scheduled_livestream_last_time': \ + self.scheduled_livestream_last_time, + 'autostop_time_flag': self.autostop_time_flag, 'autostop_time_value': self.autostop_time_value, 'autostop_time_unit': self.autostop_time_unit, @@ -3104,12 +3713,17 @@ def save_config(self): time.sleep(0.1) if os.path.isfile(lock_path): - self.disable_load_save() - self.file_error_dialogue( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' config file (file is locked)\n\nFile load/save' \ - + ' has been disabled', - ) + + msg = _( + 'Failed to save the Tartube config file (file is' \ + + ' locked)', + ) + '\n\n' + _('File load/save has been disabled') + + if not self.main_win_obj: + self.disable_load_save(msg) + else: + self.disable_load_save() + self.file_error_dialogue(msg) return @@ -3121,11 +3735,17 @@ def save_config(self): except: - self.disable_load_save( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' config file (file already in use)', + msg = _( + 'Failed to save the Tartube config file (file already' \ + + ' in use)' ) + if not self.main_win_obj: + self.disable_load_save(msg) + else: + self.disable_load_save() + self.file_error_dialogue(msg) + return # Try to save the config file @@ -3135,11 +3755,17 @@ def save_config(self): except: os.remove(lock_path) - self.disable_load_save() - self.file_error_dialogue( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' config file\n\nFile load/save has been disabled', - ) + + msg = _('Failed to save the Tartube config file') \ + + '\n\n' + _('File load/save has been disabled') + + if not self.main_win_obj: + self.disable_load_save(msg) + else: + self.disable_load_save() + self.file_error_dialogue(msg) + + return # Procedure successful; remove the lock if not self.debug_ignore_lockfile_flag: @@ -3160,7 +3786,7 @@ def load_db(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 3134 load_db') + utils.debug_time('app 3789 load_db') # Sanity check path = os.path.abspath(os.path.join(self.data_dir, self.db_file_name)) @@ -3179,8 +3805,7 @@ def load_db(self): # (The True argument signals that the user should be prompted # to artificially remove the lockfile) self.disable_load_save( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' database file', + _('Failed to load the Tartube database file'), True, ) @@ -3198,8 +3823,7 @@ def load_db(self): # (The True argument signals that the user should be # prompted to artificially remove the lockfile) self.disable_load_save( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' database file', + _('Failed to load the Tartube database file'), True, ) @@ -3207,12 +3831,13 @@ def load_db(self): # 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 + # database generate messages there) if self.main_win_obj: self.main_win_obj.video_index_reset() self.main_win_obj.video_catalogue_reset() self.main_win_obj.progress_list_reset() self.main_win_obj.results_list_reset() - self.main_win_obj.errors_list_reset() self.main_win_obj.show_all() # Most main widgets are desensitised, until the database file has been @@ -3228,8 +3853,7 @@ def load_db(self): except: self.remove_db_lock_file() self.disable_load_save( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' database file', + _('Failed to load the Tartube database file'), ) return False @@ -3244,7 +3868,7 @@ def load_db(self): self.remove_db_lock_file() self.file_error_dialogue( - 'The ' + __main__.__prettyname__ + ' database file is invalid', + _('The Tartube database file is invalid'), ) return False @@ -3260,8 +3884,7 @@ def load_db(self): self.remove_db_lock_file() self.disable_load_save( - 'Database file can\'t be read\nby this version of ' \ - + __main__.__prettyname__, + _('Database file can\'t be read by this version of Tartube'), ) return False @@ -3289,6 +3912,22 @@ def load_db(self): self.media_reg_dict = load_dict['media_reg_dict'] self.media_name_dict = load_dict['media_name_dict'] self.media_top_level_list = load_dict['media_top_level_list'] + if version >= 2000048: # v2.0.048 + self.media_reg_live_dict = load_dict['media_reg_live_dict'] + if version >= 2000052: # v2.0.052 + self.media_reg_auto_notify_dict \ + = load_dict['media_reg_auto_notify_dict'] + if version >= 2000068: # v2.0.068 + self.media_reg_auto_alarm_dict \ + = load_dict['media_reg_auto_alarm_dict'] + if version >= 2000052: # v2.0.052 + self.media_reg_auto_open_dict \ + = load_dict['media_reg_auto_open_dict'] + if version >= 2000054: # v2.0.054 + self.media_reg_auto_dl_start_dict \ + = load_dict['media_reg_auto_dl_start_dict'] + self.media_reg_auto_dl_stop_dict \ + = load_dict['media_reg_auto_dl_stop_dict'] self.fixed_all_folder = load_dict['fixed_all_folder'] self.fixed_fav_folder = load_dict['fixed_fav_folder'] self.fixed_new_folder = load_dict['fixed_new_folder'] @@ -3297,70 +3936,17 @@ def load_db(self): if version >= 1004028: # v1.4.028 self.fixed_bookmark_folder = load_dict['fixed_bookmark_folder'] self.fixed_waiting_folder = load_dict['fixed_waiting_folder'] + if version >= 2000042: # v2.0.042 + self.fixed_live_folder = load_dict['fixed_live_folder'] + if version >= 2000098: # v2.0.098 + self.fixed_folder_locale = load_dict['fixed_folder_locale'] # Update the loaded data for this version of Tartube self.update_db(version) - # As of v1.3.099, some container names have become illegal. Replace any - # illegal names with legal ones - if version <= 1003099: # v1.3.099 - - for old_name in self.media_name_dict.keys(): - if not self.check_container_name_is_legal(old_name): - - dbid = self.media_name_dict[old_name] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - media_data_obj, - utils.find_available_name(self, 'downloads', -1), - ) - - # In v1.4.028, two new system folder were added - if version < 1004028: # v1.4.028 - - # If there are existing folders with the same name, they must be - # renamed - old_list = ['Bookmarks', 'Waiting Videos'] - for old_name in old_list: - - if old_name in self.media_name_dict: - - dbid = self.media_name_dict[old_name] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name. The -1 argument means to keep going - # indefinitely, until an available name is found - self.rename_container_silently( - media_data_obj, - utils.find_available_name(self, 'downloads', -1), - ) - - # Now create the new system folders - self.fixed_bookmark_folder = self.add_folder( - 'Bookmarks', - None, # No parent folder - False, # Allow downloads - True, # Fixed (folder cannot be removed) - True, # Private - True, # Can only contain videos - False, # Not temporary - ) - - self.fixed_waiting_folder = self.add_folder( - 'Waiting Videos', - None, # No parent folder - False, # Allow downloads - True, # Fixed (folder cannot be removed) - True, # Private - True, # Can only contain videos - False, # Not temporary - ) - - # If the old structure is being used, the user might try to manually - # copy the contents of the /downloads folder into the folder above + # If the old directory structure is being used, the user might try to + # manually copy the contents of the /downloads directory into the + # directory above # To prevent problems when that happens, preemptively rename any media # data object called 'downloads' if old_flag and 'downloads' in self.media_name_dict: @@ -3373,17 +3959,27 @@ def load_db(self): if new_name is not None: self.rename_container_silently(media_data_obj, new_name) + # If the locale has changed since the loaded database file was last + # saved, update the names of fixed folders + if self.fixed_folder_locale != self.custom_locale: + + self.rename_fixed_folders() + self.fixed_folder_locale = self.custom_locale + # Empty any temporary folders self.delete_temp_folders() # Auto-delete old downloaded videos self.auto_delete_old_videos() - # If the debugging flag is set, hide all fixed (system) folders + # If the debugging flag is set, hide all fixed folders if self.debug_hide_folders_flag: self.fixed_all_folder.set_hidden_flag(True) + self.fixed_bookmark_folder.set_hidden_flag(True) self.fixed_fav_folder.set_hidden_flag(True) + self.fixed_live_folder.set_hidden_flag(True) self.fixed_new_folder.set_hidden_flag(True) + self.fixed_waiting_folder.set_hidden_flag(True) self.fixed_temp_folder.set_hidden_flag(True) self.fixed_misc_folder.set_hidden_flag(True) @@ -3415,10 +4011,10 @@ def update_db(self, version): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 3389 update_db') + utils.debug_time('app 4014 update_db') - # (self.fixed_bookmark_folder and self.fixed_waiting_folder, having - # been added later, are not required by this list) + # (Other system folders, having been added later, are not required by + # this list) fixed_folder_list = [ self.fixed_all_folder, self.fixed_fav_folder, @@ -3468,6 +4064,7 @@ def update_db(self, version): 0, dl_count, fav_count, + 0, new_count, 0, ) @@ -3513,8 +4110,8 @@ def update_db(self, version): # This version fixes issues with sorting videos. Channels, # playlists and folders in a loaded database might not be sorted # correctly, so just sort them all using the new algorithms - # (self.fixed_bookmark_folder and self.fixed_waiting_folder, - # having been added later, are not required by this list) + # (Other system folders, having been added later, are not required + # by this list) container_list = [ self.fixed_all_folder, self.fixed_new_folder, @@ -3568,6 +4165,7 @@ def update_db(self, version): 0, dl_count, fav_count, + 0, new_count, 0, ) @@ -3586,9 +4184,9 @@ def update_db(self, version): if self.media_reg_dict.len() > 1000: dialogue_win = self.dialogue_manager_obj.show_msg_dialogue( - __main__.__prettyname__ \ - + ' is applying an essential database update.\n\nThis' \ - + ' might take a few minutes, so please be patient.', + _('Tartube is applying an essential database update') \ + + '\n\n' \ + + _('This might take a few minutes, so please be patient'), 'info', 'ok', self.main_win_obj, @@ -3776,6 +4374,23 @@ def update_db(self, version): options_obj.options_dict['second_video_format'] \ = options_obj.options_dict['third_video_format'] + if version <= 1003099: # v1.3.099 + + # In this version, some container names have become illegal. + # Replace any illegal names with legal ones + for old_name in self.media_name_dict.keys(): + if not self.check_container_name_is_legal(old_name): + + dbid = self.media_name_dict[old_name] + media_data_obj = self.media_reg_dict[dbid] + + # Generate a new name. The -1 argument means to keep going + # indefinitely, until an available name is found + self.rename_container_silently( + media_data_obj, + utils.find_available_name(self, 'downloads', 2, -1), + ) + if version < 1003106: # v1.3.106 # This version adds a new option to options.OptionsManager @@ -3813,26 +4428,67 @@ def update_db(self, version): options_obj.options_dict['output_format'] \ = output_format + 1 - if version < 1004037: # v1.4.037 - - # This version adds 'Bookmarks' and 'Waiting Videos' system - # folders, and corresponding new IVs for each media.Video object - for dbid in self.media_name_dict.values(): - container_obj = self.media_reg_dict[dbid] - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - child_obj.bookmark_flag = False - child_obj.waiting_flag = False + if version < 1004028: # v1.4.028 - if version < 1004037: # v1.4.037 + # This version adds two new fixed folders. If there are existing + # folders with the same name, they must be renamed + old_list \ + = [formats.FOLDER_BOOKMARKS, formats.FOLDER_WAITING_VIDEOS] + for old_name in old_list: - # This version adds new IVs to channels, playlists and folders - for dbid in self.media_name_dict.values(): - container_obj = self.media_reg_dict[dbid] + if old_name in self.media_name_dict: - container_obj.bookmark_count = 0 - container_obj.waiting_count = 0 + dbid = self.media_name_dict[old_name] + media_data_obj = self.media_reg_dict[dbid] + + # Generate a new name. The -1 argument means to keep going + # indefinitely, until an available name is found + self.rename_container_silently( + media_data_obj, + utils.find_available_name(self, 'downloads', 2, -1), + ) + + # Now create the new fixed folders + self.fixed_bookmark_folder = self.add_folder( + formats.FOLDER_BOOKMARKS, + None, # No parent folder + False, # Allow downloads + True, # Fixed (folder cannot be removed) + True, # Private + True, # Can only contain videos + False, # Not temporary + ) + + self.fixed_waiting_folder = self.add_folder( + formats.FOLDER_WAITING_VIDEOS, + None, # No parent folder + False, # Allow downloads + True, # Fixed (folder cannot be removed) + True, # Private + True, # Can only contain videos + False, # Not temporary + ) + + if version < 1004037: # v1.4.037 + + # Having added new fixed folders, add corresponding new IVs for + # each media.Video object + for dbid in self.media_name_dict.values(): + container_obj = self.media_reg_dict[dbid] + + for child_obj in container_obj.child_list: + if isinstance(child_obj, media.Video): + child_obj.bookmark_flag = False + child_obj.waiting_flag = False + + if version < 1004037: # v1.4.037 + + # This version adds new IVs to channels, playlists and folders + for dbid in self.media_name_dict.values(): + container_obj = self.media_reg_dict[dbid] + + container_obj.bookmark_count = 0 + container_obj.waiting_count = 0 # Some of the count IVs were not working 100%, so we'll just # recalculate them all @@ -3875,6 +4531,74 @@ def update_db(self, version): if os.path.isfile(unsorted_path): os.remove(unsorted_path) + if version < 2000025: # v2.0.025 + + # This version adds the Classic Mode Tab, and new IVs used by it. + # Most of them are only created when needed + for media_data_obj in self.media_reg_dict.values(): + if isinstance(media_data_obj, media.Video): + media_data_obj.dummy_flag = False + + if version < 2000035: # v2.0.035 + + # This version adds IVs for livestream detection on compatible + # websites + for media_data_obj in self.media_reg_dict.values(): + if isinstance(media_data_obj, media.Video): + media_data_obj.live_mode = 0 + elif not isinstance(media_data_obj, media.Folder): + media_data_obj.rss = None + + if version < 2000042: # v2.0.042 + + # This version adds new IVs to channels, playlists and folders + for dbid in self.media_name_dict.values(): + container_obj = self.media_reg_dict[dbid] + + container_obj.live_count = 0 + + # This version also creates a new fixed folder. If there are + # existing folders with the same name, they must be renamed + if formats.FOLDER_LIVESTREAMS in self.media_name_dict: + + dbid = self.media_name_dict[formats.FOLDER_LIVESTREAMS] + media_data_obj = self.media_reg_dict[dbid] + + # Generate a new name. The -1 argument means to keep going + # indefinitely, until an available name is found + self.rename_container_silently( + media_data_obj, + utils.find_available_name(self, 'downloads', 2, -1), + ) + + # Now create the new fixed folder + self.fixed_live_folder = self.add_folder( + formats.FOLDER_LIVESTREAMS, + None, # No parent folder + False, # Allow downloads + True, # Fixed (folder cannot be removed) + True, # Private + True, # Can only contain videos + False, # Not temporary + ) + + if version < 2000105: # v2.0.105 + + # This version adds new options to options.OptionsManager, and + # deletes some existing ones + for options_obj in options_obj_list: + + options_obj.options_dict['video_format_list'] = [] + + if options_obj.options_dict['all_formats']: + options_obj.options_dict['video_format_mode'] = 'all' + options_obj.options_dict['all_formats'] = False + else: + options_obj.options_dict['video_format_mode'] = 'single' + + options_obj.options_dict.pop('second_video_format') + options_obj.options_dict.pop('third_video_format') + def save_db(self): @@ -3895,7 +4619,7 @@ def save_db(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 3839 save_db') + utils.debug_time('app 4622 save_db') # Sanity check if self.current_manager_obj \ @@ -3932,13 +4656,21 @@ def save_db(self): 'media_reg_dict': self.media_reg_dict, 'media_name_dict': self.media_name_dict, 'media_top_level_list': self.media_top_level_list, + 'media_reg_live_dict': self.media_reg_live_dict, + 'media_reg_auto_notify_dict': self.media_reg_auto_notify_dict, + 'media_reg_auto_alarm_dict': self.media_reg_auto_alarm_dict, + 'media_reg_auto_open_dict': self.media_reg_auto_open_dict, + 'media_reg_auto_dl_start_dict': self.media_reg_auto_dl_start_dict, + 'media_reg_auto_dl_stop_dict': self.media_reg_auto_dl_stop_dict, 'fixed_all_folder': self.fixed_all_folder, 'fixed_bookmark_folder': self.fixed_bookmark_folder, 'fixed_fav_folder': self.fixed_fav_folder, + 'fixed_live_folder': self.fixed_live_folder, 'fixed_new_folder': self.fixed_new_folder, 'fixed_waiting_folder': self.fixed_waiting_folder, 'fixed_temp_folder': self.fixed_temp_folder, 'fixed_misc_folder': self.fixed_misc_folder, + 'fixed_folder_locale': self.fixed_folder_locale, } # Back up any existing file @@ -3949,10 +4681,13 @@ def save_db(self): except: self.disable_load_save() self.file_error_dialogue( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' database file\n\n(Could not make a backup copy of' \ - + ' the existing file)\n\nFile load/save has been' \ - + ' disabled', + _('Failed to save the Tartube database file') \ + + '\n\n' \ + + _( + '(Could not make a backup copy of the existing file)' + ) \ + + '\n\n' \ + + _('File load/save has been disabled'), ) return False @@ -3967,10 +4702,10 @@ def save_db(self): if os.path.isfile(lock_path): self.system_error( - 101, + 103, 'Database file \'' + lock_path + '\' already exists,' \ + ' and is locked', - ) + ) return False @@ -3984,8 +4719,10 @@ def save_db(self): except: self.disable_load_save( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' database file (file already in use)', + _( + 'Failed to save the Tartube database file (file' \ + + ' already in use)', + ), ) return False @@ -4002,18 +4739,17 @@ def save_db(self): if os.path.isfile(temp_bu_path): self.file_error_dialogue( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' database file\n\n' \ - + 'A backup of the previous file can be found at:\n\n' \ - + ' ' + temp_bu_path \ - + '\n\nFile load/save has been disabled', + _('Failed to save the Tartube database file') \ + + '\n\n' \ + + _('A backup of the previous file can be found at:') \ + + '\n\n ' + temp_bu_path + '\n\n' \ + + _('File load/save has been disabled'), ) else: self.file_error_dialogue( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' database file\n\nFile load/save has been' \ - + ' disabled', + _('Failed to save the Tartube database file') \ + + '\n\n' + _('File load/save has been disabled'), ) return False @@ -4106,7 +4842,7 @@ def switch_db(self, data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4050 switch_db') + utils.debug_time('app 4845 switch_db') # Extract values from the argument list path = data_list.pop(0) @@ -4202,11 +4938,12 @@ def switch_db(self, data_list): if not os.path.isfile(db_path): # Reset main window widgets + # (Don't reset the Erors/Warnings tab, as failed attempts to load a + # database generate messages there) self.main_win_obj.video_index_reset() self.main_win_obj.video_catalogue_reset() self.main_win_obj.progress_list_reset() self.main_win_obj.results_list_reset() - self.main_win_obj.errors_list_reset() # Reset database IVs self.reset_db() @@ -4229,7 +4966,7 @@ def switch_db(self, data_list): pref_win_obj.select_switch_db_tab() self.dialogue_manager_obj.show_msg_dialogue( - 'Database file created', + _('Database file created'), 'info', 'ok', pref_win_obj, @@ -4239,7 +4976,7 @@ def switch_db(self, data_list): # (Parent window is the main window) self.dialogue_manager_obj.show_msg_dialogue( - 'Database file created', + _('Database file created'), 'info', 'ok', ) @@ -4275,7 +5012,7 @@ def choose_alt_db(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4219 choose_alt_db') + utils.debug_time('app 5015 choose_alt_db') db_file_path = os.path.abspath( os.path.join(self.data_dir, self.db_file_name), @@ -4289,8 +5026,21 @@ def choose_alt_db(self): os.path.isfile(lock_file_path) \ and not self.debug_ignore_lockfile_flag ): + self.system_warning( + 104, + _( + '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}\'', + ).format(self.data_dir, lock_file_path), + ) + for alt_data_dir in self.data_dir_alt_list: + if alt_data_dir == self.data_dir: + # Already tried this one + continue + alt_db_file_path = os.path.abspath( os.path.join(alt_data_dir, self.db_file_name), ) @@ -4326,6 +5076,18 @@ def choose_alt_db(self): return + else: + + self.system_warning( + 105, + _( + '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}\'', + ).format(alt_data_dir, alt_lock_file_path), + ) + def forget_db(self, data_list): @@ -4348,7 +5110,7 @@ def forget_db(self, data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4292 forget_db') + utils.debug_time('app 5113 forget_db') # Extract values from the argument list path = data_list.pop(0) @@ -4395,7 +5157,7 @@ def forget_all_db(self, pref_win_obj=None): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4339 forget_all_db') + utils.debug_time('app 5160 forget_all_db') # Sanity check if self.current_manager_obj or self.disable_load_save_flag: @@ -4414,49 +5176,6 @@ def forget_all_db(self, pref_win_obj=None): return True - def reorder_db(self, data_dir, down_flag=False): - - """Called by - config.SystemPrefWin.on_data_dir_move_up_button_clicked() or - .on_data_dir_move_down_button_clicked(). - - In the list of alternative data directories, moves the specified item - up or down one position. - - Args: - - data_dir (str): One of the items in self.data_dir_alt_list - - down_flag (bool): False to move up, True to move down - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4376 reorder_db') - - # Find the specified data directory's position - posn = self.data_dir_alt_list.index(data_dir) - total = len(self.data_dir_alt_list) - - if posn != -1 and total > 1: - - # Move up - if not down_flag and posn > 0: - - self.data_dir_alt_list[posn], \ - self.data_dir_alt_list[posn - 1] \ - = self.data_dir_alt_list[posn - 1], \ - self.data_dir_alt_list[posn] - - # Move down - elif down_flag and posn < (total - 1): - - self.data_dir_alt_list[posn], \ - self.data_dir_alt_list[posn + 1] \ - = self.data_dir_alt_list[posn + 1], \ - self.data_dir_alt_list[posn] - - def reset_db(self): """Called by self.switch_db(). @@ -4466,7 +5185,7 @@ def reset_db(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4410 reset_db') + utils.debug_time('app 5188 reset_db') # Reset IVs to their default states self.general_options_obj = options.OptionsManager() @@ -4474,17 +5193,24 @@ def reset_db(self): self.media_reg_dict = {} self.media_name_dict = {} self.media_top_level_list = [] + self.media_reg_live_dict = {} + self.media_reg_auto_notify_dict = {} + self.media_reg_auto_alarm_dict = {} + self.media_reg_auto_open_dict = {} + self.media_reg_auto_dl_start_dict = {} + self.media_reg_auto_dl_stop_dict = {} self.fixed_all_folder = None self.fixed_bookmark_folder = None self.fixed_fav_folder = None + self.fixed_live_folder = None self.fixed_new_folder = None self.fixed_waiting_folder = None self.fixed_temp_folder = None self.fixed_misc_folder = None - # Create new system folders (which sets the values of + # Create new fixed folders (which sets the values of # self.fixed_all_folder, etc) - self.create_system_folders() + self.create_fixed_folders() def check_integrity_db(self): @@ -4502,23 +5228,25 @@ def check_integrity_db(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4446 check_integrity_db') + utils.debug_time('app 5231 check_integrity_db') # Basic checks if self.disable_load_save_flag: self.system_error( - 102, + 106, 'Cannot check/fix database after load/save has been disabled', - ) + ) return if self.current_manager_obj: self.dialogue_manager_obj.show_msg_dialogue( - __main__.__prettyname__ + '\'s database can\'t be checked' \ - + ' while an operation is in progress', + _( + 'Tartube\'s database can\'t be checked while an operation is' \ + + ' in progress', + ), 'error', 'ok', ) @@ -4548,6 +5276,32 @@ def check_integrity_db(self): if not dbid in self.media_reg_dict: error_reg_dict[dbid] = None + # Check that entries in self.media_reg_live_dict (and its subsets) + # appear in self.media_reg_dict + for dbid in self.media_reg_live_dict.keys(): + if not dbid in self.media_reg_dict: + error_reg_dict[dbid] = None + + for dbid in self.media_reg_auto_notify_dict.keys(): + if not dbid in self.media_reg_dict: + error_reg_dict[dbid] = None + + for dbid in self.media_reg_auto_alarm_dict.keys(): + if not dbid in self.media_reg_dict: + error_reg_dict[dbid] = None + + for dbid in self.media_reg_auto_open_dict.keys(): + if not dbid in self.media_reg_dict: + error_reg_dict[dbid] = None + + for dbid in self.media_reg_auto_dl_start_dict.keys(): + if not dbid in self.media_reg_dict: + error_reg_dict[dbid] = None + + for dbid in self.media_reg_auto_dl_stop_dict.keys(): + if not dbid in self.media_reg_dict: + error_reg_dict[dbid] = None + # self.media_reg_dict contains, in theory, every video/channel/ # playlist/folder object # Walk the tree whose top level is self.media_top_level_list to get a @@ -4674,7 +5428,7 @@ def check_integrity_db(self): and not error_slave_dict: self.dialogue_manager_obj.show_msg_dialogue( - 'Database check complete, no inconsistencies found', + _('Database check complete, no inconsistencies found'), 'info', 'ok', ) @@ -4688,10 +5442,12 @@ def check_integrity_db(self): # Prompt the user before deleting stuff self.dialogue_manager_obj.show_msg_dialogue( - 'Database check complete, problems found: ' \ - + str(total) + '\n\nDo you want to repair these problems?' \ - + ' (The database will be fixed, but no files will be' \ - + ' deleted)', + _('Database check complete, problems found:') \ + + ' ' + str(total) + '\n\n' \ + + _( + 'Do you want to repair these problems? (The database will be' \ + + ' fixed, but no files will be deleted)', + ), 'question', 'yes-no', None, # Parent window is main window @@ -4742,7 +5498,7 @@ def fix_integrity_db(self, data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4686 fix_integrity_db') + utils.debug_time('app 5501 fix_integrity_db') # Extract the arguments error_reg_dict = data_list.pop(0) @@ -4766,6 +5522,24 @@ def fix_integrity_db(self, data_list): if dbid in self.media_top_level_list: self.media_top_level_list.remove(dbid) + if dbid in self.media_reg_live_dict: + del self.media_reg_live_dict[dbid] + + if dbid in self.media_reg_auto_notify_dict: + del self.media_reg_auto_notify_dict[dbid] + + if dbid in self.media_reg_auto_alarm_dict: + del self.media_reg_auto_alarm_dict[dbid] + + if dbid in self.media_reg_auto_open_dict: + del self.media_reg_auto_open_dict[dbid] + + if dbid in self.media_reg_auto_dl_start_dict: + del self.media_reg_auto_dl_start_dict[dbid] + + if dbid in self.media_reg_auto_dl_stop_dict: + del self.media_reg_auto_dl_stop_dict[dbid] + # Check each media data object's child list, and remove anything that # should be removed for media_data_obj in self.media_reg_dict.values(): @@ -4811,7 +5585,7 @@ def fix_integrity_db(self, data_list): # Show confirmation self.dialogue_manager_obj.show_msg_dialogue( - 'Database inconsistencies repaired', + _('Database inconsistencies repaired'), 'info', 'ok', ) @@ -4826,7 +5600,7 @@ def auto_delete_old_videos(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4770 auto_delete_old_videos') + utils.debug_time('app 5603 auto_delete_old_videos') if not self.auto_delete_flag: return @@ -4876,7 +5650,7 @@ def convert_version(self, version): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4820 convert_version') + utils.debug_time('app 5653 convert_version') num_list = version.split('.') if len(num_list) != 3: @@ -4886,7 +5660,62 @@ def convert_version(self, version): + int(num_list[2]) - def create_system_folders(self): + def find_sound_effects(self): + + """Called by self.start(). + + Set the directory in which sound files are stored. + + When installed via PyPI, the files are moved to ../tartube/sounds. + + When installed via a Debian/RPM package, the files are moved to + /usr/share/tartube/sounds. + + Compiles a list of paths to sound effects found in the /sounds + directory, and updates the IVs. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 5679 find_sound_effects') + + sound_dir_list = [] + sound_dir_list.append( + os.path.abspath( + os.path.join(self.script_parent_dir, 'sounds'), + ), + ) + + sound_dir_list.append( + os.path.abspath( + os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'sounds', + ), + ), + ) + + sound_dir_list.append( + os.path.join( + '/', 'usr', 'share', __main__.__packagename__, 'sounds', + ) + ) + + for sound_dir_path in sound_dir_list: + if os.path.isdir(sound_dir_path): + self.sound_dir = sound_dir_path + + # Get a list of available sound files, and sort alphabetically + for (dirpath, dir_list, file_list) in os.walk(self.sound_dir): + for filename in file_list: + if filename != 'COPYING': + self.sound_list.append(filename) + + self.sound_list.sort() + + return + + + def create_fixed_folders(self): """Called by self.start() and .reset_db(). @@ -4895,10 +5724,10 @@ def create_system_folders(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4839 create_system_folders') + utils.debug_time('app 5727 create_fixed_folders') self.fixed_all_folder = self.add_folder( - 'All Videos', + formats.FOLDER_ALL_VIDEOS, None, # No parent folder False, # Allow downloads True, # Fixed (folder cannot be removed) @@ -4908,7 +5737,7 @@ def create_system_folders(self): ) self.fixed_bookmark_folder = self.add_folder( - 'Bookmarks', + formats.FOLDER_BOOKMARKS, None, # No parent folder False, # Allow downloads True, # Fixed (folder cannot be removed) @@ -4918,7 +5747,7 @@ def create_system_folders(self): ) self.fixed_fav_folder = self.add_folder( - 'Favourite Videos', + formats.FOLDER_FAVOURITE_VIDEOS, None, # No parent folder False, # Allow downloads True, # Fixed (folder cannot be removed) @@ -4928,8 +5757,18 @@ def create_system_folders(self): ) self.fixed_fav_folder.set_fav_flag(True) + self.fixed_live_folder = self.add_folder( + formats.FOLDER_LIVESTREAMS, + None, # No parent folder + False, # Allow downloads + True, # Fixed (folder cannot be removed) + True, # Private + True, # Can only contain videos + False, # Not temporary + ) + self.fixed_new_folder = self.add_folder( - 'New Videos', + formats.FOLDER_NEW_VIDEOS, None, # No parent folder False, # Allow downloads True, # Fixed (folder cannot be removed) @@ -4939,7 +5778,7 @@ def create_system_folders(self): ) self.fixed_waiting_folder = self.add_folder( - 'Waiting Videos', + formats.FOLDER_WAITING_VIDEOS, None, # No parent folder False, # Allow downloads True, # Fixed (folder cannot be removed) @@ -4949,7 +5788,7 @@ def create_system_folders(self): ) self.fixed_temp_folder = self.add_folder( - 'Temporary Videos', + formats.FOLDER_TEMPORARY_VIDEOS, None, # No parent folder False, # Allow downloads True, # Fixed (folder cannot be removed) @@ -4959,7 +5798,7 @@ def create_system_folders(self): ) self.fixed_misc_folder = self.add_folder( - 'Unsorted Videos', + formats.FOLDER_UNSORTED_VIDEOS, None, # No parent folder False, # Allow downloads True, # Fixed (folder cannot be removed) @@ -4969,6 +5808,103 @@ def create_system_folders(self): ) + def rename_fixed_folders(self): + + """Called by self.load_db() (only). + + If the locale used when saving the database file has changed then, + having loaded the file, we can rename all the fixed folders to match + the new locale. + + This function must only be called for that reason; fixed folders cannot + otherwise be renamed. + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 5825 rename_fixed_folders') + + self.rename_fixed_folder( + self.fixed_all_folder, + formats.FOLDER_ALL_VIDEOS, + ) + + self.rename_fixed_folder( + self.fixed_bookmark_folder, + formats.FOLDER_BOOKMARKS, + ) + + self.rename_fixed_folder( + self.fixed_fav_folder, + formats.FOLDER_FAVOURITE_VIDEOS, + ) + + self.rename_fixed_folder( + self.fixed_live_folder, + formats.FOLDER_LIVESTREAMS, + ) + + self.rename_fixed_folder( + self.fixed_new_folder, + formats.FOLDER_NEW_VIDEOS, + ) + + self.rename_fixed_folder( + self.fixed_waiting_folder, + formats.FOLDER_WAITING_VIDEOS, + ) + + self.rename_fixed_folder( + self.fixed_temp_folder, + formats.FOLDER_TEMPORARY_VIDEOS, + ) + + self.rename_fixed_folder( + self.fixed_misc_folder, + formats.FOLDER_UNSORTED_VIDEOS, + ) + + + def rename_fixed_folder(self, media_data_obj, new_name): + + """Called by self.rename_fixed_folders() (only). + + Renames the specified media.Folder object to match the new locale. + + Args: + + media_data_obj (media.Folder): The folder to rename + + new_name (str): The folder's new name, matching (for example) + formats.FOLDER_ALL_VIDEOS, formats.FOLDER_BOOKMARKS, etc + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 5884 rename_fixed_folder') + + # If there is (by chance) a folder with the same name, it must be + # renamed + if new_name in self.media_name_dict: + + other_dbid = self.media_name_dict[new_name] + other_obj = self.media_reg_dict[other_dbid] + + # Sanity check: don't rename another fixed folder + if isinstance(other_obj, media.Folder) and other_obj.fixed_flag: + return + + # Generate a new name. The -1 argument means to keep going + # indefinitely, until an available name is found + self.rename_container_silently( + other_obj, + utils.find_available_name(self, other_obj.name, 2, -1), + ) + + # Now rename the specified folder + self.rename_container_silently(media_data_obj, new_name) + + def delete_temp_folders(self): """Called by self.stop_continue() and self.load_db(). @@ -4978,7 +5914,7 @@ def delete_temp_folders(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4922 delete_temp_folders') + utils.debug_time('app 5917 delete_temp_folders') # (Must compile a list of top-level container objects first, or Python # will complain about the dictionary changing size) @@ -5016,7 +5952,7 @@ def open_temp_folders(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4960 open_temp_folders') + utils.debug_time('app 5955 open_temp_folders') for dbid in self.media_name_dict.values(): media_data_obj = self.media_reg_dict[dbid] @@ -5048,7 +5984,7 @@ def disable_load_save(self, error_msg=None, lock_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 4992 disable_load_save') + utils.debug_time('app 5987 disable_load_save') # Ignore subsequent calls to this function; only the initial error # is of interest @@ -5073,7 +6009,7 @@ def remove_db_lock_file(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5017 remove_db_lock_file') + utils.debug_time('app 6012 remove_db_lock_file') if self.db_lock_file_path is not None: @@ -5093,7 +6029,7 @@ def remove_stale_lock_file(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5037 remove_stale_lock_file') + utils.debug_time('app 6032 remove_stale_lock_file') lock_path = os.path.abspath( os.path.join(self.data_dir, self.db_file_name + '.lock'), @@ -5105,8 +6041,7 @@ def remove_stale_lock_file(self): def file_error_dialogue(self, msg): - """Called by self.start(), load_config(), .save_config(), load_db() and - .save_db(). + """Called by self.start(), .save_config(), load_db() and .save_db(). After a failure to load/save a file, display a dialogue window if the main window is open, or write to the terminal if not. @@ -5118,7 +6053,7 @@ def file_error_dialogue(self, msg): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5062 file_error_dialogue') + utils.debug_time('app 6056 file_error_dialogue') if self.main_win_obj and self.dialogue_manager_obj: self.dialogue_manager_obj.show_msg_dialogue(msg, 'error', 'ok') @@ -5157,7 +6092,7 @@ def make_directory(self, dir_path): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5101 make_directory') + utils.debug_time('app 6095 make_directory') try: os.makedirs(dir_path) @@ -5190,7 +6125,7 @@ def move_backup_files(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5134 move_backup_files') + utils.debug_time('app 6128 move_backup_files') for filename in os.listdir(path=self.data_dir): if re.search(r'^tartube_BU_.*\.db$', filename): @@ -5223,7 +6158,7 @@ def notify_user_of_data_dir(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5167 notify_user_of_data_dir') + utils.debug_time('app 6161 notify_user_of_data_dir') if os.name == 'nt': @@ -5231,7 +6166,7 @@ def notify_user_of_data_dir(self): # C:\msys64\home\USERNAME\tartube-data, which is not very # convenient. Force the user to nominate the directory they want dialogue_win = mainwin.SetDirectoryDialogue_MSWin( - self.main_win_obj, + self.fake_main_win_obj, ) dialogue_win.run() @@ -5245,7 +6180,7 @@ def notify_user_of_data_dir(self): # data directory specified by self.data_dir, or specifying their # own data directory dialogue_win = mainwin.SetDirectoryDialogue_LinuxBSD( - self.main_win_obj, + self.fake_main_win_obj, self.data_dir, ) @@ -5281,16 +6216,18 @@ def prompt_user_for_data_dir(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5225 prompt_user_for_data_dir') + utils.debug_time('app 6219 prompt_user_for_data_dir') - if os.name == 'nt': - folder = 'folder' + # If the main window hasn't been created yet, use the fake main + # window created by self.start() + if self.main_win_obj: + parent_win_obj = self.main_win_obj else: - folder = 'directory' + parent_win_obj = self.fake_main_win_obj file_chooser_win = Gtk.FileChooserDialog( - 'Please select ' + __main__.__prettyname__ + '\'s data ' + folder, - self.main_win_obj, + _('Please select Tartube\'s data folder'), + parent_win_obj, Gtk.FileChooserAction.SELECT_FOLDER, ( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, @@ -5298,6 +6235,7 @@ def prompt_user_for_data_dir(self): ), ) + # Get the user's response response = file_chooser_win.run() if response == Gtk.ResponseType.OK: @@ -5349,10 +6287,6 @@ def download_manager_start(self, operation_type, \ """Can be called by anything. - When the user clicks the 'Check all' or 'Download all' buttons (or - their equivalents in the main window's menu or toolbar), initiate a - download operation. - Creates a new downloads.DownloadManager object to handle the download operation. When the operation is complete, self.download_manager_finished() is called. @@ -5364,9 +6298,11 @@ def download_manager_start(self, operation_type, \ if videos should be downloaded (or not) depending on each media data object's .dl_sim_flag IV. 'custom' is like 'real', but with additional options applied (specified by IVs like - self.custom_dl_by_video_flag) + self.custom_dl_by_video_flag). 'classic' if the Classic Mode + Tab is open, and the user has clicked the download button there - automatic_flag (bool): True when called by self.start() or + automatic_flag (bool): True when called by + self.script_fast_timer_callback() or self.script_slow_timer_callback(). If the download operation does not start, no dialogue window is displayed (as it normally would be) @@ -5375,19 +6311,34 @@ def download_manager_start(self, operation_type, \ media.Playlist and/or media.Folder objects. If not an empty list, only those media data objects and their descendants are checked/downloaded. If an empty list, all media data objects - are checked/downloaded + are checked/downloaded. If operation_type is 'classic', then + the media_data_list contains a list of dummy media.Video + objects from a previous call to this function. If an empty + list, all dummy media.Video objects are downloaded """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5324 download_manager_start') + utils.debug_time('app 6322 download_manager_start') + + # The operation may have been scheduled to begin on startup. For + # 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 + + # If a livestream operation is running, tell it to stop immediately + if self.livestream_manager_obj: + self.livestream_manager_obj.stop_livestream_operation() + # 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 if not automatic_flag: self.system_error( - 103, + 107, 'Download, update, refresh, info or tidy operation' \ + ' already in progress', ) @@ -5400,8 +6351,10 @@ def download_manager_start(self, operation_type, \ # open if not automatic_flag: self.dialogue_manager_obj.show_msg_dialogue( + _( 'A download operation cannot start if one or more' \ + ' configuration windows are still open', + ), 'error', 'ok', ) @@ -5422,10 +6375,9 @@ def download_manager_start(self, operation_type, \ # Refuse to proceed with the operation if not automatic_flag: self.dialogue_manager_obj.show_msg_dialogue( - 'You only have ' + str(disk_space) + ' / ' \ - + str(total_space) + 'Mb remaining on your device', - 'error', - 'ok', + _( + 'You only have {0} / {1} Mb remaining on your device', + ).format(str(disk_space), str(total_space)), ) return @@ -5445,9 +6397,11 @@ def download_manager_start(self, operation_type, \ # Warn the user that their free disk space is running low, and # get confirmation before starting the download operation self.dialogue_manager_obj.show_msg_dialogue( - 'You only have ' + str(disk_space) + ' / ' \ - + str(total_space) + 'Mb remaining on your device.' \ - + '\n\nAre you sure you want to continue?', + _( + 'You only have {0} / {1} Mb remaining on your device', + ).format(str(disk_space), str(total_space)) \ + + '\n\n' \ + + _('Are you sure you want to continue?'), 'question', 'yes-no', None, # Parent window is main window @@ -5488,13 +6442,15 @@ def download_manager_continue(self, arg_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5432 download_manager_continue') + utils.debug_time('app 6445 download_manager_continue') # Extract arguments from arg_list operation_type = arg_list.pop(0) automatic_flag = arg_list.pop(0) media_data_list = arg_list.pop(0) + # When not called by the Classic Mode Tab: + # # The media data registry consists of a collection of media data # objects (media.Video, media.Channel, media.Playlist and # media.Folder) @@ -5505,6 +6461,17 @@ def download_manager_continue(self, arg_list): # downloads.DownloadItem object # Those downloads.DownloadItem objects are collectively stored in a # downloads.DownloadList object + # + # When called by the Classic Mode Tab: + # + # The user has added one or more URLs to the tab's download list and, + # in response, Tartube has created a number of dummy media.Video + # objects (which have not been added to the media data registry). + # Each dummy object corresponds to a single URL (which might + # represent a video, channel or playlist) + # If a list of dummy media.Video objects was specified by the calling + # function, they are downloaded. Otherwise all dummy media.Video + # objects are downloaded download_list_obj = downloads.DownloadList( self, operation_type, @@ -5515,9 +6482,9 @@ def download_manager_continue(self, arg_list): if not automatic_flag: if operation_type == 'sim': - msg = 'There is nothing to check!' + msg = _('There is nothing to check!') else: - msg = 'There is nothing to download!' + msg = _('There is nothing to download!') self.dialogue_manager_obj.show_msg_dialogue(msg, 'error', 'ok') @@ -5552,10 +6519,15 @@ def download_manager_continue(self, arg_list): self.no_dialogue_this_time_flag = True # During a download operation, show a progress bar in the Videos Tab - if operation_type == 'sim': - self.main_win_obj.show_progress_bar('check') + # (except when launched from the Classic Mode Tab, in which case we + # just desensitise the existing buttons) + if operation_type != 'classic': + if operation_type == 'sim': + self.main_win_obj.show_progress_bar('check') + else: + self.main_win_obj.show_progress_bar('download') else: - self.main_win_obj.show_progress_bar('download') + self.main_win_obj.sensitise_progress_bar(False) # Reset the Progress List self.main_win_obj.progress_list_reset() @@ -5563,9 +6535,15 @@ def download_manager_continue(self, arg_list): self.main_win_obj.results_list_reset() # Reset the Output Tab self.main_win_obj.output_tab_reset_pages() - # Initialise the Progress List with one row for each media data object - # in the downloads.DownloadList object - self.main_win_obj.progress_list_init(download_list_obj) + + if operation_type != 'classic': + + # Initialise the Progress List with one row for each media data + # object in the downloads.DownloadList object + # (The Classic Progress List, if in use, has already been + # initialised) + self.main_win_obj.progress_list_init(download_list_obj) + # (De)sensitise other widgets, as appropriate self.main_win_obj.sensitise_operation_widgets(False) # Make the widget changes visible @@ -5573,10 +6551,13 @@ def download_manager_continue(self, arg_list): # During a download operation, a GObject timer runs, so that the # Progress Tab and Output Tab can be updated at regular intervals - # There is also a delay between the instant at which youtube-dl - # reports a video file has been downloaded, and the instant at which - # it appears in the filesystem. The timer checks for newly-existing + # There is also a delay between the instant at which youtube-dl reports + # a video file has been downloaded, and the instant at which it + # appears in the filesystem. The timer checks for newly-existing # files at regular intervals, too + # (When called from the Classic Mode Tab, we use a similar GObject + # timer that updates only the list in that tab) + # # Create the timer self.dl_timer_id = GObject.timeout_add( self.dl_timer_time, @@ -5607,7 +6588,7 @@ def download_manager_halt_timer(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5551 download_manager_halt_timer') + utils.debug_time('app 6591 download_manager_halt_timer') if self.dl_timer_id: self.dl_timer_check_time \ @@ -5624,7 +6605,14 @@ def download_manager_finished(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5568 download_manager_finished') + utils.debug_time('app 6608 download_manager_finished') + + # This function behaves differently, if the download operation was + # launched from the Classic Mode Tab + if self.download_manager_obj.operation_type != 'classic': + classic_mode_flag = False + else: + classic_mode_flag = True # Get the time taken by the download operation, so we can convert it # into a nice string below (e.g. '05:15') @@ -5653,41 +6641,50 @@ def download_manager_finished(self): # empty this list) self.watch_after_dl_list = [] - # After a download operation, save files, if allowed - if self.operation_save_flag: + # After a download operation, save files, if allowed (but don't bother + # when launched from the Classic Mode Tab) + if not classic_mode_flag and self.operation_save_flag: self.save_config() self.save_db() # After a download operation, 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() - # If lines in the Progress should be hidden, hide any remaining lines - if self.progress_list_hide_flag: - self.main_win_obj.progress_list_check_hide_rows(True) - # (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 not classic_mode_flag: + + # Remove the progress bar in the Videos Tab + self.main_win_obj.hide_progress_bar() + + # If remaining lines in the Progress List should be hidden, hide + # them + if self.progress_list_hide_flag: + self.main_win_obj.progress_list_check_hide_rows(True) + + else: + + # No progress bar exists; just resensitise the existing buttons + self.main_win_obj.sensitise_progress_bar(True) + + # (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() - # Reset operation IVs - self.operation_halted_flag = False - - # If updates to the Video Index were disabled because of Gtk issues, - # we must now redraw the Video Index and Video Catalogue from - # scratch - if self.gtk_broken_flag or self.gtk_emulate_broken_flag: + # If updates to the Video Index were disabled because of Gtk issues, we + # must now redraw the Video Index and Video Catalogue from scratch + if not classic_mode_flag \ + and (self.gtk_broken_flag or self.gtk_emulate_broken_flag): # Redraw the Video Index and Video Catalogue, re-selecting the # current selection, if any self.main_win_obj.video_index_catalogue_reset(True) - # If the youtube-dl archive file was temporarily renamed to enable a - # video to be re-downloaded (by + # If the youtube-dl archive file(s) were temporarily renamed to enable + # video(s) to be re-downloaded (by # mainwin.MainWin.on_video_catalogue_re_download() ), restore the - # archive file's original name + # archive file(s) original names self.reset_backup_archive() # If Tartube is due to shut down, then shut it down @@ -5698,12 +6695,12 @@ def download_manager_finished(self): elif not self.no_dialogue_this_time_flag: if not self.operation_halted_flag: - msg = 'Download operation complete' + msg = _('Download operation complete') else: - msg = 'Download operation halted' + msg = _('Download operation halted') if time_num >= 10: - msg += '\n\nTime taken: ' \ + msg += '\n\n' + _('Time taken:') + ' ' \ + utils.convert_seconds_to_string(time_num, True) if self.operation_dialogue_mode == 'dialogue': @@ -5714,6 +6711,8 @@ def download_manager_finished(self): # In any case, reset those IVs self.halt_after_operation_flag = False self.no_dialogue_this_time_flag = False + # Also reset operation IVs + self.operation_halted_flag = False def update_manager_start(self, update_type): @@ -5738,12 +6737,18 @@ def update_manager_start(self, update_type): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5682 update_manager_start') + utils.debug_time('app 6740 update_manager_start') + # If a livestream operation is running, tell it to stop immediately + if self.livestream_manager_obj: + self.livestream_manager_obj.stop_livestream_operation() + + # 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 return self.system_error( - 104, + 108, 'Download, update, refresh, info or tidy operation already' \ + ' in progress', ) @@ -5752,8 +6757,10 @@ def update_manager_start(self, update_type): # Update operation is not allowed when a configuration window is # open self.dialogue_manager_obj.show_msg_dialogue( + _( 'An update operation cannot start if one or more' \ + ' configuration windows are still open', + ), 'error', 'ok', ) @@ -5765,9 +6772,8 @@ def update_manager_start(self, update_type): # not be possible to call this function, but we'll show an error # message anyway return self.system_error( - 105, - 'Update operations are disabled in this version of ' \ - + __main__.__prettyname__, + 109, + 'Update operations are disabled in this version of Tartube', ) elif update_type == 'ffmpeg' and os.name != 'nt': @@ -5775,7 +6781,7 @@ def update_manager_start(self, update_type): # installation of Tartube. It should not be possible to call this # function, but we'll show an error message anyway return self.system_error( - 106, + 110, 'Update operation cannot install FFmpeg on your operating' \ + ' system', ) @@ -5813,7 +6819,7 @@ def update_manager_halt_timer(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5757 update_manager_halt_timer') + utils.debug_time('app 6822 update_manager_halt_timer') if self.update_timer_id: self.update_timer_check_time \ @@ -5829,7 +6835,7 @@ def update_manager_finished(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5773 update_manager_finished') + utils.debug_time('app 6838 update_manager_finished') # Import IVs from updates.UpdateManager, before it is destroyed update_type = self.update_manager_obj.update_type @@ -5865,21 +6871,22 @@ def update_manager_finished(self): if update_type == 'ffmpeg': if not success_flag: - msg = 'Installation failed' + msg = _('Installation failed') else: - msg = 'Installation complete' + msg = _('Installation complete') else: if not success_flag: - msg = 'Update operation failed' + msg = _('Update operation failed') elif self.operation_halted_flag: - msg = 'Update operation halted' + msg = _('Update operation halted') else: - msg = 'Update operation complete' + msg = _('Update operation complete') \ + + '\n\n' + _('youtube-dl version:') + ' ' if ytdl_version is not None: - msg += '\n\nyoutube-dl version: ' + ytdl_version + msg += ytdl_version else: - msg += '\n\nyoutube-dl version: (unknown)' + msg += _('(unknown)') if self.operation_dialogue_mode == 'dialogue': self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') @@ -5926,12 +6933,17 @@ def refresh_manager_start(self, media_data_obj=None): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5870 refresh_manager_start') + utils.debug_time('app 6936 refresh_manager_start') + # If a livestream operation is running, tell it to stop immediately + if self.livestream_manager_obj: + self.livestream_manager_obj.stop_livestream_operation() + + # 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 return self.system_error( - 107, + 111, 'Download, update, refresh, info or tidy operation already' \ + ' in progress', ) @@ -5939,7 +6951,7 @@ def refresh_manager_start(self, media_data_obj=None): elif media_data_obj is not None \ and isinstance(media_data_obj, media.Video): return self.system_error( - 108, + 112, 'Refresh operation cannot be applied to an individual video', ) @@ -5947,8 +6959,10 @@ def refresh_manager_start(self, media_data_obj=None): # Refresh operation is not allowed when a configuration window is # open self.dialogue_manager_obj.show_msg_dialogue( + _( 'A refresh operation cannot start if one or more' \ + ' configuration windows are still open', + ), 'error', 'ok', ) @@ -5958,32 +6972,50 @@ def refresh_manager_start(self, media_data_obj=None): # The user might not be aware of what a refresh operation is, or the # effect it might have on Tartube's database # Warn them, and give them the opportunity to back out - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' + msg = _( + 'During a refresh operation, Tartube analyses its data folder,' \ + + ' looking for videos that haven\'t yet been added to its' \ + + ' database', + ) + '\n\n' + _( + 'You only need to perform a refresh operation if you have' \ + + ' manually copied videos into Tartube\'s data folder', + ) + '\n\n' if not media_data_obj: - string = 'click the \'Check all\' button in the main window.\n\n' + + msg += _( + 'Before starting a refresh operation, you should click the' \ + + ' \'Check all\' button in the main window', + ) + elif isinstance(media_data_obj, media.Channel): - string = ' right-click the channel and select \'Check channel\'' \ - + '.\n\n' + + msg += _( + 'Before starting a refresh operation, you should right-click' \ + + ' the channel and select \'Check channel\'', + ) + elif isinstance(media_data_obj, media.Playlist): - string = ' right-click the playlist and select \'Check' \ - + ' playlist\'.\n\n' + + msg += _( + 'Before starting a refresh operation, you should right-click' \ + + ' the playlist and select \'Check playlist\'', + ) + else: - string = ' right-click the folder and select \'Check folder\'' \ - + '.\n\n' + + msg += _( + 'Before starting a refresh operation, you should right-click' \ + + ' the folder and select \'Check folder\'', + ) + + msg += '\n\n' + _( + 'Are you sure you want to proceed with the refresh operation?', + ) + self.dialogue_manager_obj.show_msg_dialogue( - 'During a refresh operation, ' + __main__.__prettyname__ \ - + ' analyses its data ' + folder + ', looking for videos that' \ - + ' haven\'t yet been added to its database.\n\n' \ - + 'You only need to perform a refresh operation if you have' \ - + ' manually copied videos into ' + __main__.__prettyname__ \ - + '\'s data ' + folder + '.\n\n' \ - + 'Before starting a refresh operation, you should ' + string \ - + 'Are you sure you want to procede with the refresh operation?', + msg, 'question', 'yes-no', None, # Parent window is main window @@ -6012,7 +7044,7 @@ def refresh_manager_continue(self, media_data_obj=None): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 5956 refresh_manager_continue') + utils.debug_time('app 7047 refresh_manager_continue') # For earlier versions of Gtk, refresh operations on a channel/ # playlist/folder cause frequent crashes. We can work around that by @@ -6059,7 +7091,7 @@ def refresh_manager_halt_timer(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6003 refresh_manager_halt_timer') + utils.debug_time('app 7094 refresh_manager_halt_timer') if self.refresh_timer_id: self.refresh_timer_check_time \ @@ -6075,7 +7107,7 @@ def refresh_manager_finished(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6019 refresh_manager_finished') + utils.debug_time('app 7110 refresh_manager_finished') # Get the time taken by the refresh operation, so we can convert it # into a nice string below (e.g. '05:15') @@ -6130,12 +7162,12 @@ def refresh_manager_finished(self): if self.operation_dialogue_mode != 'default': if not self.operation_halted_flag: - msg = 'Refresh operation complete' + msg = _('Refresh operation complete') else: - msg = 'Refresh operation halted' + msg = _('Refresh operation halted') if time_num >= 10: - msg += '\n\nTime taken: ' \ + msg += '\n\n' + _('Time taken:') + ' ' \ + utils.convert_seconds_to_string(time_num, True) if self.operation_dialogue_mode == 'dialogue': @@ -6192,12 +7224,17 @@ def info_manager_start(self, info_type, media_data_obj=None, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6136 info_manager_start') + utils.debug_time('app 7227 info_manager_start') + + # If a livestream operation is running, tell it to stop immediately + if self.livestream_manager_obj: + self.livestream_manager_obj.stop_livestream_operation() + # 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 return self.system_error( - 109, + 113, 'Download, update, refresh, info or tidy operation already' \ + ' in progress', ) @@ -6207,7 +7244,7 @@ def info_manager_start(self, info_type, media_data_obj=None, and info_type != 'test_ytdl': # Unrecognised argument return self.system_error( - 110, + 114, 'Invalid info operation argument', ) @@ -6218,20 +7255,23 @@ def info_manager_start(self, info_type, media_data_obj=None, ): # Unusable media data object return self.system_error( - 111, + 115, 'Wrong media data object type or missing source', ) elif self.main_win_obj.config_win_list: # Info operation is not allowed when a configuration window is open - if not automatic_flag: - self.dialogue_manager_obj.show_msg_dialogue( - 'An info operation cannot start if one or more' \ - + ' configuration windows are still open', - 'error', - 'ok', - ) + self.dialogue_manager_obj.show_msg_dialogue( + _( + 'An info operation cannot start if one or more' \ + + ' configuration windows are still open', + ), + 'error', + 'ok', + ) + + return # During an info operation, certain widgets are modified and/or # desensitised @@ -6283,7 +7323,7 @@ def info_manager_halt_timer(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6227 info_manager_halt_timer') + utils.debug_time('app 7326 info_manager_halt_timer') if self.info_timer_id: self.info_timer_check_time \ @@ -6298,7 +7338,7 @@ def info_manager_finished(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6242 info_manager_finished') + utils.debug_time('app 7341 info_manager_finished') # Import IVs from info.InfoManager, before it is destroyed info_type = self.info_manager_obj.info_type @@ -6337,11 +7377,11 @@ def info_manager_finished(self): if self.operation_dialogue_mode != 'default': if not success_flag: - msg = 'Operation failed' + msg = _('Operation failed') else: - msg = 'Operation complete' + msg = _('Operation complete') - msg += '\n\nClick the Output Tab to see the results' + msg += '\n\n' + _('Click the Output Tab to see the results') if self.operation_dialogue_mode == 'dialogue': self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') @@ -6418,12 +7458,17 @@ def tidy_manager_start(self, choices_dict): if DEBUG_FUNC_FLAG: - utils.debug_time('app 6362 tidy_manager_start') + utils.debug_time('app 7461 tidy_manager_start') + + # If a livestream operation is running, tell it to stop immediately + if self.livestream_manager_obj: + self.livestream_manager_obj.stop_livestream_operation() + # 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 return self.system_error( - 112, + 116, 'Download, update, refresh, info or tidy operation already' \ + ' in progress', ) @@ -6433,12 +7478,16 @@ def tidy_manager_start(self, choices_dict): # Tidy operation is not allowed when a configuration window is open if not automatic_flag: self.dialogue_manager_obj.show_msg_dialogue( + _( 'A tidy operation cannot start if one or more' \ + ' configuration windows are still open', + ), 'error', 'ok', ) + return + # For earlier versions of Gtk, tidy operations on a channel/ # playlist/folder cause frequent crashes. We can work around that by # resetting the Video Index and Video Catalogue @@ -6483,7 +7532,7 @@ def tidy_manager_halt_timer(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6427 tidy_manager_halt_timer') + utils.debug_time('app 7535 tidy_manager_halt_timer') if self.tidy_timer_id: self.tidy_timer_check_time \ @@ -6498,7 +7547,7 @@ def tidy_manager_finished(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6442 tidy_manager_finished') + utils.debug_time('app 7550 tidy_manager_finished') # Get the time taken by the tidy operation, so we can convert it into a # nice string below (e.g. '05:15') @@ -6559,12 +7608,12 @@ def tidy_manager_finished(self): if self.operation_dialogue_mode != 'default': if not self.operation_halted_flag: - msg = 'Tidy operation complete' + msg = _('Tidy operation complete') else: - msg = 'Tidy operation halted' + msg = _('Tidy operation halted') if time_num >= 10: - msg += '\n\nTime taken: ' \ + msg += '\n\n' + _('Time taken:') + ' ' \ + utils.convert_seconds_to_string(time_num, True) if self.operation_dialogue_mode == 'dialogue': @@ -6576,6 +7625,190 @@ def tidy_manager_finished(self): self.operation_halted_flag = False + def livestream_manager_start(self): + + """Can be called by anything. + + Initiates a livestream operation to check the status of all media.Video + objects marked as livestreams (everything in self.media_reg_live_dict). + + This is one by telling youtube-dl to fetch the video's JSON data. + + If a waiting livestream has started, the data is received (otherwise an + error is received). + + If a current livestream has finished, the JSON data will say so. + + Creates a new downloads.LivestreamManager object to handle the + livestream operation. When the operation is complete, + self.livestream_manager_finished() is called. + + """ + + 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: + if self.current_manager_obj \ + or self.livestream_manager_obj \ + or self.main_win_obj.config_win_list \ + or not self.media_reg_live_dict: + + # Don't show a dialogue window as we would for other operations, as + # the livestream operation occurs silently + return + + # For the benefit of future scheduled livestream operations, set the + # 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 + # (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) + self.livestream_manager_obj = downloads.LivestreamManager(self) + + + def livestream_manager_finished(self): + + """Called by downloads.LivestreamManager.run(). + + The livestream operation has finished, so update IVs and main window + widgets. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 7685 livestream_manager_finished') + + # The operation generated three dictionaries of videos whose livestream + # status has changed + # Before destroying the downloads.LivestreamManager object, import them + video_started_dict \ + = self.livestream_manager_obj.video_started_dict.copy() + video_stopped_dict \ + = self.livestream_manager_obj.video_stopped_dict.copy() + video_missing_dict \ + = self.livestream_manager_obj.video_missing_dict.copy() + + # Any code can check whether livestream operation is in progress, or + # not, by checking this IV + self.livestream_manager_obj = None + + # Any videos marked as missing can be removed from the media registry + for video_obj in video_missing_dict.values(): + + # The True argument tells the function to delete files associated + # with the video (the thumbnail, in this case) + self.delete_video(video_obj, True) + + # Any videos whose livestream status has changed must be redrawn in + # the Video catalogue + if self.main_win_obj.video_index_current \ + == self.fixed_live_folder.name: + + # Livestreams folder visible; just redraw it + self.main_win_obj.video_catalogue_redraw_all( + self.fixed_live_folder.name, + ) + + else: + + for video_obj in video_started_dict.values(): + + if video_obj.dbid in self.media_reg_dict: + self.main_win_obj.video_catalogue_update_row(video_obj) + + for video_obj in video_stopped_dict.values(): + + if video_obj.dbid in self.media_reg_dict: + self.main_win_obj.video_catalogue_update_row(video_obj) + + # Notify the user and/or open videos in the system's web browser, if + # a waiting livestream has just gone live (and if allowed to do so) + for video_obj in video_started_dict.values(): + + if video_obj.dbid in self.media_reg_dict: + + # Use the video's thumbnail as the notification icon, if + # available (or None, if not, in which case a generic icon is + # automatically used) + if video_obj.dbid in self.media_reg_auto_notify_dict: + self.main_win_obj.notify_desktop( + _('Livestream has started'), + video_obj.name, + utils.find_thumbnail(self, video_obj), + video_obj.source, + ) + + if video_obj.dbid in self.media_reg_auto_open_dict \ + and video_obj.source: + utils.open_file(video_obj.source) + + # Play a sound effect (but only one) if any waiting livestream has + # gone live + if video_started_dict: + self.play_sound() + + # If the livestream has just started or just stopped, download it (if + # required to do so) + # First compile a dictionary to eliminate duplicate videos + dl_dict = {} + for video_obj in video_started_dict.values(): + if video_obj.dbid in self.media_reg_auto_dl_start_dict: + dl_dict[video_obj.dbid] = video_obj + + for video_obj in video_stopped_dict.values(): + if video_obj.dbid in self.media_reg_auto_dl_stop_dict: + dl_dict[video_obj.dbid] = video_obj + + # If the livestream was downloaded when it was still + # broadcasting, then a new download must overwrite the + # original file + # As of April 2020, the youtube-dl --yes-overwrites option is + # still not available, so as a temporary measure we will + # rename the original file (in case the download fails) + self.prepare_overwrite_video(video_obj) + + # Then download the videos + if dl_dict: + + if not self.download_manager_obj: + + # Start a new download operation + self.download_manager_start( + 'real', + False, + list(dl_dict.values()), + ) + + else: + + # Download operation already in progress (unlikely, but + # possible) + for video_obj in dl_dict.values(): + + download_item_obj \ + = self.download_manager_obj.download_list_obj.create_item( + video_obj, + True, + ) + + if download_item_obj: + + # Add a row to the Progress List + self.main_win_obj.progress_list_add_row( + download_item_obj.item_id, + video_obj, + ) + + # Update the main window's progress bar + self.download_manager_obj.nudge_progress_bar() + + # (Download operation support functions) def create_video_from_download(self, download_item_obj, dir_path, \ @@ -6613,7 +7846,7 @@ def create_video_from_download(self, download_item_obj, dir_path, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6557 create_video_from_download') + utils.debug_time('app 7849 create_video_from_download') # The downloads.DownloadItem handles a download for a video, a channel # or a playlist @@ -6678,6 +7911,18 @@ def create_video_from_download(self, download_item_obj, dir_path, \ # immediately video_obj.set_file(filename, extension) + # If the video is marked as a livestream, then the livestream has + # finished + if video_obj.live_mode: + + self.mark_video_live( + video_obj, + 0, # Not a livestream + True, # Don't update Video Index yet + True, # Don't update Video Catalogue yet + no_sort_flag, + ) + # If the video is in a channel or a playlist, assume that youtube-dl is # supplying a list of videos in the order of upload, newest first - # in which case, now is a good time to set the video's .receive_time @@ -6729,7 +7974,7 @@ def convert_video_from_download(self, container_obj, options_manager_obj, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6673 convert_video_from_download') + utils.debug_time('app 7977 convert_video_from_download') # Does the container object already contain this video? video_obj = None @@ -6806,7 +8051,7 @@ def announce_video_download(self, download_item_obj, video_obj, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6750 announce_video_download') + utils.debug_time('app 8054 announce_video_download') # If the video's parent media data object (a channel, playlist or # folder) is selected in the Video Index, update the Video Catalogue @@ -6824,6 +8069,79 @@ def announce_video_download(self, download_item_obj, video_obj, \ ) + def create_livestream_from_download(self, container_obj, live_mode, + video_name, video_source, video_descrip, video_upload_time): + + """Called by downloads.JSONFetcher.do_download(). + + A modified form of self.create_video_from_download(), called at the end + of a download operation when the RSS feed for a channel or playlist is + checked, and contains an unfamiliar video (indicating that it's a + livestream). + + Creates a new media.Video object for the livestream. + + Args: + + containe_obj (media.Channel or media.Playlist): The channel or + playlist in which a livestream has been detected + + live_mode (int): Matches media.Video.live_mode: 1 for a waiting + livestream, 2 for a livestream that has started + + video_name, video_source, video_descrip (str): Information about + the detected livestream, grabbed from the RSS feed itself + + video_upload_time (int): The video's upload time (in Unix time, to + match media.Video.upload_time) + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 8101 create_livestream_from_download') + + # Fetch the options.OptionsManager object that applies to the container + options_manager_obj = utils.get_options_manager(self, container_obj) + + # Create a new media data object for the video + override_name = options_manager_obj.options_dict['use_fixed_folder'] + if override_name is not None and override_name in self.media_name_dict: + + other_dbid = self.media_name_dict[override_name] + container_obj = self.media_reg_dict[other_dbid] + + video_obj = self.add_video( + container_obj, + video_source, + False, # Not a simulated download + True, # Let the calling function sort the container + ) + + # Update its IVs + video_obj.set_receive_time() + video_obj.set_name(video_name) + video_obj.set_nickname(video_name) + video_obj.set_video_descrip( + video_descrip, + self.main_win_obj.descrip_line_max_len, + ) + video_obj.set_upload_time(video_upload_time) + + # Give it a fake filename/extension, so that the Video Catalogue can + # find the thumbnail + # (If a youtube-dl output template is applied, the file that might be + # downloaded later will have a modified name and/or extension) + video_obj.set_file(video_name, '.mp4') + + # Mark it as a livestream + self.mark_video_live(video_obj, live_mode) + + # We can now sort the parent containers + video_obj.parent_obj.sort_children() + self.fixed_all_folder.sort_children() + self.fixed_live_folder.sort_children() + + def update_video_when_file_found(self, video_obj, video_path, temp_dict, \ mkv_flag=False): @@ -6861,7 +8179,7 @@ def update_video_when_file_found(self, video_obj, video_path, temp_dict, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6805 update_video_when_file_found') + utils.debug_time('app 8182 update_video_when_file_found') # Only set the .name IV if the video is currently unnamed if video_obj.name == self.default_video_name: @@ -6972,7 +8290,7 @@ def announce_video_clone(self, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6916 announce_video_clone') + utils.debug_time('app 8293 announce_video_clone') video_path = video_obj.get_actual_path(self) @@ -7015,7 +8333,7 @@ def update_video_from_json(self, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 6959 update_video_from_json') + utils.debug_time('app 8336 update_video_from_json') json_path = video_obj.get_actual_path_by_ext(self, '.info.json') @@ -7064,7 +8382,7 @@ def update_video_from_filesystem(self, video_obj, video_path): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7008 update_video_from_filesystem') + utils.debug_time('app 8385 update_video_from_filesystem') if video_obj.upload_time is None: video_obj.set_upload_time(os.path.getmtime(video_path)) @@ -7094,11 +8412,10 @@ def update_video_from_filesystem(self, video_obj, video_path): this_thread.join(self.refresh_moviepy_timeout) if this_thread.is_alive(): self.system_error( - 113, + 117, '\'' + video_obj.parent_obj.name \ - + '\': moviepy module' \ - + 'failed to fetch duration of video \'' \ - + video_obj.name + '\'', + + '\': moviepy module failed to fetch duration' \ + + ' of video \'' + video_obj.name + '\'', ) # (Can't set the video source directly) @@ -7129,21 +8446,64 @@ def set_duration_from_moviepy(self, video_obj, video_path): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7073 set_duration_from_moviepy') + utils.debug_time('app 8449 set_duration_from_moviepy') try: clip = moviepy.editor.VideoFileClip(video_path) video_obj.set_duration(clip.duration) except: self.system_error( - 114, + 118, '\'' + video_obj.parent_obj.name + '\': moviepy module' \ + 'failed to fetch duration of video \'' \ + video_obj.name + '\'', ) - def set_backup_archive(self, media_data_obj): + def prepare_overwrite_video(self, video_obj): + + """Called by self.livestream_manager_finished() and + mainwin.MainWin.on_click_watch_player_label(). + + If the specified video is a livestream that was downloaded when it was + still broadcasting, then a new download must overwrite the original + file. + + As of April 2020, the youtube-dl --yes-overwrites option is still not + available, so as a temporary measure we will rename the original file + (in case the download fails). + + Args: + + video_obj (media.Video): The video which this function assumes is + (or was) a livestream + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 8484 prepare_overwrite_video') + + path = os.path.abspath( + os.path.join( + video_obj.parent_obj.get_actual_dir(self), + video_obj.file_name + video_obj.file_ext, + ), + ) + + bu_path = path + '_BU' + + if os.path.isfile(path): + + # (On MSWin, can't do os.rename if the destination file already + # exists) + if os.path.isfile(bu_path): + os.remove(bu_path) + + # (os.rename sometimes fails on external hard drives; this is safer + shutil.move(path, bu_path) + + + def set_backup_archive(self, dir_path): """Called by mainwin.MainWin.on_video_catalogue_re_download(). @@ -7159,27 +8519,23 @@ def set_backup_archive(self, media_data_obj): Args: - media_data_obj (media.Video): The video object to be re-downloaded + dir_path (str): The full path to the directory containing the + video(s) to be re-downloaded """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7108 set_backup_archive') + utils.debug_time('app 8528 set_backup_archive') archive_path = os.path.abspath( - os.path.join( - media_data_obj.parent_obj.get_default_dir(self), - 'ytdl-archive.txt', - ) + os.path.join(dir_path, 'ytdl-archive.txt'), ) - if os.path.isfile(archive_path): + if os.path.isfile(archive_path) \ + and not archive_path in self.ytdl_archive_path_list: bu_path = os.path.abspath( - os.path.join( - media_data_obj.parent_obj.get_default_dir(self), - 'bu_archive.txt', - ) + os.path.join(dir_path, 'bu_archive.txt'), ) # (On MSWin, can't do os.rename if the destination file already @@ -7193,42 +8549,42 @@ def set_backup_archive(self, media_data_obj): # Store both paths, so self.reset_backup_archive() can retrieve # them - self.ytdl_archive_path = archive_path - self.ytdl_archive_backup_path = bu_path + self.ytdl_archive_path_list.append(archive_path) + self.ytdl_archive_backup_path_list.append(bu_path) def reset_backup_archive(self): """Called by self.download_manager_finished(). - If the youtube-dl archive file was temporarily renamed (in a call to - self.set_backup_archive()), in order to enable the video to be - re-downloaded, then restore the archive file's original name. + If youtube-dl archive file(s) were temporarily renamed (in a call to + self.set_backup_archive()) in order to enable the video to be + re-downloaded, then restore the archive files to their original names. """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7151 reset_backup_archive') + utils.debug_time('app 8566 reset_backup_archive') - if self.ytdl_archive_path is not None \ - and self.ytdl_archive_backup_path is not None \ - and os.path.isfile(self.ytdl_archive_backup_path): + while self.ytdl_archive_path_list: - # (On MSWin, can't do os.rename if the destination file already - # exists) - if os.path.isfile(self.ytdl_archive_path): - os.remove(self.ytdl_archive_path) + archive_path = self.ytdl_archive_path_list.pop() + bu_path = self.ytdl_archive_backup_path_list.pop() - # (os.rename sometimes fails on external hard drives; this is - # safer) - shutil.move( - self.ytdl_archive_backup_path, - self.ytdl_archive_path, - ) + if os.path.isfile(bu_path): + + # (On MSWin, can't do os.rename if the destination file already + # exists) + if os.path.isfile(archive_path): + os.remove(archive_path) - # Regardless of whether a backup archive file was created during a + # (os.rename sometimes fails on external hard drives; this is + # safer) + shutil.move(bu_path, archive_path) + + # Regardless of whether backup archive file(s) were created during a # re-download operation, or not, reset the IVs - self.ytdl_archive_path = None - self.ytdl_archive_backup_path = None + self.ytdl_archive_path_list = [] + self.ytdl_archive_backup_path_list = [] # (Add media data objects) @@ -7266,12 +8622,12 @@ def add_video(self, parent_obj, source=None, dl_sim_flag=False, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7210 add_video') + utils.debug_time('app 8625 add_video') # Videos can't be placed inside other videos if parent_obj and isinstance(parent_obj, media.Video): return self.system_error( - 115, + 119, 'Videos cannot be placed inside other videos', ) @@ -7279,7 +8635,7 @@ def add_video(self, parent_obj, source=None, dl_sim_flag=False, elif parent_obj and isinstance(parent_obj, media.Folder) \ and parent_obj.priv_flag: return self.system_error( - 116, + 120, 'Videos cannot be placed inside a private folder', ) @@ -7347,7 +8703,7 @@ def add_channel(self, name, parent_obj=None, source=None, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7291 add_channel') + utils.debug_time('app 8706 add_channel') # Channels can only be placed inside an unrestricted media.Folder # object (if they have a parent at all) @@ -7357,7 +8713,7 @@ def add_channel(self, name, parent_obj=None, source=None, \ or parent_obj.restrict_flag ): return self.system_error( - 117, + 121, 'Channels cannot be added to a restricted folder', ) @@ -7365,7 +8721,7 @@ def add_channel(self, name, parent_obj=None, source=None, \ # registry if parent_obj and parent_obj.get_depth() >= self.media_max_level: return self.system_error( - 118, + 122, 'Channel exceeds maximum depth of media registry', ) @@ -7374,7 +8730,7 @@ def add_channel(self, name, parent_obj=None, source=None, \ or re.match('\s*$', name) \ or not self.check_container_name_is_legal(name): return self.system_error( - 119, + 123, 'Illegal channel name', ) @@ -7436,7 +8792,7 @@ def add_playlist(self, name, parent_obj=None, source=None, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7380 add_playlist') + utils.debug_time('app 8795 add_playlist') # Playlists can only be place inside an unrestricted media.Folder # object (if they have a parent at all) @@ -7446,7 +8802,7 @@ def add_playlist(self, name, parent_obj=None, source=None, \ or parent_obj.restrict_flag ): return self.system_error( - 120, + 124, 'Playlists cannot be added to a restricted folder', ) @@ -7454,7 +8810,7 @@ def add_playlist(self, name, parent_obj=None, source=None, \ # registry if parent_obj and parent_obj.get_depth() >= self.media_max_level: return self.system_error( - 121, + 125, 'Playlist exceeds maximum depth of media registry', ) @@ -7463,7 +8819,7 @@ def add_playlist(self, name, parent_obj=None, source=None, \ or re.match('\s*$', name) \ or not self.check_container_name_is_legal(name): return self.system_error( - 122, + 126, 'Illegal playlist name', ) @@ -7527,7 +8883,7 @@ def add_folder(self, name, parent_obj=None, dl_sim_flag=False, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7471 add_folder') + utils.debug_time('app 8886 add_folder') # Folders can only be placed inside an unrestricted media.Folder object # (if they have a parent at all) @@ -7537,7 +8893,7 @@ def add_folder(self, name, parent_obj=None, dl_sim_flag=False, or parent_obj.restrict_flag ): return self.system_error( - 123, + 127, 'Folders cannot be added to another restricted folder', ) @@ -7545,7 +8901,7 @@ def add_folder(self, name, parent_obj=None, dl_sim_flag=False, # registry if parent_obj and parent_obj.get_depth() >= self.media_max_level: return self.system_error( - 124, + 128, 'Folder exceeds maximum depth of media registry', ) @@ -7554,7 +8910,7 @@ def add_folder(self, name, parent_obj=None, dl_sim_flag=False, or re.match('\s*$', name) \ or not self.check_container_name_is_legal(name): return self.system_error( - 125, + 129, 'Illegal folder name', ) @@ -7613,13 +8969,13 @@ def move_container_to_top(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7557 move_container_to_top') + utils.debug_time('app 8972 move_container_to_top') # Do some basic checks if media_data_obj is None or isinstance(media_data_obj, media.Video) \ or self.current_manager_obj or not media_data_obj.parent_obj: return self.system_error( - 126, + 130, 'Move container to top request failed sanity check', ) @@ -7634,20 +8990,18 @@ def move_container_to_top(self, media_data_obj): if os.path.isdir(target_path) or os.path.isfile(target_path): - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' - # (The same error message appears in self.move_container() ) self.dialogue_manager_obj.show_msg_dialogue( - 'Cannot move anything to\n\n' + target_path + '\n\nbecause a' \ - + ' file or ' + folder + ' with the same name already ' \ - + 'exists (although ' + __main__.__prettyname__ \ - + '\'s database doesn\'t know anything about it).\n\n' \ - + 'You probably created that file/' + folder \ - + ' accidentally, in which case, you should delete it' \ - + ' manually before trying again.', + _('Cannot move anything to:') + '\n\n' + target_path + '\n\n' \ + + _( + 'because a file or folder with the same name already' \ + + ' exists (although Tartube\'s database doesn\'t know' \ + + ' anything about it)', + ) + '\n\n' + _( + + 'You probably created that file/folder accidentally,' \ + + ' in which case you should delete it manually before' \ + + ' trying again', + ), 'error', 'ok', ) @@ -7657,13 +9011,22 @@ def move_container_to_top(self, media_data_obj): # Prompt the user for confirmation. If the user clicks 'yes', call # self.move_container_to_top_continue() to complete the move media_type = media_data_obj.get_type() + if media_type == 'channel': + msg = _('Are you sure you want to move this channel:') + elif media_type == 'playlist': + msg = _('Are you sure you want to move this playlist:') + else: + msg = _('Are you sure you want to move this folder:') + + msg += '\n\n ' + media_data_obj.name + '\n\n' + + msg += _( + + 'This procedure will move all downloaded files to the top' \ + + ' level of Tartube\'s data folder', + ) self.dialogue_manager_obj.show_msg_dialogue( - 'Are you sure you want to move this ' + media_type + ':\n\n' \ - + ' ' + media_data_obj.name + '\n\n' \ - + 'This procedure will move all downloaded files' \ - + ' to the top level of ' + __main__.__prettyname__ \ - + '\'s data directory', + msg, 'question', 'yes-no', None, # Parent window is main window @@ -7690,7 +9053,7 @@ def move_container_to_top_continue(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7634 move_container_to_top_continue') + utils.debug_time('app 9056 move_container_to_top_continue') # Move the sub-directories to their new location shutil.move(media_data_obj.get_default_dir(self), self.downloads_dir) @@ -7735,13 +9098,13 @@ def move_container(self, source_obj, dest_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7679 move_container') + utils.debug_time('app 9101 move_container') # Do some basic checks if source_obj is None or isinstance(source_obj, media.Video) \ or dest_obj is None or isinstance(dest_obj, media.Video): return self.system_error( - 127, + 131, 'Move container request failed sanity check', ) @@ -7758,8 +9121,10 @@ def move_container(self, source_obj, dest_obj): elif not isinstance(dest_obj, media.Folder): self.dialogue_manager_obj.show_msg_dialogue( + _( 'Channels, playlists and folders can only be dragged into' \ + ' a folder', + ), 'error', 'ok', ) @@ -7769,8 +9134,10 @@ def move_container(self, source_obj, dest_obj): elif isinstance(source_obj, media.Folder) and source_obj.fixed_flag: self.dialogue_manager_obj.show_msg_dialogue( - 'The fixed folder \'' + dest_obj.name \ - + '\' cannot be moved (but it can still be hidden)', + _( + 'The fixed folder \'{0}\' cannot be moved (but it can still' \ + + ' be hidden)', + ).format(dest_obj.name), 'error', 'ok', ) @@ -7780,8 +9147,9 @@ def move_container(self, source_obj, dest_obj): elif dest_obj.restrict_flag: self.dialogue_manager_obj.show_msg_dialogue( - 'The folder \'' + dest_obj.name \ - + '\' can only contain videos', + _( + 'The folder \'{0}\' can only contain videos', + ).format(dest_obj.name), 'error', 'ok', ) @@ -7799,19 +9167,18 @@ def move_container(self, source_obj, dest_obj): if os.path.isdir(target_path) or os.path.isfile(target_path): - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' - self.dialogue_manager_obj.show_msg_dialogue( - 'Cannot move anything to\n\n' + target_path + '\n\nbecause a' \ - + ' file or ' + folder + ' with the same name already ' \ - + 'exists (although ' + __main__.__prettyname__ \ - + '\'s database doesn\'t know anything about it).\n\n' \ - + 'You probably created that file/' + folder \ - + ' accidentally, in which case, you should delete it' \ - + ' manually before trying again.', + _('Cannot move anything to:') + '\n\n' + target_path + '\n\n' \ + + _( + 'because a file or folder with the same name already exists' \ + + ' (although Tartube\'s database doesn\'t know anything' \ + + ' about it)', + ) + '\n\n' \ + + _( + + 'You probably created that file/folder accidentally, in' \ + + ' which case, you should delete it manually before trying' \ + + ' again', + ), 'error', 'ok', ) @@ -7820,24 +9187,32 @@ def move_container(self, source_obj, dest_obj): # Prompt the user for confirmation source_type = source_obj.get_type() - - if not dest_obj.temp_flag: - temp_string = '' + if source_type == 'channel': + msg = _('Are you sure you want to move this channel:') + elif source_type == 'playlist': + msg = _('Are you sure you want to move this playlist:') else: - temp_string = '\n\nWARNING: The destination folder is marked' \ - + ' as temporary, so everything inside it will be DELETED when ' \ - + __main__.__prettyname__ + ' shuts down!', + msg = _('Are you sure you want to move this folder:') + + msg += '\n\n ' + source_obj.name + '\n\n' + _('into this folder:') \ + + '\n\n ' + dest_obj.name + '\n\n' + + msg += _( + 'This procedure will move all downloaded files to the new' \ + + ' location', + ) + + if dest_obj.temp_flag: + msg = '\n\n' + _( + 'WARNING: The destination folder is marked as temporary, so' \ + + ' everything inside it will be DELETED when Tartube' \ + + ' restarts!', + ) # If the user clicks 'yes', call self.move_container_continue() to # complete the move self.dialogue_manager_obj.show_msg_dialogue( - 'Are you sure you want to move this ' + source_type + ':\n\n' \ - + ' ' + source_obj.name + '\n\n' \ - + 'into this folder:\n\n' \ - + ' ' + dest_obj.name + '\n\n' \ - + 'This procedure will move all downloaded files to the new' \ - + ' location' \ - + temp_string, + msg, 'question', 'yes-no', None, # Parent window is main window @@ -7863,7 +9238,7 @@ def move_container_continue(self, media_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7807 move_container_continue') + utils.debug_time('app 9241 move_container_continue') source_obj = media_list[0] dest_obj = media_list[1] @@ -7926,14 +9301,14 @@ def convert_remote_container(self, old_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7870 convert_remote_container') + utils.debug_time('app 9304 convert_remote_container') if ( not isinstance(old_obj, media.Channel) \ and not isinstance(old_obj, media.Playlist) ) or self.current_manager_obj: return self.system_error( - 128, + 132, 'Convert container request failed sanity check', ) @@ -8019,11 +9394,11 @@ def delete_video(self, video_obj, delete_files_flag=False, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 7963 delete_video') + utils.debug_time('app 9397 delete_video') if not isinstance(video_obj, media.Video): return self.system_error( - 129, + 133, 'Delete video request failed sanity check', ) @@ -8041,6 +9416,9 @@ def delete_video(self, video_obj, delete_files_flag=False, if self.fixed_fav_folder.del_child(video_obj): update_list.append(self.fixed_fav_folder) + if self.fixed_live_folder.del_child(video_obj): + update_list.append(self.fixed_live_folder) + if self.fixed_new_folder.del_child(video_obj): update_list.append(self.fixed_new_folder) @@ -8055,6 +9433,24 @@ def delete_video(self, video_obj, delete_files_flag=False, if video_obj.dbid in self.media_reg_dict: del self.media_reg_dict[video_obj.dbid] + if video_obj.dbid in self.media_reg_live_dict: + del self.media_reg_live_dict[video_obj.dbid] + + if video_obj.dbid in self.media_reg_auto_notify_dict: + del self.media_reg_auto_notify_dict[video_obj.dbid] + + if video_obj.dbid in self.media_reg_auto_alarm_dict: + del self.media_reg_auto_alarm_dict[video_obj.dbid] + + if video_obj.dbid in self.media_reg_auto_open_dict: + del self.media_reg_auto_open_dict[video_obj.dbid] + + if video_obj.dbid in self.media_reg_auto_dl_start_dict: + del self.media_reg_auto_dl_start_dict[video_obj.dbid] + + if video_obj.dbid in self.media_reg_auto_dl_stop_dict: + del self.media_reg_auto_dl_stop_dict[video_obj.dbid] + # Delete files from the filesystem, if required # If the parent container has an alternative download destination set, # the files are in the corresponding directory. We don't delete the @@ -8131,7 +9527,7 @@ def delete_container(self, media_data_obj, empty_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8075 delete_container') + utils.debug_time('app 9530 delete_container') # Check this isn't a video or a fixed folder (which cannot be removed) if isinstance(media_data_obj, media.Video) \ @@ -8140,7 +9536,7 @@ def delete_container(self, media_data_obj, empty_flag=False): and media_data_obj.fixed_flag ): return self.system_error( - 130, + 134, 'Delete container request failed sanity check', ) @@ -8189,8 +9585,10 @@ def delete_container(self, media_data_obj, empty_flag=False): if delete_file_flag: self.dialogue_manager_obj.show_msg_dialogue( + _( 'Are you SURE you want to delete files? This procedure' \ ' cannot be reversed!', + ), 'question', 'yes-no', None, # Parent window is main window @@ -8223,7 +9621,7 @@ def delete_container_continue(self, data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8167 delete_container_continue') + utils.debug_time('app 9624 delete_container_continue') # Unpack the arguments media_data_obj = data_list[0] @@ -8271,7 +9669,7 @@ def delete_container_complete(self, media_data_obj, empty_flag, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8215 delete_container_complete') + utils.debug_time('app 9672 delete_container_complete') # Confirmation has been obtained, and any files have been deleted (if # required), so now deal with the media data registry @@ -8328,6 +9726,10 @@ def delete_container_complete(self, media_data_obj, empty_flag, self.fixed_fav_folder, ) + self.main_win_obj.video_index_update_row_text( + self.fixed_live_folder, + ) + self.main_win_obj.video_index_update_row_text( self.fixed_new_folder, ) @@ -8375,7 +9777,7 @@ def prepare_mark_video(self, data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8319 prepare_mark_video') + utils.debug_time('app 9780 prepare_mark_video') action_type = data_list.pop(0) action_flag = data_list.pop(0) @@ -8476,7 +9878,7 @@ def mark_video_bookmark(self, video_obj, bookmark_flag, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8420 mark_video_bookmark') + utils.debug_time('app 9881 mark_video_bookmark') # (List of Video Index rows to update, at the end of this function) update_list = [self.fixed_bookmark_folder] @@ -8485,6 +9887,8 @@ def mark_video_bookmark(self, video_obj, bookmark_flag, \ update_list.append(self.fixed_all_folder) if video_obj.fav_flag: update_list.append(self.fixed_fav_folder) + if video_obj.live_mode: + update_list.append(self.fixed_live_folder) if video_obj.new_flag: update_list.append(self.fixed_new_folder) if video_obj.waiting_flag: @@ -8493,7 +9897,7 @@ def mark_video_bookmark(self, video_obj, bookmark_flag, \ # Mark the video as bookmarked or not bookmarked if not isinstance(video_obj, media.Video): return self.system_error( - 131, + 135, 'Mark video as bookmarked request failed sanity check', ) @@ -8532,6 +9936,8 @@ def mark_video_bookmark(self, video_obj, bookmark_flag, \ self.fixed_bookmark_folder.dec_bookmark_count() if video_obj.fav_flag: self.fixed_fav_folder.dec_bookmark_count() + if video_obj.live_mode: + self.fixed_live_folder.dec_bookmark_count() if video_obj.new_flag: self.fixed_new_folder.dec_bookmark_count() if video_obj.waiting_flag: @@ -8559,6 +9965,8 @@ def mark_video_bookmark(self, video_obj, bookmark_flag, \ self.fixed_bookmark_folder.inc_dl_count() if video_obj.fav_flag: self.fixed_bookmark_folder.inc_fav_count() + if video_obj.live_mode: + self.fixed_bookmark_folder.inc_live_count() if video_obj.new_flag: self.fixed_bookmark_folder.inc_new_count() if video_obj.waiting_flag: @@ -8572,6 +9980,8 @@ def mark_video_bookmark(self, video_obj, bookmark_flag, \ self.fixed_all_folder.inc_bookmark_count() if video_obj.fav_flag: self.fixed_fav_folder.inc_bookmark_count() + if video_obj.live_mode: + self.fixed_live_folder.inc_bookmark_count() if video_obj.new_flag: self.fixed_new_folder.inc_bookmark_count() if video_obj.waiting_flag: @@ -8605,7 +10015,7 @@ def mark_video_downloaded(self, video_obj, dl_flag, not_new_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8549 mark_video_downloaded') + utils.debug_time('app 10018 mark_video_downloaded') # (List of Video Index rows to update, at the end of this function) update_list = [video_obj.parent_obj, self.fixed_all_folder] @@ -8613,7 +10023,7 @@ def mark_video_downloaded(self, video_obj, dl_flag, not_new_flag=False): # Mark the video as downloaded or not downloaded if not isinstance(video_obj, media.Video): return self.system_error( - 132, + 136, 'Mark video as downloaded request failed sanity check', ) @@ -8642,6 +10052,9 @@ def mark_video_downloaded(self, video_obj, dl_flag, not_new_flag=False): if video_obj.fav_flag: self.fixed_fav_folder.dec_dl_count() update_list.append(self.fixed_fav_folder) + if video_obj.live_mode: + self.fixed_live_folder.dec_dl_count() + update_list.append(self.fixed_live_folder) if video_obj.waiting_flag: self.fixed_waiting_folder.dec_dl_count() update_list.append(self.fixed_waiting_folder) @@ -8678,6 +10091,9 @@ def mark_video_downloaded(self, video_obj, dl_flag, not_new_flag=False): if video_obj.fav_flag: self.fixed_fav_folder.inc_dl_count() update_list.append(self.fixed_fav_folder) + if video_obj.live_mode: + self.fixed_live_folder.inc_dl_count() + update_list.append(self.fixed_live_folder) if video_obj.waiting_flag: self.fixed_waiting_folder.inc_dl_count() update_list.append(self.fixed_waiting_folder) @@ -8723,7 +10139,7 @@ def mark_video_favourite(self, video_obj, fav_flag, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8667 mark_video_favourite') + utils.debug_time('app 10142 mark_video_favourite') # (List of Video Index rows to update, at the end of this function) update_list = [self.fixed_fav_folder] @@ -8732,6 +10148,8 @@ def mark_video_favourite(self, video_obj, fav_flag, \ update_list.append(self.fixed_all_folder) if video_obj.bookmark_flag: update_list.append(self.fixed_bookmark_folder) + if video_obj.live_mode: + update_list.append(self.fixed_live_folder) if video_obj.new_flag: update_list.append(self.fixed_new_folder) if video_obj.waiting_flag: @@ -8740,7 +10158,7 @@ def mark_video_favourite(self, video_obj, fav_flag, \ # Mark the video as favourite or not favourite if not isinstance(video_obj, media.Video): return self.system_error( - 133, + 137, 'Mark video as favourite request failed sanity check', ) @@ -8780,6 +10198,8 @@ def mark_video_favourite(self, video_obj, fav_flag, \ self.fixed_fav_folder.dec_fav_count() if video_obj.bookmark_flag: self.fixed_bookmark_folder.dec_fav_count() + if video_obj.live_mode: + self.fixed_live_folder.dec_fav_count() if video_obj.new_flag: self.fixed_new_folder.dec_fav_count() if video_obj.waiting_flag: @@ -8807,6 +10227,8 @@ def mark_video_favourite(self, video_obj, fav_flag, \ self.fixed_fav_folder.inc_bookmark_count() if video_obj.dl_flag: self.fixed_fav_folder.inc_dl_count() + if video_obj.live_mode: + self.fixed_fav_folder.inc_live_count() if video_obj.new_flag: self.fixed_fav_folder.inc_new_count() if video_obj.waiting_flag: @@ -8820,6 +10242,8 @@ def mark_video_favourite(self, video_obj, fav_flag, \ self.fixed_all_folder.inc_fav_count() if video_obj.bookmark_flag: self.fixed_bookmark_folder.inc_fav_count() + if video_obj.live_mode: + self.fixed_live_folder.inc_fav_count() if video_obj.new_flag: self.fixed_new_folder.inc_fav_count() if video_obj.waiting_flag: @@ -8830,6 +10254,193 @@ def mark_video_favourite(self, video_obj, fav_flag, \ self.main_win_obj.video_index_update_row_text(container_obj) + def mark_video_live(self, video_obj, live_mode, \ + no_update_index_flag=False, no_update_catalogue_flag=False, \ + no_sort_flag=False): + + """Can be called by anything. + + Marks a video object as a livestream. + + The video object's .live_mode IV is updated. + + Args: + + video_obj (media.Video): The media.Video object to mark + + live_mode (int): 0 if the video is not a livestream (or if it was a + livestream which has now finished, and behaves like a normal + uploaded video), 1 if the livestream has not started, 2 if the + livestream is currently being broadcast + + no_update_index_flag (bool): True if the Video Index should not be + updated (except for the system 'Livestreams' folder), because + the calling function wants to do that itself + + no_update_catalogue_flag (bool): True if rows in the Video + Catalogue should not be updated, because the calling function + wants to redraw the whole catalogue itself + + no_sort_flag (bool): True if the parent container's .child_list + should not be sorted, because the calling function wants to do + that itself + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 10291 mark_video_live') + + # (List of Video Index rows to update, at the end of this function) + update_list = [self.fixed_live_folder] + if not no_update_index_flag: + update_list.append(video_obj.parent_obj) + update_list.append(self.fixed_all_folder) + if video_obj.bookmark_flag: + update_list.append(self.fixed_bookmark_folder) + if video_obj.fav_flag: + update_list.append(self.fixed_fav_folder) + if video_obj.new_flag: + update_list.append(self.fixed_new_folder) + if video_obj.waiting_flag: + update_list.append(self.fixed_waiting_folder) + + # Mark the video as a livestream or not a livestream + if not isinstance(video_obj, media.Video): + return self.system_error( + 138, + 'Mark video as livestream request failed sanity check', + ) + + elif live_mode == 0: + + # Mark video as not a livestream + if video_obj.live_mode == 0: + + # Already marked + return + + else: + + # Update the main registries + if video_obj.dbid in self.media_reg_live_dict: + del self.media_reg_live_dict[video_obj.dbid] + if video_obj.dbid in self.media_reg_auto_alarm_dict: + del self.media_reg_auto_alarm_dict[video_obj.dbid] + if video_obj.dbid in self.media_reg_auto_open_dict: + del self.media_reg_auto_open_dict[video_obj.dbid] + if video_obj.dbid in self.media_reg_auto_dl_start_dict: + del self.media_reg_auto_dl_start_dict[video_obj.dbid] + if video_obj.dbid in self.media_reg_auto_dl_stop_dict: + del self.media_reg_auto_dl_stop_dict[video_obj.dbid] + + # Update the video object's IVs + video_obj.set_live_mode(live_mode) + # Update the parent object + video_obj.parent_obj.dec_live_count() + + # Remove this video from the private 'Livestreams' folder + # (the folder's count IVs are automatically updated) + self.fixed_live_folder.del_child(video_obj) + # Update the Video Catalogue, if that folder is the visible one + # (deleting the row, if the 'Livestreams' folder is visible) + if not no_update_catalogue_flag: + + if self.main_win_obj.video_index_current is not None \ + and self.main_win_obj.video_index_current \ + == self.fixed_live_folder.name: + self.main_win_obj.video_catalogue_delete_row(video_obj) + + else: + self.main_win_obj.video_catalogue_update_row(video_obj) + + # Update other private folders + self.fixed_all_folder.dec_live_count() + self.fixed_live_folder.dec_live_count() + if video_obj.bookmark_flag: + self.fixed_bookmark_folder.dec_live_count() + if video_obj.fav_flag: + self.fixed_fav_folder.dec_live_count() + if video_obj.new_flag: + self.fixed_new_folder.dec_live_count() + if video_obj.waiting_flag: + self.fixed_waiting_folder.dec_waiting_count() + + else: + + # Mark video as a livestream + if video_obj.live_mode == live_mode: + + # Already marked as either a 'waiting' or a 'live' livestream + return + + else: + + if video_obj.live_mode == 0: + # Video was not a livestream, but now is + convert_flag = False + else: + # Video was a 'waiting' livestream, and is now 'live' (or + # vice-versa) + convert_flag = True + + # Update the main registry + self.media_reg_live_dict[video_obj.dbid] = video_obj + if self.livestream_auto_notify_flag: + self.media_reg_auto_notify_dict[video_obj.dbid] = video_obj + if HAVE_PLAYSOUND_FLAG \ + and self.livestream_auto_alarm_flag: + self.media_reg_auto_alarm_dict[video_obj.dbid] = video_obj + if self.livestream_auto_open_flag: + self.media_reg_auto_open_dict[video_obj.dbid] = video_obj + if self.livestream_auto_dl_start_flag: + self.media_reg_auto_dl_start_dict[video_obj.dbid] \ + = video_obj + if self.livestream_auto_dl_stop_flag: + self.media_reg_auto_dl_stop_dict[video_obj.dbid] \ + = video_obj + + # Update the video object's IVs + video_obj.set_live_mode(live_mode) + # Update the parent object + if not convert_flag: + video_obj.parent_obj.inc_waiting_count() + + # Add this video to the private 'Livestreams' folder + if not convert_flag: + self.fixed_live_folder.add_child(video_obj, no_sort_flag) + self.fixed_live_folder.inc_live_count() + if video_obj.bookmark_flag: + self.fixed_live_folder.inc_bookmark_count() + if video_obj.dl_flag: + self.fixed_live_folder.inc_dl_count() + if video_obj.fav_flag: + self.fixed_live_folder.inc_fav_count() + if video_obj.new_flag: + self.fixed_live_folder.inc_new_count() + if video_obj.waiting_flag: + self.fixed_live_folder.inc_waiting_count() + + # Update the Video Catalogue, if that folder is the visible one + if not no_update_catalogue_flag: + self.main_win_obj.video_catalogue_update_row(video_obj) + + # Update other private folders + if not convert_flag: + self.fixed_all_folder.inc_live_count() + if video_obj.bookmark_flag: + self.fixed_bookmark_folder.inc_live_count() + if video_obj.fav_flag: + self.fixed_fav_folder.inc_live_count() + if video_obj.new_flag: + self.fixed_new_folder.inc_live_count() + if video_obj.waiting_flag: + self.fixed_waiting_folder.inc_live_count() + + # Update rows in the Video Index + for container_obj in update_list: + self.main_win_obj.video_index_update_row_text(container_obj) + + def mark_video_new(self, video_obj, new_flag, no_update_index_flag=False, no_update_catalogue_flag=False, no_sort_flag=False): @@ -8862,7 +10473,7 @@ def mark_video_new(self, video_obj, new_flag, no_update_index_flag=False, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8806 mark_video_new') + utils.debug_time('app 10476 mark_video_new') # (List of Video Index rows to update, at the end of this function) update_list = [self.fixed_new_folder] @@ -8873,13 +10484,15 @@ def mark_video_new(self, video_obj, new_flag, no_update_index_flag=False, update_list.append(self.fixed_bookmark_folder) if video_obj.fav_flag: update_list.append(self.fixed_fav_folder) + if video_obj.live_mode: + update_list.append(self.fixed_live_folder) if video_obj.waiting_flag: update_list.append(self.fixed_waiting_folder) # Mark the video as new or not new if not isinstance(video_obj, media.Video): return self.system_error( - 134, + 139, 'Mark video as new request failed sanity check', ) @@ -8920,6 +10533,8 @@ def mark_video_new(self, video_obj, new_flag, no_update_index_flag=False, self.fixed_bookmark_folder.dec_new_count() if video_obj.fav_flag: self.fixed_fav_folder.dec_new_count() + if video_obj.live_mode: + self.fixed_live_folder.dec_new_count() if video_obj.waiting_flag: self.fixed_waiting_folder.dec_new_count() @@ -8945,6 +10560,8 @@ def mark_video_new(self, video_obj, new_flag, no_update_index_flag=False, self.fixed_new_folder.inc_bookmark_count() if video_obj.fav_flag: self.fixed_new_folder.inc_fav_count() + if video_obj.live_mode: + self.fixed_new_folder.inc_live_count() if video_obj.waiting_flag: self.fixed_new_folder.inc_waiting_count() # Update the Video Catalogue, if that folder is the visible one @@ -8957,6 +10574,8 @@ def mark_video_new(self, video_obj, new_flag, no_update_index_flag=False, self.fixed_bookmark_folder.inc_new_count() if video_obj.fav_flag: self.fixed_fav_folder.inc_new_count() + if video_obj.live_mode: + self.fixed_live_folder.inc_new_count() if video_obj.waiting_flag: self.fixed_waiting_folder.inc_new_count() @@ -8983,7 +10602,7 @@ def mark_video_waiting(self, video_obj, waiting_flag, \ False to mark it as not in the waiting list no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'Waiting Videos' folder), + updated (except for the system 'Waiting Videos' folder), because the calling function wants to do that itself no_update_catalogue_flag (bool): True if rows in the Video @@ -8997,7 +10616,7 @@ def mark_video_waiting(self, video_obj, waiting_flag, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 8941 mark_video_waiting') + utils.debug_time('app 10619 mark_video_waiting') # (List of Video Index rows to update, at the end of this function) update_list = [self.fixed_waiting_folder] @@ -9008,13 +10627,15 @@ def mark_video_waiting(self, video_obj, waiting_flag, \ update_list.append(self.fixed_bookmark_folder) if video_obj.fav_flag: update_list.append(self.fixed_fav_folder) + if video_obj.live_mode: + update_list.append(self.fixed_live_folder) if video_obj.new_flag: update_list.append(self.fixed_new_folder) # Mark the video as in the waiting list or not in the waiting list if not isinstance(video_obj, media.Video): return self.system_error( - 135, + 140, 'Mark video as in waiting list request failed sanity check', ) @@ -9056,6 +10677,8 @@ def mark_video_waiting(self, video_obj, waiting_flag, \ self.fixed_bookmark_folder.dec_waiting_count() if video_obj.fav_flag: self.fixed_fav_folder.dec_waiting_count() + if video_obj.live_mode: + self.fixed_live_folder.dec_waiting_count() if video_obj.new_flag: self.fixed_new_folder.dec_waiting_count() @@ -9083,6 +10706,8 @@ def mark_video_waiting(self, video_obj, waiting_flag, \ self.fixed_waiting_folder.inc_dl_count() if video_obj.fav_flag: self.fixed_waiting_folder.inc_fav_count() + if video_obj.live_mode: + self.fixed_waiting_folder.inc_live_count() if video_obj.new_flag: self.fixed_waiting_folder.inc_new_count() @@ -9096,6 +10721,8 @@ def mark_video_waiting(self, video_obj, waiting_flag, \ self.fixed_bookmark_folder.inc_waiting_count() if video_obj.fav_flag: self.fixed_fav_folder.inc_waiting_count() + if video_obj.live_mode: + self.fixed_live_folder.inc_waiting_count() if video_obj.new_flag: self.fixed_new_folder.inc_waiting_count() @@ -9123,11 +10750,11 @@ def mark_folder_hidden(self, folder_obj, flag): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9067 mark_folder_hidden') + utils.debug_time('app 10756 mark_folder_hidden') if not isinstance(folder_obj, media.Folder): return self.system_error( - 136, + 141, 'Mark folder as hidden request failed sanity check', ) @@ -9185,11 +10812,11 @@ def mark_container_archived(self, media_data_obj, archive_flag, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9129 mark_container_archived') + utils.debug_time('app 10815 mark_container_archived') if isinstance(media_data_obj, media.Video): return self.system_error( - 137, + 142, 'Mark container as archived request failed sanity check', ) @@ -9220,6 +10847,15 @@ def mark_container_archived(self, media_data_obj, archive_flag, and other_obj.fav_flag: other_obj.set_archive_flag(archive_flag) + elif not archive_flag and media_data_obj == self.fixed_live_folder: + + # Check videos in this folder + for other_obj in self.fixed_live_folder.child_list: + + if isinstance(other_obj, media.Video) and other_obj.dl_flag \ + and other_obj.live_mode: + other_obj.set_archive_flag(archive_flag) + elif media_data_obj == self.fixed_new_folder: # Check videos in this folder @@ -9291,11 +10927,11 @@ def mark_container_favourite(self, media_data_obj, fav_flag, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9235 mark_container_favourite') + utils.debug_time('app 10930 mark_container_favourite') if isinstance(media_data_obj, media.Video): return self.system_error( - 138, + 143, 'Mark container as favourite request failed sanity check', ) @@ -9332,6 +10968,15 @@ def mark_container_favourite(self, media_data_obj, fav_flag, and other_obj.fav_flag: video_list.append(other_obj) + elif media_data_obj == self.fixed_live_folder: + + # Check videos in this folder + for other_obj in self.fixed_live_folder.child_list: + + if isinstance(other_obj, media.Video) \ + and other_obj.live_mode: + video_list.append(other_obj) + elif media_data_obj == self.fixed_new_folder: # Check videos in this folder @@ -9402,10 +11047,29 @@ def mark_container_favourite(self, media_data_obj, fav_flag, # This might take a few tens of seconds, so prompt the user for # confirmation first + media_type = media_data_obj.get_type() + if media_type == 'channel': + msg = _( + 'The channel contains {0} item(s), so this action may' \ + + ' take a while', + ).format(str(count)) + + elif media_type == 'playlist': + msg = _( + 'The playlist contains {0} item(s), so this action may' \ + + ' take a while', + ).format(str(count)) + + else: + msg = _( + 'The folder contains {0} item(s), so this action may' \ + + ' take a while', + ).format(str(count)) + + msg += '\n\n' + _('Are you sure you want to continue?') + self.dialogue_manager_obj.show_msg_dialogue( - 'The ' + media_data_obj.get_type() + ' contains ' \ - + str(count) + ' items, so this action might take a' \ - + 'while. \n\nAre you sure you want to continue?', + msg, 'question', 'yes-no', None, # Parent window is main window @@ -9446,11 +11110,11 @@ def mark_container_new(self, media_data_obj, new_flag, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9390 mark_container_new') + utils.debug_time('app 11113 mark_container_new') if isinstance(media_data_obj, media.Video): return self.system_error( - 139, + 144, 'Mark container as new request failed sanity check', ) @@ -9485,6 +11149,15 @@ def mark_container_new(self, media_data_obj, new_flag, and other_obj.fav_flag: video_list.append(other_obj) + elif media_data_obj == self.fixed_live_folder: + + # Check videos in this folder + for other_obj in self.fixed_live_folder.child_list: + + if isinstance(other_obj, media.Video) \ + and other_obj.live_mode: + video_list.append(other_obj) + elif media_data_obj == self.fixed_waiting_folder: # Check videos in this folder @@ -9539,10 +11212,29 @@ def mark_container_new(self, media_data_obj, new_flag, # This might take a few tens of seconds, so prompt the user for # confirmation first + media_type = media_data_obj.get_type() + if media_type == 'channel': + msg = _( + 'The channel contains {0} item(s), so this action may' \ + + ' take a while', + ).format(str(count)) + + elif media_type == 'playlist': + msg = _( + 'The playlist contains {0} item(s), so this action may' \ + + ' take a while', + ).format(str(count)) + + else: + msg = _( + 'The folder contains {0} item(s), so this action may' \ + + ' take a while', + ).format(str(count)) + + msg += '\n\n' + _('Are you sure you want to continue?') + self.dialogue_manager_obj.show_msg_dialogue( - 'The ' + media_data_obj.get_type() + ' contains ' \ - + str(count) + ' items, so this action might take a' \ - + 'while. \n\nAre you sure you want to continue?', + msg, 'question', 'yes-no', None, # Parent window is main window @@ -9569,7 +11261,7 @@ def rename_container(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9513 rename_container') + utils.debug_time('app 11264 rename_container') # Do some basic checks if media_data_obj is None or isinstance(media_data_obj, media.Video) \ @@ -9579,7 +11271,7 @@ def rename_container(self, media_data_obj): and media_data_obj.fixed_flag ): return self.system_error( - 140, + 145, 'Rename container request failed sanity check', ) @@ -9603,7 +11295,7 @@ def rename_container(self, media_data_obj): or re.match('\s*$', new_name) \ or not self.check_container_name_is_legal(new_name): return self.dialogue_manager_obj.show_msg_dialogue( - 'The name \'' + new_name + '\' is not allowed', + _('The name \'{0}\' is not allowed').format(new_name), 'error', 'ok', ) @@ -9612,7 +11304,7 @@ def rename_container(self, media_data_obj): # using this name if new_name in self.media_name_dict: return self.dialogue_manager_obj.show_msg_dialogue( - 'The name \'' + new_name + '\' is already in use', + _('The name \'{0}\' is already in use').format(new_name), 'error', 'ok', ) @@ -9625,7 +11317,7 @@ def rename_container(self, media_data_obj): except: return self.dialogue_manager_obj.show_msg_dialogue( - 'Failed to rename \'' + media_data_obj.name + '\'', + _('Failed to rename \'{0}\'').format(media_data_obj.name), 'error', 'ok', ) @@ -9648,7 +11340,7 @@ def rename_container(self, media_data_obj): def rename_container_silently(self, media_data_obj, new_name): - """Called by self.load_db(). + """Called by self.load_db() and .rename_fixed_folder(). A modified form of self.rename_container. No dialogue windows are used, no widgets are updated or desensitised, and the Tartube database file @@ -9669,31 +11361,35 @@ def rename_container_silently(self, media_data_obj, new_name): new_name (str): The object's new name Returns: + True on success, False on failure """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9618 rename_container_silently') + utils.debug_time('app 11370 rename_container_silently') # Nothing in the Tartube code should be capable of calling this # function with an illegal name, but we'll still check if not self.check_container_name_is_legal(new_name): self.system_error( - 141, + 146, 'Illegal container name', ) return False # Attempt to rename the sub-directory itself - old_dir = media_data_obj.get_default_dir(self) - new_dir = media_data_obj.get_default_dir(self, new_name) - try: - shutil.move(old_dir, new_dir) + # (Private folders don't have a sub-directory to rename, so check for + # that) + if not media_data_obj.priv_flag: + old_dir = media_data_obj.get_default_dir(self) + new_dir = media_data_obj.get_default_dir(self, new_name) + try: + shutil.move(old_dir, new_dir) - except: - return False + except: + return False # Filesystem updated, so now update the media data object itself. This # call also updates the object's .nickname IV @@ -9727,7 +11423,7 @@ def apply_download_options(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9671 apply_download_options') + utils.debug_time('app 11426 apply_download_options') if self.current_manager_obj \ or media_data_obj.options_obj\ @@ -9736,7 +11432,7 @@ def apply_download_options(self, media_data_obj): and media_data_obj.priv_flag ): return self.system_error( - 142, + 147, 'Apply download options request failed sanity check', ) @@ -9772,11 +11468,11 @@ def remove_download_options(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9716 remove_download_options') + utils.debug_time('app 11471 remove_download_options') if self.current_manager_obj or not media_data_obj.options_obj: return self.system_error( - 143, + 148, 'Remove download options request failed sanity check', ) @@ -9809,7 +11505,7 @@ def check_container_name_is_legal(self, name): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9753 check_container_name_is_legal') + utils.debug_time('app 11508 check_container_name_is_legal') for regex in self.illegal_name_regex_list: if re.search(regex, name, re.IGNORECASE): @@ -9848,7 +11544,7 @@ def export_from_db(self, media_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 9792 export_from_db') + utils.debug_time('app 11547 export_from_db') # If the specified list is empty, a summary of the whole database is # exported @@ -9877,7 +11573,7 @@ def export_from_db(self, media_list): # Prompt the user for the file path to use file_chooser_win = Gtk.FileChooserDialog( - 'Select where to save the database export', + _('Select where to save the database export'), self.main_win_obj, Gtk.FileChooserAction.SAVE, ( @@ -10006,7 +11702,7 @@ def export_from_db(self, media_list): if not db_dict: return self.dialogue_manager_obj.show_msg_dialogue( - 'There is nothing to export!', + _('There is nothing to export!'), 'error', 'ok', ) @@ -10039,7 +11735,7 @@ def export_from_db(self, media_list): except: return self.dialogue_manager_obj.show_msg_dialogue( - 'Failed to save the database export file', + _('Failed to save the database export file'), 'error', 'ok', ) @@ -10097,14 +11793,14 @@ def export_from_db(self, media_list): except: return self.dialogue_manager_obj.show_msg_dialogue( - 'Failed to save the database export file', + _('Failed to save the database export file'), 'error', 'ok', ) # Export was successful self.dialogue_manager_obj.show_msg_dialogue( - 'Database export file saved to:\n\n' + file_path, + _('Database export file saved to:') + '\n\n' + file_path, 'info', 'ok', ) @@ -10137,11 +11833,11 @@ def import_into_db(self, json_flag): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 10081 import_into_db') + utils.debug_time('app 11836 import_into_db') # Prompt the user for the export file to load file_chooser_win = Gtk.FileChooserDialog( - 'Select the database export', + _('Select the database export'), self.main_win_obj, Gtk.FileChooserAction.OPEN, ( @@ -10166,7 +11862,7 @@ def import_into_db(self, json_flag): text = self.file_manager_obj.load_text(file_path) if text is None: return self.dialogue_manager_obj.show_msg_dialogue( - 'Failed to load the database export file', + _('Failed to load the database export file'), 'error', 'ok', ) @@ -10180,7 +11876,7 @@ def import_into_db(self, json_flag): json_dict = self.file_manager_obj.load_json(file_path) if not json_dict: return self.dialogue_manager_obj.show_msg_dialogue( - 'Failed to load the database export file', + _('Failed to load the database export file'), 'error', 'ok', ) @@ -10197,7 +11893,7 @@ def import_into_db(self, json_flag): or json_dict['script_name'] != __main__.__packagename__ \ or json_dict['file_type'] != 'db_export': return self.dialogue_manager_obj.show_msg_dialogue( - 'The database export file is invalid', + _('The database export file is invalid'), 'error', 'ok', ) @@ -10208,7 +11904,7 @@ def import_into_db(self, json_flag): if not db_dict: return self.dialogue_manager_obj.show_msg_dialogue( - 'The database export file is invalid (or empty)', + _('The database export file is invalid (or empty)'), 'error', 'ok', ) @@ -10252,7 +11948,7 @@ def import_into_db(self, json_flag): if not video_count and not channel_count and not playlist_count \ and not folder_count: self.dialogue_manager_obj.show_msg_dialogue( - 'Nothing was imported from the database export file', + _('Nothing was imported from the database export file'), 'error', 'ok', ) @@ -10266,11 +11962,11 @@ def import_into_db(self, json_flag): ) # Show a confirmation - msg = 'Imported:' \ - + '\n\nVideos: ' + str(video_count) \ - + '\n\nChannels: ' + str(channel_count) \ - + '\n\nPlaylists: ' + str(playlist_count) \ - + '\n\nFolders: ' + str(folder_count) + msg = _('Imported:') \ + + '\n\n' + _('Videos:') + ' ' + str(video_count) \ + + '\n\n' + _('Channels:') + ' ' + str(channel_count) \ + + '\n\n' + _('Playlists:') + ' ' + str(playlist_count) \ + + '\n\n' + _('Folders:') + ' ' + str(folder_count) self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') @@ -10308,7 +12004,7 @@ def parse_text_import(self, text): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 10252 parse_text_import') + utils.debug_time('app 12007 parse_text_import') db_dict = {} dbid = 0 @@ -10427,7 +12123,7 @@ def process_import(self, db_dict, flat_db_dict, parent_obj, """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 10371 process_import') + utils.debug_time('app 12126 process_import') # To optimise the code below, compile a dictionary for quick lookup, # containing the source URLs for all videos in the parent channel/ @@ -10594,7 +12290,7 @@ def rename_imported_container(self, name): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 10538 rename_imported_container') + utils.debug_time('app 12293 rename_imported_container') count = 1 while True: @@ -10623,15 +12319,17 @@ def watch_video_in_player(self, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 10567 watch_video_in_player') + utils.debug_time('app 12322 watch_video_in_player') path = video_obj.get_actual_path(self) if not os.path.isfile(path): self.dialogue_manager_obj.show_msg_dialogue( - 'The video file is missing from ' + __main__.__prettyname__ \ - + '\'s data directory (try downloading the video again!', + _( + 'The video file is missing from Tartube\'s data folder' \ + + ' (try downloading the video again!)', + ), 'error', 'ok', ) @@ -10658,13 +12356,13 @@ def download_watch_videos(self, video_list, watch_flag=True): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 10602 download_watch_videos') + utils.debug_time('app 12359 download_watch_videos') # Sanity check: this function is only for videos for video_obj in video_list: if not isinstance(video_obj, media.Video): return self.system_error( - 144, + 149, 'Download and watch video request failed sanity check', ) @@ -10693,6 +12391,9 @@ def download_watch_videos(self, video_list, watch_flag=True): video_obj, ) + # Update the main window's progress bar + self.download_manager_obj.nudge_progress_bar() + else: # Start a new download operation to download this video @@ -10722,7 +12423,7 @@ def clone_general_options_manager(self, data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 10666 clone_general_options_manager') + utils.debug_time('app 12426 clone_general_options_manager') edit_win_obj = data_list.pop(0) options_obj = data_list.pop(0) @@ -10750,7 +12451,7 @@ def reset_options_manager(self, data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 10694 reset_options_manager') + utils.debug_time('app 12454 reset_options_manager') edit_win_obj = data_list.pop(0) @@ -10774,6 +12475,36 @@ def reset_options_manager(self, data_list): edit_win_obj.reset_with_new_edit_obj(options_obj) + # (Sound effects) + + def play_sound(self, sound_name=None): + + """Can be called by anything. + + Plays the specified sound effect. + + Args: + + sound_name (str): The sound effect to play, one of the items in + self.sound_list. If no sound effect is specified, plays the + user's chosen sound effect, self.sound_custom + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 12495 play_sound') + + if sound_name is None: + sound_name = self.sound_custom + + path = os.path.abspath( + os.path.join(self.sound_dir, sound_name), + ) + + if os.path.isfile(path): + playsound.playsound(path) + + # Callback class methods @@ -10785,7 +12516,10 @@ def script_slow_timer_callback(self): """Called by GObject timer created by self.start(). Once a minute, check whether it's time to perform a scheduled 'Download - all' or 'Check all' operation and, if so, perform it. + all' or 'Check all' download operation and, if so, perform it. + + Otherwise, check whether it's time to perform a scheduled livestream + operation and, if so, perform it. Returns: @@ -10794,7 +12528,7 @@ def script_slow_timer_callback(self): """ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10738 script_slow_timer_callback') + utils.debug_time('app 12531 script_slow_timer_callback') if not self.disable_load_save_flag \ and not self.current_manager_obj \ @@ -10804,20 +12538,42 @@ def script_slow_timer_callback(self): wait_time = self.scheduled_dl_wait_hours * 3600 if (self.scheduled_dl_last_time + wait_time) < time.time(): + self.download_manager_start( 'real', # 'Download all' True, # This function is the calling function ) + # Return 1 to keep the timer going + return 1 + elif self.scheduled_check_mode == 'scheduled': wait_time = self.scheduled_check_wait_hours * 3600 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 + + # If no download operation was started, we're free to start a + # livestream operation instead (but only if there is at least one + # media.Video object marked as a livestream) + if self.media_reg_live_dict: + + wait_time = self.scheduled_livestream_wait_mins * 60 + if (self.scheduled_livestream_last_time + wait_time) \ + < time.time(): + + self.livestream_manager_start() + + # Return 1 to keep the timer going + return 1 + # Return 1 to keep the timer going return 1 @@ -10829,6 +12585,10 @@ def script_fast_timer_callback(self): Once a second, check whether there are any mainwin.Catalogue objects to add to the Video Catalogue and, if so, add them. + Also checks whether a download operation that was due to beging at + startup, should begin now. (For aesthetic reasons, we wait a few + seconds before starting the scheduled operation). + Returns: 1 to keep the timer going, or None to halt it @@ -10836,10 +12596,29 @@ def script_fast_timer_callback(self): """ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10780 script_fast_timer_callback') + utils.debug_time('app 12599 script_fast_timer_callback') + # Update the Video Catalogue self.main_win_obj.video_catalogue_retry_insert_items() + # Check scheduled operations + current_time = time.time() + if self.scheduled_dl_start_check_time is not None \ + and self.scheduled_dl_start_check_time < current_time: + + self.download_manager_start( + 'real', # 'Download all' + 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: + + self.download_manager_start( + 'sim', # 'Check all' + True, # This function is the calling function + ) + # Return 1 to keep the timer going return 1 @@ -10849,7 +12628,9 @@ def dl_timer_callback(self): """Called by GObject timer created by self.download_manager_continue(). During a download operation, a GObject timer runs, so that the Progress - Tab and Output Tab can be updated at regular intervals. + Tab and Output Tab can be updated at regular intervals. (When the + download operation is launched from the Classic Mode Tab, the + Classic Progress List and Output Tab are updated.) There is also a delay between the instant at which youtube-dl reports a video file has been downloaded, and the instant at which it appears in @@ -10872,7 +12653,15 @@ def dl_timer_callback(self): """ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10816 dl_timer_callback') + utils.debug_time('app 12656 dl_timer_callback') + + # This function behaves differently, if the download operation was + # launched from the Classic Mode Tab + if not self.download_manager_obj \ + or self.download_manager_obj.operation_type != 'classic': + classic_mode_flag = False + else: + classic_mode_flag = True # Periodically check (if required) whether the device is running out of # disk space @@ -10896,7 +12685,7 @@ def dl_timer_callback(self): # Stop the download operation self.system_error( - 145, + 150, 'Download operation halted because the device is running' \ + ' out of space', ) @@ -10907,25 +12696,27 @@ def dl_timer_callback(self): return 1 # Disk space check complete, now update main window widgets - if self.dl_timer_check_time is None: - self.main_win_obj.progress_list_display_dl_stats() - self.main_win_obj.results_list_update_row() - self.main_win_obj.output_tab_update_pages() - if self.progress_list_hide_flag: - self.main_win_obj.progress_list_check_hide_rows() + check_time = self.dl_timer_check_time + if check_time is None or check_time > time.time(): - # Download operation still in progress, return 1 to keep the timer - # going - return 1 + if not classic_mode_flag: + self.main_win_obj.progress_list_display_dl_stats() + self.main_win_obj.results_list_update_row() + else: + self.main_win_obj.classic_mode_tab_display_dl_stats() - elif self.dl_timer_check_time > time.time(): - self.main_win_obj.progress_list_display_dl_stats() - self.main_win_obj.results_list_update_row() self.main_win_obj.output_tab_update_pages() - if self.progress_list_hide_flag: + if not classic_mode_flag and self.progress_list_hide_flag: self.main_win_obj.progress_list_check_hide_rows() - if self.main_win_obj.results_list_temp_list: + if check_time is None: + + # Download operation still in progress, return 1 to keep the + # timer going + return 1 + + elif self.main_win_obj.results_list_temp_list: + # Not all downloaded files confirmed to exist yet, so return 1 # to keep the timer going a little longer return 1 @@ -10958,7 +12749,7 @@ def update_timer_callback(self): """ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10902 update_timer_callback') + utils.debug_time('app 12752 update_timer_callback') if self.update_timer_check_time is None: @@ -11002,7 +12793,7 @@ def refresh_timer_callback(self): """ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10946 refresh_timer_callback') + utils.debug_time('app 12796 refresh_timer_callback') if self.refresh_timer_check_time is None: @@ -11046,7 +12837,7 @@ def info_timer_callback(self): """ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10990 info_timer_callback') + utils.debug_time('app 12840 info_timer_callback') if self.info_timer_check_time is None: @@ -11090,7 +12881,7 @@ def tidy_timer_callback(self): """ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 11034 tidy_timer_callback') + utils.debug_time('app 12884 tidy_timer_callback') if self.tidy_timer_check_time is None: @@ -11130,12 +12921,12 @@ def on_button_apply_filter(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11074 on_button_apply_filter') + utils.debug_time('app 12924 on_button_apply_filter') # Sanity check if not self.main_win_obj.video_catalogue_dict: return self.system_error( - 146, + 151, 'Apply filter request failed sanity check', ) @@ -11143,11 +12934,340 @@ def on_button_apply_filter(self, action, par): self.main_win_obj.video_catalogue_apply_filter() - def on_button_cancel_filter(self, action, par): + def on_button_cancel_filter(self, action, par): + + """Called from a callback in self.do_startup(). + + Cancels the filter, restoring all hidden videos in the Video Catalogue. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 12952 on_button_cancel_filter') + + # Sanity check + if not self.main_win_obj.video_catalogue_dict: + return self.system_error( + 152, + 'Cancel filter request failed sanity check', + ) + + # Cancel the filter + self.main_win_obj.video_catalogue_cancel_filter() + + + def on_button_classic_add_urls(self, action, par): + + """Called from a callback in self.do_startup(). + + In the Classic Mode Tab, transfers URLs in the textview into the + Classic Progress List, creating a new dummy media.Video object for each + URL, and updating IVs. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 12982 on_button_classic_add_urls') + + self.main_win_obj.classic_mode_tab_add_urls() + + + def on_button_classic_auto_copy(self, action, par): + + """Called from a callback in self.do_startup(). + + Toggles the auto copy/paste button in the Classic Mode Tab. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 13002 on_button_classic_auto_copy') + + # Toggle the button + self.main_win_obj.classic_mode_tab_toggle_auto_copy() + + + def on_button_classic_dest_dir(self, action, par): + + """Called from a callback in self.do_startup(). + + Opens the file chooser dialogue, so the user can set a new destination + directory for videos downloaded in the Classic Mode Tab. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 13024 on_button_classic_dest_dir') + + file_chooser_win = Gtk.FileChooserDialog( + _('Please select a destination folder'), + self.main_win_obj, + Gtk.FileChooserAction.SELECT_FOLDER, + ( + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, Gtk.ResponseType.OK, + ), + ) + + response = file_chooser_win.run() + dest_dir = file_chooser_win.get_filename() + file_chooser_win.destroy() + + if response == Gtk.ResponseType.OK: + + # Update IVs. Don't add a duplicate directory, but do move a + # duplicate to the top (and apply the maximum size, if required) + mod_list = [dest_dir] + for item in self.classic_dir_list: + + if item != dest_dir: + mod_list.append(item) + + if len(mod_list) >= self.classic_dir_max: + break + + self.classic_dir_list = mod_list.copy() + + # Update the combo in the main window + self.main_win_obj.classic_mode_tab_add_dest_dir() + + + def on_button_classic_download(self, action, par): + + """Called from a callback in self.do_startup(). + + Starts a download operation for the URLs added to the Classic Progress + List. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 13075 on_button_classic_download') + + # Start the download operation + self.download_manager_start('classic') + + + def on_button_classic_move_up(self, action, par): + + """Called from a callback in self.do_startup(). + + In the Classic Progress List, moves the selected item(s) up. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 13096 on_button_classic_move_up') + + self.main_win_obj.classic_mode_tab_move_row(True) + + + def on_button_classic_move_down(self, action, par): + + """Called from a callback in self.do_startup(). + + In the Classic Progress List, moves the selected item(s) down. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 13116 on_button_move_down_play') + + self.main_win_obj.classic_mode_tab_move_row(False) + + + def on_button_classic_play(self, action, par): + + """Called from a callback in self.do_startup(). + + Plays any videos downloaded from the selected rows in the Classic + Progress List. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 13137 on_button_classic_play') + + selection = self.main_win_obj.classic_progress_treeview.get_selection() + (model, path_list) = selection.get_selected_rows() + if not path_list: + + # Nothing selected + return + + # Get the the dummy media.Video objects for each selected row, and + # filter out those for which no video(s) have been downloaded + video_list = [] + for path in path_list: + + this_iter = model.get_iter(path) + dbid = model[this_iter][0] + video_obj = self.main_win_obj.classic_media_dict[dbid] + if video_obj.dummy_path is not None: + video_list.append(video_obj.dummy_path) + + if not video_list: + + self.dialogue_manager_obj.show_msg_dialogue( + _('No video(s) have been downloaded'), + 'error', + 'ok', + ) + + else: + + for video_path in video_list: + utils.open_file(video_path) + + + def on_button_classic_redownload(self, action, par): + + """Called from a callback in self.do_startup(). + + Redownloads the selected rows in the Classic Progress List. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 13186 on_button_classic_redownload') + + selection = self.main_win_obj.classic_progress_treeview.get_selection() + (model, path_list) = selection.get_selected_rows() + if not path_list: + + # Nothing selected + return + + # Get the dummy media.Video objects for each selected row + video_list = [] + for path in path_list: + + this_iter = model.get_iter(path) + dbid = model[this_iter][0] + video_obj = self.main_win_obj.classic_media_dict[dbid] + video_list.append(video_obj) + + # If mainapp.TartubeApp.allow_ytdl_archive_flag is set, youtube-dl + # will have created a ytdl_archive.txt, recording every video + # ever downloaded in the parent directory + # This will prevent a successful re-downloading of the video(s). + # Change the name of the archive file temporarily; after the + # download operation is complete, the file is give its original + # name + self.set_backup_archive(video_obj.dummy_dir) + + # Start the download operation + self.download_manager_start('classic', False, video_list) + + + def on_button_classic_remove(self, action, par): + + """Called from a callback in self.do_startup(). + + Removes the selected rows from the Classic Progress List. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 13232 on_button_classic_remove') + + selection = self.main_win_obj.classic_progress_treeview.get_selection() + (model, path_list) = selection.get_selected_rows() + if not path_list: + + # Nothing selected + return + + # Get the .dbid of the dummy media.Video objects for each selected + # row + dbid_list = [] + for path in path_list: + + this_iter = model.get_iter(path) + dbid_list.append(model[this_iter][0]) + + # Prompt for confirmation + msg = _('Are you sure you want to remove the selected item(s)?') + + self.dialogue_manager_obj.show_msg_dialogue( + msg, + 'question', + 'yes-no', + None, # Parent window is main window + { + 'yes': 'main_win_classic_mode_tab_remove_rows', + # Specified options + 'data': dbid_list, + }, + ) + + + def on_button_classic_stop(self, action, par): """Called from a callback in self.do_startup(). - Cancels the filter, restoring all hidden videos in the Video Catalogue. + If a download operation is in progress, halts downloads for any of + the selected rows in the Classic Progress List. Args: @@ -11158,17 +13278,34 @@ def on_button_cancel_filter(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11102 on_button_cancel_filter') + utils.debug_time('app 13281 on_button_classic_stop') - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 147, - 'Cancel filter request failed sanity check', - ) + selection = self.main_win_obj.classic_progress_treeview.get_selection() + (model, path_list) = selection.get_selected_rows() + if not path_list: - # Cancel the filter - self.main_win_obj.video_catalogue_cancel_filter() + # Nothing selected + return + + # Get the .dbid of the dummy media.Video objects for each selected + # row + dbid_dict = {} + for path in path_list: + + this_iter = model.get_iter(path) + dbid_dict[model[this_iter][0]] = None + + # Now, if a download operation is in progress, stop any downloads + # matching one of these dbids + if self.download_manager_obj: + + for worker_obj in self.download_manager_obj.worker_list: + + if worker_obj.running_flag \ + and worker_obj.download_item_obj \ + and worker_obj.download_item_obj.media_data_obj.dbid \ + in dbid_dict: + worker_obj.video_downloader_obj.stop() def on_button_find_date(self, action, par): @@ -11188,12 +13325,12 @@ def on_button_find_date(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11132 on_button_find_date') + utils.debug_time('app 13328 on_button_find_date') # Sanity check if not self.main_win_obj.video_catalogue_dict: return self.system_error( - 148, + 153, 'Find videos by date request failed sanity check', ) @@ -11259,7 +13396,7 @@ def on_button_first_page(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11203 on_button_first_page') + utils.debug_time('app 13399 on_button_first_page') self.main_win_obj.video_catalogue_redraw_all( self.main_win_obj.video_index_current, @@ -11282,7 +13419,7 @@ def on_button_last_page(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11226 on_button_last_page') + utils.debug_time('app 13422 on_button_last_page') self.main_win_obj.video_catalogue_redraw_all( self.main_win_obj.video_index_current, @@ -11305,7 +13442,7 @@ def on_button_next_page(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11249 on_button_next_page') + utils.debug_time('app 13445 on_button_next_page') self.main_win_obj.video_catalogue_redraw_all( self.main_win_obj.video_index_current, @@ -11328,7 +13465,7 @@ def on_button_previous_page(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11272 on_button_previous_page') + utils.debug_time('app 13468 on_button_previous_page') self.main_win_obj.video_catalogue_redraw_all( self.main_win_obj.video_index_current, @@ -11351,7 +13488,7 @@ def on_button_scroll_down(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11295 on_button_scroll_down') + utils.debug_time('app 13491 on_button_scroll_down') adjust = self.main_win_obj.catalogue_scrolled.get_vadjustment() adjust.set_value(adjust.get_upper()) @@ -11372,7 +13509,7 @@ def on_button_scroll_up(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11316 on_button_scroll_up') + utils.debug_time('app 13512 on_button_scroll_up') self.main_win_obj.catalogue_scrolled.get_vadjustment().set_value(0) @@ -11393,7 +13530,7 @@ def on_button_show_filter(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11337 on_button_show_filter') + utils.debug_time('app 13533 on_button_show_filter') if not self.catalogue_show_filter_flag: self.catalogue_show_filter_flag = True @@ -11420,12 +13557,12 @@ def on_button_sort_type(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11364 on_button_sort_type') + utils.debug_time('app 13560 on_button_sort_type') # Sanity check if not self.main_win_obj.video_catalogue_dict: return self.system_error( - 149, + 154, 'Change catalogue sort type request failed sanity check', ) @@ -11450,7 +13587,9 @@ def on_button_stop_operation(self, action, par): """Called from a callback in self.do_startup(). - Stops the current download/update/refresh operation. + 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). Args: @@ -11461,10 +13600,12 @@ def on_button_stop_operation(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11405 on_button_stop_operation') + utils.debug_time('app 13603 on_button_stop_operation') self.operation_halted_flag = True + # (The livestream operation runs silently in the background, so the + # toolbar button is desensitised and can't be used to stop it) if self.download_manager_obj: self.download_manager_obj.stop_download_operation() elif self.update_manager_obj: @@ -11494,7 +13635,7 @@ def on_button_switch_view(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11438 on_button_switch_view') + utils.debug_time('app 13638 on_button_switch_view') # There are four modes in a fixed sequence; switch to the next mode in # the sequence @@ -11536,12 +13677,12 @@ def on_button_use_regex(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11480 on_button_use_regex') + utils.debug_time('app 13680 on_button_use_regex') # Sanity check if not self.main_win_obj.video_catalogue_dict: return self.system_error( - 150, + 155, 'Use regex request failed sanity check', ) @@ -11566,7 +13707,7 @@ def on_menu_about(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11510 on_menu_about') + utils.debug_time('app 13710 on_menu_about') dialogue_win = Gtk.AboutDialog() dialogue_win.set_transient_for(self.main_win_obj) @@ -11606,7 +13747,7 @@ def on_menu_about_close(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11550 on_menu_about_close') + utils.debug_time('app 13750 on_menu_about_close') action.destroy() @@ -11627,7 +13768,7 @@ def on_menu_add_channel(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11571 on_menu_add_channel') + utils.debug_time('app 13771 on_menu_add_channel') keep_open_flag = True dl_sim_flag = False @@ -11686,7 +13827,7 @@ def on_menu_add_channel(self, action, par): keep_open_flag = False self.dialogue_manager_obj.show_msg_dialogue( - 'You must give the channel a name', + _('You must give the channel a name'), 'error', 'ok', ) @@ -11695,7 +13836,7 @@ def on_menu_add_channel(self, action, par): keep_open_flag = False self.dialogue_manager_obj.show_msg_dialogue( - 'The name \'' + name + '\' is not allowed', + _('The name \'{0}\' is not allowed').format(name), 'error', 'ok', ) @@ -11704,7 +13845,7 @@ def on_menu_add_channel(self, action, par): keep_open_flag = False self.dialogue_manager_obj.show_msg_dialogue( - 'You must enter a valid URL', + _('You must enter a valid URL'), 'error', 'ok', ) @@ -11781,7 +13922,7 @@ def on_menu_add_folder(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11725 on_menu_add_folder') + utils.debug_time('app 13925 on_menu_add_folder') # If a folder is selected in the Video Index, the dialogue window # should suggest that as the new folder's parent folder @@ -11819,7 +13960,7 @@ def on_menu_add_folder(self, action, par): if name is None or re.match('\s*$', name): self.dialogue_manager_obj.show_msg_dialogue( - 'You must give the folder a name', + _('You must give the folder a name'), 'error', 'ok', ) @@ -11827,7 +13968,7 @@ def on_menu_add_folder(self, action, par): elif not self.check_container_name_is_legal(name): self.dialogue_manager_obj.show_msg_dialogue( - 'The name \'' + name + '\' is not allowed', + _('The name \'{0}\' is not allowed').format(name), 'error', 'ok', ) @@ -11890,7 +14031,7 @@ def on_menu_add_playlist(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11834 on_menu_add_playlist') + utils.debug_time('app 14034 on_menu_add_playlist') keep_open_flag = True dl_sim_flag = False @@ -11949,7 +14090,7 @@ def on_menu_add_playlist(self, action, par): keep_open_flag = False self.dialogue_manager_obj.show_msg_dialogue( - 'You must give the playlist a name', + _('You must give the playlist a name'), 'error', 'ok', ) @@ -11958,7 +14099,7 @@ def on_menu_add_playlist(self, action, par): keep_open_flag = False self.dialogue_manager_obj.show_msg_dialogue( - 'The name \'' + name + '\' is not allowed', + _('The name \'{0}\' is not allowed').format(name), 'error', 'ok', ) @@ -11967,7 +14108,7 @@ def on_menu_add_playlist(self, action, par): keep_open_flag = False self.dialogue_manager_obj.show_msg_dialogue( - 'You must enter a valid URL', + _('You must enter a valid URL'), 'error', 'ok', ) @@ -12044,7 +14185,7 @@ def on_menu_add_video(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 11988 on_menu_add_video') + utils.debug_time('app 14188 on_menu_add_video') dialogue_win = mainwin.AddVideoDialogue(self.main_win_obj) response = dialogue_win.run() @@ -12104,7 +14245,7 @@ def on_menu_add_video(self, action, par): # If any duplicates were found, inform the user if duplicate_list: - msg = 'The following videos are duplicates:\n\n' + msg = _('The following videos are duplicates:') for line in duplicate_list: msg += '\n\n' + line @@ -12115,6 +14256,75 @@ def on_menu_add_video(self, action, par): ) + def on_menu_cancel_live(self, action, par): + + """Called from a callback in self.do_startup(). + + Cancels all livestream actions (auto-notify, auto-open, download at + start, download at stop). + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 14275 on_menu_cancel_live') + + # The actions are stored in five different dictionaries. Compile a + # single dictionary, eliminating duplicates, so we can count how + # many media.Video objects are affected (and updte the Video + # Catalogue) + video_dict = {} + + for video_obj in self.media_reg_auto_notify_dict.values(): + video_dict[video_obj.dbid] = video_obj + + for video_obj in self.media_reg_auto_alarm_dict.values(): + video_dict[video_obj.dbid] = video_obj + + for video_obj in self.media_reg_auto_open_dict.values(): + video_dict[video_obj.dbid] = video_obj + + for video_obj in self.media_reg_auto_dl_start_dict.values(): + video_dict[video_obj.dbid] = video_obj + + for video_obj in self.media_reg_auto_dl_stop_dict.values(): + video_dict[video_obj.dbid] = video_obj + + # Cancel livestream actions by emptying the IVs + self.media_reg_auto_notify_dict = {} + self.media_reg_auto_alarm_dict = {} + self.media_reg_auto_open_dict = {} + self.media_reg_auto_dl_start_dict = {} + self.media_reg_auto_dl_stop_dict = {} + + # Update the Video Catalogue + for video_obj in video_dict.values(): + self.main_win_obj.video_catalogue_update_row(video_obj) + + # Show confirmation + count = len(video_dict) + if not count: + msg = _('There were no livestream alerts to cancel') + elif count == 1: + msg = _('Livestream alerts for 1 video were cancelled') + else: + msg = _( + 'Livestream alerts for {0} videos were cancelled', + ).format(str(count)) + + self.dialogue_manager_obj.show_msg_dialogue( + msg, + 'info', + 'ok', + None, # Parent window is main window + ) + + def on_menu_change_db(self, action, par): """Called from a callback in self.do_startup(). @@ -12131,9 +14341,9 @@ def on_menu_change_db(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12075 on_menu_change_db') + utils.debug_time('app 14344 on_menu_change_db') - config.SystemPrefWin(self, True) + config.SystemPrefWin(self, 'db') def on_menu_check_all(self, action, par): @@ -12151,7 +14361,7 @@ def on_menu_check_all(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12095 on_menu_check_all') + utils.debug_time('app 14363 on_menu_check_all') self.download_manager_start('sim') @@ -12171,7 +14381,7 @@ def on_menu_close_tray(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12115 on_menu_close_tray') + utils.debug_time('app 14384 on_menu_close_tray') self.main_win_obj.toggle_visibility() @@ -12192,7 +14402,7 @@ def on_menu_custom_dl_all(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12136 on_menu_custom_dl_all') + utils.debug_time('app 14405 on_menu_custom_dl_all') self.download_manager_start('custom') @@ -12212,7 +14422,7 @@ def on_menu_download_all(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12156 on_menu_download_all') + utils.debug_time('app 14425 on_menu_download_all') self.download_manager_start('real') @@ -12232,7 +14442,7 @@ def on_menu_export_db(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12176 on_menu_export_db') + utils.debug_time('app 14445 on_menu_export_db') self.export_from_db( [] ) @@ -12252,7 +14462,7 @@ def on_menu_general_options(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12196 on_menu_general_options') + utils.debug_time('app 14465 on_menu_general_options') config.OptionsEditWin(self, self.general_options_obj, None) @@ -12272,7 +14482,7 @@ def on_menu_go_website(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12216 on_menu_go_website') + utils.debug_time('app 14485 on_menu_go_website') utils.open_file(__main__.__website__) @@ -12292,7 +14502,7 @@ def on_menu_import_json(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12236 on_menu_import_json') + utils.debug_time('app 14505 on_menu_import_json') self.import_into_db(True) @@ -12313,7 +14523,7 @@ def on_menu_import_plain_text(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12257 on_menu_import_plain_text') + utils.debug_time('app 14526 on_menu_import_plain_text') self.import_into_db(False) @@ -12333,11 +14543,31 @@ def on_menu_install_ffmpeg(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12277 on_menu_install_ffmpeg') + utils.debug_time('app 14546 on_menu_install_ffmpeg') self.update_manager_start('ffmpeg') + def on_menu_live_preferences(self, action, par): + + """Called from a callback in self.do_startup(). + + Opens a preference window to edit livestream preferences. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 14566 on_menu_live_preferences') + + config.SystemPrefWin(self, 'live') + + def on_menu_refresh_db(self, action, par): """Called from a callback in self.do_startup(). @@ -12353,7 +14583,7 @@ def on_menu_refresh_db(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12297 on_menu_refresh_db') + utils.debug_time('app 14586 on_menu_refresh_db') self.refresh_manager_start() @@ -12373,7 +14603,7 @@ def on_menu_save_all(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12317 on_menu_save_all') + utils.debug_time('app 14606 on_menu_save_all') if not self.disable_load_save_flag: self.save_config() @@ -12385,7 +14615,7 @@ def on_menu_save_all(self, action, par): if not self.disable_load_save_flag: self.dialogue_manager_obj.show_msg_dialogue( - 'Data saved', + _('Data saved'), 'info', 'ok', ) @@ -12406,7 +14636,7 @@ def on_menu_save_db(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12350 on_menu_save_db') + utils.debug_time('app 14639 on_menu_save_db') self.save_db() @@ -12415,12 +14645,32 @@ def on_menu_save_db(self, action, par): if not self.disable_load_save_flag: self.dialogue_manager_obj.show_msg_dialogue( - 'Database saved', + _('Database saved'), 'info', 'ok', ) + def on_menu_send_feedback(self, action, par): + + """Called from a callback in self.do_startup(). + + Opens the Tartube feedback website. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 14669 on_menu_send_feedback') + + utils.open_file(__main__.__website_bugs__) + + def on_menu_show_hidden(self, action, par): """Called from a callback in self.do_startup(). @@ -12436,7 +14686,7 @@ def on_menu_show_hidden(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12380 on_menu_show_hidden') + utils.debug_time('app 14689 on_menu_show_hidden') for name in self.media_name_dict: @@ -12463,7 +14713,7 @@ def on_menu_system_preferences(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12407 on_menu_system_preferences') + utils.debug_time('app 14716 on_menu_system_preferences') config.SystemPrefWin(self) @@ -12484,7 +14734,7 @@ def on_menu_test(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12428 on_menu_test') + utils.debug_time('app 14737 on_menu_test') # Add media data objects for testing: videos, channels, playlists and/ # or folders @@ -12517,7 +14767,7 @@ def on_menu_test_ytdl(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12461 on_menu_test_ytdl') + utils.debug_time('app 14770 on_menu_test_ytdl') # Prompt the user for what should be tested dialogue_win = mainwin.TestCmdDialogue(self.main_win_obj) @@ -12564,7 +14814,7 @@ def on_menu_tidy_up(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12508 on_menu_tidy_up') + utils.debug_time('app 14817 on_menu_tidy_up') # Prompt the user to specify which actions should be applied to # Tartube's data directory @@ -12615,8 +14865,10 @@ def on_menu_tidy_up(self, action, par): or choices_dict['del_archive_flag']: self.dialogue_manager_obj.show_msg_dialogue( + _( 'Files cannot be recovered, after being deleted. Are you' \ + ' sure you want to continue?', + ), 'question', 'yes-no', None, # Parent window is main window @@ -12633,6 +14885,43 @@ def on_menu_tidy_up(self, action, par): self.tidy_manager_start(choices_dict) + def on_menu_update_live(self, action, par): + + """Called from a callback in self.do_startup(). + + Forces the livestream operation to start. Ignored if any operation + (including an existing livestream operation) is running. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 14904 on_menu_update_live') + + # 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 + msg = _('Cannot update existing livestreams because') + if self.current_manager_obj: + msg += ' ' + _('there is another operation running') + elif self.livestream_manager_obj: + msg += ' ' + _('they are currently being updated') + elif self.main_win_obj.config_win_list: + msg += ' ' + _('one or more configuration windows are open') + elif not self.media_reg_live_dict: + msg += ' ' + _('there are no livestreams to update') + else: + self.livestream_manager_start() + return + + self.dialogue_manager_obj.show_msg_dialogue(msg, 'error', 'ok') + + def on_menu_update_ytdl(self, action, par): """Called from a callback in self.do_startup(). @@ -12648,7 +14937,7 @@ def on_menu_update_ytdl(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12592 on_menu_update_ytdl') + utils.debug_time('app 14940 on_menu_update_ytdl') self.update_manager_start('ytdl') @@ -12668,7 +14957,7 @@ def on_menu_quit(self, action, par): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12612 on_menu_quit') + utils.debug_time('app 14960 on_menu_quit') self.stop() @@ -12692,16 +14981,21 @@ def reject_container_name(self, name): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12636 reject_container_name') + utils.debug_time('app 14984 reject_container_name') # Get the existing media data object with this name dbid = self.media_name_dict[name] media_data_obj = self.media_reg_dict[dbid] media_type = media_data_obj.get_type() + if media_type == 'channel': + msg = _('There is already a channel with that name') + elif media_type == 'playlist': + msg = _('There is already a playlist with that name') + else: + msg = _('There is already a folder with that name') self.dialogue_manager_obj.show_msg_dialogue( - 'There is already a ' + media_type + ' with that name ' \ - + '(so please choose a different name)', + msg + ' ' + _('(so please choose a different name)'), 'error', 'ok', ) @@ -12713,7 +15007,7 @@ def reject_container_name(self, name): def set_allow_ytdl_archive_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12657 set_allow_ytdl_archive_flag') + utils.debug_time('app 15010 set_allow_ytdl_archive_flag') if not flag: self.allow_ytdl_archive_flag = False @@ -12724,7 +15018,7 @@ def set_allow_ytdl_archive_flag(self, flag): def set_apply_json_timeout_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12668 set_apply_json_timeout_flag') + utils.debug_time('app 15021 set_apply_json_timeout_flag') if not flag: self.apply_json_timeout_flag = False @@ -12732,10 +15026,27 @@ def set_apply_json_timeout_flag(self, flag): self.apply_json_timeout_flag = True + def add_auto_alarm_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15032 add_auto_alarm_dict') + + self.media_reg_auto_alarm_dict[video_obj.dbid] = video_obj + + + def del_auto_alarm_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15040 del_auto_alarm_dict') + + if video_obj.dbid in self.media_reg_auto_alarm_dict: + del self.media_reg_auto_alarm_dict[video_obj.dbid] + + def set_auto_clone_options_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12679 set_auto_clone_options_flag') + utils.debug_time('app 15049 set_auto_clone_options_flag') if not flag: self.auto_clone_options_flag = False @@ -12746,7 +15057,7 @@ def set_auto_clone_options_flag(self, flag): def set_auto_delete_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12690 set_auto_delete_flag') + utils.debug_time('app 15060 set_auto_delete_flag') if not flag: self.auto_delete_flag = False @@ -12757,7 +15068,7 @@ def set_auto_delete_flag(self, flag): def set_auto_delete_days(self, days): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12701 set_auto_delete_days') + utils.debug_time('app 15071 set_auto_delete_days') self.auto_delete_days = days @@ -12765,7 +15076,7 @@ def set_auto_delete_days(self, days): def set_auto_delete_watched_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12709 set_auto_delete_watched_flag') + utils.debug_time('app 15079 set_auto_delete_watched_flag') if not flag: self.auto_delete_watched_flag = False @@ -12776,7 +15087,7 @@ def set_auto_delete_watched_flag(self, flag): def set_auto_expand_video_index_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12720 set_auto_expand_video_index_flag') + utils.debug_time('app 15090 set_auto_expand_video_index_flag') if not flag: self.auto_expand_video_index_flag = False @@ -12784,10 +15095,78 @@ def set_auto_expand_video_index_flag(self, flag): self.auto_expand_video_index_flag = True + def add_auto_dl_start_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15101 add_auto_dl_start_dict') + + self.media_reg_auto_dl_start_dict[video_obj.dbid] = video_obj + + + def del_auto_dl_start_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15109 del_auto_dl_start_dict') + + if video_obj.dbid in self.media_reg_auto_dl_start_dict: + del self.media_reg_auto_dl_start_dict[video_obj.dbid] + + + def add_auto_dl_stop_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15118 add_auto_dl_stop_dict') + + self.media_reg_auto_dl_stop_dict[video_obj.dbid] = video_obj + + + def del_auto_dl_stop_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15126 del_auto_dl_stop_dict') + + if video_obj.dbid in self.media_reg_auto_dl_stop_dict: + del self.media_reg_auto_dl_stop_dict[video_obj.dbid] + + + def add_auto_notify_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15135 add_auto_notify_dict') + + self.media_reg_auto_notify_dict[video_obj.dbid] = video_obj + + + def del_auto_notify_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15143 del_auto_notify_dict') + + if video_obj.dbid in self.media_reg_auto_notify_dict: + del self.media_reg_auto_notify_dict[video_obj.dbid] + + + def add_auto_open_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15152 add_auto_open_dict') + + self.media_reg_auto_open_dict[video_obj.dbid] = video_obj + + + def del_auto_open_dict(self, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15160 del_auto_open_dict') + + if video_obj.dbid in self.media_reg_auto_open_dict: + del self.media_reg_auto_open_dict[video_obj.dbid] + + def set_autostop_size_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12731 set_autostop_size_flag') + utils.debug_time('app 15169 set_autostop_size_flag') if not flag: self.autostop_size_flag = False @@ -12798,7 +15177,7 @@ def set_autostop_size_flag(self, flag): def set_autostop_size_unit(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12742 set_autostop_size_unit') + utils.debug_time('app 15180 set_autostop_size_unit') self.autostop_size_unit = value @@ -12806,7 +15185,7 @@ def set_autostop_size_unit(self, value): def set_autostop_size_value(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12750 set_autostop_size_value') + utils.debug_time('app 15188 set_autostop_size_value') self.autostop_size_value = value @@ -12814,7 +15193,7 @@ def set_autostop_size_value(self, value): def set_autostop_time_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12758 set_autostop_time_flag') + utils.debug_time('app 15196 set_autostop_time_flag') if not flag: self.autostop_time_flag = False @@ -12825,7 +15204,7 @@ def set_autostop_time_flag(self, flag): def set_autostop_time_unit(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12769 set_autostop_time_unit') + utils.debug_time('app 15207 set_autostop_time_unit') self.autostop_time_unit = value @@ -12833,7 +15212,7 @@ def set_autostop_time_unit(self, value): def set_autostop_time_value(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12777 set_autostop_time_value') + utils.debug_time('app 15215 set_autostop_time_value') self.autostop_time_value = value @@ -12841,7 +15220,7 @@ def set_autostop_time_value(self, value): def set_autostop_videos_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12785 set_autostop_videos_flag') + utils.debug_time('app 15223 set_autostop_videos_flag') if not flag: self.autostop_videos_flag = False @@ -12852,7 +15231,7 @@ def set_autostop_videos_flag(self, flag): def set_autostop_videos_value(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12796 set_autostop_videos_value') + utils.debug_time('app 15234 set_autostop_videos_value') self.autostop_videos_value = value @@ -12866,7 +15245,7 @@ def set_bandwidth_apply_flag(self, flag): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12810 set_bandwidth_apply_flag') + utils.debug_time('app 15248 set_bandwidth_apply_flag') if not flag: self.bandwidth_apply_flag = False @@ -12883,11 +15262,11 @@ def set_bandwidth_default(self, value): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12827 set_bandwidth_default') + utils.debug_time('app 15265 set_bandwidth_default') if value < self.bandwidth_min or value > self.bandwidth_max: return self.system_error( - 151, + 156, 'Set bandwidth request failed sanity check', ) @@ -12897,15 +15276,23 @@ def set_bandwidth_default(self, value): def set_catalogue_page_size(self, size): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12841 set_catalogue_page_size') + utils.debug_time('app 15279 set_catalogue_page_size') self.catalogue_page_size = size + def set_classic_dir_previous(self, directory): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15287 set_classic_dir_previous') + + self.classic_dir_previous = directory + + def set_close_to_tray_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12849 set_close_to_tray_flag') + utils.debug_time('app 15295 set_close_to_tray_flag') if not flag: self.close_to_tray_flag = False @@ -12916,7 +15303,7 @@ def set_close_to_tray_flag(self, flag): def set_complex_index_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12860 set_complex_index_flag') + utils.debug_time('app 15306 set_complex_index_flag') if not flag: self.complex_index_flag = False @@ -12927,7 +15314,7 @@ def set_complex_index_flag(self, flag): def set_custom_dl_by_video_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12871 set_custom_dl_by_video_flag') + utils.debug_time('app 15317 set_custom_dl_by_video_flag') if not flag: self.custom_dl_by_video_flag = False @@ -12938,7 +15325,7 @@ def set_custom_dl_by_video_flag(self, flag): def set_custom_dl_delay_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12882 set_custom_dl_delay_flag') + utils.debug_time('app 15328 set_custom_dl_delay_flag') if not flag: self.custom_dl_delay_flag = False @@ -12949,7 +15336,7 @@ def set_custom_dl_delay_flag(self, flag): def set_custom_dl_delay_max(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12893 set_custom_dl_delay_max') + utils.debug_time('app 15339 set_custom_dl_delay_max') self.custom_dl_delay_max = value @@ -12957,7 +15344,7 @@ def set_custom_dl_delay_max(self, value): def set_custom_dl_delay_min(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12901 set_custom_dl_delay_min') + utils.debug_time('app 15345 set_custom_dl_delay_min') self.custom_dl_delay_min = value @@ -12965,11 +15352,20 @@ def set_custom_dl_delay_min(self, value): def set_custom_dl_divert_mode(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12909 set_custom_dl_divert_mode') + utils.debug_time('app 15355 set_custom_dl_divert_mode') self.custom_dl_divert_mode = value + def set_custom_locale(self, value): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15363 set_custom_locale') + + self.custom_locale = value + self.update_locale_flag = True + + def set_data_dir(self, path): """Called by mainwin.MountDriveDialogue.on_button_clicked() only; @@ -12980,7 +15376,7 @@ def set_data_dir(self, path): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12924 set_data_dir') + utils.debug_time('app 15379 set_data_dir') self.data_dir = path @@ -12995,7 +15391,7 @@ def reset_data_dir(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 12939 reset_data_dir') + utils.debug_time('app 15394 reset_data_dir') self.data_dir = self.default_data_dir @@ -13003,7 +15399,7 @@ def reset_data_dir(self): def set_data_dir_add_from_list_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12947 set_data_dir_add_from_list_flag') + utils.debug_time('app 15402 set_data_dir_add_from_list_flag') if not flag: self.data_dir_add_from_list_flag = False @@ -13011,10 +15407,18 @@ def set_data_dir_add_from_list_flag(self, flag): self.data_dir_add_from_list_flag = True + def set_data_dir_alt_list(self, dir_list): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15413 set_data_dir_add_from_list_flag') + + self.data_dir_alt_list = dir_list.copy() + + def set_data_dir_use_first_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12958 set_data_dir_use_first_flag') + utils.debug_time('app 15421 set_data_dir_use_first_flag') if not flag: self.data_dir_use_first_flag = False @@ -13025,7 +15429,7 @@ def set_data_dir_use_first_flag(self, flag): def set_data_dir_use_list_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12969 set_data_dir_use_list_flag') + utils.debug_time('app 15432 set_data_dir_use_list_flag') if not flag: self.data_dir_use_list_flag = False @@ -13036,7 +15440,7 @@ def set_data_dir_use_list_flag(self, flag): def set_db_backup_mode(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12980 set_db_backup_mode') + utils.debug_time('app 15443 set_db_backup_mode') self.db_backup_mode = value @@ -13044,7 +15448,7 @@ def set_db_backup_mode(self, value): def set_delete_on_shutdown_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12988 set_delete_on_shutdown_flag') + utils.debug_time('app 15451 set_delete_on_shutdown_flag') if not flag: self.delete_on_shutdown_flag = False @@ -13055,7 +15459,7 @@ def set_delete_on_shutdown_flag(self, flag): def set_dialogue_copy_clipboard_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 12999 set_dialogue_copy_clipboard_flag') + utils.debug_time('app 15462 set_dialogue_copy_clipboard_flag') if not flag: self.dialogue_copy_clipboard_flag = False @@ -13066,7 +15470,7 @@ def set_dialogue_copy_clipboard_flag(self, flag): def set_dialogue_keep_open_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13010 set_dialogue_keep_open_flag') + utils.debug_time('app 15473 set_dialogue_keep_open_flag') if not flag: self.dialogue_keep_open_flag = False @@ -13077,7 +15481,7 @@ def set_dialogue_keep_open_flag(self, flag): def set_disable_dl_all_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13021 set_disable_dl_all_flag') + utils.debug_time('app 15484 set_disable_dl_all_flag') if not flag: self.disable_dl_all_flag = False @@ -13091,7 +15495,7 @@ def set_disable_dl_all_flag(self, flag): def set_disk_space_stop_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13035 set_disk_space_stop_flag') + utils.debug_time('app 15498 set_disk_space_stop_flag') if not flag: self.disk_space_stop_flag = False @@ -13102,7 +15506,7 @@ def set_disk_space_stop_flag(self, flag): def set_disk_space_stop_limit(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13046 set_disk_space_stop_limit') + utils.debug_time('app 15509 set_disk_space_stop_limit') self.disk_space_stop_limit = value @@ -13110,7 +15514,7 @@ def set_disk_space_stop_limit(self, value): def set_disk_space_warn_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13054 set_disk_space_warn_flag') + utils.debug_time('app 15517 set_disk_space_warn_flag') if not flag: self.disk_space_warn_flag = False @@ -13121,15 +15525,26 @@ def set_disk_space_warn_flag(self, flag): def set_disk_space_warn_limit(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13065 set_disk_space_warn_limit') + utils.debug_time('app 15528 set_disk_space_warn_limit') self.disk_space_warn_limit = value + def set_enable_livestreams_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15536 set_enable_livestreams_flag') + + if not flag: + self.enable_livestreams_flag = False + else: + self.enable_livestreams_flag = True + + def set_ffmpeg_path(self, path): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13073 set_ffmpeg_path') + utils.debug_time('app 15547 set_ffmpeg_path') self.ffmpeg_path = path @@ -13137,7 +15552,7 @@ def set_ffmpeg_path(self, path): def set_full_expand_video_index_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13074 set_full_expand_video_index_flag') + utils.debug_time('app 15555 set_full_expand_video_index_flag') if not flag: self.full_expand_video_index_flag = False @@ -13148,7 +15563,7 @@ def set_full_expand_video_index_flag(self, flag): def set_gtk_emulate_broken_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13081 set_gtk_emulate_broken_flag') + utils.debug_time('app 15566 set_gtk_emulate_broken_flag') if not flag: self.gtk_emulate_broken_flag = False @@ -13159,7 +15574,7 @@ def set_gtk_emulate_broken_flag(self, flag): def set_ignore_child_process_exit_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13092 set_ignore_child_process_exit_flag') + utils.debug_time('app 15577 set_ignore_child_process_exit_flag') if not flag: self.ignore_child_process_exit_flag = False @@ -13170,7 +15585,7 @@ def set_ignore_child_process_exit_flag(self, flag): def set_ignore_custom_msg_list(self, custom_list): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13103 set_ignore_custom_msg_list') + utils.debug_time('app 15588 set_ignore_custom_msg_list') self.ignore_custom_msg_list = custom_list.copy() @@ -13178,7 +15593,7 @@ def set_ignore_custom_msg_list(self, custom_list): def set_ignore_custom_regex_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13111 set_ignore_custom_regex_flag') + utils.debug_time('app 15596 set_ignore_custom_regex_flag') if not flag: self.ignore_custom_regex_flag = False @@ -13189,7 +15604,7 @@ def set_ignore_custom_regex_flag(self, flag): def set_ignore_data_block_error_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13122 set_ignore_data_block_error_flag') + utils.debug_time('app 15607 set_ignore_data_block_error_flag') if not flag: self.ignore_data_block_error_flag = False @@ -13200,7 +15615,7 @@ def set_ignore_data_block_error_flag(self, flag): def set_ignore_http_404_error_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13133 set_ignore_http_404_error_flag') + utils.debug_time('app 15618 set_ignore_http_404_error_flag') if not flag: self.ignore_http_404_error_flag = False @@ -13211,7 +15626,7 @@ def set_ignore_http_404_error_flag(self, flag): def set_ignore_merge_warning_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13144 set_ignore_merge_warning_flag') + utils.debug_time('app 15629 set_ignore_merge_warning_flag') if not flag: self.ignore_merge_warning_flag = False @@ -13222,7 +15637,7 @@ def set_ignore_merge_warning_flag(self, flag): def set_ignore_missing_format_error_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13155 set_ignore_missing_format_error_flag') + utils.debug_time('app 15640 set_ignore_missing_format_error_flag') if not flag: self.ignore_missing_format_error_flag = False @@ -13233,7 +15648,7 @@ def set_ignore_missing_format_error_flag(self, flag): def set_ignore_no_annotations_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13166 set_ignore_no_annotations_flag') + utils.debug_time('app 15651 set_ignore_no_annotations_flag') if not flag: self.ignore_no_annotations_flag = False @@ -13244,7 +15659,7 @@ def set_ignore_no_annotations_flag(self, flag): def set_ignore_no_subtitles_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13177 set_ignore_no_subtitles_flag') + utils.debug_time('app 15662 set_ignore_no_subtitles_flag') if not flag: self.ignore_no_subtitles_flag = False @@ -13255,7 +15670,7 @@ def set_ignore_no_subtitles_flag(self, flag): def set_ignore_yt_age_restrict_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13188 set_ignore_yt_age_restrict_flag') + utils.debug_time('app 15673 set_ignore_yt_age_restrict_flag') if not flag: self.ignore_yt_age_restrict_flag = False @@ -13266,7 +15681,7 @@ def set_ignore_yt_age_restrict_flag(self, flag): def set_ignore_yt_copyright_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13199 set_ignore_yt_copyright_flag') + utils.debug_time('app 15684 set_ignore_yt_copyright_flag') if not flag: self.ignore_yt_copyright_flag = False @@ -13277,7 +15692,7 @@ def set_ignore_yt_copyright_flag(self, flag): def set_ignore_yt_uploader_deleted_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13210 set_ignore_yt_uploader_deleted_flag') + utils.debug_time('app 15695 set_ignore_yt_uploader_deleted_flag') if not flag: self.ignore_yt_uploader_deleted_flag = False @@ -13285,10 +15700,84 @@ def set_ignore_yt_uploader_deleted_flag(self, flag): self.ignore_yt_uploader_deleted_flag = True + def set_livestream_max_days(self, value): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15706 set_livestream_max_days') + + self.livestream_max_days = value + + + def set_livestream_auto_alarm_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15714 set_livestream_auto_alarm_flag') + + if not flag: + self.livestream_auto_alarm_flag = False + else: + self.livestream_auto_alarm_flag = True + + + def set_livestream_auto_dl_start_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15725 set_livestream_auto_dl_start_flag') + + if not flag: + self.livestream_auto_dl_start_flag = False + else: + self.livestream_auto_dl_start_flag = True + + + def set_livestream_auto_dl_stop_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15736 set_livestream_auto_dl_stop_flag') + + if not flag: + self.livestream_auto_dl_stop_flag = False + else: + self.livestream_auto_dl_stop_flag = True + + + def set_livestream_auto_notify_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15747 set_livestream_auto_notify_flag') + + if not flag: + self.livestream_auto_notify_flag = False + else: + self.livestream_auto_notify_flag = True + + + def set_livestream_auto_open_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15758 set_livestream_auto_open_flag') + + if not flag: + self.livestream_auto_open_flag = False + else: + self.livestream_auto_open_flag = True + + + def set_livestream_use_colour_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 15769 set_livestream_use_colour_flag') + + if not flag: + self.livestream_use_colour_flag = False + else: + self.livestream_use_colour_flag = True + + def set_main_win_save_size_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13221 set_main_win_save_size_flag') + utils.debug_time('app 15780 set_main_win_save_size_flag') if not flag: self.main_win_save_size_flag = False @@ -13303,7 +15792,7 @@ def set_main_win_save_size_flag(self, flag): def set_match_first_chars(self, num_chars): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13236 set_match_first_chars') + utils.debug_time('app 15795 set_match_first_chars') self.match_first_chars = num_chars @@ -13311,7 +15800,7 @@ def set_match_first_chars(self, num_chars): def set_match_ignore_chars(self, num_chars): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13244 set_match_ignore_chars') + utils.debug_time('app 15803 set_match_ignore_chars') self.match_ignore_chars = num_chars @@ -13319,7 +15808,7 @@ def set_match_ignore_chars(self, num_chars): def set_match_method(self, method): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13253 set_match_method') + utils.debug_time('app 15811 set_match_method') self.match_method = method @@ -13334,7 +15823,7 @@ def set_num_worker_apply_flag(self, flag): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 13267 set_num_worker_apply_flag') + utils.debug_time('app 15826 set_num_worker_apply_flag') if not flag: self.bandwidth_apply_flag = False @@ -13354,11 +15843,11 @@ def set_num_worker_default(self, value): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 13287 set_num_worker_default') + utils.debug_time('app 15846 set_num_worker_default') if value < self.num_worker_min or value > self.num_worker_max: return self.system_error( - 152, + 157, 'Set simultaneous downloads request failed sanity check', ) @@ -13375,7 +15864,7 @@ def set_num_worker_default(self, value): def set_open_temp_on_desktop_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13308 set_open_temp_on_desktop_flag') + utils.debug_time('app 15687 set_open_temp_on_desktop_flag') if not flag: self.open_temp_on_desktop_flag = False @@ -13386,7 +15875,7 @@ def set_open_temp_on_desktop_flag(self, flag): def set_operation_auto_update_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13319 set_operation_auto_update_flag') + utils.debug_time('app 15878 set_operation_auto_update_flag') if not flag: self.operation_auto_update_flag = False @@ -13397,7 +15886,7 @@ def set_operation_auto_update_flag(self, flag): def set_operation_check_limit(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13330 set_operation_check_limit') + utils.debug_time('app 15889 set_operation_check_limit') self.operation_check_limit = value @@ -13405,7 +15894,7 @@ def set_operation_check_limit(self, value): def set_operation_convert_mode(self, mode): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13338 set_operation_convert_mode') + utils.debug_time('app 15897 set_operation_convert_mode') if mode == 'disable' or mode == 'multi' or mode == 'channel' \ or mode == 'playlist': @@ -13415,7 +15904,7 @@ def set_operation_convert_mode(self, mode): def set_operation_dialogue_mode(self, mode): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13348 set_operation_dialogue_mode') + utils.debug_time('app 15907 set_operation_dialogue_mode') if mode == 'default' or mode == 'desktop' or mode == 'dialogue': self.operation_dialogue_mode = mode @@ -13424,7 +15913,7 @@ def set_operation_dialogue_mode(self, mode): def set_operation_download_limit(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13357 set_operation_download_limit') + utils.debug_time('app 15916 set_operation_download_limit') self.operation_download_limit = value @@ -13432,7 +15921,7 @@ def set_operation_download_limit(self, value): def set_operation_error_show_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13365 set_operation_error_show_flag') + utils.debug_time('app 15924 set_operation_error_show_flag') if not flag: self.operation_error_show_flag = False @@ -13443,7 +15932,7 @@ def set_operation_error_show_flag(self, flag): def set_operation_halted_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13376 set_operation_halted_flag') + utils.debug_time('app 15935 set_operation_halted_flag') if not flag: self.operation_halted_flag = False @@ -13454,7 +15943,7 @@ def set_operation_halted_flag(self, flag): def set_operation_limit_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13387 set_operation_limit_flag') + utils.debug_time('app 15946 set_operation_limit_flag') if not flag: self.operation_limit_flag = False @@ -13465,7 +15954,7 @@ def set_operation_limit_flag(self, flag): def set_operation_save_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13398 set_operation_save_flag') + utils.debug_time('app 15957 set_operation_save_flag') if not flag: self.operation_save_flag = False @@ -13476,7 +15965,7 @@ def set_operation_save_flag(self, flag): def set_operation_sim_shortcut_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13409 set_operation_sim_shortcut_flag') + utils.debug_time('app 15968 set_operation_sim_shortcut_flag') if not flag: self.operation_sim_shortcut_flag = False @@ -13487,7 +15976,7 @@ def set_operation_sim_shortcut_flag(self, flag): def set_operation_warning_show_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13420 set_operation_warning_show_flag') + utils.debug_time('app 15979 set_operation_warning_show_flag') if not flag: self.operation_warning_show_flag = False @@ -13498,7 +15987,7 @@ def set_operation_warning_show_flag(self, flag): def set_progress_list_hide_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13431 set_progress_list_hide_flag') + utils.debug_time('app 15990 set_progress_list_hide_flag') if not flag: self.progress_list_hide_flag = False @@ -13513,7 +16002,7 @@ def set_progress_list_hide_flag(self, flag): def set_refresh_moviepy_timeout(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13446 set_refresh_moviepy_timeout') + utils.debug_time('app 16005 set_refresh_moviepy_timeout') self.refresh_moviepy_timeout = value @@ -13521,7 +16010,7 @@ def set_refresh_moviepy_timeout(self, value): def set_refresh_output_verbose_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13454 set_refresh_output_verbose_flag') + utils.debug_time('app 16013 set_refresh_output_verbose_flag') if not flag: self.refresh_output_verbose_flag = False @@ -13532,7 +16021,7 @@ def set_refresh_output_verbose_flag(self, flag): def set_refresh_output_videos_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13465 set_refresh_output_videos_flag') + utils.debug_time('app 16024 set_refresh_output_videos_flag') if not flag: self.refresh_output_videos_flag = False @@ -13543,7 +16032,7 @@ def set_refresh_output_videos_flag(self, flag): def set_results_list_reverse_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13476 set_results_list_reverse_flag') + utils.debug_time('app 16035 set_results_list_reverse_flag') if not flag: self.results_list_reverse_flag = False @@ -13554,7 +16043,7 @@ def set_results_list_reverse_flag(self, flag): def set_scheduled_check_mode(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13487 set_scheduled_check_mode') + utils.debug_time('app 16046 set_scheduled_check_mode') self.scheduled_check_mode = value @@ -13562,7 +16051,7 @@ def set_scheduled_check_mode(self, value): def set_scheduled_check_wait_hours(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13495 set_scheduled_check_wait_hours') + utils.debug_time('app 16054 set_scheduled_check_wait_hours') self.scheduled_check_wait_hours = value @@ -13570,7 +16059,7 @@ def set_scheduled_check_wait_hours(self, value): def set_scheduled_dl_mode(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13503 set_scheduled_dl_mode') + utils.debug_time('app 16062 set_scheduled_dl_mode') self.scheduled_dl_mode = value @@ -13578,15 +16067,34 @@ def set_scheduled_dl_mode(self, value): def set_scheduled_dl_wait_hours(self, value): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13511 set_scheduled_dl_wait_hours') + utils.debug_time('app 16070 set_scheduled_dl_wait_hours') self.scheduled_dl_wait_hours = value + def set_scheduled_livestream_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 16078 set_scheduled_livestream_flag') + + if not flag: + self.scheduled_livestream_flag = False + else: + self.scheduled_livestream_flag = True + + + def set_scheduled_livestream_wait_mins(self, value): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 16089 set_scheduled_livestream_wait_mins') + + self.scheduled_livestream_wait_mins = value + + def set_scheduled_shutdown_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13519 set_scheduled_shutdown_flag') + utils.debug_time('app 16097 set_scheduled_shutdown_flag') if not flag: self.scheduled_shutdown_flag = False @@ -13597,7 +16105,7 @@ def set_scheduled_shutdown_flag(self, flag): def set_simple_options_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13530 set_simple_options_flag') + utils.debug_time('app 16108 set_simple_options_flag') if not flag: self.simple_options_flag = False @@ -13605,6 +16113,17 @@ def set_simple_options_flag(self, flag): self.simple_options_flag = True + def set_show_classic_tab_on_startup_flag(self, flag): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 16119 set_show_classic_tab_on_startup_flag') + + if not flag: + self.show_classic_tab_on_startup_flag = False + else: + self.show_classic_tab_on_startup_flag = True + + def set_show_pretty_dates_flag(self, flag): """Called by config.SystemPrefWin.on_pretty_date_button_toggled(). @@ -13613,7 +16132,7 @@ def set_show_pretty_dates_flag(self, flag): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 13546 set_show_pretty_dates_flag') + utils.debug_time('app 16135 set_show_pretty_dates_flag') if not flag: self.show_pretty_dates_flag = False @@ -13632,7 +16151,7 @@ def set_show_pretty_dates_flag(self, flag): def set_show_small_icons_in_index(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13565 set_show_small_icons_in_index') + utils.debug_time('app 16154 set_show_small_icons_in_index') if not flag: self.show_small_icons_in_index = False @@ -13651,7 +16170,7 @@ def set_show_status_icon_flag(self, flag): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 13584 set_show_status_icon_flag') + utils.debug_time('app 16173 set_show_status_icon_flag') if not flag: self.show_status_icon_flag = False @@ -13667,7 +16186,7 @@ def set_show_status_icon_flag(self, flag): def set_show_tooltips_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13600 set_show_tooltips_flag') + utils.debug_time('app 16189 set_show_tooltips_flag') if not flag: self.show_tooltips_flag = False @@ -13679,10 +16198,18 @@ def set_show_tooltips_flag(self, flag): self.main_win_obj.enable_tooltips(True) + def set_sound_custom(self, value): + + if DEBUG_FUNC_FLAG: + utils.debug_time('app 16204 set_sound_custom') + + self.sound_custom = value + + def set_system_error_show_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13615 set_system_error_show_flag') + utils.debug_time('app 16212 set_system_error_show_flag') if not flag: self.system_error_show_flag = False @@ -13693,7 +16220,7 @@ def set_system_error_show_flag(self, flag): def set_system_msg_keep_totals_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13626 set_system_msg_keep_totals_flag') + utils.debug_time('app 16223 set_system_msg_keep_totals_flag') if not flag: self.system_msg_keep_totals_flag = False @@ -13704,7 +16231,7 @@ def set_system_msg_keep_totals_flag(self, flag): def set_system_warning_show_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13637 set_system_warning_show_flag') + utils.debug_time('app 16234 set_system_warning_show_flag') if not flag: self.system_warning_show_flag = False @@ -13715,7 +16242,7 @@ def set_system_warning_show_flag(self, flag): def set_toolbar_squeeze_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13648 set_toolbar_squeeze_flag') + utils.debug_time('app 16245 set_toolbar_squeeze_flag') if not flag: self.toolbar_squeeze_flag = False @@ -13729,7 +16256,7 @@ def set_toolbar_squeeze_flag(self, flag): def set_use_module_moviepy_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13662 set_use_module_moviepy_flag') + utils.debug_time('app 16259 set_use_module_moviepy_flag') if not flag: self.use_module_moviepy_flag = False @@ -13746,7 +16273,7 @@ def set_video_res_apply_flag(self, flag): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 13679 set_video_res_apply_flag') + utils.debug_time('app 16276 set_video_res_apply_flag') if not flag: self.video_res_apply_flag = False @@ -13770,11 +16297,11 @@ def set_video_res_default(self, value): """ if DEBUG_FUNC_FLAG: - utils.debug_time('app 13703 set_video_res_default') + utils.debug_time('app 16300 set_video_res_default') if not value in formats.VIDEO_RESOLUTION_DICT: return self.system_error( - 153, + 158, 'Set video resolution request failed sanity check', ) @@ -13784,7 +16311,7 @@ def set_video_res_default(self, value): def set_ytdl_output_ignore_json_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13717 set_ytdl_output_ignore_json_flag') + utils.debug_time('app 16314 set_ytdl_output_ignore_json_flag') if not flag: self.ytdl_output_ignore_json_flag = False @@ -13795,7 +16322,7 @@ def set_ytdl_output_ignore_json_flag(self, flag): def set_ytdl_output_ignore_progress_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13728 set_ytdl_output_ignore_progress_flag') + utils.debug_time('app 16325 set_ytdl_output_ignore_progress_flag') if not flag: self.ytdl_output_ignore_progress_flag = False @@ -13806,7 +16333,7 @@ def set_ytdl_output_ignore_progress_flag(self, flag): def set_ytdl_output_show_summary_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13739 set_ytdl_output_show_summary_flag') + utils.debug_time('app 16336 set_ytdl_output_show_summary_flag') if not flag: self.ytdl_output_show_summary_flag = False @@ -13817,7 +16344,7 @@ def set_ytdl_output_show_summary_flag(self, flag): def set_ytdl_output_start_empty_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13750 set_ytdl_output_start_empty_flag') + utils.debug_time('app 16347 set_ytdl_output_start_empty_flag') if not flag: self.ytdl_output_start_empty_flag = False @@ -13828,7 +16355,7 @@ def set_ytdl_output_start_empty_flag(self, flag): def set_ytdl_output_stderr_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13761 set_ytdl_output_stderr_flag') + utils.debug_time('app 16358 set_ytdl_output_stderr_flag') if not flag: self.ytdl_output_stderr_flag = False @@ -13839,7 +16366,7 @@ def set_ytdl_output_stderr_flag(self, flag): def set_ytdl_output_stdout_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13772 set_ytdl_output_stdout_flag') + utils.debug_time('app 16369 set_ytdl_output_stdout_flag') if not flag: self.ytdl_output_stdout_flag = False @@ -13850,7 +16377,7 @@ def set_ytdl_output_stdout_flag(self, flag): def set_ytdl_output_system_cmd_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13783 set_ytdl_output_system_cmd_flag') + utils.debug_time('app 16380 set_ytdl_output_system_cmd_flag') if not flag: self.ytdl_output_system_cmd_flag = False @@ -13861,7 +16388,7 @@ def set_ytdl_output_system_cmd_flag(self, flag): def set_ytdl_path(self, path): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13794 set_ytdl_path') + utils.debug_time('app 16391 set_ytdl_path') self.ytdl_path = path @@ -13869,7 +16396,7 @@ def set_ytdl_path(self, path): def set_ytdl_update_current(self, string): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13802 set_ytdl_update_current') + utils.debug_time('app 16399 set_ytdl_update_current') self.ytdl_update_current = string @@ -13877,7 +16404,7 @@ def set_ytdl_update_current(self, string): def set_ytdl_write_ignore_json_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13810 set_ytdl_write_ignore_json_flag') + utils.debug_time('app 16407 set_ytdl_write_ignore_json_flag') if not flag: self.ytdl_write_ignore_json_flag = False @@ -13888,7 +16415,7 @@ def set_ytdl_write_ignore_json_flag(self, flag): def set_ytdl_write_ignore_progress_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13821 set_ytdl_write_ignore_progress_flag') + utils.debug_time('app 16418 set_ytdl_write_ignore_progress_flag') if not flag: self.ytdl_write_ignore_progress_flag = False @@ -13899,7 +16426,7 @@ def set_ytdl_write_ignore_progress_flag(self, flag): def set_ytdl_write_stderr_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13832 set_ytdl_write_stderr_flag') + utils.debug_time('app 16429 set_ytdl_write_stderr_flag') if not flag: self.ytdl_write_stderr_flag = False @@ -13910,7 +16437,7 @@ def set_ytdl_write_stderr_flag(self, flag): def set_ytdl_write_stdout_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13843 set_ytdl_write_stdout_flag') + utils.debug_time('app 16440 set_ytdl_write_stdout_flag') if not flag: self.ytdl_write_stdout_flag = False @@ -13921,7 +16448,7 @@ def set_ytdl_write_stdout_flag(self, flag): def set_ytdl_write_system_cmd_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13854 set_ytdl_write_system_cmd_flag') + utils.debug_time('app 16451 set_ytdl_write_system_cmd_flag') if not flag: self.ytdl_write_system_cmd_flag = False @@ -13932,7 +16459,7 @@ def set_ytdl_write_system_cmd_flag(self, flag): def set_ytdl_write_verbose_flag(self, flag): if DEBUG_FUNC_FLAG: - utils.debug_time('app 13865 set_ytdl_write_verbose_flag') + utils.debug_time('app 16462 set_ytdl_write_verbose_flag') if not flag: self.ytdl_write_verbose_flag = False diff --git a/tartube/mainwin.py b/tartube/mainwin.py index e0985453..4fe4f75b 100755 --- a/tartube/mainwin.py +++ b/tartube/mainwin.py @@ -50,6 +50,8 @@ import media import options import utils +# Use same gettext translations +from mainapp import _ # Debugging flag (calls utils.debug_time at the start of every function) @@ -92,7 +94,7 @@ class MainWin(Gtk.ApplicationWindow): def __init__(self, app_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 95 __init__') + utils.debug_time('mwn 97 __init__') super(MainWin, self).__init__( title=__main__.__packagename__.title() + ' v' \ @@ -134,6 +136,8 @@ def __init__(self, app_obj): self.install_ffmpeg_menu_item = None # Gtk.MenuItem self.tidy_up_menu_item = None # Gtk.MenuItem self.stop_operation_menu_item = None # Gtk.MenuItem + self.cancel_live_menu_item = None # Gtk.MenuItem + self.update_live_menu_item = None # Gtk.MenuItem # (from self.setup_main_toolbar) self.main_toolbar = None # Gtk.Toolbar self.add_video_toolbutton = None # Gtk.ToolButton @@ -230,7 +234,31 @@ def __init__(self, app_obj): self.show_operation_warning_checkbutton = None # Gtk.CheckButton self.error_list_button = None # Gtk.Button - + # (from self.setup_classic_mode_tab) + self.classic_grid = None # Gtk.Grid + self.classic_options_button = None # Gtk.Button + self.classic_update_ytdl_button = None # Gtk.Button + self.classic_auto_copy_button = None # Gtk.Button + self.classic_textview = None # Gtk.TextView + self.classic_textbuffer = None # Gtk.TextBuffer + self.classic_mark_start = None # Gtk.TextMark + self.classic_mark_end = None # Gtk.TextMark + self.classic_dest_dir_liststore = None # Gtk.ListStore + self.classic_dest_dir_combo = None # Gtk.ComboBox + self.classic_dest_dir_button = None # Gtk.Button + self.classic_format_liststore = None # Gtk.ListStore + self.classic_format_combo = None # Gtk.ComboBox + self.classic_add_urls_button = None # Gtk.Button + self.classic_progress_treeview = None # Gtk.TreeView + self.classic_progress_liststore = None # Gtk.ListStore + self.classic_progress_tooltip_column = 1 + self.classic_remove_button = None # Gtk.Button + self.classic_play_button = None # Gtk.Button + self.classic_move_up_button = None # Gtk.Button + self.classic_move_down_button = None # Gtk.Button + self.classic_redownload_button = None # Gtk.Button + self.classic_stop_button = None # Gtk.Button + self.classic_download_button = None # Gtk.Button # IV list - other # --------------- @@ -262,6 +290,7 @@ def __init__(self, app_obj): # Standard limits for the length of strings displayed in various # widgets + self.exceedingly_long_string_max_len = 80 self.very_long_string_max_len = 64 self.long_string_max_len = 48 self.quite_long_string_max_len = 40 @@ -342,6 +371,11 @@ def __init__(self, app_obj): # an empty list) self.video_catalogue_filtered_list = [] + # Background colours used in the Video Catalogue to highlight + # livestreams + self.waiting_colour = Gdk.RGBA(1, 0, 0, 0.1) + self.live_colour = Gdk.RGBA(0, 1, 0, 0.1) + # The video catalogue splits its video list into pages (as Gtk # struggles with a list of hundreds, or thousands, of videos) # The number of videos per page is specified by @@ -373,7 +407,7 @@ def __init__(self, app_obj): # temporarily stores the download statistics it has received in this # IV. The statistics are received in a dictionary in the standard # format described in the comments to - # media.VideoDownloader.extract_stdout_data() + # downloads.VideoDownloader.extract_stdout_data() # Then, during calls at fixed intervals to # self.progress_list_display_dl_stats(), those download statistics # are displayed @@ -427,6 +461,41 @@ def __init__(self, app_obj): # dictionary at all for simulated downloads) self.results_list_temp_list = [] + # Classic Mode Tab IVs + # During a normal download operation, stats are displayed in the + # Progress Tab + # During a download operation launched from the Classic Mode Tab, stats + # are displayed in the Classic Progress List instead. In addition, we + # create a set of dummy media.Video objects, one for each URL to + # download. Each dummy media.Video object has a negative .dbid, and + # none of them are added to the media data registry + # The dummy media.Video object's URL may be a single video, or even a + # channel or playlist (Tartube doesn't really care which) + # Dictionary in the form + # key = The unique ID (dbid) for the dummy media.Video object + # handling the URL + # value = The dummy media.Video object itself + self.classic_media_dict = {} + # The total number of dummy media.Video objects created since Tartube + # started (used to give each one a unique ID) + self.classic_media_total = 0 + # During a download operation launched from the Classic Mode Tab, + # incoming stats are stored in this dictionary, just as they are + # stored in self.progress_list_temp_dict during a normal download + # operation + # Dictionary in the form + # key = The downloads.DownloadItem.item_id for the download item + # handling the media data object + # value = A dictionary of download statistics dictionary in the + # standard format + self.classic_temp_dict = {} + # Flag set to True when automatic copy/paste has been enabled (always + # disabled on startup) + self.classic_auto_copy_flag = False + # IVs for clipboard monitoring, when required + self.classic_clipboard_timer_id = None + self.classic_clipboard_timer_time = 250 + # Output Tab IVs # Flag set to True when the summary tab is added, during the first call # to self.output_tab_setup_pages() (might not be added at all, if @@ -498,6 +567,22 @@ def __init__(self, app_obj): # The value is set/reset by a call to self.set_previous_alt_dest_dbid() self.previous_alt_dest_dbid = None + # Desktop notification IVs + # The desktop notification has an optional button to click. When the + # button is used, we need to retain a reference to the + # Notify.Notification, or the callback won't work + # The number of desktop notifications (with buttons) created during + # this session (used to give each one a unique ID) + self.notify_desktop_count = 0 + # Dictionary of Notify.Notification objects. Each entry is removed when + # the notification is closed + # Dictionary in the form + # key: unique ID for the notification (based on + # self.notify_desktop_count) + # value: the corresponding Notify.Notification object + self.notify_desktop_dict = {} + + # Code # ---- @@ -519,7 +604,7 @@ def setup_pixbufs(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 522 setup_pixbufs') + utils.debug_time('mwn 607 setup_pixbufs') # The default location for icons is ../icons # When installed via PyPI, the icons are moved to ../tartube/icons @@ -578,12 +663,22 @@ def setup_pixbufs(self): ) self.icon_dict[key] = full_path - # (At the moment, the system preference window only uses one - # flag, but more may be added later) - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'locale', 'flag_uk.png'), - ) - self.icon_dict['flag_uk'] = full_path + for key in formats.EXTERNAL_ICON_DICT: + rel_path = formats.EXTERNAL_ICON_DICT[key] + full_path = os.path.abspath( + os.path.join(icon_dir_path, 'external', rel_path), + ) + self.icon_dict[key] = full_path + + for locale in formats.LOCALE_LIST: + full_path = os.path.abspath( + os.path.join( + icon_dir_path, + 'locale', + 'flag_' + locale + '.png', + ), + ) + self.icon_dict['flag_' + locale] = full_path # Now create the pixbufs themselves for key in self.icon_dict: @@ -611,8 +706,7 @@ def setup_pixbufs(self): # No icons directory found; this is a fatal error print( - __main__.__prettyname__ + ' cannot start because it cannot find' \ - + ' its icons directory (folder)', + _('Tartube cannot start because it cannot find its icons folder'), file=sys.stderr, ) @@ -628,7 +722,7 @@ def setup_win(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 631 setup_win') + utils.debug_time('mwn 725 setup_win') # Set the default window size self.set_default_size( @@ -645,7 +739,8 @@ def setup_win(self): # Allow the user to drag-and-drop videos (for example, from the web # browser) into the main window, adding it the currently selected - # folder (or to 'Unsorted Videos' if something else is selected) + # folder (or to 'Unsorted Videos' if something else is selected, or + # into the Classic Mode Tab if it is visible) self.connect('drag_data_received', self.on_window_drag_data_received) # (Without this line, we get Gtk warnings on some systems) self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) @@ -656,7 +751,7 @@ def setup_win(self): # Set up desktop notifications. Notifications can be sent by calling # self.notify_desktop() if os.name != 'nt': - Notify.init(__main__.__prettyname__) + Notify.init('Tartube') # Create main window widgets self.setup_grid() @@ -665,6 +760,7 @@ def setup_win(self): self.setup_notebook() self.setup_videos_tab() self.setup_progress_tab() + self.setup_classic_mode_tab() self.setup_output_tab() self.setup_errors_tab() @@ -680,7 +776,7 @@ def setup_grid(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 683 setup_grid') + utils.debug_time('mwn 779 setup_grid') self.grid = Gtk.Grid() self.add(self.grid) @@ -694,20 +790,20 @@ def setup_menubar(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 697 setup_menubar') + utils.debug_time('mwn 793 setup_menubar') self.menubar = Gtk.MenuBar() self.grid.attach(self.menubar, 0, 0, 1, 1) # File column - file_menu_column = Gtk.MenuItem.new_with_mnemonic('_File') + file_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_File')) self.menubar.add(file_menu_column) file_sub_menu = Gtk.Menu() file_menu_column.set_submenu(file_sub_menu) self.change_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Database preferences...', + _('_Database preferences...'), ) file_sub_menu.append(self.change_db_menu_item) self.change_db_menu_item.set_action_name('app.change_db_menu') @@ -716,13 +812,13 @@ def setup_menubar(self): file_sub_menu.append(Gtk.SeparatorMenuItem()) self.save_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Save database', + _('_Save database'), ) file_sub_menu.append(self.save_db_menu_item) self.save_db_menu_item.set_action_name('app.save_db_menu') self.save_all_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Save _all', + _('Save _all'), ) file_sub_menu.append(self.save_all_menu_item) self.save_all_menu_item.set_action_name('app.save_all_menu') @@ -730,60 +826,62 @@ def setup_menubar(self): # Separator file_sub_menu.append(Gtk.SeparatorMenuItem()) - close_tray_menu_item = Gtk.MenuItem.new_with_mnemonic('_Close to tray') + close_tray_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('_Close to tray'), + ) file_sub_menu.append(close_tray_menu_item) close_tray_menu_item.set_action_name('app.close_tray_menu') - quit_menu_item = Gtk.MenuItem.new_with_mnemonic('_Quit') + quit_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Quit')) file_sub_menu.append(quit_menu_item) quit_menu_item.set_action_name('app.quit_menu') # Edit column - edit_menu_column = Gtk.MenuItem.new_with_mnemonic('_Edit') + edit_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Edit')) self.menubar.add(edit_menu_column) edit_sub_menu = Gtk.Menu() edit_menu_column.set_submenu(edit_sub_menu) self.system_prefs_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_System preferences...', + _('_System preferences...'), ) edit_sub_menu.append(self.system_prefs_menu_item) self.system_prefs_menu_item.set_action_name('app.system_prefs_menu') self.gen_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_General download options...', + _('_General download options...'), ) edit_sub_menu.append(self.gen_options_menu_item) self.gen_options_menu_item.set_action_name('app.gen_options_menu') # Media column - media_menu_column = Gtk.MenuItem.new_with_mnemonic('_Media') + media_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Media')) self.menubar.add(media_menu_column) media_sub_menu = Gtk.Menu() media_menu_column.set_submenu(media_sub_menu) self.add_video_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Add _videos...', + _('Add _videos...'), ) media_sub_menu.append(self.add_video_menu_item) self.add_video_menu_item.set_action_name('app.add_video_menu') self.add_channel_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Add _channel...', + _('Add _channel...'), ) media_sub_menu.append(self.add_channel_menu_item) self.add_channel_menu_item.set_action_name('app.add_channel_menu') self.add_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Add _playlist...', + _('Add _playlist...'), ) media_sub_menu.append(self.add_playlist_menu_item) self.add_playlist_menu_item.set_action_name('app.add_playlist_menu') self.add_folder_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Add _folder...', + _('Add _folder...'), ) media_sub_menu.append(self.add_folder_menu_item) self.add_folder_menu_item.set_action_name('app.add_folder_menu') @@ -792,7 +890,7 @@ def setup_menubar(self): media_sub_menu.append(Gtk.SeparatorMenuItem()) self.export_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Export from database', + _('_Export from database'), ) media_sub_menu.append(self.export_db_menu_item) self.export_db_menu_item.set_action_name('app.export_db_menu') @@ -800,19 +898,19 @@ def setup_menubar(self): import_sub_menu = Gtk.Menu() import_json_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_JSON export file', + _('_JSON export file'), ) import_sub_menu.append(import_json_menu_item) import_json_menu_item.set_action_name('app.import_json_menu') import_text_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Plain _text export file', + _('Plain _text export file'), ) import_sub_menu.append(import_text_menu_item) import_text_menu_item.set_action_name('app.import_text_menu') self.import_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Import into database' + _('_Import into database') ) self.import_db_menu_item.set_submenu(import_sub_menu) media_sub_menu.append(self.import_db_menu_item) @@ -821,12 +919,12 @@ def setup_menubar(self): media_sub_menu.append(Gtk.SeparatorMenuItem()) self.switch_view_menu_item = \ - Gtk.MenuItem.new_with_mnemonic('_Switch between views') + Gtk.MenuItem.new_with_mnemonic(_('_Switch between views')) media_sub_menu.append(self.switch_view_menu_item) self.switch_view_menu_item.set_action_name('app.switch_view_menu') self.show_hidden_menu_item = \ - Gtk.MenuItem.new_with_mnemonic('Show _hidden folders') + Gtk.MenuItem.new_with_mnemonic(_('Show _hidden folders')) media_sub_menu.append(self.show_hidden_menu_item) self.show_hidden_menu_item.set_action_name('app.show_hidden_menu') @@ -836,29 +934,31 @@ def setup_menubar(self): media_sub_menu.append(Gtk.SeparatorMenuItem()) self.test_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Add test media', + _('_Add test media'), ) media_sub_menu.append(self.test_menu_item) self.test_menu_item.set_action_name('app.test_menu') # Operations column - ops_menu_column = Gtk.MenuItem.new_with_mnemonic('_Operations') + ops_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Operations')) self.menubar.add(ops_menu_column) ops_sub_menu = Gtk.Menu() ops_menu_column.set_submenu(ops_sub_menu) - self.check_all_menu_item = Gtk.MenuItem.new_with_mnemonic('_Check all') + self.check_all_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('_Check all'), + ) ops_sub_menu.append(self.check_all_menu_item) self.check_all_menu_item.set_action_name('app.check_all_menu') self.download_all_menu_item = \ - Gtk.MenuItem.new_with_mnemonic('_Download all') + Gtk.MenuItem.new_with_mnemonic(_('_Download all')) ops_sub_menu.append(self.download_all_menu_item) self.download_all_menu_item.set_action_name('app.download_all_menu') self.custom_dl_all_menu_item = \ - Gtk.MenuItem.new_with_mnemonic('C_ustom download all') + Gtk.MenuItem.new_with_mnemonic(_('C_ustom download all')) ops_sub_menu.append(self.custom_dl_all_menu_item) self.custom_dl_all_menu_item.set_action_name('app.custom_dl_all_menu') @@ -866,7 +966,7 @@ def setup_menubar(self): ops_sub_menu.append(Gtk.SeparatorMenuItem()) self.refresh_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Refresh database...', + _('_Refresh database...'), ) ops_sub_menu.append(self.refresh_db_menu_item) self.refresh_db_menu_item.set_action_name('app.refresh_db_menu') @@ -875,13 +975,13 @@ def setup_menubar(self): ops_sub_menu.append(Gtk.SeparatorMenuItem()) self.update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Update _youtube-dl', + _('Update _youtube-dl'), ) 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 youtube-dl...'), ) ops_sub_menu.append(self.test_ytdl_menu_item) self.test_ytdl_menu_item.set_action_name('app.test_ytdl_menu') @@ -890,7 +990,7 @@ def setup_menubar(self): ops_sub_menu.append(Gtk.SeparatorMenuItem()) self.install_ffmpeg_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Install FFmpeg', + _('_Install FFmpeg'), ) ops_sub_menu.append(self.install_ffmpeg_menu_item) self.install_ffmpeg_menu_item.set_action_name( @@ -901,7 +1001,7 @@ def setup_menubar(self): ops_sub_menu.append(Gtk.SeparatorMenuItem()) self.tidy_up_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Tidy up _files...', + _('Tidy up _files...'), ) ops_sub_menu.append(self.tidy_up_menu_item) self.tidy_up_menu_item.set_action_name( @@ -912,27 +1012,61 @@ def setup_menubar(self): ops_sub_menu.append(Gtk.SeparatorMenuItem()) self.stop_operation_menu_item = \ - Gtk.MenuItem.new_with_mnemonic('_Stop current operation') + Gtk.MenuItem.new_with_mnemonic(_('_Stop current operation')) ops_sub_menu.append(self.stop_operation_menu_item) self.stop_operation_menu_item.set_action_name( 'app.stop_operation_menu', ) + # Livestreams column + live_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Livestreams')) + self.menubar.add(live_menu_column) + + live_sub_menu = Gtk.Menu() + live_menu_column.set_submenu(live_sub_menu) + + self.live_prefs_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('_Livestream preferences...'), + ) + live_sub_menu.append(self.live_prefs_menu_item) + self.live_prefs_menu_item.set_action_name('app.live_prefs_menu') + + # Separator + live_sub_menu.append(Gtk.SeparatorMenuItem()) + + self.update_live_menu_item = \ + Gtk.MenuItem.new_with_mnemonic(_('_Update existing livestreams')) + live_sub_menu.append(self.update_live_menu_item) + self.update_live_menu_item.set_action_name('app.update_live_menu') + + self.cancel_live_menu_item = \ + Gtk.MenuItem.new_with_mnemonic(_('_Cancel all livestream alerts')) + live_sub_menu.append(self.cancel_live_menu_item) + self.cancel_live_menu_item.set_action_name('app.cancel_live_menu') + # Help column - help_menu_column = Gtk.MenuItem.new_with_mnemonic('_Help') + help_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Help')) self.menubar.add(help_menu_column) help_sub_menu = Gtk.Menu() help_menu_column.set_submenu(help_sub_menu) - about_menu_item = Gtk.MenuItem.new_with_mnemonic('_About...') + about_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_About...')) help_sub_menu.append(about_menu_item) about_menu_item.set_action_name('app.about_menu') - go_website_menu_item = Gtk.MenuItem.new_with_mnemonic('Go to _website') + go_website_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('Go to _website'), + ) help_sub_menu.append(go_website_menu_item) go_website_menu_item.set_action_name('app.go_website_menu') + send_feedback_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('Send _feedback'), + ) + help_sub_menu.append(send_feedback_menu_item) + send_feedback_menu_item.set_action_name('app.send_feedback_menu') + def setup_main_toolbar(self): @@ -944,7 +1078,7 @@ def setup_main_toolbar(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 947 setup_main_toolbar') + utils.debug_time('mwn 1081 setup_main_toolbar') # If a toolbar already exists, destroy it to make room for the new one if self.main_toolbar: @@ -964,7 +1098,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_video_small'], ), ) - self.add_video_toolbutton.set_label('Videos') + self.add_video_toolbutton.set_label(_('Videos')) self.add_video_toolbutton.set_is_important(True) else: self.add_video_toolbutton = Gtk.ToolButton.new( @@ -974,7 +1108,7 @@ def setup_main_toolbar(self): ) self.main_toolbar.insert(self.add_video_toolbutton, -1) - self.add_video_toolbutton.set_tooltip_text('Add new video(s)') + self.add_video_toolbutton.set_tooltip_text(_('Add new video(s)')) self.add_video_toolbutton.set_action_name('app.add_video_toolbutton') if not squeeze_flag: @@ -983,7 +1117,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_channel_small'], ), ) - self.add_channel_toolbutton.set_label('Channel') + self.add_channel_toolbutton.set_label(_('Channel')) self.add_channel_toolbutton.set_is_important(True) else: self.add_channel_toolbutton = Gtk.ToolButton.new( @@ -993,7 +1127,7 @@ def setup_main_toolbar(self): ) self.main_toolbar.insert(self.add_channel_toolbutton, -1) - self.add_channel_toolbutton.set_tooltip_text('Add a new channel') + self.add_channel_toolbutton.set_tooltip_text(_('Add a new channel')) self.add_channel_toolbutton.set_action_name( 'app.add_channel_toolbutton', ) @@ -1004,7 +1138,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_playlist_small'], ), ) - self.add_playlist_toolbutton.set_label('Playlist') + self.add_playlist_toolbutton.set_label(_('Playlist')) self.add_playlist_toolbutton.set_is_important(True) else: self.add_playlist_toolbutton = Gtk.ToolButton.new( @@ -1014,7 +1148,7 @@ def setup_main_toolbar(self): ) self.main_toolbar.insert(self.add_playlist_toolbutton, -1) - self.add_playlist_toolbutton.set_tooltip_text('Add a new playlist') + self.add_playlist_toolbutton.set_tooltip_text(_('Add a new playlist')) self.add_playlist_toolbutton.set_action_name( 'app.add_playlist_toolbutton', ) @@ -1025,7 +1159,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_folder_small'], ), ) - self.add_folder_toolbutton.set_label('Folder') + self.add_folder_toolbutton.set_label(_('Folder')) self.add_folder_toolbutton.set_is_important(True) else: self.add_folder_toolbutton = Gtk.ToolButton.new( @@ -1035,7 +1169,7 @@ def setup_main_toolbar(self): ) self.main_toolbar.insert(self.add_folder_toolbutton, -1) - self.add_folder_toolbutton.set_tooltip_text('Add a new folder') + self.add_folder_toolbutton.set_tooltip_text(_('Add a new folder')) self.add_folder_toolbutton.set_action_name('app.add_folder_toolbutton') # (Conversely, if there are no labels, then we have enough room for a @@ -1049,7 +1183,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_check_small'], ), ) - self.check_all_toolbutton.set_label('Check') + self.check_all_toolbutton.set_label(_('Check')) self.check_all_toolbutton.set_is_important(True) else: self.check_all_toolbutton = Gtk.ToolButton.new( @@ -1060,7 +1194,7 @@ def setup_main_toolbar(self): self.main_toolbar.insert(self.check_all_toolbutton, -1) self.check_all_toolbutton.set_tooltip_text( - 'Check all videos, channels, playlists and folders', + _('Check all videos, channels, playlists and folders'), ) self.check_all_toolbutton.set_action_name('app.check_all_toolbutton') @@ -1070,7 +1204,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_download_small'], ), ) - self.download_all_toolbutton.set_label('Download') + self.download_all_toolbutton.set_label(_('Download')) self.download_all_toolbutton.set_is_important(True) else: self.download_all_toolbutton = Gtk.ToolButton.new( @@ -1081,7 +1215,7 @@ def setup_main_toolbar(self): self.main_toolbar.insert(self.download_all_toolbutton, -1) self.download_all_toolbutton.set_tooltip_text( - 'Download all videos, channels, playlists and folders', + _('Download all videos, channels, playlists and folders'), ) self.download_all_toolbutton.set_action_name( 'app.download_all_toolbutton', @@ -1096,7 +1230,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_stop_small'], ), ) - self.stop_operation_toolbutton.set_label('Stop') + self.stop_operation_toolbutton.set_label(_('Stop')) self.stop_operation_toolbutton.set_is_important(True) else: self.stop_operation_toolbutton = Gtk.ToolButton.new( @@ -1108,7 +1242,7 @@ def setup_main_toolbar(self): self.main_toolbar.insert(self.stop_operation_toolbutton, -1) self.stop_operation_toolbutton.set_sensitive(False) self.stop_operation_toolbutton.set_tooltip_text( - 'Stop the current operation', + _('Stop the current operation'), ) self.stop_operation_toolbutton.set_action_name( 'app.stop_operation_toolbutton', @@ -1120,7 +1254,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_switch_small'], ), ) - self.switch_view_toolbutton.set_label('Switch') + self.switch_view_toolbutton.set_label(_('Switch')) self.switch_view_toolbutton.set_is_important(True) else: self.switch_view_toolbutton = Gtk.ToolButton.new( @@ -1131,7 +1265,7 @@ def setup_main_toolbar(self): self.main_toolbar.insert(self.switch_view_toolbutton, -1) self.switch_view_toolbutton.set_tooltip_text( - 'Switch between simple and complex views', + _('Switch between simple and complex views'), ) self.switch_view_toolbutton.set_action_name( 'app.switch_view_toolbutton', @@ -1145,7 +1279,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_test_small'], ), ) - self.test_toolbutton.set_label('Test') + self.test_toolbutton.set_label(_('Test')) self.test_toolbutton.set_is_important(True) else: self.test_toolbutton = Gtk.ToolButton.new( @@ -1156,7 +1290,7 @@ def setup_main_toolbar(self): self.main_toolbar.insert(self.test_toolbutton, -1) self.test_toolbutton.set_tooltip_text( - 'Add test media data objects', + _('Add test media data objects'), ) self.test_toolbutton.set_action_name('app.test_toolbutton') @@ -1169,7 +1303,7 @@ def setup_main_toolbar(self): self.pixbuf_dict['tool_quit_small'], ), ) - quit_button.set_label('Quit') + quit_button.set_label(_('Quit')) quit_button.set_is_important(True) else: quit_button = Gtk.ToolButton.new( @@ -1179,7 +1313,7 @@ def setup_main_toolbar(self): ) self.main_toolbar.insert(quit_button, -1) - quit_button.set_tooltip_text('Close ' + __main__.__prettyname__) + quit_button.set_tooltip_text(_('Close Tartube')) quit_button.set_action_name('app.quit_toolbutton') @@ -1192,7 +1326,7 @@ def setup_notebook(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1195 setup_notebook') + utils.debug_time('mwn 1329 setup_notebook') self.notebook = Gtk.Notebook() self.grid.attach(self.notebook, 0, 2, 1, 1) @@ -1201,7 +1335,7 @@ def setup_notebook(self): # Videos Tab self.videos_tab = Gtk.Box() - self.videos_label = Gtk.Label.new_with_mnemonic('_Videos') + self.videos_label = Gtk.Label.new_with_mnemonic(_('_Videos')) self.notebook.append_page(self.videos_tab, self.videos_label) self.videos_tab.set_hexpand(True) self.videos_tab.set_vexpand(True) @@ -1209,15 +1343,23 @@ def setup_notebook(self): # Progress Tab self.progress_tab = Gtk.Box() - self.progress_label = Gtk.Label.new_with_mnemonic('_Progress') + self.progress_label = Gtk.Label.new_with_mnemonic(_('_Progress')) self.notebook.append_page(self.progress_tab, self.progress_label) self.progress_tab.set_hexpand(True) self.progress_tab.set_vexpand(True) self.progress_tab.set_border_width(self.spacing_size) + # Classic Tab + self.classic_tab = Gtk.Box() + self.classic_label = Gtk.Label.new_with_mnemonic(_('_Classic Mode')) + self.notebook.append_page(self.classic_tab, self.classic_label) + self.classic_tab.set_hexpand(True) + self.classic_tab.set_vexpand(True) + self.classic_tab.set_border_width(self.spacing_size) + # Output Tab self.output_tab = Gtk.Box() - self.output_label = Gtk.Label.new_with_mnemonic('_Output') + self.output_label = Gtk.Label.new_with_mnemonic(_('_Output')) self.notebook.append_page(self.output_tab, self.output_label) self.output_tab.set_hexpand(True) self.output_tab.set_vexpand(True) @@ -1225,7 +1367,9 @@ def setup_notebook(self): # Errors Tab self.errors_tab = Gtk.Box() - self.errors_label = Gtk.Label.new_with_mnemonic('_Errors / Warnings') + self.errors_label = Gtk.Label.new_with_mnemonic( + _('_Errors / Warnings'), + ) self.notebook.append_page(self.errors_tab, self.errors_label) self.errors_tab.set_hexpand(True) self.errors_tab.set_vexpand(True) @@ -1240,7 +1384,7 @@ def setup_videos_tab(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1243 setup_videos_tab') + utils.debug_time('mwn 1387 setup_videos_tab') self.videos_paned = Gtk.HPaned() self.videos_tab.pack_start(self.videos_paned, True, True, 0) @@ -1280,17 +1424,17 @@ def setup_videos_tab(self): True, self.spacing_size, ) - self.check_media_button.set_label('Check all') + self.check_media_button.set_label(_('Check all')) self.check_media_button.set_tooltip_text( - 'Check all videos, channels, playlists and folders', + _('Check all videos, channels, playlists and folders'), ) self.check_media_button.set_action_name('app.check_all_button') self.download_media_button = Gtk.Button() self.button_box.pack_start(self.download_media_button, True, True, 0) - self.download_media_button.set_label('Download all') + self.download_media_button.set_label(_('Download all')) self.download_media_button.set_tooltip_text( - 'Download all videos, channels, playlists and folders', + _('Download all videos, channels, playlists and folders'), ) self.download_media_button.set_action_name('app.download_all_button') @@ -1345,7 +1489,7 @@ def setup_videos_tab(self): toolitem = Gtk.ToolItem.new() self.catalogue_toolbar.insert(toolitem, -1) - label = Gtk.Label('Page ') + label = Gtk.Label(_('Page') + ' ') toolitem.add(label) toolitem2 = Gtk.ToolItem.new() @@ -1357,7 +1501,7 @@ def setup_videos_tab(self): ) self.catalogue_page_entry.set_width_chars(4) self.catalogue_page_entry.set_sensitive(False) - self.catalogue_page_entry.set_tooltip_text('Set visible page') + self.catalogue_page_entry.set_tooltip_text(_('Set visible page')) self.catalogue_page_entry.connect( 'activate', self.on_video_catalogue_page_entry_activated, @@ -1365,7 +1509,7 @@ def setup_videos_tab(self): toolitem3 = Gtk.ToolItem.new() self.catalogue_toolbar.insert(toolitem3, -1) - label2 = Gtk.Label(' of ') + label2 = Gtk.Label(' / ') toolitem3.add(label2) toolitem4 = Gtk.ToolItem.new() @@ -1381,7 +1525,7 @@ def setup_videos_tab(self): toolitem5 = Gtk.ToolItem.new() self.catalogue_toolbar.insert(toolitem5, -1) - label3 = Gtk.Label(' Size ') + label3 = Gtk.Label(' ' + _('Size') + ' ') toolitem5.add(label3) toolitem6 = Gtk.ToolItem.new() @@ -1392,7 +1536,7 @@ def setup_videos_tab(self): str(self.app_obj.catalogue_page_size), ) self.catalogue_size_entry.set_width_chars(4) - self.catalogue_size_entry.set_tooltip_text('Set page size') + self.catalogue_size_entry.set_tooltip_text(_('Set page size')) self.catalogue_size_entry.connect( 'activate', self.on_video_catalogue_size_entry_activated, @@ -1405,7 +1549,7 @@ def setup_videos_tab(self): = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GOTO_FIRST) self.catalogue_toolbar.insert(self.catalogue_first_button, -1) self.catalogue_first_button.set_sensitive(False) - self.catalogue_first_button.set_tooltip_text('Go to first page') + self.catalogue_first_button.set_tooltip_text(_('Go to first page')) self.catalogue_first_button.set_action_name( 'app.first_page_toolbutton', ) @@ -1414,7 +1558,7 @@ def setup_videos_tab(self): = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_BACK) self.catalogue_toolbar.insert(self.catalogue_back_button, -1) self.catalogue_back_button.set_sensitive(False) - self.catalogue_back_button.set_tooltip_text('Go to previous page') + self.catalogue_back_button.set_tooltip_text(_('Go to previous page')) self.catalogue_back_button.set_action_name( 'app.previous_page_toolbutton', ) @@ -1423,7 +1567,7 @@ def setup_videos_tab(self): = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_FORWARD) self.catalogue_toolbar.insert(self.catalogue_forwards_button, -1) self.catalogue_forwards_button.set_sensitive(False) - self.catalogue_forwards_button.set_tooltip_text('Go to next page') + self.catalogue_forwards_button.set_tooltip_text(_('Go to next page')) self.catalogue_forwards_button.set_action_name( 'app.next_page_toolbutton', ) @@ -1432,7 +1576,7 @@ def setup_videos_tab(self): = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GOTO_LAST) self.catalogue_toolbar.insert(self.catalogue_last_button, -1) self.catalogue_last_button.set_sensitive(False) - self.catalogue_last_button.set_tooltip_text('Go to last page') + self.catalogue_last_button.set_tooltip_text(_('Go to last page')) self.catalogue_last_button.set_action_name( 'app.last_page_toolbutton', ) @@ -1441,7 +1585,7 @@ def setup_videos_tab(self): = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_UP) self.catalogue_toolbar.insert(self.catalogue_scroll_up_button, -1) self.catalogue_scroll_up_button.set_sensitive(False) - self.catalogue_scroll_up_button.set_tooltip_text('Scroll up') + self.catalogue_scroll_up_button.set_tooltip_text(_('Scroll up')) self.catalogue_scroll_up_button.set_action_name( 'app.scroll_up_toolbutton', ) @@ -1450,7 +1594,7 @@ def setup_videos_tab(self): = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_DOWN) self.catalogue_toolbar.insert(self.catalogue_scroll_down_button, -1) self.catalogue_scroll_down_button.set_sensitive(False) - self.catalogue_scroll_down_button.set_tooltip_text('Scroll down') + self.catalogue_scroll_down_button.set_tooltip_text(_('Scroll down')) self.catalogue_scroll_down_button.set_action_name( 'app.scroll_down_toolbutton', ) @@ -1460,7 +1604,7 @@ def setup_videos_tab(self): self.catalogue_toolbar.insert(self.catalogue_show_filter_button, -1) self.catalogue_show_filter_button.set_sensitive(False) self.catalogue_show_filter_button.set_tooltip_text( - 'Show filter options', + _('Show filter options'), ) self.catalogue_show_filter_button.set_action_name( 'app.show_filter_toolbutton', @@ -1473,14 +1617,14 @@ def setup_videos_tab(self): toolitem7 = Gtk.ToolItem.new() self.catalogue_toolbar2.insert(toolitem7, -1) - label4 = Gtk.Label('Sort by') + label4 = Gtk.Label(_('Sort by')) toolitem7.add(label4) self.catalogue_sort_button \ = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SPELL_CHECK) self.catalogue_toolbar2.insert(self.catalogue_sort_button, -1) self.catalogue_sort_button.set_sensitive(False) - self.catalogue_sort_button.set_tooltip_text('Sort alphabetically') + self.catalogue_sort_button.set_tooltip_text(_('Sort alphabetically')) self.catalogue_sort_button.set_action_name( 'app.sort_type_toolbutton', ) @@ -1490,7 +1634,7 @@ def setup_videos_tab(self): toolitem8 = Gtk.ToolItem.new() self.catalogue_toolbar2.insert(toolitem8, -1) - label5 = Gtk.Label('Filter ') + label5 = Gtk.Label(_('Filter') + ' ') toolitem8.add(label5) toolitem9 = Gtk.ToolItem.new() @@ -1499,16 +1643,20 @@ def setup_videos_tab(self): toolitem9.add(self.catalogue_filter_entry) self.catalogue_filter_entry.set_width_chars(16) self.catalogue_filter_entry.set_sensitive(False) - self.catalogue_filter_entry.set_tooltip_text('Enter search text') + self.catalogue_filter_entry.set_tooltip_text(_('Enter search text')) toolitem10 = Gtk.ToolItem.new() self.catalogue_toolbar2.insert(toolitem10, -1) self.catalogue_regex_togglebutton \ - = Gtk.ToggleButton('Regex') + = Gtk.ToggleButton(_('Regex')) toolitem10.add(self.catalogue_regex_togglebutton) self.catalogue_regex_togglebutton.set_sensitive(False) + if not self.app_obj.catologue_use_regex_flag: + self.catalogue_regex_togglebutton.set_active(False) + else: + self.catalogue_regex_togglebutton.set_active(True) self.catalogue_regex_togglebutton.set_tooltip_text( - 'Select if search text is a regex', + _('Select if search text is a regex'), ) self.catalogue_regex_togglebutton.set_action_name( 'app.use_regex_togglebutton', @@ -1519,7 +1667,7 @@ def setup_videos_tab(self): self.catalogue_toolbar2.insert(self.catalogue_apply_filter_button, -1) self.catalogue_apply_filter_button.set_sensitive(False) self.catalogue_apply_filter_button.set_tooltip_text( - 'Filter videos', + _('Filter videos'), ) self.catalogue_apply_filter_button.set_action_name( 'app.apply_filter_toolbutton', @@ -1530,7 +1678,7 @@ def setup_videos_tab(self): self.catalogue_toolbar2.insert(self.catalogue_cancel_filter_button, -1) self.catalogue_cancel_filter_button.set_sensitive(False) self.catalogue_cancel_filter_button.set_tooltip_text( - 'Cancel filter', + _('Cancel filter'), ) self.catalogue_cancel_filter_button.set_action_name( 'app.cancel_filter_toolbutton', @@ -1541,7 +1689,7 @@ def setup_videos_tab(self): toolitem11 = Gtk.ToolItem.new() self.catalogue_toolbar2.insert(toolitem11, -1) - label6 = Gtk.Label('Find date') + label6 = Gtk.Label(_('Find date')) toolitem11.add(label6) self.catalogue_find_date_button \ @@ -1549,7 +1697,7 @@ def setup_videos_tab(self): self.catalogue_toolbar2.insert(self.catalogue_find_date_button, -1) self.catalogue_find_date_button.set_sensitive(False) self.catalogue_find_date_button.set_tooltip_text( - 'Find videos by date', + _('Find videos by date'), ) self.catalogue_find_date_button.set_action_name( 'app.find_date_toolbutton', @@ -1567,7 +1715,7 @@ def setup_progress_tab(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1570 setup_progress_tab') + utils.debug_time('mwn 1718 setup_progress_tab') vbox = Gtk.VBox() self.progress_tab.pack_start(vbox, True, True, 0) @@ -1592,9 +1740,8 @@ def setup_progress_tab(self): self.progress_list_treeview = Gtk.TreeView() self.progress_list_scrolled.add(self.progress_list_treeview) self.progress_list_treeview.set_can_focus(False) - # (Tooltips are initially enabled, and disabled by a call to - # self.disable_tooltips() after the config file is loaded, if - # necessary) + # (Tooltips are initially enabled, and if necessary are disabled by a + # call to self.disable_tooltips() shortly afterwards) self.progress_list_treeview.set_tooltip_column( self.progress_list_tooltip_column, ) @@ -1604,10 +1751,15 @@ def setup_progress_tab(self): self.on_progress_list_right_click, ) + translate_note = _( + 'TRANSLATOR\'S NOTE: Ext is short for a file extension, e.g. .EXE', + ) + for i, column_title in enumerate( [ - 'hide', 'hide', 'hide', '', 'Source', '#', 'Status', - 'Incoming file', 'Ext', '%', 'Speed', 'ETA', 'Size', + 'hide', 'hide', 'hide', '', _('Source'), '#', _('Status'), + _('Incoming file'), _('Ext'), '%', _('Speed'), _('ETA'), + _('Size'), ] ): if not column_title: @@ -1655,9 +1807,8 @@ def setup_progress_tab(self): self.results_list_treeview = Gtk.TreeView() self.results_list_scrolled.add(self.results_list_treeview) self.results_list_treeview.set_can_focus(False) - # (Tooltips are initially enabled, and disabled by a call to - # self.disable_tooltips() after the config file is loaded, if - # necessary) + # (Tooltips are initially enabled, and if necessary are disabled by a + # call to self.disable_tooltips() shortly afterwards) self.results_list_treeview.set_tooltip_column( self.results_list_tooltip_column, ) @@ -1669,8 +1820,8 @@ def setup_progress_tab(self): for i, column_title in enumerate( [ - 'hide', 'hide', '', 'New videos', 'Duration', 'Size', 'Date', - 'File', '', 'Downloaded to', + 'hide', 'hide', '', _('New videos'), _('Duration'), _('Size'), + _('Date'), _('File'), '', _('Downloaded to'), ] ): if not column_title: @@ -1683,7 +1834,7 @@ def setup_progress_tab(self): self.results_list_treeview.append_column(column_pixbuf) column_pixbuf.set_resizable(False) - elif column_title == 'File': + elif i == 7: renderer_toggle = Gtk.CellRendererToggle() column_toggle = Gtk.TreeViewColumn( column_title, @@ -1726,7 +1877,7 @@ def setup_progress_tab(self): self.num_worker_checkbutton = Gtk.CheckButton() grid.attach(self.num_worker_checkbutton, 0, 0, 1, 1) - self.num_worker_checkbutton.set_label('Max downloads') + self.num_worker_checkbutton.set_label(_('Max downloads')) self.num_worker_checkbutton.set_active( self.app_obj.num_worker_apply_flag, ) @@ -1749,7 +1900,7 @@ def setup_progress_tab(self): self.bandwidth_checkbutton = Gtk.CheckButton() grid.attach(self.bandwidth_checkbutton, 2, 0, 1, 1) - self.bandwidth_checkbutton.set_label('D/L speed (KiB/s)') + self.bandwidth_checkbutton.set_label(_('D/L speed (KiB/s)')) self.bandwidth_checkbutton.set_active( self.app_obj.bandwidth_apply_flag, ) @@ -1775,7 +1926,7 @@ def setup_progress_tab(self): self.video_res_checkbutton = Gtk.CheckButton() grid.attach(self.video_res_checkbutton, 4, 0, 1, 1) - self.video_res_checkbutton.set_label('Video resolution') + self.video_res_checkbutton.set_label(_('Video resolution')) self.video_res_checkbutton.set_active( self.app_obj.video_res_apply_flag, ) @@ -1794,7 +1945,14 @@ def setup_progress_tab(self): self.video_res_combobox.pack_start(renderer_text, True) self.video_res_combobox.add_attribute(renderer_text, 'text', 0) self.video_res_combobox.set_entry_text_column(0) - self.set_video_res_limit(None) # Uses default resolution, 720p + # (Check we're using a recognised value) + resolution = self.app_obj.video_res_default + if not resolution in formats.VIDEO_RESOLUTION_LIST: + resolution = formats.VIDEO_RESOLUTION_DEFAULT + # (Set the active item) + self.video_res_combobox.set_active( + formats.VIDEO_RESOLUTION_LIST.index(resolution), + ) self.video_res_combobox.connect( 'changed', self.on_video_res_combobox_changed, @@ -1803,7 +1961,7 @@ def setup_progress_tab(self): self.hide_finished_checkbutton = Gtk.CheckButton() grid.attach(self.hide_finished_checkbutton, 0, 1, 2, 1) self.hide_finished_checkbutton.set_label( - 'Hide active rows after they are finished', + _('Hide rows when they are finished'), ) self.hide_finished_checkbutton.set_active( self.app_obj.progress_list_hide_flag, @@ -1816,7 +1974,7 @@ def setup_progress_tab(self): self.reverse_results_checkbutton = Gtk.CheckButton() grid.attach(self.reverse_results_checkbutton, 2, 1, 4, 1) self.reverse_results_checkbutton.set_label( - 'Add newest videos to the top of the list') + _('Add newest videos to the top of the list')) self.reverse_results_checkbutton.set_active( self.app_obj.results_list_reverse_flag, ) @@ -1826,6 +1984,385 @@ def setup_progress_tab(self): ) + def setup_classic_mode_tab(self): + + """Called by self.setup_win(). + + Creates widgets for the Classic Mode Tab. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 1995 setup_classic_tab') + + grid_width = 7 + + self.classic_grid = Gtk.Grid() + self.classic_tab.pack_start(self.classic_grid, True, True, 0) + self.classic_grid.set_column_spacing(self.spacing_size) + self.classic_grid.set_row_spacing(self.spacing_size * 2) + + # First row - some decoration, and some buttons to edit download + # options, update youtube-dl, and enable automatic copy/paste + # -------------------------------------------------------------------- + + hbox = Gtk.HBox() + self.classic_grid.attach(hbox, 0, 0, grid_width, 1) + + # (The youtube-dl-gui icon looks neat, but also solves spacing issues + # on this grid row) + frame = Gtk.Frame() + hbox.pack_start(frame, False, False, 0) + frame.set_hexpand(False) + + hbox2 = Gtk.HBox() + frame.add(hbox2) + hbox2.set_border_width(self.spacing_size) + + image = Gtk.Image() + hbox2.pack_start(image, False, False, 0) + image.set_from_pixbuf(self.pixbuf_dict['ytdl-gui']) + + frame2 = Gtk.Frame() + hbox.pack_start(frame2, True, True, self.spacing_size) + frame2.set_hexpand(True) + + vbox = Gtk.VBox() + frame2.add(vbox) + vbox.set_border_width(self.spacing_size) + + label = Gtk.Label() + vbox.pack_start(label, True, True, 0) + label.set_markup( + '' + _( + 'This tab emulates the classic youtube-dl-gui interface', + ) + '', + ) + + label2 = Gtk.Label() + vbox.pack_start(label2, True, True, 0) + label2.set_markup( + '' + _( + 'Videos downloaded here are not added to Tartube\'s' \ + + ' database', + ) + '', + ) + + self.classic_options_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_PROPERTIES, + Gtk.IconSize.BUTTON, + ) + hbox.pack_start(self.classic_options_button, False, False, 0) + self.classic_options_button.set_action_name( + 'app.classic_options_button', + ) + self.classic_options_button.set_tooltip_text( + _('General download options'), + ) + + self.classic_update_ytdl_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_REDO, + Gtk.IconSize.BUTTON, + ) + hbox.pack_start( + self.classic_update_ytdl_button, + False, + False, + self.spacing_size, + ) + self.classic_update_ytdl_button.set_action_name( + 'app.classic_update_ytdl_button', + ) + self.classic_update_ytdl_button.set_tooltip_text( + _('Update youtube-dl'), + ) + + self.classic_auto_copy_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_COPY, + Gtk.IconSize.BUTTON, + ) + hbox.pack_start(self.classic_auto_copy_button, False, False, 0) + self.classic_auto_copy_button.set_action_name( + 'app.classic_auto_copy_button', + ) + self.classic_auto_copy_button.set_tooltip_text( + _('Enable automatic copy/paste'), + ) + + # Second row - a textview for entering URLs. If automatic copy/paste is + # enabled, URLs are automatically copied into this textview + # -------------------------------------------------------------------- + + label3 = Gtk.Label(_('Enter URLs below')) + self.classic_grid.attach(label3, 0, 1, grid_width, 1) + label3.set_alignment(0, 0.5) + + frame3 = Gtk.Frame() + self.classic_grid.attach(frame3, 0, 2, grid_width, 1) + + scrolled = Gtk.ScrolledWindow() + frame3.add(scrolled) + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + # Set a fixed size, because we assume that the user wants to expand the + # window to see more downloads, not more undownloaded URLs + scrolled.set_size_request(-1, 120) + + self.classic_textview = Gtk.TextView() + scrolled.add(self.classic_textview) + + self.classic_textbuffer = self.classic_textview.get_buffer() + + # (Some callbacks will complain about invalid iterators, if we try to + # use Gtk.TextIters, so use Gtk.TextMarks instead) + self.classic_mark_start = self.classic_textbuffer.create_mark( + 'mark_start', + self.classic_textbuffer.get_start_iter(), + True, # Left gravity + ) + self.classic_mark_end = self.classic_textbuffer.create_mark( + 'mark_end', + self.classic_textbuffer.get_end_iter(), + False, # Not left gravity + ) + + # Third row - widgets to set the download destination and video/audio + # format. The user clicks the 'Add URLs' button to create dummy + # media.Video objects for each URL. Each object is associated with + # the specified destination and format + # -------------------------------------------------------------------- + + # Destination directory + label4 = Gtk.Label(_('Destination:')) + self.classic_grid.attach(label4, 0, 3, 1, 1) + + self.classic_dest_dir_liststore = Gtk.ListStore(str) + for string in self.app_obj.classic_dir_list: + self.classic_dest_dir_liststore.append( [string] ) + + self.classic_dest_dir_combo = Gtk.ComboBox.new_with_model( + self.classic_dest_dir_liststore, + ) + self.classic_grid.attach(self.classic_dest_dir_combo, 1, 3, 1, 1) + renderer_text = Gtk.CellRendererText() + self.classic_dest_dir_combo.pack_start(renderer_text, True) + self.classic_dest_dir_combo.add_attribute(renderer_text, 'text', 0) + self.classic_dest_dir_combo.set_entry_text_column(0) + self.classic_dest_dir_combo.set_active(0) + self.classic_dest_dir_combo.set_hexpand(True) + self.classic_dest_dir_combo.connect( + 'changed', + self.on_classic_dest_dir_combo_changed, + ) + + self.classic_dest_dir_button = Gtk.Button('...') + self.classic_grid.attach(self.classic_dest_dir_button, 2, 3, 1, 1) + self.classic_dest_dir_button.set_action_name( + 'app.classic_dest_dir_button', + ) + self.classic_dest_dir_button.set_tooltip_text( + _('Add a new destination folder'), + ) + self.classic_dest_dir_button.set_hexpand(False) + + # Video/audio format + label5 = Gtk.Label(' ' + _('Format:')) + self.classic_grid.attach(label5, 3, 3, 1, 1) + + combo_list = [_('Default'), _('Video:')] + for item in formats.VIDEO_FORMAT_LIST: + combo_list.append(' ' + item) + + combo_list.append(_('Audio:')) + for item in formats.AUDIO_FORMAT_LIST: + combo_list.append(' ' + item) + + self.classic_format_liststore = Gtk.ListStore(str) + for string in combo_list: + self.classic_format_liststore.append( [string] ) + + self.classic_format_combo = Gtk.ComboBox.new_with_model( + self.classic_format_liststore, + ) + self.classic_grid.attach(self.classic_format_combo, 4, 3, 1, 1) + renderer_text = Gtk.CellRendererText() + self.classic_format_combo.pack_start(renderer_text, True) + self.classic_format_combo.add_attribute(renderer_text, 'text', 0) + self.classic_format_combo.set_entry_text_column(0) + self.classic_format_combo.set_active(0) + # If the user selects the 'Video:' or 'Audio:' items, automatically + # select the first item below that + self.classic_format_combo.connect( + 'changed', + self.on_classic_format_combo_changed, + ) + + # (Add a label for spacing) + label6 = Gtk.Label(' ') + self.classic_grid.attach(label6, 5, 3, 1, 1) + + # Add URLs button + self.classic_add_urls_button = Gtk.Button( + ' ' + _('Add URLs') + ' ', + ) + self.classic_grid.attach(self.classic_add_urls_button, 6, 3, 1, 1) + self.classic_add_urls_button.set_action_name( + 'app.classic_add_urls_button', + ) + self.classic_add_urls_button.set_tooltip_text(_('Add these URLs')) + + # Fourth row - the Classic Progress List. A treeview to display the + # progress of downloads (in Classic Mode, ongoing download + # information is displayed here, rather than in the Progress Tab) + # -------------------------------------------------------------------- + + frame4 = Gtk.Frame() + self.classic_grid.attach(frame4, 0, 4, grid_width, 1) + frame4.set_hexpand(True) + frame4.set_vexpand(True) + + scrolled2 = Gtk.ScrolledWindow() + frame4.add(scrolled2) + scrolled2.set_policy( + Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.AUTOMATIC, + ) + + self.classic_progress_treeview = Gtk.TreeView() + scrolled2.add(self.classic_progress_treeview) + # (Tooltips are initially enabled, and if necessary are disabled by a + # call to self.disable_tooltips() shortly afterwards) + self.classic_progress_treeview.set_tooltip_column( + self.classic_progress_tooltip_column, + ) + # (Detect right-clicks on the treeview) + self.classic_progress_treeview.connect( + 'button-press-event', + self.on_classic_progress_list_right_click, + ) + # (Enable selection of multiple lines) + selection = self.classic_progress_treeview.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + + for i, column_title in enumerate( + [ + 'hide', 'hide', _('Source'), '#', _('Status'), + _('Incoming file'), _('Ext'), '%', _('Speed'), _('ETA'), + _('Size'), + ] + ): + renderer_text = Gtk.CellRendererText() + column_text = Gtk.TreeViewColumn( + column_title, + renderer_text, + text=i, + ) + self.classic_progress_treeview.append_column(column_text) + column_text.set_resizable(True) + column_text.set_min_width(20) + if column_title == 'hide': + column_text.set_visible(False) + + self.classic_progress_liststore = Gtk.ListStore( + int, str, str, str, str, str, str, str, str, str, str, + ) + self.classic_progress_treeview.set_model( + self.classic_progress_liststore, + ) + + # 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 + # -------------------------------------------------------------------- + + hbox3 = Gtk.HBox() + self.classic_grid.attach(hbox3, 0, 5, grid_width, 1) + + self.classic_remove_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_DELETE, + Gtk.IconSize.BUTTON, + ) + hbox3.pack_start(self.classic_remove_button, False, False, 0) + self.classic_remove_button.set_action_name( + 'app.classic_remove_button', + ) + self.classic_remove_button.set_tooltip_text(_('Remove from list')) + + self.classic_play_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_MEDIA_PLAY, + Gtk.IconSize.BUTTON, + ) + hbox3.pack_start( + self.classic_play_button, + False, + False, + self.spacing_size, + ) + self.classic_play_button.set_action_name( + 'app.classic_play_button', + ) + self.classic_play_button.set_tooltip_text(_('Play video')) + + self.classic_move_up_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_GO_UP, + Gtk.IconSize.BUTTON, + ) + hbox3.pack_start(self.classic_move_up_button, False, False, 0) + self.classic_move_up_button.set_action_name( + 'app.classic_move_up_button', + ) + self.classic_move_up_button.set_tooltip_text(_('Move up')) + + self.classic_move_down_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_GO_DOWN, + Gtk.IconSize.BUTTON, + ) + hbox3.pack_start( + self.classic_move_down_button, + False, + False, + self.spacing_size, + ) + self.classic_move_down_button.set_action_name( + 'app.classic_move_down_button', + ) + self.classic_move_down_button.set_tooltip_text(_('Move down')) + + self.classic_redownload_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_REFRESH, + Gtk.IconSize.BUTTON, + ) + hbox3.pack_start(self.classic_redownload_button, False, False, 0) + self.classic_redownload_button.set_action_name( + 'app.classic_redownload_button', + ) + self.classic_redownload_button.set_tooltip_text(_('Re-download')) + + self.classic_stop_button = Gtk.Button.new_from_icon_name( + Gtk.STOCK_MEDIA_STOP, + Gtk.IconSize.BUTTON, + ) + hbox3.pack_start( + self.classic_stop_button, + False, + False, + self.spacing_size, + ) + self.classic_stop_button.set_action_name( + 'app.classic_stop_button', + ) + self.classic_stop_button.set_tooltip_text(_('Stop download')) + + self.classic_download_button = Gtk.Button( + ' ' + _('Download all') + ' ', + ) + hbox3.pack_end(self.classic_download_button, False, False, 0) + self.classic_download_button.set_action_name( + 'app.classic_download_button', + ) + self.classic_download_button.set_tooltip_text( + _('Download the URLs above'), + ) + + def setup_output_tab(self): """Called by self.setup_win(). @@ -1834,7 +2371,7 @@ def setup_output_tab(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1837 setup_output_tab') + utils.debug_time('mwn 2374 setup_output_tab') vbox = Gtk.VBox() self.output_tab.pack_start(vbox, True, True, 0) @@ -1864,7 +2401,7 @@ def setup_errors_tab(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1867 setup_errors_tab') + utils.debug_time('mwn 2404 setup_errors_tab') vbox = Gtk.VBox() self.errors_tab.pack_start(vbox, True, True, 0) @@ -1884,8 +2421,9 @@ def setup_errors_tab(self): self.errors_list_scrolled.add(self.errors_list_treeview) self.errors_list_treeview.set_can_focus(False) - for i, column_title in enumerate(['', '', 'Time', 'Media', 'Message']): - + for i, column_title in enumerate( + ['', '', _('Time'), _('Type'), _('Message')], + ): if not column_title: renderer_pixbuf = Gtk.CellRendererPixbuf() column_pixbuf = Gtk.TreeViewColumn( @@ -1918,7 +2456,7 @@ def setup_errors_tab(self): self.show_system_error_checkbutton = Gtk.CheckButton() hbox.pack_start(self.show_system_error_checkbutton, False, False, 0) self.show_system_error_checkbutton.set_label( - 'Show ' + __main__.__prettyname__ + ' errors', + _('Show Tartube errors'), ) self.show_system_error_checkbutton.set_active( self.app_obj.system_error_show_flag, @@ -1931,7 +2469,7 @@ def setup_errors_tab(self): self.show_system_warning_checkbutton = Gtk.CheckButton() hbox.pack_start(self.show_system_warning_checkbutton, False, False, 0) self.show_system_warning_checkbutton.set_label( - 'Show ' + __main__.__prettyname__ + ' warnings', + _('Show Tartube warnings'), ) self.show_system_warning_checkbutton.set_active( self.app_obj.system_warning_show_flag, @@ -1944,7 +2482,7 @@ def setup_errors_tab(self): self.show_operation_error_checkbutton = Gtk.CheckButton() hbox.pack_start(self.show_operation_error_checkbutton, False, False, 0) self.show_operation_error_checkbutton.set_label( - 'Show server errors', + _('Show server errors'), ) self.show_operation_error_checkbutton.set_active( self.app_obj.operation_error_show_flag, @@ -1962,7 +2500,7 @@ def setup_errors_tab(self): 0, ) self.show_operation_warning_checkbutton.set_label( - 'Show server warnings', + _('Show server warnings'), ) self.show_operation_warning_checkbutton.set_active( self.app_obj.operation_warning_show_flag, @@ -1974,7 +2512,7 @@ def setup_errors_tab(self): self.error_list_button = Gtk.Button() hbox.pack_end(self.error_list_button, False, False, 0) - self.error_list_button.set_label('Clear list') + self.error_list_button.set_label(_('Clear list')) self.error_list_button.connect( 'clicked', self.on_errors_list_clear, @@ -1994,7 +2532,7 @@ def toggle_visibility(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1997 toggle_visibility') + utils.debug_time('mwn 2535 toggle_visibility') if self.is_visible(): self.set_visible(False) @@ -2004,15 +2542,14 @@ def toggle_visibility(self): def redraw_main_toolbar(self): - """Called by mainapp.TartubeApp.load_config(), and also by - .set_toolbar_squeeze_flag() when the value of the flag is changed. + """Called by mainapp.TartubeApp.set_toolbar_squeeze_flag(). Redraws the main toolbar, with or without labels, depending on the value of the flag. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2015 redraw_main_toolbar') + utils.debug_time('mwn 2552 redraw_main_toolbar') self.setup_main_toolbar() @@ -2039,7 +2576,7 @@ def sensitise_widgets_if_database(self, sens_flag): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2042 sensitise_widgets_if_database') + utils.debug_time('mwn 2579 sensitise_widgets_if_database') # Menu items self.change_db_menu_item.set_sensitive(sens_flag) @@ -2111,6 +2648,16 @@ def sensitise_widgets_if_database(self, sens_flag): self.video_res_checkbutton.set_sensitive(sens_flag) self.video_res_combobox.set_sensitive(sens_flag) + # Classic Mode Tab + if __main__.__pkg_strict_install_flag__: + self.classic_update_ytdl_button.set_sensitive(False) + else: + self.classic_update_ytdl_button.set_sensitive(sens_flag) + + self.classic_redownload_button.set_sensitive(sens_flag) + self.classic_stop_button.set_sensitive(False) + self.classic_download_button.set_sensitive(sens_flag) + # Errors/Warnings tab self.show_system_error_checkbutton.set_sensitive(sens_flag) self.show_system_warning_checkbutton.set_sensitive(sens_flag) @@ -2128,7 +2675,7 @@ def desensitise_test_widgets(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2131 desensitise_test_widgets') + utils.debug_time('mwn 2678 desensitise_test_widgets') if self.test_menu_item: self.test_menu_item.set_sensitive(False) @@ -2157,7 +2704,7 @@ def sensitise_operation_widgets(self, sens_flag, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2160 sensitise_operation_widgets') + utils.debug_time('mwn 2707 sensitise_operation_widgets') self.system_prefs_menu_item.set_sensitive(sens_flag) self.gen_options_menu_item.set_sensitive(sens_flag) @@ -2184,6 +2731,8 @@ def sensitise_operation_widgets(self, sens_flag, \ self.test_ytdl_menu_item.set_sensitive(sens_flag) self.install_ffmpeg_menu_item.set_sensitive(sens_flag) + self.update_live_menu_item.set_sensitive(sens_flag) + # (The 'Add videos', 'Add channel' etc menu items/buttons are # sensitised during a download operation, but desensitised during # other operations) @@ -2213,6 +2762,12 @@ def sensitise_operation_widgets(self, sens_flag, \ self.stop_operation_menu_item.set_sensitive(False) self.stop_operation_toolbutton.set_sensitive(False) + # The corresponding buttons in the Classic Mode Tab must also be + # updated + self.classic_redownload_button.set_sensitive(sens_flag) + self.classic_stop_button.set_sensitive(not sens_flag) + self.classic_download_button.set_sensitive(sens_flag) + def show_progress_bar(self, operation_type): @@ -2233,7 +2788,7 @@ def show_progress_bar(self, operation_type): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2236 show_progress_bar') + utils.debug_time('mwn 2791 show_progress_bar') if self.progress_bar: return self.app_obj.system_error( @@ -2266,13 +2821,13 @@ def show_progress_bar(self, operation_type): self.check_media_button.set_action_name('app.check_all_button') self.check_media_button.set_sensitive(False) if operation_type == 'check': - self.check_media_button.set_label('Checking...') + self.check_media_button.set_label(_('Checking...')) elif operation_type == 'download': - self.check_media_button.set_label('Downloading...') + self.check_media_button.set_label(_('Downloading...')) elif operation_type == 'refresh': - self.check_media_button.set_label('Refreshing...') + self.check_media_button.set_label(_('Refreshing...')) else: - self.check_media_button.set_label('Tidying...') + self.check_media_button.set_label(_('Tidying...')) # (Put the progress bar inside a box, so it doesn't touch the divider, # because that doesn't look nice) @@ -2294,13 +2849,13 @@ def show_progress_bar(self, operation_type): self.progress_bar.set_fraction(0) self.progress_bar.set_show_text(True) if operation_type == 'check': - self.progress_bar.set_text('Checking...') + self.progress_bar.set_text(_('Checking...')) elif operation_type == 'download': - self.progress_bar.set_text('Downloading...') + self.progress_bar.set_text(_('Downloading...')) elif operation_type == 'refresh': - self.progress_bar.set_text('Refreshing...') + self.progress_bar.set_text(_('Refreshing...')) else: - self.progress_bar.set_text('Tidying...') + self.progress_bar.set_text(_('Tidying...')) # Make the changes visible self.button_box.show_all() @@ -2316,7 +2871,7 @@ def hide_progress_bar(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2319 hide_progress_bar') + utils.debug_time('mwn 2874 hide_progress_bar') if not self.progress_bar: return self.app_obj.system_error( @@ -2338,17 +2893,17 @@ def hide_progress_bar(self): # Add replacement widgets self.check_media_button = Gtk.Button() self.button_box.pack_start(self.check_media_button, True, True, 0) - self.check_media_button.set_label('Check all') + self.check_media_button.set_label(_('Check all')) self.check_media_button.set_tooltip_text( - 'Check all videos, channels, playlists and folders', + _('Check all videos, channels, playlists and folders'), ) self.check_media_button.set_action_name('app.check_all_button') self.download_media_button = Gtk.Button() self.button_box.pack_start(self.download_media_button, True, True, 0) - self.download_media_button.set_label('Download all') + self.download_media_button.set_label(_('Download all')) self.download_media_button.set_tooltip_text( - 'Download all videos, channels, playlists and folders', + _('Download all videos, channels, playlists and folders'), ) self.download_media_button.set_action_name('app.download_all_button') @@ -2363,6 +2918,29 @@ def hide_progress_bar(self): self.button_box.show_all() + def sensitise_progress_bar(self, sens_flag): + + """Called by mainapp.TartubeApp.download_manager_continue(). + + When a download operation is launched from the Classic Mode Tab, we + don't replace the main Check all/Download all buttons with a progress + bar; instead, we just (de)sensitise the existing buttons. + + Args: + + sens_flag (bool): True to sensitise the buttons, False to + desensitise them + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 2937 sensitise_progress_bar') + + self.check_media_button.set_sensitive(sens_flag) + self.download_media_button.set_sensitive(sens_flag) + self.classic_download_button.set_sensitive(sens_flag) + + def update_progress_bar(self, text, count, total): """Called by downloads.DownloadManager.run(), @@ -2389,7 +2967,7 @@ def update_progress_bar(self, text, count, total): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2392 update_progress_bar') + utils.debug_time('mwn 2970 update_progress_bar') if not self.progress_bar: return self.app_obj.system_error( @@ -2430,7 +3008,7 @@ def sensitise_check_dl_buttons(self, finish_flag, operation_type=None): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2433 sensitise_check_dl_buttons') + utils.debug_time('mwn 3011 sensitise_check_dl_buttons') if operation_type is not None \ and operation_type != 'ffmpeg' and operation_type != 'ytdl' \ @@ -2463,19 +3041,19 @@ def sensitise_check_dl_buttons(self, finish_flag, operation_type=None): if not finish_flag: if operation_type == 'ffmpeg': - self.check_media_button.set_label('Installing') + 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.check_media_button.set_label(_('Updating')) self.download_media_button.set_label('youtube-dl') elif operation_type == 'formats': - self.check_media_button.set_label('Fetching') + self.check_media_button.set_label(_('Fetching')) self.download_media_button.set_label('format list') elif operation_type == 'subs': - self.check_media_button.set_label('Fetching') + self.check_media_button.set_label(_('Fetching')) self.download_media_button.set_label('subtitle list') else: - self.check_media_button.set_label('Testing') + self.check_media_button.set_label(_('Testing')) self.download_media_button.set_label('youtube-dl') self.check_media_button.set_sensitive(False) @@ -2484,16 +3062,16 @@ def sensitise_check_dl_buttons(self, finish_flag, operation_type=None): self.sensitise_operation_widgets(False, True) else: - self.check_media_button.set_label('Check all') + self.check_media_button.set_label(_('Check all')) self.check_media_button.set_sensitive(True) self.check_media_button.set_tooltip_text( - 'Check all videos, channels, playlists and folders', + _('Check all videos, channels, playlists and folders'), ) self.download_media_button.set_label('Download all') self.download_media_button.set_tooltip_text( - 'Download all videos, channels, playlists and folders', + _('Download all videos, channels, playlists and folders'), ) if not self.app_obj.disable_dl_all_flag: @@ -2511,7 +3089,8 @@ def enable_tooltips(self, update_catalogue_flag=False): """Called by mainapp.TartubeApp.set_show_tooltips_flag(). - Enables tooltips in the Video Index and Video Catalogue (only). + Enables tooltips in the Video Index, Video Catalogue, Progress List, + Results List and Classic Mode Tab (only). Args: @@ -2522,7 +3101,7 @@ def enable_tooltips(self, update_catalogue_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2525 enable_tooltips') + utils.debug_time('mwn 3104 enable_tooltips') # Update the Video Index self.video_index_treeview.set_tooltip_column( @@ -2546,13 +3125,19 @@ def enable_tooltips(self, update_catalogue_flag=False): self.results_list_tooltip_column, ) + # Update the Classic Mode Tab + self.classic_progress_treeview.set_tooltip_column( + self.classic_progress_tooltip_column, + ) + def disable_tooltips(self, update_catalogue_flag=False): """Called by mainapp.TartubeApp.load_config() and .set_show_tooltips_flag(). - Disables tooltips in the Video Index and Video Catalogue (only). + Disables tooltips in the Video Index, Video Catalogue, Progress List, + Results List and Classic Mode Tab (only). Args: @@ -2563,7 +3148,7 @@ def disable_tooltips(self, update_catalogue_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2566 disable_tooltips') + utils.debug_time('mwn 3151 disable_tooltips') # Update the Video Index. Using a dummy column makes the tooltips # invisible @@ -2582,6 +3167,9 @@ def disable_tooltips(self, update_catalogue_flag=False): # Update the Results List self.results_list_treeview.set_tooltip_column(-1) + # Update the Classic Mode Tab + self.classic_progress_treeview.set_tooltip_column(-1) + def enable_dl_all_buttons(self): @@ -2591,7 +3179,7 @@ def enable_dl_all_buttons(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2594 enable_dl_all_buttons') + utils.debug_time('mwn 3182 enable_dl_all_buttons') # This setting doesn't apply during a download/update/refresh/info/tidy # operation @@ -2603,14 +3191,14 @@ def enable_dl_all_buttons(self): def disable_dl_all_buttons(self): - """Called by mainapp.TartubeApp.load_config() and + """Called by mainapp.TartubeApp.start() and set_disable_dl_all_flag(). Disables (desensitises) the 'Download all' buttons and menu items. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2613 disable_dl_all_buttons') + utils.debug_time('mwn 3201 disable_dl_all_buttons') # This setting doesn't apply during a download/update/refresh/info/tidy # operation @@ -2620,38 +3208,7 @@ def disable_dl_all_buttons(self): self.download_media_button.set_sensitive(False) - def set_video_res_limit(self, resolution): - - """Called by mainapp.TartubeApp.load_config() and - self.setup_progress_tab(). - - Sets a new video resolution limit. Updates the combobox in the - Progress Tab, and calls the main application to update its IV. - - Args: - - resolution (str): The new progressive scan resolution; a key in - formats.VIDEO_RESOLUTION_DICT (e.g. '720p'), or None to use the - default resolution limit specified by - formats.VIDEO_RESOLUTION_DEFAULT. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2641 set_video_res_limit') - - # Check it's a recognised value - if not resolution in formats.VIDEO_RESOLUTION_LIST: - resolution = formats.VIDEO_RESOLUTION_DEFAULT - - self.video_res_combobox.set_active( - formats.VIDEO_RESOLUTION_LIST.index(resolution), - ) - - self.app_obj.set_video_res_default(resolution) - - - def notify_desktop(self, title=None, msg=None, icon_path=None): + def notify_desktop(self, title=None, msg=None, icon_path=None, url=None): """Can be called by anything. @@ -2659,28 +3216,31 @@ def notify_desktop(self, title=None, msg=None, icon_path=None): Args: - title (str): The notification title. If None, __prettyname__ is + title (str): The notification title. If None, 'Tartube' is used used - msg (str): The message to show. If None, __prettyname__ is used + msg (str): The message to show. If None, 'Tartube' is used icon_path (str): The absolute path to the icon file to use. If None, a default icon is used + url (str): If specified, a 'Click to open' button is added to the + desktop notification. Clicking the button opens the URL + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2673 notify_desktop') + utils.debug_time('mwn 3233 notify_desktop') # Desktop notifications don't work on MS Windows if os.name != 'nt': if title is None: - title = __main__.__prettyname__ + title = 'Tartube' if msg is None: # Emergency fallback - better than an empty message - msg = __main__.__prettyname__ + msg = 'Tartube' if icon_path is None: icon_path = os.path.abspath( @@ -2692,13 +3252,36 @@ def notify_desktop(self, title=None, msg=None, icon_path=None): ) notify_obj = Notify.Notification.new(title, msg, icon_path) + + if url is not None: + + # We need to retain a reference to the Notify.Notification, or + # the callback won't work + self.notify_desktop_count += 1 + self.notify_desktop_dict[self.notify_desktop_count] \ + = notify_obj + + notify_obj.add_action( + 'action_click', + 'Watch', + self.on_notify_desktop_clicked, + self.notify_desktop_count, + url, + ) + + notify_obj.connect( + 'closed', + self.on_notify_desktop_closed, + self.notify_desktop_count, + ) + + # Notification is ready; show it notify_obj.show() def update_show_filter_widgets(self): - """Called by mainapp.TartubeApp.load_config() and - .on_button_show_filter() + """Called by mainapp.TartubeApp.start() and .on_button_show_filter(). The toolbar just below the Video Catalogue consists of two rows, the second of which is hidden by default. Show or hide the second row, @@ -2706,7 +3289,7 @@ def update_show_filter_widgets(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2709 update_show_filter_widgets') + utils.debug_time('mwn 3292 update_show_filter_widgets') if not self.app_obj.catalogue_show_filter_flag: @@ -2716,7 +3299,7 @@ def update_show_filter_widgets(self): ) self.catalogue_show_filter_button.set_tooltip_text( - 'Show filter options', + _('Show filter options'), ) if self.catalogue_toolbar2 \ @@ -2732,7 +3315,7 @@ def update_show_filter_widgets(self): ) self.catalogue_show_filter_button.set_tooltip_text( - 'Hide filter options', + _('Hide filter options'), ) if not self.catalogue_toolbar2 \ @@ -2757,8 +3340,7 @@ def update_show_filter_widgets(self): def update_alpha_sort_widgets(self): - """Called by mainapp.TartubeApp.load_config() and - .on_button_sort_type(). + """Called by mainapp.TartubeApp.start() and .on_button_sort_type(). Videos in the Video Catalogue can be sorted by date (default), or alphabetically. When the user switches between them, update the @@ -2766,46 +3348,31 @@ def update_alpha_sort_widgets(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2769 update_alpha_sort_widgets') + utils.debug_time('mwn 3351 update_alpha_sort_widgets') if not self.app_obj.catalogue_alpha_sort_flag: self.catalogue_sort_button.set_stock_id( Gtk.STOCK_SPELL_CHECK, ) - self.catalogue_sort_button.set_tooltip_text('Sort alphabetically') + self.catalogue_sort_button.set_tooltip_text( + _('Sort alphabetically'), + ) else: self.catalogue_sort_button.set_stock_id( Gtk.STOCK_INDEX, ) - self.catalogue_sort_button.set_tooltip_text('Sort by date') + self.catalogue_sort_button.set_tooltip_text(_('Sort by date')) - def update_use_regex_widgets(self): + # (Auto-sort functions for main window widgets) - """Called by mainapp.TartubeApp.load_config(). - After loading the config file, toggle the 'Regex' button in the toolbar - just below the Video Catalogue. - """ + def video_index_auto_sort(self, treestore, row_iter1, row_iter2, data): - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2795 update_use_regex_widgets') - - if not self.app_obj.catologue_use_regex_flag: - self.catalogue_regex_togglebutton.set_active(False) - else: - self.catalogue_regex_togglebutton.set_active(True) - - - # (Auto-sort functions for main window widgets) - - - def video_index_auto_sort(self, treestore, row_iter1, row_iter2, data): - - """Sorting function created by self.video_index_reset(). + """Sorting function created by self.video_index_reset(). Automatically sorts rows in the Video Index. @@ -2826,7 +3393,7 @@ def video_index_auto_sort(self, treestore, row_iter1, row_iter2, data): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2829 video_index_auto_sort') + utils.debug_time('mwn 3396 video_index_auto_sort') # If auto-sorting is disabled temporarily, we can prevent the list # being sorted by returning -1 for all cases @@ -2919,7 +3486,7 @@ def video_catalogue_auto_sort(self, row1, row2, data, notify): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2922 video_catalogue_auto_sort') + utils.debug_time('mwn 3489 video_catalogue_auto_sort') # Get the media.Video objects displayed on each row obj1 = row1.video_obj @@ -2928,11 +3495,17 @@ def video_catalogue_auto_sort(self, row1, row2, data, notify): # Sort by date if not self.app_obj.catalogue_alpha_sort_flag: - # Sort videos by playlist index (if set), then by upload time, and - # then by receive (download) time + # Sort videos by livestream mode (if applicable), then by playlist + # index (if set), then by upload time, and then by receive + # (download) time # The video's index is not relevant unless sorting a playlist (and # not relevant in private folders, e.g. 'All Videos') - if isinstance(obj1.parent_obj, media.Playlist) \ + if obj1.live_mode > obj2.live_mode: + return -1 + elif obj1.live_mode < obj2.live_mode: + return 1 + + elif isinstance(obj1.parent_obj, media.Playlist) \ and not self.video_index_current_priv_flag \ and obj1.parent_obj == obj2.parent_obj \ and obj1.index is not None and obj2.index is not None: @@ -2989,8 +3562,8 @@ def video_index_popup_menu(self, event, name): """Called by self.on_video_index_right_click(). - When the user right-clicks on the Video Index, show a context-sensitive - popup menu. + When the user right-clicks on the Video Index, shows a + context-sensitive popup menu. Args: @@ -3001,7 +3574,7 @@ def video_index_popup_menu(self, event, name): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 3004 video_index_popup_menu') + utils.debug_time('mwn 3577 video_index_popup_menu') # Find the right-clicked media data object (and a string to describe # its type) @@ -3013,9 +3586,14 @@ def video_index_popup_menu(self, event, name): popup_menu = Gtk.Menu() # Check/download/refresh items - check_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Check ' + media_type, - ) + if media_type == 'channel': + msg = _('_Check channel') + elif media_type == 'playlist': + msg = _('_Check playlist') + else: + msg = _('_Check folder') + + check_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) check_menu_item.connect( 'activate', self.on_video_index_check, @@ -3029,9 +3607,14 @@ def video_index_popup_menu(self, event, name): check_menu_item.set_sensitive(False) popup_menu.append(check_menu_item) - download_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Download ' + media_type, - ) + if media_type == 'channel': + msg = _('_Download channel') + elif media_type == 'playlist': + msg = _('_Download playlist') + else: + msg = _('_Download folder') + + download_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) download_menu_item.connect( 'activate', self.on_video_index_download, @@ -3045,9 +3628,14 @@ def video_index_popup_menu(self, event, name): download_menu_item.set_sensitive(False) popup_menu.append(download_menu_item) - custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'C_ustom download ' + media_type, - ) + if media_type == 'channel': + msg = _('C_ustom download channel') + elif media_type == 'playlist': + msg = _('C_ustom download playlist') + else: + msg = _('C_ustom download folder') + + custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) custom_dl_menu_item.connect( 'activate', self.on_video_index_custom_dl, @@ -3090,7 +3678,7 @@ def video_index_popup_menu(self, event, name): all_contents_submenu.append(Gtk.SeparatorMenuItem()) empty_folder_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Empty folder', + _('_Empty folder'), ) empty_folder_menu_item.connect( 'activate', @@ -3102,7 +3690,7 @@ def video_index_popup_menu(self, event, name): empty_folder_menu_item.set_sensitive(False) all_contents_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_All contents', + _('_All contents'), ) all_contents_menu_item.set_submenu(all_contents_submenu) contents_submenu.append(all_contents_menu_item) @@ -3120,7 +3708,7 @@ def video_index_popup_menu(self, event, name): just_videos_submenu.append(Gtk.SeparatorMenuItem()) empty_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Remove videos', + _('_Remove videos'), ) empty_videos_menu_item.connect( 'activate', @@ -3132,14 +3720,19 @@ def video_index_popup_menu(self, event, name): empty_videos_menu_item.set_sensitive(False) just_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Just folder videos', + _('_Just folder videos'), ) just_videos_menu_item.set_submenu(just_videos_submenu) contents_submenu.append(just_videos_menu_item) - contents_menu_item = Gtk.MenuItem.new_with_mnemonic( - utils.upper_case_first(media_type) + ' co_ntents', - ) + if media_type == 'channel': + string = _('Channel co_ntents') + elif media_type == 'playlist': + string = _('Playlist co_ntents') + else: + string = _('Folder co_ntents') + + contents_menu_item = Gtk.MenuItem.new_with_mnemonic(string) contents_menu_item.set_submenu(contents_submenu) popup_menu.append(contents_menu_item) if not media_data_obj.child_list: @@ -3149,7 +3742,7 @@ def video_index_popup_menu(self, event, name): actions_submenu = Gtk.Menu() move_top_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Move to top level', + _('_Move to top level'), ) move_top_menu_item.connect( 'activate', @@ -3165,16 +3758,16 @@ def video_index_popup_menu(self, event, name): actions_submenu.append(Gtk.SeparatorMenuItem()) convert_text = None - if isinstance(media_data_obj, media.Channel): - convert_text = '_Convert to playlist' - elif isinstance(media_data_obj, media.Playlist): - convert_text = '_Convert to channel' + if media_type == 'channel': + msg = _('_Convert to playlist') + elif media_type == 'playlist': + msg = _('_Convert to channel') else: - convert_text = None + msg = None - if convert_text: + if msg: - convert_menu_item = Gtk.MenuItem.new_with_mnemonic(convert_text) + convert_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) convert_menu_item.connect( 'activate', self.on_video_index_convert_container, @@ -3190,7 +3783,7 @@ def video_index_popup_menu(self, event, name): if isinstance(media_data_obj, media.Folder): hide_folder_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Hide folder', + _('_Hide folder'), ) hide_folder_menu_item.connect( 'activate', @@ -3199,9 +3792,14 @@ def video_index_popup_menu(self, event, name): ) actions_submenu.append(hide_folder_menu_item) - rename_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Rename ' + media_type + '...', - ) + if media_type == 'channel': + msg = _('_Rename channel...') + elif media_type == 'playlist': + msg = _('_Rename playlist...') + else: + msg = _('_Rename folder...') + + rename_location_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) rename_location_menu_item.connect( 'activate', self.on_video_index_rename_location, @@ -3216,7 +3814,7 @@ def video_index_popup_menu(self, event, name): rename_location_menu_item.set_sensitive(False) set_nickname_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Set _nickname...', + _('Set _nickname...'), ) set_nickname_menu_item.connect( 'activate', @@ -3229,7 +3827,7 @@ def video_index_popup_menu(self, event, name): set_nickname_menu_item.set_sensitive(False) set_destination_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Set _download destination...', + _('Set _download destination...'), ) set_destination_menu_item.connect( 'activate', @@ -3244,9 +3842,14 @@ def video_index_popup_menu(self, event, name): # Separator actions_submenu.append(Gtk.SeparatorMenuItem()) - export_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Export ' + media_type + '...', - ) + if media_type == 'channel': + msg = _('_Export channel...') + elif media_type == 'playlist': + msg = _('_Export playlist...') + else: + msg = _('_Export folder...') + + export_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) export_menu_item.connect( 'activate', self.on_video_index_export, @@ -3256,9 +3859,14 @@ def video_index_popup_menu(self, event, name): if self.app_obj.current_manager_obj: export_menu_item.set_sensitive(False) - refresh_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Re_fresh ' + media_type, - ) + if media_type == 'channel': + msg = _('Re_fresh channel') + elif media_type == 'playlist': + msg = _('Re_fresh playlist') + else: + msg = _('Re_fresh folder') + + refresh_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) refresh_menu_item.connect( 'activate', self.on_video_index_refresh, @@ -3272,9 +3880,14 @@ def video_index_popup_menu(self, event, name): refresh_menu_item.set_sensitive(False) actions_submenu.append(refresh_menu_item) - tidy_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Tidy up ' + media_type, - ) + if media_type == 'channel': + msg = _('_Tidy up channel') + elif media_type == 'playlist': + msg = _('_Tidy up playlist') + else: + msg = _('_Tidy up folder') + + tidy_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) tidy_menu_item.connect( 'activate', self.on_video_index_tidy, @@ -3288,9 +3901,14 @@ def video_index_popup_menu(self, event, name): tidy_menu_item.set_sensitive(False) actions_submenu.append(tidy_menu_item) - actions_menu_item = Gtk.MenuItem.new_with_mnemonic( - utils.upper_case_first(media_type) + ' _actions', - ) + if media_type == 'channel': + msg = _('Channel _actions') + elif media_type == 'playlist': + msg = _('Playlist _actions') + else: + msg = _('Folder _actions') + + actions_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) actions_menu_item.set_submenu(actions_submenu) popup_menu.append(actions_menu_item) @@ -3308,7 +3926,7 @@ def video_index_popup_menu(self, event, name): if not media_data_obj.options_obj: apply_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Apply download options...', + _('_Apply download options...'), ) apply_options_menu_item.connect( 'activate', @@ -3326,7 +3944,7 @@ def video_index_popup_menu(self, event, name): else: remove_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Remove download options', + _('_Remove download options'), ) remove_options_menu_item.connect( 'activate', @@ -3342,7 +3960,7 @@ def video_index_popup_menu(self, event, name): remove_options_menu_item.set_sensitive(False) edit_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Edit download options...', + _('_Edit download options...'), ) edit_options_menu_item.connect( 'activate', @@ -3358,7 +3976,7 @@ def video_index_popup_menu(self, event, name): downloads_submenu.append(Gtk.SeparatorMenuItem()) show_system_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Show system command', + _('_Show system command'), ) show_system_menu_item.connect( 'activate', @@ -3371,7 +3989,7 @@ def video_index_popup_menu(self, event, name): downloads_submenu.append(Gtk.SeparatorMenuItem()) disable_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - '_Disable checking/downloading', + _('_Disable checking/downloading'), ) disable_menu_item.set_active(media_data_obj.dl_disable_flag) disable_menu_item.connect( @@ -3383,7 +4001,7 @@ def video_index_popup_menu(self, event, name): # (Widget sensitivity set below) enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - '_Just disable downloading', + _('_Just disable downloading'), ) enforce_check_menu_item.set_active(media_data_obj.dl_sim_flag) enforce_check_menu_item.connect( @@ -3408,16 +4026,21 @@ def video_index_popup_menu(self, event, name): disable_menu_item.set_sensitive(False) enforce_check_menu_item.set_sensitive(False) - downloads_menu_item = Gtk.MenuItem.new_with_mnemonic('D_ownloads') + downloads_menu_item = Gtk.MenuItem.new_with_mnemonic(_('D_ownloads')) downloads_menu_item.set_submenu(downloads_submenu) popup_menu.append(downloads_menu_item) # Show show_submenu = Gtk.Menu() - show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic( - utils.upper_case_first(media_type) + ' _properties...', - ) + if media_type == 'channel': + msg = _('Channel _properties...') + elif media_type == 'playlist': + msg = _('Playlist _properties...') + else: + msg = _('Folder _properties...') + + show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) show_properties_menu_item.connect( 'activate', self.on_video_index_show_properties, @@ -3431,7 +4054,7 @@ def video_index_popup_menu(self, event, name): show_submenu.append(Gtk.SeparatorMenuItem()) show_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Default location', + _('_Default location'), ) show_location_menu_item.connect( 'activate', @@ -3444,7 +4067,7 @@ def video_index_popup_menu(self, event, name): show_location_menu_item.set_sensitive(False) show_destination_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Actual location', + _('_Actual location'), ) show_destination_menu_item.connect( 'activate', @@ -3456,7 +4079,7 @@ def video_index_popup_menu(self, event, name): and media_data_obj.priv_flag: show_destination_menu_item.set_sensitive(False) - show_menu_item = Gtk.MenuItem.new_with_mnemonic('_Show') + show_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Show')) show_menu_item.set_submenu(show_submenu) popup_menu.append(show_menu_item) @@ -3464,9 +4087,14 @@ def video_index_popup_menu(self, event, name): popup_menu.append(Gtk.SeparatorMenuItem()) # Delete items - delete_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'D_elete ' + media_type, - ) + if media_type == 'channel': + msg = _('D_elete channel') + elif media_type == 'playlist': + msg = _('D_elete playlist') + else: + msg = _('D_elete folder') + + delete_menu_item = Gtk.MenuItem.new_with_mnemonic(msg) delete_menu_item.connect( 'activate', self.on_video_index_delete_container, @@ -3486,7 +4114,7 @@ def video_catalogue_popup_menu(self, event, video_obj): """Called by mainwin.SimpleCatalogueItem.on_right_click_row() and mainwin.ComplexCatalogueItem.on_right_click_row(). - When the user right-clicks on the Video Catalogue, show a context- + When the user right-clicks on the Video Catalogue, shows a context- sensitive popup menu. Args: @@ -3499,7 +4127,7 @@ def video_catalogue_popup_menu(self, event, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 3502 video_catalogue_popup_menu') + utils.debug_time('mwn 4130 video_catalogue_popup_menu') # Use a different popup menu for multiple selected rows # Because of Gtk weirdness, check that the clicked row is actually @@ -3523,54 +4151,71 @@ def video_catalogue_popup_menu(self, event, video_obj): # Check/download videos check_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Check video' + _('_Check video'), ) check_menu_item.connect( 'activate', self.on_video_catalogue_check, video_obj, ) - if self.app_obj.current_manager_obj: + # (We can add another video to the downloads.DownloadList object, even + # after a download operation has started, but this isn't allowed when + # a different type of operation is running) + if ( + self.app_obj.current_manager_obj \ + and not self.app_obj.download_manager_obj + ) or ( + self.app_obj.download_manager_obj \ + and self.app_obj.download_manager_obj.operation_type != 'sim' + ): check_menu_item.set_sensitive(False) popup_menu.append(check_menu_item) if not video_obj.dl_flag: download_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Download video' + _('_Download video'), ) download_menu_item.connect( 'activate', self.on_video_catalogue_download, video_obj, ) - if self.app_obj.current_manager_obj: + if ( + self.app_obj.current_manager_obj \ + and not self.app_obj.download_manager_obj + ) or ( + self.app_obj.download_manager_obj \ + and self.app_obj.download_manager_obj.operation_type != 'real' + ) or video_obj.live_mode == 1: download_menu_item.set_sensitive(False) popup_menu.append(download_menu_item) else: download_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Re-_download this video' + _('Re-_download this video') ) download_menu_item.connect( 'activate', self.on_video_catalogue_re_download, video_obj, ) - if self.app_obj.current_manager_obj: + if self.app_obj.current_manager_obj \ + or video_obj.live_mode == 1: download_menu_item.set_sensitive(False) popup_menu.append(download_menu_item) custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'C_ustom download video' + _('C_ustom download video') ) custom_dl_menu_item.connect( 'activate', self.on_video_catalogue_custom_dl, video_obj, ) - if self.app_obj.current_manager_obj: + if self.app_obj.current_manager_obj \ + or video_obj.live_mode != 0: custom_dl_menu_item.set_sensitive(False) popup_menu.append(custom_dl_menu_item) @@ -3595,7 +4240,7 @@ def video_catalogue_popup_menu(self, event, video_obj): if not video_obj.options_obj: apply_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Apply download options...', + _('_Apply download options...'), ) apply_options_menu_item.connect( 'activate', @@ -3609,7 +4254,7 @@ def video_catalogue_popup_menu(self, event, video_obj): else: remove_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Remove download options', + _('_Remove download options'), ) remove_options_menu_item.connect( 'activate', @@ -3621,7 +4266,7 @@ def video_catalogue_popup_menu(self, event, video_obj): remove_options_menu_item.set_sensitive(False) edit_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Edit download options...', + _('_Edit download options...'), ) edit_options_menu_item.connect( 'activate', @@ -3637,7 +4282,7 @@ def video_catalogue_popup_menu(self, event, video_obj): downloads_submenu.append(Gtk.SeparatorMenuItem()) show_system_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Show system _command', + _('Show system _command'), ) show_system_menu_item.connect( 'activate', @@ -3647,7 +4292,7 @@ def video_catalogue_popup_menu(self, event, video_obj): downloads_submenu.append(show_system_menu_item) test_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Test system command', + _('_Test system command'), ) test_dl_menu_item.connect( 'activate', @@ -3662,7 +4307,7 @@ def video_catalogue_popup_menu(self, event, video_obj): downloads_submenu.append(Gtk.SeparatorMenuItem()) enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - '_Disable downloads', + _('_Disable downloads'), ) enforce_check_menu_item.set_active(video_obj.dl_sim_flag) enforce_check_menu_item.connect( @@ -3680,7 +4325,7 @@ def video_catalogue_popup_menu(self, event, video_obj): enforce_check_menu_item.set_sensitive(False) downloads_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Down_loads', + _('D_ownloads'), ) downloads_menu_item.set_submenu(downloads_submenu) popup_menu.append(downloads_menu_item) @@ -3692,7 +4337,7 @@ def video_catalogue_popup_menu(self, event, video_obj): mark_video_submenu = Gtk.Menu() archive_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is _archived', + _('Video is _archived'), ) archive_video_menu_item.set_active(video_obj.archive_flag) archive_video_menu_item.connect( @@ -3705,7 +4350,7 @@ def video_catalogue_popup_menu(self, event, video_obj): archive_video_menu_item.set_sensitive(False) bookmark_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is _bookmarked', + _('Video is _bookmarked'), ) bookmark_video_menu_item.set_active(video_obj.bookmark_flag) bookmark_video_menu_item.connect( @@ -3716,7 +4361,7 @@ def video_catalogue_popup_menu(self, event, video_obj): mark_video_submenu.append(bookmark_video_menu_item) fav_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is _favourite', + _('Video is _favourite'), ) fav_video_menu_item.set_active(video_obj.fav_flag) fav_video_menu_item.connect( @@ -3727,7 +4372,7 @@ def video_catalogue_popup_menu(self, event, video_obj): mark_video_submenu.append(fav_video_menu_item) new_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is _new', + _('Video is _new'), ) new_video_menu_item.set_active(video_obj.new_flag) new_video_menu_item.connect( @@ -3740,7 +4385,7 @@ def video_catalogue_popup_menu(self, event, video_obj): new_video_menu_item.set_sensitive(False) playlist_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is in _waiting list', + _('Video is in _waiting list'), ) playlist_video_menu_item.set_active(video_obj.waiting_flag) playlist_video_menu_item.connect( @@ -3751,16 +4396,18 @@ def video_catalogue_popup_menu(self, event, video_obj): mark_video_submenu.append(playlist_video_menu_item) mark_video_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Mark video', + _('_Mark video'), ) mark_video_menu_item.set_submenu(mark_video_submenu) popup_menu.append(mark_video_menu_item) + if video_obj.live_mode != 0: + mark_video_menu_item.set_sensitive(False) # Show location/properties show_submenu = Gtk.Menu() show_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Location', + _('_Location'), ) show_location_menu_item.connect( 'activate', @@ -3770,7 +4417,7 @@ def video_catalogue_popup_menu(self, event, video_obj): show_submenu.append(show_location_menu_item) show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Properties...', + _('_Properties...'), ) show_properties_menu_item.connect( 'activate', @@ -3782,7 +4429,7 @@ def video_catalogue_popup_menu(self, event, video_obj): show_properties_menu_item.set_sensitive(False) show_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Show video', + _('_Show video'), ) show_menu_item.set_submenu(show_submenu) popup_menu.append(show_menu_item) @@ -3791,7 +4438,7 @@ def video_catalogue_popup_menu(self, event, video_obj): fetch_submenu = Gtk.Menu() fetch_formats_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Available _formats', + _('Available _formats'), ) fetch_formats_menu_item.connect( 'activate', @@ -3801,7 +4448,7 @@ def video_catalogue_popup_menu(self, event, video_obj): fetch_submenu.append(fetch_formats_menu_item) fetch_subs_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Available _subtitles', + _('Available _subtitles'), ) fetch_subs_menu_item.connect( 'activate', @@ -3811,7 +4458,7 @@ def video_catalogue_popup_menu(self, event, video_obj): fetch_submenu.append(fetch_subs_menu_item) fetch_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Fetch', + _('_Fetch'), ) fetch_menu_item.set_submenu(fetch_submenu) popup_menu.append(fetch_menu_item) @@ -3822,7 +4469,7 @@ def video_catalogue_popup_menu(self, event, video_obj): popup_menu.append(Gtk.SeparatorMenuItem()) # Delete video - delete_menu_item = Gtk.MenuItem.new_with_mnemonic('D_elete video') + delete_menu_item = Gtk.MenuItem.new_with_mnemonic(_('D_elete video')) delete_menu_item.connect( 'activate', self.on_video_catalogue_delete_video, @@ -3840,7 +4487,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): """Called by self.video_catalogue_popup_menu(). When multiple rows are selected in the Video Catalogue and the user - right-clicks one of them, show a context-sensitive popup menu. + right-clicks one of them, shows a context-sensitive popup menu. Args: @@ -3853,7 +4500,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 3856 video_catalogue_multi_popup_menu') + utils.debug_time('mwn 4503 video_catalogue_multi_popup_menu') # Convert row_list, a list of mainwin.CatalogueRow objects, into a # list of media.Video objects @@ -3889,43 +4536,73 @@ def video_catalogue_multi_popup_menu(self, event, row_list): temp_folder_flag = True break + # Also work out if any videos are waiting or broadcasting livestreams + live_flag = False + live_wait_flag = False + for video_obj in video_list: + if video_obj.live_mode == 1: + live_flag = True + live_wait_flag = True + break + + live_broadcast_flag = False + for video_obj in video_list: + if video_obj.live_mode == 2: + live_flag = True + live_broadcast_flag = True + break + # Set up the popup menu popup_menu = Gtk.Menu() # Check/download videos - check_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Check videos' - ) + check_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Check videos')) check_menu_item.connect( 'activate', self.on_video_catalogue_check_multi, video_list, ) - if self.app_obj.current_manager_obj: + # (We can add another video to the downloads.DownloadList object, even + # after a download operation has started, but this isn't allowed when + # a different type of operation is running) + if ( + self.app_obj.current_manager_obj \ + and not self.app_obj.download_manager_obj + ) or ( + self.app_obj.download_manager_obj \ + and self.app_obj.download_manager_obj.operation_type != 'sim' + ): check_menu_item.set_sensitive(False) popup_menu.append(check_menu_item) download_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Download videos' + _('_Download videos') ) download_menu_item.connect( 'activate', self.on_video_catalogue_download_multi, video_list, + live_wait_flag, ) - if self.app_obj.current_manager_obj: + if ( + self.app_obj.current_manager_obj \ + and not self.app_obj.download_manager_obj + ) or ( + self.app_obj.download_manager_obj \ + and self.app_obj.download_manager_obj.operation_type != 'real' + ) or live_wait_flag: download_menu_item.set_sensitive(False) popup_menu.append(download_menu_item) custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'C_ustom download videos' + _('C_ustom download videos') ) custom_dl_menu_item.connect( 'activate', self.on_video_catalogue_custom_dl_multi, video_list, ) - if self.app_obj.current_manager_obj: + if self.app_obj.current_manager_obj or live_flag: custom_dl_menu_item.set_sensitive(False) popup_menu.append(custom_dl_menu_item) @@ -3933,10 +4610,10 @@ def video_catalogue_multi_popup_menu(self, event, row_list): popup_menu.append(Gtk.SeparatorMenuItem()) # Watch video in player/download and watch - if not_dl_flag: + if not_dl_flag or live_flag: dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'D_ownload and watch', + _('D_ownload and watch'), ) dl_watch_menu_item.connect( 'activate', @@ -3946,13 +4623,14 @@ def video_catalogue_multi_popup_menu(self, event, row_list): popup_menu.append(dl_watch_menu_item) if not source_flag \ or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj: + or self.app_obj.refresh_manager_obj \ + or live_flag: dl_watch_menu_item.set_sensitive(False) else: watch_player_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch in _player', + _('Watch in _player'), ) watch_player_menu_item.connect( 'activate', @@ -3962,7 +4640,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): popup_menu.append(watch_player_menu_item) watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _website', + _('Watch on _website'), ) watch_website_menu_item.connect( 'activate', @@ -3978,14 +4656,10 @@ def video_catalogue_multi_popup_menu(self, event, row_list): # Download to Temporary Videos temp_submenu = Gtk.Menu() - if not video_obj.source \ - or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or temp_folder_flag: - temp_submenu.set_sensitive(False) mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Mark for download') + _('_Mark for download'), + ) mark_temp_dl_menu_item.connect( 'activate', self.on_video_catalogue_mark_temp_dl_multi, @@ -3996,7 +4670,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): # Separator temp_submenu.append(Gtk.SeparatorMenuItem()) - temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic('_Download') + temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download')) temp_dl_menu_item.connect( 'activate', self.on_video_catalogue_temp_dl_multi, @@ -4006,7 +4680,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): temp_submenu.append(temp_dl_menu_item) temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Download and watch', + _('_Download and watch'), ) temp_dl_watch_menu_item.connect( 'activate', @@ -4017,10 +4691,16 @@ def video_catalogue_multi_popup_menu(self, event, row_list): temp_submenu.append(temp_dl_watch_menu_item) temp_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Temporary', + _('_Temporary'), ) temp_menu_item.set_submenu(temp_submenu) popup_menu.append(temp_menu_item) + if not video_obj.source \ + or self.app_obj.update_manager_obj \ + or self.app_obj.refresh_manager_obj \ + or temp_folder_flag \ + or live_flag: + temp_menu_item.set_sensitive(False) # Separator popup_menu.append(Gtk.SeparatorMenuItem()) @@ -4029,7 +4709,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu = Gtk.Menu() archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Archived' + _('_Archived'), ) archive_menu_item.connect( 'activate', @@ -4042,7 +4722,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(archive_menu_item) not_archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not a_rchived' + _('Not a_rchived'), ) not_archive_menu_item.connect( 'activate', @@ -4058,7 +4738,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(Gtk.SeparatorMenuItem()) bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Bookmarked' + _('_Bookmarked'), ) bookmark_menu_item.connect( 'activate', @@ -4071,7 +4751,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(bookmark_menu_item) not_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not b_ookmarked' + _('Not b_ookmarked'), ) not_bookmark_menu_item.connect( 'activate', @@ -4087,7 +4767,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(Gtk.SeparatorMenuItem()) fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Favourite' + _('_Favourite'), ) fav_menu_item.connect( 'activate', @@ -4100,7 +4780,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(fav_menu_item) not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not fa_vourite' + _('Not fa_vourite'), ) not_fav_menu_item.connect( 'activate', @@ -4116,7 +4796,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(Gtk.SeparatorMenuItem()) new_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_New' + _('_New'), ) new_menu_item.connect( 'activate', @@ -4129,7 +4809,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(new_menu_item) not_new_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not n_ew' + _('Not n_ew'), ) not_new_menu_item.connect( 'activate', @@ -4145,7 +4825,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(Gtk.SeparatorMenuItem()) playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'In _waiting list' + _('In _waiting list'), ) playlist_menu_item.connect( 'activate', @@ -4158,7 +4838,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(playlist_menu_item) not_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not in w_aiting list' + _('Not in w_aiting list'), ) not_playlist_menu_item.connect( 'activate', @@ -4171,14 +4851,16 @@ def video_catalogue_multi_popup_menu(self, event, row_list): mark_videos_submenu.append(not_playlist_menu_item) mark_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Mark videos', + _('_Mark videos'), ) mark_videos_menu_item.set_submenu(mark_videos_submenu) popup_menu.append(mark_videos_menu_item) + if live_flag: + mark_videos_menu_item.set_sensitive(False) # Show properties show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Show p_roperties...', + _('Show p_roperties...'), ) show_properties_menu_item.connect( 'activate', @@ -4193,7 +4875,7 @@ def video_catalogue_multi_popup_menu(self, event, row_list): popup_menu.append(Gtk.SeparatorMenuItem()) # Delete videos - delete_menu_item = Gtk.MenuItem.new_with_mnemonic('D_elete videos') + delete_menu_item = Gtk.MenuItem.new_with_mnemonic(_('D_elete videos')) delete_menu_item.connect( 'activate', self.on_video_catalogue_delete_video_multi, @@ -4210,7 +4892,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): """Called by self.on_progress_list_right_click(). - When the user right-clicks on the Progress List, show a context- + When the user right-clicks on the Progress List, shows a context- sensitive popup menu. Args: @@ -4225,7 +4907,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4228 progress_list_popup_menu') + utils.debug_time('mwn 4910 progress_list_popup_menu') # Find the downloads.VideoDownloader which is currently handling the # clicked media data object (if any) @@ -4258,9 +4940,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): popup_menu = Gtk.Menu() # Stop check/download - stop_now_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Stop now', - ) + stop_now_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Stop now')) stop_now_menu_item.connect( 'activate', self.on_progress_list_stop_now, @@ -4274,7 +4954,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): stop_now_menu_item.set_sensitive(False) stop_soon_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Stop after this _video', + _('Stop after this _video'), ) stop_soon_menu_item.connect( 'activate', @@ -4289,7 +4969,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): stop_soon_menu_item.set_sensitive(False) stop_all_soon_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Stop after these v_ideos', + _('Stop after these v_ideos'), ) stop_all_soon_menu_item.connect( 'activate', @@ -4304,7 +4984,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): # Check/download next/last dl_next_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Download _next', + _('Download _next'), ) dl_next_menu_item.connect( 'activate', @@ -4316,7 +4996,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): dl_next_menu_item.set_sensitive(False) dl_last_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Download _last', + _('Download _last'), ) dl_last_menu_item.connect( 'activate', @@ -4339,7 +5019,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): if utils.is_youtube(media_data_obj.source): watch_youtube_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _YouTube', + _('Watch on _YouTube'), ) watch_youtube_menu_item.connect( 'activate', @@ -4349,7 +5029,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): popup_menu.append(watch_youtube_menu_item) watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _HookTube', + _('Watch on _HookTube'), ) watch_hooktube_menu_item.connect( 'activate', @@ -4359,7 +5039,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): popup_menu.append(watch_hooktube_menu_item) watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _Invidious', + _('Watch on _Invidious'), ) watch_invidious_menu_item.connect( 'activate', @@ -4371,7 +5051,7 @@ def progress_list_popup_menu(self, event, item_id, dbid): else: watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _Website', + _('Watch on _Website'), ) watch_website_menu_item.connect( 'activate', @@ -4389,7 +5069,7 @@ def results_list_popup_menu(self, event, path, dbid): """Called by self.on_results_list_right_click(). - When the user right-clicks on the Results List, show a context- + When the user right-clicks on the Results List, shows a context- sensitive popup menu. Args: @@ -4403,7 +5083,7 @@ def results_list_popup_menu(self, event, path, dbid): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4406 results_list_popup_menu') + utils.debug_time('mwn 5086 results_list_popup_menu') # Find the right-clicked video object, and check it still exists if not dbid in self.app_obj.media_reg_dict: @@ -4423,7 +5103,7 @@ def results_list_popup_menu(self, event, path, dbid): popup_menu.append(Gtk.SeparatorMenuItem()) # Delete video - delete_menu_item = Gtk.MenuItem.new_with_mnemonic('_Delete video') + delete_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Delete video')) delete_menu_item.connect( 'activate', self.on_results_list_delete_video, @@ -4439,6 +5119,70 @@ def results_list_popup_menu(self, event, path, dbid): popup_menu.popup(None, None, None, None, event.button, event.time) + def classic_progress_list_popup_menu(self, event, path, dbid): + + """Called by self.on_classic_progress_list_right_click(). + + When the user right-clicks on the Classic Progress List, shows a + context-sensitive popup menu. + + Args: + + event (Gdk.EventButton): The mouse click event + + path (Gtk.TreePath): Path to the clicked row in the treeview + + dbid (int): The dbid of the clicked video object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 5140 classic_progress_list_popup_menu') + + # Find the right-clicked dummy media.Video object, and check it still + # exists + if not dbid in self.classic_media_dict: + return + else: + dummy_obj = self.classic_media_dict[dbid] + + # Set up the popup menu + popup_menu = Gtk.Menu() + + # Get URL + get_url_menu_item = Gtk.MenuItem.new_with_mnemonic(_('Get _URL')) + get_url_menu_item.connect( + 'activate', + self.on_classic_progress_list_get_url, + dummy_obj, + ) + popup_menu.append(get_url_menu_item) + + # Get command + get_cmd_menu_item = Gtk.MenuItem.new_with_mnemonic(_('Get _command')) + get_cmd_menu_item.connect( + 'activate', + self.on_classic_progress_list_get_cmd, + dummy_obj, + ) + popup_menu.append(get_cmd_menu_item) + + # Open destination + open_destination_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('_Open destination'), + ) + open_destination_menu_item.connect( + 'activate', + self.on_classic_progress_list_open_destination, + dummy_obj, + ) + popup_menu.append(open_destination_menu_item) + + # Create the popup menu + popup_menu.show_all() + popup_menu.popup(None, None, None, None, event.button, event.time) + + def video_index_setup_contents_submenu(self, submenu, media_data_obj, only_child_videos_flag=False): @@ -4463,10 +5207,10 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4466 video_index_setup_contents_submenu') + utils.debug_time('mwn 5210 video_index_setup_contents_submenu') mark_archived_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as _archived', + _('Mark as _archived'), ) mark_archived_menu_item.connect( 'activate', @@ -4477,7 +5221,7 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, submenu.append(mark_archived_menu_item) mark_not_archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as not a_rchived', + _('Mark as not a_rchived'), ) mark_not_archive_menu_item.connect( 'activate', @@ -4491,7 +5235,7 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, submenu.append(Gtk.SeparatorMenuItem()) mark_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as _bookmarked', + _('Mark as _bookmarked'), ) mark_bookmark_menu_item.connect( 'activate', @@ -4503,7 +5247,7 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, mark_bookmark_menu_item.set_sensitive(False) mark_not_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as not b_ookmarked', + _('Mark as not b_ookmarked'), ) mark_not_bookmark_menu_item.connect( 'activate', @@ -4516,7 +5260,7 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, submenu.append(Gtk.SeparatorMenuItem()) mark_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as _favourite', + _('Mark as _favourite'), ) mark_fav_menu_item.connect( 'activate', @@ -4529,7 +5273,7 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, mark_fav_menu_item.set_sensitive(False) mark_not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as not fa_vourite', + _('Mark as not fa_vourite'), ) mark_not_fav_menu_item.connect( 'activate', @@ -4542,7 +5286,7 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, # Separator submenu.append(Gtk.SeparatorMenuItem()) - mark_new_menu_item = Gtk.MenuItem.new_with_mnemonic('Mark as _new') + mark_new_menu_item = Gtk.MenuItem.new_with_mnemonic(_('Mark as _new')) mark_new_menu_item.connect( 'activate', self.on_video_index_mark_new, @@ -4553,7 +5297,9 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, if media_data_obj == self.app_obj.fixed_new_folder: mark_new_menu_item.set_sensitive(False) - mark_old_menu_item = Gtk.MenuItem.new_with_mnemonic('Mark as not n_ew') + mark_old_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('Mark as not n_ew'), + ) mark_old_menu_item.connect( 'activate', self.on_video_index_mark_not_new, @@ -4566,7 +5312,7 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, submenu.append(Gtk.SeparatorMenuItem()) mark_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as in _waiting list', + _('Mark as in _waiting list'), ) mark_playlist_menu_item.connect( 'activate', @@ -4578,7 +5324,7 @@ def video_index_setup_contents_submenu(self, submenu, media_data_obj, mark_playlist_menu_item.set_sensitive(False) mark_not_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as not in wai_ting list', + _('Mark as not in wai_ting list'), ) mark_not_playlist_menu_item.connect( 'activate', @@ -4604,13 +5350,13 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4607 add_watch_video_menu_items') + utils.debug_time('mwn 5353 add_watch_video_menu_items') # Watch video in player/download and watch if not video_obj.dl_flag and not self.app_obj.current_manager_obj: dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Download and _watch', + _('Download and _watch'), ) dl_watch_menu_item.connect( 'activate', @@ -4620,13 +5366,14 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): popup_menu.append(dl_watch_menu_item) if not video_obj.source \ or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj: + or self.app_obj.refresh_manager_obj \ + or video_obj.live_mode != 0: dl_watch_menu_item.set_sensitive(False) else: watch_player_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch in _player', + _('Watch in _player'), ) watch_player_menu_item.connect( 'activate', @@ -4634,14 +5381,17 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): video_obj, ) popup_menu.append(watch_player_menu_item) + if video_obj.live_mode != 0: + watch_player_menu_item.set_sensitive(False) # Watch video online. For YouTube URLs, offer alternative websites - if not video_obj.source: + if not video_obj.source or video_obj.live_mode != 0: watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _website', + _('Watch on _website'), ) - watch_website_menu_item.set_sensitive(False) + if not video_obj.source: + watch_website_menu_item.set_sensitive(False) popup_menu.append(watch_website_menu_item) else: @@ -4649,7 +5399,7 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): if not utils.is_youtube(video_obj.source): watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _website', + _('Watch on _website'), ) watch_website_menu_item.connect( 'activate', @@ -4663,7 +5413,7 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): alt_submenu = Gtk.Menu() watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_YouTube', + _('_YouTube'), ) watch_website_menu_item.connect( 'activate', @@ -4673,7 +5423,7 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): alt_submenu.append(watch_website_menu_item) watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_HookTube', + _('_HookTube'), ) watch_hooktube_menu_item.connect( 'activate', @@ -4683,7 +5433,7 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): alt_submenu.append(watch_hooktube_menu_item) watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Invidious', + _('_Invidious'), ) watch_invidious_menu_item.connect( 'activate', @@ -4692,8 +5442,13 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): ) alt_submenu.append(watch_invidious_menu_item) + translate_note = _( + 'TRANSLATOR\'S NOTE: Watch on YouTube, Watch on' \ + + ' HookTube, etc', + ) + alt_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'W_atch on', + _('W_atch on'), ) alt_menu_item.set_submenu(alt_submenu) popup_menu.append(alt_menu_item) @@ -4701,53 +5456,147 @@ def add_watch_video_menu_items(self, popup_menu, video_obj): # Separator popup_menu.append(Gtk.SeparatorMenuItem()) - # Download to Temporary Videos - temp_submenu = Gtk.Menu() + if video_obj.live_mode != 0: - mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Mark for download') - mark_temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_mark_temp_dl, - video_obj, - ) - temp_submenu.append(mark_temp_dl_menu_item) + # Livestream + livestream_submenu = Gtk.Menu() - # Separator - temp_submenu.append(Gtk.SeparatorMenuItem()) + auto_notify_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( + _('Auto _notify'), + ) + if video_obj.dbid in self.app_obj.media_reg_auto_notify_dict: + auto_notify_menu_item.set_active(True) + auto_notify_menu_item.connect( + 'activate', + self.on_video_catalogue_livestream_toggle, + video_obj, + 'notify', + ) + livestream_submenu.append(auto_notify_menu_item) + # Currently disabled on MS Windows + if os.name == 'nt': + auto_notify_menu_item.set_sensitive(False) - temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic('_Download') - temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_temp_dl, - video_obj, - False, - ) - temp_submenu.append(temp_dl_menu_item) + auto_alarm_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( + _('Auto _sound alarm'), + ) + if video_obj.dbid in self.app_obj.media_reg_auto_alarm_dict: + auto_alarm_menu_item.set_active(True) + auto_alarm_menu_item.connect( + 'activate', + self.on_video_catalogue_livestream_toggle, + video_obj, + 'alarm', + ) + livestream_submenu.append(auto_alarm_menu_item) + if not mainapp.HAVE_PLAYSOUND_FLAG: + auto_alarm_menu_item.set_sensitive(False) - temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Download and _watch', - ) - temp_dl_watch_menu_item.connect( - 'activate', - self.on_video_catalogue_temp_dl, - video_obj, - True, - ) - temp_submenu.append(temp_dl_watch_menu_item) + auto_open_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( + _('Auto _open'), + ) + if video_obj.dbid in self.app_obj.media_reg_auto_open_dict: + auto_open_menu_item.set_active(True) + auto_open_menu_item.connect( + 'activate', + self.on_video_catalogue_livestream_toggle, + video_obj, + 'open', + ) + livestream_submenu.append(auto_open_menu_item) - temp_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Temporary', - ) - temp_menu_item.set_submenu(temp_submenu) - popup_menu.append(temp_menu_item) - if not video_obj.source \ - or self.app_obj.current_manager_obj \ - or ( - isinstance(video_obj.parent_obj, media.Folder) - and video_obj.parent_obj.temp_flag - ): - temp_menu_item.set_sensitive(False) + auto_dl_start_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( + _('_Download on start'), + ) + if video_obj.dbid in self.app_obj.media_reg_auto_dl_start_dict: + auto_dl_start_menu_item.set_active(True) + auto_dl_start_menu_item.connect( + 'activate', + self.on_video_catalogue_livestream_toggle, + video_obj, + 'dl_start', + ) + livestream_submenu.append(auto_dl_start_menu_item) + + auto_dl_stop_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( + _('Download on _stop'), + ) + if video_obj.dbid in self.app_obj.media_reg_auto_dl_stop_dict: + auto_dl_stop_menu_item.set_active(True) + auto_dl_stop_menu_item.connect( + 'activate', + self.on_video_catalogue_livestream_toggle, + video_obj, + 'dl_stop', + ) + livestream_submenu.append(auto_dl_stop_menu_item) + + # Separator + livestream_submenu.append(Gtk.SeparatorMenuItem()) + + not_live_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('Not a _livestream'), + ) + not_live_menu_item.connect( + 'activate', + self.on_video_catalogue_not_livestream, + video_obj, + ) + livestream_submenu.append(not_live_menu_item) + + livestream_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('_Livestream'), + ) + livestream_menu_item.set_submenu(livestream_submenu) + popup_menu.append(livestream_menu_item) + + else: + + # Temporary + temp_submenu = Gtk.Menu() + + mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('_Mark for download')) + mark_temp_dl_menu_item.connect( + 'activate', + self.on_video_catalogue_mark_temp_dl, + video_obj, + ) + temp_submenu.append(mark_temp_dl_menu_item) + + # Separator + temp_submenu.append(Gtk.SeparatorMenuItem()) + + temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download')) + temp_dl_menu_item.connect( + 'activate', + self.on_video_catalogue_temp_dl, + video_obj, + False, + ) + temp_submenu.append(temp_dl_menu_item) + + temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('Download and _watch'), + ) + temp_dl_watch_menu_item.connect( + 'activate', + self.on_video_catalogue_temp_dl, + video_obj, + True, + ) + temp_submenu.append(temp_dl_watch_menu_item) + + temp_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Temporary')) + temp_menu_item.set_submenu(temp_submenu) + popup_menu.append(temp_menu_item) + if not video_obj.source \ + or self.app_obj.current_manager_obj \ + or ( + isinstance(video_obj.parent_obj, media.Folder) + and video_obj.parent_obj.temp_flag + ) or video_obj.live_mode != 0: + temp_menu_item.set_sensitive(False) # (Video Index) @@ -4769,7 +5618,7 @@ def video_index_catalogue_reset(self, reselect_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4772 video_index_catalogue_reset') + utils.debug_time('mwn 5621 video_index_catalogue_reset') video_index_current = self.video_index_current @@ -4796,7 +5645,7 @@ def video_index_reset(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4799 video_index_reset') + utils.debug_time('mwn 5648 video_index_reset') # Reset IVs self.video_index_current = None @@ -4822,9 +5671,8 @@ def video_index_reset(self): self.video_index_scrolled.add(self.video_index_treeview) self.video_index_treeview.set_can_focus(False) self.video_index_treeview.set_headers_visible(False) - # (Tooltips are initially enabled, and disabled by a call to - # self.disable_tooltips() after the config file is loaded, if - # necessary) + # (Tooltips are initially enabled, and if necessary are disabled by a + # call to self.disable_tooltips() shortly afterwards) self.video_index_treeview.set_tooltip_column( self.video_index_tooltip_column, ) @@ -4926,7 +5774,7 @@ def video_index_populate(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4929 video_index_populate') + utils.debug_time('mwn 5777 video_index_populate') for dbid in self.app_obj.media_top_level_list: @@ -4963,7 +5811,7 @@ def video_index_setup_row(self, media_data_obj, parent_pointer=None): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4966 video_index_setup_row') + utils.debug_time('mwn 5814 video_index_setup_row') # Don't show a hidden folder, or any of its children if isinstance(media_data_obj, media.Folder) \ @@ -5026,7 +5874,7 @@ def video_index_add_row(self, media_data_obj, no_select_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5029 video_index_add_row') + utils.debug_time('mwn 5877 video_index_add_row') # Don't add a hidden folder, or any of its children if media_data_obj.is_hidden(): @@ -5129,7 +5977,7 @@ def video_index_delete_row(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5132 video_index_delete_row') + utils.debug_time('mwn 5980 video_index_delete_row') # Videos can't be shown in the Video Index if isinstance(media_data_obj, media.Video): @@ -5182,7 +6030,7 @@ def video_index_select_row(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5182 video_index_select_row') + utils.debug_time('mwn 6033 video_index_select_row') # Cannot select a hidden folder, or any of its children if isinstance(media_data_obj, media.Video) \ @@ -5235,7 +6083,7 @@ def video_index_update_row_icon(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5235 video_index_update_row_icon') + utils.debug_time('mwn 6086 video_index_update_row_icon') # Videos can't be shown in the Video Index if isinstance(media_data_obj, media.Video): @@ -5290,7 +6138,7 @@ def video_index_update_row_text(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5290 video_index_update_row_text') + utils.debug_time('mwn 6141 video_index_update_row_text') # Videos can't be shown in the Video Index if isinstance(media_data_obj, media.Video): @@ -5345,7 +6193,7 @@ def video_index_update_row_tooltip(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5345 video_index_update_row_tooltip') + utils.debug_time('mwn 6196 video_index_update_row_tooltip') # Videos can't be shown in the Video Index if isinstance(media_data_obj, media.Video): @@ -5413,7 +6261,7 @@ def video_index_get_icon(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5413 video_index_get_icon') + utils.debug_time('mwn 6264 video_index_get_icon') icon = None if not self.app_obj.show_small_icons_in_index: @@ -5527,7 +6375,7 @@ def video_index_get_text(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5527 video_index_get_text') + utils.debug_time('mwn 6378 video_index_get_text') text = utils.shorten_string( media_data_obj.nickname, @@ -5542,13 +6390,21 @@ def video_index_get_text(self, media_data_obj): else: + translate_note = _( + 'TRANSLATOR\'S NOTE: V = number of videos B = (number of' \ + + ' videos) bookmarked D = downloaded F = favourite' \ + + ' L = live/livestream N = new W = in waiting list' \ + + ' E = (number of) errors W = warnings', + ) + if media_data_obj.vid_count: - text += '\nV:' + str(media_data_obj.vid_count) \ - + ' B:' + str(media_data_obj.bookmark_count) \ - + ' D:' + str(media_data_obj.dl_count) \ - + ' F:' + str(media_data_obj.fav_count) \ - + ' N:' + str(media_data_obj.new_count) \ - + ' P:' + str(media_data_obj.waiting_count) + text += '\n' + _('V:') + str(media_data_obj.vid_count) \ + + ' ' + _('B:') + str(media_data_obj.bookmark_count) \ + + ' ' + _('D:') + str(media_data_obj.dl_count) \ + + ' ' + _('F:') + str(media_data_obj.fav_count) \ + + ' ' + _('L:') + str(media_data_obj.live_count) \ + + ' ' + _('N:') + str(media_data_obj.new_count) \ + + ' ' + _('W:') + str(media_data_obj.waiting_count) if not isinstance(media_data_obj, media.Folder) \ and (media_data_obj.error_list or media_data_obj.warning_list): @@ -5558,8 +6414,8 @@ def video_index_get_text(self, media_data_obj): else: text += ' ' - text += 'E:' + str(len(media_data_obj.error_list)) \ - + ' W:' + str(len(media_data_obj.warning_list)) + text += _('E:') + str(len(media_data_obj.error_list)) \ + + ' ' + _('W:') + str(len(media_data_obj.warning_list)) return text @@ -5589,7 +6445,7 @@ def video_index_render_text(self, col, renderer, model, tree_iter, data): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5589 video_index_render_text') + utils.debug_time('mwn 6448 video_index_render_text') # Because of Gtk issues, we don't update the Video Index during a # download/refresh/tidy operation if the flag is set @@ -5645,7 +6501,7 @@ def video_catalogue_reset(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5645 video_catalogue_reset') + utils.debug_time('mwn 6504 video_catalogue_reset') # If not called by self.setup_videos_tab()... if self.catalogue_frame.get_child(): @@ -5732,7 +6588,7 @@ def video_catalogue_redraw_all(self, name, page_num=1, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5732 video_catalogue_redraw_all') + utils.debug_time('mwn 6591 video_catalogue_redraw_all') # If actually switching to a different channel/playlist/folder, or a # different page on the same channel/playlist/folder, must reset the @@ -5865,139 +6721,152 @@ def video_catalogue_update_row(self, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5865 video_catalogue_update_row') + utils.debug_time('mwn 6724 video_catalogue_update_row') app_obj = self.app_obj - # Is the video's parent channel, playlist or folder the one that is - # currently selected in the Video Index? If not, the video is not - # displayed in the Video Catalogue - if self.video_index_current is None: - return - - # Special measures during a refresh/tidy operation: don't update or - # create any new rows while the operation is in progress, if Gtk is - # broken + # Special measures during a download/refresh/tidy operation: don't + # update or create any new rows while the operation is in progress, + # if Gtk is broken if ( self.app_obj.gtk_broken_flag or self.app_obj.gtk_emulate_broken_flag ) and ( - self.app_obj.refresh_manager_obj + self.app_obj.download_manager_obj + or self.app_obj.refresh_manager_obj or self.app_obj.tidy_manager_obj ): return - elif self.video_index_current != video_obj.parent_obj.name \ - and self.video_index_current != app_obj.fixed_all_folder.name \ - and ( - self.video_index_current != app_obj.fixed_new_folder.name \ - or video_obj.new_flag - ) and ( - self.video_index_current != app_obj.fixed_bookmark_folder.name \ - or video_obj.bookmark_flag - ) and ( - self.video_index_current != app_obj.fixed_fav_folder.name \ - or video_obj.fav_flag - ) and ( - self.video_index_current != app_obj.fixed_waiting_folder.name \ - or video_obj.waiting_flag + # Is the video's parent channel, playlist or folder the one that is + # currently selected in the Video Index? If not, the video is not + # currently displayed in the Video Catalogue + if self.video_index_current is None \ + or not ( + self.video_index_current == video_obj.parent_obj.name + or self.video_index_current == app_obj.fixed_all_folder.name + or ( + self.video_index_current == app_obj.fixed_new_folder.name + and video_obj.new_flag + ) or ( + self.video_index_current \ + == app_obj.fixed_bookmark_folder.name \ + and video_obj.bookmark_flag + ) or ( + self.video_index_current == app_obj.fixed_fav_folder.name \ + and video_obj.fav_flag + ) or ( + self.video_index_current == app_obj.fixed_live_folder.name \ + and video_obj.live_mode + ) or ( + self.video_index_current == app_obj.fixed_waiting_folder.name \ + and video_obj.waiting_flag + ) ): return # Does a mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem # object already exist for this video? + already_exist_flag = False if video_obj.dbid in self.video_catalogue_dict: + already_exist_flag = True + # Update the catalogue item object, which updates the widgets in # the Gtk.ListBox catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid] catalogue_item_obj.update_widgets() - else: + # Now, deal with the video's position in the catalogue. If a catalogue + # item object already existed, its position may have changed + # (perhaps staying on the current page, perhaps moving to another) + container_dbid = app_obj.media_name_dict[self.video_index_current] + container_obj = app_obj.media_reg_dict[container_dbid] + + # Find the Video Catalogue page on which this video should be shown + page_num = 1 + current_page_num = self.catalogue_toolbar_current_page + page_size = app_obj.catalogue_page_size + # At the same time, reduce the parent container's list of children, + # eliminating those which are media.Channel, media.Playlist and + # media.Folder objects + sibling_video_list = [] + + for child_obj in container_obj.child_list: + if isinstance(child_obj, media.Video): - # Find the video's position in the parent container's list of - # child objects, ignoring any child objects that aren't videos - # At the same time, count the number of child video object so that - # we can update the toolbar widgets - video_count = 0 - page_num = 1 - current_page_num = self.catalogue_toolbar_current_page - page_size = app_obj.catalogue_page_size - - dbid = app_obj.media_name_dict[self.video_index_current] - container_obj = app_obj.media_reg_dict[dbid] - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - video_count += 1 - # If the page size is 0, then all videos are drawn on one - # page - if child_obj == video_obj and page_size: - page_num = int((video_count - 1) / page_size) + 1 - - # If the video should be drawn on the current page, or on any - # previous page, and if the current page is already full, then we - # might need to remove a catalogue item from this page, and - # replace it with another - if page_num <= current_page_num \ - and len(self.video_catalogue_dict) >= page_size: - - # Compile a dictionary of videos which are currently visible on - # this page - visible_dict = {} - for catalogue_item in self.video_catalogue_dict.values(): - visible_dict[catalogue_item.video_obj.dbid] \ - = catalogue_item.video_obj - - # Check the videos which should be visible on this page. This - # code leaves us with 'visible_dict' containing videos that - # should no longer be visible on the page, and 'missing_dict' - # containing videos that should be visible on the page, but - # are not - # Each dictionary should have 0 or 1 entries, but the code will - # cope if it's more than that - missing_dict = {} - for index in range ( - (((current_page_num - 1) * page_size) + 1), - ((current_page_num * page_size) + 1), - ): - if index <= video_count: - child_obj = container_obj.child_list[index] - if not child_obj.dbid in visible_dict: - missing_dict[child_obj.dbid] = child_obj - else: - del visible_dict[child_obj.dbid] - - # Remove any catalogue items for videos that shouldn't be - # visible, but are - for dbid in visible_dict: - catalogue_item_obj = self.video_catalogue_dict[dbid] - self.catalogue_listbox.remove( - catalogue_item_obj.catalogue_row, - ) - - del self.video_catalogue_dict[dbid] - - # Add any new catalogue items for videos which should be - # visible, but aren't - for dbid in missing_dict: + sibling_video_list.append(child_obj) + + # (If the page size is 0, then all videos are drawn on one + # page, i.e. the current value of page_num, which is 1) + if child_obj == video_obj and page_size: + page_num = int( + (len(sibling_video_list) - 1) / page_size + ) + 1 + + sibling_video_count = len(sibling_video_list) + + # Decide whether to move any catalogue items from this page and, if so, + # what (if anything) should be moved into their place + # If a catalogue item was already visible for this video, then the + # video might need to be displayed on a different page, its position + # on this page being replaced by a different video + # If a catalogue item was not already visible for this video, and if + # it should be drawn on this page or any previous page, then we + # need to remove a catalogue item from this page and replace it with + # another + if (already_exist_flag and page_num != current_page_num) \ + or (not already_exist_flag and page_num <= current_page_num): + + # Compile a dictionary of videos which are currently visible on + # this page + visible_dict = {} + for catalogue_item in self.video_catalogue_dict.values(): + visible_dict[catalogue_item.video_obj.dbid] \ + = catalogue_item.video_obj + + # Check the videos which should be visible on this page. This + # code block leaves us with 'visible_dict' containing videos + # that should no longer be visible on the page, and + # 'missing_dict' containing videos that should be visible on + # the page, but are not + missing_dict = {} + for index in range ( + ((current_page_num - 1) * page_size), + (current_page_num * page_size), + ): + if index < sibling_video_count: + child_obj = sibling_video_list[index] + if not child_obj.dbid in visible_dict: + missing_dict[child_obj.dbid] = child_obj + else: + del visible_dict[child_obj.dbid] + + # Remove any catalogue items for videos that shouldn't be + # visible, but still are + for dbid in visible_dict: + catalogue_item_obj = self.video_catalogue_dict[dbid] + self.catalogue_listbox.remove( + catalogue_item_obj.catalogue_row, + ) - # Get the media.Video object - missing_obj = app_obj.media_reg_dict[dbid] + del self.video_catalogue_dict[dbid] - # Create a new catalogue item - self.video_catalogue_insert_item(missing_obj) + # Add any new catalogue items for videos which should be + # visible, but aren't + for dbid in missing_dict: - else: + # Get the media.Video object + missing_obj = app_obj.media_reg_dict[dbid] - # Page is not full, so just create a new catalogue item - self.video_catalogue_insert_item(video_obj) + # Create a new catalogue item + self.video_catalogue_insert_item(missing_obj) - # Update widgets in the toolbar - self.video_catalogue_toolbar_update( - self.catalogue_toolbar_current_page, - video_count, - ) + # Update widgets in the toolbar + self.video_catalogue_toolbar_update( + self.catalogue_toolbar_current_page, + sibling_video_count, + ) # Force the Gtk.ListBox to sort its rows, so that videos are displayed # in the correct order @@ -6033,7 +6902,7 @@ def video_catalogue_insert_item(self, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6033 video_catalogue_insert_item') + utils.debug_time('mwn 6905 video_catalogue_insert_item') # Create the new catalogue item if self.app_obj.catalogue_mode == 'simple_hide_parent' \ @@ -6084,7 +6953,7 @@ def video_catalogue_retry_insert_items(self): """ if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('mwn 6084 video_catalogue_retry_insert_items') + utils.debug_time('mwn 6956 video_catalogue_retry_insert_items') if self.video_catalogue_temp_list: @@ -6133,7 +7002,7 @@ def video_catalogue_delete_row(self, video_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6133 video_catalogue_delete_row') + utils.debug_time('mwn 7005 video_catalogue_delete_row') # Is the video's parent channel, playlist or folder the one that is # currently selected in the Video Index? If not, the video is not @@ -6154,6 +7023,9 @@ def video_catalogue_delete_row(self, video_obj): ) and ( self.video_index_current != app_obj.fixed_fav_folder.name \ or video_obj.fav_flag + ) and ( + self.video_index_current != app_obj.fixed_live_folder.name \ + or video_obj.live_mode ) and ( self.video_index_current != app_obj.fixed_waiting_folder.name \ or video_obj.waiting_flag @@ -6243,7 +7115,7 @@ def video_catalogue_toolbar_reset(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6243 video_catalogue_toolbar_reset') + utils.debug_time('mwn 7118 video_catalogue_toolbar_reset') self.catalogue_toolbar_current_page = 1 self.catalogue_toolbar_last_page = 1 @@ -6293,7 +7165,7 @@ def video_catalogue_toolbar_update(self, page_num, video_count): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6293 video_catalogue_toolbar_update') + utils.debug_time('mwn 7168 video_catalogue_toolbar_update') self.catalogue_toolbar_current_page = page_num @@ -6359,7 +7231,7 @@ def video_catalogue_apply_filter(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6359 video_catalogue_apply_filter') + utils.debug_time('mwn 7234 video_catalogue_apply_filter') # Sanity check - something must be selected in the Video Index, and it # must not be a media.Video object @@ -6435,7 +7307,7 @@ def video_catalogue_cancel_filter(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6435 video_catalogue_cancel_filter') + utils.debug_time('mwn 7310 video_catalogue_cancel_filter') # Reset IVs... self.video_catalogue_filtered_flag = False @@ -6462,7 +7334,7 @@ def progress_list_reset(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6462 progress_list_reset') + utils.debug_time('mwn 7337 progress_list_reset') # Reset widgets self.progress_list_liststore = Gtk.ListStore( @@ -6503,7 +7375,7 @@ def progress_list_init(self, download_list_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6503 progress_list_init') + utils.debug_time('mwn 7378 progress_list_init') # For each download item object, add a row to the treeview, and store # the download item's .dbid IV so that @@ -6536,7 +7408,7 @@ def progress_list_add_row(self, item_id, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6536 progress_list_add_row') + utils.debug_time('mwn 7411 progress_list_add_row') # Prepare the icon if isinstance(media_data_obj, media.Channel): @@ -6569,7 +7441,7 @@ def progress_list_add_row(self, item_id, media_data_obj): ), ) row_list.append(None) - row_list.append('Waiting') + row_list.append(_('Waiting')) row_list.append(None) row_list.append(None) row_list.append(None) @@ -6602,7 +7474,7 @@ def progress_list_receive_dl_stats(self, download_item_obj, dl_stat_dict, Thus, when this function is called, it is passed a dictionary of download statistics in a standard format (the one described in the - comments to media.VideoDownloader.extract_stdout_data() ). + comments to downloads.VideoDownloader.extract_stdout_data() ). We store that dictionary temporarily. During periodic calls to self.progress_list_display_dl_stats(), the contents of any stored @@ -6625,7 +7497,7 @@ def progress_list_receive_dl_stats(self, download_item_obj, dl_stat_dict, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6625 progress_list_receive_dl_stats') + utils.debug_time('mwn 7500 progress_list_receive_dl_stats') # Check that the Progress List actually has a row for the specified # downloads.DownloadItem object @@ -6667,7 +7539,7 @@ def progress_list_display_dl_stats(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6667 progress_list_display_dl_stats') + utils.debug_time('mwn 7542 progress_list_display_dl_stats') # Import the contents of the IV (in case it gets updated during the # call to this function), and use the imported copy @@ -6679,7 +7551,7 @@ def progress_list_display_dl_stats(self): # Get a dictionary of download statistics for this media object # The dictionary is in the standard format described in the - # comments to media.VideoDownloader.extract_stdout_data() + # comments to downloads.VideoDownloader.extract_stdout_data() dl_stat_dict = temp_dict[item_id] # Get the corresponding treeview row @@ -6757,7 +7629,7 @@ def progress_list_check_hide_rows(self, force_flag=False): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6757 progress_list_check_hide_rows') + utils.debug_time('mwn 7632 progress_list_check_hide_rows') current_time = time.time() hide_list = [] @@ -6788,7 +7660,7 @@ def progress_list_do_hide_row(self, item_id): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6788 progress_list_do_hide_row') + utils.debug_time('mwn 7663 progress_list_do_hide_row') row_num = self.progress_list_row_dict[item_id] @@ -6818,6 +7690,51 @@ def progress_list_do_hide_row(self, item_id): del self.progress_list_finish_dict[item_id] + def progress_list_update_video_name(self, download_item_obj, video_obj): + + """Called by self.results_list_add_row(). + + In the Progress List, an individual video (one inside a media.Folder) + will be visible using the system's default video name, rather than the + video's actual name. The final call to + self.progress_list_display_dl_stats() cannot set the actual name, as it + might not be available yet. + + The Results List is updated some time after the last call to the + Progress List. If the video has a non-default name, then display it in + the Progress List now. + + Args: + + download_item_obj (downloads.DownloadItem): The download item + object handling a download for a media data object + + video_obj (media.Video): The media data object for the downloaded + video + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 7718 progress_list_update_video_name') + + if download_item_obj.item_id in self.progress_list_row_dict \ + and download_item_obj.media_data_obj == video_obj: + + # Get the Progress List treeview row + tree_path = Gtk.TreePath( + self.progress_list_row_dict[download_item_obj.item_id], + ) + + 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, + ), + ) + + # (Results List) @@ -6830,7 +7747,7 @@ def results_list_reset(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6830 results_list_reset') + utils.debug_time('mwn 7750 results_list_reset') # Reset widgets self.results_list_liststore = Gtk.ListStore( @@ -6883,10 +7800,14 @@ def results_list_add_row(self, download_item_obj, video_obj, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6883 results_list_add_row') + utils.debug_time('mwn 7803 results_list_add_row') # Prepare the icons - if self.app_obj.download_manager_obj.operation_type == 'sim' \ + if video_obj.live_mode == 1: + pixbuf = self.pixbuf_dict['stream_wait_small'] + elif video_obj.live_mode == 2: + pixbuf = self.pixbuf_dict['stream_live_small'] + elif self.app_obj.download_manager_obj.operation_type == 'sim' \ or download_item_obj.media_data_obj.dl_sim_flag: pixbuf = self.pixbuf_dict['check_small'] else: @@ -6987,6 +7908,12 @@ def results_list_add_row(self, download_item_obj, video_obj, \ # next call to this function) self.results_list_row_count += 1 + # Special measures for individual videos. The video name may not have + # been known when the Progress List was updated for the last time + # (but is known now). Update the name displayed in the Progress List, + # just to be sure + self.progress_list_update_video_name(download_item_obj, video_obj) + def results_list_update_row(self): @@ -7004,7 +7931,7 @@ def results_list_update_row(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7004 results_list_update_row') + utils.debug_time('mwn 7934 results_list_update_row') new_temp_list = [] @@ -7048,6 +7975,8 @@ def results_list_update_row(self): self.app_obj.fixed_bookmark_folder.sort_children() if video_obj.fav_flag: self.app_obj.fixed_fav_folder.sort_children() + if video_obj.live_mode: + self.app_obj.fixed_live_folder.sort_children() if video_obj.new_flag: self.app_obj.fixed_new_folder.sort_children() if video_obj.waiting_flag: @@ -7130,1084 +8059,1225 @@ def results_list_update_row(self): self.results_list_temp_list = new_temp_list - # (Output tab) + # (Classic Mode tab) - def output_tab_setup_pages(self): + def classic_mode_tab_add_dest_dir(self): - """Called by mainapp.TartubeApp.start() and .set_num_worker_default(). + """Called by mainapp.TartubeApp.on_button_classic_dest_dir(). - Makes sure there are enough pages in the Output Tab's notebook for - each simultaneous download allowed (a value specified by - mainapp.TartubeApp.num_worker_default). + A new destination directory has been added, so add it to the combobox + in the Classic Mode Tab. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7143 output_tab_setup_pages') - - # The first page in the Output Tab's notebook shows a summary of what - # the threads created by downloads.py are doing - if not self.output_tab_summary_flag \ - and self.app_obj.ytdl_output_show_summary_flag: - self.output_tab_add_page(True) - self.output_tab_summary_flag = True + utils.debug_time('mwn 8074 classic_mode_tab_add_dest_dir') - # The number of pages in the notebook (not including the summary page) - # should match the highest value of - # mainapp.TartubeApp.num_worker_default during this session (i.e. if - # the user reduces its value, we don't remove pages; but we do add - # pages if the user increases its value) - if self.output_page_count < self.app_obj.num_worker_default: + # Reset the contents of the combobox + self.classic_dest_dir_liststore = Gtk.ListStore(str) + for string in self.app_obj.classic_dir_list: + self.classic_dest_dir_liststore.append( [string] ) - for num in range(1, (self.app_obj.num_worker_default + 1)): - if not num in self.output_textview_dict: - self.output_tab_add_page() + self.classic_dest_dir_combo.set_model(self.classic_dest_dir_liststore) + self.classic_dest_dir_combo.set_active(0) + self.show_all() - def output_tab_add_page(self, summary_flag=False): + def classic_mode_tab_add_row(self, dummy_obj): - """Called by self.output_tab_setup_pages(). + """Called by self.classic_mode_tab_add_urls(). - Adds a new page to the Output Tab's notebook, and updates IVs. + Adds a row to the Classic Progress List. Args: - summary_flag (bool): If True, add the (first) summary page to the - notebook, showing what the threads are doing + dummy_obj (media.Video): The dummy media.Video object handling the + download of a single URL (which might represent a video, + channel or playlist) """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7178 output_tab_add_page') - - # Each page (except the summary page) corresponds to a single - # downloads.DownloadWorker object. The page number matches the - # worker's .worker_id. The first worker is numbered #1 - if not summary_flag: - self.output_page_count += 1 - - # Add the new page - tab = Gtk.Box() + utils.debug_time('mwn 8101 classic_mode_tab_add_row') - if not summary_flag: - label = Gtk.Label.new_with_mnemonic( - 'Thread #_' + str(self.output_page_count), - ) - else: - label = Gtk.Label.new_with_mnemonic('_Summary') + # Prepare the new row in the treeview + row_list = [] - self.output_notebook.append_page(tab, label) - tab.set_hexpand(True) - tab.set_vexpand(True) - tab.set_border_width(self.spacing_size) + row_list.append(dummy_obj.dbid) # Hidden + row_list.append( # Hidden + html.escape( + dummy_obj.fetch_tooltip_text( + self.app_obj, + self.tooltip_max_len, + ), + ), + ) + row_list.append( + utils.shorten_string( + dummy_obj.source, + self.medium_string_max_len, + ), + ) + row_list.append(None) + row_list.append(_('Waiting')) + row_list.append(None) + row_list.append(None) + row_list.append(None) + row_list.append(None) + row_list.append(None) + row_list.append(None) - # Add a textview to the tab, using a css style sheet to provide - # monospaced white text on a black background - scrolled = Gtk.ScrolledWindow() - tab.pack_start(scrolled, True, True, 0) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + # Create a new row in the treeview. Doing the .show_all() first + # prevents a Gtk error (for unknown reasons) + self.classic_progress_treeview.show_all() + self.classic_progress_liststore.append(row_list) - frame = Gtk.Frame() - scrolled.add_with_viewport(frame) - style_provider = self.output_tab_set_textview_css( - '#css_text_id_' + str(self.output_page_count) \ - + ', textview text {\n' \ - + ' background-color: ' + self.output_tab_bg_colour + ';\n' \ - + ' color: ' + self.output_tab_text_colour + ';\n' \ - + '}\n' \ - + '#css_label_id_' + str(self.output_page_count) \ - + ', textview {\n' \ - + ' font-family: monospace, monospace;\n' \ - + ' font-size: 10pt;\n' \ - + '}' - ) + def classic_mode_tab_move_row(self, up_flag): - textview = Gtk.TextView() - frame.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_editable(False) - textview.set_cursor_visible(False) + """Called by mainapp.TartubeApp.on_button_classic_move_up() and + .on_button_classic_move_down(). - context = textview.get_style_context() - context.add_provider(style_provider, 600) + Moves the selected row(s) up/down in the Classic Progress List. - # Reset css properties for the next Gtk.TextView created (for example, - # by AddVideoDialogue) so it uses default values, rather than the - # white text on black background used above - # To do that, create a dummy textview, and apply a css style to it - textview2 = Gtk.TextView() - style_provider2 = self.output_tab_set_textview_css( - '#css_text_id_default, textview text {\n' \ - + ' background-color: unset;\n' \ - + ' color: unset;\n' \ - + '}\n' \ - + '#css_label_id_default, textview {\n' \ - + ' font-family: unset;\n' \ - + ' font-size: unset;\n' \ - + '}' - ) + Args: - context = textview2.get_style_context() - context.add_provider(style_provider2, 600) + up_flag (bool): True to move up, False to move down - # Set up auto-scrolling - textview.connect( - 'size-allocate', - self.output_tab_do_autoscroll, - scrolled, - ) + """ - # Make the page visible - self.show_all() + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 8150 classic_mode_tab_move_row') - # Update IVs - if not summary_flag: - self.output_textview_dict[self.output_page_count] = textview - else: - self.output_textview_dict[0] = textview + selection = self.classic_progress_treeview.get_selection() + (model, path_list) = selection.get_selected_rows() + if not path_list: + # Nothing selected + return - def output_tab_set_textview_css(self, css_string): + # Move each selected row up (or down) + if up_flag: - """Called by self.output_tab_add_page(). + # Move up + for path in path_list: - Applies a CSS style to the current screen. Called once to create a - white-on-black Gtk.TextView, then a second time to create a dummy - textview with default properties. + this_iter = model.get_iter(path) + if model.iter_previous(this_iter): - Args: + self.classic_progress_liststore.move_before( + this_iter, + model.iter_previous(this_iter), + ) - css_string (str): The CSS style to apply + else: - Returns: + # If the first item won't move up, then successive items + # will be moved above this one (which is not what we + # want) + return - The Gtk.CssProvider created + else: - """ + # Move down + path_list.reverse() - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7287 output_tab_set_textview_css') + for path in path_list: - style_provider = Gtk.CssProvider() - style_provider.load_from_data(bytes(css_string.encode())) - Gtk.StyleContext.add_provider_for_screen( - Gdk.Screen.get_default(), - style_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ) + this_iter = model.get_iter(path) + if model.iter_next(this_iter): - return style_provider + self.classic_progress_liststore.move_after( + this_iter, + model.iter_next(this_iter), + ) + else: - def output_tab_write_stdout(self, page_num, msg): + return - """Called by various functions in downloads.py, info.py, refresh.py, - tidy.py and updates.py. - During a download operation, youtube-dl sends output to STDOUT. If - permitted, this output is displayed in the Output Tab. However, it - can't be displayed immediately, because Gtk widgets can't be updated - from within a thread. + def classic_mode_tab_remove_rows(self, dbid_list): - Instead, add the received values to a list, and wait for the GObject - timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update(). + """Called by mainapp.TartubeApp.on_button_classic_remove(). - Other operations also call this function to display text in the - default colour. + Removes the selected rows from the Classic Progress List and updates + IVs. Args: - page_num (int): The page number on which this message should be - displayed. Matches a key in self.output_textview_dict - - msg (str): The message to display. A newline character will be - added by self.output_tab_update_pages(). + dbid_list (list): The .dbids for the dummy media.Video object + corresponding to each selected row """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7327 output_tab_write_stdout') + utils.debug_time('mwn 8215 classic_mode_tab_remove_rows') - self.output_tab_insert_list.extend( [page_num, msg, 'default'] ) + # (Import IVs for convenience) + manager_obj = self.app_obj.download_manager_obj + # Check each row in turn + for dbid in dbid_list: - def output_tab_write_stderr(self, page_num, msg): + # If there is a current download operation, we need to update it + if manager_obj: - """Called by various functions in downloads.py and info.py. + # If this dummy media.Video object is the one being downloaded, + # halt the download + for worker_obj in manager_obj.worker_list: - During a download operation, youtube-dl sends output to STDERR. If - permitted, this output is displayed in the Output Tab. However, it - can't be displayed immediately, because Gtk widgets can't be updated - from within a thread. + if worker_obj.running_flag \ + and worker_obj.download_item_obj \ + and worker_obj.download_item_obj.media_data_obj.dbid \ + == dbid: + worker_obj.video_downloader_obj.stop() - Instead, add the received values to a list, and wait for the GObject - timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update(). + # Delete the dummy media.Video object + del self.classic_media_dict[dbid] - Other operations also call this function to display text in the - non-default colour. + # Remove the row from the treeview + row_iter = self.classic_mode_tab_find_row_iter(dbid) + if row_iter: + self.classic_progress_liststore.remove(row_iter) - Args: - page_num (int): The page number on which this message should be - displayed. Matches a key in self.output_textview_dict + def classic_mode_tab_add_urls(self): - msg (str): The message to display. A newline character will be - added by self.output_tab_update_pages(). + """Called by mainapp.TartubeApp.on_button_classic_add_urls(). + In the Classic Mode Tab, transfers URLs from the textview into the + Classic Progress List (a treeview), creating a new dummy media.Video + object for each URL, and updating IVs. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7358 output_tab_write_stderr') + utils.debug_time('mwn 8255 classic_mode_tab_add_urls') - self.output_tab_insert_list.extend( [page_num, msg, 'error_warning'] ) + # Get the specified download destination + tree_iter = self.classic_dest_dir_combo.get_active_iter() + model = self.classic_dest_dir_combo.get_model() + dest_dir = model[tree_iter][0] + # Get the specified video/audio format, leaving the value as None if + # a default format is selected + tree_iter = self.classic_format_combo.get_active_iter() + model = self.classic_format_combo.get_model() + format_str = model[tree_iter][0] + # (Valid formats begin with whitespace) + if not re.search('^\s', format_str): + format_str = None + else: + format_str = re.sub('^\s*', '', format_str) + # (One last check for a valid video/audio format) + if not format_str in formats.VIDEO_FORMAT_LIST \ + and not format_str in formats.AUDIO_FORMAT_LIST: + format_str = None + + # Extract a list of URLs from the treeview + url_string = self.classic_textbuffer.get_text( + self.classic_textbuffer.get_start_iter(), + self.classic_textbuffer.get_end_iter(), + False, + ) - def output_tab_write_system_cmd(self, page_num, msg): + url_list = url_string.splitlines() - """Called by various functions in downloads.py, info.py and updates.py. + # Remove initial/final whitespace, and ignore invalid/duplicate links + mod_list = [] + invalid_url_string = '' + for url in url_list: - During a download operation, youtube-dl system commands are displayed - in the Output Tab (if permitted). However, they can't be displayed - immediately, because Gtk widgets can't be updated from within a thread. + # Strip whitespace + mod_url = utils.strip_whitespace(url) - Instead, add the received values to a list, and wait for the GObject - timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update(). + # Check for duplicates + invalid_flag = False + for other_obj in self.classic_media_dict.values(): + if other_obj.source == url: + invalid_flag = True + break - Other operations also call this function to display text in the - non-default colour. + if not invalid_flag and not utils.check_url(mod_url): + invalid_flag = True - Args: + if not invalid_flag: + mod_list.append(mod_url) + else: + # Invalid links can stay in the textview. Hopefully it's + # obvious to the user why an invalid link hasn't been added + if not invalid_url_string: + invalid_url_string = mod_url + else: + invalid_url_string += '\n' + mod_url - page_num (int): The page number on which this message should be - displayed. Matches a key in self.output_textview_dict + # For each valid link, create a dummy media.Video object. The dummy + # objects have negative .dbids, and are not added to the media data + # registry + for url in mod_list: - msg (str): The message to display. A newline character will be - added by self.output_tab_update_pages(). + self.classic_media_total += 1 - """ + new_obj = media.Video( + (self.classic_media_total) * -1, # Negative .dbid + 'Dummy video', + ) - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7388 output_tab_write_system_cmd') + new_obj.set_dummy(url, dest_dir, format_str) - self.output_tab_insert_list.extend( [page_num, msg, 'system_cmd'] ) + # Add a line to the treeview + self.classic_mode_tab_add_row(new_obj) + # Update IVs + self.classic_media_dict[new_obj.dbid] = new_obj - def output_tab_update_pages(self): + # If a download operation, generated by the Classic Mode Tab, is + # in progress, then we can add this URL directly to the + # downloads.DownloadList object + manager_obj = self.app_obj.download_manager_obj - """Can be called by anything. + if manager_obj \ + and manager_obj.operation_type == 'classic' \ + and manager_obj.running_flag \ + and manager_obj.download_list_obj: + manager_obj.download_list_obj.create_dummy_item(new_obj) - During a download operation, youtube-dl sends output to STDOUT/STDERR. - If permitted, this output is displayed in the Output Tab, along with - any system commands. + # Any invalid links remain in the textview (but all valid links are + # removed from it) + self.classic_textbuffer.set_text(invalid_url_string) - However, the text can't be displayed immediately, because Gtk widgets - can't be updated from within a thread. - Instead, the text has been added to self.output_tab_insert_list, and - can now be displayed (and the list can be emptied). + def classic_mode_tab_find_row_iter(self, dbid): - Other operations also call this function to display text added to - self.output_tab_insert_list. + """Called by self.classic_mode_tab_remove_rows() and + .classic_mode_tab_display_dl_stats(). + + Finds the GtkTreeIter for the Classic Progress List row displaying the + specified data for the dummy media.Video object. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7412 output_tab_update_pages') + utils.debug_time('mwn 8360 classic_mode_tab_add_urls') - update_dict = {} + for row in self.classic_progress_liststore: + if self.classic_progress_liststore[row.iter][0] == dbid: + return row.iter - if self.output_tab_insert_list: - while self.output_tab_insert_list: + def classic_mode_tab_receive_dl_stats(self, download_item_obj, + dl_stat_dict, finish_flag=False): - page_num = self.output_tab_insert_list.pop(0) - msg = self.output_tab_insert_list.pop(0) - msg_type = self.output_tab_insert_list.pop(0) + """Called by downloads.DownloadWorker.data_callback(). - # Add the output to the textview. STDERR messages and system - # commands are displayed in a different colour - # (The summary page is not necessarily visible) - if page_num in self.output_textview_dict: + A modified form of self.progress_list_receive_dl_stats(), used during + a download operation launched from the Classic Mode Tab. - textview = self.output_textview_dict[page_num] - textbuffer = textview.get_buffer() - update_dict[page_num] = textview + Stores download statistics until they can be displayed (as in the + original function) - if msg_type != 'default': + Args: - # The .markup_escape_text() call won't escape curly - # braces, so we need to replace those manually - msg = re.sub('{', '(', msg) - msg = re.sub('}', ')', msg) + download_item_obj (downloads.DownloadItem): The download item + object handling a download for a dummy media.Video object - string = '' \ - + GObject.markup_escape_text(msg) + '\n' + dl_stat_dict (dict): The dictionary of download statistics + described in the original function - if msg_type == 'system_cmd': + finish_flag (bool): True if the worker has finished with its + dummy media.Video object, meaning that dl_stat_dict is the + final set of statistics, and that the progress list row can be + hidden, if required - textbuffer.insert_markup( - textbuffer.get_end_iter(), - string.format( - self.output_tab_system_cmd_colour, - ), - -1, - ) + """ - else: + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 8394 classic_mode_tab_receive_dl_stats') - # STDERR - textbuffer.insert_markup( - textbuffer.get_end_iter(), - string.format(self.output_tab_stderr_colour), - -1, - ) + # Temporarily store the dictionary of download statistics + if not download_item_obj.item_id in self.classic_temp_dict: + new_dl_stat_dict = {} + else: + new_dl_stat_dict \ + = self.classic_temp_dict[download_item_obj.item_id] - else: + for key in dl_stat_dict: + new_dl_stat_dict[key] = dl_stat_dict[key] - # STDOUT - textbuffer.insert( - textbuffer.get_end_iter(), - msg + '\n', - ) + self.classic_temp_dict[download_item_obj.item_id] \ + = new_dl_stat_dict - # Make the new output visible - for textview in update_dict.values(): - textview.show_all() + def classic_mode_tab_display_dl_stats(self): - def output_tab_do_autoscroll(self, textview, rect, scrolled): + """Called by downloads.DownloadManager.run() and + mainapp.TartubeApp.dl_timer_callback(). - """Called from a callback in self.output_tab_add_page(). + A modified form of self.progress_list_display_dl_stats(), used during + a download operation launched from the Classic Mode Tab. + """ - When one of the textviews in the Output Tab is modified (text added or - removed), make sure the page is scrolled to the bottom. + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 8420 classic_mode_tab_display_dl_stats') - Args: + # Import the contents of the IV (in case it gets updated during the + # call to this function), and use the imported copy + temp_dict = self.classic_temp_dict + self.classic_temp_dict = {} - textview (Gtk.TextView): The textview to scroll + # For each dummy media.Video object displayed in the download list... + for dbid in temp_dict: - rect (Gdk.Rectangle): Ignored + # Get a dictionary of download statistics for this dummy + # media.Video object + # The dictionary is in the standard format described in the + # comments to downloads.VideoDownloader.extract_stdout_data() + dl_stat_dict = temp_dict[dbid] - scrolled (Gtk.ScrolledWindow): The scroller which contains the - textview + # Get the dummy media.Video object itself + if not dbid in self.classic_media_dict: + # Row has already been deleted by the user + continue + else: + media_data_obj = self.classic_media_dict[dbid] - """ + # Get the corresponding treeview row + row_iter = self.classic_mode_tab_find_row_iter(dbid) + if not row_iter: + # Row has already been deleted by the user + continue + else: + row_path = self.classic_progress_liststore.get_path(row_iter) - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7494 output_tab_do_autoscroll') + # Update statistics displayed in that row + # (Columns 0-2 are not modified, once the row has been added to the + # treeview) + column = 2 - adj = scrolled.get_vadjustment() - adj.set_value(adj.get_upper() - adj.get_page_size()) + for key in ( + 'playlist_index', + 'status', + 'filename', + 'extension', + 'percent', + 'speed', + 'eta', + 'filesize', + ): + column += 1 + if key in dl_stat_dict: - def output_tab_scroll_visible_page(self, page_num): + if key == 'playlist_index': - """Called by self.on_output_notebook_switch_page() and - .on_notebook_switch_page(). + if 'dl_sim_flag' in dl_stat_dict \ + and dl_stat_dict['dl_sim_flag']: + # (Don't know how many videos there are in a + # channel/playlist, so ignore value of + # 'playlist_size') + string = str(dl_stat_dict['playlist_index']) - When the user switches between pages in the Output Tab, scroll the - visible textview to the bottom (otherwise it gets confusing). + else: + string = str(dl_stat_dict['playlist_index']) + if 'playlist_size' in dl_stat_dict: + string = string + '/' \ + + str(dl_stat_dict['playlist_size']) + else: + string = string + '/1' - Args: + else: + string = utils.shorten_string( + dl_stat_dict[key], + self.medium_string_max_len, + ) + + self.classic_progress_liststore.set( + self.classic_progress_liststore.get_iter(row_path), + column, + string, + ) - page_num (int): The page to be scrolled, matching a key in - self.output_textview_dict + def classic_mode_tab_timer_callback(self): + + """Called from a callback in self.classic_mode_tab_toggle_auto_copy(). + + Periodically checks the system's clipboard, and adds any new URLs to + the Classic Progress List. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7516 output_tab_scroll_visible_page') - - if page_num in self.output_textview_dict: - textview = self.output_textview_dict[page_num] + utils.debug_time('mwn 8509 classic_mode_tab_timer_callback') - frame = textview.get_parent() - viewport = frame.get_parent() - scrolled = viewport.get_parent() + utils.add_links_to_textview_from_clipboard( + self.app_obj, + self.classic_textbuffer, + self.classic_mark_start, + self.classic_mark_end, + ) - adj = scrolled.get_vadjustment() - adj.set_value(adj.get_upper() - adj.get_page_size()) + # Return 1 to keep the timer going + return 1 - def output_tab_reset_pages(self): + def classic_mode_tab_toggle_auto_copy(self): - """Called by mainapp.TartubeApp.download_manager_continue(), - .update_manager_start(), .refresh_manager_continue(), - .info_manager_start() and .tidy_manager_start(). + """Called by mainapp.TartubeApp.on_button_classic_auto_copy(). - At the start of a download/update/refresh/info/tidy operation, empty - the pages in the Output Tab (if allowed). + Toggles the auto copy/paste button in the Classic Mode tab. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7540 output_tab_reset_pages') + utils.debug_time('mwn 8530 classic_mode_tab_toggle_auto_copy') - for textview in self.output_textview_dict.values(): - textbuffer = textview.get_buffer() - textbuffer.set_text('') - textview.show_all() + if not self.classic_auto_copy_flag: + # Update IVs + self.classic_auto_copy_flag = True - # (Errors Tab) + # Update the button itself + self.classic_auto_copy_button.set_image( + Gtk.Image.new_from_stock( + Gtk.STOCK_PASTE, + Gtk.IconSize.BUTTON, + ), + ) + self.classic_auto_copy_button.set_tooltip_text( + _('Disable automatic copy/paste'), + ) - def errors_list_reset(self): + # Start a timer to periodically check the clipboard + self.classic_clipboard_timer_id = GObject.timeout_add( + self.classic_clipboard_timer_time, + self.classic_mode_tab_timer_callback, + ) - """Can be called by anything. + else: - Empties the Gtk.TreeView in the Errors List, ready for it to be - refilled. (There are no IVs to reset.) + # Update IVs + self.classic_auto_copy_flag = False + + # Update the button itself + self.classic_auto_copy_button.set_image( + Gtk.Image.new_from_stock( + Gtk.STOCK_COPY, + Gtk.IconSize.BUTTON, + ), + ) + + self.classic_auto_copy_button.set_tooltip_text( + _('Enable automatic copy/paste'), + ) + + # Stop the timer + GObject.source_remove(self.classic_clipboard_timer_id) + self.classic_clipboard_timer_id = None + + + # (Output tab) + + + def output_tab_setup_pages(self): + + """Called by mainapp.TartubeApp.start() and .set_num_worker_default(). + + Makes sure there are enough pages in the Output Tab's notebook for + each simultaneous download allowed (a value specified by + mainapp.TartubeApp.num_worker_default). """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7560 errors_list_reset') + utils.debug_time('mwn 8590 output_tab_setup_pages') - # Reset widgets - self.errors_list_liststore = Gtk.ListStore( - GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, - str, str, str, - ) - self.errors_list_treeview.set_model(self.errors_list_liststore) + # The first page in the Output Tab's notebook shows a summary of what + # the threads created by downloads.py are doing + if not self.output_tab_summary_flag \ + and self.app_obj.ytdl_output_show_summary_flag: + self.output_tab_add_page(True) + self.output_tab_summary_flag = True - self.tab_error_count = 0 - self.tab_warning_count = 0 - self.errors_list_refresh_label() + # The number of pages in the notebook (not including the summary page) + # should match the highest value of + # mainapp.TartubeApp.num_worker_default during this session (i.e. if + # the user reduces its value, we don't remove pages; but we do add + # pages if the user increases its value) + if self.output_page_count < self.app_obj.num_worker_default: + for num in range(1, (self.app_obj.num_worker_default + 1)): + if not num in self.output_textview_dict: + self.output_tab_add_page() - def errors_list_add_row(self, media_data_obj): - """Called by downloads.DownloadWorker.run(). + def output_tab_add_page(self, summary_flag=False): - When a download job generates error and/or warning messages, this - function is called to display them in the Errors List. + """Called by self.output_tab_setup_pages(). + + Adds a new page to the Output Tab's notebook, and updates IVs. Args: - media_data_obj (media.Video, media.Channel or media.Playlist): The - media data object whose download (real or simulated) generated - the error/warning messages. + summary_flag (bool): If True, add the (first) summary page to the + notebook, showing what the threads are doing """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7590 errors_list_add_row') + utils.debug_time('mwn 8625 output_tab_add_page') - # Create a new row for every error and warning message - # Use the same time on each - utc = datetime.datetime.utcfromtimestamp(time.time()) - time_string = str(utc.strftime('%H:%M:%S')) + # Each page (except the summary page) corresponds to a single + # downloads.DownloadWorker object. The page number matches the + # worker's .worker_id. The first worker is numbered #1 + if not summary_flag: + self.output_page_count += 1 - if self.app_obj.operation_error_show_flag: + # Add the new page + tab = Gtk.Box() - for msg in media_data_obj.error_list: + translate_note = _( + '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', + ) - # Prepare the icons - pixbuf = self.pixbuf_dict['error_small'] + if not summary_flag: + label = Gtk.Label.new_with_mnemonic( + _('Thread') + ' #_' + str(self.output_page_count), + ) + else: + label = Gtk.Label.new_with_mnemonic(_('_Summary')) - if isinstance(media_data_obj, media.Video): - pixbuf2 = self.pixbuf_dict['video_small'] - elif isinstance(media_data_obj, media.Channel): - pixbuf2 = self.pixbuf_dict['channel_small'] - elif isinstance(media_data_obj, media.Playlist): - pixbuf2 = self.pixbuf_dict['playlist_small'] - else: - return self.app_obj.system_error( - 218, - 'Errors List add row request failed sanity check', - ) - - # Prepare the new row in the treeview - row_list = [] - row_list.append(pixbuf) - row_list.append(pixbuf2) - row_list.append(time_string) - row_list.append( - utils.shorten_string( - media_data_obj.name, - self.medium_string_max_len, - ), - ) - row_list.append(utils.tidy_up_long_string(msg)) + self.output_notebook.append_page(tab, label) + tab.set_hexpand(True) + tab.set_vexpand(True) + tab.set_border_width(self.spacing_size) - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.errors_list_treeview.show_all() - self.errors_list_liststore.append(row_list) + # Add a textview to the tab, using a css style sheet to provide + # monospaced white text on a black background + scrolled = Gtk.ScrolledWindow() + tab.pack_start(scrolled, True, True, 0) + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - # (Don't update the Errors/Warnings tab label if it's the - # visible tab) - if self.visible_tab_num != 3: - self.tab_error_count += 1 + frame = Gtk.Frame() + scrolled.add_with_viewport(frame) - if self.app_obj.operation_warning_show_flag: + style_provider = self.output_tab_set_textview_css( + '#css_text_id_' + str(self.output_page_count) \ + + ', textview text {\n' \ + + ' background-color: ' + self.output_tab_bg_colour + ';\n' \ + + ' color: ' + self.output_tab_text_colour + ';\n' \ + + '}\n' \ + + '#css_label_id_' + str(self.output_page_count) \ + + ', textview {\n' \ + + ' font-family: monospace, monospace;\n' \ + + ' font-size: 10pt;\n' \ + + '}' + ) - for msg in media_data_obj.warning_list: + textview = Gtk.TextView() + frame.add(textview) + textview.set_wrap_mode(Gtk.WrapMode.WORD) + textview.set_editable(False) + textview.set_cursor_visible(False) - # Prepare the icons - pixbuf = self.pixbuf_dict['warning_small'] + context = textview.get_style_context() + context.add_provider(style_provider, 600) - if isinstance(media_data_obj, media.Video): - pixbuf2 = self.pixbuf_dict['video_small'] - elif isinstance(media_data_obj, media.Channel): - pixbuf2 = self.pixbuf_dict['channel_small'] - elif isinstance(media_data_obj, media.Playlist): - pixbuf2 = self.pixbuf_dict['playlist_small'] - else: - return self.app_obj.system_error( - 219, - 'Errors List add row request failed sanity check', - ) + # Reset css properties for the next Gtk.TextView created (for example, + # by AddVideoDialogue) so it uses default values, rather than the + # white text on black background used above + # To do that, create a dummy textview, and apply a css style to it + textview2 = Gtk.TextView() + style_provider2 = self.output_tab_set_textview_css( + '#css_text_id_default, textview text {\n' \ + + ' background-color: unset;\n' \ + + ' color: unset;\n' \ + + '}\n' \ + + '#css_label_id_default, textview {\n' \ + + ' font-family: unset;\n' \ + + ' font-size: unset;\n' \ + + '}' + ) - # Prepare the new row in the treeview - row_list = [] - row_list.append(pixbuf) - row_list.append(pixbuf2) - row_list.append(time_string) - row_list.append( - utils.shorten_string( - media_data_obj.name, - self.medium_string_max_len, - ), - ) - row_list.append(utils.tidy_up_long_string(msg)) + context = textview2.get_style_context() + context.add_provider(style_provider2, 600) - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.errors_list_treeview.show_all() - self.errors_list_liststore.append(row_list) + # Set up auto-scrolling + textview.connect( + 'size-allocate', + self.output_tab_do_autoscroll, + scrolled, + ) - # (Don't update the Errors/Warnings tab label if it's the - # visible tab) - if self.visible_tab_num != 3: - self.tab_warning_count += 1 + # Make the page visible + self.show_all() - # Update the tab's label to show the number of warnings/errors visible - if self.visible_tab_num != 3: - self.errors_list_refresh_label() + # Update IVs + if not summary_flag: + self.output_textview_dict[self.output_page_count] = textview + else: + self.output_textview_dict[0] = textview - def errors_list_add_system_error(self, error_code, msg): + def output_tab_set_textview_css(self, css_string): - """Can be called by anything. The quickest way is to call - mainapp.TartubeApp.system_error(), which acts as a wrapper for this - function. + """Called by self.output_tab_add_page(). - Display a system error message in the Errors List. + Applies a CSS style to the current screen. Called once to create a + white-on-black Gtk.TextView, then a second time to create a dummy + textview with default properties. Args: - error_code (int): An error code in the range 100-999 (see - the .system_error() function) + css_string (str): The CSS style to apply - msg (str): The system error message to display + Returns: + + The Gtk.CssProvider created """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7704 errors_list_add_system_error') - - if not self.app_obj.system_error_show_flag: - # Do nothing - return False - - # Prepare the icons - pixbuf = self.pixbuf_dict['error_small'] - pixbuf2 = self.pixbuf_dict['system_error_small'] - - # Prepare the new row in the treeview - row_list = [] - utc = datetime.datetime.utcfromtimestamp(time.time()) - time_string = str(utc.strftime('%H:%M:%S')) + utils.debug_time('mwn 8740 output_tab_set_textview_css') - row_list.append(pixbuf) - row_list.append(pixbuf2) - row_list.append(time_string) - row_list.append(__main__.__prettyname__ + ' error') - row_list.append( - utils.tidy_up_long_string('#' + str(error_code) + ': ' + msg), + style_provider = Gtk.CssProvider() + style_provider.load_from_data(bytes(css_string.encode())) + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), + style_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.errors_list_treeview.show_all() - self.errors_list_liststore.append(row_list) + return style_provider - # (Don't update the Errors/Warnings tab label if it's the visible - # tab) - if self.visible_tab_num != 3: - self.tab_error_count += 1 - self.errors_list_refresh_label() + def output_tab_write_stdout(self, page_num, msg): + + """Called by various functions in downloads.py, info.py, refresh.py, + tidy.py and updates.py. - def errors_list_add_system_warning(self, error_code, msg): + During a download operation, youtube-dl sends output to STDOUT. If + permitted, this output is displayed in the Output Tab. However, it + can't be displayed immediately, because Gtk widgets can't be updated + from within a thread. - """Can be called by anything. The quickest way is to call - mainapp.TartubeApp.system_warning(), which acts as a wrapper for this - function. + Instead, add the received values to a list, and wait for the GObject + timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update(). - Display a system warning message in the Errors List. + Other operations also call this function to display text in the + default colour. Args: - error_code (int): An error code in the range 100-999 (see - the .system_error() function) + page_num (int): The page number on which this message should be + displayed. Matches a key in self.output_textview_dict - msg (str): The system warning message to display + msg (str): The message to display. A newline character will be + added by self.output_tab_update_pages(). """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7757 errors_list_add_system_warning') + utils.debug_time('mwn 8780 output_tab_write_stdout') - if not self.app_obj.system_warning_show_flag: - # Do nothing - return False + self.output_tab_insert_list.extend( [page_num, msg, 'default'] ) - # Prepare the icons - pixbuf = self.pixbuf_dict['warning_small'] - pixbuf2 = self.pixbuf_dict['system_warning_small'] - # Prepare the new row in the treeview - row_list = [] - utc = datetime.datetime.utcfromtimestamp(time.time()) - time_string = str(utc.strftime('%H:%M:%S')) + def output_tab_write_stderr(self, page_num, msg): - row_list.append(pixbuf) - row_list.append(pixbuf2) - row_list.append(time_string) - row_list.append(__main__.__prettyname__ + ' warning') - row_list.append( - utils.tidy_up_long_string('#' + str(error_code) + ': ' + msg), - ) + """Called by various functions in downloads.py and info.py. - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.errors_list_treeview.show_all() - self.errors_list_liststore.append(row_list) + During a download operation, youtube-dl sends output to STDERR. If + permitted, this output is displayed in the Output Tab. However, it + can't be displayed immediately, because Gtk widgets can't be updated + from within a thread. - # (Don't update the Errors/Warnings tab label if it's the visible - # tab) - if self.visible_tab_num != 3: - self.tab_warning_count += 1 - self.errors_list_refresh_label() + Instead, add the received values to a list, and wait for the GObject + timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update(). + Other operations also call this function to display text in the + non-default colour. - def errors_list_refresh_label(self): + Args: - """Called by self.errors_list_reset(), .errors_list_add_row(), - .errors_list_add_system_error(), .errors_list_add_system_warning() - and .on_notebook_switch_page(). + page_num (int): The page number on which this message should be + displayed. Matches a key in self.output_textview_dict - When the Errors / Warnings tab becomes the visible one, reset the - tab's label (to show 'Errors / Warnings') + msg (str): The message to display. A newline character will be + added by self.output_tab_update_pages(). - When an error or warning is added to the Error List, refresh the tab's - label (to show something like 'Errors (4) / Warnings (1)' ) """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7806 errors_list_refresh_label') + utils.debug_time('mwn 8811 output_tab_write_stderr') - text = '_Errors' - if self.tab_error_count: - text += ' (' + str(self.tab_error_count) + ')' + self.output_tab_insert_list.extend( [page_num, msg, 'error_warning'] ) - text += ' / Warnings' - if self.tab_warning_count: - text += ' (' + str(self.tab_warning_count) + ')' - self.errors_label.set_text_with_mnemonic(text) + def output_tab_write_system_cmd(self, page_num, msg): + """Called by various functions in downloads.py, info.py and updates.py. - # Callback class methods - - - def on_video_index_apply_options(self, menu_item, media_data_obj): + During a download operation, youtube-dl system commands are displayed + in the Output Tab (if permitted). However, they can't be displayed + immediately, because Gtk widgets can't be updated from within a thread. - """Called from a callback in self.video_index_popup_menu(). + Instead, add the received values to a list, and wait for the GObject + timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update(). - Adds a set of download options (handled by an - options.OptionsManager object) to the specified media data object. + Other operations also call this function to display text in the + non-default colour. Args: - menu_item (Gtk.MenuItem): The clicked menu item + page_num (int): The page number on which this message should be + displayed. Matches a key in self.output_textview_dict - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + msg (str): The message to display. A newline character will be + added by self.output_tab_update_pages(). """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7839 on_video_index_apply_options') + utils.debug_time('mwn 8841 output_tab_write_system_cmd') - if self.app_obj.current_manager_obj \ - or media_data_obj.options_obj\ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ): - return self.app_obj.system_error( - 220, - 'Callback request denied due to current conditions', - ) + self.output_tab_insert_list.extend( [page_num, msg, 'system_cmd'] ) - # Apply download options to the media data object - self.app_obj.apply_download_options(media_data_obj) - # Open an edit window to show the options immediately - config.OptionsEditWin( - self.app_obj, - media_data_obj.options_obj, - media_data_obj, - ) + def output_tab_update_pages(self): + """Can be called by anything. - def on_video_index_check(self, menu_item, media_data_obj): + During a download operation, youtube-dl sends output to STDOUT/STDERR. + If permitted, this output is displayed in the Output Tab, along with + any system commands. - """Called from a callback in self.video_index_popup_menu(). + However, the text can't be displayed immediately, because Gtk widgets + can't be updated from within a thread. - Check the right-clicked media data object. + Instead, the text has been added to self.output_tab_insert_list, and + can now be displayed (and the list can be emptied). - Args: + Other operations also call this function to display text added to + self.output_tab_insert_list. + """ - menu_item (Gtk.MenuItem): The clicked menu item + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 8865 output_tab_update_pages') - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + update_dict = {} - """ + if self.output_tab_insert_list: - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7879 on_video_index_check') + while self.output_tab_insert_list: - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 221, - 'Callback request denied due to current conditions', - ) + page_num = self.output_tab_insert_list.pop(0) + msg = self.output_tab_insert_list.pop(0) + msg_type = self.output_tab_insert_list.pop(0) - # Start a download operation - self.app_obj.download_manager_start('sim', False, [media_data_obj] ) + # Add the output to the textview. STDERR messages and system + # commands are displayed in a different colour + # (The summary page is not necessarily visible) + if page_num in self.output_textview_dict: + textview = self.output_textview_dict[page_num] + textbuffer = textview.get_buffer() + update_dict[page_num] = textview - def on_video_index_convert_container(self, menu_item, media_data_obj): + if msg_type != 'default': - """Called from a callback in self.video_index_popup_menu(). + # The .markup_escape_text() call won't escape curly + # braces, so we need to replace those manually + msg = re.sub('{', '(', msg) + msg = re.sub('}', ')', msg) - Converts a channel to a playlist, or a playlist to a channel. + string = '' \ + + GObject.markup_escape_text(msg) + '\n' - Args: + if msg_type == 'system_cmd': - menu_item (Gtk.MenuItem): The clicked menu item + textbuffer.insert_markup( + textbuffer.get_end_iter(), + string.format( + self.output_tab_system_cmd_colour, + ), + -1, + ) - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + else: - """ + # STDERR + textbuffer.insert_markup( + textbuffer.get_end_iter(), + string.format(self.output_tab_stderr_colour), + -1, + ) - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7907 on_video_index_convert_container') + else: - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 222, - 'Callback request denied due to current conditions', - ) + # STDOUT + textbuffer.insert( + textbuffer.get_end_iter(), + msg + '\n', + ) - self.app_obj.convert_remote_container(media_data_obj) + # Make the new output visible + for textview in update_dict.values(): + textview.show_all() - def on_video_index_custom_dl(self, menu_item, media_data_obj): + def output_tab_do_autoscroll(self, textview, rect, scrolled): - """Called from a callback in self.video_index_popup_menu(). + """Called from a callback in self.output_tab_add_page(). - Custom download the right-clicked media data object. + When one of the textviews in the Output Tab is modified (text added or + removed), make sure the page is scrolled to the bottom. Args: - menu_item (Gtk.MenuItem): The clicked menu item + textview (Gtk.TextView): The textview to scroll - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + rect (Gdk.Rectangle): Ignored + + scrolled (Gtk.ScrolledWindow): The scroller which contains the + textview """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7934 on_video_index_custom_dl') - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 223, - 'Callback request denied due to current conditions', - ) + utils.debug_time('mwn 8947 output_tab_do_autoscroll') - # Start a custom download operation - self.app_obj.download_manager_start('custom', False, [media_data_obj] ) + adj = scrolled.get_vadjustment() + adj.set_value(adj.get_upper() - adj.get_page_size()) - def on_video_index_delete_container(self, menu_item, media_data_obj): + def output_tab_scroll_visible_page(self, page_num): - """Called from a callback in self.video_index_popup_menu(). + """Called by self.on_output_notebook_switch_page() and + .on_notebook_switch_page(). - Deletes the channel, playlist or folder. + When the user switches between pages in the Output Tab, scroll the + visible textview to the bottom (otherwise it gets confusing). Args: - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The clicked media data object + page_num (int): The page to be scrolled, matching a key in + self.output_textview_dict """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7962 on_video_index_delete_container') - - self.app_obj.delete_container(media_data_obj) + utils.debug_time('mwn 8969 output_tab_scroll_visible_page') + if page_num in self.output_textview_dict: + textview = self.output_textview_dict[page_num] - def on_video_index_dl_disable(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). + frame = textview.get_parent() + viewport = frame.get_parent() + scrolled = viewport.get_parent() - Set the media data object's flag to disable checking and downloading. + adj = scrolled.get_vadjustment() + adj.set_value(adj.get_upper() - adj.get_page_size()) - Args: - menu_item (Gtk.MenuItem): The clicked menu item + def output_tab_reset_pages(self): - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + """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). """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7983 on_video_index_dl_disable') - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 224, - 'Callback request denied due to current conditions', - ) - - if not media_data_obj.dl_disable_flag: - media_data_obj.set_dl_disable_flag(True) - else: - media_data_obj.set_dl_disable_flag(False) + utils.debug_time('mwn 8993 output_tab_reset_pages') - self.video_index_update_row_text(media_data_obj) - - - def on_video_index_download(self, menu_item, media_data_obj): + for textview in self.output_textview_dict.values(): + textbuffer = textview.get_buffer() + textbuffer.set_text('') + textview.show_all() - """Called from a callback in self.video_index_popup_menu(). - Download the right-clicked media data object. + # (Errors Tab) - Args: - menu_item (Gtk.MenuItem): The clicked menu item + def errors_list_reset(self): - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + """Can be called by anything. + Empties the Gtk.TreeView in the Errors List, ready for it to be + refilled. (There are no IVs to reset.) """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8015 on_video_index_download') + utils.debug_time('mwn 9013 errors_list_reset') - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 225, - 'Callback request denied due to current conditions', - ) + # Reset widgets + self.errors_list_liststore = Gtk.ListStore( + GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, + str, str, str, + ) + self.errors_list_treeview.set_model(self.errors_list_liststore) - # Start a download operation - self.app_obj.download_manager_start('real', False, [media_data_obj] ) + self.tab_error_count = 0 + self.tab_warning_count = 0 + self.errors_list_refresh_label() - def on_video_index_drag_data_received(self, treeview, drag_context, x, y, \ - selection_data, info, timestamp): + def errors_list_add_row(self, media_data_obj): - """Called from callback in self.video_index_reset(). + """Called by downloads.DownloadWorker.run(). - Retrieve the source and destination media data objects, and pass them - on to a function in the main application. + When a download job generates error and/or warning messages, this + function is called to display them in the Errors List. Args: - treeview (Gtk.TreeView): The Video Index's treeview - - drag_context (GdkX11.X11DragContext): Data from the drag procedure - - x, y (int): Cell coordinates in the treeview + media_data_obj (media.Video, media.Channel or media.Playlist): The + media data object whose download (real or simulated) generated + the error/warning messages. - selection_data (Gtk.SelectionData): Data from the dragged row + """ - info (int): Ignored + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9043 errors_list_add_row') - timestamp (int): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8052 on_video_index_drag_data_received') - - # Must override the usual Gtk handler - treeview.stop_emission('drag_data_received') - - # Extract the drop destination - drop_info = treeview.get_dest_row_at_pos(x, y) - if drop_info is not None: + # Create a new row for every error and warning message + # Use the same time on each + utc = datetime.datetime.utcfromtimestamp(time.time()) + time_string = str(utc.strftime('%H:%M:%S')) - # Get the dragged media data object - old_selection = self.video_index_treeview.get_selection() - (model, start_iter) = old_selection.get_selected() - drag_name = model[start_iter][1] + if self.app_obj.operation_error_show_flag: - # Get the destination media data object - drop_path, drop_posn = drop_info[0], drop_info[1] - drop_iter = model.get_iter(drop_path) - dest_name = model[drop_iter][1] + for msg in media_data_obj.error_list: - if drag_name and dest_name: + # Prepare the icons + pixbuf = self.pixbuf_dict['error_small'] - drag_id = self.app_obj.media_name_dict[drag_name] - dest_id = self.app_obj.media_name_dict[dest_name] + if isinstance(media_data_obj, media.Video): + pixbuf2 = self.pixbuf_dict['video_small'] + elif isinstance(media_data_obj, media.Channel): + pixbuf2 = self.pixbuf_dict['channel_small'] + elif isinstance(media_data_obj, media.Playlist): + pixbuf2 = self.pixbuf_dict['playlist_small'] + else: + return self.app_obj.system_error( + 218, + 'Errors List add row request failed sanity check', + ) - self.app_obj.move_container( - self.app_obj.media_reg_dict[drag_id], - self.app_obj.media_reg_dict[dest_id], + # Prepare the new row in the treeview + row_list = [] + row_list.append(pixbuf) + row_list.append(pixbuf2) + row_list.append(time_string) + row_list.append( + utils.shorten_string( + media_data_obj.name, + self.medium_string_max_len, + ), ) + row_list.append(utils.tidy_up_long_string(msg)) + # Create a new row in the treeview. Doing the .show_all() first + # prevents a Gtk error (for unknown reasons) + self.errors_list_treeview.show_all() + self.errors_list_liststore.append(row_list) - def on_video_index_drag_drop(self, treeview, drag_context, x, y, time): - - """Called from callback in self.video_index_reset(). - - Override the usual Gtk handler, and allow - self.on_video_index_drag_data_received() to collect the results of the - drag procedure. - - Args: + # (Don't update the Errors/Warnings tab label if it's the + # visible tab) + if self.visible_tab_num != 4: + self.tab_error_count += 1 - treeview (Gtk.TreeView): The Video Index's treeview + if self.app_obj.operation_warning_show_flag: - drag_context (GdkX11.X11DragContext): Data from the drag procedure + for msg in media_data_obj.warning_list: - x, y (int): Cell coordinates in the treeview + # Prepare the icons + pixbuf = self.pixbuf_dict['warning_small'] - time (int): A timestamp + if isinstance(media_data_obj, media.Video): + pixbuf2 = self.pixbuf_dict['video_small'] + elif isinstance(media_data_obj, media.Channel): + pixbuf2 = self.pixbuf_dict['channel_small'] + elif isinstance(media_data_obj, media.Playlist): + pixbuf2 = self.pixbuf_dict['playlist_small'] + else: + return self.app_obj.system_error( + 219, + 'Errors List add row request failed sanity check', + ) - """ + # Prepare the new row in the treeview + row_list = [] + row_list.append(pixbuf) + row_list.append(pixbuf2) + row_list.append(time_string) + row_list.append( + utils.shorten_string( + media_data_obj.name, + self.medium_string_max_len, + ), + ) + row_list.append(utils.tidy_up_long_string(msg)) - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8103 on_video_index_drag_drop') + # Create a new row in the treeview. Doing the .show_all() first + # prevents a Gtk error (for unknown reasons) + self.errors_list_treeview.show_all() + self.errors_list_liststore.append(row_list) - # Must override the usual Gtk handler - treeview.stop_emission('drag_drop') + # (Don't update the Errors/Warnings tab label if it's the + # visible tab) + if self.visible_tab_num != 4: + self.tab_warning_count += 1 - # The second of these lines cause the 'drag-data-received' signal to be - # emitted - target_list = drag_context.list_targets() - treeview.drag_get_data(drag_context, target_list[-1], time) + # Update the tab's label to show the number of warnings/errors visible + if self.visible_tab_num != 4: + self.errors_list_refresh_label() - def on_video_index_edit_options(self, menu_item, media_data_obj): + def errors_list_add_system_error(self, error_code, msg): - """Called from a callback in self.video_index_popup_menu(). + """Can be called by anything. The quickest way is to call + mainapp.TartubeApp.system_error(), which acts as a wrapper for this + function. - Edit the download options (handled by an - options.OptionsManager object) for the specified media data object. + Display a system error message in the Errors List. Args: - menu_item (Gtk.MenuItem): The clicked menu item + error_code (int): An error code in the range 100-999 (see + the .system_error() function) - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + msg (str): The system error message to display """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8131 on_video_index_edit_options') + utils.debug_time('mwn 9157 errors_list_add_system_error') - if self.app_obj.current_manager_obj or not media_data_obj.options_obj: - return self.app_obj.system_error( - 226, - 'Callback request denied due to current conditions', - ) + if not self.app_obj.system_error_show_flag: + # Do nothing + return False - # Open an edit window - config.OptionsEditWin( - self.app_obj, - media_data_obj.options_obj, - media_data_obj, + # Prepare the icons + pixbuf = self.pixbuf_dict['error_small'] + pixbuf2 = self.pixbuf_dict['system_error_small'] + + # Prepare the new row in the treeview + row_list = [] + utc = datetime.datetime.utcfromtimestamp(time.time()) + time_string = str(utc.strftime('%H:%M:%S')) + + row_list.append(pixbuf) + row_list.append(pixbuf2) + row_list.append(time_string) + row_list.append(_('Tartube error')) + row_list.append( + utils.tidy_up_long_string('#' + str(error_code) + ': ' + msg), ) + # Create a new row in the treeview. Doing the .show_all() first + # prevents a Gtk error (for unknown reasons) + self.errors_list_treeview.show_all() + self.errors_list_liststore.append(row_list) - def on_video_index_empty_folder(self, menu_item, media_data_obj): + # (Don't update the Errors/Warnings tab label if it's the visible + # tab) + if self.visible_tab_num != 4: + self.tab_error_count += 1 + self.errors_list_refresh_label() - """Called from a callback in self.video_index_popup_menu(). - Empties the folder. + def errors_list_add_system_warning(self, error_code, msg): + + """Can be called by anything. The quickest way is to call + mainapp.TartubeApp.system_warning(), which acts as a wrapper for this + function. + + Display a system warning message in the Errors List. Args: - menu_item (Gtk.MenuItem): The clicked menu item + error_code (int): An error code in the range 100-999 (see + the .system_error() function) - media_data_obj (media.Folder): The clicked media data object + msg (str): The system warning message to display """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8162 on_video_index_empty_folder') + utils.debug_time('mwn 9210 errors_list_add_system_warning') - # The True flag tells the function to empty the container, rather than - # delete it - self.app_obj.delete_container(media_data_obj, True) + if not self.app_obj.system_warning_show_flag: + # Do nothing + return False + + # Prepare the icons + pixbuf = self.pixbuf_dict['warning_small'] + pixbuf2 = self.pixbuf_dict['system_warning_small'] + # Prepare the new row in the treeview + row_list = [] + utc = datetime.datetime.utcfromtimestamp(time.time()) + time_string = str(utc.strftime('%H:%M:%S')) - def on_video_index_enforce_check(self, menu_item, media_data_obj): + row_list.append(pixbuf) + row_list.append(pixbuf2) + row_list.append(time_string) + row_list.append(_('Tartube warning')) + row_list.append( + utils.tidy_up_long_string('#' + str(error_code) + ': ' + msg), + ) - """Called from a callback in self.video_index_popup_menu(). + # Create a new row in the treeview. Doing the .show_all() first + # prevents a Gtk error (for unknown reasons) + self.errors_list_treeview.show_all() + self.errors_list_liststore.append(row_list) - Set the media data object's flag to force checking of the channel/ - playlist/folder (disabling actual downloads). + # (Don't update the Errors/Warnings tab label if it's the visible + # tab) + if self.visible_tab_num != 4: + self.tab_warning_count += 1 + self.errors_list_refresh_label() - Args: - menu_item (Gtk.MenuItem): The clicked menu item + def errors_list_refresh_label(self): - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + """Called by self.errors_list_reset(), .errors_list_add_row(), + .errors_list_add_system_error(), .errors_list_add_system_warning() + and .on_notebook_switch_page(). + + When the Errors / Warnings tab becomes the visible one, reset the + tab's label (to show 'Errors / Warnings') + When an error or warning is added to the Error List, refresh the tab's + label (to show something like 'Errors (4) / Warnings (1)' ) """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8186 on_video_index_enforce_check') + utils.debug_time('mwn 9259 errors_list_refresh_label') - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 227, - 'Callback request denied due to current conditions', - ) + text = _('_Errors') + if self.tab_error_count: + text += ' (' + str(self.tab_error_count) + ')' - if not media_data_obj.dl_sim_flag: - media_data_obj.set_dl_sim_flag(True) - else: - media_data_obj.set_dl_sim_flag(False) + text += ' / ' + _('Warnings') + if self.tab_warning_count: + text += ' (' + str(self.tab_warning_count) + ')' - self.video_index_update_row_text(media_data_obj) + self.errors_label.set_text_with_mnemonic(text) - def on_video_index_export(self, menu_item, media_data_obj): + # Callback class methods + + + def on_video_index_apply_options(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). - Exports a summary of the database, containing the selected channel/ - playlist/folder and its descendants. + Adds a set of download options (handled by an + options.OptionsManager object) to the specified media data object. Args: @@ -8219,16 +9289,35 @@ def on_video_index_export(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8219 on_video_index_export') - - self.app_obj.export_from_db( [media_data_obj] ) + utils.debug_time('mwn 9292 on_video_index_apply_options') + if self.app_obj.current_manager_obj \ + or media_data_obj.options_obj\ + or ( + isinstance(media_data_obj, media.Folder) + and media_data_obj.priv_flag + ): + return self.app_obj.system_error( + 220, + 'Callback request denied due to current conditions', + ) - def on_video_index_hide_folder(self, menu_item, media_data_obj): + # Apply download options to the media data object + self.app_obj.apply_download_options(media_data_obj) + + # Open an edit window to show the options immediately + config.OptionsEditWin( + self.app_obj, + media_data_obj.options_obj, + media_data_obj, + ) + + + def on_video_index_check(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). - Hides the folder in the Video Index. + Check the right-clicked media data object. Args: @@ -8240,18 +9329,23 @@ def on_video_index_hide_folder(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8240 on_video_index_hide_folder') + utils.debug_time('mwn 9332 on_video_index_check') - self.app_obj.mark_folder_hidden(media_data_obj, True) + if self.app_obj.current_manager_obj: + return self.app_obj.system_error( + 221, + 'Callback request denied due to current conditions', + ) + + # Start a download operation + self.app_obj.download_manager_start('sim', False, [media_data_obj] ) - def on_video_index_mark_archived(self, menu_item, media_data_obj, - only_child_videos_flag): + def on_video_index_convert_container(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). - Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as archived. + Converts a channel to a playlist, or a playlist to a channel. Args: @@ -8260,29 +9354,25 @@ def on_video_index_mark_archived(self, menu_item, media_data_obj, media_data_obj (media.Channel, media.Playlist or media.Channel): The clicked media data object - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8267 on_video_index_mark_archived') + utils.debug_time('mwn 9360 on_video_index_convert_container') - self.app_obj.mark_container_archived( - media_data_obj, - True, - only_child_videos_flag, - ) + if self.app_obj.current_manager_obj: + return self.app_obj.system_error( + 222, + 'Callback request denied due to current conditions', + ) + self.app_obj.convert_remote_container(media_data_obj) - def on_video_index_mark_not_archived(self, menu_item, media_data_obj, - only_child_videos_flag): + + def on_video_index_custom_dl(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). - Mark all videos in this folder (and in any child channels, playlists - and folders) as not archived. + Custom download the right-clicked media data object. Args: @@ -8291,83 +9381,47 @@ def on_video_index_mark_not_archived(self, menu_item, media_data_obj, media_data_obj (media.Channel, media.Playlist or media.Channel): The clicked media data object - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8298 on_video_index_mark_not_archived') + utils.debug_time('mwn 9387 on_video_index_custom_dl') - self.app_obj.mark_container_archived( - media_data_obj, - False, - only_child_videos_flag, - ) + if self.app_obj.current_manager_obj: + return self.app_obj.system_error( + 223, + 'Callback request denied due to current conditions', + ) + # Start a custom download operation + self.app_obj.download_manager_start('custom', False, [media_data_obj] ) - def on_video_index_mark_bookmark(self, menu_item, media_data_obj): + + def on_video_index_delete_container(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). - Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as bookmarked. + Deletes the channel, playlist or folder. Args: menu_item (Gtk.MenuItem): The clicked menu item - media_data_obj (media.Channel, media.Playlist or media.Channel): + media_data_obj (media.Channel, media.Playlist or media.Folder): The clicked media data object """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8324 on_video_index_mark_bookmark') - - # In earlier versions of Tartube, this action could take a very long - # time (perhaps hours) - count = len(media_data_obj.child_list) - if count < self.mark_video_lower_limit: - - # The operation 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) + utils.debug_time('mwn 9415 on_video_index_delete_container') - elif count < self.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.app_obj.prepare_mark_video( - ['bookmark', True, media_data_obj], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - 'The ' + media_data_obj.get_type() + ' contains ' \ - + str(count) + ' items, so this action might take a while.' \ - + '\n\nAre you sure you want to continue?', - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': ['bookmark', True, media_data_obj], - }, - ) + self.app_obj.delete_container(media_data_obj) - def on_video_index_mark_not_bookmark(self, menu_item, media_data_obj): + def on_video_index_dl_disable(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). - Mark all videos in this folder (and in any child channels, playlists - and folders) as not bookmarked. + Set the media data object's flag to disable checking and downloading. Args: @@ -8379,51 +9433,27 @@ def on_video_index_mark_not_bookmark(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8379 on_video_index_mark_not_bookmark') - - # In earlier versions of Tartube, this action could take a very long - # time (perhaps hours) - count = len(media_data_obj.child_list) - if count < self.mark_video_lower_limit: + utils.debug_time('mwn 9436 on_video_index_dl_disable') - # The operation 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) - - elif count < self.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.app_obj.prepare_mark_video( - ['bookmark', False, media_data_obj], + if self.app_obj.current_manager_obj: + return self.app_obj.system_error( + 224, + 'Callback request denied due to current conditions', ) + if not media_data_obj.dl_disable_flag: + media_data_obj.set_dl_disable_flag(True) else: + media_data_obj.set_dl_disable_flag(False) - # This might take a few tens of seconds, so prompt the user for - # confirmation first - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - 'The ' + media_data_obj.get_type() + ' contains ' \ - + str(count) + ' items, so this action might take a while.' \ - + '\n\nAre you sure you want to continue?', - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': ['bookmark', False, media_data_obj], - }, - ) + self.video_index_update_row_text(media_data_obj) - def on_video_index_mark_favourite(self, menu_item, media_data_obj, - only_child_videos_flag): + def on_video_index_download(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). - Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as favourite. + Download the right-clicked media data object. Args: @@ -8432,92 +9462,246 @@ def on_video_index_mark_favourite(self, menu_item, media_data_obj, media_data_obj (media.Channel, media.Playlist or media.Channel): The clicked media data object - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked - """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8439 on_video_index_mark_favourite') + utils.debug_time('mwn 9468 on_video_index_download') - self.app_obj.mark_container_favourite( - media_data_obj, - True, - only_child_videos_flag, - ) + if self.app_obj.current_manager_obj: + return self.app_obj.system_error( + 225, + 'Callback request denied due to current conditions', + ) + # Start a download operation + self.app_obj.download_manager_start('real', False, [media_data_obj] ) - def on_video_index_mark_not_favourite(self, menu_item, media_data_obj, - only_child_videos_flag): - """Called from a callback in self.video_index_popup_menu(). + def on_video_index_drag_data_received(self, treeview, drag_context, x, y, \ + selection_data, info, timestamp): - Mark all videos in this folder (and in any child channels, playlists - and folders) as not favourite. + """Called from callback in self.video_index_reset(). + + Retrieve the source and destination media data objects, and pass them + on to a function in the main application. Args: - menu_item (Gtk.MenuItem): The clicked menu item + treeview (Gtk.TreeView): The Video Index's treeview - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + drag_context (GdkX11.X11DragContext): Data from the drag procedure - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked + x, y (int): Cell coordinates in the treeview - """ + selection_data (Gtk.SelectionData): Data from the dragged row - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8470 on_video_index_mark_not_favourite') + info (int): Ignored - self.app_obj.mark_container_favourite( - media_data_obj, - False, - only_child_videos_flag, - ) + timestamp (int): Ignored + """ - def on_video_index_mark_new(self, menu_item, media_data_obj, - only_child_videos_flag): + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9505 on_video_index_drag_data_received') - """Called from a callback in self.video_index_popup_menu(). + # Must override the usual Gtk handler + treeview.stop_emission('drag_data_received') - Mark all videos in this channel, playlist or folder (and in any child - channels, playlists and folders) as new (but only if they have been - downloaded). + # Extract the drop destination + drop_info = treeview.get_dest_row_at_pos(x, y) + if drop_info is not None: - Args: + # Get the dragged media data object + old_selection = self.video_index_treeview.get_selection() + (model, start_iter) = old_selection.get_selected() + drag_name = model[start_iter][1] - menu_item (Gtk.MenuItem): The clicked menu item + # Get the destination media data object + drop_path, drop_posn = drop_info[0], drop_info[1] + drop_iter = model.get_iter(drop_path) + dest_name = model[drop_iter][1] - media_data_obj (media.Channel, media.Playlist or media.Channel): - The clicked media data object + if drag_name and dest_name: - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if all descendants should be - marked + drag_id = self.app_obj.media_name_dict[drag_name] + dest_id = self.app_obj.media_name_dict[dest_name] - """ + self.app_obj.move_container( + self.app_obj.media_reg_dict[drag_id], + self.app_obj.media_reg_dict[dest_id], + ) - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8502 on_video_index_mark_new') - self.app_obj.mark_container_new( - media_data_obj, - True, - only_child_videos_flag, - ) + def on_video_index_drag_drop(self, treeview, drag_context, x, y, time): + """Called from callback in self.video_index_reset(). - def on_video_index_mark_not_new(self, menu_item, media_data_obj, - only_child_videos_flag): + Override the usual Gtk handler, and allow + self.on_video_index_drag_data_received() to collect the results of the + drag procedure. + + Args: + + treeview (Gtk.TreeView): The Video Index's treeview + + drag_context (GdkX11.X11DragContext): Data from the drag procedure + + x, y (int): Cell coordinates in the treeview + + time (int): A timestamp + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9556 on_video_index_drag_drop') + + # Must override the usual Gtk handler + treeview.stop_emission('drag_drop') + + # The second of these lines cause the 'drag-data-received' signal to be + # emitted + target_list = drag_context.list_targets() + treeview.drag_get_data(drag_context, target_list[-1], time) + + + def on_video_index_edit_options(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). - Mark all videos in this channel, playlist or folder (and in any child - channels, playlists and folders) as not new. + Edit the download options (handled by an + options.OptionsManager object) for the specified media data object. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9584 on_video_index_edit_options') + + if self.app_obj.current_manager_obj or not media_data_obj.options_obj: + return self.app_obj.system_error( + 226, + 'Callback request denied due to current conditions', + ) + + # Open an edit window + config.OptionsEditWin( + self.app_obj, + media_data_obj.options_obj, + media_data_obj, + ) + + + def on_video_index_empty_folder(self, menu_item, media_data_obj): + + """Called from a callback in self.video_index_popup_menu(). + + Empties the folder. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Folder): The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9615 on_video_index_empty_folder') + + # The True flag tells the function to empty the container, rather than + # delete it + self.app_obj.delete_container(media_data_obj, True) + + + def on_video_index_enforce_check(self, menu_item, media_data_obj): + + """Called from a callback in self.video_index_popup_menu(). + + Set the media data object's flag to force checking of the channel/ + playlist/folder (disabling actual downloads). + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9639 on_video_index_enforce_check') + + if self.app_obj.current_manager_obj: + return self.app_obj.system_error( + 227, + 'Callback request denied due to current conditions', + ) + + if not media_data_obj.dl_sim_flag: + media_data_obj.set_dl_sim_flag(True) + else: + media_data_obj.set_dl_sim_flag(False) + + self.video_index_update_row_text(media_data_obj) + + + def on_video_index_export(self, menu_item, media_data_obj): + + """Called from a callback in self.video_index_popup_menu(). + + Exports a summary of the database, containing the selected channel/ + playlist/folder and its descendants. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9672 on_video_index_export') + + self.app_obj.export_from_db( [media_data_obj] ) + + + def on_video_index_hide_folder(self, menu_item, media_data_obj): + + """Called from a callback in self.video_index_popup_menu(). + + Hides the folder in the Video Index. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9693 on_video_index_hide_folder') + + self.app_obj.mark_folder_hidden(media_data_obj, True) + + + def on_video_index_mark_archived(self, menu_item, media_data_obj, + only_child_videos_flag): + + """Called from a callback in self.video_index_popup_menu(). + + Mark all of the children of this channel, playlist or folder (and all + of their children, and so on) as archived. Args: @@ -8533,21 +9717,52 @@ def on_video_index_mark_not_new(self, menu_item, media_data_obj, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8533 on_video_index_mark_not_new') + utils.debug_time('mwn 9720 on_video_index_mark_archived') - self.app_obj.mark_container_new( + self.app_obj.mark_container_archived( + media_data_obj, + True, + only_child_videos_flag, + ) + + + def on_video_index_mark_not_archived(self, menu_item, media_data_obj, + only_child_videos_flag): + + """Called from a callback in self.video_index_popup_menu(). + + Mark all videos in this folder (and in any child channels, playlists + and folders) as not archived. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + only_child_videos_flag (bool): Set to True if only child video + objects should be marked; False if all descendants should be + marked + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9751 on_video_index_mark_not_archived') + + self.app_obj.mark_container_archived( media_data_obj, False, only_child_videos_flag, ) - def on_video_index_mark_waiting(self, menu_item, media_data_obj): + def on_video_index_mark_bookmark(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as in the waiting list. + of their children, and so on) as bookmarked. Args: @@ -8559,7 +9774,7 @@ def on_video_index_mark_waiting(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8559 on_video_index_mark_waiting') + utils.debug_time('mwn 9777 on_video_index_mark_bookmark') # In earlier versions of Tartube, this action could take a very long # time (perhaps hours) @@ -8569,13 +9784,13 @@ def on_video_index_mark_waiting(self, menu_item, media_data_obj): # The operation 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) + self.app_obj.mark_video_bookmark(child_obj, True) elif count < self.mark_video_higher_limit: # This will take a few seconds, so don't prompt the user self.app_obj.prepare_mark_video( - ['waiting', True, media_data_obj], + ['bookmark', True, media_data_obj], ) else: @@ -8583,26 +9798,24 @@ def on_video_index_mark_waiting(self, menu_item, media_data_obj): # This might take a few tens of seconds, so prompt the user for # confirmation first self.app_obj.dialogue_manager_obj.show_msg_dialogue( - 'The ' + media_data_obj.get_type() + ' contains ' \ - + str(count) + ' items, so this action might take a while.' \ - + '\n\nAre you sure you want to continue?', + self.get_take_a_while_msg(media_data_obj, count), 'question', 'yes-no', None, # Parent window is main window { 'yes': 'prepare_mark_video', # Specified options - 'data': ['waiting', True, media_data_obj], + 'data': ['bookmark', True, media_data_obj], }, ) - def on_video_index_mark_not_waiting(self, menu_item, media_data_obj): + def on_video_index_mark_not_bookmark(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). Mark all videos in this folder (and in any child channels, playlists - and folders) as not in the waiting list. + and folders) as not bookmarked. Args: @@ -8614,7 +9827,7 @@ def on_video_index_mark_not_waiting(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8614 on_video_index_mark_not_waiting') + utils.debug_time('mwn 9830 on_video_index_mark_not_bookmark') # In earlier versions of Tartube, this action could take a very long # time (perhaps hours) @@ -8624,13 +9837,13 @@ def on_video_index_mark_not_waiting(self, menu_item, media_data_obj): # The operation 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) + self.app_obj.mark_video_bookmark(child_obj, False) elif count < self.mark_video_higher_limit: # This will take a few seconds, so don't prompt the user self.app_obj.prepare_mark_video( - ['waiting', False, media_data_obj], + ['bookmark', False, media_data_obj], ) else: @@ -8638,26 +9851,25 @@ def on_video_index_mark_not_waiting(self, menu_item, media_data_obj): # This might take a few tens of seconds, so prompt the user for # confirmation first self.app_obj.dialogue_manager_obj.show_msg_dialogue( - 'The ' + media_data_obj.get_type() + ' contains ' \ - + str(count) + ' items, so this action might take a while.' \ - + '\n\nAre you sure you want to continue?', + self.get_take_a_while_msg(media_data_obj, count), 'question', 'yes-no', None, # Parent window is main window { 'yes': 'prepare_mark_video', # Specified options - 'data': ['waiting', False, media_data_obj], + 'data': ['bookmark', False, media_data_obj], }, ) - def on_video_index_move_to_top(self, menu_item, media_data_obj): + def on_video_index_mark_favourite(self, menu_item, media_data_obj, + only_child_videos_flag): """Called from a callback in self.video_index_popup_menu(). - Moves a channel, playlist or folder to the top level (in other words, - removes its parent folder). + Mark all of the children of this channel, playlist or folder (and all + of their children, and so on) as favourite. Args: @@ -8666,21 +9878,29 @@ def on_video_index_move_to_top(self, menu_item, media_data_obj): media_data_obj (media.Channel, media.Playlist or media.Channel): The clicked media data object + only_child_videos_flag (bool): Set to True if only child video + objects should be marked; False if all descendants should be + marked + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8669 on_video_index_move_to_top') + utils.debug_time('mwn 9888 on_video_index_mark_favourite') - self.app_obj.move_container_to_top(media_data_obj) + self.app_obj.mark_container_favourite( + media_data_obj, + True, + only_child_videos_flag, + ) - def on_video_index_refresh(self, menu_item, media_data_obj): + def on_video_index_mark_not_favourite(self, menu_item, media_data_obj, + only_child_videos_flag): """Called from a callback in self.video_index_popup_menu(). - Refresh the right-clicked media data object, checking the corresponding - directory on the user's filesystem against video objects in the - database. + Mark all videos in this folder (and in any child channels, playlists + and folders) as not favourite. Args: @@ -8689,22 +9909,244 @@ def on_video_index_refresh(self, menu_item, media_data_obj): media_data_obj (media.Channel, media.Playlist or media.Channel): The clicked media data object + only_child_videos_flag (bool): Set to True if only child video + objects should be marked; False if all descendants should be + marked + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8692 on_video_index_refresh') + utils.debug_time('mwn 9919 on_video_index_mark_not_favourite') - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 228, - 'Callback request denied due to current conditions', - ) - - # Start a refresh operation - self.app_obj.refresh_manager_start(media_data_obj) + self.app_obj.mark_container_favourite( + media_data_obj, + False, + only_child_videos_flag, + ) - def on_video_index_remove_options(self, menu_item, media_data_obj): + def on_video_index_mark_new(self, menu_item, media_data_obj, + only_child_videos_flag): + + """Called from a callback in self.video_index_popup_menu(). + + Mark all videos in this channel, playlist or folder (and in any child + channels, playlists and folders) as new (but only if they have been + downloaded). + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + only_child_videos_flag (bool): Set to True if only child video + objects should be marked; False if all descendants should be + marked + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9951 on_video_index_mark_new') + + self.app_obj.mark_container_new( + media_data_obj, + True, + only_child_videos_flag, + ) + + + def on_video_index_mark_not_new(self, menu_item, media_data_obj, + only_child_videos_flag): + + """Called from a callback in self.video_index_popup_menu(). + + Mark all videos in this channel, playlist or folder (and in any child + channels, playlists and folders) as not new. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + only_child_videos_flag (bool): Set to True if only child video + objects should be marked; False if all descendants should be + marked + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 9982 on_video_index_mark_not_new') + + self.app_obj.mark_container_new( + media_data_obj, + False, + only_child_videos_flag, + ) + + + def on_video_index_mark_waiting(self, menu_item, media_data_obj): + + """Called from a callback in self.video_index_popup_menu(). + + Mark all of the children of this channel, playlist or folder (and all + of their children, and so on) as in the waiting list. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 10008 on_video_index_mark_waiting') + + # In earlier versions of Tartube, this action could take a very long + # time (perhaps hours) + count = len(media_data_obj.child_list) + if count < self.mark_video_lower_limit: + + # The operation 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) + + elif count < self.mark_video_higher_limit: + + # This will take a few seconds, so don't prompt the user + self.app_obj.prepare_mark_video( + ['waiting', True, media_data_obj], + ) + + else: + + # This might take a few tens of seconds, so prompt the user for + # confirmation first + self.app_obj.dialogue_manager_obj.show_msg_dialogue( + self.get_take_a_while_msg(media_data_obj, count), + 'question', + 'yes-no', + None, # Parent window is main window + { + 'yes': 'prepare_mark_video', + # Specified options + 'data': ['waiting', True, media_data_obj], + }, + ) + + + def on_video_index_mark_not_waiting(self, menu_item, media_data_obj): + + """Called from a callback in self.video_index_popup_menu(). + + Mark all videos in this folder (and in any child channels, playlists + and folders) as not in the waiting list. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 10061 on_video_index_mark_not_waiting') + + # In earlier versions of Tartube, this action could take a very long + # time (perhaps hours) + count = len(media_data_obj.child_list) + if count < self.mark_video_lower_limit: + + # The operation 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) + + elif count < self.mark_video_higher_limit: + + # This will take a few seconds, so don't prompt the user + self.app_obj.prepare_mark_video( + ['waiting', False, media_data_obj], + ) + + else: + + # This might take a few tens of seconds, so prompt the user for + # confirmation first + self.app_obj.dialogue_manager_obj.show_msg_dialogue( + self.get_take_a_while_msg(media_data_obj, count), + 'question', + 'yes-no', + None, # Parent window is main window + { + 'yes': 'prepare_mark_video', + # Specified options + 'data': ['waiting', False, media_data_obj], + }, + ) + + + def on_video_index_move_to_top(self, menu_item, media_data_obj): + + """Called from a callback in self.video_index_popup_menu(). + + Moves a channel, playlist or folder to the top level (in other words, + removes its parent folder). + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 10114 on_video_index_move_to_top') + + self.app_obj.move_container_to_top(media_data_obj) + + + def on_video_index_refresh(self, menu_item, media_data_obj): + + """Called from a callback in self.video_index_popup_menu(). + + Refresh the right-clicked media data object, checking the corresponding + directory on the user's filesystem against video objects in the + database. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Channel, media.Playlist or media.Channel): + The clicked media data object + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 10137 on_video_index_refresh') + + if self.app_obj.current_manager_obj: + return self.app_obj.system_error( + 228, + 'Callback request denied due to current conditions', + ) + + # Start a refresh operation + self.app_obj.refresh_manager_start(media_data_obj) + + + def on_video_index_remove_options(self, menu_item, media_data_obj): """Called from a callback in self.video_index_popup_menu(). @@ -8721,7 +10163,7 @@ def on_video_index_remove_options(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8721 on_video_index_remove_options') + utils.debug_time('mwn 10166 on_video_index_remove_options') if self.app_obj.current_manager_obj \ or not media_data_obj.options_obj: @@ -8750,7 +10192,7 @@ def on_video_index_remove_videos(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8750 on_video_index_remove_videos') + utils.debug_time('mwn 10195 on_video_index_remove_videos') for child_obj in media_data_obj.child_list: if isinstance(child_obj, media.Video): @@ -8774,7 +10216,7 @@ def on_video_index_rename_location(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8774 on_video_index_rename_location') + utils.debug_time('mwn 10219 on_video_index_rename_location') self.app_obj.rename_container(media_data_obj) @@ -8795,7 +10237,7 @@ def on_video_index_right_click(self, treeview, event): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8795 on_video_index_right_click') + utils.debug_time('mwn 10240 on_video_index_right_click') if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: @@ -8837,7 +10279,7 @@ def on_video_index_selection_changed(self, selection): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8837 on_video_index_selection_changed') + utils.debug_time('mwn 10282 on_video_index_selection_changed') (model, iter) = selection.get_selected() if iter is None or not model.iter_is_valid(iter): @@ -8906,7 +10348,7 @@ def on_video_index_set_destination(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8897 on_video_index_set_destination') + utils.debug_time('mwn 10351 on_video_index_set_destination') if isinstance(media_data_obj, media.Video): return self.app_obj.system_error( @@ -8947,7 +10389,7 @@ def on_video_index_set_nickname(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8938 on_video_index_set_nickname') + utils.debug_time('mwn 10392 on_video_index_set_nickname') if isinstance(media_data_obj, media.Video): return self.app_obj.system_error( @@ -8991,7 +10433,7 @@ def on_video_index_show_destination(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8982 on_video_index_show_destination') + utils.debug_time('mwn 10436 on_video_index_show_destination') other_obj = self.app_obj.media_reg_dict[media_data_obj.master_dbid] path = other_obj.get_actual_dir(self.app_obj) @@ -9017,7 +10459,7 @@ def on_video_index_show_location(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9008 on_video_index_show_location') + utils.debug_time('mwn 10462 on_video_index_show_location') path = media_data_obj.get_default_dir(self.app_obj) utils.open_file(path) @@ -9039,7 +10481,7 @@ def on_video_index_show_properties(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9030 on_video_index_show_properties') + utils.debug_time('mwn 10484 on_video_index_show_properties') if self.app_obj.current_manager_obj: return self.app_obj.system_error( @@ -9070,7 +10512,7 @@ def on_video_index_show_system_cmd(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9061 on_video_index_show_system_cmd') + utils.debug_time('mwn 10515 on_video_index_show_system_cmd') # Show the dialogue window dialogue_win = SystemCmdDialogue(self, media_data_obj) @@ -9094,7 +10536,7 @@ def on_video_index_tidy(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9085 on_video_index_tidy') + utils.debug_time('mwn 10539 on_video_index_tidy') if self.app_obj.current_manager_obj: return self.app_obj.system_error( @@ -9151,8 +10593,10 @@ def on_video_index_tidy(self, menu_item, media_data_obj): or choices_dict['del_archive_flag']: self.app_obj.dialogue_manager_obj.show_msg_dialogue( + _( 'Files cannot be recovered, after being deleted. Are you' \ + ' sure you want to continue?', + ), 'question', 'yes-no', None, # Parent window is main window @@ -9185,7 +10629,7 @@ def on_video_catalogue_apply_options(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9176 on_video_catalogue_apply_options') + utils.debug_time('mwn 10632 on_video_catalogue_apply_options') if self.app_obj.current_manager_obj or media_data_obj.options_obj: return self.app_obj.system_error( @@ -9221,16 +10665,51 @@ def on_video_catalogue_check(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9212 on_video_catalogue_check') + utils.debug_time('mwn 10668 on_video_catalogue_check') - if self.app_obj.current_manager_obj: + download_manager_obj = self.app_obj.download_manager_obj + + if ( + self.app_obj.current_manager_obj \ + and not download_manager_obj + ) or ( + download_manager_obj \ + and download_manager_obj.operation_type != 'sim' + ): return self.app_obj.system_error( 235, 'Callback request denied due to current conditions', ) - # Start a download operation - self.app_obj.download_manager_start('sim', False, [media_data_obj] ) + if download_manager_obj: + + # Download operation already in progress. Add this video to its + # list + download_item_obj \ + = download_manager_obj.download_list_obj.create_item( + media_data_obj, + True, + ) + + if download_item_obj: + + # Add a row to the Progress List + self.progress_list_add_row( + download_item_obj.item_id, + media_data_obj, + ) + + # Update the main window's progress bar + self.app_obj.download_manager_obj.nudge_progress_bar() + + else: + + # Start a new download operation to download this video + self.app_obj.download_manager_start( + 'sim', + False, + [media_data_obj], + ) def on_video_catalogue_check_multi(self, menu_item, media_data_list): @@ -9248,28 +10727,64 @@ def on_video_catalogue_check_multi(self, menu_item, media_data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9239 on_video_catalogue_check_multi') + utils.debug_time('mwn 10730 on_video_catalogue_check_multi') - if self.app_obj.current_manager_obj: + download_manager_obj = self.app_obj.download_manager_obj + + if ( + self.app_obj.current_manager_obj \ + and not download_manager_obj + ) or ( + download_manager_obj \ + and download_manager_obj.operation_type != 'sim' + ): return self.app_obj.system_error( 236, 'Callback request denied due to current conditions', ) - # Start a download operation - self.app_obj.download_manager_start('sim', False, media_data_list) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() + if download_manager_obj: + # Download operation already in progress. Add these video to its + # list + for media_data_obj in media_data_list: + download_item_obj \ + = download_manager_obj.download_list_obj.create_item( + media_data_obj, + True, + ) - def on_video_catalogue_custom_dl(self, menu_item, media_data_obj): + if download_item_obj: - """Called from a callback in self.video_catalogue_popup_menu(). + # Add a row to the Progress List + self.progress_list_add_row( + download_item_obj.item_id, + media_data_obj, + ) - Custom download the right-clicked media data object. + # Update the main window's progress bar + self.app_obj.download_manager_obj.nudge_progress_bar() - Args: + else: + + # Start a new download operation to download these videos + self.app_obj.download_manager_start( + 'sim', + False, + media_data_list, + ) + + # Standard de-selection of everything in the Video Catalogue + self.catalogue_listbox.unselect_all() + + + def on_video_catalogue_custom_dl(self, menu_item, media_data_obj): + + """Called from a callback in self.video_catalogue_popup_menu(). + + Custom download the right-clicked media data object. + + Args: menu_item (Gtk.MenuItem): The clicked menu item @@ -9278,7 +10793,7 @@ def on_video_catalogue_custom_dl(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9269 on_video_catalogue_custom_dl') + utils.debug_time('mwn 10796 on_video_catalogue_custom_dl') if self.app_obj.current_manager_obj: return self.app_obj.system_error( @@ -9305,7 +10820,7 @@ def on_video_catalogue_custom_dl_multi(self, menu_item, media_data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9296 on_video_catalogue_custom_dl_multi') + utils.debug_time('mwn 10823 on_video_catalogue_custom_dl_multi') if self.app_obj.current_manager_obj: return self.app_obj.system_error( @@ -9335,7 +10850,7 @@ def on_video_catalogue_delete_video(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9326 on_video_catalogue_delete_video') + utils.debug_time('mwn 10853 on_video_catalogue_delete_video') self.app_obj.delete_video(media_data_obj, True) @@ -9356,7 +10871,7 @@ def on_video_catalogue_delete_video_multi(self, menu_item, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9347 on_video_catalogue_delete_video_multi') + utils.debug_time('mwn 10874 on_video_catalogue_delete_video_multi') for media_data_obj in media_data_list: self.app_obj.delete_video(media_data_obj, True) @@ -9381,7 +10896,7 @@ def on_video_catalogue_dl_and_watch(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9372 on_video_catalogue_dl_and_watch') + 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 @@ -9414,7 +10929,7 @@ def on_video_catalogue_dl_and_watch_multi(self, menu_item, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9405 on_video_catalogue_dl_and_watch_multi') + utils.debug_time('mwn 10932 on_video_catalogue_dl_and_watch_multi') # Only download videos which have a source URL mod_list = [] @@ -9454,19 +10969,55 @@ def on_video_catalogue_download(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9445 on_video_catalogue_download') + utils.debug_time('mwn 10972 on_video_catalogue_download') - if self.app_obj.current_manager_obj: + download_manager_obj = self.app_obj.download_manager_obj + + if ( + self.app_obj.current_manager_obj \ + and not download_manager_obj + ) or ( + self.app_obj.download_manager_obj \ + and download_manager_obj.operation_type != 'real' + ) or media_data_obj.live_mode == 1: return self.app_obj.system_error( 239, 'Callback request denied due to current conditions', ) - # Start a download operation - self.app_obj.download_manager_start('real', False, [media_data_obj] ) + if download_manager_obj: + + # Download operation already in progress. Add this video to its + # list + download_item_obj \ + = download_manager_obj.download_list_obj.create_item( + media_data_obj, + True, + ) + + if download_item_obj: + + # Add a row to the Progress List + self.progress_list_add_row( + download_item_obj.item_id, + media_data_obj, + ) + # Update the main window's progress bar + self.app_obj.download_manager_obj.nudge_progress_bar() - def on_video_catalogue_download_multi(self, menu_item, media_data_list): + else: + + # Start a new download operation to download this video + self.app_obj.download_manager_start( + 'real', + False, + [media_data_obj], + ) + + + def on_video_catalogue_download_multi(self, menu_item, media_data_list, + live_wait_flag): """Called from a callback in self.video_catalogue_multi_popup_menu(). @@ -9478,19 +11029,58 @@ def on_video_catalogue_download_multi(self, menu_item, media_data_list): media_data_list (list): List of one or more media.Video objects + live_wait_flag (bool): True if any of the videos in media_data_list + are livestreams that have not started; False otherwise + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9472 on_video_catalogue_download_multi') + utils.debug_time('mwn 11038 on_video_catalogue_download_multi') - if self.app_obj.current_manager_obj: + download_manager_obj = self.app_obj.download_manager_obj + + if ( + self.app_obj.current_manager_obj \ + and not download_manager_obj + ) or ( + self.app_obj.download_manager_obj \ + and download_manager_obj.operation_type != 'real' + ) or live_wait_flag: return self.app_obj.system_error( 240, 'Callback request denied due to current conditions', ) - # Start a download operation - self.app_obj.download_manager_start('real', False, media_data_list) + if download_manager_obj: + + # Download operation already in progress. Add these videos to its + # list + for media_data_obj in media_data_list: + download_item_obj \ + = download_manager_obj.download_list_obj.create_item( + media_data_obj, + True, + ) + + if download_item_obj: + + # Add a row to the Progress List + self.progress_list_add_row( + download_item_obj.item_id, + media_data_obj, + ) + + # Update the main window's progress bar + self.app_obj.download_manager_obj.nudge_progress_bar() + + else: + + # Start a new download operation to download this video + self.app_obj.download_manager_start( + 'real', + False, + media_data_list, + ) # Standard de-selection of everything in the Video Catalogue self.catalogue_listbox.unselect_all() @@ -9512,7 +11102,7 @@ def on_video_catalogue_edit_options(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9503 on_video_catalogue_edit_options') + utils.debug_time('mwn 11105 on_video_catalogue_edit_options') if self.app_obj.current_manager_obj or not media_data_obj.options_obj: return self.app_obj.system_error( @@ -9544,7 +11134,7 @@ def on_video_catalogue_enforce_check(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9535 on_video_catalogue_enforce_check') + utils.debug_time('mwn 11137 on_video_catalogue_enforce_check') # (Don't allow the user to change the setting of # media.Video.dl_sim_flag if the video is in a channel or playlist, @@ -9581,7 +11171,7 @@ def on_video_catalogue_fetch_formats(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9572 on_video_catalogue_fetch_formats') + utils.debug_time('mwn 11174 on_video_catalogue_fetch_formats') # Can't start an info operation if any type of operation has started # since the popup menu was created @@ -9608,7 +11198,7 @@ def on_video_catalogue_fetch_subs(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9599 on_video_catalogue_fetch_subs') + utils.debug_time('mwn 11201 on_video_catalogue_fetch_subs') # Can't start an info operation if any type of operation has started # since the popup menu was created @@ -9619,6 +11209,62 @@ def on_video_catalogue_fetch_subs(self, menu_item, media_data_obj): self.app_obj.info_manager_start('subs', media_data_obj) + def on_video_catalogue_livestream_toggle(self, menu_item, media_data_obj, + action): + + """Called from a callback in self.video_catalogue_popup_menu(). + + Toggles one of five livestream action settings. + + Args: + + menu_item (Gtk.MenuItem): The clicked menu item + + media_data_obj (media.Video): The clicked video object + + action (str): 'notify', 'alarm', 'open', 'dl_start', 'dl_stop' + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 11230 on_video_catalogue_livestream_toggle') + + # Update the IV + if action == 'notify': + if not media_data_obj.dbid \ + in self.app_obj.media_reg_auto_notify_dict: + self.app_obj.add_auto_notify_dict(media_data_obj) + else: + self.app_obj.del_auto_notify_dict(media_data_obj) + elif action == 'alarm': + if not media_data_obj.dbid \ + in self.app_obj.media_reg_auto_alarm_dict: + self.app_obj.add_auto_alarm_dict(media_data_obj) + else: + self.app_obj.del_auto_alarm_dict(media_data_obj) + elif action == 'open': + if not media_data_obj.dbid \ + in self.app_obj.media_reg_auto_open_dict: + self.app_obj.add_auto_open_dict(media_data_obj) + else: + self.app_obj.del_auto_open_dict(media_data_obj) + elif action == 'dl_start': + if not media_data_obj.dbid \ + in self.app_obj.media_reg_auto_dl_start_dict: + self.app_obj.add_auto_dl_start_dict(media_data_obj) + else: + self.app_obj.del_auto_dl_start_dict(media_data_obj) + elif action == 'dl_stop': + if not media_data_obj.dbid \ + in self.app_obj.media_reg_auto_dl_stop_dict: + self.app_obj.add_auto_dl_stop_dict(media_data_obj) + else: + self.app_obj.del_auto_dl_stop_dict(media_data_obj) + + # Update the catalogue item + self.video_catalogue_update_row(media_data_obj) + + def on_video_catalogue_mark_temp_dl(self, menu_item, media_data_obj): """Called from a callback in self.video_catalogue_popup_menu(). @@ -9635,7 +11281,7 @@ def on_video_catalogue_mark_temp_dl(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9626 on_video_catalogue_mark_temp_dl') + 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 @@ -9671,7 +11317,7 @@ def on_video_catalogue_mark_temp_dl_multi(self, menu_item, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9662 on_video_catalogue_temp_dl_multi') + utils.debug_time('mwn 11320 on_video_catalogue_temp_dl_multi') # Only download videos which have a source URL mod_list = [] @@ -9700,6 +11346,30 @@ def on_video_catalogue_mark_temp_dl_multi(self, menu_item, self.catalogue_listbox.unselect_all() + def on_video_catalogue_not_livestream(self, menu_item, media_data_obj): + + """Called from a callback in self.video_catalogue_popup_menu(). + + Marks the specified video as not a livestream after all. + + 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 11364 on_video_catalogue_not_livestream') + + # Update the video + self.app_obj.mark_video_live(media_data_obj, 0) + + # Update the catalogue item + self.video_catalogue_update_row(media_data_obj) + + def on_video_catalogue_page_entry_activated(self, entry): """Called from a callback in self.setup_videos_tab(). @@ -9715,7 +11385,7 @@ def on_video_catalogue_page_entry_activated(self, entry): if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 9706 on_video_catalogue_page_entry_activated', + 'mwn 11388 on_video_catalogue_page_entry_activated', ) page_num = utils.strip_whitespace(entry.get_text()) @@ -9751,7 +11421,7 @@ def on_video_catalogue_re_download(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9742 on_video_catalogue_re_download') + utils.debug_time('mwn 11424 on_video_catalogue_re_download') if self.app_obj.current_manager_obj: return self.app_obj.system_error( @@ -9780,7 +11450,9 @@ def on_video_catalogue_re_download(self, menu_item, media_data_obj): # This will prevent a successful re-downloading of the video. Change # the name of the archive file temporarily; after the download # operation is complete, the file is give its original name - self.app_obj.set_backup_archive(media_data_obj) + self.app_obj.set_backup_archive( + media_data_obj.parent_obj.get_default_dir(self.app_obj), + ) # Now we're ready to start the download operation self.app_obj.download_manager_start('real', False, [media_data_obj] ) @@ -9802,7 +11474,7 @@ def on_video_catalogue_remove_options(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9793 on_video_catalogue_remove_options') + utils.debug_time('mwn 11477 on_video_catalogue_remove_options') if self.app_obj.current_manager_obj or not media_data_obj.options_obj: return self.app_obj.system_error( @@ -9831,7 +11503,7 @@ def on_video_catalogue_size_entry_activated(self, entry): if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 9822 on_video_catalogue_size_entry_activated', + 'mwn 11506 on_video_catalogue_size_entry_activated', ) size = utils.strip_whitespace(entry.get_text()) @@ -9866,7 +11538,7 @@ def on_video_catalogue_show_location(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9857 on_video_catalogue_show_location') + utils.debug_time('mwn 11541 on_video_catalogue_show_location') parent_obj = media_data_obj.parent_obj other_obj = self.app_obj.media_reg_dict[parent_obj.master_dbid] @@ -9889,7 +11561,7 @@ def on_video_catalogue_show_properties(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9880 on_video_catalogue_show_properties') + utils.debug_time('mwn 11564 on_video_catalogue_show_properties') if self.app_obj.current_manager_obj: return self.app_obj.system_error( @@ -9918,7 +11590,7 @@ def on_video_catalogue_show_properties_multi(self, menu_item, if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 9909 on_video_catalogue_show_properties_multi', + 'mwn 11593 on_video_catalogue_show_properties_multi', ) if self.app_obj.current_manager_obj: @@ -9951,7 +11623,7 @@ def on_video_catalogue_show_system_cmd(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9942 on_video_catalogue_show_system_cmd') + utils.debug_time('mwn 11626 on_video_catalogue_show_system_cmd') # Show the dialogue window dialogue_win = SystemCmdDialogue(self, media_data_obj) @@ -9982,7 +11654,7 @@ def on_video_catalogue_temp_dl(self, menu_item, media_data_obj, \ """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9973 on_video_catalogue_temp_dl') + 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 @@ -10033,7 +11705,7 @@ def on_video_catalogue_temp_dl_multi(self, menu_item, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10024 on_video_catalogue_temp_dl_multi') + utils.debug_time('mwn 11708 on_video_catalogue_temp_dl_multi') # Only download videos which have a source URL mod_list = [] @@ -10089,7 +11761,7 @@ def on_video_catalogue_test_dl(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10080 on_video_catalogue_test_dl') + utils.debug_time('mwn 11764 on_video_catalogue_test_dl') # Can't start an info operation if any type of operation has started # since the popup menu was created @@ -10142,7 +11814,7 @@ def on_video_catalogue_toggle_archived_video(self, menu_item, \ if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10133 on_video_catalogue_toggle_archived_video', + 'mwn 11817 on_video_catalogue_toggle_archived_video', ) if not media_data_obj.archive_flag: @@ -10173,7 +11845,7 @@ def on_video_catalogue_toggle_archived_video_multi(self, menu_item, if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10164 on_video_catalogue_toggle_archived_video_multi', + 'mwn 11848 on_video_catalogue_toggle_archived_video_multi', ) for media_data_obj in media_data_list: @@ -10201,7 +11873,7 @@ def on_video_catalogue_toggle_bookmark_video(self, menu_item, \ if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10192 on_video_catalogue_toggle_bookmark_video', + 'mwn 11876 on_video_catalogue_toggle_bookmark_video', ) if not media_data_obj.bookmark_flag: @@ -10230,7 +11902,7 @@ def on_video_catalogue_toggle_bookmark_video_multi(self, menu_item, if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10221 on_video_catalogue_toggle_bookmark_video_multi', + 'mwn 11905 on_video_catalogue_toggle_bookmark_video_multi', ) for media_data_obj in media_data_list: @@ -10257,7 +11929,7 @@ def on_video_catalogue_toggle_favourite_video(self, menu_item, \ if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10248 on_video_catalogue_toggle_favourite_video', + 'mwn 11932 on_video_catalogue_toggle_favourite_video', ) if not media_data_obj.fav_flag: @@ -10286,7 +11958,7 @@ def on_video_catalogue_toggle_favourite_video_multi(self, menu_item, if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10277 on_video_catalogue_toggle_favourite_video_multi', + 'mwn 11961 on_video_catalogue_toggle_favourite_video_multi', ) for media_data_obj in media_data_list: @@ -10311,7 +11983,7 @@ def on_video_catalogue_toggle_new_video(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10302 on_video_catalogue_toggle_new_video') + utils.debug_time('mwn 11986 on_video_catalogue_toggle_new_video') if not media_data_obj.new_flag: self.app_obj.mark_video_new(media_data_obj, True) @@ -10339,7 +12011,7 @@ def on_video_catalogue_toggle_new_video_multi(self, menu_item, if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10330 on_video_catalogue_toggle_new_video_multi', + 'mwn 12014 on_video_catalogue_toggle_new_video_multi', ) for media_data_obj in media_data_list: @@ -10366,7 +12038,7 @@ def on_video_catalogue_toggle_waiting_video(self, menu_item, \ if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10357 on_video_catalogue_toggle_waiting_video', + 'mwn 12041 on_video_catalogue_toggle_waiting_video', ) if not media_data_obj.waiting_flag: @@ -10395,7 +12067,7 @@ def on_video_catalogue_toggle_waiting_video_multi(self, menu_item, if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10386 on_video_catalogue_toggle_waiting_video_multi', + 'mwn 12070 on_video_catalogue_toggle_waiting_video_multi', ) for media_data_obj in media_data_list: @@ -10420,7 +12092,7 @@ def on_video_catalogue_watch_hooktube(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10411 on_video_catalogue_watch_hooktube') + utils.debug_time('mwn 12095 on_video_catalogue_watch_hooktube') # Launch the video utils.open_file( @@ -10450,7 +12122,7 @@ def on_video_catalogue_watch_invidious(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10441 on_video_catalogue_watch_invidious') + utils.debug_time('mwn 12125 on_video_catalogue_watch_invidious') # Launch the video utils.open_file( @@ -10481,7 +12153,7 @@ def on_video_catalogue_watch_video(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10472 on_video_catalogue_watch_video') + utils.debug_time('mwn 12156 on_video_catalogue_watch_video') # Launch the video self.app_obj.watch_video_in_player(media_data_obj) @@ -10510,7 +12182,7 @@ def on_video_catalogue_watch_video_multi(self, menu_item, media_data_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10501 on_video_catalogue_watch_video_multi') + utils.debug_time('mwn 12185 on_video_catalogue_watch_video_multi') # Only watch videos which are marked as downloaded for media_data_obj in media_data_list: @@ -10544,7 +12216,7 @@ def on_video_catalogue_watch_website(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10535 on_video_catalogue_watch_website') + utils.debug_time('mwn 12219 on_video_catalogue_watch_website') # Launch the video utils.open_file(media_data_obj.source) @@ -10574,7 +12246,7 @@ def on_video_catalogue_watch_website_multi(self, menu_item, if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 10565 on_video_catalogue_watch_website_multi', + 'mwn 12249 on_video_catalogue_watch_website_multi', ) # Only watch videos which have a source URL @@ -10612,7 +12284,7 @@ def on_progress_list_dl_last(self, menu_item, download_item_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10603 on_progress_list_dl_last') + utils.debug_time('mwn 12287 on_progress_list_dl_last') # Check that, since the popup menu was created, the media data object # hasn't been assigned a worker @@ -10659,7 +12331,7 @@ def on_progress_list_dl_next(self, menu_item, download_item_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10650 on_progress_list_dl_next') + utils.debug_time('mwn 12234 on_progress_list_dl_next') # Check that, since the popup menu was created, the media data object # hasn't been assigned a worker @@ -10702,7 +12374,7 @@ def on_progress_list_right_click(self, treeview, event): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10693 on_progress_list_right_click') + utils.debug_time('mwn 12377 on_progress_list_right_click') if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: @@ -10742,7 +12414,7 @@ def on_progress_list_stop_all_soon(self, menu_item): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10733 on_progress_list_stop_soon') + utils.debug_time('mwn 12417 on_progress_list_stop_soon') # Check that, since the popup menu was created, the download operation # hasn't finished @@ -10778,7 +12450,7 @@ def on_progress_list_stop_now(self, menu_item, download_item_obj, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10769 on_progress_list_stop_now') + utils.debug_time('mwn 12453 on_progress_list_stop_now') # Check that, since the popup menu was created, the video downloader # hasn't already finished checking/downloading the selected media @@ -10819,7 +12491,7 @@ def on_progress_list_stop_soon(self, menu_item, download_item_obj, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10810 on_progress_list_stop_soon') + utils.debug_time('mwn 12494 on_progress_list_stop_soon') # Check that, since the popup menu was created, the video downloader # hasn't already finished checking/downloading the selected media @@ -10852,7 +12524,7 @@ def on_progress_list_watch_hooktube(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10843 on_progress_list_watch_hooktube') + utils.debug_time('mwn 12527 on_progress_list_watch_hooktube') if isinstance(media_data_obj, media.Video): @@ -10885,7 +12557,7 @@ def on_progress_list_watch_invidious(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10876 on_progress_list_watch_invidious') + utils.debug_time('mwn 12560 on_progress_list_watch_invidious') if isinstance(media_data_obj, media.Video): @@ -10917,7 +12589,7 @@ def on_progress_list_watch_website(self, menu_item, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10908 on_progress_list_watch_website') + utils.debug_time('mwn 12592 on_progress_list_watch_website') if isinstance(media_data_obj, media.Video) \ and media_data_obj.source: @@ -10943,7 +12615,7 @@ def on_results_list_delete_video(self, menu_item, media_data_obj, path): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10934 on_results_list_delete_video') + utils.debug_time('mwn 12618 on_results_list_delete_video') # Delete the video self.app_obj.delete_video(media_data_obj, True) @@ -10969,7 +12641,7 @@ def on_results_list_right_click(self, treeview, event): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10960 on_results_list_right_click') + utils.debug_time('mwn 12644 on_results_list_right_click') if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: @@ -11009,11 +12681,199 @@ def on_errors_list_clear(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11000 on_errors_list_clear') + utils.debug_time('mwn 12684 on_errors_list_clear') self.errors_list_reset() + def on_classic_dest_dir_combo_changed(self, combo): + + """Called from callback in self.setup_classic_mode_tab(). + + In the combobox displaying destination directories, remember the most + recent directory specified by the user, so it can be restored when + Tartube restarts. + + Args: + + combo (Gtk.ComboBox): The clicked widget + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 12704 on_classic_dest_dir_combo_changed') + + tree_iter = self.classic_dest_dir_combo.get_active_iter() + model = self.classic_dest_dir_combo.get_model() + self.app_obj.set_classic_dir_previous(model[tree_iter][0]) + + + def on_classic_format_combo_changed(self, combo): + + """Called from callback in self.setup_classic_mode_tab(). + + In the combobox displaying video/audio formats, if the user selects the + line 'Video:' or 'Audio:', select the line immediately below that + (which should be a valid format). + + Args: + + combo (Gtk.ComboBox): The clicked widget + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 12726 on_classic_format_combo_changed') + + tree_iter = self.classic_format_combo.get_active_iter() + model = self.classic_format_combo.get_model() + text = model[tree_iter][0] + + if text == _('Video:') or text == _('Audio:'): + self.classic_format_combo.set_active( + self.classic_format_combo.get_active() + 1, + ) + + + def on_classic_progress_list_get_cmd(self, menu_item, dummy_obj): + + """Called from a callback in self.classic_progress_list_popup_menu(). + + Copies the youtube-dl system command for the specified dummy + media.Video object to the clipboard. + + Args: + + menu_item (Gtk.MenuItem): The menu item that was clicked + + media_data_obj (media.Video): The dummy media.Video objects on the + clicked row + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 12755 on_classic_progress_list_get_cmd') + + # Generate the list of download options for the dummy media.Video + # object + options_parser_obj = options.OptionsParser(self.app_obj) + options_list = options_parser_obj.parse( + dummy_obj, + self.app_obj.general_options_obj, + True, # Classic Mode Tab + ) + + # Obtain the system command used to download this media data object + cmd_list = utils.generate_system_cmd( + self.app_obj, + dummy_obj, + options_list, + False, + True, # Classic Mode Tab + ) + + # Copy it to the clipboard + if cmd_list: + char = ' ' + system_cmd = char.join(cmd_list) + + else: + system_cmd = '' + + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clipboard.set_text(system_cmd, -1) + + + def on_classic_progress_list_get_url(self, menu_item, dummy_obj): + + """Called from a callback in self.classic_progress_list_popup_menu(). + + Copies the URL for the specified dummy media.Video object to the + clipboard. + + Args: + + menu_item (Gtk.MenuItem): The menu item that was clicked + + media_data_obj (media.Video): The dummy media.Video objects on the + clicked row + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 12804 on_classic_progress_list_get_url') + + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clipboard.set_text(dummy_obj.source, -1) + + + def on_classic_progress_list_open_destination(self, menu_item, dummy_obj): + + """Called from a callback in self.classic_progress_list_popup_menu(). + + Opens the download destination for the specified dummy media.Video + object. + + Args: + + menu_item (Gtk.MenuItem): The menu item that was clicked + + media_data_obj (media.Video): The dummy media.Video objects on the + clicked row + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time( + 'mwn 12828 on_classic_progress_list_open_destination', + ) + + if dummy_obj.dummy_dir: + utils.open_file(dummy_obj.dummy_dir) + + + def on_classic_progress_list_right_click(self, treeview, event): + + """Called from callback in self.setup_classic_mode_tab(). + + When the user right-clicks an item in the Classic Progress List, opens + a context-sensitive popup menu. + + Args: + + treeview (Gtk.TreeView): The Results List's treeview + + event (Gdk.EventButton): The event emitting the Gtk signal + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 12851 on_classic_progress_list_right_click') + + if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: + + # If the user right-clicked on empty space, the call to + # .get_path_at_pos returns None (or an empty list) + if not treeview.get_path_at_pos( + int(event.x), + int(event.y), + ): + return + + path, column, cellx, celly = treeview.get_path_at_pos( + int(event.x), + int(event.y), + ) + + iter = self.classic_progress_liststore.get_iter(path) + if iter is not None: + self.classic_progress_list_popup_menu( + event, + path, + self.classic_progress_liststore[iter][0], + ) + + def on_bandwidth_spinbutton_changed(self, spinbutton): """Called from callback in self.setup_progress_tab(). @@ -11029,7 +12889,7 @@ def on_bandwidth_spinbutton_changed(self, spinbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11020 on_bandwidth_spinbutton_changed') + utils.debug_time('mwn 12892 on_bandwidth_spinbutton_changed') self.app_obj.set_bandwidth_default( int(self.bandwidth_spinbutton.get_value()) @@ -11051,7 +12911,7 @@ def on_bandwidth_checkbutton_changed(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11042 on_bandwidth_checkbutton_changed') + utils.debug_time('mwn 12914 on_bandwidth_checkbutton_changed') self.app_obj.set_bandwidth_apply_flag( self.bandwidth_checkbutton.get_active(), @@ -11074,7 +12934,7 @@ def on_delete_event(self, widget, event): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11065 on_delete_event') + utils.debug_time('mwn 12937 on_delete_event') if self.app_obj.status_icon_obj \ and self.app_obj.show_status_icon_flag \ @@ -11104,7 +12964,7 @@ def on_hide_finished_checkbutton_changed(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11095 on_hide_finished_checkbutton_changed') + utils.debug_time('mwn 12967 on_hide_finished_checkbutton_changed') self.app_obj.set_progress_list_hide_flag(checkbutton.get_active()) @@ -11129,11 +12989,12 @@ def on_notebook_switch_page(self, notebook, box, page_num): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11120 on_notebook_switch_page') + utils.debug_time('mwn 12992 on_notebook_switch_page') self.visible_tab_num = page_num - if page_num == 2: + if page_num == 3: + # Switching between tabs causes pages in the Output Tab to scroll # to the top. Make sure they're all scrolled back to the bottom @@ -11146,57 +13007,112 @@ def on_notebook_switch_page(self, notebook, box, page_num): for page_num in range(1, page_count): self.output_tab_scroll_visible_page(page_num) - elif page_num == 3 and not self.app_obj.system_msg_keep_totals_flag: + elif page_num == 4 and not self.app_obj.system_msg_keep_totals_flag: + # Update the tab's label self.tab_error_count = 0 self.tab_warning_count = 0 self.errors_list_refresh_label() - def on_num_worker_spinbutton_changed(self, spinbutton): + def on_notify_desktop_clicked(self, notification, action_name, notify_id, \ + url): - """Called from callback in self.setup_progress_tab(). + """Called from callback in self.notify_desktop(). - In the Progress Tab, when the user sets the number of simultaneous - downloads allowed, inform mainapp.TartubeApp, which in turn informs the - downloads.DownloadManager object. + When the user clicks the button in a desktop notification, open the + corresponding URL in the system's web browser. Args: - spinbutton (Gtk.SpinButton) - The clicked widget + notification: The Notify.Notification object + + action_name (str): 'action_click' + + notify_id (int): A key in self.notify_desktop_dict + + url (str): The URL to open """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11159 on_num_worker_spinbutton_changed') + utils.debug_time('mwn 13039 on_notify_desktop_clicked') - if self.num_worker_checkbutton.get_active(): - self.app_obj.set_num_worker_default( - int(self.num_worker_spinbutton.get_value()) - ) + utils.open_file(url) + # This callback isn't needed any more, so we don't need to retain a + # reference to the Notify.Notification + if notify_id in self.notify_desktop_dict: + del self.notify_desktop_dict[notify_id] - def on_num_worker_checkbutton_changed(self, checkbutton): - """Called from callback in self.setup_progress_tab(). + def on_notify_desktop_closed(self, notification, notify_id): - In the Progress Tab, when the user sets the number of simultaneous - downloads allowed, inform mainapp.TartubeApp, which in turn informs the - downloads.DownloadManager object. + """Called from callback in self.notify_desktop(). + + When the desktop notification (which includes a button) is closed, + we no longer need a reference to the Notify.Notification object, so + remove it. Args: - checkbutton (Gtk.CheckButton) - The clicked widget + notification: The Notify.Notification object + + notify_id (int): A key in self.notify_desktop_dict """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11182 on_num_worker_checkbutton_changed') + utils.debug_time('mwn 13066 on_notify_desktop_closed') - if self.num_worker_checkbutton.get_active(): + if notify_id in self.notify_desktop_dict: + del self.notify_desktop_dict[notify_id] - self.app_obj.set_num_worker_apply_flag(True) - self.app_obj.set_num_worker_default( + + def on_num_worker_spinbutton_changed(self, spinbutton): + + """Called from callback in self.setup_progress_tab(). + + In the Progress Tab, when the user sets the number of simultaneous + downloads allowed, inform mainapp.TartubeApp, which in turn informs the + downloads.DownloadManager object. + + Args: + + spinbutton (Gtk.SpinButton) - The clicked widget + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13087 on_num_worker_spinbutton_changed') + + if self.num_worker_checkbutton.get_active(): + self.app_obj.set_num_worker_default( + int(self.num_worker_spinbutton.get_value()) + ) + + + def on_num_worker_checkbutton_changed(self, checkbutton): + + """Called from callback in self.setup_progress_tab(). + + In the Progress Tab, when the user sets the number of simultaneous + downloads allowed, inform mainapp.TartubeApp, which in turn informs the + downloads.DownloadManager object. + + Args: + + checkbutton (Gtk.CheckButton) - The clicked widget + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13110 on_num_worker_checkbutton_changed') + + if self.num_worker_checkbutton.get_active(): + + self.app_obj.set_num_worker_apply_flag(True) + self.app_obj.set_num_worker_default( int(self.num_worker_spinbutton.get_value()) ) @@ -11219,7 +13135,7 @@ def on_operation_error_checkbutton_changed(self, checkbutton): if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 11210 on_operation_error_checkbutton_changed', + 'mwn 13138 on_operation_error_checkbutton_changed', ) self.app_obj.set_operation_error_show_flag(checkbutton.get_active()) @@ -11239,7 +13155,7 @@ def on_operation_warning_checkbutton_changed(self, checkbutton): if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 11230 on_operation_warning_checkbutton_changed', + 'mwn 13158 on_operation_warning_checkbutton_changed', ) self.app_obj.set_operation_warning_show_flag(checkbutton.get_active()) @@ -11265,7 +13181,7 @@ def on_output_notebook_switch_page(self, notebook, box, page_num): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11256 on_output_notebook_switch_page') + utils.debug_time('mwn 13184 on_output_notebook_switch_page') # Output Tab IVs number the first page as #1, and so on self.output_tab_scroll_visible_page(page_num + 1) @@ -11285,7 +13201,7 @@ def on_reverse_results_checkbutton_changed(self, checkbutton): if DEBUG_FUNC_FLAG: utils.debug_time( - 'mwn 11276 on_reverse_results_checkbutton_changed', + 'mwn 13204 on_reverse_results_checkbutton_changed', ) self.app_obj.set_results_list_reverse_flag(checkbutton.get_active()) @@ -11304,7 +13220,7 @@ def on_system_error_checkbutton_changed(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11295 on_system_error_checkbutton_changed') + utils.debug_time('mwn 13223 on_system_error_checkbutton_changed') self.app_obj.set_system_error_show_flag(checkbutton.get_active()) @@ -11322,7 +13238,7 @@ def on_system_warning_checkbutton_changed(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11313 on_system_warning_checkbutton_changed') + utils.debug_time('mwn 13241 on_system_warning_checkbutton_changed') self.app_obj.set_system_warning_show_flag(checkbutton.get_active()) @@ -11337,16 +13253,37 @@ def on_window_drag_data_received(self, widget, context, x, y, data, info, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11328 on_window_drag_data_received') + utils.debug_time('mwn 13256 on_window_drag_data_received') + text = None if info == 0: text = data.get_text() - if text is not None: - # Hopefully, 'text' contains one or more valid URLs - # Decide where to add this video. If a suitable folder is - # selected in the Video Index, use that; otherwise, use - # 'Unsorted Videos' + if text is not None: + + # Hopefully, 'text' contains one or more valid URLs + + # Decide where to add this video + # If a suitable folder is selected in the Video Index, use + # that; otherwise, use 'Unsorted Videos' + # However, if the Classic Mode Tab is visible, copy the URL + # into its textview instead + if self.notebook.get_current_page == 2: + + # Classic Mode Tab is visible. The final argument tells the + # called function to use that argument, instead of the + # clipboard + utils.add_links_to_textview_from_clipboard( + self.app_obj, + self.classic_textbuffer, + self.classic_mark_start, + self.classic_mark_end, + text, + ) + + else: + + # Classic Mode Tab is not visible parent_obj = None if self.video_index_current is not None: dbid \ @@ -11388,7 +13325,7 @@ def on_window_drag_data_received(self, widget, context, x, y, data, info, # If any duplicates were found, inform the user if duplicate_list: - msg = 'The following videos are duplicates:' + msg = _('The following videos are duplicates:') for line in duplicate_list: msg += '\n\n' + line @@ -11403,1394 +13340,2438 @@ def on_window_drag_data_received(self, widget, context, x, y, data, info, context.finish(True, False, time) - def on_video_res_combobox_changed(self, combo): + def on_video_res_combobox_changed(self, combo): + + """Called from callback in self.setup_progress_tab(). + + In the Progress Tab, when the user sets the video resolution limit, + inform mainapp.TartubeApp. The new setting is applied to the next + download job. + + Args: + + combo (Gtk.ComboBox): The clicked widget + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13358 on_video_res_combobox_changed') + + tree_iter = self.video_res_combobox.get_active_iter() + model = self.video_res_combobox.get_model() + self.app_obj.set_video_res_default(model[tree_iter][0]) + + + def on_video_res_checkbutton_changed(self, checkbutton): + + """Called from callback in self.setup_progress_tab(). + + In the Progress Tab, when the user turns the video resolution limit + on/off, inform mainapp.TartubeApp. The new setting is applied to the + next download job. + + Args: + + checkbutton (Gtk.CheckButton): The clicked widget + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13380 on_video_res_checkbutton_changed') + + self.app_obj.set_video_res_apply_flag( + self.video_res_checkbutton.get_active(), + ) + + + # (Callback support functions) + + + def get_take_a_while_msg(self, media_data_obj, count): + + """Called by self.on_video_index_mark_bookmark(), + .on_video_index_mark_not_bookmark(), .on_video_index_mark_waiting(), + .on_video_index_mark_not_waiting(). + + Composes a (translated) message to display in a dialogue window. + + Args: + + media_data_obj (media.Channel, media.Playlist, media.Folder): The + media data object to be marked/unmarked + + count (int): The number of child media data objects in the + specified channel, playlist or folder + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13409 get_take_a_while_msg') + + media_type = media_data_obj.get_type() + if media_type == 'channel': + + msg = _( + 'The channel contains {0} items, so this action may take' \ + + ' a while', + ).format(str(count)) + + elif media_type == 'playlist': + + msg = _( + 'The playlist contains {0} items, so this action may take' \ + + ' a while', + ).format(str(count)) + + else: + + msg = _( + 'The folder contains {0} items, so this action may take' \ + + ' a while', + ).format(str(count)) + + msg += '\n\n' + _('Are you sure you want to continue?') + + return msg + + + # Set accessors + + + def add_child_window(self, config_win_obj): + + """Called by config.GenericConfigWin.setup(). + + When a configuration window opens, add it to our list of such windows. + + Args: + + config_win_obj (config.GenericConfigWin): The window to add + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13454 add_child_window') + + # Check that the window isn't already in the list (unlikely, but check + # anyway) + if config_win_obj in self.config_win_list: + return self.app_obj.system_error( + 247, + 'Callback request denied due to current conditions', + ) + + # Update the IV + self.config_win_list.append(config_win_obj) + + + def del_child_window(self, config_win_obj): + + """Called by config.GenericConfigWin.close(). + + When a configuration window closes, remove it to our list of such + windows. + + Args: + + config_win_obj (config.GenericConfigWin): The window to remove + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13482 del_child_window') + + # Update the IV + # (Don't show an error if the window isn't in the list, as it's + # conceivable this function might be called twice) + if config_win_obj in self.config_win_list: + self.config_win_list.remove(config_win_obj) + + + def set_previous_alt_dest_dbid(self, value): + + """Called by functions in SetDestinationDialogue. + + The specified value may be a .dbid, or None. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13499 set_previous_alt_dest_dbid') + + self.previous_alt_dest_dbid = value + + +class SimpleCatalogueItem(object): + + """Called by MainWin.video_catalogue_redraw_all() and + .video_catalogue_insert_item(). + + Python class that handles a single row in the Video Catalogue. + + Each mainwin.SimpleCatalogueItem objects stores widgets used in that row, + and updates them when required. + + This class offers a simple view with a minimum of widgets (for example, no + video thumbnails). The mainwin.ComplexCatalogueItem class offers a more + complex view (for example, with video thumbnails). + + Args: + + main_win_obj (mainwin.MainWin): The main window object + + video_obj (media.Video): The media data object itself (always a video) + + """ + + + # Standard class methods + + + def __init__(self, main_win_obj, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13533 __init__') + + # IV list - class objects + # ----------------------- + # The main window object + self.main_win_obj = main_win_obj + # The media data object itself (always a video) + self.video_obj = video_obj + + + # IV list - Gtk widgets + # --------------------- + self.catalogue_row = None # mainwin.CatalogueRow + self.hbox = None # Gtk.HBox + self.status_image = None # Gtk.Image + self.name_label = None # Gtk.Label + self.parent_label = None # Gtk.Label + self.stats_label = None # Gtk.Label + + + # IV list - other + # --------------- + # Unique ID for this object, matching the .dbid for self.video_obj (an + # integer) + self.dbid = video_obj.dbid + # Size (in pixels) of gaps between various widgets + self.spacing_size = 5 + + # Whenever self.draw_widgets() or .update_widgets() is called, the + # background colour might be changed + # This IV shows the value of the self.video_obj.live_mode, the last + # time either of those functions was called. If the value has + # actually changed, then we ask Gtk to change the background + # (otherwise, we don't) + self.previous_live_mode = 0 + + + # Public class methods + + + def draw_widgets(self, catalogue_row): + + """Called by mainwin.MainWin.video_catalogue_redraw_all() and + .video_catalogue_insert_item(). + + After a Gtk.ListBoxRow has been created for this object, populate it + with widgets. + + Args: + + catalogue_row (mainwin.CatalogueRow): A wrapper for a + Gtk.ListBoxRow object, storing the media.Video object displayed + in that row. + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13590 draw_widgets') + + self.catalogue_row = catalogue_row + + event_box = Gtk.EventBox() + self.catalogue_row.add(event_box) + event_box.connect('button-press-event', self.on_right_click_row) + + self.hbox = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + spacing=0, + ) + event_box.add(self.hbox) + self.hbox.set_border_width(0) + + # Highlight livestreams by specifying a background colour + self.update_background() + + self.status_image = Gtk.Image() + self.hbox.pack_start( + self.status_image, + False, + False, + self.spacing_size, + ) + + vbox = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + spacing=0, + ) + self.hbox.pack_start(vbox, True, True, self.spacing_size) + + # Video name + self.name_label = Gtk.Label('', xalign = 0) + vbox.pack_start(self.name_label, True, True, 0) + + # Parent channel/playlist/folder name (if allowed) + if self.main_win_obj.app_obj.catalogue_mode == 'simple_show_parent': + self.parent_label = Gtk.Label('', xalign = 0) + vbox.pack_start(self.parent_label, True, True, 0) + + # Video stats + self.stats_label = Gtk.Label('', xalign=0) + vbox.pack_start(self.stats_label, True, True, 0) + + + def update_widgets(self): + + """Called by mainwin.MainWin.video_catalogue_redraw_all(), + .video_catalogue_update_row() and .video_catalogue_insert_item(). + + Sets the values displayed by each widget. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13645 update_widgets') + + self.update_background() + self.update_tooltips() + self.update_status_image() + self.update_video_name() + self.update_parent_name() + self.update_video_stats() + + + def update_background(self): + + """Calledy by self.draw_widgets() and .update_widgets(). + + Updates the background colour to show which videos are livestreams + (but only when a video's livestream mode has changed). + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13664 update_background') + + if self.previous_live_mode != self.video_obj.live_mode: + + self.previous_live_mode = self.video_obj.live_mode + + if self.video_obj.live_mode == 0 \ + or not self.main_win_obj.app_obj.livestream_use_colour_flag: + self.hbox.override_background_color( + Gtk.StateType.NORMAL, + None, + ) + elif self.video_obj.live_mode == 1: + self.hbox.override_background_color( + Gtk.StateType.NORMAL, + self.main_win_obj.waiting_colour, + ) + elif self.video_obj.live_mode == 2: + self.hbox.override_background_color( + Gtk.StateType.NORMAL, + self.main_win_obj.live_colour, + ) + + + def update_tooltips(self): + + """Called by anything, but mainly called by self.update_widgets(). + + Updates the tooltips for the Gtk.HBox that contains everything. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13696 update_tooltips') + + if self.main_win_obj.app_obj.show_tooltips_flag: + self.hbox.set_tooltip_text( + self.video_obj.fetch_tooltip_text( + self.main_win_obj.app_obj, + self.main_win_obj.tooltip_max_len, + ), + ) + + + def update_status_image(self): + + """Called by anything, but mainly called by self.update_widgets(). + + Updates the Gtk.Image widget to display the video's download status. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13715 update_status_image') + + # Set the download status + if self.video_obj.live_mode == 1: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['stream_wait_small'], + ) + elif self.video_obj.live_mode == 2: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['stream_live_small'], + ) + elif self.video_obj.dl_flag: + if self.video_obj.archive_flag: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['archived_small'], + ) + else: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['have_file_small'], + ) + else: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['no_file_small'], + ) + + + def update_video_name(self): + + """Called by anything, but mainly called by self.update_widgets(). + + Updates the Gtk.Label widget to display the video's current name. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13749 update_video_name') + + # For videos whose name is unknown, display the URL, rather than the + # usual '(video with no name)' string + name = self.video_obj.nickname + if name is None \ + or name == self.main_win_obj.app_obj.default_video_name: + + if self.video_obj.source is not None: + + # Using pango markup to display a URL is too risky, so just use + # ordinary text + self.name_label.set_text( + utils.shorten_string( + self.video_obj.source, + self.main_win_obj.very_long_string_max_len, + ), + ) + + return + + else: + + # No URL to show, so we're forced to use '(video with no name)' + name = self.main_win_obj.app_obj.default_video_name + + string = '' + if self.video_obj.new_flag: + string += ' font_weight="bold"' + + if self.video_obj.dl_sim_flag: + string += ' style="italic"' + + self.name_label.set_markup( + '' + \ + html.escape( + utils.shorten_string( + name, + self.main_win_obj.very_long_string_max_len, + ), + quote=True, + ) + '' + ) + + + def update_parent_name(self): + + """Called by anything, but mainly called by self.update_widgets(). + + Updates the Gtk.Label widget to display the name of the parent channel, + playlist or folder. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13803 update_parent_name') + + if self.main_win_obj.app_obj.catalogue_mode != 'simple_show_parent': + return + + if isinstance(self.video_obj.parent_obj, media.Channel): + string = _('From channel:') + ' \'' + elif isinstance(self.video_obj.parent_obj, media.Playlist): + string = _('From playlist:') + ' \'' + else: + string = _('From folder:') + ' \'' + + string2 = html.escape( + utils.shorten_string( + self.video_obj.parent_obj.name, + self.main_win_obj.long_string_max_len, + ), + quote=True, + ) + + self.parent_label.set_markup(string + string2 + '\'') + + + def update_video_stats(self): + + """Called by anything, but mainly called by self.update_widgets(). + + Updates the Gtk.Label widget to display the video's current side/ + duration/date information. + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13835 update_video_stats') + + if self.video_obj.live_mode == 1: + + msg = _('Livestream has not started yet') + + elif self.video_obj.live_mode == 2: + + msg = _('Livestream has started') + + else: + + if self.video_obj.duration is not None: + msg = _('Duration:') + ' ' + utils.convert_seconds_to_string( + self.video_obj.duration, + True, + ) + + else: + msg = _('Duration:') + ' ' + _('unknown') + '' + + size = self.video_obj.get_file_size_string() + if size is not None: + msg += ' - ' + _('Size:') + ' ' + size + else: + msg += ' - ' + _('Size:') + ' ' + _('unknown') + '' + + date = self.video_obj.get_upload_date_string( + self.main_win_obj.app_obj.show_pretty_dates_flag, + ) + + if date is not None: + msg += ' - ' + _('Date:') + ' ' + date + else: + msg += ' - ' + _('Date:') + ' ' + _('unknown') + '' + + self.stats_label.set_markup(msg) + + + # Callback methods + + + def on_right_click_row(self, event_box, event): + + """Called from callback in self.draw_widgets(). + + When the user right-clicks an a row, create a context-sensitive popup + menu. + + Args: + + event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the + signal emitted by the click + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13892 on_right_click_row') + + if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: + + self.main_win_obj.video_catalogue_popup_menu(event, self.video_obj) + + +class ComplexCatalogueItem(object): + + """Called by MainWin.video_catalogue_redraw_all() and + .video_catalogue_insert_item(). + + Python class that handles a single row in the Video Catalogue. + + Each mainwin.ComplexCatalogueItem objects stores widgets used in that row, + and updates them when required. + + The mainwin.SimpleCatalogueItem class offers a simple view with a minimum + of widgets (for example, no video thumbnails). This class offers a more + complex view (for example, with video thumbnails). + + Args: + + main_win_obj (mainwin.MainWin): The main window object + + video_obj (media.Video): The media data object itself (always a video) + + """ + + + # Standard class methods + + + def __init__(self, main_win_obj, video_obj): + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 13928 __init__') + + # IV list - class objects + # ----------------------- + # The main window object + self.main_win_obj = main_win_obj + # The media data object itself (always a video) + self.video_obj = video_obj + + + # IV list - Gtk widgets + # --------------------- + self.catalogue_row = None # mainwin.CatalogueRow + self.frame = None # Gtk.Frame + self.thumb_box = None # Gtk.Box + self.thumb_image = None # Gtk.Image + self.label_box = None # Gtk.Box + self.name_label = None # Gtk.Label + self.status_image = None # Gtk.Image + self.error_image = None # Gtk.Image + self.warning_image = None # Gtk.Image + self.descrip_label = None # Gtk.Label + self.expand_label = None # Gtk.Label + self.stats_label = None # Gtk.Label + self.live_auto_notify_label = None # Gtk.Label + self.live_auto_alarm_label = None # Gtk.Label + self.live_auto_open_label = None # Gtk.Label + self.live_auto_dl_start_label = None + # Gtk.Label + self.live_auto_dl_stop_label = None # Gtk.Label + self.watch_label = None # Gtk.Label + self.watch_player_label = None # Gtk.Label + self.watch_web_label = None # Gtk.Label + self.watch_hooktube_label = None # Gtk.Label + self.watch_invidious_label = None # Gtk.Label + self.temp_box = None # Gtk.Box + self.temp_label = None # Gtk.Label + self.temp_mark_label = None # Gtk.Label + self.temp_dl_label = None # Gtk.Label + self.temp_dl_watch_label = None # Gtk.Label + self.marked_box = None # Gtk.Box + self.marked_label = None # Gtk.Label + self.marked_archive_label = None # Gtk.Label + self.marked_bookmark_label = None # Gtk.Label + self.marked_fav_label = None # Gtk.Label + self.marked_new_label = None # Gtk.Label + self.marked_waiting_label = None # Gtk.Label + + + # IV list - other + # --------------- + # Unique ID for this object, matching the .dbid for self.video_obj (an + # integer) + self.dbid = video_obj.dbid + # Size (in pixels) of gaps between various widgets + self.spacing_size = 5 + # The state of the More/Less label. False if the video's short + # description (or no description at all) is visible, True if the + # video's full description is visible + self.expand_descrip_flag = False + # Flag set to True if the video's parent folder is a temporary folder, + # meaning that some widgets don't need to be drawn at all + self.no_temp_widgets_flag = False + + # Whenever self.draw_widgets() or .update_widgets() is called, the + # background colour might be changed + # This IV shows the value of the self.video_obj.live_mode, the last + # time either of those functions was called. If the value has + # actually changed, then we ask Gtk to change the background + # (otherwise, we don't) + self.previous_live_mode = 0 + # Flag set to True when the temporary labels box (self.temp_box) is + # visible, False when not + self.temp_box_visible_flag = False + # Flag set to True when the marked labels box (self.marked_box) is + # visible, False when not + self.marked_box_visible_flag = False + + + # Public class methods + + + def draw_widgets(self, catalogue_row): + + """Called by mainwin.MainWin.video_catalogue_redraw_all() and + .video_catalogue_insert_item(). + + After a Gtk.ListBoxRow has been created for this object, populate it + with widgets. + + Args: + + catalogue_row (mainwin.CatalogueRow): A wrapper for a + Gtk.ListBoxRow object, storing the media.Video object displayed + in that row. + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 14027 draw_widgets') + + # If the video's parent folder is a temporary folder, then we don't + # need one row of widgets at all + parent_obj = self.video_obj.parent_obj + if isinstance(parent_obj, media.Folder) \ + and parent_obj.temp_flag: + self.no_temp_widgets_flag = True + else: + self.no_temp_widgets_flag = False + + # Draw the widgets + self.catalogue_row = catalogue_row + + event_box = Gtk.EventBox() + self.catalogue_row.add(event_box) + event_box.connect('button-press-event', self.on_right_click_row) + + self.frame = Gtk.Frame() + event_box.add(self.frame) + self.frame.set_border_width(self.spacing_size) + + # Highlight livestreams by specifying a background colour + self.update_background() + + hbox = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + spacing=0, + ) + self.frame.add(hbox) + hbox.set_border_width(self.spacing_size) + + # The thumbnail is in its own vbox, so we can keep it in the top-left + # when the video's description has multiple lines + self.thumb_box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + spacing=0, + ) + hbox.pack_start(self.thumb_box, False, False, 0) + + self.thumb_image = Gtk.Image() + self.thumb_box.pack_start(self.thumb_image, False, False, 0) + + # Everything to the right of the thumbnail is in a second vbox + self.label_box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + spacing=0, + ) + hbox.pack_start(self.label_box, True, True, self.spacing_size) + + # First row - video name + hbox2 = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + spacing=0, + ) + self.label_box.pack_start(hbox2, True, True, 0) + + self.name_label = Gtk.Label('', xalign = 0) + hbox2.pack_start(self.name_label, True, True, 0) + + # Status/error/warning icons + self.status_image = Gtk.Image() + hbox2.pack_end(self.status_image, False, False, 0) + + self.warning_image = Gtk.Image() + hbox2.pack_end(self.warning_image, False, False, self.spacing_size) + + self.error_image = Gtk.Image() + hbox2.pack_end(self.error_image, False, False, self.spacing_size) + + # Second row - video description (incorporating the the More/Less + # label), or the name of the parent channel/playlist/folder, + # depending on settings + self.descrip_label = Gtk.Label('', xalign=0) + self.label_box.pack_start(self.descrip_label, True, True, 0) + self.descrip_label.connect( + 'activate-link', + self.on_click_descrip_label, + ) + + # Third row - video stats, or livestream notification options, + # depending on settings + hbox3 = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + spacing=0, + ) + self.label_box.pack_start(hbox3, True, True, 0) + + # (This label is visible in both situations) + self.stats_label = Gtk.Label('', xalign=0) + hbox3.pack_start(self.stats_label, False, False, 0) + + # (These labels are visible only for livestreams) + # Auto-notify + self.live_auto_notify_label = Gtk.Label('', xalign=0) + hbox3.pack_start( + self.live_auto_notify_label, + False, + False, + 0, + ) + self.live_auto_notify_label.connect( + 'activate-link', + self.on_click_live_auto_notify_label, + ) + + # Auto-sound alarm + self.live_auto_alarm_label = Gtk.Label('', xalign=0) + hbox3.pack_start( + self.live_auto_alarm_label, + False, + False, + (self.spacing_size * 2), + ) + self.live_auto_alarm_label.connect( + 'activate-link', + self.on_click_live_auto_alarm_label, + ) + + # Auto-open + self.live_auto_open_label = Gtk.Label('', xalign=0) + hbox3.pack_start( + self.live_auto_open_label, + False, + False, + 0, + ) + self.live_auto_open_label.connect( + 'activate-link', + self.on_click_live_auto_open_label, + ) + + # D/L on start + self.live_auto_dl_start_label = Gtk.Label('', xalign=0) + hbox3.pack_start( + self.live_auto_dl_start_label, + False, + False, + (self.spacing_size * 2), + ) + self.live_auto_dl_start_label.connect( + 'activate-link', + self.on_click_live_auto_dl_start_label, + ) + + # D/L on stop + self.live_auto_dl_stop_label = Gtk.Label('', xalign=0) + hbox3.pack_start( + self.live_auto_dl_stop_label, + False, + False, + 0, + ) + self.live_auto_dl_stop_label.connect( + 'activate-link', + self.on_click_live_auto_dl_stop_label, + ) + + # Fourth row - Watch... + hbox4 = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + spacing=0, + ) + self.label_box.pack_start(hbox4, True, True, 0) + + self.watch_label = Gtk.Label(_('Watch:') + ' ', xalign=0) + hbox4.pack_start(self.watch_label, False, False, 0) + + # Watch in player + self.watch_player_label = Gtk.Label('', xalign=0) + hbox4.pack_start(self.watch_player_label, False, False, 0) + self.watch_player_label.connect( + 'activate-link', + self.on_click_watch_player_label, + ) + + # Watch on website/YouTube + self.watch_web_label = Gtk.Label('', xalign=0) + hbox4.pack_start( + self.watch_web_label, + False, + False, + (self.spacing_size * 2), + ) + self.watch_web_label.connect( + 'activate-link', + self.on_click_watch_web_label, + ) + + # Watch on HookTube + self.watch_hooktube_label = Gtk.Label('', xalign=0) + hbox4.pack_start(self.watch_hooktube_label, False, False, 0) + self.watch_hooktube_label.connect( + 'activate-link', + self.on_click_watch_hooktube_label, + ) + + # Watch on Indvidious + self.watch_invidious_label = Gtk.Label('', xalign=0) + hbox4.pack_start( + self.watch_invidious_label, + False, + False, + (self.spacing_size * 2), + ) + self.watch_invidious_label.connect( + 'activate-link', + self.on_click_watch_invidious_label, + ) + + # Optional rows + + # Fifth row: Temporary... + self.temp_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + spacing=0, + ) + if self.temp_box_is_visible: + self.label_box.pack_start(self.temp_box, True, True, 0) + self.temp_box_visible_flag = True + + self.temp_label = Gtk.Label(_('Temporary:') + ' ', xalign=0) + self.temp_box.pack_start(self.temp_label, False, False, 0) + + # Mark for download + self.temp_mark_label = Gtk.Label('', xalign=0) + self.temp_box.pack_start(self.temp_mark_label, False, False, 0) + self.temp_mark_label.connect( + 'activate-link', + self.on_click_temp_mark_label, + ) + + # Download + self.temp_dl_label = Gtk.Label('', xalign=0) + self.temp_box.pack_start( + self.temp_dl_label, + False, + False, + (self.spacing_size * 2), + ) + self.temp_dl_label.connect( + 'activate-link', + self.on_click_temp_dl_label, + ) + + # Download and watch + self.temp_dl_watch_label = Gtk.Label('', xalign=0) + self.temp_box.pack_start(self.temp_dl_watch_label, False, False, 0) + self.temp_dl_watch_label.connect( + 'activate-link', + self.on_click_temp_dl_watch_label, + ) + + # Sixth row: Marked... + self.marked_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + spacing=0, + ) + if self.marked_box_is_visible: + # (For the sixth row we use .pack_end, so that the fifth row can be + # added and removed, without affecting the visible order) + self.label_box.pack_end(self.marked_box, True, True, 0) + self.marked_box_visible_flag = True + + self.marked_label = Gtk.Label(_('Marked:') + ' ', xalign=0) + self.marked_box.pack_start(self.marked_label, False, False, 0) + + # Archived/not archived + self.marked_archive_label = Gtk.Label('', xalign=0) + self.marked_box.pack_start(self.marked_archive_label, False, False, 0) + self.marked_archive_label.connect( + 'activate-link', + self.on_click_marked_archive_label, + ) + + # Bookmarked/not bookmarked + self.marked_bookmark_label = Gtk.Label('', xalign=0) + self.marked_box.pack_start( + self.marked_bookmark_label, + False, + False, + (self.spacing_size * 2), + ) + self.marked_bookmark_label.connect( + 'activate-link', + self.on_click_marked_bookmark_label, + ) + + # Favourite/not favourite + self.marked_fav_label = Gtk.Label('', xalign=0) + self.marked_box.pack_start(self.marked_fav_label, False, False, 0) + self.marked_fav_label.connect( + 'activate-link', + self.on_click_marked_fav_label, + ) + + # New/not new + self.marked_new_label = Gtk.Label('', xalign=0) + self.marked_box.pack_start( + self.marked_new_label, + False, + False, + (self.spacing_size * 2), + ) + self.marked_new_label.connect( + 'activate-link', + self.on_click_marked_new_label, + ) - """Called from callback in self.setup_progress_tab(). + # In waiting list/not in waiting list + self.marked_waiting_label = Gtk.Label('', xalign=0) + self.marked_box.pack_start(self.marked_waiting_label, False, False, 0) + self.marked_waiting_label.connect( + 'activate-link', + self.on_click_marked_waiting_list_label, + ) - In the Progress Tab, when the user sets the video resolution limit, - inform mainapp.TartubeApp. The new setting is applied to the next - download job. - Args: + def update_widgets(self): - combo (Gtk.ComboBox): The clicked widget + """Called by mainwin.MainWin.video_catalogue_redraw_all(), + .video_catalogue_update_row() and .video_catalogue_insert_item(). + Sets the values displayed by each widget. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11409 on_video_res_combobox_changed') + utils.debug_time('mwn 14354 update_widgets') - tree_iter = self.video_res_combobox.get_active_iter() - model = self.video_res_combobox.get_model() - self.app_obj.set_video_res_default(model[tree_iter][0]) + self.update_background() + self.update_tooltips() + self.update_thumb_image() + self.update_video_name() + self.update_status_images() + self.update_video_descrip() + self.update_video_stats() + self.update_watch_player() + self.update_watch_web() + # If the fifth/sixth rows are not currently visible, but need to be + # visible, make them visible (and vice-versa) + if not self.temp_box_is_visible(): - def on_video_res_checkbutton_changed(self, checkbutton): + if self.temp_box_visible_flag: + self.label_box.remove(self.temp_box) + self.temp_box_visible_flag = False - """Called from callback in self.setup_progress_tab(). + else: - In the Progress Tab, when the user turns the video resolution limit - on/off, inform mainapp.TartubeApp. The new setting is applied to the - next download job. + self.update_temp_labels() + if not self.temp_box_visible_flag: + self.label_box.pack_start(self.temp_box, True, True, 0) + self.temp_box_visible_flag = True - Args: + if not self.marked_box_is_visible(): - checkbutton (Gtk.CheckButton): The clicked widget + if self.marked_box_visible_flag: + self.label_box.remove(self.marked_box) + self.marked_box_visible_flag = False - """ + else: - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11431 on_video_res_checkbutton_changed') + self.update_marked_labels() + if not self.marked_box_visible_flag: + self.label_box.pack_end(self.marked_box, True, True, 0) + self.marked_box_visible_flag = True - self.app_obj.set_video_res_apply_flag( - self.video_res_checkbutton.get_active(), - ) + def update_background(self): - # Set accessors + """Calledy by self.draw_widgets() and .update_widgets(). + Updates the background colour to show which videos are livestreams + (but only when a video's livestream mode has changed). + """ - def add_child_window(self, config_win_obj): + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 14404 update_background') - """Called by config.GenericConfigWin.setup(). + if self.previous_live_mode != self.video_obj.live_mode: - When a configuration window opens, add it to our list of such windows. + self.previous_live_mode = self.video_obj.live_mode - Args: + if self.video_obj.live_mode == 0 \ + or not self.main_win_obj.app_obj.livestream_use_colour_flag: + self.frame.override_background_color( + Gtk.StateType.NORMAL, + None, + ) + elif self.video_obj.live_mode == 1: + self.frame.override_background_color( + Gtk.StateType.NORMAL, + self.main_win_obj.waiting_colour, + ) + elif self.video_obj.live_mode == 2: + self.frame.override_background_color( + Gtk.StateType.NORMAL, + self.main_win_obj.live_colour, + ) - config_win_obj (config.GenericConfigWin): The window to add + def update_tooltips(self): + + """Called by anything, but mainly called by self.update_widgets(). + + Updates the tooltips for the Gtk.Frame that contains everything. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11454 add_child_window') + utils.debug_time('mwn 14436 update_tooltips') - # Check that the window isn't already in the list (unlikely, but check - # anyway) - if config_win_obj in self.config_win_list: - return self.app_obj.system_error( - 247, - 'Callback request denied due to current conditions', + if self.main_win_obj.app_obj.show_tooltips_flag: + self.frame.set_tooltip_text( + self.video_obj.fetch_tooltip_text( + self.main_win_obj.app_obj, + self.main_win_obj.tooltip_max_len, + ), ) - # Update the IV - self.config_win_list.append(config_win_obj) + def update_thumb_image(self): - def del_child_window(self, config_win_obj): + """Called by anything, but mainly called by self.update_widgets(). - """Called by config.GenericConfigWin.close(). + Updates the Gtk.Image widget to display the video's thumbnail, if + available. + """ - When a configuration window closes, remove it to our list of such - windows. + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 14456 update_thumb_image') - Args: + # See if the video's thumbnail file has been downloaded + thumb_flag = False + if self.video_obj.file_name: - config_win_obj (config.GenericConfigWin): The window to remove + # No way to know which image format is used by all websites for + # their video thumbnails, so look for the most common ones + # The True argument means that if the thumbnail isn't found in + # Tartube's main data directory, look in the temporary directory + # too + path = utils.find_thumbnail( + self.main_win_obj.app_obj, + self.video_obj, + True, + ) - """ + if path: - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11482 del_child_window') + # 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, + ), + ) - # Update the IV - # (Don't show an error if the window isn't in the list, as it's - # conceivable this function might be called twice) - if config_win_obj in self.config_win_list: - self.config_win_list.remove(config_win_obj) + # No thumbnail file found, so use a standard icon file + if not thumb_flag: + if self.video_obj.fav_flag and self.video_obj.options_obj: + self.thumb_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['video_both_large'], + ) + elif self.video_obj.fav_flag: + self.thumb_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['video_left_large'], + ) + elif self.video_obj.options_obj: + self.thumb_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['video_right_large'], + ) + else: + self.thumb_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['video_none_large'], + ) - def set_previous_alt_dest_dbid(self, value): + def update_video_name(self): - """Called by functions in SetDestinationDialogue. + """Called by anything, but mainly called by self.update_widgets(). - The specified value may be a .dbid, or None. + Updates the Gtk.Label widget to display the video's current name. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11499 set_previous_alt_dest_dbid') - - self.previous_alt_dest_dbid = value - + utils.debug_time('mwn 14513 update_video_name') -class SimpleCatalogueItem(object): + # For videos whose name is unknown, display the URL, rather than the + # usual '(video with no name)' string + name = self.video_obj.nickname + if name is None \ + or name == self.main_win_obj.app_obj.default_video_name: - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_item(). + if self.video_obj.source is not None: - Python class that handles a single row in the Video Catalogue. + # Using pango markup to display a URL is too risky, so just use + # ordinary text + self.name_label.set_text( + utils.shorten_string( + self.video_obj.source, + self.main_win_obj.quite_long_string_max_len, + ), + ) - Each mainwin.SimpleCatalogueItem objects stores widgets used in that row, - and updates them when required. + return - This class offers a simple view with a minimum of widgets (for example, no - video thumbnails). The mainwin.ComplexCatalogueItem class offers a more - complex view (for example, with video thumbnails). + else: - Args: + # No URL to show, so we're forced to use '(video with no name)' + name = self.main_win_obj.app_obj.default_video_name - main_win_obj (mainwin.MainWin): The main window object + string = '' + if self.video_obj.new_flag: + string += ' font_weight="bold"' - video_obj (media.Video): The media data object itself (always a video) + if self.video_obj.dl_sim_flag: + string += ' style="italic"' - """ + self.name_label.set_markup( + '' + \ + html.escape( + utils.shorten_string( + name, + self.main_win_obj.quite_long_string_max_len, + ), + quote=True, + ) + '' + ) - # Standard class methods + def update_status_images(self): + """Called by anything, but mainly called by self.update_widgets(). - def __init__(self, main_win_obj, video_obj): + Updates the Gtk.Image widgets to display the video's download status, + error and warning settings. + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11533 __init__') + utils.debug_time('mwn 14567 update_status_images') - # IV list - class objects - # ----------------------- - # The main window object - self.main_win_obj = main_win_obj - # The media data object itself (always a video) - self.video_obj = video_obj + # Set the download status + if self.video_obj.live_mode == 1: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['stream_wait_small'], + ) + elif self.video_obj.live_mode == 2: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['stream_live_small'], + ) + elif self.video_obj.dl_flag: + if self.video_obj.archive_flag: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['archived_small'], + ) + else: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['have_file_small'], + ) + else: + self.status_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['no_file_small'], + ) + # Set an indication of any error/warning messages. If there is an error + # but no warning, show the error icon in the warning image (so there + # isn't a large gap in the middle) + if self.video_obj.error_list and self.video_obj.warning_list: - # IV list - Gtk widgets - # --------------------- - self.catalogue_row = None # mainwin.CatalogueRow - self.hbox = None # Gtk.HBox - self.status_image = None # Gtk.Image - self.name_label = None # Gtk.Label - self.parent_label = None # Gtk.Label - self.stats_label = None # Gtk.Label + self.warning_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['warning_small'], + ) + self.error_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['error_small'], + ) - # IV list - other - # --------------- - # Unique ID for this object, matching the .dbid for self.video_obj (an - # integer) - self.dbid = video_obj.dbid - # Size (in pixels) of gaps between various widgets - self.spacing_size = 5 + elif self.video_obj.error_list: + self.warning_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['error_small'], + ) - # Public class methods + self.error_image.clear() + elif self.video_obj.warning_list: - def draw_widgets(self, catalogue_row): + self.warning_image.set_from_pixbuf( + self.main_win_obj.pixbuf_dict['warning_small'], + ) - """Called by mainwin.MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_item(). + self.error_image.clear() - After a Gtk.ListBoxRow has been created for this object, populate it - with widgets. + else: - Args: + self.error_image.clear() + self.warning_image.clear() - catalogue_row (mainwin.CatalogueRow): A wrapper for a - Gtk.ListBoxRow object, storing the media.Video object displayed - in that row. - """ + def update_video_descrip(self): - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11582 draw_widgets') + """Called by anything, but mainly called by self.update_widgets(). - self.catalogue_row = catalogue_row + Updates the Gtk.Label widget to display the video's current + description. + """ - event_box = Gtk.EventBox() - self.catalogue_row.add(event_box) - event_box.connect('button-press-event', self.on_right_click_row) + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 14636 update_video_descrip') - self.hbox = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - event_box.add(self.hbox) - self.hbox.set_border_width(0) + if self.main_win_obj.app_obj.catalogue_mode == 'complex_hide_parent' \ + or self.main_win_obj.app_obj.catalogue_mode \ + == 'complex_hide_parent_ext': - self.status_image = Gtk.Image() - self.hbox.pack_start( - self.status_image, - False, - False, - self.spacing_size, - ) + # Show the first line of the video description, or all of it, + # depending on settings + if self.video_obj.short: - vbox = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, - spacing=0, - ) - self.hbox.pack_start(vbox, True, True, self.spacing_size) + # Work with a list of lines, displaying either the fist line, + # or all of them, as the user clicks the More/Less button + line_list = self.video_obj.descrip.split('\n') - # Video name - self.name_label = Gtk.Label('', xalign = 0) - vbox.pack_start(self.name_label, True, True, 0) + if not self.expand_descrip_flag: - # Parent channel/playlist/folder name (if allowed) - if self.main_win_obj.app_obj.catalogue_mode == 'simple_show_parent': - self.parent_label = Gtk.Label('', xalign = 0) - vbox.pack_start(self.parent_label, True, True, 0) + string = html.escape( + utils.shorten_string( + line_list[0], + self.main_win_obj.very_long_string_max_len, + ), + quote=True, + ) - # Video stats - self.stats_label = Gtk.Label('', xalign=0) - vbox.pack_start(self.stats_label, True, True, 0) + if len(line_list) > 1: + self.descrip_label.set_markup( + '' + _('More') + ' ' + string, + ) + else: + self.descrip_label.set_text(string) + else: - def update_widgets(self): + descrip = html.escape(self.video_obj.descrip, quote=True) - """Called by mainwin.MainWin.video_catalogue_redraw_all(), - .video_catalogue_update_row() and .video_catalogue_insert_item(). + if len(line_list) > 1: + self.descrip_label.set_markup( + '' + _('Less') + ' ' + descrip + '\n', + ) + else: + self.descrip_label.set_text(descrip) - Sets the values displayed by each widget. - """ + else: + self.descrip_label.set_markup('No description set') - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11634 update_widgets') + else: - self.update_tooltips() - self.update_status_image() - self.update_video_name() - self.update_parent_name() - self.update_video_stats() + # Show the name of the parent channel/playlist/folder, optionally + # followed by the whole video description, depending on settings + if isinstance(self.video_obj.parent_obj, media.Channel): + string = _('From channel:') + ' \'' + elif isinstance(self.video_obj.parent_obj, media.Playlist): + string = _('From playlist:') + ' \'' + else: + string = _('From folder:') + ' \'' + string += html.escape( + utils.shorten_string( + self.video_obj.parent_obj.name, + self.main_win_obj.very_long_string_max_len, + ), + quote=True, + ) + '\'' - def update_tooltips(self): + if not self.video_obj.descrip: + self.descrip_label.set_text(string) - """Called by anything, but mainly called by self.update_widgets(). + elif not self.expand_descrip_flag: - Updates the tooltips for the Gtk.HBox that contains everything. - """ + self.descrip_label.set_markup( + '' + _('More') + ' ' + string, + ) - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11651 update_tooltips') + else: - if self.main_win_obj.app_obj.show_tooltips_flag: - self.hbox.set_tooltip_text( - self.video_obj.fetch_tooltip_text( - self.main_win_obj.app_obj, - self.main_win_obj.tooltip_max_len, - ), - ) + descrip = html.escape(self.video_obj.descrip, quote=True) + self.descrip_label.set_markup( + '' + _('Less') + ' ' + descrip + '\n', + ) - def update_status_image(self): + def update_video_stats(self): """Called by anything, but mainly called by self.update_widgets(). - Updates the Gtk.Image widget to display the video's download status. + Updates the Gtk.Label widget to display the video's current side/ + duration/date information. + + For livestreams, instead displays livestream options. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11670 update_status_image') + utils.debug_time('mwn 14736 update_video_stats') + + if not self.video_obj.live_mode: + + if self.video_obj.duration is not None: + string = _('Duration:') + ' ' \ + + utils.convert_seconds_to_string( + self.video_obj.duration, + True, + ) - # Set the download status - if self.video_obj.dl_flag: - if self.video_obj.archive_flag: - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['archived_small'], - ) else: - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['have_file_small'], - ) - else: - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['no_file_small'], + string = _('Duration:') + ' ' + _('unknown') + '' + + size = self.video_obj.get_file_size_string() + if size is not None: + string = string + ' - ' + _('Size:') + ' ' + size + else: + string = string + ' - ' + _('Size:') + ' ' \ + + _('unknown') + '' + + date = self.video_obj.get_upload_date_string( + self.main_win_obj.app_obj.show_pretty_dates_flag, ) + if date is not None: + string = string + ' - ' + _('Date:') + ' ' + date + else: + string = string + ' - ' + _('Date:') + ' ' \ + + _('unknown') + '' - def update_video_name(self): + self.stats_label.set_markup(string) - """Called by anything, but mainly called by self.update_widgets(). + self.live_auto_notify_label.set_text('') + self.live_auto_alarm_label.set_text('') + self.live_auto_open_label.set_text('') + self.live_auto_dl_start_label.set_text('') + self.live_auto_dl_stop_label.set_text('') - Updates the Gtk.Label widget to display the video's current name. - """ + else: - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11696 update_video_name') + name = html.escape(self.video_obj.name) + app_obj = self.main_win_obj.app_obj + dbid = self.video_obj.dbid - # For videos whose name is unknown, display the URL, rather than the - # usual '(video with no name)' string - name = self.video_obj.nickname - if name is None \ - or name == self.main_win_obj.app_obj.default_video_name: + self.stats_label.set_markup(_('Live:') + ' ') - if self.video_obj.source is not None: + if dbid in app_obj.media_reg_auto_notify_dict: + label = '' + _('Notify') + '' + else: + label = _('Notify') - # Using pango markup to display a URL is too risky, so just use - # ordinary text - self.name_label.set_text( - utils.shorten_string( - self.video_obj.source, - self.main_win_obj.very_long_string_max_len, - ), + # Currently disabled on MS Windows + if os.name == 'nt': + self.live_auto_notify_label.set_markup(_('Notify')) + else: + self.live_auto_notify_label.set_markup( + '' + label + '', ) - return + if not mainapp.HAVE_PLAYSOUND_FLAG: + + self.live_auto_alarm_label.set_markup('Alarm') else: - # No URL to show, so we're forced to use '(video with no name)' - name = self.main_win_obj.app_obj.default_video_name + if dbid in app_obj.media_reg_auto_alarm_dict: + label = '' + _('Alarm') + '' + else: + label = _('Alarm') - string = '' - if self.video_obj.new_flag: - string += ' font_weight="bold"' + self.live_auto_alarm_label.set_markup( + '' + label + '', + ) - if self.video_obj.dl_sim_flag: - string += ' style="italic"' + if dbid in app_obj.media_reg_auto_open_dict: + label = '' + _('Open') + '' + else: + label = _('Open') - self.name_label.set_markup( - '' + \ - html.escape( - utils.shorten_string( - name, - self.main_win_obj.very_long_string_max_len, - ), - quote=True, - ) + '' - ) + self.live_auto_open_label.set_markup( + '' + label + '', + ) + + if dbid in app_obj.media_reg_auto_dl_start_dict: + label = '' + _('D/L on start') + '' + else: + label = _('D/L on start') + self.live_auto_dl_start_label.set_markup( + '' + label + '', + ) - def update_parent_name(self): + if dbid in app_obj.media_reg_auto_dl_stop_dict: + label = '' + _('D/L on stop') + '' + else: + label = _('D/L on stop') + + self.live_auto_dl_stop_label.set_markup( + '' + label + '', + ) + + + def update_watch_player(self): """Called by anything, but mainly called by self.update_widgets(). - Updates the Gtk.Label widget to display the name of the parent channel, - playlist or folder. + Updates the clickable Gtk.Label widget for watching the video in an + external media player. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11750 update_parent_name') + utils.debug_time('mwn 14858 update_watch_player') - if self.main_win_obj.app_obj.catalogue_mode != 'simple_show_parent': - return + if self.video_obj.live_mode == 1: - if isinstance(self.video_obj.parent_obj, media.Channel): - string = 'From channel \'' - elif isinstance(self.video_obj.parent_obj, media.Playlist): - string = 'From playlist \'' - else: - string = 'From folder \'' + # Link not clickable + self.watch_player_label.set_markup(_('Download')) - string2 = html.escape( - utils.shorten_string( - self.video_obj.parent_obj.name, - self.main_win_obj.long_string_max_len, - ), - quote=True, - ) + elif self.video_obj.live_mode == 2: + + # Link clickable + self.watch_player_label.set_markup( + '' \ + + _('Download') + '', + ) + + elif self.video_obj.file_name and self.video_obj.dl_flag: + + # Link clickable + self.watch_player_label.set_markup( + '' \ + + _('Player') + '', + ) + + 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: + + translate_note = _( + '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', + ) + + # Link clickable + self.watch_player_label.set_markup( + '' + _('Download & watch') + '', + ) - self.parent_label.set_markup(string + string2 + '\'') + else: + # Link not clickable + self.watch_player_label.set_markup( + '' + _('Not downloaded') + '', + ) - def update_video_stats(self): + + def update_watch_web(self): """Called by anything, but mainly called by self.update_widgets(). - Updates the Gtk.Label widget to display the video's current side/ - duration/date information. + Updates the clickable Gtk.Label widget for watching the video in an + external web browser. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11782 update_video_stats') + utils.debug_time('mwn 14919 update_watch_web') - if self.video_obj.duration is not None: - string = 'Duration: ' + utils.convert_seconds_to_string( - self.video_obj.duration, - True, - ) + if self.video_obj.source: - else: - string = 'Duration: unknown' + # For YouTube URLs, offer alternative links + source = self.video_obj.source + if utils.is_youtube(source): - size = self.video_obj.get_file_size_string() - if size is not None: - string = string + ' - Size: ' + size - else: - string = string + ' - Size: unknown' + # Link clickable + self.watch_web_label.set_markup( + '' \ + + _('YouTube') + '', + ) - date = self.video_obj.get_upload_date_string( - self.main_win_obj.app_obj.show_pretty_dates_flag, - ) + if not self.video_obj.live_mode: + + # Links clickable + self.watch_hooktube_label.set_markup( + '' \ + + _('HookTube') + '', + ) - if date is not None: - string = string + ' - Date: ' + date - else: - string = string + ' - Date: unknown' + self.watch_invidious_label.set_markup( + '' \ + + _('Invidious') + '', + ) - self.stats_label.set_markup(string) + else: + # Links not clickable + self.watch_hooktube_label.set_text('') + self.watch_invidious_label.set_text('') - # Callback methods + else: + + # Link clickable + self.watch_web_label.set_markup( + '' \ + + _('Website') + '', + ) + # Links not clickable + self.watch_hooktube_label.set_text('') + self.watch_invidious_label.set_text('') - def on_right_click_row(self, event_box, event): + else: - """Called from callback in self.draw_widgets(). + # Links not clickable + self.watch_web_label.set_markup('' + _('No link') + '') + self.watch_hooktube_label.set_text('') + self.watch_invidious_label.set_text('') - When the user right-clicks an a row, create a context-sensitive popup - menu. - Args: + def update_livestream_labels(self): - event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the - signal emitted by the click + """Called by anything, but mainly called by self.update_widgets(). + Updates the clickable Gtk.Label widget for video properties. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11829 on_right_click_row') + utils.debug_time('mwn 14992 update_livestream_labels') - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - - self.main_win_obj.video_catalogue_popup_menu(event, self.video_obj) + name = html.escape(self.video_obj.name) + app_obj = self.main_win_obj.app_obj + dbid = self.video_obj.dbid + # Notify/don't notify + if not dbid in app_obj.media_reg_auto_notify_dict: + label = _('Notify') + else: + label = '' + _('Notify') + '' -class ComplexCatalogueItem(object): + # Currently disabled on MS Windows + if os.name == 'nt': + self.live_auto_notify_label.set_markup(_('Notify')) + else: + self.live_auto_notify_label.set_markup( + '' + label + '', + ) - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_item(). + # Sound alarm/don't sound alarm + if not dbid in app_obj.media_reg_auto_alarm_dict: + label = _('Alarm') + else: + label = '' + _('Alarm') + '' - Python class that handles a single row in the Video Catalogue. + self.live_auto_alarm_label.set_markup( + '' + label + '', + ) - Each mainwin.ComplexCatalogueItem objects stores widgets used in that row, - and updates them when required. + # Open/don't open + if not dbid in app_obj.media_reg_auto_open_dict: + label = _('Open') + else: + label = '' + _('Open') + '' - The mainwin.SimpleCatalogueItem class offers a simple view with a minimum - of widgets (for example, no video thumbnails). This class offers a more - complex view (for example, with video thumbnails). + self.live_auto_open_label.set_markup( + '' + label + '', + ) - Args: + # D/L on start/Don't download + if not dbid in app_obj.media_reg_auto_dl_start_dict: + label = _('D/L on start') + else: + label = '' + _('D/L on start') + '' - main_win_obj (mainwin.MainWin): The main window object + self.live_auto_dl_start_label.set_markup( + '' + label + '', + ) - video_obj (media.Video): The media data object itself (always a video) + # D/L on stop/Don't download + if not dbid in app_obj.media_reg_auto_dl_stop_dict: + label = _('D/L on stop') + else: + label = '' + _('D/L on stop') + '' - """ + self.live_auto_dl_stop_label.set_markup( + '' + label + '', + ) - # Standard class methods + def update_temp_labels(self): + """Called by anything, but mainly called by self.update_widgets(). - def __init__(self, main_win_obj, video_obj): + Updates the clickable Gtk.Label widget for temporary video downloads. + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11865 __init__') - - # IV list - class objects - # ----------------------- - # The main window object - self.main_win_obj = main_win_obj - # The media data object itself (always a video) - self.video_obj = video_obj - - - # IV list - Gtk widgets - # --------------------- - self.catalogue_row = None # mainwin.CatalogueRow - self.frame = None # Gtk.Frame - self.thumb_image = None # Gtk.Image - self.name_label = None # Gtk.Label - self.status_image = None # Gtk.Image - self.error_image = None # Gtk.Image - self.warning_image = None # Gtk.Image - self.descrip_label = None # Gtk.Label - self.expand_label = None # Gtk.Label - self.stats_label = None # Gtk.Label - self.watch_label = None # Gtk.Label - self.watch_player_label = None # Gtk.Label - self.watch_web_label = None # Gtk.Label - self.watch_hooktube_label = None # Gtk.Label - self.watch_invidious_label = None # Gtk.Label - self.temp_label = None # Gtk.Label - self.temp_mark_label = None # Gtk.Label - self.temp_dl_label = None # Gtk.Label - self.temp_dl_watch_label = None # Gtk.Label - self.marked_label = None # Gtk.Label - self.marked_archive_label = None # Gtk.Label - self.marked_bookmark_label = None # Gtk.Label - self.marked_fav_label = None # Gtk.Label - self.marked_new_label = None # Gtk.Label - self.marked_playlist_label = None # Gtk.Label + utils.debug_time('mwn 15071 update_temp_labels') + if self.video_obj.file_name: + link_text = self.video_obj.get_actual_path( + self.main_win_obj.app_obj, + ) + elif self.video_obj.source: + link_text = self.video_obj.source + else: + link_text = '' - # IV list - other - # --------------- - # Unique ID for this object, matching the .dbid for self.video_obj (an - # integer) - self.dbid = video_obj.dbid - # Size (in pixels) of gaps between various widgets - self.spacing_size = 5 - # The state of the More/Less label. False if the video's short - # description (or no description at all) is visible, True if the - # video's full description is visible - self.expand_descrip_flag = False - # Flag set to True if the video's parent folder is a temporary folder, - # meaning that some widgets don't need to be drawn at all - self.no_temp_widgets_flag = False + # (Video can't be temporarily downloaded if it has no source URL) + if self.video_obj.source is not None: + self.temp_mark_label.set_markup( + '' + _('Mark for download') + '', + ) - # Public class methods + self.temp_dl_label.set_markup( + '' + _('Download') + '', + ) + self.temp_dl_watch_label.set_markup( + '' + _('D/L and watch') + '', + ) - def draw_widgets(self, catalogue_row): + else: - """Called by mainwin.MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_item(). + self.temp_mark_label.set_text(_('Mark for download')) + self.temp_dl_label.set_text(_('Download')) + self.temp_dl_watch_label.set_text(_('D/L and watch')) - After a Gtk.ListBoxRow has been created for this object, populate it - with widgets. - Args: + def update_marked_labels(self): - catalogue_row (mainwin.CatalogueRow): A wrapper for a - Gtk.ListBoxRow object, storing the media.Video object displayed - in that row. + """Called by anything, but mainly called by self.update_widgets(). + Updates the clickable Gtk.Label widget for video properties. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11940 draw_widgets') + utils.debug_time('mwn 15118 update_marked_labels') - # If the video's parent folder is a temporary folder, then we don't - # need one row of widgets at all - parent_obj = self.video_obj.parent_obj - if isinstance(parent_obj, media.Folder) \ - and parent_obj.temp_flag: - self.no_temp_widgets_flag = True + if self.video_obj.file_name: + link_text = self.video_obj.get_actual_path( + self.main_win_obj.app_obj, + ) + elif self.video_obj.source: + link_text = self.video_obj.source else: - self.no_temp_widgets_flag = False - - # Draw the widgets - self.catalogue_row = catalogue_row - - event_box = Gtk.EventBox() - self.catalogue_row.add(event_box) - event_box.connect('button-press-event', self.on_right_click_row) - - self.frame = Gtk.Frame() - event_box.add(self.frame) - self.frame.set_border_width(self.spacing_size) - - hbox = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - self.frame.add(hbox) - hbox.set_border_width(self.spacing_size) + link_text = '' - # The thumbnail is in its own vbox, so we can keep it in the top-left - # when the video's description has multiple lines - vbox = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, - spacing=0, - ) - hbox.pack_start(vbox, False, False, 0) + # Archived/not archived + text = '' - self.thumb_image = Gtk.Image() - vbox.pack_start(self.thumb_image, False, False, 0) + if not self.video_obj.archive_flag: + self.marked_archive_label.set_markup( + text + _('Archived') + '', + ) + else: + self.marked_archive_label.set_markup( + text + '' + _('Archived') + '', + ) - # Everything to the right of the thumbnail is in vbox2 - vbox2 = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, - spacing=0, - ) - hbox.pack_start(vbox2, True, True, self.spacing_size) + # Bookmarked/not bookmarked + text = '' - # First row - video name - hbox2 = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - vbox2.pack_start(hbox2, True, True, 0) + if not self.video_obj.bookmark_flag: + self.marked_bookmark_label.set_markup( + text + _('Bookmarked') + '', + ) + else: + self.marked_bookmark_label.set_markup( + text + '' + _('Bookmarked') + '', + ) - self.name_label = Gtk.Label('', xalign = 0) - hbox2.pack_start(self.name_label, True, True, 0) + # Favourite/not favourite + text = '' - # Status/error/warning icons - self.status_image = Gtk.Image() - hbox2.pack_end(self.status_image, False, False, 0) + if not self.video_obj.fav_flag: + self.marked_fav_label.set_markup( + text + _('Favourite') + '', + ) + else: + self.marked_fav_label.set_markup( + text + '' + _('Favourite') + '') - self.warning_image = Gtk.Image() - hbox2.pack_end(self.warning_image, False, False, self.spacing_size) + # New/not new + text = '' - self.error_image = Gtk.Image() - hbox2.pack_end(self.error_image, False, False, self.spacing_size) + if not self.video_obj.new_flag: + self.marked_new_label.set_markup( + text + _('New') + '', + ) + else: + self.marked_new_label.set_markup( + text + '' + _('New') + '', + ) - # Second row - video description (incorporating the the More/Less - # label), or the name of the parent channel/playlist/folder, - # depending on settings - self.descrip_label = Gtk.Label('', xalign=0) - vbox2.pack_start(self.descrip_label, True, True, 0) - self.descrip_label.connect( - 'activate-link', - self.on_click_descrip_label, - ) + # In waiting list/not in waiting list + text = '' + if not self.video_obj.waiting_flag: + self.marked_waiting_label.set_markup( + text + _('In waiting list') + '', + ) + else: + self.marked_waiting_label.set_markup( + text + '' + _('In Waiting list') + '', + ) - # Third row - video stats - self.stats_label = Gtk.Label('', xalign=0) - vbox2.pack_start(self.stats_label, True, True, 0) - # Fourth row - Watch... - hbox3 = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - vbox2.pack_start(hbox3, True, True, 0) + def temp_box_is_visible(self): - self.watch_label = Gtk.Label('Watch: ', xalign=0) - hbox3.pack_start(self.watch_label, False, False, 0) + """Called by self.draw_widgets and .update_widgets(). - # Watch in player - self.watch_player_label = Gtk.Label('', xalign=0) - hbox3.pack_start(self.watch_player_label, False, False, 0) - self.watch_player_label.connect( - 'activate-link', - self.on_click_watch_player_label, - ) + Checks whether the fifth row of labels (for temporary actions) should + be visible, or not. - # Watch on website/YouTube - self.watch_web_label = Gtk.Label('', xalign=0) - hbox3.pack_start( - self.watch_web_label, - False, - False, - (self.spacing_size * 2), - ) - self.watch_web_label.connect( - 'activate-link', - self.on_click_watch_web_label, - ) + Return values: - # Watch on HookTube - self.watch_hooktube_label = Gtk.Label('', xalign=0) - hbox3.pack_start(self.watch_hooktube_label, False, False, 0) - self.watch_hooktube_label.connect( - 'activate-link', - self.on_click_watch_hooktube_label, - ) + True if the row should be visible, False if not - # Watch on Indvidious - self.watch_invidious_label = Gtk.Label('', xalign=0) - hbox3.pack_start( - self.watch_invidious_label, - False, - False, - (self.spacing_size * 2), - ) - self.watch_invidious_label.connect( - 'activate-link', - self.on_click_watch_invidious_label, - ) + """ - # Optional rows + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 15207 temp_box_is_visible') - # Fifth row: Temporary... if ( self.main_win_obj.app_obj.catalogue_mode \ == 'complex_hide_parent_ext' \ or self.main_win_obj.app_obj.catalogue_mode \ == 'complex_show_parent_ext' - ) and not self.no_temp_widgets_flag: + ) and not self.no_temp_widgets_flag \ + and not self.video_obj.live_mode: + return True + else: + return False - hbox4 = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - vbox2.pack_start(hbox4, True, True, 0) - self.temp_label = Gtk.Label('Temporary: ', xalign=0) - hbox4.pack_start(self.temp_label, False, False, 0) + def marked_box_is_visible(self): - # Mark for download - self.temp_mark_label = Gtk.Label('', xalign=0) - hbox4.pack_start(self.temp_mark_label, False, False, 0) - self.temp_mark_label.connect( - 'activate-link', - self.on_click_temp_mark_label, - ) + """Called by self.draw_widgets and .update_widgets(). - # Download - self.temp_dl_label = Gtk.Label('', xalign=0) - hbox4.pack_start( - self.temp_dl_label, - False, - False, - (self.spacing_size * 2), - ) - self.temp_dl_label.connect( - 'activate-link', - self.on_click_temp_dl_label, - ) + Checks whether the sixth row of labels (for marked video actions) + should be visible, or not. - # Download and watch - self.temp_dl_watch_label = Gtk.Label('', xalign=0) - hbox4.pack_start(self.temp_dl_watch_label, False, False, 0) - self.temp_dl_watch_label.connect( - 'activate-link', - self.on_click_temp_dl_watch_label, - ) + Return values: + + True if the row should be visible, False if not + + """ + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 15235 marked_box_is_visible') - # Sixth row: Marked... if ( self.main_win_obj.app_obj.catalogue_mode \ == 'complex_hide_parent_ext' \ or self.main_win_obj.app_obj.catalogue_mode \ == 'complex_show_parent_ext' - ): - hbox5 = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - vbox2.pack_start(hbox5, True, True, 0) + ) and not self.video_obj.live_mode: + return True + else: + return False - self.marked_label = Gtk.Label('Marked: ', xalign=0) - hbox5.pack_start(self.marked_label, False, False, 0) - # Archived/not archived - self.marked_archive_label = Gtk.Label('', xalign=0) - hbox5.pack_start(self.marked_archive_label, False, False, 0) - self.marked_archive_label.connect( - 'activate-link', - self.on_click_marked_archive_label, - ) + # Callback methods - # Bookmarked/not bookmarked - self.marked_bookmark_label = Gtk.Label('', xalign=0) - hbox5.pack_start( - self.marked_bookmark_label, - False, - False, - (self.spacing_size * 2), - ) - self.marked_bookmark_label.connect( - 'activate-link', - self.on_click_marked_bookmark_label, - ) - # Favourite/not favourite - self.marked_fav_label = Gtk.Label('', xalign=0) - hbox5.pack_start(self.marked_fav_label, False, False, 0) - self.marked_fav_label.connect( - 'activate-link', - self.on_click_marked_fav_label, - ) + def on_click_descrip_label(self, label, uri): - # New/not new - self.marked_new_label = Gtk.Label('', xalign=0) - hbox5.pack_start( - self.marked_new_label, - False, - False, - (self.spacing_size * 2), - ) - self.marked_new_label.connect( - 'activate-link', - self.on_click_marked_new_label, - ) + """Called from callback in self.draw_widgets(). - # In waiting list/not in waiting list - self.marked_playlist_label = Gtk.Label('', xalign=0) - hbox5.pack_start(self.marked_playlist_label, False, False, 0) - self.marked_playlist_label.connect( - 'activate-link', - self.on_click_marked_waiting_list_label, - ) + When the user clicks on the More/Less label, show more or less of the + video's description. + Args: - def update_widgets(self): + label (Gtk.Label): The clicked widget - """Called by mainwin.MainWin.video_catalogue_redraw_all(), - .video_catalogue_update_row() and .video_catalogue_insert_item(). + uri (str): Ignored - Sets the values displayed by each widget. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12197 update_widgets') + utils.debug_time('mwn 15267 on_click_descrip_label') - self.update_tooltips() - self.update_thumb_image() - self.update_video_name() - self.update_status_images() - self.update_video_descrip() - self.update_video_stats() - self.update_watch_player() - self.update_watch_web() + if not self.expand_descrip_flag: + self.expand_descrip_flag = True + else: + self.expand_descrip_flag = False - if ( - self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_hide_parent_ext' \ - or self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_show_parent_ext' - ) and not self.no_temp_widgets_flag: - self.update_temp_labels() + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.descrip_label.set_text('') + GObject.timeout_add(0, self.update_video_descrip) - if ( - self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_hide_parent_ext' \ - or self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_show_parent_ext' - ): - self.update_marked_labels() + def on_click_live_auto_alarm_label(self, label, uri): - def update_tooltips(self): + """Called from callback in self.draw_widgets(). - """Called by anything, but mainly called by self.update_widgets(). + Toggles auto-sounding alarms when a livestream starts. + + Args: + + label (Gtk.Label): The clicked widget + + uri (str): Ignored + + Returns: + + True to show the action has been handled - Updates the tooltips for the Gtk.Frame that contains everything. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12233 update_tooltips') + utils.debug_time('mwn 15301 on_click_live_auto_alarm_label') - if self.main_win_obj.app_obj.show_tooltips_flag: - self.frame.set_tooltip_text( - self.video_obj.fetch_tooltip_text( - self.main_win_obj.app_obj, - self.main_win_obj.tooltip_max_len, - ), - ) + # Toggle the setting + if not self.video_obj.dbid \ + in self.main_win_obj.app_obj.media_reg_auto_alarm_dict: + self.main_win_obj.app_obj.add_auto_alarm_dict(self.video_obj) + label = _('Undo alarm') - def update_thumb_image(self): + else: - """Called by anything, but mainly called by self.update_widgets(). + self.main_win_obj.app_obj.del_auto_alarm_dict(self.video_obj) + label = _('Alarm') + + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.live_auto_alarm_label.set_markup(label) + + GObject.timeout_add(0, self.update_livestream_labels) + + return True + + + def on_click_live_auto_dl_start_label(self, label, uri): + + """Called from callback in self.draw_widgets(). + + Toggles auto-downloading the video when a livestream starts. + + Args: + + label (Gtk.Label): The clicked widget + + uri (str): Ignored + + Returns: + + True to show the action has been handled - Updates the Gtk.Image widget to display the video's thumbnail, if - available. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12253 update_thumb_image') + utils.debug_time('mwn 15345 on_click_live_auto_dl_start_label') - # See if the video's thumbnail file has been downloaded - thumb_flag = False - if self.video_obj.file_name: + # Toggle the setting + if not self.video_obj.dbid \ + in self.main_win_obj.app_obj.media_reg_auto_dl_start_dict: - # No way to know which image format is used by all websites for - # their video thumbnails, so look for the most common ones - # The True argument means that if the thumbnail isn't found in - # Tartube's main data directory, look in the temporary directory - # too - path = utils.find_thumbnail( - self.main_win_obj.app_obj, - self.video_obj, - True, - ) + self.main_win_obj.app_obj.add_auto_dl_start_dict(self.video_obj) + label = _('Don\'t D/L') - if path: + else: - # 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, - ), - ) + self.main_win_obj.app_obj.del_auto_dl_start_dict(self.video_obj) + label = _('D/L on start') - # No thumbnail file found, so use a standard icon file - if not thumb_flag: - if self.video_obj.fav_flag and self.video_obj.options_obj: - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['video_both_large'], - ) - elif self.video_obj.fav_flag: - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['video_left_large'], - ) - elif self.video_obj.options_obj: - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['video_right_large'], - ) - else: - self.thumb_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['video_none_large'], - ) + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.live_auto_dl_start_label.set_markup(label) + GObject.timeout_add(0, self.update_livestream_labels) - def update_video_name(self): + return True - """Called by anything, but mainly called by self.update_widgets(). - Updates the Gtk.Label widget to display the video's current name. + def on_click_live_auto_dl_stop_label(self, label, uri): + + + """Called from callback in self.draw_widgets(). + + Toggles auto-downloading the video when a livestream stops. + + Args: + + label (Gtk.Label): The clicked widget + + uri (str): Ignored + + Returns: + + True to show the action has been handled + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12310 update_video_name') + utils.debug_time('mwn 15390 on_click_live_auto_dl_stop_label') - # For videos whose name is unknown, display the URL, rather than the - # usual '(video with no name)' string - name = self.video_obj.nickname - if name is None \ - or name == self.main_win_obj.app_obj.default_video_name: + # Toggle the setting + if not self.video_obj.dbid \ + in self.main_win_obj.app_obj.media_reg_auto_dl_stop_dict: - if self.video_obj.source is not None: + self.main_win_obj.app_obj.add_auto_dl_stop_dict(self.video_obj) + label = _('Don\'t D/L') - # Using pango markup to display a URL is too risky, so just use - # ordinary text - self.name_label.set_text( - utils.shorten_string( - self.video_obj.source, - self.main_win_obj.quite_long_string_max_len, - ), - ) + else: + + self.main_win_obj.app_obj.del_auto_dl_stop_dict(self.video_obj) + label = _('D/L on stop') + + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.live_auto_dl_stop_label.set_markup(label) - return + GObject.timeout_add(0, self.update_livestream_labels) - else: + return True - # No URL to show, so we're forced to use '(video with no name)' - name = self.main_win_obj.app_obj.default_video_name - string = '' - if self.video_obj.new_flag: - string += ' font_weight="bold"' + def on_click_live_auto_notify_label(self, label, uri): - if self.video_obj.dl_sim_flag: - string += ' style="italic"' + """Called from callback in self.draw_widgets(). - self.name_label.set_markup( - '' + \ - html.escape( - utils.shorten_string( - name, - self.main_win_obj.quite_long_string_max_len, - ), - quote=True, - ) + '' - ) + Toggles auto-notification when a livestream starts. + Args: - def update_status_images(self): + label (Gtk.Label): The clicked widget - """Called by anything, but mainly called by self.update_widgets(). + uri (str): Ignored + + Returns: + + True to show the action has been handled - Updates the Gtk.Image widgets to display the video's download status, - error and warning settings. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12364 update_status_images') + utils.debug_time('mwn 15434 on_click_live_auto_notify_label') - # Set the download status - if self.video_obj.dl_flag: - if self.video_obj.archive_flag: - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['archived_small'], - ) - else: - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['have_file_small'], - ) - else: - self.status_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['no_file_small'], - ) + # Toggle the setting + if not self.video_obj.dbid \ + in self.main_win_obj.app_obj.media_reg_auto_notify_dict: - # Set an indication of any error/warning messages. If there is an error - # but no warning, show the error icon in the warning image (so there - # isn't a large gap in the middle) - if self.video_obj.error_list and self.video_obj.warning_list: + self.main_win_obj.app_obj.add_auto_notify_dict(self.video_obj) + label = _('Undo notify') - self.warning_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['warning_small'], - ) + else: - self.error_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['error_small'], - ) + self.main_win_obj.app_obj.del_auto_notify_dict(self.video_obj) + label = _('Notify') - elif self.video_obj.error_list: + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.live_auto_notify_label.set_markup(label) - self.warning_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['error_small'], - ) + GObject.timeout_add(0, self.update_livestream_labels) - self.error_image.clear() + return True - elif self.video_obj.warning_list: - self.warning_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['warning_small'], - ) + def on_click_live_auto_open_label(self, label, uri): - self.error_image.clear() + """Called from callback in self.draw_widgets(). - else: + Toggles auto-opening the video in the system's web browser when a + livestream starts. - self.error_image.clear() - self.warning_image.clear() + Args: + label (Gtk.Label): The clicked widget - def update_video_descrip(self): + uri (str): Ignored - """Called by anything, but mainly called by self.update_widgets(). + Returns: + + True to show the action has been handled - Updates the Gtk.Label widget to display the video's current - description. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12425 update_video_descrip') - - if self.main_win_obj.app_obj.catalogue_mode == 'complex_hide_parent' \ - or self.main_win_obj.app_obj.catalogue_mode \ - == 'complex_hide_parent_ext': + utils.debug_time('mwn 15479 on_click_live_auto_open_label') - # Show the first line of the video description, or all of it, - # depending on settings - if self.video_obj.short: + # Toggle the setting + if not self.video_obj.dbid \ + in self.main_win_obj.app_obj.media_reg_auto_open_dict: - # Work with a list of lines, displaying either the fist line, - # or all of them, as the user clicks the More/Less button - line_list = self.video_obj.descrip.split('\n') + self.main_win_obj.app_obj.add_auto_open_dict(self.video_obj) + label = _('Undo open') - if not self.expand_descrip_flag: + else: - string = html.escape( - utils.shorten_string( - line_list[0], - self.main_win_obj.very_long_string_max_len, - ), - quote=True, - ) + self.main_win_obj.app_obj.del_auto_open_dict(self.video_obj) + label = _('Open') - if len(line_list) > 1: - self.descrip_label.set_markup( - 'More ' + string, - ) - else: - self.descrip_label.set_text(string) + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.live_auto_open_label.set_markup(label) - else: + GObject.timeout_add(0, self.update_livestream_labels) - descrip = html.escape(self.video_obj.descrip, quote=True) + return True - if len(line_list) > 1: - self.descrip_label.set_markup( - 'Less ' + descrip + '\n', - ) - else: - self.descrip_label.set_text(descrip) - else: - self.descrip_label.set_markup('No description set') + def on_click_marked_archive_label(self, label, uri): - else: + """Called from callback in self.draw_widgets(). - # Show the name of the parent channel/playlist/folder, optionally - # followed by the whole video description, depending on settings - if isinstance(self.video_obj.parent_obj, media.Channel): - string = 'From channel \'' - elif isinstance(self.video_obj.parent_obj, media.Playlist): - string = 'From playlist \'' - else: - string = 'From folder \'' + Mark the video as archived or not archived. - string += html.escape( - utils.shorten_string( - self.video_obj.parent_obj.name, - self.main_win_obj.very_long_string_max_len, - ), - quote=True, - ) + '\'' + Args: - if not self.video_obj.descrip: - self.descrip_label.set_text(string) + label (Gtk.Label): The clicked widget - elif not self.expand_descrip_flag: + uri (str): Ignored - self.descrip_label.set_markup( - 'More ' + string, - ) + Returns: - else: + True to show the action has been handled - descrip = html.escape(self.video_obj.descrip, quote=True) - self.descrip_label.set_markup( - 'Less ' + string + '\n' + descrip \ - + '\n', - ) + """ + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 15523 on_click_marked_archive_label') - def update_video_stats(self): + # Mark the video as archived/not archived + if not self.video_obj.archive_flag: + self.video_obj.set_archive_flag(True) + else: + self.video_obj.set_archive_flag(False) - """Called by anything, but mainly called by self.update_widgets(). + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.marked_archive_label.set_markup(_('Archived')) - Updates the Gtk.Label widget to display the video's current side/ - duration/date information. - """ + GObject.timeout_add(0, self.update_marked_labels) - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12516 update_video_stats') + return True - if self.video_obj.duration is not None: - string = 'Duration: ' + utils.convert_seconds_to_string( - self.video_obj.duration, - True, - ) - else: - string = 'Duration: unknown' + def on_click_marked_bookmark_label(self, label, uri): - size = self.video_obj.get_file_size_string() - if size is not None: - string = string + ' - Size: ' + size - else: - string = string + ' - Size: unknown' + """Called from callback in self.draw_widgets(). - date = self.video_obj.get_upload_date_string( - self.main_win_obj.app_obj.show_pretty_dates_flag, - ) + Mark the video as bookmarked or not bookmarked. - if date is not None: - string = string + ' - Date: ' + date - else: - string = string + ' - Date: unknown' + Args: - self.stats_label.set_markup(string) + label (Gtk.Label): The clicked widget + uri (str): Ignored - def update_watch_player(self): + Returns: - """Called by anything, but mainly called by self.update_widgets(). + True to show the action has been handled - Updates the clickable Gtk.Label widget for watching the video in an - external media player. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12554 update_watch_player') + utils.debug_time('mwn 15561 on_click_marked_bookmark_label') - if self.video_obj.file_name and self.video_obj.dl_flag: + # Mark the video as bookmarked/not bookmarked + if not self.video_obj.bookmark_flag: + self.main_win_obj.app_obj.mark_video_bookmark( + self.video_obj, + True, + ) - # Link clickable - self.watch_player_label.set_markup( - 'Player', + else: + self.main_win_obj.app_obj.mark_video_bookmark( + self.video_obj, + False, ) - 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: + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.marked_bookmark_label.set_markup(_('Not bookmarked')) - # Link clickable - self.watch_player_label.set_markup( - 'Download & watch', - ) + GObject.timeout_add(0, self.update_marked_labels) - else: + return True - # Link not clickable - self.watch_player_label.set_markup('Not downloaded') + def on_click_marked_fav_label(self, label, uri): - def update_watch_web(self): + """Called from callback in self.draw_widgets(). - """Called by anything, but mainly called by self.update_widgets(). + Mark the video as favourite or not favourite. + + Args: + + label (Gtk.Label): The clicked widget + + uri (str): Ignored + + Returns: + + True to show the action has been handled - Updates the clickable Gtk.Label widget for watching the video in an - external web browser. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12591 update_watch_web') + utils.debug_time('mwn 15606 on_click_marked_fav_label') - if self.video_obj.source: + # Mark the video as favourite/not favourite + if not self.video_obj.fav_flag: + self.main_win_obj.app_obj.mark_video_favourite( + self.video_obj, + True, + ) - # For YouTube URLs, offer alternative links - source = self.video_obj.source - if utils.is_youtube(source): + else: + self.main_win_obj.app_obj.mark_video_favourite( + self.video_obj, + False, + ) + + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.marked_fav_label.set_markup(_('Favourite')) - # Links clickable - self.watch_web_label.set_markup( - 'YouTube', - ) + GObject.timeout_add(0, self.update_marked_labels) - self.watch_hooktube_label.set_markup( - 'HookTube', - ) + return True - self.watch_invidious_label.set_markup( - 'Invidious', - ) - else: + def on_click_marked_new_label(self, label, uri): - self.watch_web_label.set_markup( - 'Website', - ) + """Called from callback in self.draw_widgets(). - self.watch_hooktube_label.set_text('') - self.watch_invidious_label.set_text('') + Mark the video as new or not new. - else: + Args: - # Link not clickable - self.watch_web_label.set_markup('No weblink') - self.watch_hooktube_label.set_text('') - self.watch_invidious_label.set_text('') + label (Gtk.Label): The clicked widget + uri (str): Ignored - def update_temp_labels(self): + Returns: - """Called by anything, but mainly called by self.update_widgets(). + True to show the action has been handled - Updates the clickable Gtk.Label widget for temporary video downloads. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12651 update_temp_labels') + utils.debug_time('mwn 15651 on_click_marked_new_label') - if self.video_obj.file_name: - link_text = self.video_obj.get_actual_path( - self.main_win_obj.app_obj, - ) - elif self.video_obj.source: - link_text = self.video_obj.source + # Mark the video as new/not new + if not self.video_obj.new_flag: + self.main_win_obj.app_obj.mark_video_new(self.video_obj, True) else: - link_text = '' + self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # (Video can't be temporarily downloaded if it has no source URL) - if self.video_obj.source is not None: + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.marked_new_label.set_markup(_('New')) - self.temp_mark_label.set_markup( - 'Mark for download', - ) + GObject.timeout_add(0, self.update_marked_labels) - self.temp_dl_label.set_markup( - 'Download', - ) + return True - self.temp_dl_watch_label.set_markup( - 'D/L and watch', - ) - else: + def on_click_marked_waiting_list_label(self, label, uri): - self.temp_mark_label.set_text('Mark for download') - self.temp_dl_label.set_text('Download') - self.temp_dl_watch_label.set_text('D/L and watch') + """Called from callback in self.draw_widgets(). + Mark the video as in the waiting list or not in the waiting list. - def update_marked_labels(self): + Args: - """Called by anything, but mainly called by self.update_widgets(). + label (Gtk.Label): The clicked widget + + uri (str): Ignored + + Returns: + + True to show the action has been handled - Updates the clickable Gtk.Label widget for video properties. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12693 update_marked_labels') + utils.debug_time('mwn 15689 on_click_marked_waiting_list_label') - if self.video_obj.file_name: - link_text = self.video_obj.get_actual_path( - self.main_win_obj.app_obj, + # Mark the video as in waiting list/not in waiting list + if not self.video_obj.waiting_flag: + self.main_win_obj.app_obj.mark_video_waiting( + self.video_obj, + True, ) - elif self.video_obj.source: - link_text = self.video_obj.source - else: - link_text = '' - - # Archived/not archived - if not self.video_obj.archive_flag: - self.marked_archive_label.set_markup( - 'Archived', + else: + self.main_win_obj.app_obj.mark_video_waiting( + self.video_obj, + False, ) - else: + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.marked_waiting_label.set_markup(_('Not in waiting list')) - self.marked_archive_label.set_markup( - 'Archived', - ) + GObject.timeout_add(0, self.update_marked_labels) - # Bookmarked/not bookmarked - if not self.video_obj.bookmark_flag: + return True - self.marked_bookmark_label.set_markup( - 'Bookmarked', - ) - else: + def on_click_temp_dl_label(self, label, uri): - self.marked_bookmark_label.set_markup( - 'Bookmarked', - ) + """Called from callback in self.draw_widgets(). - # Favourite/not favourite - if not self.video_obj.fav_flag: + Download the video into the 'Temporary Videos' folder. - self.marked_fav_label.set_markup( - 'Favourite', - ) + Args: - else: + label (Gtk.Label): The clicked widget - self.marked_fav_label.set_markup( - 'Favourite', - ) + uri (str): Ignored - # New/not new - if not self.video_obj.new_flag: + Returns: - self.marked_new_label.set_markup( - 'New', - ) + True to show the action has been handled - else: + """ - self.marked_new_label.set_markup( - 'New', - ) + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 15734 on_click_temp_dl_label') - # In waiting list/not in waiting list - if not self.video_obj.waiting_flag: + # Can't download the video if an update/refresh/tidy 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: - self.marked_playlist_label.set_markup( - 'In waiting list', + # Create a new media.Video object in the 'Temporary Videos' folder + new_media_data_obj = self.main_win_obj.app_obj.add_video( + self.main_win_obj.app_obj.fixed_temp_folder, + self.video_obj.source, ) - else: + if new_media_data_obj: - self.marked_playlist_label.set_markup( - 'In waiting list', - ) + # Download the video. If a download operation is already in + # progress, the video is added to it + # Optionally open the video in the system's default media + # player + self.main_win_obj.app_obj.download_watch_videos( + [new_media_data_obj], + False, + ) + # Because of an unexplained Gtk problem, there is usually a crash after + # 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) + self.temp_dl_label.set_markup(_('Download')) + GObject.timeout_add(0, self.update_temp_labels) - # Callback methods + return True - def on_click_descrip_label(self, label, uri): + def on_click_temp_dl_watch_label(self, label, uri): """Called from callback in self.draw_widgets(). - When the user clicks on the More/Less label, show more or less of the - video's description. + Download the video into the 'Temporary Videos' folder. Args: @@ -12798,29 +15779,53 @@ def on_click_descrip_label(self, label, uri): uri (str): Ignored + Returns: + + True to show the action has been handled + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12792 on_click_descrip_label') + utils.debug_time('mwn 15789 on_click_temp_dl_watch_label') - if not self.expand_descrip_flag: - self.expand_descrip_flag = True - else: - self.expand_descrip_flag = False + # Can't download the video if an update/refresh/tidy 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: + + # Create a new media.Video object in the 'Temporary Videos' folder + new_media_data_obj = self.main_win_obj.app_obj.add_video( + self.main_win_obj.app_obj.fixed_temp_folder, + self.video_obj.source, + ) + + if new_media_data_obj: + + # Download the video. If a download operation is already in + # progress, the video is added to it + # Optionally open the video in the system's default media + # player + self.main_win_obj.app_obj.download_watch_videos( + [new_media_data_obj], + True, + ) # Because of an unexplained Gtk problem, there is usually a crash after # 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) - self.descrip_label.set_markup('') - GObject.timeout_add(0, self.update_video_descrip) + self.temp_dl_watch_label.set_markup(_('D/L and watch')) + GObject.timeout_add(0, self.update_temp_labels) + return True - def on_click_marked_archive_label(self, label, uri): + + def on_click_temp_mark_label(self, label, uri): """Called from callback in self.draw_widgets(). - Mark the video as archived or not archived. + Mark the video for download into the 'Temporary Videos' folder. Args: @@ -12835,30 +15840,35 @@ def on_click_marked_archive_label(self, label, uri): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12826 on_click_marked_archive_label') + utils.debug_time('mwn 15843 on_click_temp_mark_label') - # Mark the video as archived/not archived - if not self.video_obj.archive_flag: - self.video_obj.set_archive_flag(True) - else: - self.video_obj.set_archive_flag(False) + # Can't mark the video for download if an update/refresh/tidy 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: + + # Create a new media.Video object in the 'Temporary Videos' folder + new_media_data_obj = self.main_win_obj.app_obj.add_video( + self.main_win_obj.app_obj.fixed_temp_folder, + self.video_obj.source, + ) # Because of an unexplained Gtk problem, there is usually a crash after # 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) - self.marked_archive_label.set_markup('Archived') - - GObject.timeout_add(0, self.update_marked_labels) + self.temp_mark_label.set_markup(_('Mark for download')) + GObject.timeout_add(0, self.update_temp_labels) return True - def on_click_marked_bookmark_label(self, label, uri): + def on_click_watch_hooktube_label(self, label, uri): """Called from callback in self.draw_widgets(). - Mark the video as bookmarked or not bookmarked. + Watch a YouTube video on HookTube. Args: @@ -12873,17 +15883,17 @@ def on_click_marked_bookmark_label(self, label, uri): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12964 on_click_marked_bookmark_label') + utils.debug_time('mwn 15886 on_click_watch_hooktube_label') - # Mark the video as bookmarked/not bookmarked - if not self.video_obj.bookmark_flag: - self.main_win_obj.app_obj.mark_video_bookmark( - self.video_obj, - True, - ) + # Launch the video + utils.open_file(uri) - else: - self.main_win_obj.app_obj.mark_video_bookmark( + # Mark the video as not new (having been watched) + if self.video_obj.new_flag: + self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) + # Remove the video from the waiting list (having been watched) + if self.video_obj.waiting_flag: + self.main_win_obj.app_obj.mark_video_waiting( self.video_obj, False, ) @@ -12892,18 +15902,17 @@ def on_click_marked_bookmark_label(self, label, uri): # 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) - self.marked_bookmark_label.set_markup('Not bookmarked') - - GObject.timeout_add(0, self.update_marked_labels) + self.watch_hooktube_label.set_markup(_('HookTube')) + GObject.timeout_add(0, self.update_watch_web) return True - def on_click_marked_fav_label(self, label, uri): + def on_click_watch_invidious_label(self, label, uri): """Called from callback in self.draw_widgets(). - Mark the video as favourite or not favourite. + Watch a YouTube video on Invidious. Args: @@ -12918,17 +15927,17 @@ def on_click_marked_fav_label(self, label, uri): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12909 on_click_marked_fav_label') + utils.debug_time('mwn 15930 on_click_watch_invidious_label') - # Mark the video as favourite/not favourite - if not self.video_obj.fav_flag: - self.main_win_obj.app_obj.mark_video_favourite( - self.video_obj, - True, - ) + # Launch the video + utils.open_file(uri) - else: - self.main_win_obj.app_obj.mark_video_favourite( + # Mark the video as not new (having been watched) + if self.video_obj.new_flag: + self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) + # Remove the video from the waiting list (having been watched) + if self.video_obj.waiting_flag: + self.main_win_obj.app_obj.mark_video_waiting( self.video_obj, False, ) @@ -12937,18 +15946,18 @@ def on_click_marked_fav_label(self, label, uri): # 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) - self.marked_fav_label.set_markup('Favourite') - - GObject.timeout_add(0, self.update_marked_labels) + self.watch_invidious_label.set_markup(_('Invidious')) + GObject.timeout_add(0, self.update_watch_web) return True - def on_click_marked_new_label(self, label, uri): + def on_click_watch_player_label(self, label, uri): """Called from callback in self.draw_widgets(). - Mark the video as new or not new. + Watch a video using the system's default media player, first checking + that a file actually exists. Args: @@ -12963,30 +15972,91 @@ def on_click_marked_new_label(self, label, uri): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12954 on_click_marked_new_label') + utils.debug_time('mwn 15975 on_click_watch_player_label') + + if self.video_obj.live_mode == 2: + + # Download the video. If a download operation is in progress, the + # video is added to it + app_obj = self.main_win_obj.app_obj + + # If the livestream was downloaded when it was still broadcasting, + # then a new download must overwrite the original file + # As of April 2020, the youtube-dl --yes-overwrites option is still + # not available, so as a temporary measure we will rename the + # original file (in case the download fails) + app_obj.prepare_overwrite_video(self.video_obj) + + if not app_obj.download_manager_obj: + + # Start a new download operation + app_obj.download_manager_start( + 'real', + False, + [ self.video_obj ], + ) + + else: + + # Download operation already in progress + download_item_obj \ + = app_obj.download_manager_obj.download_list_obj.create_item( + self.video_obj, + True, + ) + + if download_item_obj: + + # Add a row to the Progress List + self.main_win_obj.progress_list_add_row( + download_item_obj.item_id, + self.video_obj, + ) + + # Update the main window's progress bar + app_obj.download_manager_obj.nudge_progress_bar() + + 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: + + # Download the video, and mark it to be opened in the system's + # default media player as soon as the download operation is + # complete + # If a download operation is already in progress, the video is + # added to it + self.main_win_obj.app_obj.download_watch_videos( [self.video_obj] ) - # Mark the video as new/not new - if not self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, True) else: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) + + # Launch the video in the system's media player + self.main_win_obj.app_obj.watch_video_in_player(self.video_obj) + + # Mark the video as not new (having been watched) + if self.video_obj.new_flag: + self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) + # Remove the video from the waiting list (having been watched) + if self.video_obj.waiting_flag: + self.main_win_obj.app_obj.mark_video_waiting( + self.video_obj, + False, + ) # Because of an unexplained Gtk problem, there is usually a crash after # 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) - self.marked_new_label.set_markup('New') - - GObject.timeout_add(0, self.update_marked_labels) + self.watch_player_label.set_markup(_('Player')) + GObject.timeout_add(0, self.update_watch_player) return True - def on_click_marked_waiting_list_label(self, label, uri): + def on_click_watch_web_label(self, label, uri): """Called from callback in self.draw_widgets(). - Mark the video as in the waiting list or not in the waiting list. + Watch a video on its primary website. Args: @@ -13001,16 +16071,16 @@ def on_click_marked_waiting_list_label(self, label, uri): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12992 on_click_marked_waiting_list_label') + utils.debug_time('mwn 16074 on_click_watch_web_label') - # Mark the video as in waiting list/not in waiting list - if not self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - True, - ) + # Launch the video + utils.open_file(uri) - else: + # Mark the video as not new (having been watched) + if self.video_obj.new_flag: + self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) + # Remove the video from the waiting list (having been watched) + if self.video_obj.waiting_flag: self.main_win_obj.app_obj.mark_video_waiting( self.video_obj, False, @@ -13020,731 +16090,769 @@ def on_click_marked_waiting_list_label(self, label, uri): # 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) - self.marked_playlist_label.set_markup('Not in waiting list') + if utils.is_youtube(self.video_obj.source): + self.watch_web_label.set_markup(_('YouTube')) + else: + self.watch_web_label.set_markup(_('Website')) - GObject.timeout_add(0, self.update_marked_labels) + GObject.timeout_add(0, self.update_watch_web) return True - def on_click_temp_dl_label(self, label, uri): + def on_right_click_row(self, event_box, event): """Called from callback in self.draw_widgets(). - Download the video into the 'Temporary Videos' folder. + When the user right-clicks an a row, create a context-sensitive popup + menu. Args: - label (Gtk.Label): The clicked widget + event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the + signal emitted by the click - uri (str): Ignored + """ - Returns: + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16118 on_right_click_row') - True to show the action has been handled + if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: - """ + self.main_win_obj.video_catalogue_popup_menu(event, self.video_obj) - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13037 on_click_temp_dl_label') - # Can't download the video if an update/refresh/tidy 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: +class CatalogueRow(Gtk.ListBoxRow): - # Create a new media.Video object in the 'Temporary Videos' folder - new_media_data_obj = self.main_win_obj.app_obj.add_video( - self.main_win_obj.app_obj.fixed_temp_folder, - self.video_obj.source, - ) + """Called by MainWin.video_catalogue_redraw_all() and + .video_catalogue_insert_item(). - if new_media_data_obj: + Python class acting as a wrapper for Gtk.ListBoxRow, so that we can + retrieve the media.Video object displayed in each row. - # Download the video. If a download operation is already in - # progress, the video is added to it - # Optionally open the video in the system's default media - # player - self.main_win_obj.app_obj.download_watch_videos( - [new_media_data_obj], - False, - ) + Args: - # Because of an unexplained Gtk problem, there is usually a crash after - # 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) - self.temp_dl_label.set_markup('Download') - GObject.timeout_add(0, self.update_temp_labels) + video_obj (media.Video): The video object displayed on this row - return True + """ - def on_click_temp_dl_watch_label(self, label, uri): + # Standard class methods - """Called from callback in self.draw_widgets(). - Download the video into the 'Temporary Videos' folder. + def __init__(self, video_obj): - Args: + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16146 __init__') - label (Gtk.Label): The clicked widget + super(Gtk.ListBoxRow, self).__init__() - uri (str): Ignored + # IV list - class objects + # ----------------------- - Returns: + self.video_obj = video_obj - True to show the action has been handled - """ +class StatusIcon(Gtk.StatusIcon): - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13091 on_click_temp_dl_watch_label') + """Called by mainapp.TartubeApp.start(). - # Can't download the video if an update/refresh/tidy 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: + Python class acting as a wrapper for Gtk.StatusIcon. - # Create a new media.Video object in the 'Temporary Videos' folder - new_media_data_obj = self.main_win_obj.app_obj.add_video( - self.main_win_obj.app_obj.fixed_temp_folder, - self.video_obj.source, - ) + Args: - if new_media_data_obj: + app_obj (mainapp.TartubeApp): The main application - # Download the video. If a download operation is already in - # progress, the video is added to it - # Optionally open the video in the system's default media - # player - self.main_win_obj.app_obj.download_watch_videos( - [new_media_data_obj], - True, - ) + """ - # Because of an unexplained Gtk problem, there is usually a crash after - # 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) - self.temp_dl_watch_label.set_markup('D/L and watch') - GObject.timeout_add(0, self.update_temp_labels) - return True + # Standard class methods - def on_click_temp_mark_label(self, label, uri): + def __init__(self, app_obj): - """Called from callback in self.draw_widgets(). + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16175 __init__') - Mark the video for download into the 'Temporary Videos' folder. + super(Gtk.StatusIcon, self).__init__() - Args: + # IV list - class objects + # ----------------------- + # The main application + self.app_obj = app_obj - label (Gtk.Label): The clicked widget - uri (str): Ignored + # IV list - other + # --------------- + # Flag set to True (by self.show_icon() ) when the status icon is + # actually visible + self.icon_visible_flag = False - Returns: - True to show the action has been handled + # Code + # ---- + + self.setup() + + # Public class methods + + + def setup(self): + + """Called by self.__init__. + + Sets up the Gtk widget, and creates signal_connects for left- and + right-clicks on the status icon. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13145 on_click_temp_mark_label') + utils.debug_time('mwn 16210 setup') - # Can't mark the video for download if an update/refresh/tidy 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: + # Display the default status icon, to start with... + self.update_icon() + # ...but the status icon isn't visible straight away + self.set_visible(False) - # Create a new media.Video object in the 'Temporary Videos' folder - new_media_data_obj = self.main_win_obj.app_obj.add_video( - self.main_win_obj.app_obj.fixed_temp_folder, - self.video_obj.source, - ) + # Set the tooltip + self.set_has_tooltip(True) + self.set_tooltip_text('Tartube') - # Because of an unexplained Gtk problem, there is usually a crash after - # 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) - self.temp_mark_label.set_markup('Mark for download') - GObject.timeout_add(0, self.update_temp_labels) + # signal connects + self.connect('button_press_event', self.on_button_press_event) + self.connect('popup_menu', self.on_popup_menu) - return True + + def show_icon(self): + + """Can be called by anything. + + Makes the status icon visible in the system tray (if it isn't already + visible).""" + + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16234 show_icon') + + if not self.icon_visible_flag: + self.icon_visible_flag = True + self.set_visible(True) - def on_click_watch_hooktube_label(self, label, uri): + def hide_icon(self): - """Called from callback in self.draw_widgets(). + """Can be called by anything. - Watch a YouTube video on HookTube. + Makes the status icon invisible in the system tray (if it isn't already + invisible).""" - Args: + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16249 hide_icon') - label (Gtk.Label): The clicked widget + if self.icon_visible_flag: + self.icon_visible_flag = False + self.set_visible(False) - uri (str): Ignored - Returns: + def update_icon(self): - True to show the action has been handled + """Called by self.setup(), and then by mainapp.TartubeApp whenever a + download/update/refresh/info/tidy 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. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13188 on_click_watch_hooktube_label') + utils.debug_time('mwn 16266 update_icon') - # Launch the video - utils.open_file(uri) + if self.app_obj.download_manager_obj: + if self.app_obj.download_manager_obj.operation_type == 'sim': + icon = formats.STATUS_ICON_DICT['check_icon'] + else: + icon = formats.STATUS_ICON_DICT['download_icon'] + elif self.app_obj.update_manager_obj: + icon = formats.STATUS_ICON_DICT['update_icon'] + elif self.app_obj.refresh_manager_obj: + icon = formats.STATUS_ICON_DICT['refresh_icon'] + elif self.app_obj.info_manager_obj: + icon = formats.STATUS_ICON_DICT['info_icon'] + elif self.app_obj.tidy_manager_obj: + icon = formats.STATUS_ICON_DICT['tidy_icon'] + else: + icon = formats.STATUS_ICON_DICT['default_icon'] - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, + self.set_from_file( + os.path.abspath( + os.path.join( + self.app_obj.main_win_obj.icon_dir_path, + 'status', + icon, + ), ) + ) - # Because of an unexplained Gtk problem, there is usually a crash after - # 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) - self.watch_hooktube_label.set_markup('HookTube') - GObject.timeout_add(0, self.update_watch_web) - return True + # Callback class methods - def on_click_watch_invidious_label(self, label, uri): + # (Clicks on the status icon) - """Called from callback in self.draw_widgets(). - Watch a YouTube video on Invidious. + def on_button_press_event(self, widget, event_button): - Args: + """Called from a callback in self.setup(). - label (Gtk.Label): The clicked widget + When the status icon is left-clicked, toggle the main window's + visibility. - uri (str): Ignored + Args: - Returns: + widget (mainwin.StatusIcon): This object - True to show the action has been handled + event_button (Gdk.EventButton): Ignored """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13232 on_click_watch_invidious_label') - - # Launch the video - utils.open_file(uri) + utils.debug_time('mwn 16317 on_button_press_event') - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - - # Because of an unexplained Gtk problem, there is usually a crash after - # 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) - self.watch_invidious_label.set_markup('Invidious') - GObject.timeout_add(0, self.update_watch_web) + if event_button.button == 1: + self.app_obj.main_win_obj.toggle_visibility() + return True - return True + else: + return False - def on_click_watch_player_label(self, label, uri): + def on_popup_menu(self, widget, button, time): - """Called from callback in self.draw_widgets(). + """Called from a callback in self.setup(). - Watch a video using the system's default media player, first checking - that a file actually exists. + When the status icon is right-clicked, open a popup men. Args: - label (Gtk.Label): The clicked widget - - uri (str): Ignored + widget (mainwin.StatusIcon): This object - Returns: + button_type (int): Ignored - True to show the action has been handled + time (int): Ignored """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13277 on_click_watch_player_label') + utils.debug_time('mwn 16344 on_popup_menu') - if 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: + # Set up the popup menu + popup_menu = Gtk.Menu() - # Download the video, and mark it to be opened in the system's - # default media player as soon as the download operation is - # complete - # If a download operation is already in progress, the video is - # added to it - self.main_win_obj.app_obj.download_watch_videos( [self.video_obj] ) + # Check all + check_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Check all')) + check_menu_item.connect('activate', self.on_check_menu_item) + popup_menu.append(check_menu_item) + if self.app_obj.current_manager_obj: + check_menu_item.set_sensitive(False) - else: + # Download all + download_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download all')) + download_menu_item.connect('activate', self.on_download_menu_item) + popup_menu.append(download_menu_item) + if self.app_obj.current_manager_obj: + download_menu_item.set_sensitive(False) - # Launch the video in the system's media player - self.main_win_obj.app_obj.watch_video_in_player(self.video_obj) + # Separator + popup_menu.append(Gtk.SeparatorMenuItem()) - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) + # Stop current operation + stop_menu_item = Gtk.MenuItem.new_with_mnemonic( + _('_Stop current operation'), + ) + stop_menu_item.connect('activate', self.on_stop_menu_item) + popup_menu.append(stop_menu_item) + if not self.app_obj.current_manager_obj: + stop_menu_item.set_sensitive(False) - # Because of an unexplained Gtk problem, there is usually a crash after - # 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) - self.watch_player_label.set_markup('Player') - GObject.timeout_add(0, self.update_watch_player) + # Separator + popup_menu.append(Gtk.SeparatorMenuItem()) - return True + # Quit + quit_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Quit')) + quit_menu_item.connect('activate', self.on_quit_menu_item) + popup_menu.append(quit_menu_item) + # Create the popup menu + popup_menu.show_all() + popup_menu.popup(None, None, None, self, 3, time) - def on_click_watch_web_label(self, label, uri): - """Called from callback in self.draw_widgets(). + # (Menu item callbacks) - Watch a video on its primary website. - Args: + def on_check_menu_item(self, menu_item): - label (Gtk.Label): The clicked widget + """Called from a callback in self.popup_menu(). - uri (str): Ignored + Starts the download manager. - Returns: + Args: - True to show the action has been handled + menu_item (Gtk.MenuItem): The menu item clicked """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13334 on_click_watch_web_label') + utils.debug_time('mwn 16404 on_check_menu_item') - # Launch the video - utils.open_file(uri) + if not self.app_obj.current_manager_obj: + self.app_obj.download_manager_start('sim') - # Mark the video as not new (having been watched) - if self.video_obj.new_flag: - self.main_win_obj.app_obj.mark_video_new(self.video_obj, False) - # Remove the video from the waiting list (having been watched) - if self.video_obj.waiting_flag: - self.main_win_obj.app_obj.mark_video_waiting( - self.video_obj, - False, - ) - # Because of an unexplained Gtk problem, there is usually a crash after - # 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) - if utils.is_youtube(self.video_obj.source): - self.watch_web_label.set_markup('YouTube') - else: - self.watch_web_label.set_markup('Website') + def on_download_menu_item(self, menu_item): - GObject.timeout_add(0, self.update_watch_web) + """Called from a callback in self.popup_menu(). - return True + Starts the download manager. + Args: - def on_right_click_row(self, event_box, event): + menu_item (Gtk.MenuItem): The menu item clicked - """Called from callback in self.draw_widgets(). + """ - When the user right-clicks an a row, create a context-sensitive popup - menu. + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16423 on_download_menu_item') - Args: + if not self.app_obj.current_manager_obj: + self.app_obj.download_manager_start('real') - event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the - signal emitted by the click - """ + def on_stop_menu_item(self, menu_item): - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13378 on_right_click_row') + """Called from a callback in self.popup_menu(). - if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: + 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). - self.main_win_obj.video_catalogue_popup_menu(event, self.video_obj) + Args: + menu_item (Gtk.MenuItem): The menu item clicked -class CatalogueRow(Gtk.ListBoxRow): + """ - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_item(). + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16444 on_stop_menu_item') - Python class acting as a wrapper for Gtk.ListBoxRow, so that we can - retrieve the media.Video object displayed in each row. + if self.app_obj.current_manager_obj: - Args: + self.app_obj.set_operation_halted_flag(True) - video_obj (media.Video): The video object displayed on this row + if self.app_obj.download_manager_obj: + self.app_obj.download_manager_obj.stop_download_operation() + elif self.app_obj.update_manager_obj: + self.app_obj.update_manager_obj.stop_update_operation() + elif self.app_obj.refresh_manager_obj: + self.app_obj.refresh_manager_obj.stop_refresh_operation() + elif self.app_obj.info_manager_obj: + 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() - """ + def on_quit_menu_item(self, menu_item): + + """Called from a callback in self.popup_menu(). + + Close the application. - # Standard class methods + Args: + menu_item (Gtk.MenuItem): The menu item clicked - def __init__(self, video_obj): + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13406 __init__') - - super(Gtk.ListBoxRow, self).__init__() + utils.debug_time('mwn 16475 on_quit_menu_item') - # IV list - class objects - # ----------------------- + self.app_obj.stop() - self.video_obj = video_obj +# (Minor window classes) -class StatusIcon(Gtk.StatusIcon): - """Called by mainapp.TartubeApp.start(). +class GenericMinorWin(Gtk.Window): - Python class acting as a wrapper for Gtk.StatusIcon. + """Generic Python class for dialogue windows which can't be implemented as + Gtk.Dialog windows, because the main window hasn't been created yet.""" - Args: - app_obj (mainapp.TartubeApp): The main application + # Standard class methods - """ +# def __init__(): # Provided by child object - # Standard class methods + # Public class methods - def __init__(self, app_obj): - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13435 __init__') + def setup_pixbufs(self): - super(Gtk.StatusIcon, self).__init__() + """Called by self.__init__(). - # IV list - class objects - # ----------------------- - # The main application - self.app_obj = app_obj + Based on the same function in mainwin.MainWin. Creates a subset of the + pixbufs created by that function. + """ + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16507 setup_pixbufs') - # IV list - other - # --------------- - # Flag set to True (by self.show_icon() ) when the status icon is - # actually visible - self.icon_visible_flag = False + # The default location for icons is ../icons + # When installed via PyPI, the icons are moved to ../tartube/icons + # When installed via a Debian/RPM package, the icons are moved to + # /usr/share/tartube/icons + icon_dir_list = [] + icon_dir_list.append( + os.path.abspath( + os.path.join(self.app_obj.script_parent_dir, 'icons'), + ), + ) + icon_dir_list.append( + os.path.abspath( + os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'icons', + ), + ), + ) - # Code - # ---- + icon_dir_list.append( + os.path.join( + '/', 'usr', 'share', __main__.__packagename__, 'icons', + ) + ) - self.setup() + for icon_dir_path in icon_dir_list: + if os.path.isdir(icon_dir_path): + for key in formats.DIALOGUE_ICON_DICT: + rel_path = formats.DIALOGUE_ICON_DICT[key] + full_path = os.path.abspath( + os.path.join(icon_dir_path, 'dialogue', rel_path), + ) + self.icon_dict[key] = full_path - # Public class methods + # Now create the pixbufs themselves + for key in self.icon_dict: + full_path = self.icon_dict[key] + if not os.path.isfile(full_path): + self.pixbuf_dict[key] = None + else: + self.pixbuf_dict[key] \ + = GdkPixbuf.Pixbuf.new_from_file(full_path) - def setup(self): + for rel_path in formats.WIN_ICON_LIST: + full_path = os.path.abspath( + os.path.join(icon_dir_path, 'win', rel_path), + ) + self.win_pixbuf_list.append( + GdkPixbuf.Pixbuf.new_from_file(full_path), + ) - """Called by self.__init__. + # Store the correct icon_dir_path, so that StatusIcon can use + # it + self.icon_dir_path = icon_dir_path - Sets up the Gtk widget, and creates signal_connects for left- and - right-clicks on the status icon. - """ + return - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13470 setup') + # No icons directory found; this is a fatal error + print( + 'Tartube cannot start because it cannot find its icons directory' \ + + ' (folder)', + file=sys.stderr, + ) - # Display the default status icon, to start with... - self.update_icon() - # ...but the status icon isn't visible straight away - self.set_visible(False) + self.app_obj.do_shutdown() - # Set the tooltip - self.set_has_tooltip(True) - self.set_tooltip_text(__main__.__prettyname__) - # signal connects - self.connect('button_press_event', self.on_button_press_event) - self.connect('popup_menu', self.on_popup_menu) +# def setup_win(): # Provided by child object - def show_icon(self): + # Callbacks - """Can be called by anything. - Makes the status icon visible in the system tray (if it isn't already - visible).""" + def on_delete_event(self, widget, event): - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13494 show_icon') + """Called from callback in self.setup_win(). - if not self.icon_visible_flag: - self.icon_visible_flag = True - self.set_visible(True) + If the user click-closes the window, halt the main application. + Args: - def hide_icon(self): + widget (mainwin.MainWin): The main window - """Can be called by anything. + event (Gdk.Event): Ignored - Makes the status icon invisible in the system tray (if it isn't already - invisible).""" + """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13509 hide_icon') - - if self.icon_visible_flag: - self.icon_visible_flag = False - self.set_visible(False) + utils.debug_time('mwn 16600 on_delete_event') + self.app_obj.quit() - def update_icon(self): - """Called by self.setup(), and then by mainapp.TartubeApp whenever a - download/update/refresh/info/tidy operation starts or stops. +class StartErrorWin(GenericMinorWin): - 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. - """ + """Called by mainapp.TartubeApp.start(). - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13526 update_icon') + Minor window displayed when Tartube fails to load its config file (and + before the main window is created). - if self.app_obj.download_manager_obj: - if self.app_obj.download_manager_obj.operation_type == 'sim': - icon = formats.STATUS_ICON_DICT['check_icon'] - else: - icon = formats.STATUS_ICON_DICT['download_icon'] - elif self.app_obj.update_manager_obj: - icon = formats.STATUS_ICON_DICT['update_icon'] - elif self.app_obj.refresh_manager_obj: - icon = formats.STATUS_ICON_DICT['refresh_icon'] - elif self.app_obj.info_manager_obj: - icon = formats.STATUS_ICON_DICT['info_icon'] - elif self.app_obj.tidy_manager_obj: - icon = formats.STATUS_ICON_DICT['tidy_icon'] - else: - icon = formats.STATUS_ICON_DICT['default_icon'] + When this window is closed, the main application halts. - self.set_from_file( - os.path.abspath( - os.path.join( - self.app_obj.main_win_obj.icon_dir_path, - 'status', - icon, - ), - ) - ) + Args: + app_obj (mainapp.TartubeApp): The main application object - # Callback class methods + error_msg (str): An error message to display + """ - # (Clicks on the status icon) + # Standard class methods - def on_button_press_event(self, widget, event_button): - """Called from a callback in self.setup(). + def __init__(self, app_obj, error_msg): - When the status icon is left-clicked, toggle the main window's - visibility. + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16629 __init__') - Args: + super(StartErrorWin, self).__init__( + title=__main__.__packagename__.title() + ' v' \ + + __main__.__version__, + application=app_obj + ) - widget (mainwin.StatusIcon): This object + # IV list - class objects + # ----------------------- + # The main application + self.app_obj = app_obj - event_button (Gdk.EventButton): Ignored - """ + # IV list - other + # --------------- + # Size (in pixels) of gaps between window widgets + self.spacing_size = self.app_obj.default_spacing_size + # Standard length for labels (used in calls to + # utils.tidy_up_long_string(); the same value as + # mainwin.MainWin.long_string_max_len ) + self.label_length = 48 - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13577 on_button_press_event') + # Paths to Tartube standard icon files (a subset of those used by + # mainwin.MainWin) + # Dictionary in the form + # key - a string like 'video_both_large' + # value - full filepath to the icon file + self.icon_dict = {} + # Loading icon files whenever they're neeeded causes frequent Gtk + # crashes. Instead, we create a GdkPixbuf.Pixbuf for all standard + # icon files at the beginning + # A dictionary of those pixbufs, created by self.setup_pixbufs() + # Dictionary in the form + # key - a string like 'system_icon' (the same key set used by + # self.icon_dict) + # value - A GdkPixbuf.Pixbuf object + self.pixbuf_dict = {} + # List of pixbufs used as each window's icon list + self.win_pixbuf_list = [] + # The full path to the directory in which self.setup_pixbufs() found + # the icons; stores so that StatusIcon can use it + self.icon_dir_path = None - if event_button.button == 1: - self.app_obj.main_win_obj.toggle_visibility() - return True + # The message to display + self.error_msg = error_msg - else: - return False + # Code + # ---- + # Create GdkPixbuf.Pixbufs for a small subset Tartube standard icons + self.setup_pixbufs() + # Set up the window + self.setup_win() - def on_popup_menu(self, widget, button, time): - """Called from a callback in self.setup(). + # Public class methods - When the status icon is right-clicked, open a popup men. - Args: +# def setup_pixbufs(): # Inherited from GenericMinorWin - widget (mainwin.StatusIcon): This object - button_type (int): Ignored + def setup_win(self): - time (int): Ignored + """Called by self.__init__(). + Sets up the window. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13604 on_popup_menu') - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Check all - check_menu_item = Gtk.MenuItem.new_with_mnemonic('_Check all') - check_menu_item.connect('activate', self.on_check_menu_item) - popup_menu.append(check_menu_item) - if self.app_obj.current_manager_obj: - check_menu_item.set_sensitive(False) + utils.debug_time('mwn 16699 setup_win') - # Download all - download_menu_item = Gtk.MenuItem.new_with_mnemonic('_Download all') - download_menu_item.connect('activate', self.on_download_menu_item) - popup_menu.append(download_menu_item) - if self.app_obj.current_manager_obj: - download_menu_item.set_sensitive(False) + # Set the window's Gtk icon list + self.set_icon_list(self.win_pixbuf_list) - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) + # Intercept the user's attempts to close the window, so we can halt + # the main application + self.connect('delete_event', self.on_delete_event) - # Stop current operation - stop_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Stop current operation', - ) - stop_menu_item.connect('activate', self.on_stop_menu_item) - popup_menu.append(stop_menu_item) - if not self.app_obj.current_manager_obj: - stop_menu_item.set_sensitive(False) + # Set up widgets on a grid + grid = Gtk.Grid() + self.add(grid) + grid.set_border_width(self.spacing_size) + grid.set_row_spacing(self.spacing_size * 2) + grid.set_column_spacing(self.spacing_size * 2) - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) + image = Gtk.Image.new_from_pixbuf( + self.pixbuf_dict['system_icon'], + ) + grid.attach(image, 0, 0, 1, 1) - # Quit - quit_menu_item = Gtk.MenuItem.new_with_mnemonic('_Quit') - quit_menu_item.connect('activate', self.on_quit_menu_item) - popup_menu.append(quit_menu_item) + label = Gtk.Label() + grid.attach(label, 1, 0, 1, 1) + label.set_markup( + utils.tidy_up_long_string( + _('Tartube failed to start because:'), + self.label_length, + ) + '\n\n' \ + + utils.tidy_up_long_string( + self.error_msg, + self.label_length, + ) + '\n\n' \ + + utils.tidy_up_long_string( + _( + 'If you don\'t know how to resolve this error, please' \ + + ' contact the authors', + ), + self.label_length, + ) + ' ' \ + + _('here') + '\n' + ) - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, self, 3, time) + button = Gtk.Button.new_with_label(_('OK')) + grid.attach(button, 1, 1, 1, 1) + button.connect('clicked', self.on_button_clicked) + + # Show the window + self.show_all() - # (Menu item callbacks) + # (Callbacks) - def on_check_menu_item(self, menu_item): + def on_button_clicked(self, button): - """Called from a callback in self.popup_menu(). + """Called from a callback in self.setup_win(). - Starts the download manager. + Halts the application. Args: - menu_item (Gtk.MenuItem): The menu item clicked + button (Gtk.Button): The widget clicked """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13664 on_check_menu_item') + utils.debug_time('mwn 16765 on_button_clicked') - if not self.app_obj.current_manager_obj: - self.app_obj.download_manager_start('sim') + self.app_obj.quit() - def on_download_menu_item(self, menu_item): +class FakeMainWin(GenericMinorWin): - """Called from a callback in self.popup_menu(). + """Called by mainapp.TartubeApp.notify_user_of_data_dir(). - Starts the download manager. + A Gtk.Window taking the place of the main window (which hasn't been + created yet), and which is never made visible. - Args: + Args: - menu_item (Gtk.MenuItem): The menu item clicked + app_obj (mainapp.TartubeApp): The main application object - """ + """ - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13683 on_download_menu_item') - if not self.app_obj.current_manager_obj: - self.app_obj.download_manager_start('real') + # Standard class methods - def on_stop_menu_item(self, menu_item): + def __init__(self, app_obj): - """Called from a callback in self.popup_menu(). + if DEBUG_FUNC_FLAG: + utils.debug_time('mwn 16790 __init__') - Halts the current download operation + Gtk.Window.__init__(self, title=_('Welcome to Tartube!')) - Args: + # IV list - class objects + # ----------------------- + # The main application + self.app_obj = app_obj - menu_item (Gtk.MenuItem): The menu item clicked - """ + # IV list - other + # --------------- + # Size (in pixels) of gaps between window widgets + self.spacing_size = self.app_obj.default_spacing_size + # Standard length for labels (used in calls to + # utils.tidy_up_long_string(); the same value as + # mainwin.MainWin.long_string_max_len ) + self.label_length = 48 - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13702 on_stop_menu_item') + # Paths to Tartube standard icon files (a subset of those used by + # mainwin.MainWin) + # Dictionary in the form + # key - a string like 'video_both_large' + # value - full filepath to the icon file + self.icon_dict = {} + # Loading icon files whenever they're neeeded causes frequent Gtk + # crashes. Instead, we create a GdkPixbuf.Pixbuf for all standard + # icon files at the beginning + # A dictionary of those pixbufs, created by self.setup_pixbufs() + # Dictionary in the form + # key - a string like 'system_icon' (the same key set used by + # self.icon_dict) + # value - A GdkPixbuf.Pixbuf object + self.pixbuf_dict = {} + # List of pixbufs used as each window's icon list + self.win_pixbuf_list = [] + # The full path to the directory in which self.setup_pixbufs() found + # the icons; stores so that StatusIcon can use it + self.icon_dir_path = None - if self.app_obj.current_manager_obj: + # Code + # ---- - self.app_obj.set_operation_halted_flag(True) + # Create GdkPixbuf.Pixbufs for a small subset Tartube standard icons + self.setup_pixbufs() + # Set up the window + self.setup_win() - if self.app_obj.download_manager_obj: - self.app_obj.download_manager_obj.stop_download_operation() - elif self.app_obj.update_manager_obj: - self.app_obj.update_manager_obj.stop_update_operation() - elif self.app_obj.refresh_manager_obj: - self.app_obj.refresh_manager_obj.stop_refresh_operation() - elif self.app_obj.info_manager_obj: - 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() + # Public class methods - def on_quit_menu_item(self, menu_item): - """Called from a callback in self.popup_menu(). +# def setup_pixbufs(): # Inherited from GenericMinorWin - Close the application. - Args: + def setup_win(self): - menu_item (Gtk.MenuItem): The menu item clicked + """Called by self.__init__(). + Sets up the window. """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13733 on_quit_menu_item') + utils.debug_time('mwn 16853 setup_win') - self.app_obj.stop() + self.set_visible(False) # (Dialogue window classes) @@ -13781,7 +16889,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None, dl_sim_flag=False, monitor_flag=False): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13772 __init__') + utils.debug_time('mwn 16892 __init__') # IV list - class objects # ----------------------- @@ -13813,7 +16921,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None, Gtk.Dialog.__init__( self, - 'Add channel', + _('Add channel'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -13832,19 +16940,20 @@ def __init__(self, main_win_obj, suggest_parent_name=None, grid.set_border_width(main_win_obj.spacing_size) grid.set_row_spacing(main_win_obj.spacing_size) - label = Gtk.Label('Enter the channel name') + label = Gtk.Label(_('Enter the channel name')) grid.attach(label, 0, 0, 2, 1) label2 = Gtk.Label() grid.attach(label2, 0, 1, 2, 1) label2.set_markup( - '(Use the channel\'s real name or a customised name)', + '' + _('(Use the channel\'s real name or a customised name)') \ + + '', ) self.entry = Gtk.Entry() grid.attach(self.entry, 0, 2, 2, 1) self.entry.set_hexpand(True) - label3 = Gtk.Label('Copy and paste a link to the channel') + label3 = Gtk.Label(_('Copy and paste a link to the channel')) grid.attach(label3, 0, 3, 2, 1) self.entry2 = Gtk.Entry() @@ -13891,7 +17000,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None, if suggest_parent_name is not None: self.folder_list.insert(0, suggest_parent_name) - label4 = Gtk.Label('(Optional) Add this channel inside a folder') + label4 = Gtk.Label(_('(Optional) Add this channel inside a folder')) grid.attach(label4, 0, 6, 2, 1) box = Gtk.Box() @@ -13921,21 +17030,21 @@ def __init__(self, main_win_obj, suggest_parent_name=None, self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( None, - 'I want to download videos from this channel automatically', + _('I want to download videos from this channel automatically'), ) grid.attach(self.radiobutton, 0, 9, 2, 1) self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) grid.attach(self.radiobutton2, 0, 10, 2, 1) self.radiobutton2.set_label( - 'Don\'t download anything, just check for new videos', + _('Don\'t download anything, just check for new videos'), ) if dl_sim_flag: self.radiobutton2.set_active(True) self.checkbutton = Gtk.CheckButton() grid.attach(self.checkbutton, 0, 11, 2, 1) - self.checkbutton.set_label('Monitor the clipboard') + self.checkbutton.set_label(_('Enable automatic copy/paste')) self.checkbutton.connect('toggled', self.on_checkbutton_toggled) if monitor_flag: @@ -13982,7 +17091,7 @@ def on_checkbutton_toggled(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13973 on_checkbutton_toggled') + utils.debug_time('mwn 17094 on_checkbutton_toggled') if not checkbutton.get_active() \ and self.clipboard_timer_id is not None: @@ -14014,7 +17123,7 @@ def on_combo_changed(self, combo): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14005 on_combo_changed') + utils.debug_time('mwn 17126 on_combo_changed') self.parent_name = self.folder_list[combo.get_active()] @@ -14028,7 +17137,7 @@ def on_window_drag_data_received(self, window, context, x, y, data, info, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14019 on_window_drag_data_received') + utils.debug_time('mwn 17140 on_window_drag_data_received') utils.add_links_to_entry_from_clipboard( self.main_win_obj.app_obj, @@ -14052,7 +17161,7 @@ def clipboard_timer_callback(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14043 clipboard_timer_callback') + utils.debug_time('mwn 17164 clipboard_timer_callback') utils.add_links_to_entry_from_clipboard( self.main_win_obj.app_obj, @@ -14088,7 +17197,7 @@ class AddFolderDialogue(Gtk.Dialog): def __init__(self, main_win_obj, suggest_parent_name=None): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14079 __init__') + utils.debug_time('mwn 17200 __init__') # IV list - class objects # ----------------------- @@ -14116,7 +17225,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None): Gtk.Dialog.__init__( self, - 'Add folder', + _('Add folder'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -14135,7 +17244,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None): grid.set_border_width(main_win_obj.spacing_size) grid.set_row_spacing(main_win_obj.spacing_size) - label = Gtk.Label('Enter the folder name') + label = Gtk.Label(_('Enter the folder name')) grid.attach(label, 0, 0, 2, 1) # (Store various widgets as IVs, so the calling function can retrieve @@ -14178,7 +17287,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None): self.parent_name = self.folder_list[0] label4 = Gtk.Label( - '(Optional) Add this folder inside another folder', + _('(Optional) Add this folder inside another folder'), ) grid.attach(label4, 0, 3, 2, 1) @@ -14209,13 +17318,13 @@ def __init__(self, main_win_obj, suggest_parent_name=None): self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( None, - 'I want to download videos from this folder automatically', + _('I want to download videos from this folder automatically'), ) grid.attach(self.radiobutton, 0, 6, 2, 1) self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) self.radiobutton2.set_label( - 'Don\'t download anything, just check for new videos', + _('Don\'t download anything, just check for new videos'), ) grid.attach(self.radiobutton2, 0, 7, 2, 1) @@ -14240,7 +17349,7 @@ def on_combo_changed(self, combo): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14231 on_combo_changed') + utils.debug_time('mwn 17352 on_combo_changed') self.parent_name = self.folder_list[combo.get_active()] @@ -14276,7 +17385,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None, dl_sim_flag=False, monitor_flag=False): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14267 __init__') + utils.debug_time('mwn 17388 __init__') # IV list - class objects # ----------------------- @@ -14307,7 +17416,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None, Gtk.Dialog.__init__( self, - 'Add playlist', + _('Add playlist'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -14326,19 +17435,20 @@ def __init__(self, main_win_obj, suggest_parent_name=None, grid.set_border_width(main_win_obj.spacing_size) grid.set_row_spacing(main_win_obj.spacing_size) - label = Gtk.Label('Enter the playlist name') + label = Gtk.Label(_('Enter the playlist name')) grid.attach(label, 0, 0, 2, 1) label2 = Gtk.Label() grid.attach(label2, 0, 1, 2, 1) label2.set_markup( - '(Use the playlist\'s real name or a customised name)', + '' + _('(Use the playlist\'s real name or a customised name)') \ + + '', ) self.entry = Gtk.Entry() grid.attach(self.entry, 0, 2, 2, 1) self.entry.set_hexpand(True) - label3 = Gtk.Label('Copy and paste a link to the playlist') + label3 = Gtk.Label(_('Copy and paste a link to the playlist')) grid.attach(label3, 0, 3, 2, 1) self.entry2 = Gtk.Entry() @@ -14385,7 +17495,7 @@ def __init__(self, main_win_obj, suggest_parent_name=None, if suggest_parent_name is not None: self.folder_list.insert(0, suggest_parent_name) - label4 = Gtk.Label('(Optional) Add this playlist inside a folder') + label4 = Gtk.Label(_('(Optional) Add this playlist inside a folder')) grid.attach(label4, 0, 6, 2, 1) box = Gtk.Box() @@ -14415,21 +17525,21 @@ def __init__(self, main_win_obj, suggest_parent_name=None, self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( None, - 'I want to download videos from this playlist automatically', + _('I want to download videos from this playlist automatically'), ) grid.attach(self.radiobutton, 0, 9, 2, 1) self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) grid.attach(self.radiobutton2, 0, 10, 2, 1) self.radiobutton2.set_label( - 'Don\'t download anything, just check for new videos', + _('Don\'t download anything, just check for new videos'), ) if dl_sim_flag: self.radiobutton2.set_active(True) self.checkbutton = Gtk.CheckButton() grid.attach(self.checkbutton, 0, 11, 2, 1) - self.checkbutton.set_label('Monitor the clipboard') + self.checkbutton.set_label(_('Enable automatic copy/paste')) self.checkbutton.connect('toggled', self.on_checkbutton_toggled) if monitor_flag: @@ -14476,7 +17586,7 @@ def on_checkbutton_toggled(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14467 on_checkbutton_toggled') + utils.debug_time('mwn 17589 on_checkbutton_toggled') if not checkbutton.get_active() \ and self.clipboard_timer_id is not None: @@ -14508,7 +17618,7 @@ def on_combo_changed(self, combo): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14499 on_combo_changed') + utils.debug_time('mwn 17621 on_combo_changed') self.parent_name = self.folder_list[combo.get_active()] @@ -14522,7 +17632,7 @@ def on_window_drag_data_received(self, window, context, x, y, data, info, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14513 on_window_drag_data_received') + utils.debug_time('mwn 17635 on_window_drag_data_received') utils.add_links_to_entry_from_clipboard( self.main_win_obj.app_obj, @@ -14546,7 +17656,7 @@ def clipboard_timer_callback(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14537 clipboard_timer_callback') + utils.debug_time('mwn 17659 clipboard_timer_callback') utils.add_links_to_entry_from_clipboard( self.main_win_obj.app_obj, @@ -14578,7 +17688,7 @@ class AddVideoDialogue(Gtk.Dialog): def __init__(self, main_win_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14589 __init__') + utils.debug_time('mwn 17691 __init__') # IV list - class objects # ----------------------- @@ -14612,7 +17722,7 @@ def __init__(self, main_win_obj): Gtk.Dialog.__init__( self, - 'Add videos', + _('Add videos'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -14631,24 +17741,36 @@ def __init__(self, main_win_obj): grid.set_border_width(main_win_obj.spacing_size) grid.set_row_spacing(main_win_obj.spacing_size) - label = Gtk.Label('Copy and paste the links to one or more videos') + label = Gtk.Label(_('Copy and paste the links to one or more videos')) grid.attach(label, 0, 0, 2, 1) if main_win_obj.app_obj.operation_convert_mode == 'channel': - text = 'Links containing multiple videos will be converted to' \ - + ' a channel' + + text = _( + 'Links containing multiple videos will be converted to' \ + + ' a channel', + ) elif main_win_obj.app_obj.operation_convert_mode == 'playlist': - text = 'Links containing multiple videos will be converted to a' \ - + ' playlist' + + text = _( + 'Links containing multiple videos will be converted to a' \ + + ' playlist', + ) elif main_win_obj.app_obj.operation_convert_mode == 'multi': - text = 'Links containing multiple videos will be downloaded' \ - + ' separately' + + text = _( + 'Links containing multiple videos will be downloaded' \ + + ' separately', + ) elif main_win_obj.app_obj.operation_convert_mode == 'disable': - text = 'Links containing multiple videos will not be downloaded' - + ' at all' + + text = _( + 'Links containing multiple videos will not be downloaded' + + ' at all', + ) label = Gtk.Label() label.set_markup('' + text + '') @@ -14728,7 +17850,7 @@ def __init__(self, main_win_obj): # retrieve it. self.parent_name = self.folder_list[0] - label2 = Gtk.Label('Add the videos to this folder') + label2 = Gtk.Label(_('Add the videos to this folder')) grid.attach(label2, 0, 4, 2, 1) box = Gtk.Box() @@ -14758,19 +17880,19 @@ def __init__(self, main_win_obj): self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( None, - 'I want to download these videos automatically', + _('I want to download these videos automatically'), ) grid.attach(self.radiobutton, 0, 7, 2, 1) self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton) self.radiobutton2.set_label( - 'Don\'t download anything, just check the videos', + _('Don\'t download anything, just check the videos'), ) grid.attach(self.radiobutton2, 0, 8, 2, 1) self.checkbutton = Gtk.CheckButton() grid.attach(self.checkbutton, 0, 9, 2, 1) - self.checkbutton.set_label('Monitor the clipboard') + self.checkbutton.set_label(_('Enable automatic copy/paste')) self.checkbutton.connect('toggled', self.on_checkbutton_toggled) # Paste in the contents of the clipboard (if it contains valid URLs) @@ -14802,7 +17924,7 @@ def on_checkbutton_toggled(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14793 on_checkbutton_toggled') + utils.debug_time('mwn 17927 on_checkbutton_toggled') if not checkbutton.get_active() \ and self.clipboard_timer_id is not None: @@ -14834,7 +17956,7 @@ def on_combo_changed(self, combo): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14825 on_combo_changed') + utils.debug_time('mwn 17959 on_combo_changed') self.parent_name = self.folder_list[combo.get_active()] @@ -14848,7 +17970,7 @@ def on_window_drag_data_received(self, window, context, x, y, data, info, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14839 on_window_drag_data_received') + utils.debug_time('mwn 17973 on_window_drag_data_received') utils.add_links_to_textview_from_clipboard( self.main_win_obj.app_obj, @@ -14873,7 +17995,7 @@ def clipboard_timer_callback(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14864 clipboard_timer_callback') + utils.debug_time('mwn 17998 clipboard_timer_callback') utils.add_links_to_textview_from_clipboard( self.main_win_obj.app_obj, @@ -14911,7 +18033,7 @@ class CalendarDialogue(Gtk.Dialog): def __init__(self, parent_win_obj, date=None): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14902 __init__') + utils.debug_time('mwn 18036 __init__') # IV list - class objects # ----------------------- @@ -14929,7 +18051,7 @@ def __init__(self, parent_win_obj, date=None): Gtk.Dialog.__init__( self, - 'Select a date', + _('Select a date'), parent_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -14995,7 +18117,7 @@ class DeleteContainerDialogue(Gtk.Dialog): def __init__(self, main_win_obj, media_data_obj, empty_flag): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14986 __init__') + utils.debug_time('mwn 18120 __init__') # IV list - class objects # ----------------------- @@ -15018,7 +18140,9 @@ def __init__(self, main_win_obj, media_data_obj, empty_flag): # ---- # Prepare variables - pkg_string = __main__.__prettyname__ + spacing_size = self.main_win_obj.spacing_size + label_length = self.main_win_obj.long_string_max_len + media_type = media_data_obj.get_type() if media_type == 'video': return self.app_obj.system_error( @@ -15032,9 +18156,19 @@ def __init__(self, main_win_obj, media_data_obj, empty_flag): # Create the dialogue window if not empty_flag: - title = 'Delete ' + media_type + if media_type == 'channel': + title = _('Delete channel') + elif media_type == 'playlist': + title = _('Delete playlist') + else: + title = _('Delete folder') else: - title = 'Empty ' + media_type + if media_type == 'channel': + title = _('Empty channel') + elif media_type == 'playlist': + title = _('Empty playlist') + else: + title = _('Empty folder') Gtk.Dialog.__init__( self, @@ -15055,8 +18189,8 @@ def __init__(self, main_win_obj, media_data_obj, empty_flag): 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_border_width(spacing_size) + grid.set_row_spacing(spacing_size) label = Gtk.Label() grid.attach(label, 0, 0, 1, 1) @@ -15067,61 +18201,71 @@ def __init__(self, main_win_obj, media_data_obj, empty_flag): if not total_count: - if media_type == 'folder': + if media_type == 'channel': + string = _('This channel does not contain any videos') + elif media_type == 'playlist': + string = _('This playlist does not contain any videos') + else: + string = _('This folder doesn\'t contain anything') - label2 = Gtk.Label( - 'This ' + media_type + ' does not contain any videos,' \ - + ' channels,\nplaylists or folders (but there might be' \ - + ' some files\nin ' + pkg_string + '\'s data directory)', - ) - else: - label2 = Gtk.Label( - 'This ' + media_type + ' does not contain any videos' \ - + ' (but there might\nbe some files in ' + pkg_string \ - + '\'s data directory)', - ) + label2 = Gtk.Label( + utils.tidy_up_long_string( + string + ' ' + _( + '(but there might be some files in Tartube\'s data' + + ' folder)', + ), + label_length, + ), + ) grid.attach(label2, 0, 2, 1, 5) label2.set_alignment(0, 0.5) else: - label2 = Gtk.Label('This ' + media_type + ' contains:') + if media_type == 'channel': + string = _('This channel contains:') + elif media_type == 'playlist': + string = _('This playlist contains:') + else: + string = _('This folder contains:') + + label2 = Gtk.Label(string) grid.attach(label2, 0, 2, 1, 1) label2.set_alignment(0, 0.5) if folder_count == 1: - label_string = '1 folder' + label_string = _('1 folder') else: - label_string = '' + str(folder_count) + ' folders' + label_string = _('{0} folders').format(str(folder_count)) label3 = Gtk.Label() grid.attach(label3, 0, 3, 1, 1) label3.set_markup(label_string) if channel_count == 1: - label_string = '1 channel' + label_string = _('1 channel') else: - label_string = '' + str(channel_count) + ' channels' + label_string = _('{0} channels').format(str(channel_count)) label4 = Gtk.Label() grid.attach(label4, 0, 4, 1, 1) label4.set_markup(label_string) if playlist_count == 1: - label_string = '1 playlist' + label_string = _('1 playlist') else: - label_string = '' + str(playlist_count) + ' playlists' + label_string = _('{0} playlists').format(str(playlist_count)) label5 = Gtk.Label() grid.attach(label5, 0, 5, 1, 1) label5.set_markup(label_string) if self.video_count == 1: - label_string = '1 video' + label_string = _('1 video') else: - label_string = '' + str(self.video_count) + ' videos' + label_string = _('{0} videos').format(str(self.video_count)) label6 = Gtk.Label() grid.attach(label6, 0, 6, 1, 1) @@ -15131,40 +18275,79 @@ def __init__(self, main_win_obj, media_data_obj, empty_flag): grid.attach(Gtk.HSeparator(), 0, 7, 1, 1) if not empty_flag: - label7 = Gtk.Label( - 'Do you want to delete the ' + media_type + ' from ' \ - + pkg_string + '\'s data\ndirectory, deleting all of its' \ - + ' files, or do you just want to\nremove the ' + media_type \ - + ' from this list?', - ) + + if media_type == 'channel': + string = _( + 'Do you want to delete the channel from Tartube\'s data' \ + + ' folder, or do you just want to remove the channel' \ + + ' from this list?', + ) + elif media_type == 'playlist': + string = _( + 'Do you want to delete the playlist from Tartube\'s data' \ + + ' folder, or do you just want to remove the playlist' \ + + ' from this list?', + ) + else: + string = _( + 'Do you want to delete the folder from Tartube\'s data' \ + + ' folder, or do you just want to remove the folder' \ + + ' from this list?', + ) + else: - label7 = Gtk.Label( - 'Do you want to empty the ' + media_type + ' in ' \ - + pkg_string + '\'s data\ndirectory, deleting all of its' \ - + ' files, or do you just want to\nempty the ' + media_type \ - + ' in this list?', - ) + if media_type == 'channel': + string = _( + 'Do you want to empty the channel in Tartube\'s data' \ + + ' folder, or do you just want to empty the channel' \ + + ' in this list?', + ) + elif media_type == 'playlist': + string = _( + 'Do you want to empty the playlist in Tartube\'s data' \ + + ' folder, or do you just want to empty the playlist' \ + + ' in this list?', + ) + else: + string = _( + 'Do you want to empty the folder in Tartube\'s data' \ + + ' folder, or do you just want to empty the folder' \ + + ' in this list?', + ) + + label7 = Gtk.Label( + utils.tidy_up_long_string( + string, + label_length, + ), + ) grid.attach(label7, 0, 8, 1, 1) label7.set_alignment(0, 0.5) if not empty_flag: - self.button = Gtk.RadioButton.new_with_label_from_widget( - None, - 'Just remove the ' + media_type + ' from this list', - ) + + if media_type == 'channel': + string = _('Just remove the channel from this list') + elif media_type == 'playlist': + string = _('Just remove the playlist from this list') + else: + string = _('Just remove the folder from this list') + else: - self.button = Gtk.RadioButton.new_with_label_from_widget( - None, - 'Just empty the ' + media_type + ' in this list', - ) + if media_type == 'channel': + string = _('Just empty the channel in this list') + elif media_type == 'playlist': + string = _('Just empty the playlist in this list') + else: + string = _('Just empty the folder in this list') + + self.button = Gtk.RadioButton.new_with_label_from_widget(None, string) grid.attach(self.button, 0, 9, 1, 1) self.button2 = Gtk.RadioButton.new_from_widget(self.button) - self.button2.set_label( - 'Delete all files', - ) + self.button2.set_label(_('Delete all files')) grid.attach(self.button2, 0, 10, 1, 1) # Display the dialogue window @@ -15194,7 +18377,7 @@ class ExportDialogue(Gtk.Dialog): def __init__(self, main_win_obj, whole_flag): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15185 __init__') + utils.debug_time('mwn 18380 __init__') # IV list - class objects # ----------------------- @@ -15216,7 +18399,7 @@ def __init__(self, main_win_obj, whole_flag): Gtk.Dialog.__init__( self, - 'Export from database', + _('Export from database'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -15228,53 +18411,65 @@ def __init__(self, main_win_obj, whole_flag): self.set_modal(False) # Set up the dialogue window + spacing_size = self.main_win_obj.spacing_size + label_length = self.main_win_obj.long_string_max_len + 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_border_width(spacing_size) + grid.set_row_spacing(spacing_size) if not whole_flag: - msg = __main__.__prettyname__ \ - + ' is ready to export a partial summary of its\ndatabase,' \ - + ' containing a list of videos, channels,\nplaylists and/or' \ - + ' folders (but not including the\nvideos themselves)' + msg = _( + '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)', + ) else: - msg = __main__.__prettyname__ \ - + ' is ready to export a summary of its database,\n' \ - + ' containing a list of videos, channels, playlists and/or\n' \ - + ' folders (but not including the videos themselves)' + msg = _( + '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)', + ) - label = Gtk.Label(msg) + label = Gtk.Label( + utils.tidy_up_long_string( + msg, + label_length, + ), + ) grid.attach(label, 0, 0, 1, 1) # Separator grid.attach(Gtk.HSeparator(), 0, 1, 1, 1) - label = Gtk.Label('Choose what should be included:') + label = Gtk.Label(_('Choose what should be included:')) grid.attach(label, 0, 2, 1, 1) + label.set_alignment(0, 0.5) # (Store various widgets as IVs, so the calling function can retrieve # their contents) self.checkbutton = Gtk.CheckButton() grid.attach(self.checkbutton, 0, 3, 1, 1) - self.checkbutton.set_label('Include lists of videos') + self.checkbutton.set_label(_('Include lists of videos')) self.checkbutton.set_active(False) self.checkbutton2 = Gtk.CheckButton() grid.attach(self.checkbutton2, 0, 4, 1, 1) - self.checkbutton2.set_label('Include channels') + self.checkbutton2.set_label(_('Include channels')) self.checkbutton2.set_active(True) self.checkbutton3 = Gtk.CheckButton() grid.attach(self.checkbutton3, 0, 5, 1, 1) - self.checkbutton3.set_label('Include playlists') + self.checkbutton3.set_label(_('Include playlists')) self.checkbutton3.set_active(True) self.checkbutton4 = Gtk.CheckButton() grid.attach(self.checkbutton4, 0, 6, 1, 1) - self.checkbutton4.set_label('Preserve folder structure') + self.checkbutton4.set_label(_('Preserve folder structure')) self.checkbutton4.set_active(True) # Separator @@ -15282,7 +18477,7 @@ def __init__(self, main_win_obj, whole_flag): self.checkbutton5 = Gtk.CheckButton() grid.attach(self.checkbutton5, 0, 8, 1, 1) - self.checkbutton5.set_label('Export as plain text') + self.checkbutton5.set_label(_('Export as plain text')) self.checkbutton5.set_active(False) self.checkbutton5.connect('toggled', self.on_checkbutton_toggled) @@ -15307,7 +18502,7 @@ def on_checkbutton_toggled(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15298 on_checkbutton_toggled') + utils.debug_time('mwn 18505 on_checkbutton_toggled') if not checkbutton.get_active(): self.checkbutton.set_sensitive(True) @@ -15342,7 +18537,7 @@ class ImportDialogue(Gtk.Dialog): def __init__(self, main_win_obj, db_dict): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15333 __init__') + utils.debug_time('mwn 18540 __init__') # IV list - class objects # ----------------------- @@ -15368,7 +18563,7 @@ def __init__(self, main_win_obj, db_dict): Gtk.Dialog.__init__( self, - 'Import into database', + _('Import into database'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -15391,7 +18586,7 @@ def __init__(self, main_win_obj, db_dict): grid.set_border_width(main_win_obj.spacing_size) grid.set_row_spacing(main_win_obj.spacing_size) - label = Gtk.Label('Choose which items to import') + label = Gtk.Label(_('Choose which items to import')) grid.attach(label, 0, 0, 4, 1) scrolled = Gtk.ScrolledWindow() @@ -15412,7 +18607,7 @@ def __init__(self, main_win_obj, db_dict): renderer_toggle = Gtk.CellRendererToggle() renderer_toggle.connect('toggled', self.on_checkbutton_toggled) column_toggle = Gtk.TreeViewColumn( - 'Import', + _('Import'), renderer_toggle, active=0, ) @@ -15428,7 +18623,7 @@ def __init__(self, main_win_obj, db_dict): renderer_text = Gtk.CellRendererText() column_text = Gtk.TreeViewColumn( - 'Name', + _('Name'), renderer_text, text=2, ) @@ -15448,20 +18643,20 @@ def __init__(self, main_win_obj, db_dict): self.checkbutton = Gtk.CheckButton() grid.attach(self.checkbutton, 0, 2, 1, 1) - self.checkbutton.set_label('Import videos') + self.checkbutton.set_label(_('Import videos')) self.checkbutton.set_active(False) self.checkbutton2 = Gtk.CheckButton() grid.attach(self.checkbutton2, 1, 2, 1, 1) - self.checkbutton2.set_label('Merge channels/playlists/folders') + self.checkbutton2.set_label(_('Merge channels/playlists/folders')) self.checkbutton2.set_active(False) - button = Gtk.Button.new_with_label('Select all') + button = Gtk.Button.new_with_label(_('Select all')) grid.attach(button, 2, 2, 1, 1) button.set_hexpand(False) button.connect('clicked', self.on_select_all_clicked) - button2 = Gtk.Button.new_with_label('Deselect all') + button2 = Gtk.Button.new_with_label(_('Unselect all')) grid.attach(button2, 3, 2, 1, 1) button2.set_hexpand(False) button2.connect('clicked', self.on_deselect_all_clicked) @@ -15493,9 +18688,10 @@ def __init__(self, main_win_obj, db_dict): pixbuf = main_win_obj.pixbuf_dict[mini_dict['type'] + '_small'] text = mini_dict['display_name'] if mini_dict['video_count'] == 1: - text += ' [ 1 video ]' + text += ' [ ' + _('1 video') + ' ]' elif mini_dict['video_count']: - text += ' [ ' + str(mini_dict['video_count']) + ' videos ]' + text += ' [ ' + + _('{0} videos').format(str(mini_dict['video_count'])) + ' ]' self.liststore.append( [True, pixbuf, text, mini_dict['dbid']] ) @@ -15549,7 +18745,7 @@ def convert_to_list(self, db_dict, converted_list, """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15540 convert_to_list') + utils.debug_time('mwn 18748 convert_to_list') # (Sorting function for the code immediately below) def sort_dict_by_name(this_dict): @@ -15612,7 +18808,7 @@ def on_checkbutton_toggled(self, checkbutton, path): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15603 on_checkbutton_toggled') + utils.debug_time('mwn 18811 on_checkbutton_toggled') # The user has clicked on the checkbutton widget, so toggle the widget # itself @@ -15637,7 +18833,7 @@ def on_select_all_clicked(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15628 on_select_all_clicked') + utils.debug_time('mwn 18836 on_select_all_clicked') for path in range(0, len(self.liststore)): self.liststore[path][0] = True @@ -15659,7 +18855,7 @@ def on_deselect_all_clicked(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15650 on_deselect_all_clicked') + utils.debug_time('mwn 18858 on_deselect_all_clicked') for path in range(0, len(self.liststore)): self.liststore[path][0] = False @@ -15692,7 +18888,7 @@ class MountDriveDialogue(Gtk.Dialog): def __init__(self, main_win_obj, unwriteable_flag=False): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15683 __init__') + utils.debug_time('mwn 18891 __init__') # IV list - class objects # ----------------------- @@ -15722,7 +18918,7 @@ def __init__(self, main_win_obj, unwriteable_flag=False): Gtk.Dialog.__init__( self, - 'Mount drive', + _('Mount drive'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ) @@ -15736,21 +18932,22 @@ def __init__(self, main_win_obj, unwriteable_flag=False): box.add(grid) grid.set_border_width(main_win_obj.spacing_size) grid.set_row_spacing(main_win_obj.spacing_size) + # (Actually, the grid width of the area to the right of the Tartube + # logo) grid_width = 2 - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' + image = Gtk.Image.new_from_pixbuf( + main_win_obj.pixbuf_dict['system_icon'], + ) + grid.attach(image, 0, 0, 1, 3) label = Gtk.Label( - 'The ' + __main__.__prettyname__ + ' data ' + folder \ - + ' is set to:', + _('The Tartube data folder is set to:'), ) - grid.attach(label, 0, 0, grid_width, 1) + grid.attach(label, 1, 0, grid_width, 1) label = Gtk.Label() - grid.attach(label, 0, 1, grid_width, 1) + grid.attach(label, 1, 1, grid_width, 1) label.set_markup( '' \ + utils.shorten_string(main_win_obj.app_obj.data_dir, 50) \ @@ -15758,31 +18955,28 @@ def __init__(self, main_win_obj, unwriteable_flag=False): ) if not unwriteable_flag: - label2 = Gtk.Label( - '...but this ' + folder + ' doesn\'t exist', - ) + label2 = Gtk.Label(_('...but this folder doesn\'t exist')) else: label2 = Gtk.Label( - '...but ' + __main__.__prettyname__ \ - + ' cannot write to this ' + folder, + _('...but Tartube cannot write to this folder'), ) - grid.attach(label2, 0, 2, grid_width, 1) + grid.attach(label2, 1, 2, grid_width, 1) # Separator - grid.attach(Gtk.HSeparator(), 0, 3, grid_width, 1) + grid.attach(Gtk.HSeparator(), 1, 3, grid_width, 1) self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( None, - 'I have mounted the drive, please try again', + _('I have mounted the drive, please try again'), ) - grid.attach(self.radiobutton, 0, 4, grid_width, 1) + grid.attach(self.radiobutton, 1, 4, grid_width, 1) self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget( self.radiobutton, - 'Use this data ' + folder + ':', + _('Use this data folder:'), ) - grid.attach(self.radiobutton2, 0, 5, grid_width, 1) + grid.attach(self.radiobutton2, 1, 5, grid_width, 1) # signal_connect appears below store = Gtk.ListStore(str) @@ -15790,7 +18984,7 @@ def __init__(self, main_win_obj, unwriteable_flag=False): store.append([item]) self.combo = Gtk.ComboBox.new_with_model(store) - grid.attach(self.combo, 0, 6, grid_width, 1) + grid.attach(self.combo, 1, 6, grid_width, 1) self.combo.set_hexpand(True) renderer_text = Gtk.CellRendererText() self.combo.pack_start(renderer_text, True) @@ -15807,31 +19001,31 @@ def __init__(self, main_win_obj, unwriteable_flag=False): self.radiobutton3 = Gtk.RadioButton.new_with_label_from_widget( self.radiobutton2, - 'Select a different data ' + folder, + _('Select a different data folder'), ) - grid.attach(self.radiobutton3, 0, 7, grid_width, 1) + grid.attach(self.radiobutton3, 1, 7, grid_width, 1) self.radiobutton4 = Gtk.RadioButton.new_with_label_from_widget( self.radiobutton3, - 'Use the default data ' + folder, + _('Use the default data folder'), ) - grid.attach(self.radiobutton4, 0, 8, grid_width, 1) + grid.attach(self.radiobutton4, 1, 8, grid_width, 1) self.radiobutton5 = Gtk.RadioButton.new_with_label_from_widget( self.radiobutton4, - 'Shut down ' + __main__.__prettyname__, + _('Shut down Tartube'), ) - grid.attach(self.radiobutton5, 0, 9, grid_width, 1) + grid.attach(self.radiobutton5, 1, 9, grid_width, 1) # Separator - grid.attach(Gtk.HSeparator(), 0, 10, grid_width, 1) + grid.attach(Gtk.HSeparator(), 1, 10, grid_width, 1) - button = Gtk.Button.new_with_label('Cancel') - grid.attach(button, 0, 11, 1, 1) + button = Gtk.Button.new_with_label(_('Cancel')) + grid.attach(button, 1, 11, 1, 1) button.connect('clicked', self.on_cancel_button_clicked) - button2 = Gtk.Button.new_with_label('OK') - grid.attach(button2, 1, 11, 1, 1) + button2 = Gtk.Button.new_with_label(_('OK')) + grid.attach(button2, 2, 11, 1, 1) button2.connect('clicked', self.on_ok_button_clicked) # Display the dialogue window @@ -15857,7 +19051,7 @@ def on_ok_button_clicked(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15848 on_ok_button_clicked') + utils.debug_time('mwn 19054 on_ok_button_clicked') if self.radiobutton.get_active(): self.do_try_again() @@ -15898,7 +19092,7 @@ def on_cancel_button_clicked(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15889 on_cancel_button_clicked') + utils.debug_time('mwn 19095 on_cancel_button_clicked') self.available_flag = False self.destroy() @@ -15918,7 +19112,7 @@ def on_radiobutton_toggled(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15909 on_radiobutton_toggled') + utils.debug_time('mwn 19115 on_radiobutton_toggled') if button.get_active(): self.combo.set_sensitive(True) @@ -15937,7 +19131,7 @@ def do_try_again(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15928 do_try_again') + utils.debug_time('mwn 19134 do_try_again') app_obj = self.main_win_obj.app_obj @@ -15950,15 +19144,11 @@ def do_try_again(self): else: # Data directory still does not exist. Inform the user - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' - - mini_win = app_obj.dialogue_manager_obj.show_msg_dialogue( - 'The ' + folder + ' still doesn\'t exist. Please try a' \ + _( + 'The folder still doesn\'t exist. Please try a' \ + ' different option', + ), 'error', 'ok', self, # Parent window is this window @@ -15975,7 +19165,7 @@ def do_select_dir(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15966 do_select_dir') + utils.debug_time('mwn 19168 do_select_dir') if (self.main_win_obj.app_obj.prompt_user_for_data_dir()): @@ -16004,7 +19194,7 @@ class RemoveLockFileDialogue(Gtk.Dialog): def __init__(self, main_win_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15995 __init__') + utils.debug_time('mwn 19197 __init__') # IV list - class objects # ----------------------- @@ -16023,7 +19213,7 @@ def __init__(self, main_win_obj): Gtk.Dialog.__init__( self, - 'Stale lockfile', + _('Stale lockfile'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ) @@ -16031,40 +19221,65 @@ def __init__(self, main_win_obj): self.set_modal(True) # Set up the dialogue window + spacing_size = self.main_win_obj.spacing_size + label_length = self.main_win_obj.long_string_max_len + box = self.get_content_area() + # Tartube logo on the left, widgets on the right + hbox = Gtk.HBox() + box.add(hbox) + + # Logo in the top corner + vbox = Gtk.VBox() + hbox.pack_start(vbox, False, False, spacing_size) + + image = Gtk.Image.new_from_pixbuf( + main_win_obj.pixbuf_dict['system_icon'], + ) + vbox.pack_start(image, False, False, spacing_size) + grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) + hbox.pack_start(grid, False, False, spacing_size) + grid.set_border_width(spacing_size) + grid.set_row_spacing(spacing_size) + # (Actually, the grid width of the area to the right of the Tartube + # logo) grid_width = 2 label = Gtk.Label( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' database file, because\nanother instance of ' \ - + __main__.__prettyname__ + ' seems to be using it' \ - + '\n\nIf you are SURE that this is the only instance of\n' \ - + __main__.__prettyname__ + ' running on your system,' \ - + ' click \'Yes\' to\nremove the protection (and then' \ - + ' restart ' + __main__.__prettyname__ + ')' \ - + '\n\nIf you are not sure, then click \'No\'', + utils.tidy_up_long_string( + _( + 'Failed to load the Tartube database file, because another' \ + + ' instance 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)', + ), + label_length, + ) + '\n\n' + _('If you are not sure, then click \'No\''), ) - grid.attach(label, 0, 0, grid_width, 1) + grid.attach(label, 1, 0, grid_width, 1) # Separator - grid.attach(Gtk.HSeparator(), 0, 1, grid_width, 1) + grid.attach(Gtk.HSeparator(), 1, 1, grid_width, 1) button = Gtk.Button.new_with_label( - 'Yes, I\'m sure', + _('Yes, I\'m sure'), ) - grid.attach(button, 0, 2, 1, 1) + 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', + _('No, I\'m not sure'), ) - grid.attach(button2, 0, 3, 1, 1) + grid.attach(button2, 1, 3, 1, 1) button2.set_hexpand(True) button2.connect('clicked', self.on_no_button_clicked) @@ -16089,7 +19304,7 @@ def on_yes_button_clicked(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16080 on_yes_button_clicked') + utils.debug_time('mwn 19307 on_yes_button_clicked') self.remove_flag = True self.destroy() @@ -16109,7 +19324,7 @@ def on_no_button_clicked(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16100 on_no_button_clicked') + utils.debug_time('mwn 19327 on_no_button_clicked') self.remove_flag = False self.destroy() @@ -16138,7 +19353,7 @@ class RenameContainerDialogue(Gtk.Dialog): def __init__(self, main_win_obj, media_data_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16129 __init__') + utils.debug_time('mwn 19356 __init__') # IV list - class objects # ----------------------- @@ -16155,10 +19370,16 @@ def __init__(self, main_win_obj, media_data_obj): # ---- media_type = media_data_obj.get_type() + if media_type == 'channel': + string = _('Rename channel') + elif media_type == 'playlist': + string = _('Rename playlist') + else: + string = _('Rename folder') Gtk.Dialog.__init__( self, - 'Rename ' + media_type, + string, main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -16177,12 +19398,20 @@ def __init__(self, main_win_obj, media_data_obj): grid.set_border_width(main_win_obj.spacing_size) grid.set_row_spacing(main_win_obj.spacing_size) - label = Gtk.Label( - 'Set the new name for the ' + media_type + ' \'' \ - + media_data_obj.name \ - + '\'\n\nNB This procedure will make changes to your filesystem!', - ) + if media_type == 'channel': + string = _('Set the new name for the channel:') + elif media_type == 'playlist': + string = _('Set the new name for the playlist:') + else: + string = _('Set the new name for the folder:') + + label = Gtk.Label() grid.attach(label, 0, 0, 1, 1) + label.set_markup( + string + '\n\n' + media_data_obj.name + '\n\n' + _( + 'N.B. This procedure will modify your filesystem!\n', + ) + ) # (Store various widgets as IVs, so the calling function can retrieve # their contents) @@ -16217,7 +19446,7 @@ class SetDestinationDialogue(Gtk.Dialog): def __init__(self, main_win_obj, media_data_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16208 __init__') + utils.debug_time('mwn 19449 __init__') # IV list - class objects # ----------------------- @@ -16241,7 +19470,7 @@ def __init__(self, main_win_obj, media_data_obj): Gtk.Dialog.__init__( self, - 'Set download destination', + _('Set download destination'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -16253,45 +19482,72 @@ def __init__(self, main_win_obj, media_data_obj): self.set_modal(False) # Set up the dialogue window + spacing_size = self.main_win_obj.spacing_size + label_length = self.main_win_obj.long_string_max_len + 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) - - if os.name == 'nt': - dir_name = 'folder' - else: - dir_name = 'directory' + grid.set_border_width(spacing_size) + grid.set_row_spacing(spacing_size) media_type = media_data_obj.get_type() + if media_type == 'channel': + string = _( + 'This channel can store its videos in its own system folder,' \ + + ' or it can store them in a different system folder', + ) + elif media_type == 'playlist': + string = _( + 'This playlist can store its videos in its own system' \ + + ' folder, or it can store them in a different folder', + ) + else: + string = _( + 'This folder can store its videos in its own system folder,' \ + + ' or it can store them in a different system folder', + ) label = Gtk.Label( - 'This ' + media_type + ' can store its videos in its own ' \ - + dir_name + ', or it can store\nthem in a different ' \ - + dir_name \ - + '\n\nChoose a different ' + dir_name + ' if:' \ - + '\n\n1. You want to add a channel and its playlists, without' \ - + ' downloading\nthe same video twice' \ - + '\n\n2. A video creator has channels on both YouTube and' \ - + ' BitChute, and\nyou want to add both without downloading the' \ - + ' same video twice', + utils.tidy_up_long_string( + string, + label_length, + ) + '\n\n' + _('Choose a different system folder if:') + '\n\n' \ + + utils.tidy_up_long_string( + _( + '1. You want to add a channel and its playlists, without' \ + + ' downloading the same video twice', + ), + label_length, + ) + '\n\n' \ + + utils.tidy_up_long_string( + _( + '2. A video creator has channels on both YouTube and' \ + + ' BitChute, and you want to add both without' \ + + ' downloading the same video twice', + ), + label_length, + ) ) grid.attach(label, 0, 0, 1, 1) # Separator grid.attach(Gtk.HSeparator(), 0, 1, 1, 1) - radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - 'Use this ' + media_type + '\'s own ' + dir_name, - ) + if media_type == 'channel': + string = _('Use this channel\'s own folder') + elif media_type == 'playlist': + string = _('Use this playlist\'s own folder') + else: + string = _('Use this folder\'s own system folder') + + radiobutton = Gtk.RadioButton.new_with_label_from_widget(None, string) grid.attach(radiobutton, 0, 2, 1, 1) # Signal connect appears below radiobutton2 = Gtk.RadioButton.new_from_widget(radiobutton) - radiobutton2.set_label('Choose a different ' + dir_name + ':') + radiobutton2.set_label('Choose a different system folder:') grid.attach(radiobutton2, 0, 3, 1, 1) # Signal connect appears below @@ -16417,7 +19673,7 @@ def on_combo_changed(self, combo, radiobutton2): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16408 on_combo_changed') + utils.debug_time('mwn 19676 on_combo_changed') tree_iter = combo.get_active_iter() model = combo.get_model() @@ -16453,7 +19709,7 @@ def on_radiobutton_toggled(self, radiobutton, combo): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16444 on_radiobutton_toggled') + utils.debug_time('mwn 19712 on_radiobutton_toggled') if radiobutton.get_active(): combo.set_sensitive(False) @@ -16479,7 +19735,7 @@ def on_radiobutton2_toggled(self, radiobutton2, combo): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16470 on_radiobutton2_toggled') + utils.debug_time('mwn 19378 on_radiobutton2_toggled') if radiobutton2.get_active(): combo.set_sensitive(True) @@ -16509,7 +19765,9 @@ class SetDirectoryDialogue_LinuxBSD(Gtk.Dialog): Args: - main_win_obj (mainwin.MainWin): The parent main window + main_win_obj (mainwin.FakeMainWin): Tartube's main window has not been + created yet, so the calling function creates a fake (and invisible) + one default_dir (str): The path to the default data directory, which is the current value of mainapp.TartubeApp.data_dir @@ -16523,11 +19781,12 @@ class SetDirectoryDialogue_LinuxBSD(Gtk.Dialog): def __init__(self, main_win_obj, default_dir): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16514 __init__') + utils.debug_time('mwn 19784 __init__') # IV list - class objects # ----------------------- - # Tartube's main window + # Tartube's main window has not been created yet, so the calling + # function creates a fake (and invisible) one self.main_win_obj = main_win_obj @@ -16542,7 +19801,7 @@ def __init__(self, main_win_obj, default_dir): Gtk.Dialog.__init__( self, - 'Welcome to ' + __main__.__prettyname__ + '!', + _('Welcome to Tartube!'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -16553,31 +19812,33 @@ def __init__(self, main_win_obj, default_dir): self.set_modal(True) # Set up the dialogue window + spacing_size = self.main_win_obj.spacing_size + label_length = self.main_win_obj.label_length + 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 * 2) + grid.set_border_width(spacing_size) + grid.set_row_spacing(spacing_size) + grid.set_column_spacing(spacing_size * 2) image = Gtk.Image.new_from_pixbuf( main_win_obj.pixbuf_dict['system_icon'], ) grid.attach(image, 0, 0, 1, 3) - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' - label = Gtk.Label() grid.attach(label, 1, 0, 1, 1) label.set_markup( - __main__.__prettyname__ + '\'s data ' + folder \ - + ' will be:\n\n' \ + _('Tartube\'s data folder will be:') + '\n\n' \ + html.escape( - utils.tidy_up_long_string(default_dir, 50, True, True), + utils.tidy_up_long_string( + default_dir, + label_length, + True, + True, + ), ) + '\n', ) @@ -16585,12 +19846,12 @@ def __init__(self, main_win_obj, default_dir): # their contents) self.button = Gtk.RadioButton.new_with_label_from_widget( None, - 'Use this ' + folder + _('Use this folder'), ) grid.attach(self.button, 1, 1, 1, 1) self.button2 = Gtk.RadioButton.new_from_widget(self.button) - self.button2.set_label('Choose a different ' + folder) + self.button2.set_label(_('Choose a different folder')) grid.attach(self.button2, 1, 2, 1, 1) # Display the dialogue window @@ -16608,7 +19869,9 @@ class SetDirectoryDialogue_MSWin(Gtk.Dialog): Args: - main_win_obj (mainwin.MainWin): The parent main window + main_win_obj (mainwin.FakeMainWin): Tartube's main window has not been + created yet, so the calling function creates a fake (and invisible) + one """ @@ -16619,11 +19882,12 @@ class SetDirectoryDialogue_MSWin(Gtk.Dialog): def __init__(self, main_win_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16610 __init__') + utils.debug_time('mwn 19885 __init__') # IV list - class objects # ----------------------- - # Tartube's main window + # Tartube's main window has not been created yet, so the calling + # function creates a fake (and invisible) one self.main_win_obj = main_win_obj @@ -16632,7 +19896,7 @@ def __init__(self, main_win_obj): Gtk.Dialog.__init__( self, - 'Welcome to ' + __main__.__prettyname__ + '!', + _('Welcome to Tartube!'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -16643,44 +19907,37 @@ def __init__(self, main_win_obj): self.set_modal(True) # Set up the dialogue window + spacing_size = self.main_win_obj.spacing_size + label_length = self.main_win_obj.label_length + 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 * 2) + grid.set_border_width(spacing_size) + grid.set_row_spacing(spacing_size) + grid.set_column_spacing(spacing_size * 2) image = Gtk.Image.new_from_pixbuf( main_win_obj.pixbuf_dict['system_icon'], ) grid.attach(image, 0, 0, 1, 1) - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' - - line_list = [ - 'Click OK to create a ' + folder + ' in which ' \ - + __main__.__prettyname__ + ' can store its videos', - 'If you have used ' + __main__.__prettyname__ + ' before,' \ - + ' you can select an existing ' + folder + ' instead of' \ - + ' creating a new one', - ] - newline = '\n\n' line_list = [ utils.tidy_up_long_string( - 'Click OK to create a ' + folder + ' in which ' \ - + __main__.__prettyname__ + ' can store its videos', - 40, + _( + 'Click OK to create a folder in which Tartube can store its' \ + + ' videos', + ), + label_length, ), utils.tidy_up_long_string( - 'If you have used ' + __main__.__prettyname__ + ' before,' \ - + ' you can select an existing ' + folder + ' instead of' \ - + ' creating a new one', - 40, + _( + 'If you have used Tartube before, you can select an existing' \ + + ' folder instead of creating a new one', + ), + label_length, ), ] @@ -16714,7 +19971,7 @@ class SetNicknameDialogue(Gtk.Dialog): def __init__(self, main_win_obj, media_data_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16705 __init__') + utils.debug_time('mwn 19974 __init__') # IV list - class objects # ----------------------- @@ -16732,7 +19989,7 @@ def __init__(self, main_win_obj, media_data_obj): Gtk.Dialog.__init__( self, - 'Set nickname', + _('Set nickname'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -16744,18 +20001,39 @@ def __init__(self, main_win_obj, media_data_obj): self.set_modal(False) # Set up the dialogue window + spacing_size = self.main_win_obj.spacing_size + label_length = self.main_win_obj.long_string_max_len + 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_border_width(spacing_size) + grid.set_row_spacing(spacing_size) media_type = media_data_obj.get_type() + if media_type == 'channel': + msg = _( + 'Set a nickname for the channel \'{0}\' (or leave it blank' \ + + ' to reset the nickname)', + ).format(media_data_obj.name) + elif media_type == 'playlist': + msg = _( + 'Set a nickname for the playlist \'{0}\' (or leave it blank' \ + + ' to reset the nickname)', + ).format(media_data_obj.name) + else: + msg = _( + 'Set a nickname for the folder \'{0}\' (or leave it blank' \ + + ' to reset the nickname)', + ).format(media_data_obj.name) + + label = Gtk.Label( - 'Set the nickname for the ' + media_type + ' \'' \ - + media_data_obj.name \ - + '\'\n(or leave it blank to reset the nickname)', + utils.tidy_up_long_string( + msg, + label_length, + ), ) grid.attach(label, 0, 0, 1, 1) @@ -16794,7 +20072,7 @@ class SystemCmdDialogue(Gtk.Dialog): def __init__(self, main_win_obj, media_data_obj): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16785 __init__') + utils.debug_time('mwn 20075 __init__') # IV list - class objects # ----------------------- @@ -16812,7 +20090,7 @@ def __init__(self, main_win_obj, media_data_obj): Gtk.Dialog.__init__( self, - 'Show system command', + _('Show system command'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, (Gtk.STOCK_OK, Gtk.ResponseType.OK), @@ -16856,7 +20134,7 @@ def __init__(self, main_win_obj, media_data_obj): # Initialise the textbuffer's contents self.update_textbuffer(media_data_obj) - button = Gtk.Button('Update') + button = Gtk.Button(_('Update')) grid.attach(button, 0, 2, 1, 1) button.set_hexpand(True) button.connect( @@ -16865,7 +20143,7 @@ def __init__(self, main_win_obj, media_data_obj): media_data_obj, ) - button2 = Gtk.Button('Copy to clipboard') + button2 = Gtk.Button(_('Copy to clipboard')) grid.attach(button2, 1, 2, 1, 1) button2.set_hexpand(True) button2.connect( @@ -16904,7 +20182,7 @@ def update_textbuffer(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16895 update_textbuffer') + utils.debug_time('mwn 20185 update_textbuffer') # Get the options.OptionsManager object that applies to this media # data object @@ -16960,7 +20238,7 @@ def on_copy_clicked(self, button, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16951 on_copy_clicked') + utils.debug_time('mwn 20241 on_copy_clicked') # Obtain the system command used to download this media data object, # and display it in the textbuffer @@ -16988,7 +20266,7 @@ def on_update_clicked(self, button, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16979 on_update_clicked') + utils.debug_time('mwn 20269 on_update_clicked') # Obtain the system command used to download this media data object, # and display it in the textbuffer @@ -17020,7 +20298,7 @@ class TestCmdDialogue(Gtk.Dialog): def __init__(self, main_win_obj, source_url=None): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17011 __init__') + utils.debug_time('mwn 20301 __init__') # IV list - class objects # ----------------------- @@ -17039,7 +20317,7 @@ def __init__(self, main_win_obj, source_url=None): Gtk.Dialog.__init__( self, - 'Test youtube-dl', + _('Test youtube-dl'), main_win_obj, Gtk.DialogFlags.DESTROY_WITH_PARENT, ( @@ -17059,7 +20337,7 @@ def __init__(self, main_win_obj, source_url=None): grid.set_row_spacing(main_win_obj.spacing_size) label = Gtk.Label( - 'URL of the video to download (optional)' + _('URL of the video to download (optional)'), ) grid.attach(label, 0, 0, 1, 1) @@ -17070,7 +20348,7 @@ def __init__(self, main_win_obj, source_url=None): self.entry.set_text(source_url) label2 = Gtk.Label( - 'youtube-dl command line options (optional)' + _('youtube-dl command line options (optional)'), ) grid.attach(label2, 0, 2, 1, 1) @@ -17122,7 +20400,7 @@ class TidyDialogue(Gtk.Dialog): def __init__(self, main_win_obj, media_data_obj=None): if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17113 __init__') + utils.debug_time('mwn 20403 __init__') # IV list - class objects # ----------------------- @@ -17148,13 +20426,13 @@ def __init__(self, main_win_obj, media_data_obj=None): # ---- if media_data_obj is None: - title = 'Tidy up files' + title = _('Tidy up files') elif isinstance(media_data_obj, media.Channel): - title = 'Tidy up channel' + title = _('Tidy up channel') elif isinstance(media_data_obj, media.Channel): - title = 'Tidy up playlist' + title = _('Tidy up playlist') else: - title = 'Tidy up folder' + title = _('Tidy up folder') Gtk.Dialog.__init__( self, @@ -17170,22 +20448,25 @@ def __init__(self, main_win_obj, media_data_obj=None): self.set_modal(False) # Set up the dialogue window + spacing_size = self.main_win_obj.spacing_size + label_length = self.main_win_obj.quite_long_string_max_len + 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_border_width(spacing_size) + grid.set_row_spacing(spacing_size) # Left column self.checkbutton = Gtk.CheckButton() grid.attach(self.checkbutton, 0, 0, 1, 1) - self.checkbutton.set_label('Check that videos are not corrupted') + self.checkbutton.set_label(_('Check that videos are not corrupted')) self.checkbutton.connect('toggled', self.on_checkbutton_toggled) self.checkbutton2 = Gtk.CheckButton() grid.attach(self.checkbutton2, 0, 1, 1, 1) - self.checkbutton2.set_label('Delete corrupted video files') + self.checkbutton2.set_label(_('Delete corrupted video files')) self.checkbutton2.set_sensitive(False) if not mainapp.HAVE_MOVIEPY_FLAG \ @@ -17195,53 +20476,60 @@ def __init__(self, main_win_obj, media_data_obj=None): self.checkbutton3 = Gtk.CheckButton() grid.attach(self.checkbutton3, 0, 2, 1, 1) - self.checkbutton3.set_label('Check that videos do/don\'t exist') + self.checkbutton3.set_label(_('Check that videos do/don\'t exist')) self.checkbutton4 = Gtk.CheckButton() grid.attach(self.checkbutton4, 0, 3, 1, 2) self.checkbutton4.set_label( - 'Delete downloaded video files (doesn\'t\nremove videos from ' \ - + utils.upper_case_first(__main__.__packagename__) \ - + '\'s database)', + utils.tidy_up_long_string( + _( + 'Delete downloaded video files (doesn\'t remove videos from' \ + + ' Tartube\'s database)', + ), + label_length, + ), ) self.checkbutton4.connect('toggled', self.on_checkbutton4_toggled) self.checkbutton5 = Gtk.CheckButton() grid.attach(self.checkbutton5, 0, 5, 1, 1) self.checkbutton5.set_label( - 'Also delete all video/audio files with the\nsame name', + utils.tidy_up_long_string( + _('Also delete all video/audio files with the same name'), + label_length, + ), ) 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') + self.checkbutton6.set_label(_('Delete all description files')) self.checkbutton7 = Gtk.CheckButton() grid.attach(self.checkbutton7, 1, 1, 1, 1) - self.checkbutton7.set_label('Delete all metadata (JSON) files') + self.checkbutton7.set_label(_('Delete all metadata (JSON) files')) self.checkbutton8 = Gtk.CheckButton() grid.attach(self.checkbutton8, 1, 2, 1, 1) - self.checkbutton8.set_label('Delete all annotation files') + self.checkbutton8.set_label(_('Delete all annotation files')) self.checkbutton9 = Gtk.CheckButton() grid.attach(self.checkbutton9, 1, 3, 1, 1) - self.checkbutton9.set_label('Delete all thumbnail files') + self.checkbutton9.set_label(_('Delete all thumbnail files')) self.checkbutton10 = Gtk.CheckButton() grid.attach(self.checkbutton10, 1, 4, 1, 1) - self.checkbutton10.set_label('Delete all youtube-dl archive files') + self.checkbutton10.set_label(_('Delete all youtube-dl archive files')) # Bottom strip - button = Gtk.Button.new_with_label('Select all') + button = Gtk.Button.new_with_label(_('Select all')) grid.attach(button, 0, 6, 1, 1) button.set_hexpand(False) button.connect('clicked', self.on_select_all_clicked) - button = Gtk.Button.new_with_label('Select none') + 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) @@ -17264,7 +20552,7 @@ def on_checkbutton_toggled(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17255 on_checkbutton_toggled') + utils.debug_time('mwn 20555 on_checkbutton_toggled') if not checkbutton.get_active(): self.checkbutton2.set_active(False) @@ -17288,7 +20576,7 @@ def on_checkbutton4_toggled(self, checkbutton): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17279 on_checkbutton4_toggled') + utils.debug_time('mwn 20579 on_checkbutton4_toggled') if not checkbutton.get_active(): self.checkbutton5.set_active(False) @@ -17311,7 +20599,7 @@ def on_select_all_clicked(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17302 on_select_all_clicked') + utils.debug_time('mwn 20602 on_select_all_clicked') self.checkbutton.set_active(True) self.checkbutton2.set_active(True) @@ -17338,7 +20626,7 @@ def on_select_none_clicked(self, button): """ if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17239 on_select_none_clicked') + utils.debug_time('mwn 20629 on_select_none_clicked') self.checkbutton.set_active(False) self.checkbutton2.set_active(False) diff --git a/tartube/media.py b/tartube/media.py index c86dce73..286aaf43 100755 --- a/tartube/media.py +++ b/tartube/media.py @@ -35,6 +35,8 @@ # Import our modules import mainapp import utils +# Use same gettext translations +from mainapp import _ # Classes @@ -268,6 +270,9 @@ def del_child(self, child_obj): if child_obj.fav_flag: self.fav_count -= 1 + if child_obj.live_mode: + self.live_count -= 1 + if child_obj.new_flag: self.new_count -= 1 @@ -302,26 +307,30 @@ def fetch_tooltip_text(self, app_obj, max_length): if not isinstance(self, Folder): - text += 'Source:\n' + translate_note = _( + 'TRANSLATOR\'S NOTE: Source = video/channel/playlist URL', + ) + + text += _('Source:') + '\n' if self.source is None: - text += ' ' + text += '<' + _('unknown') + '>' else: text += self.source text += '\n\n' - text += 'Location:\n' + text += _('Location:') + '\n' location = self.get_default_dir(app_obj) if location is None: - text += ' ' + text += '<' + _('unknown') + '>' else: text += location if self.master_dbid != self.dbid: dest_obj = app_obj.media_reg_dict[self.master_dbid] - text += '\n\nDownload destination: ' + dest_obj.name + text += '\n\n' + _('Download destination:') + ' ' + dest_obj.name # Need to escape question marks or we'll get a markup error text = re.sub('&', '&', text) @@ -501,27 +510,14 @@ def prepare_export(self, include_video_flag, include_channel_flag, # Ignore the types of media data object that we don't require (and all # of their children) - if isinstance(self, Video): - # (This shouldn't occur) - return - - elif isinstance(self, Channel): - if not include_channel_flag: - return - else: - media_type = 'channel' - - elif isinstance(self, Playlist): - if not include_playlist_flag: - return - else: - media_type = 'playlist' + media_type = self.get_type() - elif isinstance(self, Folder): - if self.fixed_flag: - return - else: - media_type = 'folder' + # (This function should not be called for media.Video objects) + if media_type == 'video' \ + or (media_type == 'channel' and not include_channel_flag) \ + or (media_type == 'playlist' and not include_playlist_flag) \ + or (media_type == 'folder' and self.fixed_flag): + return {} # This dictionary contains values for the children of this object db_dict = {} @@ -606,27 +602,14 @@ def prepare_flat_export(self, db_dict, include_video_flag, # Ignore the types of media data object that we don't require (and all # of their children) - if isinstance(self, Video): - # (This shouldn't occur) - return db_dict + media_type = self.get_type() - elif isinstance(self, Channel): - if not include_channel_flag: - return db_dict - else: - media_type = 'channel' - - elif isinstance(self, Playlist): - if not include_playlist_flag: - return db_dict - else: - media_type = 'playlist' - - elif isinstance(self, Folder): - if self.fixed_flag: - return db_dict - else: - media_type = 'folder' + # (This function should not be called for media.Video objects) + if media_type == 'video' \ + or (media_type == 'channel' and not include_channel_flag) \ + or (media_type == 'playlist' and not include_playlist_flag) \ + or (media_type == 'folder' and self.fixed_flag): + return db_dict # Add values to the dictionary if media_type == 'channel' or media_type == 'playlist': @@ -700,6 +683,7 @@ def recalculate_counts(self): self.bookmark_count = 0 self.dl_count = 0 self.fav_count = 0 + self.live_count = 0 self.new_count = 0 self.waiting_count = 0 @@ -717,6 +701,9 @@ def recalculate_counts(self): if child_obj.fav_flag: self.fav_count += 1 + if child_obj.live_mode: + self.live_count += 1 + if child_obj.new_flag: self.new_count += 1 @@ -728,7 +715,7 @@ def recalculate_counts(self): def reset_counts(self, vid_count, bookmark_count, dl_count, fav_count, - new_count, waiting_count): + live_count, new_count, waiting_count): """Called by mainapp.TartubeApp.update_db(). @@ -743,6 +730,7 @@ def reset_counts(self, vid_count, bookmark_count, dl_count, fav_count, self.bookmark_count = bookmark_count self.dl_count = dl_count self.fav_count = fav_count + self.live_count = live_count self.new_count = new_count self.waiting_count = waiting_count @@ -785,6 +773,16 @@ def dec_fav_count(self): self.fav_count -= 1 + def inc_live_count(self): + + self.live_count += 1 + + + def dec_live_count(self): + + self.live_count -= 1 + + def set_master_dbid(self, app_obj, dbid): if dbid == self.master_dbid: @@ -1100,13 +1098,21 @@ def do_sort(self, obj1, obj2): """ + # Livestreams come before everything else + if obj1.live_mode > obj2.live_mode: + return -1 + elif obj1.live_mode < obj2.live_mode: + return 1 + # The video's index is not relevant unless sorting a playlist - if isinstance(self, Playlist) \ + elif isinstance(self, Playlist) \ and obj1.index is not None and obj2.index is not None: if obj1.index < obj2.index: return -1 else: return 1 + + # Otherwise sort by upload time elif obj1.upload_time is not None and obj2.upload_time is not None: if obj1.upload_time > obj2.upload_time: return -1 @@ -1146,6 +1152,41 @@ def sort_children(self): self.child_list.sort(key=functools.cmp_to_key(self.do_sort)) + def get_livestreams(self, app_obj, live_mode=None): + + """Can be called by anything. + + Returns a list of child media.Video objects which are marked as + livestreams. + + If a live_mode is specified, returns either waiting livestreams or + broadcasting livestreams (only). + + Args: + + app_obj (mainapp.TartubeApp): The main application + + live_mode (int or None): 1 to return waiting livestreams only, 2 to + return broadcasting livestreams only, None to return all + livestreams + + Returns: + + A list of media.Video objects (may be an empty list) + + """ + + # Check the mainapp.TartubeApp IV is probably cheaper than checking + # self.child_list, as the latter might contain thousands of videos + return_list = [] + + for video_obj in app_obj.media_reg_live_dict.values(): + if live_mode is None or video_obj.live_mode == live_mode: + return_list.append(video_obj) + + return return_list + + # Set accessors @@ -1176,6 +1217,7 @@ def clone_properties(self, other_obj): self.bookmark_count = other_obj.bookmark_count self.dl_count = other_obj.dl_count self.fav_count = other_obj.fav_count + self.live_count = other_obj.live_count self.new_count = other_obj.new_count self.waiting_count = other_obj.waiting_count @@ -1183,6 +1225,36 @@ def clone_properties(self, other_obj): self.warning_list = other_obj.warning_list.copy() + def set_rss(self, youtube_id): + + """Can be called by anything; called frequently by + downloads.VideoDownloader.extract_stdout_data(). + + Set the RSS feed, but only if it's not already set (to save time). + + Args: + + youtube_id (str): The YouTube channel or playlist ID + + """ + + if not self.rss: + + if isinstance(self, Channel): + + self.rss = utils.convert_youtube_id_to_rss( + 'channel', + youtube_id, + ) + + else: + + self.rss = utils.convert_youtube_id_to_rss( + 'playlist', + youtube_id, + ) + + def set_source(self, source): self.source = source @@ -1213,8 +1285,8 @@ class Video(GenericMedia): # Standard class methods - def __init__(self, dbid, name, parent_obj, options_obj=None, - no_sort_flag=False): + def __init__(self, dbid, name, parent_obj=None, options_obj=None, + no_sort_flag=False, dummy_flag=False): # IV list - class objects # ----------------------- @@ -1230,6 +1302,10 @@ def __init__(self, dbid, name, parent_obj, options_obj=None, # IV list - other # --------------- # Unique media data object ID (an integer) + # When a download operation is launched from the Classic Mode Tab, + # the code creates a series of dummy media.Video objects that aren't + # added to the media data registry. Those dummy objects have negative + # dbids self.dbid = dbid # Video name @@ -1252,6 +1328,12 @@ def __init__(self, dbid, name, parent_obj, options_obj=None, # video, or False if the downloads.DownloadManager object should # decide whether to simulate, or not self.dl_sim_flag = False + # Livestream mode: 0 if the video is not a livestream (or if it was a + # livestream which has now finished, and behaves like a normal + # uploaded video), 1 if the livestream has not started, 2 if the + # livestream is currently being broadcast + # (Using a numerical mode makes the sorting algorithms more efficient) + self.live_mode = 0 # Flag set to True if the video is archived, meaning that it can't be # auto-deleted (but it can still be deleted manually by the user) @@ -1320,12 +1402,27 @@ def __init__(self, dbid, name, parent_obj, options_obj=None, self.error_list = [] self.warning_list = [] + # IVs used only when the download operation is launched from the + # Classic Mode Tab + # To save database loading time, these IVs are only added when needed + # (via a call to self.set_dummy() ) + # Flag set to True if this is a dummy media.Video object + self.dummy_flag = False +# # The destination directory for the download +# self.dummy_dir = None +# # The full path to a downloaded file, if available +# self.dummy_path = None +# # The video/audio format to use; must be one of the strings in +# # formats.VIDEO_FORMAT_LIST or formats.AUDIO_FORMAT_LIST (or None to +# # use the format(s) specified by the General Options Manager) +# self.dummy_format = None # Code # ---- # Update the parent - self.parent_obj.add_child(self, no_sort_flag) + if parent_obj: + self.parent_obj.add_child(self, no_sort_flag) # Public class methods @@ -1376,30 +1473,62 @@ def fetch_tooltip_text(self, app_obj, max_length=None): """ - text = '#' + str(self.dbid) + ': ' + self.name + '\n\n' + if not self.dummy_flag: - if self.parent_obj: + translate_note = _( + 'TRANSLATOR\'S NOTE: WAITING = livestream not started,' \ + + ' LIVE = livestream started', + ) - if isinstance(self.parent_obj, Channel): - text += 'Channel: ' - elif isinstance(self.parent_obj, Playlist): - text += 'Playlist: ' + if self.live_mode == 1: + live_str = '<' + _('WAITING') + '>' + elif self.live_mode == 2: + live_str = '<' + _('LIVE') + '>' else: - text += 'Folder: ' + live_str = '' - text += self.parent_obj.name + '\n\n' + text \ + = ' #' + str(self.dbid) + live_str + ': ' + self.name + '\n\n' - text += 'Source:\n' - if self.source is None: - text += ' ' - else: - text += self.source + if self.parent_obj: + + if isinstance(self.parent_obj, Channel): + text += _('Channel:') + ' ' + elif isinstance(self.parent_obj, Playlist): + text += _('Playlist:') + ' ' + else: + text += _('Folder:') + ' ' + + text += self.parent_obj.name + '\n\n' + + translate_note = _( + 'TRANSLATOR\'S NOTE 2: Source = video/channel/playlist URL', + ) + + text += _('Source:') + '\n' + if self.source is None: + text += '<' + _('unknown') + '>' + else: + text += self.source + + text += '\n\n' + _('File:') + '\n' + if self.file_name is None: + text += '<' + _('unknown') + '>' + else: + text += self.get_actual_path(app_obj) - text += '\n\nFile:\n' - if self.file_name is None: - text += ' ' else: - text += self.get_actual_path(app_obj) + + # When the download operation is launched from the Classic Mode + # tab, there is less to display + text = _('Source:') + '\n' + if self.source is None: + text += '<' + _('unknown') + '>' + else: + text += self.source + + if self.dummy_path: + text += '\n\n' + _('File:') + '\n' + self.dummy_path # Apply a maximum line length, if required if max_length is not None: @@ -1459,6 +1588,41 @@ def set_dl_flag(self, flag=False): # def set_dl_sim_flag(): # Inherited from GenericMedia + def set_dummy(self, url, dir_str, format_str): + + """Called by mainwin.MainWin.classic_mode_tab_add_urls(), immediately + after the call to self.new(). + + Sets up this media.Video object as a dummy object, not added to the + media data registry. + + Args: + + url (str): The URL to download (which might reperesent a video, + channel or playlist; the dummy media.Video object represents + all of them) + + dir_str (str): The destination directory for the download, chosen + by the user + + format_str (str): One of the video/audio formats specified by + formats.VIDEO_FORMAT_LIST and formats.AUDIO_FORMAT_LIST + + """ + + self.dummy_flag = True + self.dummy_dir = dir_str + self.dummy_path = None + self.dummy_format = format_str + + self.source = url + + + def set_dummy_path(self, path): + + self.dummy_path = path + + def set_duration(self, duration=None): if duration is not None: @@ -1490,6 +1654,11 @@ def set_index(self, index): self.index = int(index) + def set_live_mode(self, mode): + + self.live_mode = mode + + def set_mkv(self): """Called by mainapp.TartubeApp.update_video_when_file_found() and @@ -1793,9 +1962,9 @@ def get_upload_date_string(self, pretty_flag=False): testday_str = testday.strftime('%y%m%d') if testday_str == today_str: - return 'Today' + return _('Today') elif testday_str == yesterday_str: - return 'Yesterday' + return _('Yesterday') else: return testday.strftime('%Y-%m-%d') @@ -1869,6 +2038,10 @@ def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None): self.nickname = name # Download source (a URL) self.source = None + # RSS feed source (a URL), used by livestream operations on compatible + # websites. For YouTube channels, set automatically during a download + # operation. For channels on other websites, can be set manually + self.rss = None # Alternative download destination - the dbid of a channel, playlist or # folder in whose directory videos, thumbnails (etc) are downloaded. @@ -1904,11 +2077,12 @@ def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None): # The total number of child video objects self.vid_count = 0 # The number of child video objects that are marked as bookmarked, - # downloaded, favourite, new and in the 'Waiting Videos' system - # folder + # downloaded, favourite, livestreams, new and in the 'Waiting Videos' + # system folders self.bookmark_count = 0 self.dl_count = 0 self.fav_count = 0 + self.live_count = 0 self.new_count = 0 self.waiting_count = 0 @@ -2035,6 +2209,10 @@ def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None): self.nickname = name # Download source (a URL) self.source = None + # RSS feed source (a URL), used by livestream operations on compatible + # websites. Set automatically for YouTube videos, and can be set + # manually by the user for other websites + self.rss = None # Alternative download destination - the dbid of a channel, playlist or # folder in whose directory videos, thumbnails (etc) are downloaded. @@ -2070,11 +2248,12 @@ def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None): # The total number of child video objects self.vid_count = 0 # The number of child video objects that are marked as bookmarked, - # downloaded, favourite, new and in the 'Waiting Videos' system - # folder + # downloaded, favourite, livestreams, new and in the 'Waiting Videos' + # system folders self.bookmark_count = 0 self.dl_count = 0 self.fav_count = 0 + self.live_count = 0 self.new_count = 0 self.waiting_count = 0 @@ -2271,11 +2450,12 @@ def __init__(self, app_obj, dbid, name, parent_obj=None, \ # The total number of child video objects self.vid_count = 0 # The number of child video objects that are marked as bookmarked, - # downloaded, favourite, new and in the 'Waiting Videos' system - # folder + # downloaded, favourite, livestreams, new and in the 'Waiting Videos' + # system folders self.bookmark_count = 0 self.dl_count = 0 self.fav_count = 0 + self.live_count = 0 self.new_count = 0 self.waiting_count = 0 @@ -2386,7 +2566,14 @@ def do_sort(self, obj1, obj2): ): if isinstance(obj1, Video): - if obj1.upload_time is not None \ + # Livestreams come before everything else + if obj1.live_mode > obj2.live_mode: + return -1 + elif obj1.live_mode < obj2.live_mode: + return 1 + + # Otherwise sort by upload time, then by receive time + elif obj1.upload_time is not None \ and obj2.upload_time is not None: if obj1.upload_time > obj2.upload_time: return -1 diff --git a/tartube/options.py b/tartube/options.py index e86f4b96..e0e146a0 100755 --- a/tartube/options.py +++ b/tartube/options.py @@ -214,17 +214,13 @@ class OptionsManager(object): video_format (str): Video format to download. When this option is set to '0' youtube-dl will choose the best video format available for - the given URL. Otherwise, this option is set to one of the keys in - formats.VIDEO_FORMAT_DICT, in which case youtube-dl will use the - corresponding value to select the video format. See also the - options 'second_video_format' and 'third_video_format'. + the given URL. Otherwise, set in a call to + OptionsParser.build_video_format(), combining the contents of the + 'video_format_list' and 'video_format_mode' options. The combined + value is passed to youtube-dl with the -f switch - N.B. The options 'video_format', 'second_video_format' and - 'third_video_format' are rearranged before being used, so that - video formats appear before audio_formats (otherwise, youtube-dl - won't download them) - - all_formats (bool): If True, download all available video formats + all_formats (bool): If True, download all available video formats. + Also set in the call to OptionsParser.build_video_format() prefer_free_formats (bool): If True, prefer free video formats unless one is specfied by video_format, etc @@ -325,25 +321,6 @@ class OptionsManager(object): output_template (str): Can be any output template supported by youtube-dl. Ignored if 'output_format' is not 0 - [used to modify the 'video_format' option] - - second_video_format (str): Video format to download, if the format - specified by the 'video_format' option isn't available. This option - is ignored when its value is '0' (or when the value of the - 'video_format' option is '0'), and also if 'video_format' is set - to one of the keys in formats.VIDEO_RESOLUTION_DICT (e.g. 1080p). - Otherwise, its value is one of the keys in - formats.VIDEO_FORMAT_DICT - - third_video_format (str): Video format to download, if the formats - specified by the 'video_format' and 'second_video_format' options - aren't available. This option is ignored when its value is '0' (or - when the value of the 'video_format' and 'second_video_format' - options are '0'), and also if 'video_format' or - 'second_video_format' are set to one of the keys in - formats.VIDEO_RESOLUTION_DICT (e.g. 1080p). Otherwise, its value is - one of the keys in formats.VIDEO_FORMAT_DICT - [used in conjunction with the 'min_filesize' and 'max_filesize' options max_filesize_unit (str): Maximum file size unit. Available values: @@ -410,6 +387,21 @@ class OptionsManager(object): or caseless sub-string). Each item in the list is passed to youtube-dl as a separate --reject-title argument + video_format_list (list): List of video formats to download, in order + of preference. If an empty list, youtube-dl will choose the best + video format available for the given URL. Otherwise, the items in + this list are keys in formats.VIDEO_FORMAT_DICT. The corresponding + values are combined and stored as the 'video_format' option, first + being rearrnaged to put video formats before audio formats + (otherwise youtube-dl won't download the video formats) + + video_format_mode (str): 'all' to download all available formats, + ignoring the preference list (sets the option 'all_formats'). + 'single' to download the first available format in + 'video_format_list'. 'single_agree' to download the first format in + 'video_format_list' that's available for all videos. 'multiple' to + download all available formats in 'video_format_list' + subs_lang_list (list): List of language tags which are used to set the 'subs_lang' option @@ -461,9 +453,8 @@ def rearrange_formats(self): """Called by config.OptionsEditWin.apply_changes(). - The options 'video_format', 'second_video_format' and - 'third_video_format' specify video formats, audio formats or a mixture - of both. + The option 'video_format_list' specifies video formats, audio formats + or a mixture of both. youtube-dl won't download the specified formats properly, if audio formats appear before video formats. Therefore, this function is called @@ -471,11 +462,7 @@ def rearrange_formats(self): formats. """ - format_list = [ - self.options_dict['video_format'], - self.options_dict['second_video_format'], - self.options_dict['third_video_format'], - ] + format_list = self.options_dict['video_format_list'] video_list = [] audio_list = [] comb_list = [] @@ -492,20 +479,7 @@ def rearrange_formats(self): comb_list.extend(video_list) comb_list.extend(audio_list) - if len(comb_list) >= 1: - self.options_dict['video_format'] = comb_list[0] - else: - self.options_dict['video_format'] = '0' - - if len(comb_list) >= 2: - self.options_dict['second_video_format'] = comb_list[1] - else: - self.options_dict['second_video_format'] = '0' - - if len(comb_list) == 3: - self.options_dict['third_video_format'] = comb_list[2] - else: - self.options_dict['third_video_format'] = '0' + self.options_dict['video_format_list'] = format_list def reset_options(self): @@ -607,8 +581,6 @@ def reset_options(self): # YOUTUBE-DL-GUI OPTIONS 'output_format': 2, 'output_template': '%(title)s.%(ext)s', - 'second_video_format': '0', - 'third_video_format': '0', 'max_filesize_unit' : '', 'min_filesize_unit' : '', 'extra_cmd_string' : '', @@ -624,6 +596,8 @@ def reset_options(self): 'use_fixed_folder': None, 'match_title_list': [], 'reject_title_list': [], + 'video_format_list': [], + 'video_format_mode': 'single', 'subs_lang_list': [ 'en' ], } @@ -846,8 +820,6 @@ def __init__(self, app_obj): # YOUTUBE-DL-GUI OPTIONS (not given an options.OptionHolder object) # OptionHolder('output_format', '', 2), # OptionHolder('output_template', '', ''), -# OptionHolder('second_video_format', '', '0'), -# OptionHolder('third_video_format', '', '0'), # OptionHolder('max_filesize_unit', '', ''), # OptionHolder('min_filesize_unit', '', ''), # OptionHolder('extra_cmd_string', '', ''), @@ -863,6 +835,8 @@ def __init__(self, app_obj): # OptionHolder('use_fixed_folder', '', None), # OptionHolder('match_title_list', '', []), # OptionHolder('reject_title_list', '', []), +# OptionHolder('video_format_list', '', []), +# OptionHolder('video_format_mode', '', 'single'), # OptionHolder('subs_lang_list', '', []), ] @@ -870,7 +844,8 @@ def __init__(self, app_obj): # Public class methods - def parse(self, media_data_obj, options_manager_obj): + def parse(self, media_data_obj, options_manager_obj, + dl_classic_flag=False): """Called by downloads.DownloadWorker.prepare_download() and mainwin.MainWin.update_textbuffer(). @@ -887,6 +862,10 @@ def parse(self, media_data_obj, options_manager_obj): options_manager_obj (options.OptionsManager): The object containing the download options for this media data object + dl_classic_flag (bool): True when called by .prepare_download, and + when the download operation was launched from the Classic Mode + tab. False otherwise + Returns: List of strings with all the youtube-dl command line options @@ -899,9 +878,9 @@ def parse(self, media_data_obj, options_manager_obj): # Create a copy of the dictionary... copy_dict = options_manager_obj.options_dict.copy() # ...then modify various values in the copy. Set the 'save_path' option - self.build_save_path(media_data_obj, copy_dict) - # Set the 'video_format' option - self.build_video_format(copy_dict) + self.build_save_path(media_data_obj, copy_dict, dl_classic_flag) + # Set the 'video_format' option and 'all_formats' options + self.build_video_format(media_data_obj, copy_dict, dl_classic_flag) # Set the 'min_filesize' and 'max_filesize' options self.build_file_sizes(copy_dict) # Set the 'limit_rate' option @@ -1046,7 +1025,7 @@ def build_limit_rate(self, copy_dict): copy_dict['limit_rate'] = str(limit) + 'K' - def build_save_path(self, media_data_obj, copy_dict): + def build_save_path(self, media_data_obj, copy_dict, dl_classic_flag): """Called by self.parse(). @@ -1058,31 +1037,42 @@ def build_save_path(self, media_data_obj, copy_dict): media_data_obj (media.Video, media.Channel, media.Playlist, media.Folder): The media data object being downloaded - copy_dict (dict): Copy of the original options dictionary. + copy_dict (dict): Copy of the original options dictionary - """ + dl_classic_flag (bool): True a download operation was launched from + the Classic Mode tab. False otherwise - # Set the directory in which any downloaded videos will be saved - override_name = copy_dict['use_fixed_folder'] + """ - if not isinstance(media_data_obj, media.Video) \ - and override_name is not None \ - and override_name in self.app_obj.media_name_dict: + # Special case: if a download operation was launched from the Classic + # Mode Tab, the save path is specified in that tab + if dl_classic_flag: - # Because of the override, save all videos to a fixed folder - other_dbid = self.app_obj.media_name_dict[override_name] - other_obj = self.app_obj.media_reg_dict[other_dbid] - save_path = other_obj.get_default_dir(self.app_obj) + save_path = media_data_obj.dummy_dir else: - if isinstance(media_data_obj, media.Video): - save_path = media_data_obj.parent_obj.get_actual_dir( - self.app_obj, - ) + # Set the directory in which any downloaded videos will be saved + override_name = copy_dict['use_fixed_folder'] + + if not isinstance(media_data_obj, media.Video) \ + and override_name is not None \ + and override_name in self.app_obj.media_name_dict: + + # Because of the override, save all videos to a fixed folder + other_dbid = self.app_obj.media_name_dict[override_name] + other_obj = self.app_obj.media_reg_dict[other_dbid] + save_path = other_obj.get_default_dir(self.app_obj) else: - save_path = media_data_obj.get_actual_dir(self.app_obj) + + if isinstance(media_data_obj, media.Video): + save_path = media_data_obj.parent_obj.get_actual_dir( + self.app_obj, + ) + + else: + save_path = media_data_obj.get_actual_dir(self.app_obj) # Set the youtube-dl output template for the video's file template = formats.FILE_OUTPUT_CONVERT_DICT[copy_dict['output_format']] @@ -1095,27 +1085,62 @@ def build_save_path(self, media_data_obj, copy_dict): ) - def build_video_format(self, copy_dict): + def build_video_format(self, media_data_obj, copy_dict, dl_classic_flag): """Called by self.parse(). - Build the value of the 'video_format' option and store it in the - options dictionary. + Build the value of the 'video_format' and 'all_formats' options and + store them in the options dictionary. Args: - copy_dict (dict): Copy of the original options dictionary. + media_data_obj (media.Video, media.Channel, media.Playlist, + media.Folder): The media data object being downloaded + + copy_dict (dict): Copy of the original options dictionary + + dl_classic_flag (bool): True a download operation was launched from + the Classic Mode tab. False otherwise """ - # The 'video_format', 'second_video_format' and 'third_video_format' - # can have the values of the keys in formats.VIDEO_OPTION_DICT, which - # are either real extractor codes (e.g. '35' representing - # 'flv [480p]') or dummy extractor codes (e.g. 'mp4') + if isinstance(media_data_obj, media.Video): + + # Special case: if a download operation was launched from the + # Classic Mode Tab, the video format may be specified by that tab + if dl_classic_flag and media_data_obj.dummy_format: + + # Ignore all video/audio formats except the one specified by + # the user in the Classic Mode Tab + copy_dict['video_format'] = media_data_obj.dummy_format + copy_dict['all_formats'] = False + copy_dict['video_format_list'] = [] + copy_dict['video_format_mode'] = '' + + return + + # Special case: for broadcasting livestreams, use only HLS + # v2.0.067: Downloading livestreams doesn't work at all for me, so + # I'm not sure whether this is appropriate, or not. Once it's + # fixed, perhaps we can offer the user a choice of formats + if media_data_obj.live_mode: + + copy_dict['video_format'] = 95 + copy_dict['all_formats'] = False + copy_dict['video_format_list'] = [] + copy_dict['video_format_mode'] = '' + + return + + # The 'video_format_list' options contains values corresponding to the + # keys in formats.VIDEO_OPTION_DICT, which are either real extractor + # codes (e.g. '35' representing 'flv [480p]') or dummy extractor + # codes (e.g. 'mp4') # Some dummy extractor codes are in the form '720p', '1080p60' etc, # representing progressive scan resolutions. If the user specifies # at least one of those codes, the first one is used, and all other # extractor codes are ignored + video_format_list = copy_dict['video_format_list'] resolution_dict = formats.VIDEO_RESOLUTION_DICT.copy() fps_dict = formats.VIDEO_FPS_DICT.copy() @@ -1130,21 +1155,16 @@ def build_video_format(self, copy_dict): if self.app_obj.video_res_default in fps_dict: fps = fps_dict[self.app_obj.video_res_default] - elif copy_dict['video_format'] in resolution_dict: - height = resolution_dict[copy_dict['video_format']] - if copy_dict['video_format'] in fps_dict: - fps = fps_dict[copy_dict['video_format']] + else: - elif copy_dict['second_video_format'] in resolution_dict: - height = resolution_dict[copy_dict['second_video_format']] - if copy_dict['second_video_format'] in fps_dict: - fps = fps_dict[copy_dict['second_video_format']] + for item in video_format_list: - elif copy_dict['third_video_format'] in resolution_dict: - height = resolution_dict[copy_dict['third_video_format']] - if copy_dict['third_video_format'] in fps_dict: - fps = fps_dict[copy_dict['third_video_format']] + if item in resolution_dict: + height = resolution_dict[item] + if item in fps_dict: + fps = fps_dict[item] + break if height is not None: @@ -1156,32 +1176,42 @@ def build_video_format(self, copy_dict): copy_dict['video_format'] = 'bestvideo[height<=?' \ + str(height) + ']+bestaudio/best[height<=?' + str(height) \ + ']' - # After a progressive scan resolution, all other extract codes - # are ignored - copy_dict['second_video_format'] = '0' - copy_dict['third_video_format'] = '0' else: copy_dict['video_format'] = 'bestvideo[height<=?' \ + str(height) + '][fps<=?' + str(fps) \ + ']+bestaudio/best[height<=?' + str(height) + ']' - copy_dict['second_video_format'] = '0' - copy_dict['third_video_format'] = '0' + + copy_dict['all_formats'] = False + copy_dict['video_format_list'] = [] + copy_dict['video_format_mode'] = '' # Not using a progressive scan resolution - elif copy_dict['video_format'] != '0' and \ - copy_dict['second_video_format'] != '0': + elif video_format_list: - if copy_dict['third_video_format'] != '0': + video_format_mode = copy_dict['video_format_mode'] - copy_dict['video_format'] = copy_dict['video_format'] + '+' \ - + copy_dict['second_video_format'] + '+' \ - + copy_dict['third_video_format'] + if video_format_mode == 'all': + copy_dict['video_format'] = 0 + copy_dict['all_formats'] = True else: - copy_dict['video_format'] = copy_dict['video_format'] + '+' \ - + copy_dict['second_video_format'] + + copy_dict['all_formats'] = False + + if video_format_mode == 'single_agree': + char = '/' + elif video_format_mode == 'multiple': + char = ',' + else: + # mode is 'single' + char = '+' + + copy_dict['video_format'] = char.join(video_format_list) + + copy_dict['video_format_list'] = [] + copy_dict['video_format_mode'] = '' class OptionHolder(object): diff --git a/tartube/po/POTFILES.in b/tartube/po/POTFILES.in new file mode 100644 index 00000000..d36a58c3 --- /dev/null +++ b/tartube/po/POTFILES.in @@ -0,0 +1,11 @@ +[encoding: UTF-8] +./mainapp.py +./mainwin.py +./config.py +./downloads.py +./formats.py +./info.py +./media.py +./refresh.py +./tidy.py +./updates.py diff --git a/tartube/po/messages.pot b/tartube/po/messages.pot new file mode 100644 index 00000000..652099c9 --- /dev/null +++ b/tartube/po/messages.pot @@ -0,0 +1,5494 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-07 07:01+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: .././mainapp.py:2225 +msgid "" +"Tartube can't create the folder in which its configuration file is saved" +msgstr "" + +#: .././mainapp.py:2267 +msgid "The user declined to specify a data folder for Tartube" +msgstr "" + +#: .././mainapp.py:2456 +#, python-brace-format +msgid "" +"Gtk v{0}.{1}.{2} is broken, which may cause problems when running Tartube. " +"If possible, please update it to at least Gtk v3.24" +msgstr "" + +#: .././mainapp.py:2470 +#, python-brace-format +msgid "" +"Tartube is assuming that Gtk v{0}.{1}.{2} is broken; some minor cosmetic " +"features are disabled" +msgstr "" + +#: .././mainapp.py:2510 +msgid "The Tartube database file was not loaded, but is no longer protected" +msgstr "" + +#: .././mainapp.py:2513 +msgid "Restart Tartube to load it" +msgstr "" + +#: .././mainapp.py:2522 +msgid "Because of an error, file load/save has been disabled" +msgstr "" + +#: .././mainapp.py:2532 +msgid "Because of the error, file load/save has been disabled" +msgstr "" + +#: .././mainapp.py:2563 +msgid "" +"youtube-dl must be installed before you can use Tartube. Do you want to " +"install youtube-dl now?" +msgstr "" + +#: .././mainapp.py:2618 +msgid "There is a download operation in progress." +msgstr "" + +#: .././mainapp.py:2620 +msgid "There is an update operation in progress." +msgstr "" + +#: .././mainapp.py:2622 +msgid "There is a refresh operation in progress." +msgstr "" + +#: .././mainapp.py:2624 +msgid "There is an info operation in progress." +msgstr "" + +#: .././mainapp.py:2626 +msgid "There is a tidy operation in progress." +msgstr "" + +#: .././mainapp.py:2631 +msgid "Are you sure you want to quit Tartube?" +msgstr "" + +#: .././mainapp.py:2828 +msgid "Failed to load the Tartube config file (failed sanity check)" +msgstr "" + +#: .././mainapp.py:2850 +msgid "Failed to load the Tartube config file (file is locked)" +msgstr "" + +#: .././mainapp.py:2866 +msgid "Failed to load the Tartube config file (JSON load failure)" +msgstr "" + +#: .././mainapp.py:2882 +msgid "Failed to load the Tartube config file (file is invalid)" +msgstr "" + +#: .././mainapp.py:2899 +msgid "" +"Failed to load the Tartube config file (file cannot be read by this version)" +msgstr "" + +#: .././mainapp.py:2913 +msgid "Failed to load the Tartube config file (missing file type)" +msgstr "" + +#: .././mainapp.py:3472 +msgid "Failed to save the Tartube config file (failed sanity check)" +msgstr "" + +#: .././mainapp.py:3718 +msgid "Failed to save the Tartube config file (file is locked)" +msgstr "" + +#: .././mainapp.py:3720 .././mainapp.py:3760 .././mainapp.py:4690 +#: .././mainapp.py:4746 .././mainapp.py:4752 +msgid "File load/save has been disabled" +msgstr "" + +#: .././mainapp.py:3739 +msgid "Failed to save the Tartube config file (file already in use)" +msgstr "" + +#: .././mainapp.py:3759 +msgid "Failed to save the Tartube config file" +msgstr "" + +#: .././mainapp.py:3808 .././mainapp.py:3826 .././mainapp.py:3856 +msgid "Failed to load the Tartube database file" +msgstr "" + +#: .././mainapp.py:3871 +msgid "The Tartube database file is invalid" +msgstr "" + +#: .././mainapp.py:3887 +msgid "Database file can't be read by this version of Tartube" +msgstr "" + +#: .././mainapp.py:4187 +msgid "Tartube is applying an essential database update" +msgstr "" + +#: .././mainapp.py:4189 +msgid "This might take a few minutes, so please be patient" +msgstr "" + +#: .././mainapp.py:4684 .././mainapp.py:4742 .././mainapp.py:4751 +msgid "Failed to save the Tartube database file" +msgstr "" + +#: .././mainapp.py:4687 +msgid "(Could not make a backup copy of the existing file)" +msgstr "" + +#: .././mainapp.py:4723 +msgid "Failed to save the Tartube database file (file already in use)" +msgstr "" + +#: .././mainapp.py:4744 +msgid "A backup of the previous file can be found at:" +msgstr "" + +#: .././mainapp.py:4969 .././mainapp.py:4979 +msgid "Database file created" +msgstr "" + +#: .././mainapp.py:5032 .././mainapp.py:5084 +#, 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:5247 +msgid "Tartube's database can't be checked while an operation is in progress" +msgstr "" + +#: .././mainapp.py:5431 +msgid "Database check complete, no inconsistencies found" +msgstr "" + +#: .././mainapp.py:5445 +msgid "Database check complete, problems found:" +msgstr "" + +#: .././mainapp.py:5448 +msgid "" +"Do you want to repair these problems? (The database will be fixed, but no " +"files will be deleted)" +msgstr "" + +#: .././mainapp.py:5588 +msgid "Database inconsistencies repaired" +msgstr "" + +#: .././mainapp.py:6229 .././config.py:9731 +msgid "Please select Tartube's data folder" +msgstr "" + +#: .././mainapp.py:6355 +msgid "" +"A download operation cannot start if one or more configuration windows are " +"still open" +msgstr "" + +#: .././mainapp.py:6379 .././mainapp.py:6401 +#, python-brace-format +msgid "You only have {0} / {1} Mb remaining on your device" +msgstr "" + +#: .././mainapp.py:6404 .././mainapp.py:11069 .././mainapp.py:11234 +#: .././mainwin.py:13433 +msgid "Are you sure you want to continue?" +msgstr "" + +#: .././mainapp.py:6485 +msgid "There is nothing to check!" +msgstr "" + +#: .././mainapp.py:6487 +msgid "There is nothing to download!" +msgstr "" + +#: .././mainapp.py:6698 +msgid "Download operation complete" +msgstr "" + +#: .././mainapp.py:6700 +msgid "Download operation halted" +msgstr "" + +#: .././mainapp.py:6703 .././mainapp.py:7170 .././mainapp.py:7616 +msgid "Time taken:" +msgstr "" + +#: .././mainapp.py:6761 +msgid "" +"An update operation cannot start if one or more configuration windows are " +"still open" +msgstr "" + +#: .././mainapp.py:6874 +msgid "Installation failed" +msgstr "" + +#: .././mainapp.py:6876 +msgid "Installation complete" +msgstr "" + +#: .././mainapp.py:6880 +msgid "Update operation failed" +msgstr "" + +#: .././mainapp.py:6882 +msgid "Update operation halted" +msgstr "" + +#: .././mainapp.py:6884 +msgid "Update operation complete" +msgstr "" + +#: .././mainapp.py:6885 +msgid "youtube-dl version:" +msgstr "" + +#: .././mainapp.py:6889 +msgid "(unknown)" +msgstr "" + +#: .././mainapp.py:6963 +msgid "" +"A refresh operation cannot start if one or more configuration windows are " +"still open" +msgstr "" + +#: .././mainapp.py:6976 +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:6980 +msgid "" +"You only need to perform a refresh operation if you have manually copied " +"videos into Tartube's data folder" +msgstr "" + +#: .././mainapp.py:6987 +msgid "" +"Before starting a refresh operation, you should click the 'Check all' button " +"in the main window" +msgstr "" + +#: .././mainapp.py:6994 +msgid "" +"Before starting a refresh operation, you should right-click the channel and " +"select 'Check channel'" +msgstr "" + +#: .././mainapp.py:7001 +msgid "" +"Before starting a refresh operation, you should right-click the playlist and " +"select 'Check playlist'" +msgstr "" + +#: .././mainapp.py:7008 +msgid "" +"Before starting a refresh operation, you should right-click the folder and " +"select 'Check folder'" +msgstr "" + +#: .././mainapp.py:7013 +msgid "Are you sure you want to proceed with the refresh operation?" +msgstr "" + +#: .././mainapp.py:7165 +msgid "Refresh operation complete" +msgstr "" + +#: .././mainapp.py:7167 +msgid "Refresh operation halted" +msgstr "" + +#: .././mainapp.py:7267 +msgid "" +"An info operation cannot start if one or more configuration windows are " +"still open" +msgstr "" + +#: .././mainapp.py:7380 +msgid "Operation failed" +msgstr "" + +#: .././mainapp.py:7382 .././downloads.py:357 +msgid "Operation complete" +msgstr "" + +#: .././mainapp.py:7384 +msgid "Click the Output Tab to see the results" +msgstr "" + +#: .././mainapp.py:7482 +msgid "" +"A tidy operation cannot start if one or more configuration windows are still " +"open" +msgstr "" + +#: .././mainapp.py:7611 +msgid "Tidy operation complete" +msgstr "" + +#: .././mainapp.py:7613 +msgid "Tidy operation halted" +msgstr "" + +#: .././mainapp.py:7741 .././mainwin.py:13843 +msgid "Livestream has started" +msgstr "" + +#: .././mainapp.py:8995 .././mainapp.py:9171 +msgid "Cannot move anything to:" +msgstr "" + +#: .././mainapp.py:8997 .././mainapp.py:9173 +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:9001 +msgid "" +"You probably created that file/folder accidentally, in which case you should " +"delete it manually before trying again" +msgstr "" + +#: .././mainapp.py:9015 .././mainapp.py:9191 +msgid "Are you sure you want to move this channel:" +msgstr "" + +#: .././mainapp.py:9017 .././mainapp.py:9193 +msgid "Are you sure you want to move this playlist:" +msgstr "" + +#: .././mainapp.py:9019 .././mainapp.py:9195 +msgid "Are you sure you want to move this folder:" +msgstr "" + +#: .././mainapp.py:9024 +msgid "" +"This procedure will move all downloaded files to the top level of Tartube's " +"data folder" +msgstr "" + +#: .././mainapp.py:9125 +msgid "Channels, playlists and folders can only be dragged into a folder" +msgstr "" + +#: .././mainapp.py:9138 +#, python-brace-format +msgid "The fixed folder '{0}' cannot be moved (but it can still be hidden)" +msgstr "" + +#: .././mainapp.py:9151 +#, python-brace-format +msgid "The folder '{0}' can only contain videos" +msgstr "" + +#: .././mainapp.py:9178 +msgid "" +"You probably created that file/folder accidentally, in which case, you " +"should delete it manually before trying again" +msgstr "" + +#: .././mainapp.py:9197 +msgid "into this folder:" +msgstr "" + +#: .././mainapp.py:9201 +msgid "This procedure will move all downloaded files to the new location" +msgstr "" + +#: .././mainapp.py:9207 +msgid "" +"WARNING: The destination folder is marked as temporary, so everything inside " +"it will be DELETED when Tartube restarts!" +msgstr "" + +#: .././mainapp.py:9589 +msgid "" +"Are you SURE you want to delete files? This procedure cannot be reversed!" +msgstr "" + +#: .././mainapp.py:11053 .././mainapp.py:11218 +#, python-brace-format +msgid "The channel contains {0} item(s), so this action may take a while" +msgstr "" + +#: .././mainapp.py:11059 .././mainapp.py:11224 +#, python-brace-format +msgid "The playlist contains {0} item(s), so this action may take a while" +msgstr "" + +#: .././mainapp.py:11065 .././mainapp.py:11230 +#, python-brace-format +msgid "The folder contains {0} item(s), so this action may take a while" +msgstr "" + +#: .././mainapp.py:11298 .././mainapp.py:13839 .././mainapp.py:13971 +#: .././mainapp.py:14102 +#, python-brace-format +msgid "The name '{0}' is not allowed" +msgstr "" + +#: .././mainapp.py:11307 +#, python-brace-format +msgid "The name '{0}' is already in use" +msgstr "" + +#: .././mainapp.py:11320 +#, python-brace-format +msgid "Failed to rename '{0}'" +msgstr "" + +#: .././mainapp.py:11576 +msgid "Select where to save the database export" +msgstr "" + +#: .././mainapp.py:11705 +msgid "There is nothing to export!" +msgstr "" + +#: .././mainapp.py:11738 .././mainapp.py:11796 +msgid "Failed to save the database export file" +msgstr "" + +#: .././mainapp.py:11803 +msgid "Database export file saved to:" +msgstr "" + +#: .././mainapp.py:11840 +msgid "Select the database export" +msgstr "" + +#: .././mainapp.py:11865 .././mainapp.py:11879 +msgid "Failed to load the database export file" +msgstr "" + +#: .././mainapp.py:11896 +msgid "The database export file is invalid" +msgstr "" + +#: .././mainapp.py:11907 +msgid "The database export file is invalid (or empty)" +msgstr "" + +#: .././mainapp.py:11951 +msgid "Nothing was imported from the database export file" +msgstr "" + +#. Show a confirmation +#: .././mainapp.py:11965 +msgid "Imported:" +msgstr "" + +#: .././mainapp.py:11966 +msgid "Videos:" +msgstr "" + +#: .././mainapp.py:11967 +msgid "Channels:" +msgstr "" + +#: .././mainapp.py:11968 +msgid "Playlists:" +msgstr "" + +#: .././mainapp.py:11969 +msgid "Folders:" +msgstr "" + +#: .././mainapp.py:12330 +msgid "" +"The video file is missing from Tartube's data folder (try downloading the " +"video again!)" +msgstr "" + +#: .././mainapp.py:13027 +msgid "Please select a destination folder" +msgstr "" + +#: .././mainapp.py:13160 +msgid "No video(s) have been downloaded" +msgstr "" + +#. Prompt for confirmation +#: .././mainapp.py:13250 +msgid "Are you sure you want to remove the selected item(s)?" +msgstr "" + +#: .././mainapp.py:13830 +msgid "You must give the channel a name" +msgstr "" + +#: .././mainapp.py:13848 .././mainapp.py:14111 +msgid "You must enter a valid URL" +msgstr "" + +#: .././mainapp.py:13963 +msgid "You must give the folder a name" +msgstr "" + +#: .././mainapp.py:14093 +msgid "You must give the playlist a name" +msgstr "" + +#: .././mainapp.py:14248 .././mainwin.py:13328 +msgid "The following videos are duplicates:" +msgstr "" + +#: .././mainapp.py:14312 +msgid "There were no livestream alerts to cancel" +msgstr "" + +#: .././mainapp.py:14314 +msgid "Livestream alerts for 1 video were cancelled" +msgstr "" + +#: .././mainapp.py:14317 +#, python-brace-format +msgid "Livestream alerts for {0} videos were cancelled" +msgstr "" + +#: .././mainapp.py:14618 +msgid "Data saved" +msgstr "" + +#: .././mainapp.py:14648 +msgid "Database saved" +msgstr "" + +#: .././mainapp.py:14869 .././mainwin.py:10597 +msgid "" +"Files cannot be recovered, after being deleted. Are you sure you want to " +"continue?" +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:14909 +msgid "Cannot update existing livestreams because" +msgstr "" + +#: .././mainapp.py:14911 +msgid "there is another operation running" +msgstr "" + +#: .././mainapp.py:14913 +msgid "they are currently being updated" +msgstr "" + +#: .././mainapp.py:14915 +msgid "one or more configuration windows are open" +msgstr "" + +#: .././mainapp.py:14917 +msgid "there are no livestreams to update" +msgstr "" + +#: .././mainapp.py:14991 +msgid "There is already a channel with that name" +msgstr "" + +#: .././mainapp.py:14993 +msgid "There is already a playlist with that name" +msgstr "" + +#: .././mainapp.py:14995 +msgid "There is already a folder with that name" +msgstr "" + +#: .././mainapp.py:14998 +msgid "(so please choose a different name)" +msgstr "" + +#: .././mainwin.py:709 +msgid "Tartube cannot start because it cannot find its icons folder" +msgstr "" + +#. File column +#: .././mainwin.py:799 +msgid "_File" +msgstr "" + +#: .././mainwin.py:806 +msgid "_Database preferences..." +msgstr "" + +#: .././mainwin.py:815 +msgid "_Save database" +msgstr "" + +#: .././mainwin.py:821 +msgid "Save _all" +msgstr "" + +#: .././mainwin.py:830 +msgid "_Close to tray" +msgstr "" + +#. Quit +#: .././mainwin.py:835 .././mainwin.py:16379 +msgid "_Quit" +msgstr "" + +#. Edit column +#: .././mainwin.py:840 +msgid "_Edit" +msgstr "" + +#: .././mainwin.py:847 +msgid "_System preferences..." +msgstr "" + +#: .././mainwin.py:853 +msgid "_General download options..." +msgstr "" + +#. Media column +#: .././mainwin.py:859 +msgid "_Media" +msgstr "" + +#: .././mainwin.py:866 +msgid "Add _videos..." +msgstr "" + +#: .././mainwin.py:872 +msgid "Add _channel..." +msgstr "" + +#: .././mainwin.py:878 +msgid "Add _playlist..." +msgstr "" + +#: .././mainwin.py:884 +msgid "Add _folder..." +msgstr "" + +#: .././mainwin.py:893 +msgid "_Export from database" +msgstr "" + +#: .././mainwin.py:901 +msgid "_JSON export file" +msgstr "" + +#: .././mainwin.py:907 +msgid "Plain _text export file" +msgstr "" + +#: .././mainwin.py:913 +msgid "_Import into database" +msgstr "" + +#: .././mainwin.py:922 +msgid "_Switch between views" +msgstr "" + +#: .././mainwin.py:927 +msgid "Show _hidden folders" +msgstr "" + +#: .././mainwin.py:937 +msgid "_Add test media" +msgstr "" + +#. Operations column +#. Add this tab... +#: .././mainwin.py:943 .././config.py:7862 +msgid "_Operations" +msgstr "" + +#. Check all +#: .././mainwin.py:950 .././mainwin.py:16350 +msgid "_Check all" +msgstr "" + +#. Download all +#: .././mainwin.py:956 .././mainwin.py:16357 +msgid "_Download all" +msgstr "" + +#: .././mainwin.py:961 +msgid "C_ustom download all" +msgstr "" + +#: .././mainwin.py:969 +msgid "_Refresh database..." +msgstr "" + +#: .././mainwin.py:978 +msgid "Update _youtube-dl" +msgstr "" + +#: .././mainwin.py:984 +msgid "_Test youtube-dl..." +msgstr "" + +#: .././mainwin.py:993 +msgid "_Install FFmpeg" +msgstr "" + +#: .././mainwin.py:1004 +msgid "Tidy up _files..." +msgstr "" + +#: .././mainwin.py:1015 .././mainwin.py:16368 +msgid "_Stop current operation" +msgstr "" + +#. Livestreams column +#: .././mainwin.py:1022 .././config.py:8093 +msgid "_Livestreams" +msgstr "" + +#: .././mainwin.py:1029 +msgid "_Livestream preferences..." +msgstr "" + +#: .././mainwin.py:1038 +msgid "_Update existing livestreams" +msgstr "" + +#: .././mainwin.py:1043 +msgid "_Cancel all livestream alerts" +msgstr "" + +#. Help column +#: .././mainwin.py:1048 +msgid "_Help" +msgstr "" + +#: .././mainwin.py:1054 +msgid "_About..." +msgstr "" + +#: .././mainwin.py:1059 +msgid "Go to _website" +msgstr "" + +#: .././mainwin.py:1065 +msgid "Send _feedback" +msgstr "" + +#: .././mainwin.py:1101 +msgid "Videos" +msgstr "" + +#: .././mainwin.py:1111 +msgid "Add new video(s)" +msgstr "" + +#: .././mainwin.py:1120 +msgid "Channel" +msgstr "" + +#: .././mainwin.py:1130 +msgid "Add a new channel" +msgstr "" + +#: .././mainwin.py:1141 +msgid "Playlist" +msgstr "" + +#: .././mainwin.py:1151 +msgid "Add a new playlist" +msgstr "" + +#: .././mainwin.py:1162 +msgid "Folder" +msgstr "" + +#: .././mainwin.py:1172 +msgid "Add a new folder" +msgstr "" + +#: .././mainwin.py:1186 +msgid "Check" +msgstr "" + +#: .././mainwin.py:1197 .././mainwin.py:1429 .././mainwin.py:2898 +#: .././mainwin.py:3068 +msgid "Check all videos, channels, playlists and folders" +msgstr "" + +#. Link not clickable +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:1207 .././mainwin.py:14863 .././mainwin.py:14871 +#: .././mainwin.py:15094 .././mainwin.py:15106 .././mainwin.py:15764 +msgid "Download" +msgstr "" + +#: .././mainwin.py:1218 .././mainwin.py:1437 .././mainwin.py:2906 +#: .././mainwin.py:3074 +msgid "Download all videos, channels, playlists and folders" +msgstr "" + +#: .././mainwin.py:1233 +msgid "Stop" +msgstr "" + +#: .././mainwin.py:1245 +msgid "Stop the current operation" +msgstr "" + +#. (signal_connect appears below) +#. v2.0.079 These lines produce a Gtk error, for no obvious reason (the +#. equivalent code in mainwin.MainWin.setup_classic_mode_tab() +#. produces no error) +#. selection = treeview.get_selection() +#. selection.set_mode(Gtk.SelectionMode.MULTIPLE) +#: .././mainwin.py:1257 .././config.py:6694 +msgid "Switch" +msgstr "" + +#: .././mainwin.py:1268 +msgid "Switch between simple and complex views" +msgstr "" + +#: .././mainwin.py:1282 .././config.py:8233 +msgid "Test" +msgstr "" + +#: .././mainwin.py:1293 +msgid "Add test media data objects" +msgstr "" + +#: .././mainwin.py:1306 +msgid "Quit" +msgstr "" + +#: .././mainwin.py:1316 +msgid "Close Tartube" +msgstr "" + +#: .././mainwin.py:1338 +msgid "_Videos" +msgstr "" + +#: .././mainwin.py:1346 +msgid "_Progress" +msgstr "" + +#: .././mainwin.py:1354 +msgid "_Classic Mode" +msgstr "" + +#: .././mainwin.py:1362 +msgid "_Output" +msgstr "" + +#: .././mainwin.py:1371 .././config.py:5361 .././config.py:5713 +msgid "_Errors / Warnings" +msgstr "" + +#: .././mainwin.py:1427 .././mainwin.py:2896 .././mainwin.py:3065 +msgid "Check all" +msgstr "" + +#: .././mainwin.py:1435 .././mainwin.py:2355 .././mainwin.py:2904 +msgid "Download all" +msgstr "" + +#: .././mainwin.py:1492 +msgid "Page" +msgstr "" + +#: .././mainwin.py:1504 +msgid "Set visible page" +msgstr "" + +#: .././mainwin.py:1528 .././mainwin.py:1762 .././mainwin.py:1823 +#: .././mainwin.py:2249 +msgid "Size" +msgstr "" + +#: .././mainwin.py:1539 +msgid "Set page size" +msgstr "" + +#: .././mainwin.py:1552 +msgid "Go to first page" +msgstr "" + +#: .././mainwin.py:1561 +msgid "Go to previous page" +msgstr "" + +#: .././mainwin.py:1570 +msgid "Go to next page" +msgstr "" + +#: .././mainwin.py:1579 +msgid "Go to last page" +msgstr "" + +#: .././mainwin.py:1588 +msgid "Scroll up" +msgstr "" + +#: .././mainwin.py:1597 +msgid "Scroll down" +msgstr "" + +#: .././mainwin.py:1607 .././mainwin.py:3302 +msgid "Show filter options" +msgstr "" + +#: .././mainwin.py:1620 +msgid "Sort by" +msgstr "" + +#: .././mainwin.py:1627 .././mainwin.py:3359 +msgid "Sort alphabetically" +msgstr "" + +#: .././mainwin.py:1637 +msgid "Filter" +msgstr "" + +#: .././mainwin.py:1646 +msgid "Enter search text" +msgstr "" + +#: .././mainwin.py:1651 +msgid "Regex" +msgstr "" + +#: .././mainwin.py:1659 +msgid "Select if search text is a regex" +msgstr "" + +#: .././mainwin.py:1670 +msgid "Filter videos" +msgstr "" + +#: .././mainwin.py:1681 +msgid "Cancel filter" +msgstr "" + +#: .././mainwin.py:1692 +msgid "Find date" +msgstr "" + +#: .././mainwin.py:1700 +msgid "Find videos by date" +msgstr "" + +#: .././mainwin.py:1755 +msgid "TRANSLATOR'S NOTE: Ext is short for a file extension, e.g. .EXE" +msgstr "" + +#: .././mainwin.py:1760 .././mainwin.py:2247 +msgid "Source" +msgstr "" + +#: .././mainwin.py:1760 .././mainwin.py:2247 +msgid "Status" +msgstr "" + +#: .././mainwin.py:1761 .././mainwin.py:2248 +msgid "Incoming file" +msgstr "" + +#: .././mainwin.py:1761 .././mainwin.py:2248 +msgid "Ext" +msgstr "" + +#: .././mainwin.py:1761 .././mainwin.py:2248 +msgid "Speed" +msgstr "" + +#: .././mainwin.py:1761 .././mainwin.py:2248 +msgid "ETA" +msgstr "" + +#: .././mainwin.py:1823 .././config.py:5625 +msgid "New videos" +msgstr "" + +#: .././mainwin.py:1823 .././config.py:5138 +msgid "Duration" +msgstr "" + +#: .././mainwin.py:1824 +msgid "Date" +msgstr "" + +#: .././mainwin.py:1824 .././config.py:5109 +msgid "File" +msgstr "" + +#: .././mainwin.py:1824 +msgid "Downloaded to" +msgstr "" + +#: .././mainwin.py:1880 +msgid "Max downloads" +msgstr "" + +#: .././mainwin.py:1903 +msgid "D/L speed (KiB/s)" +msgstr "" + +#: .././mainwin.py:1929 .././config.py:2402 +msgid "Video resolution" +msgstr "" + +#: .././mainwin.py:1964 +msgid "Hide rows when they are finished" +msgstr "" + +#: .././mainwin.py:1977 +msgid "Add newest videos to the top of the list" +msgstr "" + +#: .././mainwin.py:2037 +msgid "This tab emulates the classic youtube-dl-gui interface" +msgstr "" + +#: .././mainwin.py:2045 +msgid "Videos downloaded here are not added to Tartube's database" +msgstr "" + +#: .././mainwin.py:2059 +msgid "General download options" +msgstr "" + +#: .././mainwin.py:2076 +msgid "Update youtube-dl" +msgstr "" + +#: .././mainwin.py:2088 .././mainwin.py:8569 .././mainwin.py:17047 +#: .././mainwin.py:17542 .././mainwin.py:17895 +msgid "Enable automatic copy/paste" +msgstr "" + +#. Second row - a textview for entering URLs. If automatic copy/paste is +#. enabled, URLs are automatically copied into this textview +#. -------------------------------------------------------------------- +#: .././mainwin.py:2095 +msgid "Enter URLs below" +msgstr "" + +#. Third row - widgets to set the download destination and video/audio +#. format. The user clicks the 'Add URLs' button to create dummy +#. media.Video objects for each URL. Each object is associated with +#. the specified destination and format +#. -------------------------------------------------------------------- +#. Destination directory +#: .././mainwin.py:2134 +msgid "Destination:" +msgstr "" + +#: .././mainwin.py:2162 +msgid "Add a new destination folder" +msgstr "" + +#. Video/audio format +#: .././mainwin.py:2167 +msgid "Format:" +msgstr "" + +#: .././mainwin.py:2170 +msgid "Default" +msgstr "" + +#: .././mainwin.py:2170 .././mainwin.py:12732 +msgid "Video:" +msgstr "" + +#: .././mainwin.py:2174 .././mainwin.py:12732 +msgid "Audio:" +msgstr "" + +#: .././mainwin.py:2204 +msgid "Add URLs" +msgstr "" + +#: .././mainwin.py:2210 +msgid "Add these URLs" +msgstr "" + +#: .././mainwin.py:2287 +msgid "Remove from list" +msgstr "" + +#: .././mainwin.py:2302 +msgid "Play video" +msgstr "" + +#. Signal connect below +#: .././mainwin.py:2312 .././config.py:2755 .././config.py:6731 +msgid "Move up" +msgstr "" + +#. Signal connect below +#. signal connect appears below +#: .././mainwin.py:2327 .././config.py:2759 .././config.py:6739 +msgid "Move down" +msgstr "" + +#: .././mainwin.py:2337 +msgid "Re-download" +msgstr "" + +#: .././mainwin.py:2352 +msgid "Stop download" +msgstr "" + +#: .././mainwin.py:2362 +msgid "Download the URLs above" +msgstr "" + +#: .././mainwin.py:2425 +msgid "Time" +msgstr "" + +#: .././mainwin.py:2425 +msgid "Type" +msgstr "" + +#: .././mainwin.py:2425 +msgid "Message" +msgstr "" + +#: .././mainwin.py:2459 +msgid "Show Tartube errors" +msgstr "" + +#: .././mainwin.py:2472 +msgid "Show Tartube warnings" +msgstr "" + +#: .././mainwin.py:2485 +msgid "Show server errors" +msgstr "" + +#: .././mainwin.py:2503 +msgid "Show server warnings" +msgstr "" + +#: .././mainwin.py:2515 +msgid "Clear list" +msgstr "" + +#: .././mainwin.py:2824 .././mainwin.py:2852 +msgid "Checking..." +msgstr "" + +#: .././mainwin.py:2826 .././mainwin.py:2854 +msgid "Downloading..." +msgstr "" + +#: .././mainwin.py:2828 .././mainwin.py:2856 +msgid "Refreshing..." +msgstr "" + +#: .././mainwin.py:2830 .././mainwin.py:2858 +msgid "Tidying..." +msgstr "" + +#: .././mainwin.py:3044 +msgid "Installing" +msgstr "" + +#: .././mainwin.py:3047 +msgid "Updating" +msgstr "" + +#: .././mainwin.py:3050 .././mainwin.py:3053 +msgid "Fetching" +msgstr "" + +#: .././mainwin.py:3056 +msgid "Testing" +msgstr "" + +#: .././mainwin.py:3318 +msgid "Hide filter options" +msgstr "" + +#: .././mainwin.py:3367 +msgid "Sort by date" +msgstr "" + +#: .././mainwin.py:3590 +msgid "_Check channel" +msgstr "" + +#: .././mainwin.py:3592 +msgid "_Check playlist" +msgstr "" + +#: .././mainwin.py:3594 +msgid "_Check folder" +msgstr "" + +#: .././mainwin.py:3611 +msgid "_Download channel" +msgstr "" + +#: .././mainwin.py:3613 +msgid "_Download playlist" +msgstr "" + +#: .././mainwin.py:3615 +msgid "_Download folder" +msgstr "" + +#: .././mainwin.py:3632 +msgid "C_ustom download channel" +msgstr "" + +#: .././mainwin.py:3634 +msgid "C_ustom download playlist" +msgstr "" + +#: .././mainwin.py:3636 +msgid "C_ustom download folder" +msgstr "" + +#: .././mainwin.py:3681 +msgid "_Empty folder" +msgstr "" + +#: .././mainwin.py:3693 +msgid "_All contents" +msgstr "" + +#: .././mainwin.py:3711 +msgid "_Remove videos" +msgstr "" + +#: .././mainwin.py:3723 +msgid "_Just folder videos" +msgstr "" + +#: .././mainwin.py:3729 +msgid "Channel co_ntents" +msgstr "" + +#: .././mainwin.py:3731 +msgid "Playlist co_ntents" +msgstr "" + +#: .././mainwin.py:3733 +msgid "Folder co_ntents" +msgstr "" + +#: .././mainwin.py:3745 +msgid "_Move to top level" +msgstr "" + +#: .././mainwin.py:3762 +msgid "_Convert to playlist" +msgstr "" + +#: .././mainwin.py:3764 +msgid "_Convert to channel" +msgstr "" + +#: .././mainwin.py:3786 +msgid "_Hide folder" +msgstr "" + +#: .././mainwin.py:3796 +msgid "_Rename channel..." +msgstr "" + +#: .././mainwin.py:3798 +msgid "_Rename playlist..." +msgstr "" + +#: .././mainwin.py:3800 +msgid "_Rename folder..." +msgstr "" + +#: .././mainwin.py:3817 +msgid "Set _nickname..." +msgstr "" + +#: .././mainwin.py:3830 +msgid "Set _download destination..." +msgstr "" + +#: .././mainwin.py:3846 +msgid "_Export channel..." +msgstr "" + +#: .././mainwin.py:3848 +msgid "_Export playlist..." +msgstr "" + +#: .././mainwin.py:3850 +msgid "_Export folder..." +msgstr "" + +#: .././mainwin.py:3863 +msgid "Re_fresh channel" +msgstr "" + +#: .././mainwin.py:3865 +msgid "Re_fresh playlist" +msgstr "" + +#: .././mainwin.py:3867 +msgid "Re_fresh folder" +msgstr "" + +#: .././mainwin.py:3884 +msgid "_Tidy up channel" +msgstr "" + +#: .././mainwin.py:3886 +msgid "_Tidy up playlist" +msgstr "" + +#: .././mainwin.py:3888 +msgid "_Tidy up folder" +msgstr "" + +#: .././mainwin.py:3905 +msgid "Channel _actions" +msgstr "" + +#: .././mainwin.py:3907 +msgid "Playlist _actions" +msgstr "" + +#: .././mainwin.py:3909 +msgid "Folder _actions" +msgstr "" + +#: .././mainwin.py:3929 .././mainwin.py:4243 +msgid "_Apply download options..." +msgstr "" + +#: .././mainwin.py:3947 .././mainwin.py:4257 +msgid "_Remove download options" +msgstr "" + +#: .././mainwin.py:3963 .././mainwin.py:4269 +msgid "_Edit download options..." +msgstr "" + +#: .././mainwin.py:3979 +msgid "_Show system command" +msgstr "" + +#: .././mainwin.py:3992 +msgid "_Disable checking/downloading" +msgstr "" + +#: .././mainwin.py:4004 +msgid "_Just disable downloading" +msgstr "" + +#: .././mainwin.py:4029 .././mainwin.py:4328 +msgid "D_ownloads" +msgstr "" + +#: .././mainwin.py:4037 +msgid "Channel _properties..." +msgstr "" + +#: .././mainwin.py:4039 +msgid "Playlist _properties..." +msgstr "" + +#: .././mainwin.py:4041 +msgid "Folder _properties..." +msgstr "" + +#: .././mainwin.py:4057 +msgid "_Default location" +msgstr "" + +#: .././mainwin.py:4070 +msgid "_Actual location" +msgstr "" + +#: .././mainwin.py:4082 +msgid "_Show" +msgstr "" + +#: .././mainwin.py:4091 +msgid "D_elete channel" +msgstr "" + +#: .././mainwin.py:4093 +msgid "D_elete playlist" +msgstr "" + +#: .././mainwin.py:4095 +msgid "D_elete folder" +msgstr "" + +#: .././mainwin.py:4154 +msgid "_Check video" +msgstr "" + +#: .././mainwin.py:4177 +msgid "_Download video" +msgstr "" + +#: .././mainwin.py:4197 +msgid "Re-_download this video" +msgstr "" + +#: .././mainwin.py:4210 +msgid "C_ustom download video" +msgstr "" + +#: .././mainwin.py:4285 +msgid "Show system _command" +msgstr "" + +#: .././mainwin.py:4295 +msgid "_Test system command" +msgstr "" + +#: .././mainwin.py:4310 +msgid "_Disable downloads" +msgstr "" + +#: .././mainwin.py:4340 +msgid "Video is _archived" +msgstr "" + +#: .././mainwin.py:4353 +msgid "Video is _bookmarked" +msgstr "" + +#: .././mainwin.py:4364 +msgid "Video is _favourite" +msgstr "" + +#: .././mainwin.py:4375 +msgid "Video is _new" +msgstr "" + +#: .././mainwin.py:4388 +msgid "Video is in _waiting list" +msgstr "" + +#: .././mainwin.py:4399 +msgid "_Mark video" +msgstr "" + +#: .././mainwin.py:4410 +msgid "_Location" +msgstr "" + +#: .././mainwin.py:4420 +msgid "_Properties..." +msgstr "" + +#: .././mainwin.py:4432 +msgid "_Show video" +msgstr "" + +#: .././mainwin.py:4441 +msgid "Available _formats" +msgstr "" + +#: .././mainwin.py:4451 +msgid "Available _subtitles" +msgstr "" + +#: .././mainwin.py:4461 +msgid "_Fetch" +msgstr "" + +#. Delete video +#: .././mainwin.py:4472 +msgid "D_elete video" +msgstr "" + +#. Check/download videos +#: .././mainwin.py:4559 +msgid "_Check videos" +msgstr "" + +#: .././mainwin.py:4579 +msgid "_Download videos" +msgstr "" + +#: .././mainwin.py:4598 +msgid "C_ustom download videos" +msgstr "" + +#: .././mainwin.py:4616 +msgid "D_ownload and watch" +msgstr "" + +#: .././mainwin.py:4633 .././mainwin.py:5376 +msgid "Watch in _player" +msgstr "" + +#: .././mainwin.py:4643 .././mainwin.py:5391 .././mainwin.py:5402 +msgid "Watch on _website" +msgstr "" + +#: .././mainwin.py:4661 .././mainwin.py:5559 +msgid "_Mark for download" +msgstr "" + +#: .././mainwin.py:4673 .././mainwin.py:5570 +msgid "_Download" +msgstr "" + +#: .././mainwin.py:4683 +msgid "_Download and watch" +msgstr "" + +#: .././mainwin.py:4694 .././mainwin.py:5590 +msgid "_Temporary" +msgstr "" + +#: .././mainwin.py:4712 +msgid "_Archived" +msgstr "" + +#: .././mainwin.py:4725 +msgid "Not a_rchived" +msgstr "" + +#: .././mainwin.py:4741 +msgid "_Bookmarked" +msgstr "" + +#: .././mainwin.py:4754 +msgid "Not b_ookmarked" +msgstr "" + +#: .././mainwin.py:4770 +msgid "_Favourite" +msgstr "" + +#: .././mainwin.py:4783 +msgid "Not fa_vourite" +msgstr "" + +#: .././mainwin.py:4799 +msgid "_New" +msgstr "" + +#: .././mainwin.py:4812 +msgid "Not n_ew" +msgstr "" + +#: .././mainwin.py:4828 +msgid "In _waiting list" +msgstr "" + +#: .././mainwin.py:4841 +msgid "Not in w_aiting list" +msgstr "" + +#: .././mainwin.py:4854 +msgid "_Mark videos" +msgstr "" + +#: .././mainwin.py:4863 +msgid "Show p_roperties..." +msgstr "" + +#. Delete videos +#: .././mainwin.py:4878 +msgid "D_elete videos" +msgstr "" + +#. Stop check/download +#: .././mainwin.py:4943 +msgid "_Stop now" +msgstr "" + +#: .././mainwin.py:4957 +msgid "Stop after this _video" +msgstr "" + +#: .././mainwin.py:4972 +msgid "Stop after these v_ideos" +msgstr "" + +#: .././mainwin.py:4987 +msgid "Download _next" +msgstr "" + +#: .././mainwin.py:4999 +msgid "Download _last" +msgstr "" + +#: .././mainwin.py:5022 +msgid "Watch on _YouTube" +msgstr "" + +#: .././mainwin.py:5032 +msgid "Watch on _HookTube" +msgstr "" + +#: .././mainwin.py:5042 +msgid "Watch on _Invidious" +msgstr "" + +#: .././mainwin.py:5054 +msgid "Watch on _Website" +msgstr "" + +#. Delete video +#: .././mainwin.py:5106 +msgid "_Delete video" +msgstr "" + +#. Get URL +#: .././mainwin.py:5153 +msgid "Get _URL" +msgstr "" + +#. Get command +#: .././mainwin.py:5162 +msgid "Get _command" +msgstr "" + +#: .././mainwin.py:5172 +msgid "_Open destination" +msgstr "" + +#: .././mainwin.py:5213 +msgid "Mark as _archived" +msgstr "" + +#: .././mainwin.py:5224 +msgid "Mark as not a_rchived" +msgstr "" + +#: .././mainwin.py:5238 +msgid "Mark as _bookmarked" +msgstr "" + +#: .././mainwin.py:5250 +msgid "Mark as not b_ookmarked" +msgstr "" + +#: .././mainwin.py:5263 +msgid "Mark as _favourite" +msgstr "" + +#: .././mainwin.py:5276 +msgid "Mark as not fa_vourite" +msgstr "" + +#: .././mainwin.py:5289 +msgid "Mark as _new" +msgstr "" + +#: .././mainwin.py:5301 +msgid "Mark as not n_ew" +msgstr "" + +#: .././mainwin.py:5315 +msgid "Mark as in _waiting list" +msgstr "" + +#: .././mainwin.py:5327 +msgid "Mark as not in wai_ting list" +msgstr "" + +#: .././mainwin.py:5359 .././mainwin.py:5580 +msgid "Download and _watch" +msgstr "" + +#: .././mainwin.py:5416 +msgid "_YouTube" +msgstr "" + +#: .././mainwin.py:5426 +msgid "_HookTube" +msgstr "" + +#: .././mainwin.py:5436 +msgid "_Invidious" +msgstr "" + +#: .././mainwin.py:5446 +msgid "TRANSLATOR'S NOTE: Watch on YouTube, Watch on HookTube, etc" +msgstr "" + +#: .././mainwin.py:5451 +msgid "W_atch on" +msgstr "" + +#: .././mainwin.py:5465 +msgid "Auto _notify" +msgstr "" + +#: .././mainwin.py:5481 +msgid "Auto _sound alarm" +msgstr "" + +#: .././mainwin.py:5496 +msgid "Auto _open" +msgstr "" + +#: .././mainwin.py:5509 +msgid "_Download on start" +msgstr "" + +#: .././mainwin.py:5522 +msgid "Download on _stop" +msgstr "" + +#: .././mainwin.py:5538 +msgid "Not a _livestream" +msgstr "" + +#: .././mainwin.py:5548 .././config.py:5248 +msgid "_Livestream" +msgstr "" + +#: .././mainwin.py:6394 +msgid "" +"TRANSLATOR'S NOTE: V = number of videos B = (number of videos) bookmarked D " +"= downloaded F = favourite L = live/livestream N = new W = in waiting list E " +"= (number of) errors W = warnings" +msgstr "" + +#: .././mainwin.py:6401 +msgid "V:" +msgstr "" + +#: .././mainwin.py:6402 +msgid "B:" +msgstr "" + +#: .././mainwin.py:6403 +msgid "D:" +msgstr "" + +#: .././mainwin.py:6404 +msgid "F:" +msgstr "" + +#: .././mainwin.py:6405 +msgid "L:" +msgstr "" + +#: .././mainwin.py:6406 +msgid "N:" +msgstr "" + +#: .././mainwin.py:6407 .././mainwin.py:6418 +msgid "W:" +msgstr "" + +#: .././mainwin.py:6417 +msgid "E:" +msgstr "" + +#: .././mainwin.py:7444 .././mainwin.py:8122 +msgid "Waiting" +msgstr "" + +#: .././mainwin.py:8546 +msgid "Disable automatic copy/paste" +msgstr "" + +#: .././mainwin.py:8637 +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:8644 +msgid "Thread" +msgstr "" + +#: .././mainwin.py:8647 +msgid "_Summary" +msgstr "" + +#: .././mainwin.py:9175 +msgid "Tartube error" +msgstr "" + +#: .././mainwin.py:9228 +msgid "Tartube warning" +msgstr "" + +#: .././mainwin.py:9261 +msgid "_Errors" +msgstr "" + +#: .././mainwin.py:9265 +msgid "Warnings" +msgstr "" + +#: .././mainwin.py:13415 +#, python-brace-format +msgid "The channel contains {0} items, so this action may take a while" +msgstr "" + +#: .././mainwin.py:13422 +#, python-brace-format +msgid "The playlist contains {0} items, so this action may take a while" +msgstr "" + +#: .././mainwin.py:13429 +#, python-brace-format +msgid "The folder contains {0} items, so this action may take a while" +msgstr "" + +#: .././mainwin.py:13809 .././mainwin.py:14690 +msgid "From channel:" +msgstr "" + +#: .././mainwin.py:13811 .././mainwin.py:14692 +msgid "From playlist:" +msgstr "" + +#: .././mainwin.py:13813 .././mainwin.py:14694 +msgid "From folder:" +msgstr "" + +#: .././mainwin.py:13839 +msgid "Livestream has not started yet" +msgstr "" + +#: .././mainwin.py:13848 .././mainwin.py:13854 .././mainwin.py:14741 +#: .././mainwin.py:14748 +msgid "Duration:" +msgstr "" + +#: .././mainwin.py:13854 .././mainwin.py:13860 .././mainwin.py:13869 +#: .././mainwin.py:14748 .././mainwin.py:14755 .././mainwin.py:14765 +#: .././media.py:316 .././media.py:326 .././media.py:1510 .././media.py:1516 +#: .././media.py:1526 +msgid "unknown" +msgstr "" + +#: .././mainwin.py:13858 .././mainwin.py:13860 .././mainwin.py:14752 +#: .././mainwin.py:14754 +msgid "Size:" +msgstr "" + +#: .././mainwin.py:13867 .././mainwin.py:13869 .././mainwin.py:14762 +#: .././mainwin.py:14764 +msgid "Date:" +msgstr "" + +#: .././mainwin.py:14192 +msgid "Watch:" +msgstr "" + +#: .././mainwin.py:14248 +msgid "Temporary:" +msgstr "" + +#: .././mainwin.py:14291 +msgid "Marked:" +msgstr "" + +#: .././mainwin.py:14663 .././mainwin.py:14711 +msgid "Show the full description" +msgstr "" + +#: .././mainwin.py:14664 .././mainwin.py:14712 +msgid "More" +msgstr "" + +#: .././mainwin.py:14676 .././mainwin.py:14720 +msgid "Show the short description" +msgstr "" + +#: .././mainwin.py:14677 .././mainwin.py:14721 +msgid "Less" +msgstr "" + +#: .././mainwin.py:14781 +msgid "Live:" +msgstr "" + +#: .././mainwin.py:14784 .././mainwin.py:14786 .././mainwin.py:14790 +#: .././mainwin.py:15000 .././mainwin.py:15002 .././mainwin.py:15006 +#: .././mainwin.py:15446 +msgid "Notify" +msgstr "" + +#: .././mainwin.py:14794 .././mainwin.py:15010 +msgid "When the livestream starts, notify the user" +msgstr "" + +#: .././mainwin.py:14805 .././mainwin.py:14807 .././mainwin.py:15016 +#: .././mainwin.py:15018 .././mainwin.py:15313 +msgid "Alarm" +msgstr "" + +#: .././mainwin.py:14811 .././mainwin.py:15022 +msgid "When the livestream starts, sound an alarm" +msgstr "" + +#: .././mainwin.py:14816 .././mainwin.py:14818 .././mainwin.py:15028 +#: .././mainwin.py:15030 .././mainwin.py:15491 +msgid "Open" +msgstr "" + +#: .././mainwin.py:14822 .././mainwin.py:15034 +msgid "When the livestream starts, open it" +msgstr "" + +#: .././mainwin.py:14827 .././mainwin.py:14829 .././mainwin.py:15040 +#: .././mainwin.py:15042 .././mainwin.py:15357 +msgid "D/L on start" +msgstr "" + +#: .././mainwin.py:14833 .././mainwin.py:15046 +msgid "When the livestream starts, download it" +msgstr "" + +#: .././mainwin.py:14838 .././mainwin.py:14840 .././mainwin.py:15052 +#: .././mainwin.py:15054 .././mainwin.py:15402 +msgid "D/L on stop" +msgstr "" + +#: .././mainwin.py:14844 .././mainwin.py:15058 +msgid "When the livestream stops, download it" +msgstr "" + +#: .././mainwin.py:14870 +msgid "Download this video" +msgstr "" + +#: .././mainwin.py:14881 +msgid "Watch in your media player" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:14882 .././mainwin.py:16049 +msgid "Player" +msgstr "" + +#: .././mainwin.py:14890 +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:14898 +msgid "Download and watch in your media player" +msgstr "" + +#: .././mainwin.py:14899 +msgid "Download & watch" +msgstr "" + +#: .././mainwin.py:14906 +msgid "Not downloaded" +msgstr "" + +#: .././mainwin.py:14930 +msgid "Watch on YouTube" +msgstr "" + +#: .././mainwin.py:14931 .././mainwin.py:16094 +msgid "YouTube" +msgstr "" + +#: .././mainwin.py:14943 +msgid "Watch on HookTube" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:14944 .././mainwin.py:15905 +msgid "HookTube" +msgstr "" + +#: .././mainwin.py:14953 +msgid "Watch on Invidious" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:14954 .././mainwin.py:15949 +msgid "Invidious" +msgstr "" + +#: .././mainwin.py:14968 +msgid "Watch on website" +msgstr "" + +#: .././mainwin.py:14969 .././mainwin.py:16096 +msgid "Website" +msgstr "" + +#. Links not clickable +#: .././mainwin.py:14979 +msgid "No link" +msgstr "" + +#: .././mainwin.py:15087 +msgid "Download to a temporary folder later" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15088 .././mainwin.py:15105 .././mainwin.py:15861 +msgid "Mark for download" +msgstr "" + +#: .././mainwin.py:15093 +msgid "Download to a temporary folder" +msgstr "" + +#: .././mainwin.py:15099 +msgid "Download to a temporary folder, then watch" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15100 .././mainwin.py:15107 .././mainwin.py:15818 +msgid "D/L and watch" +msgstr "" + +#. Archived/not archived +#: .././mainwin.py:15131 +msgid "Prevent automatic deletion of the video" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15135 .././mainwin.py:15139 .././mainwin.py:15535 +msgid "Archived" +msgstr "" + +#. Bookmarked/not bookmarked +#: .././mainwin.py:15144 +msgid "Show video in Bookmarks folder" +msgstr "" + +#: .././mainwin.py:15148 .././mainwin.py:15152 +msgid "Bookmarked" +msgstr "" + +#. Favourite/not favourite +#: .././mainwin.py:15157 +msgid "Show in Favourite Videos folder" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15161 .././mainwin.py:15165 .././mainwin.py:15625 +msgid "Favourite" +msgstr "" + +#. New/not new +#: .././mainwin.py:15169 +msgid "Mark video as never watched" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15173 .././mainwin.py:15177 .././mainwin.py:15663 +msgid "New" +msgstr "" + +#. In waiting list/not in waiting list +#: .././mainwin.py:15182 +msgid "Show in Waiting Videos folder" +msgstr "" + +#: .././mainwin.py:15185 +msgid "In waiting list" +msgstr "" + +#: .././mainwin.py:15189 +msgid "In Waiting list" +msgstr "" + +#: .././mainwin.py:15308 +msgid "Undo alarm" +msgstr "" + +#: .././mainwin.py:15352 .././mainwin.py:15397 +msgid "Don't D/L" +msgstr "" + +#: .././mainwin.py:15441 +msgid "Undo notify" +msgstr "" + +#: .././mainwin.py:15486 +msgid "Undo open" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15580 +msgid "Not bookmarked" +msgstr "" + +#. Because of an unexplained Gtk problem, there is usually a crash after +#. 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:15708 +msgid "Not in waiting list" +msgstr "" + +#: .././mainwin.py:16724 +msgid "Tartube failed to start because:" +msgstr "" + +#: .././mainwin.py:16733 +msgid "If you don't know how to resolve this error, please contact the authors" +msgstr "" + +#: .././mainwin.py:16738 +msgid "here" +msgstr "" + +#. 'OK' button +#: .././mainwin.py:16741 .././mainwin.py:19027 .././config.py:426 +#: .././config.py:1602 +msgid "OK" +msgstr "" + +#: .././mainwin.py:16792 .././mainwin.py:19804 .././mainwin.py:19899 +msgid "Welcome to Tartube!" +msgstr "" + +#: .././mainwin.py:16924 +msgid "Add channel" +msgstr "" + +#: .././mainwin.py:16943 +msgid "Enter the channel name" +msgstr "" + +#: .././mainwin.py:16948 +msgid "(Use the channel's real name or a customised name)" +msgstr "" + +#: .././mainwin.py:16956 +msgid "Copy and paste a link to the channel" +msgstr "" + +#: .././mainwin.py:17003 +msgid "(Optional) Add this channel inside a folder" +msgstr "" + +#: .././mainwin.py:17033 +msgid "I want to download videos from this channel automatically" +msgstr "" + +#: .././mainwin.py:17040 .././mainwin.py:17327 .././mainwin.py:17535 +msgid "Don't download anything, just check for new videos" +msgstr "" + +#: .././mainwin.py:17228 +msgid "Add folder" +msgstr "" + +#: .././mainwin.py:17247 +msgid "Enter the folder name" +msgstr "" + +#: .././mainwin.py:17290 +msgid "(Optional) Add this folder inside another folder" +msgstr "" + +#: .././mainwin.py:17321 +msgid "I want to download videos from this folder automatically" +msgstr "" + +#: .././mainwin.py:17419 +msgid "Add playlist" +msgstr "" + +#: .././mainwin.py:17438 +msgid "Enter the playlist name" +msgstr "" + +#: .././mainwin.py:17443 +msgid "(Use the playlist's real name or a customised name)" +msgstr "" + +#: .././mainwin.py:17451 +msgid "Copy and paste a link to the playlist" +msgstr "" + +#: .././mainwin.py:17498 +msgid "(Optional) Add this playlist inside a folder" +msgstr "" + +#: .././mainwin.py:17528 +msgid "I want to download videos from this playlist automatically" +msgstr "" + +#: .././mainwin.py:17725 +msgid "Add videos" +msgstr "" + +#: .././mainwin.py:17744 +msgid "Copy and paste the links to one or more videos" +msgstr "" + +#: .././mainwin.py:17750 +msgid "Links containing multiple videos will be converted to a channel" +msgstr "" + +#: .././mainwin.py:17757 +msgid "Links containing multiple videos will be converted to a playlist" +msgstr "" + +#: .././mainwin.py:17764 +msgid "Links containing multiple videos will be downloaded separately" +msgstr "" + +#: .././mainwin.py:17771 +msgid "Links containing multiple videos will not be downloaded at all" +msgstr "" + +#: .././mainwin.py:17853 +msgid "Add the videos to this folder" +msgstr "" + +#: .././mainwin.py:17883 +msgid "I want to download these videos automatically" +msgstr "" + +#: .././mainwin.py:17889 +msgid "Don't download anything, just check the videos" +msgstr "" + +#: .././mainwin.py:18054 +msgid "Select a date" +msgstr "" + +#: .././mainwin.py:18160 +msgid "Delete channel" +msgstr "" + +#: .././mainwin.py:18162 +msgid "Delete playlist" +msgstr "" + +#: .././mainwin.py:18164 +msgid "Delete folder" +msgstr "" + +#: .././mainwin.py:18167 +msgid "Empty channel" +msgstr "" + +#: .././mainwin.py:18169 +msgid "Empty playlist" +msgstr "" + +#: .././mainwin.py:18171 +msgid "Empty folder" +msgstr "" + +#: .././mainwin.py:18205 +msgid "This channel does not contain any videos" +msgstr "" + +#: .././mainwin.py:18207 +msgid "This playlist does not contain any videos" +msgstr "" + +#: .././mainwin.py:18209 +msgid "This folder doesn't contain anything" +msgstr "" + +#: .././mainwin.py:18215 +msgid "(but there might be some files in Tartube's data folder)" +msgstr "" + +#: .././mainwin.py:18228 +msgid "This channel contains:" +msgstr "" + +#: .././mainwin.py:18230 +msgid "This playlist contains:" +msgstr "" + +#: .././mainwin.py:18232 +msgid "This folder contains:" +msgstr "" + +#: .././mainwin.py:18239 +msgid "1 folder" +msgstr "" + +#: .././mainwin.py:18241 +#, python-brace-format +msgid "{0} folders" +msgstr "" + +#: .././mainwin.py:18248 +msgid "1 channel" +msgstr "" + +#: .././mainwin.py:18250 +#, python-brace-format +msgid "{0} channels" +msgstr "" + +#: .././mainwin.py:18257 +msgid "1 playlist" +msgstr "" + +#: .././mainwin.py:18259 +#, python-brace-format +msgid "{0} playlists" +msgstr "" + +#: .././mainwin.py:18266 .././mainwin.py:18691 +msgid "1 video" +msgstr "" + +#: .././mainwin.py:18268 .././mainwin.py:18694 +#, python-brace-format +msgid "{0} videos" +msgstr "" + +#: .././mainwin.py:18281 +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:18287 +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:18293 +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:18302 +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:18308 +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:18314 +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:18331 +msgid "Just remove the channel from this list" +msgstr "" + +#: .././mainwin.py:18333 +msgid "Just remove the playlist from this list" +msgstr "" + +#: .././mainwin.py:18335 +msgid "Just remove the folder from this list" +msgstr "" + +#: .././mainwin.py:18340 +msgid "Just empty the channel in this list" +msgstr "" + +#: .././mainwin.py:18342 +msgid "Just empty the playlist in this list" +msgstr "" + +#: .././mainwin.py:18344 +msgid "Just empty the folder in this list" +msgstr "" + +#: .././mainwin.py:18350 +msgid "Delete all files" +msgstr "" + +#: .././mainwin.py:18402 +msgid "Export from database" +msgstr "" + +#: .././mainwin.py:18426 +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:18433 +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:18449 +msgid "Choose what should be included:" +msgstr "" + +#: .././mainwin.py:18457 +msgid "Include lists of videos" +msgstr "" + +#: .././mainwin.py:18462 +msgid "Include channels" +msgstr "" + +#: .././mainwin.py:18467 +msgid "Include playlists" +msgstr "" + +#: .././mainwin.py:18472 +msgid "Preserve folder structure" +msgstr "" + +#: .././mainwin.py:18480 +msgid "Export as plain text" +msgstr "" + +#: .././mainwin.py:18566 +msgid "Import into database" +msgstr "" + +#: .././mainwin.py:18589 +msgid "Choose which items to import" +msgstr "" + +#: .././mainwin.py:18610 +msgid "Import" +msgstr "" + +#: .././mainwin.py:18626 +msgid "Name" +msgstr "" + +#: .././mainwin.py:18646 +msgid "Import videos" +msgstr "" + +#: .././mainwin.py:18651 +msgid "Merge channels/playlists/folders" +msgstr "" + +#. Bottom strip +#: .././mainwin.py:18654 .././mainwin.py:20527 +msgid "Select all" +msgstr "" + +#: .././mainwin.py:18659 +msgid "Unselect all" +msgstr "" + +#: .././mainwin.py:18921 +msgid "Mount drive" +msgstr "" + +#: .././mainwin.py:18945 +msgid "The Tartube data folder is set to:" +msgstr "" + +#: .././mainwin.py:18958 +msgid "...but this folder doesn't exist" +msgstr "" + +#: .././mainwin.py:18961 +msgid "...but Tartube cannot write to this folder" +msgstr "" + +#: .././mainwin.py:18971 +msgid "I have mounted the drive, please try again" +msgstr "" + +#: .././mainwin.py:18977 +msgid "Use this data folder:" +msgstr "" + +#: .././mainwin.py:19004 +msgid "Select a different data folder" +msgstr "" + +#: .././mainwin.py:19010 +msgid "Use the default data folder" +msgstr "" + +#: .././mainwin.py:19016 +msgid "Shut down Tartube" +msgstr "" + +#. 'Cancel' button +#: .././mainwin.py:19023 .././config.py:435 +msgid "Cancel" +msgstr "" + +#: .././mainwin.py:19149 +msgid "The folder still doesn't exist. Please try a different option" +msgstr "" + +#: .././mainwin.py:19216 +msgid "Stale lockfile" +msgstr "" + +#: .././mainwin.py:19253 +msgid "" +"Failed to load the Tartube database file, because another instance of " +"Tartube seems to be using it" +msgstr "" + +#: .././mainwin.py:19260 +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)" +msgstr "" + +#: .././mainwin.py:19265 +msgid "If you are not sure, then click 'No'" +msgstr "" + +#: .././mainwin.py:19273 +msgid "Yes, I'm sure" +msgstr "" + +#: .././mainwin.py:19280 +msgid "No, I'm not sure" +msgstr "" + +#: .././mainwin.py:19374 +msgid "Rename channel" +msgstr "" + +#: .././mainwin.py:19376 +msgid "Rename playlist" +msgstr "" + +#: .././mainwin.py:19378 +msgid "Rename folder" +msgstr "" + +#: .././mainwin.py:19402 +msgid "Set the new name for the channel:" +msgstr "" + +#: .././mainwin.py:19404 +msgid "Set the new name for the playlist:" +msgstr "" + +#: .././mainwin.py:19406 +msgid "Set the new name for the folder:" +msgstr "" + +#: .././mainwin.py:19412 +msgid "N.B. This procedure will modify your filesystem!\n" +msgstr "" + +#: .././mainwin.py:19473 +msgid "Set download destination" +msgstr "" + +#: .././mainwin.py:19498 +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:19503 +msgid "" +"This playlist can store its videos in its own system folder, or it can store " +"them in a different folder" +msgstr "" + +#: .././mainwin.py:19508 +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:19516 +msgid "Choose a different system folder if:" +msgstr "" + +#: .././mainwin.py:19519 +msgid "" +"1. You want to add a channel and its playlists, without downloading the same " +"video twice" +msgstr "" + +#: .././mainwin.py:19526 +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:19539 +msgid "Use this channel's own folder" +msgstr "" + +#: .././mainwin.py:19541 +msgid "Use this playlist's own folder" +msgstr "" + +#: .././mainwin.py:19543 +msgid "Use this folder's own system folder" +msgstr "" + +#: .././mainwin.py:19834 +msgid "Tartube's data folder will be:" +msgstr "" + +#: .././mainwin.py:19849 +msgid "Use this folder" +msgstr "" + +#: .././mainwin.py:19854 +msgid "Choose a different folder" +msgstr "" + +#: .././mainwin.py:19930 +msgid "Click OK to create a folder in which Tartube can store its videos" +msgstr "" + +#: .././mainwin.py:19937 +msgid "" +"If you have used Tartube before, you can select an existing folder instead " +"of creating a new one" +msgstr "" + +#: .././mainwin.py:19992 +msgid "Set nickname" +msgstr "" + +#: .././mainwin.py:20017 +#, python-brace-format +msgid "" +"Set a nickname for the channel '{0}' (or leave it blank to reset the " +"nickname)" +msgstr "" + +#: .././mainwin.py:20022 +#, python-brace-format +msgid "" +"Set a nickname for the playlist '{0}' (or leave it blank to reset the " +"nickname)" +msgstr "" + +#: .././mainwin.py:20027 +#, python-brace-format +msgid "" +"Set a nickname for the folder '{0}' (or leave it blank to reset the nickname)" +msgstr "" + +#: .././mainwin.py:20093 +msgid "Show system command" +msgstr "" + +#: .././mainwin.py:20137 +msgid "Update" +msgstr "" + +#: .././mainwin.py:20146 +msgid "Copy to clipboard" +msgstr "" + +#: .././mainwin.py:20320 +msgid "Test youtube-dl" +msgstr "" + +#: .././mainwin.py:20340 +msgid "URL of the video to download (optional)" +msgstr "" + +#: .././mainwin.py:20351 +msgid "youtube-dl command line options (optional)" +msgstr "" + +#: .././mainwin.py:20429 +msgid "Tidy up files" +msgstr "" + +#: .././mainwin.py:20431 +msgid "Tidy up channel" +msgstr "" + +#: .././mainwin.py:20433 +msgid "Tidy up playlist" +msgstr "" + +#: .././mainwin.py:20435 +msgid "Tidy up folder" +msgstr "" + +#: .././mainwin.py:20464 +msgid "Check that videos are not corrupted" +msgstr "" + +#: .././mainwin.py:20469 +msgid "Delete corrupted video files" +msgstr "" + +#: .././mainwin.py:20479 +msgid "Check that videos do/don't exist" +msgstr "" + +#: .././mainwin.py:20486 +msgid "" +"Delete downloaded video files (doesn't remove videos from Tartube's database)" +msgstr "" + +#: .././mainwin.py:20498 +msgid "Also delete all video/audio files with the same name" +msgstr "" + +#: .././mainwin.py:20507 +msgid "Delete all description files" +msgstr "" + +#: .././mainwin.py:20511 +msgid "Delete all metadata (JSON) files" +msgstr "" + +#: .././mainwin.py:20515 +msgid "Delete all annotation files" +msgstr "" + +#: .././mainwin.py:20519 +msgid "Delete all thumbnail files" +msgstr "" + +#: .././mainwin.py:20523 +msgid "Delete all youtube-dl archive files" +msgstr "" + +#: .././mainwin.py:20532 +msgid "Select none" +msgstr "" + +#. 'Reset' button +#: .././config.py:408 .././config.py:8714 +msgid "Reset" +msgstr "" + +#: .././config.py:412 +msgid "Reset changes without closing the window" +msgstr "" + +#. 'Apply' button +#: .././config.py:417 +msgid "Apply" +msgstr "" + +#: .././config.py:421 +msgid "Apply changes without closing the window" +msgstr "" + +#: .././config.py:429 +msgid "Apply changes" +msgstr "" + +#: .././config.py:438 +msgid "Cancel changes" +msgstr "" + +#: .././config.py:1279 +msgid "Listed as" +msgstr "" + +#: .././config.py:1291 +msgid "Contained in" +msgstr "" + +#: .././config.py:1350 +msgid "Channel URL" +msgstr "" + +#: .././config.py:1352 +msgid "Playlist URL" +msgstr "" + +#: .././config.py:1354 .././config.py:2370 +msgid "Video URL" +msgstr "" + +#: .././config.py:1384 +msgid "Download to" +msgstr "" + +#: .././config.py:1423 +msgid "Location" +msgstr "" + +#: .././config.py:1444 +msgid "Download _options" +msgstr "" + +#: .././config.py:1448 .././config.py:1968 .././config.py:2964 +#: .././config.py:3003 +msgid "Download options" +msgstr "" + +#: .././config.py:1452 +msgid "Apply download options" +msgstr "" + +#: .././config.py:1459 +msgid "Edit download options" +msgstr "" + +#: .././config.py:1466 +msgid "Remove download options" +msgstr "" + +#: .././config.py:1605 +msgid "Close this window" +msgstr "" + +#. Add this tab... +#: .././config.py:2156 .././config.py:5097 .././config.py:5556 +#: .././config.py:5915 .././config.py:6155 +msgid "_General" +msgstr "" + +#: .././config.py:2162 +msgid "General options" +msgstr "" + +#: .././config.py:2173 +msgid "These options have been applied to:" +msgstr "" + +#: .././config.py:2179 +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" +msgstr "" + +#: .././config.py:2243 +msgid "Show advanced download options" +msgstr "" + +#: .././config.py:2253 +msgid "Import general download options into this window" +msgstr "" + +#: .././config.py:2268 +msgid "Completely reset all download options to their default values" +msgstr "" + +#. Add this tab... +#: .././config.py:2282 +msgid "_Files" +msgstr "" + +#: .././config.py:2302 +msgid "File _names" +msgstr "" + +#: .././config.py:2310 +msgid "File name options" +msgstr "" + +#: .././config.py:2315 +msgid "Format for video file names" +msgstr "" + +#: .././config.py:2339 +msgid "youtube-dl file output template" +msgstr "" + +#: .././config.py:2359 +msgid "Add to template:" +msgstr "" + +#: .././config.py:2364 .././config.py:4986 +msgid "Video properties" +msgstr "" + +#: .././config.py:2366 +msgid "Video ID" +msgstr "" + +#: .././config.py:2367 +msgid "Video title" +msgstr "" + +#: .././config.py:2368 +msgid "Alternative video ID" +msgstr "" + +#: .././config.py:2369 +msgid "Secondary video title" +msgstr "" + +#: .././config.py:2371 +msgid "Video filename extension" +msgstr "" + +#: .././config.py:2372 +msgid "Video licence" +msgstr "" + +#: .././config.py:2373 +msgid "Age restriction (years)" +msgstr "" + +#: .././config.py:2374 +msgid "Is a livestream" +msgstr "" + +#: .././config.py:2375 +msgid "Autonumber videos, starting at 0" +msgstr "" + +#: .././config.py:2377 +msgid "Creator/uploader" +msgstr "" + +#: .././config.py:2379 .././config.py:2380 +msgid "Full name of video uploader" +msgstr "" + +#: .././config.py:2381 +msgid "Nickname/ID of video uploader" +msgstr "" + +#: .././config.py:2382 +msgid "Channel name" +msgstr "" + +#: .././config.py:2383 +msgid "Channel ID" +msgstr "" + +#: .././config.py:2384 +msgid "Playlist name" +msgstr "" + +#: .././config.py:2385 +msgid "Playlist ID" +msgstr "" + +#: .././config.py:2386 +msgid "Video index in playlist" +msgstr "" + +#: .././config.py:2388 +msgid "Date/time/location" +msgstr "" + +#: .././config.py:2390 +msgid "Release date (YYYYMMDD)" +msgstr "" + +#: .././config.py:2391 +msgid "Release time (UNIX timestamp)" +msgstr "" + +#: .././config.py:2392 +msgid "Upload data (YYYYMMDD)" +msgstr "" + +#: .././config.py:2393 +msgid "Video length (seconds)" +msgstr "" + +#: .././config.py:2394 +msgid "Filming location" +msgstr "" + +#: .././config.py:2396 .././config.py:2398 +msgid "Video format" +msgstr "" + +#: .././config.py:2399 +msgid "youtube-dl format code" +msgstr "" + +#: .././config.py:2400 +msgid "Video width" +msgstr "" + +#: .././config.py:2401 +msgid "Video height" +msgstr "" + +#: .././config.py:2403 +msgid "Video frame rate" +msgstr "" + +#: .././config.py:2404 +msgid "Average video/audio bitrate (KiB/s)" +msgstr "" + +#: .././config.py:2405 +msgid "Average video bitrate (KiB/s)" +msgstr "" + +#: .././config.py:2406 +msgid "Average audio bitrate (KiB/s)" +msgstr "" + +#: .././config.py:2408 +msgid "Ratings/comments" +msgstr "" + +#: .././config.py:2410 +msgid "Number of views" +msgstr "" + +#: .././config.py:2411 +msgid "Number of positive ratings" +msgstr "" + +#: .././config.py:2412 +msgid "Number of negative ratings" +msgstr "" + +#: .././config.py:2413 +msgid "Average rating" +msgstr "" + +#: .././config.py:2414 +msgid "Number of reposts" +msgstr "" + +#: .././config.py:2415 +msgid "Number of comments" +msgstr "" + +#: .././config.py:2451 +msgid "Add" +msgstr "" + +#. Add this tab... +#: .././config.py:2479 .././config.py:6495 +msgid "_Filesystem" +msgstr "" + +#: .././config.py:2489 +msgid "Filesystem options" +msgstr "" + +#: .././config.py:2494 +msgid "Restrict filenames to ASCII characters" +msgstr "" + +#: .././config.py:2500 +msgid "Use the server's file modification time" +msgstr "" + +#: .././config.py:2507 +msgid "Filesystem overrides" +msgstr "" + +#: .././config.py:2512 +msgid "Download all videos into this folder" +msgstr "" + +#: .././config.py:2566 +msgid "_Write files" +msgstr "" + +#: .././config.py:2572 +msgid "Write other file options" +msgstr "" + +#: .././config.py:2577 +msgid "Write video's description to a .description file" +msgstr "" + +#: .././config.py:2583 +msgid "Write video's metadata to an .info.json file" +msgstr "" + +#: .././config.py:2589 +msgid "Write video's annotations to an .annotations.xml file" +msgstr "" + +#: .././config.py:2595 +msgid "Write the video's thumbnail to the same folder" +msgstr "" + +#: .././config.py:2609 +msgid "_Keep files" +msgstr "" + +#: .././config.py:2615 +msgid "Options during real (not simulated) downloads" +msgstr "" + +#: .././config.py:2621 .././config.py:2652 +msgid "Keep the description file after Tartube shuts down" +msgstr "" + +#: .././config.py:2627 .././config.py:2658 +msgid "Keep the metadata file after Tartube shuts down" +msgstr "" + +#: .././config.py:2633 .././config.py:2664 +msgid "Keep the annotations file after Tartube shuts down" +msgstr "" + +#: .././config.py:2639 .././config.py:2670 +msgid "Keep the thumbnail file after Tartube shuts down" +msgstr "" + +#: .././config.py:2646 +msgid "Options during simulated (not real) downloads" +msgstr "" + +#. Add this tab... +#: .././config.py:2684 +msgid "F_ormats" +msgstr "" + +#: .././config.py:2703 +msgid "_Preferred" +msgstr "" + +#: .././config.py:2711 +msgid "Preferred format options" +msgstr "" + +#: .././config.py:2717 +msgid "Recognised video/audio formats" +msgstr "" + +#: .././config.py:2728 +msgid "Add format" +msgstr "" + +#: .././config.py:2734 +msgid "List of preferred formats" +msgstr "" + +#: .././config.py:2751 +msgid "Remove format" +msgstr "" + +#. Add this tab... +#: .././config.py:2811 .././config.py:3521 +msgid "_Advanced" +msgstr "" + +#: .././config.py:2820 +msgid "Multiple format options" +msgstr "" + +#: .././config.py:2829 +msgid "" +"Multiple formats will not be downloaded, because youtube-dl is creating an " +"archive file" +msgstr "" + +#: .././config.py:2832 +msgid "The archive file can be disabled in the System Preferences window" +msgstr "" + +#: .././config.py:2841 +msgid "" +"For each video, download the first available format from the preferred list" +msgstr "" + +#: .././config.py:2855 +msgid "" +"From the preferred list, download the first format that's available for all " +"videos" +msgstr "" + +#: .././config.py:2869 +msgid "For each video, download all available formats from the preferred list" +msgstr "" + +#: .././config.py:2882 +msgid "Download all available formats for all videos" +msgstr "" + +#: .././config.py:2915 +msgid "Other format options" +msgstr "" + +#: .././config.py:2920 +msgid "Prefer free video formats, unless one is specified above" +msgstr "" + +#: .././config.py:2926 +msgid "Do not download DASH-related data for YouTube videos" +msgstr "" + +#: .././config.py:2933 +msgid "If a merge is required after post-processing, output to this format" +msgstr "" + +#. Add this tab... +#: .././config.py:2958 .././config.py:2977 .././config.py:7886 +msgid "_Downloads" +msgstr "" + +#: .././config.py:3020 +msgid "_Playlists" +msgstr "" + +#: .././config.py:3035 +msgid "_Size limits" +msgstr "" + +#: .././config.py:3049 +msgid "_Dates" +msgstr "" + +#: .././config.py:3061 +msgid "_Views" +msgstr "" + +#: .././config.py:3074 +msgid "_Filtering" +msgstr "" + +#: .././config.py:3088 +msgid "_External" +msgstr "" + +#: .././config.py:3100 +msgid "_Sound only" +msgstr "" + +#: .././config.py:3105 +msgid "Sound only options" +msgstr "" + +#: .././config.py:3111 +msgid "" +"Download each video, extract the sound, and then discard the original videos" +msgstr "" + +#: .././config.py:3116 +msgid "(requires that FFmpeg or AVConv is installed on your system)" +msgstr "" + +#: .././config.py:3126 +msgid "Use this audio format:" +msgstr "" + +#: .././config.py:3141 +msgid "Use this audio quality:" +msgstr "" + +#: .././config.py:3147 .././config.py:3220 +msgid "High" +msgstr "" + +#: .././config.py:3148 .././config.py:3221 +msgid "Medium" +msgstr "" + +#: .././config.py:3149 .././config.py:3222 +msgid "Low" +msgstr "" + +#: .././config.py:3167 +msgid "_Post-process" +msgstr "" + +#: .././config.py:3173 .././config.py:3490 +msgid "Post-processing options" +msgstr "" + +#: .././config.py:3179 +msgid "Post-process video files to convert them to audio-only files" +msgstr "" + +#: .././config.py:3186 +msgid "Prefer avconv over ffmpeg" +msgstr "" + +#: .././config.py:3194 +msgid "Prefer ffmpeg over avconv (default)" +msgstr "" + +#: .././config.py:3202 +msgid "Audio format of the post-processed file" +msgstr "" + +#: .././config.py:3215 +msgid "Audio quality of the post-processed file" +msgstr "" + +#: .././config.py:3232 +msgid "Encode video to another format, if necessary" +msgstr "" + +#: .././config.py:3244 +msgid "Arguments to pass to post-processor" +msgstr "" + +#: .././config.py:3254 +msgid "Keep original file after processing it" +msgstr "" + +#: .././config.py:3261 +msgid "Merge subtitles file with video (.mp4 only)" +msgstr "" + +#: .././config.py:3272 +msgid "Embed thumbnail in audio file as cover art" +msgstr "" + +#: .././config.py:3278 +msgid "Write metadata to the video file" +msgstr "" + +#: .././config.py:3284 +msgid "Automatically correct known faults of the file" +msgstr "" + +#: .././config.py:3290 +msgid "Do nothing" +msgstr "" + +#: .././config.py:3291 +msgid "Warn, but do nothing" +msgstr "" + +#: .././config.py:3292 +msgid "Fix if possible, otherwise warn" +msgstr "" + +#. Add this tab... +#: .././config.py:3309 +msgid "S_ubtitles" +msgstr "" + +#: .././config.py:3326 +msgid "_Options" +msgstr "" + +#: .././config.py:3330 +msgid "Subtitles options" +msgstr "" + +#: .././config.py:3336 +msgid "Don't download the subtitles file" +msgstr "" + +#: .././config.py:3347 +msgid "Download the automatic subtitles file (YouTube only)" +msgstr "" + +#: .././config.py:3359 +msgid "Download all available subtitles files" +msgstr "" + +#: .././config.py:3371 +msgid "Download subtitles file for these languages:" +msgstr "" + +#: .././config.py:3394 +msgid "Add language" +msgstr "" + +#: .././config.py:3407 +msgid "Remove language" +msgstr "" + +#: .././config.py:3465 +msgid "_More options" +msgstr "" + +#: .././config.py:3471 +msgid "Subtitle format options" +msgstr "" + +#: .././config.py:3477 +msgid "Preferred subtitle format(s), e.g. 'srt', 'vtt', 'srt/ass/vtt/lrc/best'" +msgstr "" + +#: .././config.py:3495 +msgid "Applies to .mp4 videos only; requires FFmpeg/AVConv" +msgstr "" + +#: .././config.py:3502 +msgid "During post-processing, merge subtitles file with video" +msgstr "" + +#: .././config.py:3541 +msgid "_Authentication" +msgstr "" + +#: .././config.py:3549 +msgid "Authentication options" +msgstr "" + +#: .././config.py:3554 +msgid "Username with which to log in" +msgstr "" + +#: .././config.py:3564 +msgid "Password with which to log in" +msgstr "" + +#: .././config.py:3574 +msgid "Password required for this URL" +msgstr "" + +#: .././config.py:3584 +msgid "Two-factor authentication code" +msgstr "" + +#: .././config.py:3594 +msgid "Use .netrc authentication data" +msgstr "" + +#: .././config.py:3607 +msgid "_Network" +msgstr "" + +#: .././config.py:3613 +msgid "Network options" +msgstr "" + +#: .././config.py:3618 +msgid "Use this HTTP/HTTPS proxy" +msgstr "" + +#: .././config.py:3628 +msgid "Time to wait for socket connection, before giving up" +msgstr "" + +#: .././config.py:3638 +msgid "Bind with this Client-side IP address" +msgstr "" + +#: .././config.py:3648 +msgid "Connect using IPv4 only" +msgstr "" + +#: .././config.py:3654 +msgid "Connect using IPv6 only" +msgstr "" + +#: .././config.py:3668 +msgid "_Geo-restriction" +msgstr "" + +#: .././config.py:3676 +msgid "Geo-restriction options" +msgstr "" + +#: .././config.py:3681 +msgid "Use this proxy to verify IP address" +msgstr "" + +#: .././config.py:3691 +msgid "Bypass using fake X-Forwarded-For HTTP header" +msgstr "" + +#: .././config.py:3697 +msgid "Don't bypass using fake HTTP header" +msgstr "" + +#: .././config.py:3703 +msgid "Bypass geo-restriction with ISO 3166-2 country code" +msgstr "" + +#: .././config.py:3713 +msgid "Bypass with explicit IP block in CIDR notation" +msgstr "" + +#: .././config.py:3736 +msgid "Workaround options" +msgstr "" + +#: .././config.py:3741 +msgid "Custom user agent for youtube-dl" +msgstr "" + +#: .././config.py:3751 +msgid "Custom referer if video access has restricted domain" +msgstr "" + +#: .././config.py:3761 +msgid "Force this encoding (experimental)" +msgstr "" + +#: .././config.py:3771 +msgid "Suppress HTTPS certificate validation" +msgstr "" + +#: .././config.py:3778 +msgid "" +"Use an unencrypted connection to retrieve information about videos (YouTube " +"only)" +msgstr "" + +#: .././config.py:3859 +msgid "Prefer HLS (HTTP Live Streaming)" +msgstr "" + +#: .././config.py:3865 +msgid "Prefer FFMpeg over native HLS downloader" +msgstr "" + +#: .././config.py:3871 +msgid "Include advertisements (experimental feature)" +msgstr "" + +#: .././config.py:3877 +msgid "Ignore errors and continue the download operation" +msgstr "" + +#: .././config.py:3883 +msgid "Number of retries" +msgstr "" + +#: .././config.py:3903 +msgid "Download videos suitable for this age" +msgstr "" + +#: .././config.py:3923 +msgid "Playlist options" +msgstr "" + +#: .././config.py:3929 +msgid "" +"youtube-dl treats channels and playlists the same way, so these options can " +"be used with both" +msgstr "" + +#: .././config.py:3936 +msgid "Start downloading playlist from index" +msgstr "" + +#: .././config.py:3947 +msgid "Stop downloading playlist at index" +msgstr "" + +#: .././config.py:3958 +msgid "Abort operation after downloading this many videos" +msgstr "" + +#: .././config.py:3969 +msgid "Abort downloading the playlist if an error occurs" +msgstr "" + +#: .././config.py:3975 +msgid "Download playlist in reverse order" +msgstr "" + +#: .././config.py:3981 +msgid "Download playlist in random order" +msgstr "" + +#: .././config.py:3996 +msgid "Video size limit options" +msgstr "" + +#: .././config.py:4001 +msgid "Minimum file size for video downloads" +msgstr "" + +#: .././config.py:4018 +msgid "Maximum file size for video downloads" +msgstr "" + +#: .././config.py:4045 +msgid "Video date options" +msgstr "" + +#: .././config.py:4050 +msgid "Only videos uploaded on this date" +msgstr "" + +#: .././config.py:4060 .././config.py:4080 .././config.py:4100 +#: .././config.py:8710 +msgid "Set" +msgstr "" + +#: .././config.py:4070 +msgid "Only videos uploaded before this date" +msgstr "" + +#: .././config.py:4090 +msgid "Only videos uploaded after this date" +msgstr "" + +#: .././config.py:4120 +msgid "Video views options" +msgstr "" + +#: .././config.py:4125 +msgid "Minimum number of views" +msgstr "" + +#: .././config.py:4136 +msgid "Maximum number of views" +msgstr "" + +#: .././config.py:4161 +msgid "Video filtering options" +msgstr "" + +#: .././config.py:4166 +msgid "Download only matching titles (regex or caseless substring)" +msgstr "" + +#: .././config.py:4177 +msgid "Don't download only matching titles (regex or caseless substring)" +msgstr "" + +#: .././config.py:4189 +msgid "Generic video filter, for example:" +msgstr "" + +#: .././config.py:4209 +msgid "External downloader options" +msgstr "" + +#: .././config.py:4214 +msgid "Use this external downloader" +msgstr "" + +#: .././config.py:4231 +msgid "Arguments to pass to external downloader" +msgstr "" + +#: .././config.py:4304 .././config.py:4696 +msgid "This procedure cannot be reversed. Are you sure you want to continue?" +msgstr "" + +#: .././config.py:4756 +msgid "When the window is re-opened, some download options will be hidden" +msgstr "" + +#: .././config.py:4765 +msgid "Show advanced download options (when window re-opens)" +msgstr "" + +#: .././config.py:4778 +msgid "When the window is re-opened, all download options will be visible" +msgstr "" + +#: .././config.py:4787 +msgid "Hide advanced download options (when window re-opens)" +msgstr "" + +#: .././config.py:5100 .././config.py:5559 .././config.py:5918 +msgid "General properties" +msgstr "" + +#: .././config.py:5131 +msgid "Always simulate download of this video" +msgstr "" + +#: .././config.py:5154 +msgid "Video has been downloaded" +msgstr "" + +#: .././config.py:5161 +msgid "File size" +msgstr "" + +#: .././config.py:5175 +msgid "Video is marked as unwatched" +msgstr "" + +#: .././config.py:5182 +msgid "Upload time" +msgstr "" + +#: .././config.py:5196 +msgid "Video is archived" +msgstr "" + +#: .././config.py:5203 +msgid "Video is bookmarked" +msgstr "" + +#: .././config.py:5210 +msgid "Receive time" +msgstr "" + +#: .././config.py:5224 +msgid "Video is favourite" +msgstr "" + +#: .././config.py:5231 +msgid "Video is in waiting list" +msgstr "" + +#: .././config.py:5254 +msgid "Livestream properties" +msgstr "" + +#: .././config.py:5259 +msgid "Livestream status" +msgstr "" + +#: .././config.py:5270 +msgid "Waiting to start" +msgstr "" + +#: .././config.py:5272 +msgid "Stream has started" +msgstr "" + +#: .././config.py:5274 +msgid "Not a livestream" +msgstr "" + +#: .././config.py:5281 +msgid "When the livestream starts, show a desktop notification" +msgstr "" + +#: .././config.py:5290 +msgid "When the livestream starts, play an alarm" +msgstr "" + +#: .././config.py:5300 +msgid "When the livestream starts, open it in the system's web browser" +msgstr "" + +#: .././config.py:5312 +msgid "When the livestream starts, begin downloading it immediately" +msgstr "" + +#: .././config.py:5323 .././config.py:8266 +msgid "When a livestream stops, download it (overwriting any earlier file)" +msgstr "" + +#: .././config.py:5339 +msgid "_Description" +msgstr "" + +#: .././config.py:5343 +msgid "Video description" +msgstr "" + +#: .././config.py:5364 .././config.py:5716 +msgid "Errors / Warnings" +msgstr "" + +#: .././config.py:5370 +msgid "Error messages produced the last time this video was checked/downloaded" +msgstr "" + +#: .././config.py:5385 +msgid "" +"Warning messages produced the last time this video was checked/downloaded" +msgstr "" + +#: .././config.py:5441 +msgid "Channel properties" +msgstr "" + +#: .././config.py:5444 +msgid "Playlist properties" +msgstr "" + +#: .././config.py:5577 +msgid "Always simulate download of videos in this channel" +msgstr "" + +#: .././config.py:5579 +msgid "Always simulate download of videos in this playlist" +msgstr "" + +#: .././config.py:5589 +msgid "Disable checking/downloading for this channel" +msgstr "" + +#: .././config.py:5591 +msgid "Disable checking/downloading for this playlist" +msgstr "" + +#: .././config.py:5601 +msgid "This channel is marked as a favourite" +msgstr "" + +#: .././config.py:5603 +msgid "This playlist is marked as a favourite" +msgstr "" + +#: .././config.py:5613 +msgid "Total videos" +msgstr "" + +#: .././config.py:5637 +msgid "Favourite videos" +msgstr "" + +#: .././config.py:5649 +msgid "Downloaded videos" +msgstr "" + +#: .././config.py:5671 +msgid "_RSS feed" +msgstr "" + +#: .././config.py:5674 +msgid "RSS feed" +msgstr "" + +#: .././config.py:5680 +msgid "" +"If Tartube cannot detect the channel's RSS feed, you can enter the URL here" +msgstr "" + +#: .././config.py:5685 +msgid "" +"If Tartube cannot detect the playlist's RSS feed, you can enter the URL here" +msgstr "" + +#: .././config.py:5690 +msgid "(The feed is used to detect livestreams on compatible websites)" +msgstr "" + +#: .././config.py:5722 +msgid "" +"Error messages produced the last time this channel was checked/downloaded" +msgstr "" + +#: .././config.py:5727 +msgid "" +"Error messages produced the last time this playlist was checked/downloaded" +msgstr "" + +#: .././config.py:5745 +msgid "" +"Warning messages produced the last time this channel was checked/downloaded" +msgstr "" + +#: .././config.py:5750 +msgid "" +"Warning messages produced the last time this playlist was checked/downloaded" +msgstr "" + +#: .././config.py:5807 +msgid "Folder properties" +msgstr "" + +#: .././config.py:5935 +msgid "Always simulate download of videos" +msgstr "" + +#: .././config.py:5942 +msgid "Disable checking/downloading for this folder" +msgstr "" + +#: .././config.py:5949 +msgid "This folder is marked as a favourite" +msgstr "" + +#: .././config.py:5956 +msgid "This folder is hidden" +msgstr "" + +#: .././config.py:5963 +msgid "This folder can't be deleted by the user" +msgstr "" + +#: .././config.py:5970 +msgid "This is a system-controlled folder" +msgstr "" + +#: .././config.py:5977 +msgid "Only videos can be added to this folder" +msgstr "" + +#: .././config.py:5984 +msgid "All contents deleted when Tartube shuts down" +msgstr "" + +#: .././config.py:6037 +msgid "System preferences" +msgstr "" + +#: .././config.py:6174 +msgid "_Language" +msgstr "" + +#: .././config.py:6179 +msgid "Language preferences" +msgstr "" + +#: .././config.py:6184 +msgid "Language" +msgstr "" + +#: .././config.py:6220 +msgid "_Stability" +msgstr "" + +#: .././config.py:6230 +msgid "Gtk library" +msgstr "" + +#: .././config.py:6235 +msgid "Current version of the system's Gtk library" +msgstr "" + +#: .././config.py:6250 +msgid "Gtk stability" +msgstr "" + +#: .././config.py:6266 +msgid "" +"Tartube uses the Gtk graphics library. This library is notoriously " +"unreliable and may even causes crashes." +msgstr "" + +#: .././config.py:6273 +msgid "" +"If stability is a problem, you can disable some minor cosmetic features." +msgstr "" + +#: .././config.py:6280 +msgid "" +"Tartube's functionality is not affected. You can do anything, even when " +"cosmetic features are disabled." +msgstr "" + +#: .././config.py:6289 +msgid "" +"Some features are disabled because this version of the library is broken" +msgstr "" + +#: .././config.py:6299 +msgid "Assume that Gtk is broken, and disable those features anyway" +msgstr "" + +#: .././config.py:6315 +msgid "_Modules" +msgstr "" + +#: .././config.py:6320 +msgid "Module availability" +msgstr "" + +#: .././config.py:6326 +msgid "feedparser module is available (required for detecting livestreams)" +msgstr "" + +#: .././config.py:6336 +msgid "moviepy module is available (finds the length of videos, if unknown)" +msgstr "" + +#: .././config.py:6346 +msgid "playsound module is available (sound an alarm when a livestream starts)" +msgstr "" + +#: .././config.py:6356 +msgid "" +"XDG module is available (saves the config file in the standard location)" +msgstr "" + +#: .././config.py:6366 +msgid "Module preferences" +msgstr "" + +#: .././config.py:6372 +msgid "" +"Use 'moviepy' module to get a video's duration, if not known (may be slow)" +msgstr "" + +#: .././config.py:6384 +msgid "Timeout applied when moviepy checks a video file" +msgstr "" + +#: .././config.py:6409 +msgid "_Video matching" +msgstr "" + +#: .././config.py:6417 +msgid "Video matching preferences" +msgstr "" + +#: .././config.py:6422 +msgid "When matching videos on the filesystem:" +msgstr "" + +#: .././config.py:6428 +msgid "The video names must match exactly" +msgstr "" + +#: .././config.py:6435 +msgid "The first # characters must match exactly" +msgstr "" + +#: .././config.py:6449 +msgid "Ignore the last # characters; the remaining name must match exactly" +msgstr "" + +#: .././config.py:6518 +msgid "_Device" +msgstr "" + +#: .././config.py:6523 +msgid "Device preferences" +msgstr "" + +#: .././config.py:6528 +msgid "Size of device (in Mb)" +msgstr "" + +#: .././config.py:6540 +msgid "Free space on device (in Mb)" +msgstr "" + +#: .././config.py:6552 +msgid "Warn user if disk space is less than" +msgstr "" + +#: .././config.py:6570 +msgid "Halt downloads if disk space is less than" +msgstr "" + +#: .././config.py:6609 +msgid "Configuration preferences" +msgstr "" + +#: .././config.py:6614 +msgid "Tartube configuration file loaded from:" +msgstr "" + +#: .././config.py:6642 +msgid "D_atabase" +msgstr "" + +#: .././config.py:6648 +msgid "Database preferences" +msgstr "" + +#: .././config.py:6653 +msgid "Tartube data folder" +msgstr "" + +#: .././config.py:6665 +msgid "Change" +msgstr "" + +#: .././config.py:6667 +msgid "Change to a different data folder" +msgstr "" + +#: .././config.py:6675 +msgid "Recent data folders" +msgstr "" + +#: .././config.py:6696 +msgid "Switch to the selected data folder" +msgstr "" + +#: .././config.py:6706 +msgid "Forget" +msgstr "" + +#: .././config.py:6709 +msgid "Remove the selected data folder from the list" +msgstr "" + +#: .././config.py:6718 +msgid "Forget all" +msgstr "" + +#: .././config.py:6721 +msgid "Forget every folder in this list (except the current one)" +msgstr "" + +#: .././config.py:6734 +msgid "Move the selected folder up the list" +msgstr "" + +#: .././config.py:6742 +msgid "Move the selected folder down the list" +msgstr "" + +#: .././config.py:6770 +msgid "" +"On startup, load the first database on the list (not the most recently-use " +"one)" +msgstr "" + +#: .././config.py:6780 +msgid "If one database is in use, try to load others" +msgstr "" + +#: .././config.py:6788 +msgid "Add new data directories to this list" +msgstr "" + +#: .././config.py:6827 +msgid "DB _Errors" +msgstr "" + +#: .././config.py:6835 +msgid "Database error preferences" +msgstr "" + +#: .././config.py:6840 +msgid "Check Tartube's database for inconsistencies, and fix them" +msgstr "" + +#: .././config.py:6844 +msgid "Check DB" +msgstr "" + +#: .././config.py:6859 +msgid "_Backups" +msgstr "" + +#: .././config.py:6863 +msgid "Backup preferences" +msgstr "" + +#: .././config.py:6868 +msgid "" +"When saving a database file, Tartube makes a backup copy of it (in case " +"something goes wrong)" +msgstr "" + +#: .././config.py:6877 +msgid "Delete the backup file as soon as the save procedure is finished" +msgstr "" + +#: .././config.py:6887 +msgid "Keep the backup file, replacing any previous backup file" +msgstr "" + +#: .././config.py:6898 +msgid "" +"Make a new backup file once per day, after the day's first save procedure" +msgstr "" + +#: .././config.py:6909 +msgid "Make a new backup file for every save procedure" +msgstr "" + +#: .././config.py:6950 +msgid "_Video deletion" +msgstr "" + +#: .././config.py:6958 +msgid "Automatic video deletion preferences" +msgstr "" + +#: .././config.py:6963 +msgid "Automatically delete downloaded videos after this many days" +msgstr "" + +#: .././config.py:6977 +msgid "...but only delete videos which have been watched" +msgstr "" + +#: .././config.py:7008 +msgid "_Temporary folders" +msgstr "" + +#: .././config.py:7014 +msgid "Temporary folder preferences" +msgstr "" + +#: .././config.py:7019 +msgid "Empty temporary folders when Tartube shuts down" +msgstr "" + +#: .././config.py:7028 +msgid "(N.B. Temporary folders are always emptied when Tartube starts up)" +msgstr "" + +#: .././config.py:7036 +msgid "Open temporary folders (on the desktop) when Tartube shuts down" +msgstr "" + +#. Add this tab... +#: .././config.py:7062 +msgid "_Windows" +msgstr "" + +#: .././config.py:7084 +msgid "_Main window" +msgstr "" + +#: .././config.py:7090 +msgid "Main window preferences" +msgstr "" + +#: .././config.py:7095 +msgid "Remember the size of the main window when shutting down" +msgstr "" + +#: .././config.py:7103 +msgid "Don't show labels in the toolbar" +msgstr "" + +#: .././config.py:7111 +msgid "Show tooltips for videos, channels, playlists and folders" +msgstr "" + +#: .././config.py:7120 +msgid "Show smaller icons in the Video Index (left side of the Videos Tab)" +msgstr "" + +#: .././config.py:7131 +msgid "" +"In the Video Index, show detailed statistics about the videos in each " +"channel / playlist / folder" +msgstr "" + +#: .././config.py:7142 +msgid "" +"After clicking on a folder, automatically expand/collapse the tree around it" +msgstr "" + +#: .././config.py:7153 +msgid "Expand the whole tree, not just the level beneath the clicked folder" +msgstr "" + +#: .././config.py:7174 +msgid "Disable the 'Download all' buttons in the toolbar and the Videos Tab" +msgstr "" + +#: .././config.py:7184 +msgid "When Tartube starts, automatically open the Classic Mode tab" +msgstr "" + +#: .././config.py:7202 +msgid "_Tabs" +msgstr "" + +#: .././config.py:7206 +msgid "Tab preferences" +msgstr "" + +#: .././config.py:7212 +msgid "" +"In the Videos Tab, show 'today' and 'yesterday' as the date, when possible" +msgstr "" + +#: .././config.py:7223 +msgid "In the Progress Tab, hide finished videos / channels / playlists" +msgstr "" + +#: .././config.py:7232 +msgid "In the Progress Tab, show results in reverse order" +msgstr "" + +#: .././config.py:7241 +msgid "In the Errors/Warnings Tab, don't reset the tab text when it is clicked" +msgstr "" + +#: .././config.py:7259 +msgid "_System tray" +msgstr "" + +#: .././config.py:7265 +msgid "System tray preferences" +msgstr "" + +#: .././config.py:7270 +msgid "Show icon in system tray" +msgstr "" + +#: .././config.py:7279 +msgid "Close to the tray, rather than closing the application" +msgstr "" + +#: .././config.py:7305 +msgid "_Dialogues" +msgstr "" + +#: .././config.py:7311 +msgid "Dialogue window preferences" +msgstr "" + +#: .././config.py:7316 +msgid "When adding channels/playlists, keep the dialogue window open" +msgstr "" + +#: .././config.py:7326 +msgid "When the dialogue window opens, add URLs from the system clipboard" +msgstr "" + +#: .././config.py:7354 +msgid "_Errors/Warnings" +msgstr "" + +#: .././config.py:7362 +msgid "Errors/Warnings tab preferences" +msgstr "" + +#: .././config.py:7367 +msgid "Show Tartube error messages" +msgstr "" + +#: .././config.py:7375 +msgid "Show Tartube warning messages" +msgstr "" + +#: .././config.py:7383 +msgid "Show server error messages" +msgstr "" + +#: .././config.py:7394 +msgid "Show server warning messages" +msgstr "" + +#: .././config.py:7406 +msgid "youtube-dl error/warning preferences" +msgstr "" + +#: .././config.py:7411 +msgid "" +"TRANSLATOR'S NOTE: These youtube-dl error messages are always in English" +msgstr "" + +#: .././config.py:7416 +msgid "Ignore 'Child process exited with non-zero code' errors" +msgstr "" + +#: .././config.py:7425 +msgid "Ignore 'Unable to download video data: HTTP Error 404' errors" +msgstr "" + +#: .././config.py:7434 +msgid "Ignore 'Did not get any data blocks' errors" +msgstr "" + +#: .././config.py:7443 +msgid "Ignore 'Requested formats are incompatible for merge' warnings" +msgstr "" + +#: .././config.py:7452 +msgid "Ignore 'No video formats found' errors" +msgstr "" + +#: .././config.py:7460 +msgid "Ignore 'There are no annotations to write' warnings" +msgstr "" + +#: .././config.py:7468 +msgid "Ignore 'Video doesn't have subtitles' warnings" +msgstr "" + +#: .././config.py:7484 +msgid "_Websites" +msgstr "" + +#: .././config.py:7492 +msgid "YouTube error/warning preferences" +msgstr "" + +#: .././config.py:7497 +msgid "Ignore YouTube copyright errors" +msgstr "" + +#: .././config.py:7505 +msgid "Ignore YouTube age-restriction errors" +msgstr "" + +#: .././config.py:7513 +msgid "Ignore YouTube deletion by uploader errors" +msgstr "" + +#: .././config.py:7522 +msgid "General preferences" +msgstr "" + +#: .././config.py:7528 +msgid "" +"Ignore any errors/warnings which match lines in this list (applies to all " +"websites)" +msgstr "" + +#: .././config.py:7541 +msgid "These are ordinary strings" +msgstr "" + +#: .././config.py:7548 +msgid "These are regular expressions (regexes)" +msgstr "" + +#. Add this tab... +#: .././config.py:7577 +msgid "_Scheduling" +msgstr "" + +#: .././config.py:7594 +msgid "_Start" +msgstr "" + +#: .././config.py:7600 +msgid "Scheduled start preferences" +msgstr "" + +#: .././config.py:7605 +msgid "Automatic 'Download all' operations" +msgstr "" + +#: .././config.py:7611 .././config.py:7652 +msgid "Disabled" +msgstr "" + +#: .././config.py:7612 .././config.py:7653 +msgid "Performed when Tartube starts" +msgstr "" + +#: .././config.py:7613 .././config.py:7654 +msgid "Performed at regular intervals" +msgstr "" + +#: .././config.py:7633 .././config.py:7674 +msgid "Time (in hours) between operations" +msgstr "" + +#: .././config.py:7646 +msgid "Automatic 'Check all' operations" +msgstr "" + +#: .././config.py:7688 +msgid "After an automatic 'Download/Check all' operation, shut down Tartube" +msgstr "" + +#: .././config.py:7718 +msgid "S_top" +msgstr "" + +#: .././config.py:7724 +msgid "Scheduled stop preferences" +msgstr "" + +#: .././config.py:7729 +msgid "Stop all download operations after this much time" +msgstr "" + +#: .././config.py:7777 +msgid "Stop all download operations after this many videos" +msgstr "" + +#: .././config.py:7804 +msgid "Stop all download operations after this much disk space" +msgstr "" + +#: .././config.py:7847 +msgid "" +"N.B. Disk space is estimated. This setting does not apply to simulated " +"downloads" +msgstr "" + +#: .././config.py:7892 +msgid "Download operation preferences" +msgstr "" + +#: .././config.py:7898 +msgid "Automatically update youtube-dl before every download operation" +msgstr "" + +#: .././config.py:7910 +msgid "" +"Automatically save files at the end of a download/update/refresh operation" +msgstr "" + +#: .././config.py:7921 +msgid "" +"When applying download options to something, clone the general download " +"options" +msgstr "" + +#: .././config.py:7932 +msgid "For simulated downloads, don't check a video in a folder more than once" +msgstr "" + +#: .././config.py:7949 +msgid "_Custom" +msgstr "" + +#: .././config.py:7954 +msgid "Custom download preferences" +msgstr "" + +#: .././config.py:7960 +msgid "" +"In custom downloads, download each video independently of its channel or " +"playlist" +msgstr "" + +#: .././config.py:7972 +msgid "In custom downloads, obtain a YouTube video from the original website" +msgstr "" + +#: .././config.py:7982 +msgid "In custom downloads, obtain the video from HookTube rather than YouTube" +msgstr "" + +#: .././config.py:7994 +msgid "" +"In custom downloads, obtain the video from Invidious rather than YouTube" +msgstr "" + +#: .././config.py:8005 +msgid "" +"In custom downloads, apply a delay after each video/channel/playlist is " +"download" +msgstr "" + +#: .././config.py:8015 +msgid "Maximum delay to apply (in minutes)" +msgstr "" + +#: .././config.py:8032 +msgid "Minimum delay to apply (in minutes; randomises the actual delay)" +msgstr "" + +#: .././config.py:8102 +msgid "Livestream preferences (compatible websites only)" +msgstr "" + +#: .././config.py:8108 +msgid "Detect livestreams announced within this many days" +msgstr "" + +#: .././config.py:8123 +msgid "How often to check the status of livestreams (in minutes)" +msgstr "" + +#: .././config.py:8168 +msgid "Video Catalogue options" +msgstr "" + +#: .././config.py:8173 +msgid "Show livestreams with a different background colour" +msgstr "" + +#: .././config.py:8186 +msgid "Livestream actions (can be toggled for individual videos)" +msgstr "" + +#: .././config.py:8193 +msgid "(currently disabled on MS Windows)" +msgstr "" + +#: .././config.py:8198 +msgid "When a livestream starts, show a desktop notification" +msgstr "" + +#: .././config.py:8212 +msgid "When a livestream starts, sound an alarm" +msgstr "" + +#: .././config.py:8235 +msgid "Plays the selected sound effect" +msgstr "" + +#: .././config.py:8242 +msgid "When a livestream starts, open it in the system's web browser" +msgstr "" + +#: .././config.py:8254 +msgid "When a livestream starts, begin downloading it immediately" +msgstr "" + +#: .././config.py:8287 +msgid "_Notifications" +msgstr "" + +#: .././config.py:8293 +msgid "Desktop notification preferences" +msgstr "" + +#: .././config.py:8300 +msgid "" +"Show a dialogue window at the end of a download/update/refresh/info/tidy " +"operation" +msgstr "" + +#: .././config.py:8310 +msgid "" +"Show a desktop notification at the end of a download/update/refresh/info/" +"tidy operation" +msgstr "" + +#: .././config.py:8324 +msgid "" +"Don't notify the user at the end of a download/update/refresh/info/tidy " +"operation" +msgstr "" + +#: .././config.py:8359 +msgid "_URL flexibility" +msgstr "" + +#: .././config.py:8365 +msgid "URL flexibility preferences" +msgstr "" + +#: .././config.py:8372 +msgid "" +"If a video's URL represents a channel/playlist, not a video, don't download " +"it" +msgstr "" + +#: .././config.py:8381 +msgid "...or, download multiple videos into the containing folder" +msgstr "" + +#: .././config.py:8391 +msgid "...or, create a new channel, and download the videos into that" +msgstr "" + +#: .././config.py:8402 +msgid "...or, create a new playlist, and download the videos into that" +msgstr "" + +#: .././config.py:8441 +msgid "_Performance" +msgstr "" + +#: .././config.py:8449 +msgid "Performance limits" +msgstr "" + +#: .././config.py:8454 +msgid "Limit simultaneous downloads to" +msgstr "" + +#: .././config.py:8472 +msgid "Limit download speed to" +msgstr "" + +#: .././config.py:8498 +msgid "Overriding video format options, limit video resolution to" +msgstr "" + +#: .././config.py:8520 +msgid "Time-saving preferences" +msgstr "" + +#: .././config.py:8526 +msgid "" +"Stop checking/downloading a channel/playlist when it starts sending videos " +"we already have" +msgstr "" + +#: .././config.py:8537 +msgid "Stop after this many videos (when checking)" +msgstr "" + +#: .././config.py:8552 +msgid "Stop after this many videos (when downloading)" +msgstr "" + +#: .././config.py:8587 +msgid "youtube-dl preferences" +msgstr "" + +#: .././config.py:8593 +msgid "youtube-dl executable (system-dependent)" +msgstr "" + +#: .././config.py:8606 +msgid "Default path to youtube-dl executable" +msgstr "" + +#: .././config.py:8619 +msgid "Actual path to use" +msgstr "" + +#: .././config.py:8625 +msgid "Use default path" +msgstr "" + +#: .././config.py:8630 +msgid "Use local path" +msgstr "" + +#: .././config.py:8638 +msgid "Use PyPI path" +msgstr "" + +#: .././config.py:8665 +msgid "Shell command for update operations" +msgstr "" + +#: .././config.py:8692 +msgid "Post-processing preferences" +msgstr "" + +#: .././config.py:8697 +msgid "Path to the ffmpeg/avconv binary" +msgstr "" + +#: .././config.py:8720 +msgid "Install from main menu" +msgstr "" + +#: .././config.py:8726 +msgid "Other preferences" +msgstr "" + +#: .././config.py:8732 +msgid "" +"Allow youtube-dl to create its own archive file (so deleted videos are not " +"re-downloaded)" +msgstr "" + +#: .././config.py:8743 +msgid "" +"When checking videos, apply a 60-second timeout while fetching JSON data" +msgstr "" + +#. Add this tab... +#: .././config.py:8761 +msgid "Out_put" +msgstr "" + +#: .././config.py:8780 +msgid "_Output Tab" +msgstr "" + +#: .././config.py:8786 +msgid "Output Tab preferences" +msgstr "" + +#: .././config.py:8791 +msgid "Display youtube-dl system commands in the Output Tab" +msgstr "" + +#: .././config.py:8800 +msgid "Display output from youtube-dl's STDOUT in the Output Tab" +msgstr "" + +#: .././config.py:8809 .././config.py:8939 +msgid "...but don't write each video's JSON data" +msgstr "" + +#: .././config.py:8820 .././config.py:8950 +msgid "...but don't write each video's download progress" +msgstr "" + +#: .././config.py:8839 +msgid "Display output from youtube-dl's STDERR in the Output Tab" +msgstr "" + +#: .././config.py:8848 +msgid "Empty pages in the Output Tab at the start of every operation" +msgstr "" + +#: .././config.py:8858 +msgid "" +"Show a summary of active threads (changes are applied when Tartube restarts)" +msgstr "" + +#: .././config.py:8870 +msgid "During a refresh operation, show all matching videos in the Output Tab" +msgstr "" + +#: .././config.py:8881 +msgid "...also show all non-matching videos" +msgstr "" + +#: .././config.py:8910 +msgid "_Terminal window" +msgstr "" + +#: .././config.py:8916 +msgid "Terminal window preferences" +msgstr "" + +#: .././config.py:8921 +msgid "Write youtube-dl system commands to the terminal window" +msgstr "" + +#: .././config.py:8930 +msgid "Write output from youtube-dl's STDOUT to the terminal window" +msgstr "" + +#: .././config.py:8972 +msgid "Write output from youtube-dl's STDERR to the terminal window" +msgstr "" + +#: .././config.py:8991 +msgid "_Both" +msgstr "" + +#: .././config.py:8996 +msgid "" +"Special preferences (applies to both the Output Tab and the terminal window)" +msgstr "" + +#: .././config.py:9003 +msgid "Write verbose output (youtube-dl debugging mode)" +msgstr "" + +#: .././config.py:9762 +msgid "Are you sure you want to create a new database at this location?" +msgstr "" + +#: .././config.py:9869 +msgid "Are you sure you want to forget this database?" +msgstr "" + +#: .././config.py:9904 +msgid "Are you sure you want to forget all databases except the current one?" +msgstr "" + +#: .././config.py:10108 +msgid "No database exists at this location:" +msgstr "" + +#: .././config.py:10110 +msgid "Do you want to create a new one?" +msgstr "" + +#: .././config.py:10800 +msgid "The new setting will be applied when Tartube restarts" +msgstr "" + +#: .././config.py:11476 +msgid "Please select the FFmpeg executable" +msgstr "" + +#: .././config.py:12060 +msgid "Database file not loaded" +msgstr "" + +#: .././config.py:12095 +msgid "Database file loaded" +msgstr "" + +#: .././downloads.py:221 +msgid "D/L Manager:" +msgstr "" + +#: .././downloads.py:225 +msgid "Starting download operation" +msgstr "" + +#: .././downloads.py:253 +msgid "Workers: available:" +msgstr "" + +#: .././downloads.py:254 +msgid "total:" +msgstr "" + +#: .././downloads.py:284 +msgid "All threads finished" +msgstr "" + +#: .././downloads.py:306 .././downloads.py:874 .././downloads.py:925 +#: .././downloads.py:935 .././downloads.py:946 +msgid "Thread #" +msgstr "" + +#: .././downloads.py:307 +msgid "Downloading:" +msgstr "" + +#: .././downloads.py:334 +msgid "Downloads complete (or stopped)" +msgstr "" + +#: .././downloads.py:340 +msgid "Halting all workers" +msgstr "" + +#: .././downloads.py:349 +msgid "Join and collect threads" +msgstr "" + +#: .././downloads.py:875 +msgid "Assigned job:" +msgstr "" + +#: .././downloads.py:926 +msgid "Checking RSS feed" +msgstr "" + +#: .././downloads.py:936 +msgid "Job complete" +msgstr "" + +#: .././downloads.py:947 +msgid "Worker now available again" +msgstr "" + +#: .././downloads.py:1369 +msgid "Cannot download videos in a private folder" +msgstr "" + +#: .././downloads.py:2337 +msgid "Download did not start" +msgstr "" + +#: .././downloads.py:2345 .././info.py:352 .././updates.py:293 +#: .././updates.py:448 +msgid "Child process exited with non-zero code: {}" +msgstr "" + +#: .././downloads.py:2414 .././downloads.py:3198 +msgid "" +"This video has a URL that points to a channel or a playlist, not a video" +msgstr "" + +#: .././downloads.py:3090 +msgid "Simulated download of:" +msgstr "" + +#: .././formats.py:66 +msgid "seconds" +msgstr "" + +#: .././formats.py:67 +msgid "minutes" +msgstr "" + +#: .././formats.py:68 +msgid "hours" +msgstr "" + +#: .././formats.py:69 +msgid "days" +msgstr "" + +#: .././formats.py:70 +msgid "weeks" +msgstr "" + +#: .././formats.py:71 +msgid "years" +msgstr "" + +#. System folder names +#: .././formats.py:748 +msgid "All Videos" +msgstr "" + +#: .././formats.py:749 +msgid "Bookmarks" +msgstr "" + +#: .././formats.py:750 +msgid "Favourite Videos" +msgstr "" + +#: .././formats.py:751 +msgid "Livestreams" +msgstr "" + +#: .././formats.py:752 +msgid "New Videos" +msgstr "" + +#: .././formats.py:753 +msgid "Waiting Videos" +msgstr "" + +#: .././formats.py:754 +msgid "Temporary Videos" +msgstr "" + +#: .././formats.py:755 +msgid "Unsorted Videos" +msgstr "" + +#: .././formats.py:760 +msgid "Update using default youtube-dl path" +msgstr "" + +#: .././formats.py:762 +msgid "Update using local youtube-dl path" +msgstr "" + +#: .././formats.py:764 +msgid "Update using pip" +msgstr "" + +#: .././formats.py:766 +msgid "Update using pip (omit --user option)" +msgstr "" + +#: .././formats.py:768 +msgid "Update using pip3" +msgstr "" + +#: .././formats.py:770 +msgid "Update using pip3 (omit --user option)" +msgstr "" + +#: .././formats.py:772 +msgid "Update using pip3 (recommended)" +msgstr "" + +#: .././formats.py:774 +msgid "Update using PyPI youtube-dl path" +msgstr "" + +#: .././formats.py:776 +msgid "Windows 32-bit update (recommended)" +msgstr "" + +#: .././formats.py:778 +msgid "Windows 64-bit update (recommended)" +msgstr "" + +#: .././formats.py:780 +msgid "youtube-dl updates are disabled" +msgstr "" + +#. Download operation stages +#: .././formats.py:784 +msgid "Queued" +msgstr "" + +#: .././formats.py:785 +msgid "Active" +msgstr "" + +#: .././formats.py:786 +msgid "Paused" +msgstr "" + +#. (not actually used) +#: .././formats.py:787 +msgid "Completed" +msgstr "" + +#. (not actually used) +#. Sub-stages of the 'Error' stage +#: .././formats.py:788 .././formats.py:799 +msgid "Error" +msgstr "" + +#. Sub-stages of the 'Active' stage +#: .././formats.py:790 +msgid "Pre-processing" +msgstr "" + +#: .././formats.py:791 +msgid "Downloading" +msgstr "" + +#: .././formats.py:792 +msgid "Post-processing" +msgstr "" + +#: .././formats.py:793 +msgid "Checking" +msgstr "" + +#. Sub-stages of the 'Completed' stage +#: .././formats.py:795 +msgid "Finished" +msgstr "" + +#: .././formats.py:796 +msgid "Warning" +msgstr "" + +#: .././formats.py:797 +msgid "Already downloaded" +msgstr "" + +#. (not actually used) +#: .././formats.py:800 +msgid "Stopped" +msgstr "" + +#: .././formats.py:801 +msgid "Filesize abort" +msgstr "" + +#: .././formats.py:811 +msgid "" +"TRANSLATOR'S NOTE: ID refers to a video's unique ID on the website, e.g. on " +"YouTube \"CS9OO0S5w2k\"" +msgstr "" + +#: .././formats.py:819 +msgid "Custom" +msgstr "" + +#: .././formats.py:820 +msgid "ID" +msgstr "" + +#: .././formats.py:821 +msgid "Title" +msgstr "" + +#: .././formats.py:822 +msgid "Quality" +msgstr "" + +#: .././formats.py:823 +msgid "Autonumber" +msgstr "" + +#: .././formats.py:835 +msgid "Any format" +msgstr "" + +#: .././info.py:186 +msgid "Starting info operation, testing youtube-dl with specified options" +msgstr "" + +#: .././info.py:195 +#, python-brace-format +msgid "Starting info operation, fetching list of video/audio formats for '{0}'" +msgstr "" + +#: .././info.py:202 +#, python-brace-format +msgid "Starting info operation, fetching list of subtitles for '{0}'" +msgstr "" + +#: .././info.py:343 +msgid "youtube-dl process did not start" +msgstr "" + +#: .././info.py:368 +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 +msgid "Child process did not start" +msgstr "" + +#: .././media.py:311 +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:314 .././media.py:1508 .././media.py:1524 +msgid "Source:" +msgstr "" + +#: .././media.py:322 +msgid "Location:" +msgstr "" + +#: .././media.py:333 +msgid "Download destination:" +msgstr "" + +#: .././media.py:1479 +msgid "" +"TRANSLATOR'S NOTE: WAITING = livestream not started, LIVE = livestream " +"started" +msgstr "" + +#: .././media.py:1484 +msgid "WAITING" +msgstr "" + +#: .././media.py:1486 +msgid "LIVE" +msgstr "" + +#: .././media.py:1496 .././refresh.py:272 .././refresh.py:540 +msgid "Channel:" +msgstr "" + +#: .././media.py:1498 .././refresh.py:274 .././refresh.py:542 +msgid "Playlist:" +msgstr "" + +#: .././media.py:1500 .././refresh.py:276 .././refresh.py:544 +msgid "Folder:" +msgstr "" + +#: .././media.py:1505 +msgid "TRANSLATOR'S NOTE 2: Source = video/channel/playlist URL" +msgstr "" + +#: .././media.py:1514 .././media.py:1531 +msgid "File:" +msgstr "" + +#: .././media.py:1965 +msgid "Today" +msgstr "" + +#: .././media.py:1967 +msgid "Yesterday" +msgstr "" + +#: .././refresh.py:149 +msgid "Starting refresh operation, analysing whole database" +msgstr "" + +#: .././refresh.py:158 +msgid "Starting refresh operation, analysing '{}'" +msgstr "" + +#: .././refresh.py:202 +msgid "Refresh operation finished" +msgstr "" + +#: .././refresh.py:207 +msgid "Number of video files analysed:" +msgstr "" + +#: .././refresh.py:213 +msgid "Video files already in the database:" +msgstr "" + +#: .././refresh.py:219 +msgid "New videos found and added to the database:" +msgstr "" + +#: .././refresh.py:385 .././tidy.py:489 +msgid "Checking:" +msgstr "" + +#: .././refresh.py:419 .././refresh.py:592 +msgid "Match:" +msgstr "" + +#: .././refresh.py:437 +msgid "Non-match:" +msgstr "" + +#: .././refresh.py:485 +msgid "New video:" +msgstr "" + +#: .././refresh.py:491 .././refresh.py:598 +msgid "Total videos:" +msgstr "" + +#: .././refresh.py:492 .././refresh.py:599 +msgid "matched:" +msgstr "" + +#: .././refresh.py:493 +msgid "new:" +msgstr "" + +#: .././refresh.py:574 +msgid "Missing:" +msgstr "" + +#: .././refresh.py:600 +msgid "missing:" +msgstr "" + +#: .././tidy.py:215 +msgid "Starting tidy operation, tidying up whole data directory" +msgstr "" + +#: .././tidy.py:224 +#, python-brace-format +msgid "Starting tidy operation, tidying up '{0}'" +msgstr "" + +#: .././tidy.py:230 .././tidy.py:242 .././tidy.py:252 .././tidy.py:262 +#: .././tidy.py:274 .././tidy.py:284 .././tidy.py:294 .././tidy.py:304 +#: .././tidy.py:314 .././tidy.py:324 +msgid "YES" +msgstr "" + +#: .././tidy.py:232 .././tidy.py:244 .././tidy.py:254 .././tidy.py:264 +#: .././tidy.py:276 .././tidy.py:286 .././tidy.py:296 .././tidy.py:306 +#: .././tidy.py:316 .././tidy.py:326 +msgid "NO" +msgstr "" + +#: .././tidy.py:236 +msgid "Check videos are not corrupted:" +msgstr "" + +#: .././tidy.py:248 +msgid "Delete corrupted videos:" +msgstr "" + +#: .././tidy.py:258 +msgid "Check videos do/don't exist:" +msgstr "" + +#: .././tidy.py:268 +msgid "Delete all video files:" +msgstr "" + +#: .././tidy.py:280 +msgid "Delete other video/audio files:" +msgstr "" + +#: .././tidy.py:290 +msgid "Delete all description files:" +msgstr "" + +#: .././tidy.py:300 +msgid "Delete all metadata (JSON) files:" +msgstr "" + +#: .././tidy.py:310 +msgid "Delete all annotation files:" +msgstr "" + +#: .././tidy.py:320 +msgid "Delete all thumbnail files:" +msgstr "" + +#: .././tidy.py:330 +msgid "Delete youtube-dl archive files:" +msgstr "" + +#: .././tidy.py:366 +msgid "Tidy operation finished" +msgstr "" + +#: .././tidy.py:373 +msgid "Corrupted videos found:" +msgstr "" + +#: .././tidy.py:379 +msgid "Corrupted videos deleted:" +msgstr "" + +#: .././tidy.py:387 +msgid "New video files detected:" +msgstr "" + +#: .././tidy.py:393 +msgid "Missing video files detected:" +msgstr "" + +#: .././tidy.py:401 +msgid "Non-corrupted video files deleted:" +msgstr "" + +#: .././tidy.py:407 +msgid "Other video/audio files deleted:" +msgstr "" + +#: .././tidy.py:415 +msgid "Description files deleted:" +msgstr "" + +#: .././tidy.py:423 +msgid "Metadata (JSON) files deleted:" +msgstr "" + +#: .././tidy.py:431 +msgid "Annotation files deleted:" +msgstr "" + +#: .././tidy.py:439 +msgid "Thumbnail files deleted:" +msgstr "" + +#: .././tidy.py:447 +msgid "youtube-dl archive files deleted:" +msgstr "" + +#: .././tidy.py:574 +msgid "Deleted (possibly) corrupted video file:" +msgstr "" + +#: .././tidy.py:589 .././tidy.py:995 +msgid "Video file might be corrupt:" +msgstr "" + +#: .././tidy.py:633 +msgid "Video file exists:" +msgstr "" + +#: .././tidy.py:651 +msgid "Video file doesn't exist:" +msgstr "" + +#: .././updates.py:215 +msgid "Starting update operation, installing FFmpeg" +msgstr "" + +#: .././updates.py:289 +msgid "FFmpeg installation did not start" +msgstr "" + +#: .././updates.py:306 .././updates.py:464 +msgid "Update operation finished" +msgstr "" + +#: .././updates.py:335 +msgid "Starting update operation, installing/updating youtube-dl" +msgstr "" + +#: .././updates.py:439 +msgid "youtube-dl update did not start" +msgstr "" diff --git a/tartube/refresh.py b/tartube/refresh.py index 9b66edaa..643ec9fc 100755 --- a/tartube/refresh.py +++ b/tartube/refresh.py @@ -35,6 +35,8 @@ import formats import media import utils +# Use same gettext translations +from mainapp import _ # Debugging flag (calls utils.debug_time at the start of every function) @@ -68,7 +70,7 @@ class RefreshManager(threading.Thread): def __init__(self, app_obj, init_obj=None): if DEBUG_FUNC_FLAG: - utils.debug_time('rop 71 __init__') + utils.debug_time('rop 73 __init__') super(RefreshManager, self).__init__() @@ -138,13 +140,13 @@ def run(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('rop 141 run') + 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( 1, - 'Starting refresh operation, analysing whole database', + _('Starting refresh operation, analysing whole database'), ) else: @@ -153,8 +155,9 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Starting refresh operation, analysing ' + media_type \ - + ' \'' + self.init_obj.name + '\'', + _('Starting refresh operation, analysing \'{}\'').format( + self.init_obj.name, + ), ) # Compile a list of channels, playlists and folders to refresh (each @@ -196,24 +199,24 @@ def run(self): # Show a confirmation in the Output Tab self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Refresh operation finished', + _('Refresh operation finished'), ) self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Number of video files analysed: ' \ + ' ' + _('Number of video files analysed:') + ' ' \ + str(self.video_total_count), ) self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Video files already in the database: ' \ + ' ' + _('Video files already in the database:') + ' ' \ + str(self.video_match_count), ) self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' New videos found and added to the database: ' \ + ' ' + _('New videos found and added to the database:') + ' ' \ + str(self.video_new_count), ) @@ -246,7 +249,7 @@ def refresh_from_default_destination(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('rop 249 refresh_from_default_destination') + utils.debug_time('rop 252 refresh_from_default_destination') # Update the main window's progress bar self.job_count += 1 @@ -266,11 +269,11 @@ def refresh_from_default_destination(self, media_data_obj): # Update our progress in the Output Tab if isinstance(media_data_obj, media.Channel): - string = 'Channel: ' + string = _('Channel:') + ' ' elif isinstance(media_data_obj, media.Playlist): - string = 'Playlist: ' + string = _('Playlist:') + ' ' else: - string = 'Folder: ' + string = _('Folder:') + ' ' self.app_obj.main_win_obj.output_tab_write_stdout( 1, @@ -379,7 +382,7 @@ def refresh_from_default_destination(self, media_data_obj): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Checking: ' + filename, + ' ' + _('Checking:') + ' ' + filename, ) if filename in check_dict: @@ -413,7 +416,7 @@ def refresh_from_default_destination(self, media_data_obj): if self.app_obj.refresh_output_videos_flag: self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Match: ' + child_obj.name, + ' ' + _('Match:') + ' ' + filename, ) elif filename not in slave_dict: @@ -431,7 +434,7 @@ def refresh_from_default_destination(self, media_data_obj): for failed_path in check_dict.keys(): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Non-match: ' + failed_path, + ' ' + _('Non-match:') + ' ' + filename, ) # Create a new media.Video object @@ -479,15 +482,15 @@ def refresh_from_default_destination(self, media_data_obj): if self.app_obj.refresh_output_videos_flag: self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' New video: ' + video_obj.name, + ' ' + _('New video:') + ' ' + filename, ) # Check complete, display totals self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Total videos: ' + str(local_total_count) \ - + ', matched: ' + str(local_match_count) \ - + ', new: ' + str(local_new_count), + ' ' + _('Total videos:') + ' ' + str(local_total_count) \ + + ', ' + _('matched:') + ' ' + str(local_match_count) \ + + ', ' + _('new:') + ' ' + str(local_new_count), ) @@ -513,7 +516,7 @@ def refresh_from_actual_destination(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('rop 516 refresh_from_actual_destination') + utils.debug_time('rop 519 refresh_from_actual_destination') # Update the main window's progress bar self.job_count += 1 @@ -534,11 +537,11 @@ def refresh_from_actual_destination(self, media_data_obj): # Update our progress in the Output Tab if isinstance(media_data_obj, media.Channel): - string = 'Channel: ' + string = _('Channel:') + ' ' elif isinstance(media_data_obj, media.Playlist): - string = 'Playlist: ' + string = _('Playlist:') + ' ' else: - string = 'Folder: ' + string = _('Folder:') + ' ' self.app_obj.main_win_obj.output_tab_write_stdout( 1, @@ -568,7 +571,7 @@ def refresh_from_actual_destination(self, media_data_obj): # Update our progress in the Output Tab (if required) self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Missing: ' + child_obj.name, + ' ' + _('Missing:') + ' ' + child_obj.name, ) elif not child_obj.dl_flag and this_file in init_list: @@ -586,15 +589,15 @@ def refresh_from_actual_destination(self, media_data_obj): if self.app_obj.refresh_output_videos_flag: self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Match: ' + child_obj.name, + ' ' + _('Match:') + ' ' + child_obj.name, ) # Check complete, display totals self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Total videos: ' + str(local_total_count) \ - + ', matched: ' + str(local_match_count) \ - + ', missing: ' + str(local_missing_count), + ' ' + _('Total videos:') + ' ' + str(local_total_count) \ + + ', ' + _('matched:') + ' ' + str(local_match_count) \ + + ', ' + _('missing:') + ' ' + str(local_missing_count), ) @@ -607,6 +610,6 @@ def stop_refresh_operation(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('rop 610 stop_refresh_operation') + utils.debug_time('rop 613 stop_refresh_operation') self.running_flag = False diff --git a/tartube/tartube b/tartube/tartube index d41d6cb1..21c0c7a5 100755 --- a/tartube/tartube +++ b/tartube/tartube @@ -42,9 +42,8 @@ import mainapp # 'Global' variables __packagename__ = 'tartube' -__prettyname__ = 'Tartube' -__version__ = '2.0.016' -__date__ = '10 Apr 2020' +__version__ = '2.1.0' +__date__ = '7 May 2020' __copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' __license__ = """ Copyright \xa9 2019-2020 A S Lewis. @@ -69,6 +68,7 @@ __description__ = 'A front-end GUI for youtube-dl,\n' \ + 'and written in Python 3 / Gtk 3' __website__ = 'http://tartube.sourceforge.io' __app_id__ = 'io.sourceforge.tartube' +__website_bugs__ = 'https://github.com/axcore/tartube' # There are three executables; this default one, and two others used in Debian/ # RPM packaging. The others are identical, except for the values of these # variables diff --git a/tartube/testing.py b/tartube/testing.py index 4c563624..4fb02459 100755 --- a/tartube/testing.py +++ b/tartube/testing.py @@ -134,3 +134,4 @@ def add_test_media(app_obj): folder2, ) app_obj.main_win_obj.video_index_add_row(folder4) + diff --git a/tartube/tidy.py b/tartube/tidy.py index b355728c..7e87b91d 100755 --- a/tartube/tidy.py +++ b/tartube/tidy.py @@ -41,6 +41,8 @@ import formats import media import utils +# Use same gettext translations +from mainapp import _ # Debugging flag (calls utils.debug_time at the start of every function) @@ -104,7 +106,7 @@ class TidyManager(threading.Thread): def __init__(self, app_obj, choices_dict): if DEBUG_FUNC_FLAG: - utils.debug_time('top 107 __init__') + utils.debug_time('top 109 __init__') super(TidyManager, self).__init__() @@ -204,13 +206,13 @@ def run(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 207 run') + utils.debug_time('top 209 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( 1, - 'Starting tidy operation, tidying up whole data directory', + _('Starting tidy operation, tidying up whole data directory'), ) else: @@ -219,112 +221,113 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Starting tidy operation, tidying up ' + media_type \ - + ' \'' + self.init_obj.name + '\'', + _('Starting tidy operation, tidying up \'{0}\'').format( + self.init_obj.name, + ) ) if self.corrupt_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Check videos are not corrupted: ' + text, + ' ' + _('Check videos are not corrupted:') + ' ' + text, ) if self.corrupt_flag: if self.del_corrupt_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Delete corrupted videos: ' + text, + ' ' + _('Delete corrupted videos:') + ' ' + text, ) if self.exist_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Check videos do/don\'t exist: ' + text, + ' ' + _('Check videos do/don\'t exist:') + ' ' + text, ) if self.del_video_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Delete all video files: ' + text, + ' ' + _('Delete all video files:') + ' ' + text, ) if self.del_video_flag: if self.del_others_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Delete other video/audio files: ' + text, + ' ' + _('Delete other video/audio files:') + ' ' + text, ) if self.del_descrip_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Delete all description files: ' + text, + ' ' + _('Delete all description files:') + ' ' + text, ) if self.del_json_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Delete all metadata (JSON) files: ' + text, + ' ' + _('Delete all metadata (JSON) files:') + ' ' + text, ) if self.del_xml_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Delete all annotation files: ' + text, + ' ' + _('Delete all annotation files:') + ' ' + text, ) if self.del_thumb_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Delete all thumbnail files: ' + text, + ' ' + _('Delete all thumbnail files:') + ' ' + text, ) if self.del_archive_flag: - text = 'YES' + text = _('YES') else: - text = 'NO' + text = _('NO') self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Delete youtube-dl archive files: ' + text, + ' ' + _('Delete youtube-dl archive files:') + ' ' + text, ) # Compile a list of channels, playlists and folders to tidy up (each @@ -360,20 +363,20 @@ def run(self): # Show a confirmation in the Output Tab self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Tidy operation finished', + _('Tidy operation finished'), ) if self.corrupt_flag: self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Corrupted videos found: ' \ + ' ' + _('Corrupted videos found:') + ' ' \ + str(self.video_corrupt_count), ) self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Corrupted videos deleted: ' \ + ' ' + _('Corrupted videos deleted:') + ' ' \ + str(self.video_corrupt_deleted_count), ) @@ -381,13 +384,13 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' New video files detected: ' \ + ' ' + _('New video files detected:') + ' ' \ + str(self.video_exist_count), ) self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Missing video files detected: ' \ + ' ' + _('Missing video files detected:') + ' ' \ + str(self.video_no_exist_count), ) @@ -395,13 +398,13 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Non-corrupted video files deleted: ' \ + ' ' + _('Non-corrupted video files deleted:') + ' ' \ + str(self.video_deleted_count), ) self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Other video/audio files deleted: ' \ + ' ' + _('Other video/audio files deleted:') + ' ' \ + str(self.other_deleted_count), ) @@ -409,7 +412,7 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Description files deleted: ' \ + ' ' + _('Description files deleted:') + ' ' \ + str(self.descrip_deleted_count), ) @@ -417,7 +420,7 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Metadata (JSON) files deleted: ' \ + ' ' + _('Metadata (JSON) files deleted:') + ' ' \ + str(self.json_deleted_count), ) @@ -425,7 +428,7 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Annotation files deleted: ' \ + ' ' + _('Annotation files deleted:') + ' ' \ + str(self.xml_deleted_count), ) @@ -433,7 +436,7 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Thumbnail files deleted: ' \ + ' ' + _('Thumbnail files deleted:') + ' ' \ + str(self.thumb_deleted_count), ) @@ -441,7 +444,7 @@ def run(self): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' youtube-dl archive files deleted: ' \ + ' ' + _('youtube-dl archive files deleted:') + ' ' \ + str(self.archive_deleted_count), ) @@ -467,7 +470,7 @@ def tidy_directory(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 470 tidy_directory') + utils.debug_time('top 473 tidy_directory') # Update the main window's progress bar self.job_count += 1 @@ -483,7 +486,7 @@ def tidy_directory(self, media_data_obj): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Checking ' + media_type + ' \'' + media_data_obj.name + '\'', + _('Checking:') + ' \'' + media_data_obj.name + '\'', ) if self.corrupt_flag: @@ -526,7 +529,7 @@ def check_video_corrupt(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 529 check_video_corrupt') + utils.debug_time('top 532 check_video_corrupt') for video_obj in media_data_obj.compile_all_videos( [] ): @@ -567,8 +570,9 @@ def check_video_corrupt(self, media_data_obj): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Deleted (possibly) corrupted video' - + ' file: \'' + video_obj.name + '\'', + ' ' + _( + 'Deleted (possibly) corrupted video file:', + ) + ' \'' + video_obj.name + '\'', ) self.app_obj.mark_video_downloaded( @@ -581,8 +585,9 @@ def check_video_corrupt(self, media_data_obj): # Don't delete it self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Video file might be corrupt: \'' \ - + video_obj.name + '\'', + ' ' + _( + 'Video file might be corrupt:', + ) + ' \'' + video_obj.name + '\'', ) @@ -602,7 +607,7 @@ def check_videos_exist(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 605 check_videos_exist') + utils.debug_time('top 610 check_videos_exist') for video_obj in media_data_obj.compile_all_videos( [] ): @@ -624,7 +629,9 @@ def check_videos_exist(self, media_data_obj): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Video file exists: \'' + video_obj.name + '\'', + ' ' + _( + 'Video file exists:', + ) + ' \'' + video_obj.name + '\'', ) elif video_obj.dl_flag \ @@ -640,8 +647,9 @@ def check_videos_exist(self, media_data_obj): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Video file doesn\'t exist: \'' + video_obj.name \ - + '\'', + ' ' + _( + 'Video file doesn\'t exist:', + ) + ' \'' + video_obj.name + '\'', ) @@ -660,7 +668,7 @@ def delete_video(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 663 delete_video') + utils.debug_time('top 671 delete_video') ext_list = formats.VIDEO_FORMAT_LIST.copy() ext_list.extend(formats.AUDIO_FORMAT_LIST) @@ -759,7 +767,7 @@ def delete_descrip(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 762 delete_descrip') + utils.debug_time('top 770 delete_descrip') for video_obj in media_data_obj.compile_all_videos( [] ): @@ -804,7 +812,7 @@ def delete_json(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 807 delete_json') + utils.debug_time('top 815 delete_json') for video_obj in media_data_obj.compile_all_videos( [] ): @@ -849,7 +857,7 @@ def delete_xml(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 852 delete_xml') + utils.debug_time('top 860 delete_xml') for video_obj in media_data_obj.compile_all_videos( [] ): @@ -894,7 +902,7 @@ def delete_thumb(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 897 delete_thumb') + utils.debug_time('top 905 delete_thumb') for video_obj in media_data_obj.compile_all_videos( [] ): @@ -939,7 +947,7 @@ def delete_archive(self, media_data_obj): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 942 delete_archive') + utils.debug_time('top 950 delete_archive') archive_path = os.path.abspath( os.path.join( @@ -974,7 +982,7 @@ def call_moviepy(self, video_obj, video_path): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 977 call_moviepy') + utils.debug_time('top 985 call_moviepy') try: clip = moviepy.editor.VideoFileClip(video_path) @@ -984,7 +992,8 @@ def call_moviepy(self, video_obj, video_path): self.app_obj.main_win_obj.output_tab_write_stdout( 1, - ' Video file might be corrupt: \'' + video_obj.name + '\'', + ' ' + _('Video file might be corrupt:') + ' \'' \ + + video_obj.name + '\'', ) @@ -1018,7 +1027,7 @@ def check_video_in_actual_dir(self, container_obj, video_obj, file_path): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 1021 check_video_in_actual_dir') + utils.debug_time('top 1030 check_video_in_actual_dir') if container_obj.dbid == container_obj.master_dbid: @@ -1054,6 +1063,6 @@ def stop_tidy_operation(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('top 1057 stop_tidy_operation') + utils.debug_time('top 1066 stop_tidy_operation') self.running_flag = False diff --git a/tartube/updates.py b/tartube/updates.py index 4a9d1d85..e9d1af93 100755 --- a/tartube/updates.py +++ b/tartube/updates.py @@ -40,6 +40,8 @@ # Import our modules import downloads import utils +# Use same gettext translations +from mainapp import _ # Debugging flag (calls utils.debug_time at the start of every function) @@ -78,7 +80,7 @@ class UpdateManager(threading.Thread): def __init__(self, app_obj, update_type): if DEBUG_FUNC_FLAG: - utils.debug_time('uop 81 __init__') + utils.debug_time('uop 83 __init__') super(UpdateManager, self).__init__() @@ -138,7 +140,7 @@ def run(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('uop 141 run') + utils.debug_time('uop 143 run') if self.update_type == 'ffmpeg': self.install_ffmpeg() @@ -162,7 +164,7 @@ def create_child_process(self, cmd_list): """ if DEBUG_FUNC_FLAG: - utils.debug_time('uop 165 create_child_process') + utils.debug_time('uop 167 create_child_process') info = preexec = None @@ -188,7 +190,7 @@ def create_child_process(self, cmd_list): except (ValueError, OSError) as error: # (The code in self.run() will spot that the child process did not # start) - self.stderr_list.append('Child process did not start') + self.stderr_list.append(_('Child process did not start')) def install_ffmpeg(self): @@ -205,12 +207,12 @@ def install_ffmpeg(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('uop 208 install_ffmpeg') + 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, - 'Starting update operation, installing FFmpeg', + _('Starting update operation, installing FFmpeg'), ) # Create a new child process to install either the 64-bit or 32-bit @@ -284,11 +286,11 @@ def install_ffmpeg(self): # (Generate our own error messages for debugging purposes, in certain # situations) if self.child_process is None: - self.stderr_list.append('FFmpeg installation did not start') + self.stderr_list.append(_('FFmpeg installation did not start')) elif self.child_process.returncode > 0: self.stderr_list.append( - 'Child process exited with non-zero code: {}'.format( + _('Child process exited with non-zero code: {}').format( self.child_process.returncode, ) ) @@ -301,7 +303,7 @@ def install_ffmpeg(self): # Show a confirmation in the the Output Tab self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Update operation finished', + _('Update operation finished'), ) # Let the timer run for a few more seconds to prevent Gtk errors (for @@ -325,12 +327,12 @@ def install_ytdl(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('uop 328 install_ytdl') + utils.debug_time('uop 330 install_ytdl') # Show information about the update operation in the Output Tab self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Starting update operation, installing/updating youtube-dl', + _('Starting update operation, installing/updating youtube-dl'), ) # Prepare the system command @@ -434,7 +436,7 @@ def install_ytdl(self): # situations) if self.child_process is None: - msg = 'youtube-dl update did not start' + msg = _('youtube-dl update did not start') self.stderr_list.append(msg) self.app_obj.main_win_obj.output_tab_write_stdout( 1, @@ -443,7 +445,7 @@ def install_ytdl(self): elif self.child_process.returncode > 0: - msg = 'Child process exited with non-zero code: {}'.format( + msg = _('Child process exited with non-zero code: {}').format( self.child_process.returncode, ) self.app_obj.main_win_obj.output_tab_write_stdout( @@ -459,7 +461,7 @@ def install_ytdl(self): # Show a confirmation in the the Output Tab self.app_obj.main_win_obj.output_tab_write_stdout( 1, - 'Update operation finished', + _('Update operation finished'), ) # Let the timer run for a few more seconds to prevent Gtk errors (for @@ -484,7 +486,7 @@ def intercept_version_from_stdout(self, stdout): """ if DEBUG_FUNC_FLAG: - utils.debug_time('uop 487 intercept_version_from_stdout') + utils.debug_time('uop 489 intercept_version_from_stdout') substring = re.search( 'Requirement already up\-to\-date.*\(([\d\.]+)\)\s*$', @@ -521,7 +523,7 @@ def is_child_process_alive(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('uop 524 is_child_process_alive') + utils.debug_time('uop 526 is_child_process_alive') if self.child_process is None: return False @@ -540,7 +542,7 @@ def stop_update_operation(self): """ if DEBUG_FUNC_FLAG: - utils.debug_time('uop 543 stop_update_operation') + utils.debug_time('uop 545 stop_update_operation') if self.is_child_process_alive(): diff --git a/tartube/utils.py b/tartube/utils.py index 6d62832d..2c4d61d9 100755 --- a/tartube/utils.py +++ b/tartube/utils.py @@ -115,7 +115,7 @@ def add_links_to_entry_from_clipboard(app_obj, entry, duplicate_text=None, return None -def add_links_to_textview_from_clipboard(app_obj, textview, mark_start=None, +def add_links_to_textview_from_clipboard(app_obj, textbuffer, mark_start=None, mark_end=None, drag_drop_text=None): """Called by mainwin.AddVideoDialogue.__init__(), @@ -131,8 +131,8 @@ def add_links_to_textview_from_clipboard(app_obj, textview, mark_start=None, app_obj (mainapp.TartubeApp): The main application - textview (Gtk.TextBuffer): The textview to which valis URLs should be - added (unless they are duplicates) + textbuffer (Gtk.TextBuffer): The textbuffer to which valis URLs should + be added (unless they are duplicates) mark_start, mark_end (Gtk.TextMark): The marks at the start/end of the buffer (using marks rather than iters prevents Gtk errors) @@ -173,18 +173,18 @@ def add_links_to_textview_from_clipboard(app_obj, textview, mark_start=None, if mark_start is None or mark_end is None: # No Gtk.TextMarks supplied, we're forced to use iters - buffer_text = textview.get_text( - textview.get_start_iter(), - textview.get_end_iter(), + buffer_text = textbuffer.get_text( + textbuffer.get_start_iter(), + textbuffer.get_end_iter(), # Don't include hidden characters False, ) else: - buffer_text = textview.get_text( - textview.get_iter_at_mark(mark_start), - textview.get_iter_at_mark(mark_end), + buffer_text = textbuffer.get_text( + textbuffer.get_iter_at_mark(mark_start), + textbuffer.get_iter_at_mark(mark_end), False, ) @@ -202,8 +202,8 @@ def add_links_to_textview_from_clipboard(app_obj, textview, mark_start=None, if not re.search('\n\s*$', buffer_text) and buffer_text != '': mod_list[0] = '\n' + mod_list[0] - textview.insert( - textview.get_end_iter(), + textbuffer.insert( + textbuffer.get_end_iter(), str.join('\n', mod_list) + '\n', ) @@ -385,6 +385,30 @@ def convert_seconds_to_string(seconds, short_flag=False): return str(datetime.timedelta(seconds=seconds)) +def convert_youtube_id_to_rss(media_type, youtube_id): + + """Can be called by anything; usually called by + media.GenericRemoteContainer.set_rss(). + + Convert the channel/playlist ID provided by YouTube into the full URL for + the channel/playlist RSS feed. + + Args: + + media_type (str): 'channel' or 'playlist' + + youtube_id (str): The YouTube channel or playlist ID + + Return values: + + The full URL for the RSS feed + + """ + + return 'https://www.youtube.com/feeds/videos.xml?' + media_type \ + + '_id=' + youtube_id + + def convert_youtube_to_hooktube(url): """Can be called by anything. @@ -581,6 +605,11 @@ def find_available_name(app_obj, old_name, min_value=2, max_value=9999): slightly modifies the name, converting 'my_name' into 'my_name_N', where N is the smallest positive integer for which the name is available. + If the specified old_name is already in that format (for example, + 'Channel_4'), then the old number is stripped away, and this function + starts looking from the first integer after that (for example, + 'Channel_5'). + To preclude any possibility of infinite loops, the function will give up after max_value attempts. @@ -603,6 +632,19 @@ def find_available_name(app_obj, old_name, min_value=2, max_value=9999): """ + # If old_name is already in the format 'my_name_N', where N is an integer + # in the range min_value < N < max_value, then strip it away + if re.search(r'\_\d+$', old_name): + + number = int(re.sub(r'^.*\_(\d+)$', r'\1', old_name)) + mod_name = re.sub(r'^(.*)\_\d+$', r'\1', old_name) + + if number >= 2 and number < max_value: + + old_name = mod_name + min_value = number + 1 + + # Find an available name if max_value != -1: for n in range (min_value, max_value): @@ -702,7 +744,7 @@ def format_bytes(num_bytes): def generate_system_cmd(app_obj, media_data_obj, options_list, -dl_sim_flag=False, divert_mode=None): +dl_sim_flag=False, dl_classic_flag=False, divert_mode=None): """Called by downloads.VideoDownloader.do_download() and mainwin.SystemCmdDialogue.update_textbuffer(). @@ -725,6 +767,9 @@ def generate_system_cmd(app_obj, media_data_obj, options_list, dl_sim_flag (bool): True if a simulated download is to take place, False if a real download is to take place + dl_classic_flag (bool): True if the download operation was launched + from the Classic Mode Tab, False otherwise + divert_mode (str): If not None, should be one of the values of mainapp.TartubeApp.custom_dl_divert_mode: 'default', 'hooktube' or 'invidious'. If one of the latter two, a media.Video object whose @@ -748,27 +793,39 @@ def generate_system_cmd(app_obj, media_data_obj, options_list, # user deletes the videos, youtube-dl won't try to download them again # (Videos downloaded into a system folder should never create an archive # file) - if app_obj.allow_ytdl_archive_flag \ - and ( - not isinstance(media_data_obj, media.Folder) - or not media_data_obj.fixed_flag - ) and ( - not isinstance(media_data_obj, media.Video) - or not isinstance(media_data_obj.parent_obj, media.Folder) - or not media_data_obj.parent_obj.fixed_flag - ): - # (Create the archive file in the media data object's default - # sub-directory, not the alternative download destination, as this - # helps youtube-dl to work the way we want it to work) - if isinstance(media_data_obj, media.Video): - dl_path = media_data_obj.parent_obj.get_default_dir(app_obj) - else: - dl_path = media_data_obj.get_default_dir(app_obj) + if app_obj.allow_ytdl_archive_flag: + + if not dl_classic_flag \ + and ( + not isinstance(media_data_obj, media.Folder) + or not media_data_obj.fixed_flag + ) and ( + not isinstance(media_data_obj, media.Video) + or not isinstance(media_data_obj.parent_obj, media.Folder) + or not media_data_obj.parent_obj.fixed_flag + ): + # (Create the archive file in the media data object's default + # sub-directory, not the alternative download destination, as + # this helps youtube-dl to work the way we want it to work) + if isinstance(media_data_obj, media.Video): + dl_path = media_data_obj.parent_obj.get_default_dir(app_obj) + else: + dl_path = media_data_obj.get_default_dir(app_obj) - options_list.append('--download-archive') - options_list.append( - os.path.abspath(os.path.join(dl_path, 'ytdl-archive.txt')), - ) + options_list.append('--download-archive') + options_list.append( + os.path.abspath(os.path.join(dl_path, 'ytdl-archive.txt')), + ) + + elif dl_classic_flag: + + # Create the archive file in destination directory + dl_path = media_data_obj.dummy_dir + + options_list.append('--download-archive') + options_list.append( + os.path.abspath(os.path.join(dl_path, 'ytdl-archive.txt')), + ) # Show verbose output (youtube-dl debugging mode), if required if app_obj.ytdl_write_verbose_flag: diff --git a/nsis/tartube_32bit.bat b/tartube_32bit.bat similarity index 100% rename from nsis/tartube_32bit.bat rename to tartube_32bit.bat diff --git a/nsis/tartube_64bit.bat b/tartube_64bit.bat similarity index 100% rename from nsis/tartube_64bit.bat rename to tartube_64bit.bat diff --git a/tartube_mswin.sh b/tartube_mswin.sh index 1723d9f1..d30a6375 100755 --- a/tartube_mswin.sh +++ b/tartube_mswin.sh @@ -1,5 +1,5 @@ #!/bin/bash # Shell script to start Tartube on MS Windows, using the MSYS2 environment # provided by the Tartube installer -cd /home/user/tartube/tartube -python3 tartube +cd /home/user/tartube +python3 tartube/tartube