diff --git a/AUTHORS b/AUTHORS index 95c8e5e..1784d9b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,10 +5,5 @@ Authors ordered by first contribution: (none yet) Image credits: -Vectorgraphit -FatCow Web Hosting https://www.fatcow.com/> -Mr. Hopnguyen - -Other credits: -Tartube is partially based on youtube-dl-gui +Carlo Rodriguez diff --git a/CHANGES b/CHANGES index eb394b0..44603ea 100644 --- a/CHANGES +++ b/CHANGES @@ -1,965 +1,9 @@ -v2.0.0 (29 Feb 2020) +v1.002 (28 Mar 2020) ------------------------------------------------------------------------------- -MAJOR NEW FEATURES -- Tartube can now be installed from PyPI, or by using the new DEB/RPM packages - (Linux/BSD only; installation from PyPI does not work on MS Windows) -- DEB/RPM packages marked 'STRICT' are also available for uploads to - repositories with lots of rules, such as the official Debian repository. - In 'STRICT' packages, updating youtube-dl from within Tartube is disabled. - The 'STRICT' packages are compiled using new environment variables, - TARTUBE_PKG and TARTUBE_PKG_STRICT (replacing the old TARTUBE_DEBIAN) - environment variable. See the comments in setup.py for more details -- During a download operation, in the Progress Tab, you can now right-click a - video and select 'Stop after these videos'. This allows all of the current - video downloads to finish, before halting the download operation -- The download options window (in the Formats tab) did not allow users to - select an audio format before selecting a video format. The reason for this - restriction was that youtube-dl did not download the right formats, if an - audio format was selected first. Unfortuantely, it prevented users from - downloading a separate audio file, when this was available (e.g. an .m4a - file from YouTube). The restriction has now been removed; instead, Tartube - will automatically reorder the specified video/audio formats, so that video - formats are passed to youtube-dl first +- Fix for PyPI installation problems -MAJOR FIXES -- If an upload operation is automatically performed before a download - operation, and if the user tried to download a single video/channel/ - playlist/folder, everything was downloaded instead of the single video/ - channel/playlist/folder. Fixed -- Fixed an error in the 'Show system command' dialogue window, that prevented - it from opening at all -- Fixed parsing of download options inside double quotes "..." - -MINOR NEW FEATURES -- Added a 'Cancel' button to some dialogue windows that didn't already have one -- Added a copy of the XDG module to the Tartube code, so it is no longer - necessary to install it before running/installing Tartube (Linux/BSD only) - -MINOR FIXES -- Fixed a system error during a forced youtube-dl update (MS Windows only) -- Fixed wrong location for config file backups (MS Windows only) -- Fixed wrong location for Tartube temporary/test folders (all systems) -- Fixed missing (or duplicate) dialogue windows after failing to load the - config file and/or database file, in some rare situations -- The config file could not be created if its parent directory did not exist; - fixed -- Fixed loading of the wrong database file, in some rare situations -- Removed the old 'hello world' code intended for testing on MS Windows; it's - no longer required -- If Tartube can't find its icon files, a simple error message is now generated - rather than a long traceback - -v1.5.0 (22 Feb 2020) +v1.0 (27 Mar 2020) ------------------------------------------------------------------------------- -This is the first release candidate for v2.0.0. - -MAJOR NEW FEATURES -- You can now run multiple instances of Tartube on your system at the same - time. Multiple instances cannot load the same Tartube database; they must - each load their own database. Tartube will now remember the databases it - has loaded. If there are three databases (perhaps one on your main hard - disk and two on an external drive), you can start Tartube three times, and - they will each load a different database. This behaviour can be configured, - if necessary. Click 'Edit > System preferences... > Filesystem > Database' -- HookTube acts as a redirection service for YouTube. Because of lawyers and - their evil machinations, HookTube's functionality is not as extensive as it - once was. Added the Invidious website (https://invidio.us/) as an - alternative -- Added custom downloads. To start a custom download, click 'Operations > - Custom download all', or right-click a video/channel/playlist/folder. A - custom download is just like a normal download, until you customise it. To - do that, click 'Edit > System preferences > Operations > Custom'. Custom - downloads can be used to divert YouTube requests to HookTube or Invidious, - and to insert a delay between video downloads when the website is - complaining about robots -- Added a new toolbar at the bottom of the main window, below the list of - videos. The toolbar is hidden, by default. To reveal it, click the 'Show - filter options' button in the bottom right-hand corner. Buttons in the new - toolbar can be used to sort the videos alphabetically, rather than by date, - and to search for videos whose name matches a string (or regex). The button - to search for videos by date has been moved into this toolbar -- If you find a video that can't be downloaded, and you're not sure why, you - can now perform a test download. First, click 'Operations > Test - youtube-dl...' (or right-click a video in the main window's list). Copy the - video's URL into the dialogue window, and then click the OK button. Click - the Output Tab to see the results. If the test successfully downloads the - video, then the problem was with Tartube. If the test fails to download the - video, then the problem is with the underlying youtube-dl software (or with - the video website) -- During a test, it's possible to omit the video URL, while specifying some - youtube-dl download options. For example, you could fetch the youtube-dl - version number with the option --version -- Added a new operation for tidying up files in Tartube's data directory - (folder). To start the operation, click 'Operations > Tidy up files...'. - You could also right-click a channel and select 'Channel actions > Tidy up - channel', and so on. A dialogue window appears, in which you can specify - which files should be tidied up. Choose carefully, because any files - deleted as a result of this operation cannot be recovered -- The main window's switch button (in the toolbar near the top of the window) - now has six settings, rather than four. Click the button repeatedly to - cycle through them -- Interesting and important videos can now be bookmarked (e.g., by right- - clicking a video and selecting 'Mark video > Video is bookmarked'). - Bookmarked videos are visible in the new 'Bookmarks' folder. Bookmarking is - an alternative to favourites; bookmarks usually apply to a single video, - whereas favourites usually apply to a whole channel, playlist or folder -- Also added a new 'Waiting Videos' folder. This acts as your own private - playlist - a list of videos that are waiting to be watched. To make a video - visible in this folder, right-click it and select 'Mark video > Video is in - waiting list'. When you watch the video, it will automatically disappear - from the 'Waiting Videos' folder (this doesn't happen to bookmarked videos) -- The previous version was unable to delete a channel, playlist or folder (see - below). That error caused a partially-deleted channel/playlist/folder to - appear in the Videos Tab, on the left-hand side. In case similar errors - occur in the future, a feature has been added to look for errors and - inconsistencies in the Tartube database and automatically fix them. Click - 'Edit > System preferences... > Filesystem > DB Errors > Check' to use it -- Tartube can now fetch a list of available video formats for a video. Right- - click the video and select 'Fetch > Available formats'. Click the Output - Tab to see the results -- Tartube can also fetch a list of available subtitles for a video. Right-click - the video and select 'Fecth > Available subtitles'. Click the Output Tab to - see the results -- Tartube can now remember the size of its main window, and use the same size - when it restarts. This feature is disabled by default. To enable it, click - 'Edit > System preferences... > Windows > Main window > Remember the size - of the main window when shutting down' - -MAJOR FIXES -- In the previous version, Tartube was unable to delete a channel, playlist or - folder. Fixed -- Some procedures took an extremely long time. For example, after right- - clicking a channel and selecting 'Channel contents > Mark videos as new', - the procedure could take several minutes if the channel had hundreds of - videos, or several hours if it contained thousands of videos. The faulty - code has been fixed, and the procedure now takes just a few seconds, even - for many thousands of videos -- Videos in a Tartube folder (for example the 'Unsorted Videos' folder) were - added to the download list in a 'Check all' operation, even when they had - been checked before. This no longer happens, by default. To restore the - original behaviour, click 'Edit > System preferences... > Operations > - Downloads > For simulate downloads, don't check a video in a folder more - than once' to deselect it -- Fixed the button for finding videos by date, which was not working at all in - the previous version -- Fixed an occasional 'signal is not defined' error when the user stops an - operation (for example, a download operation) -- Various inconsistencies in the way alternative download destinations are - handled have all been fixed. Download operations sometimes freezed - indefinitely, because Tartube doesn't download two channels/playlists/ - folders with the same download destination at the same time. The code has - been updated to prevent the freeze from ever happening again -- Fixed some more crashes caused by Gtk during a download operation - -MINOR NEW FEATURES -- Videos downloaded into a temporary folder are deleted when Tartube restarts. - After shutting down Tartube, users often like to copy these videos - somewhere else on their hard drive. You can now ask Tartube to open the - temporary directories (folders), before shutting down, which will remind - you to do something with the videos. To enable this behaviour, click - 'Edit > System preferences... > Filesystem > Temporary folders' -- In the main window's list of videos, the date is now displayed as 'today' and - 'yesterday' when possible. This behaviour can be disabled in 'Edit > System - preferences... > Windows > Main window' -- During refresh operations, a progress bar is now visible in the bottom-left - corner of the window (just like the one visible during a download - operation) -- When setting an alternative download destination for a channel/playlist/ - folder (for example, by right-clicking a channel and selecting 'Channel - actions > Set download destination...'), the dialogue window has been - updated to show the previously selected alternative at the top of the list. - This should save a lot of time when setting the alternative download - destination for many channels/playlists/folders -- The alternative download destination, if any, is now visible in the tooltips - for the channel/playlist/folder -- Improved the appearance of the dialogue windows seen when Tartube runs for - the first time -- Tweaked the appearance of the list of channels/playlists/folders in the - main window, so that for items with long names, more text is visible - -MINOR FIXES -- Fixed a 'No such file or directory' error seen during a download operation, - if an external hard drive suddenly become disconnected (for example, if - the cable falls out) -- Fixed rare problems in loading Tartube's config file -- After a download operation, the list in the top half of the progress tab - often had one or two items in it, even when 'Hide active rows after they - are finished' was selected. Fixed -- The length of lines of text, and spacing between lines, in various dialogue - windows has been made uniform -- Improved the appearance of the main window by adding frames around everything -- Renamed some misnamed icon files. The old icon files were being used in the - MS Windows installer, so fixed that too -- If the user performed two successive refresh operations, the second one - halted after a couple of seconds. Fixed -- When videos are deleted from Tartube's database, any post-processing - artefacts are now deleted with them -- Fixed a few incorrect regex-matching actions -- The user can specify that the main 'Download all' button should be - desensitised, but the setting was not applied correctly after Tartube - restarted. Fixed -- Removed a duplicate menu option in the Video Index popup menu -- Tartube channels, playlists and folders keep counts of the number of videos - inside them, including the number of favourite videos, downloaded videos, - and so on. The code was not working correctly, so the counts were not - always accurate. This version updates the code and recalculates all of the - counts -- Fixed folder icons with an incorrect colour in various edit windows -- Fixed markup errors for videos whose URL contained an ampersand character -- Fixed the Gtk warning when closing the 'Add new video(s)' dialogue window -- Updated the installer scripts for MS Windows, so they don't try to update the - Windows registry (the code has never worked) -- You can no longer set videos as favourite, or new (etc), in an empty channel, - playlist or folder -- In the video list, labels can be right-clicked to copy a video's location - (for example, so it can be copy-pasted somewhere else). This did not work - the same way for every clickable label, and in some cases did not work at - all. Fixed -- Tooltips for videos contained & rather than a simple ampersand character. - Fixed -- Tartube debug messages for the mainapp.py file (which can only be enabled - by editing the file) now have a second debug flag, so the timer functions - can be filtered out -- Checked all keyboard shortcuts to remove duplicates - -v1.4.0 (2 Feb 2020) -------------------------------------------------------------------------------- - -MAJOR NEW FEATURES -- The structures of files and directories (folders) in Tartube's data - directory (into which all videos are downloaded) has been changed in - response to Git #28. Tartube will be able to recognise both structures - forever, so there is no need to move anything around on your computer. (If - you actually want to move things around, see the README file) -- Creating a channel/playlist/folder starting with a full stop (period) is no - longer allowed; some channels/playlists/folders might be automatically - renamed when you open Tartube -- The edit and preference windows have been reorganised, adding a second - layer of tabs in many windows. This should hopefully make things a little - easier to find -- In the download options window, you can now specify multiple languages for - your subtitles, instead of just one (Git #47) -- Added some more filename formats (in Edit > General download options... - > Files > File names). When downloading a partial playlist (for example, - starting at the 5th video), youtube-dl cannot create files with the correct - number (naming the first file downloaded #1, instead of #5). Tartube can - now handle this correctly. In the drop-down box, use one of the formats - containing 'Autonumber' (Git #47) -- You can now limit the length of a download operation. This is particularly - useful on small devices, or when leaving Tartube to run overnight. Click - Edit > System preferences... > Scheduling > Stop, and choose one or more of - the new options (Git #47) -- When adding new videos, channels or playlists, you can now turn on clipboard - monitoring. Simply select a URL (for example, in your web browser), press - CTRL+C to copy it to your system's clipboard, and then Tartube will - automatically paste it into the dialogue window (Git #52) -- The MS Windows installer now includes a copy of AtomicParsley, so there is no - need to install it yourself. This does not affect Linux/BSD users, who can - continue installing AtomicParsley by the usual methods -- The list in the top half of the Progress Tab is often full, and it's - sometimes difficult to see what is being downloaded right at this moment. - You can now hide finished rows, if you want to, so that active rows appear - at the top of the list - -MAJOR FIXES -- The Gtk graphics libraries have historically been full of bugs, which made - applications using Gtk unstable. Most of these bugs are fixed, but the - fixes can take years before they propogate into operating systems. If Gtk - v3.22 (or lower) is installed on your system, Tartube automatically - disables some minor cosmetic features to prevent crashes. If you are using - Gtk v3.24 or later, and are still experiencing unexplainable crashes, you - can now disable the cosmetic features regardless of Gtk version. Click - Edit > System preferences... > General > Modules, and select 'Assume that - Gtk is broken...' -- On Linux/BSD, attempts to update youtube-dl from the Tartube menu so,etimes - produced a 'permission denied' error. There are now new settings available - in 'Edit > System prefences... > youtube-dl > Shell command for update - operations'. If you installed youtube-dl using pip/pip3, the 'recommended' - options should now work, if they didn't work before. Some pip3 warning - messages, which caused Tartube to think the update had failed, are now - filtered out -- A user complained that his Tartube database file had been corrupted. We are - still not sure what the cause was, but the code has been changed to make - that kind of corruption impossible -- Fixed some occasional crashes when, during a download operation, Tartube - tried to sort the videos in the selected channel/playlist/folder -- When switching databases, if Tartube couldn't load the new database, it tried - again after being restarted, rather than trying to load the previous - (readable) database. This has now been fixed -- Some youtube-dl download options could be applied to playlists, but not - channels, even though youtube-dl allows them to be applied to be both. - Fixed, and updated some labels to make it clearer what the options are for - (Git #47) -- In all edit windows, the 'Apply' button at the bottom of the window did not - work. Fixed - -MINOR NEW FEATURES -- Tartube icons have been updated, in some cases making them easier to identify -- In the Progress Tab, added tooltips to assist with identifying undownloaded - videos (Git #51) -- More types of YouTube error message can now be filtered out -- We have also added a customisable list of strings (or regular expressions); - if set, any matching error/warning messages (on any website) are filtered - out -- In the Video Index popup menus, 'rename default location' has been changed - to a much more comprehensible 'rename channel', etc -- You can now open a video in its system directory (folder) by right-clicking - it, and selecting 'Show location' -- You can now switch databases from the main menu. Click File > Change database - (which opens the preference window at the correct page; hopefully this is - quicker than trying to find the right page yourself) -- There was no way to save Tartube's config file (except by shutting down - Tartube). To do that, you can now click File > Save all -- If Tartube is unable to read the config file and/or database file, the text - in the resulting dialogue windows has been improved. In some circumstances, - multiple dialogue windows were produced; this has now been fixed -- In the download options window, the option to 'embed subtitles with video' - now appears in two different places, to make it easier to find (Git #47) -- If the 'Add new video(s)', 'Add a new channel' or 'Add a new playlist' - dialogue windows are open, you can now drag-and-drop into them (Linux/BSD - only). Modifications to the code mean that it's no longer possible to - drop one URL into the middle of an existing one, rendering both of them - useless -- Tartube checked URLs for validity before adding them, but this did not work - as well as intended. The code has been improved, so less garbage should - appear in the 'Add new video(s)' dialogue window, and so on -- You could already download a temporary copy of video(s) by right-clicking - them and selecting 'Temporary > Download', but that can be inconvenient - for multiple videos, as you had to wait for each download to finish. You - can now select 'Temporary > Mark for download' instead, which creates a - copy of the video in the 'Temporary Videos' folder. When you're ready to - download them all, just download that folder -- Minor improvements to aesthetics for some textviews and treeviews - -MINOR FIXES -- Fixed incorrect operation of the checkbuttons in the Errors/Warnings Tab. - Added new checkbuttons to separate Tartube errors/warnings from youtube-dl - errors/warnings (Git #50) -- 'Child process exited with non-zero code' errors still appeared in the - Errors/Warnins tab, even if the user has disabled them. Fixed -- Tooltips for videos could not be enabled/disabled if no channel/playlist/ - folder was selected. Fixed -- On MS Windows, edit/preference windows will no longer increase in size, if - there isn't enough room for each window's tabs -- Fixed rare 'Permission denied' errors when trying to create a directory - (folder) on the filesystem -- In the download options edit window, the combobox for audio formats had - multiple and ever-increasing empty spaces. Fixed -- In the download options window, File > File names, the default value for the - custom format was garbled. Fixed, and it should now be working as intended -- During a simulated download, videos which are not in a channel or playlist - (for example, videos in the 'Unsorted Videos' folder) did not appear in - the Results List in the Progress Tab. Fixed -- Fixed an unprintable character in the licence declaration, visible in - Tartube's 'About' window -- When deleting a video, Tartube will now delete more related files (such as - those produced when post-processing a video) -- Removed a few duplicate ISO 639-1 language codes - -v1.3.077 (26 Jan 2020) -------------------------------------------------------------------------------- - -MAJOR NEW FEATURES -- Drag and drop (for example, from a web browser into Tartube's main window) - is now fully working on Linux/BSD. On MS Windows, drag and drop does not - work at all for any Gtk application. It is unlikely that the Tartube - authors can do anything about this (Git #35) -- The 'Add new video(s)' dialogue window can now handle URLs representing - channels and playlists, as well as URLs representing individual videos. - During a download operation, if Tartube is expecting an individual video - but receives a channel/playlist, it will automatically create a new - channel, and download videos into that channel. You can change this default - behaviour, if you want (Edit > System preferences... > URL flexibility - preferences) -- To change the name of the new channel/playlist, right-click it and select - 'Filesystem > Rename default location...' -- If Tartube creates a channel, which should really be a playlist, then you - can now convert one to the other. Right-click a channel and select - 'Channel actions > Convert to playlist'. Right-click a playlist and select - 'Playlist actions > Convert to channel' -- In the download options windows, it's now very easy to tell Tartube to - convert videos to sound files. Open the window by clicking 'Edit > - General download options...', click the 'Hide advanced download options' - button if necessary, click the 'Sound only' tab, select your preferences, - and apply them by clicking the OK button at the bottom of the window -- You can now see the download options applied to a video, channel, playlist - or folder without having to download anything. Right-click a video/channel/ - playlist/folder and select 'Downloads > Show system command' -- During a download operation, the system commands used are now visible (by - default) in the Output Tab. The system command can also be displayed in the - terminal, if required; this is disabled by default - -MINOR NEW FEATURES -- In the Output Tab, the summary page is now hidden by default. To make it - visible, click 'Edit > System Preferences... > Output > - Show a summary of active threads' and then restart Tartube -- In the Errors/Warnings Tab, added checkbuttons to filter out errors and/or - warning messages, if required (Git #50) -- In the Progress tab, in the top half of the window, you can now right-click - an unnamed video to open it in your web browser. This will be useful in - identifying videos that did not download, and whose name is unknown to - Tartube (Git #51) -- Columns in the Progress tab have been rearranged a little, so that the - user can more easily see how quickly the download is progressing, when - Tartube's main window is small - -MAJOR FIXES -- Fixed multiple issues with Tartube, when running under Python 3.8 -- Replaced all remaining references to the Python os.rename() function, which - can cause crashes on some filesystems (Git #34) -- Fixed crashes caused by the new YouTube error messages (January 2020), which - some versions of youtube-dl cannot handle correctly -- Fixed issues with the default location for videos, again. Fixed an issue - with adding folders inside the currently selected folder (Git #36, #46) - -MINOR FIXES -- Fixed various Gtk warning messages, visible only on some systems -- Videos whose name contains an ampersand (&) character could not be opened by - clicking the 'Media player' label in the Video Catalogue. Fixed -- The properties windows for videos, channels and playlists showed a folder - icon, instead of a video/channel/playlist icon. Fixed -- The popup menu in the Progress tab, in the top half of the tab, did not work - as intended during a download operation, and again after a download - operation. Fixed both sets of issues -- Coloured text was not displayed in the Output Tab correctly. Fixed - -v1.3.048 (23 Jan 2020) -------------------------------------------------------------------------------- - -MAJOR NEW FEATURES -- Tartube now creates an icon in the user's system tray. Closing the main - window now closes to the tray, by default. To disable this behaviour, - click Edit > System preferences > Windows > Deselect 'Close to the tray...' -- When that functionality is enabled, Tartube can be shutdown by clicking - File > Quit. Scheduled download operations will still take place if Tartube - has been closed to the tray. Implements Github issue #37 -- Tartube can now show a desktop notification at the end of a download - operation, rather that a dialogue window. This does not work on MS Windows. - On other operating systems, enable desktop notifications by clicking - Edit > System preferences... > Operations > Show a desktop notification... -- When you click the 'Add new video(s)' button, the folder displayed in the - dialogue window is now the same folder that's selected in the main window - (if any). The same applies for adding channels, playlists and folders. - Fixes Github issue #36 -- If you normally use the 'Check all' button rather than the 'Download all' - button, and if you want to download a temporary copy of one of the videos, - there's now an easier way to do it. In the Videos tab, right-click the - video, and select 'Temporary > Download' or 'Temporary > Download and - Watch'. A copy of the video is downloaded into the 'Temporary Videos' - folder, without affecting any other folders - -MINOR NEW FEATURES -- The icons for channels and playlists have been replaced, to make it easier to - tell them apart. Some other icons have been replaced too -- Videos can now be dragged and dropped from a web browser (or similar - application) into Tartube's main window, which automatically adds the video - to the currently selected folder (or 'Unsorted Videos', if no folder is - selected). Unfortunately, the code is not yet working reliably. We are - looking for a solution (Github issue #35) -- The layout of the Format tab in the download options window has been improved - to alleviate confusion experienced by users trying to download a video to - a sound format such as .mp3 (only). See the new section in the README file -- A number of new video/audio formats have been added, for example several new - 60fps formats, implementing Github issue #40 -- When you apply download options to a video/channel/playlist/folder, the - options are now cloned from the default set of options (those visible in - Edit > General download options...). To disable this behaviour, click - Edit > System preferences... > Operations > When applying download options, - automatically clone general download options. Implements Github issue #39 -- The options already applied to a video/channel/playlist/folder can now be - reset to match the general options, any time you want. Use the new button - at the bottom of the download options window, in the General tab - -MAJOR FIXES -- Fixed a rare crash when the video's JSON filename was too long for the - operating system -- In the download options window, Formats tab, the user can add up to three - video formats. The third format, if added, was always ignored. Fixed - -MINOR FIXES -- If you perform a refresh operation on a folder, the operation now applies to - all videos, channels, playlists and folders inside it -- After adding a video to the folder that's currently selected, the video does - not appear immediately in the video catalogue. Fixed -- In the Progress and Errors/Warnings tabs, the column headers scrolled away - along with the rest of the list. Fixed; they are now always visible -- Video nicknames were not set correctly after an update operation. Fixed -- During a refresh operation, a video's name was compared against the full - filepath (filename and extension), which produced none of the intended - matches. Fixed -- The edit/preference windows had a tendency to increase in size without - limits. Fixed -- A video's annotations.xml file was not deleted correctly, when required. - Fixed -- In the download options window, the option 'hls-prefer-ffmpeg' is now working - correctly -- In the download options window, the 'prefer avconv over ffmpeg' options have - been desensitised on MS Windows, as there is no known method of using - Tartube with avconv on MS Windows -- youtube-dl creates a file, ytdl-archive.txt, recording all the videos that - it has downloaded. This can interfere if the user tries to re-download the - video(s) for any reason. Create of the ytdl-archive.txt file can now be - disabled (Edit > System preferences... > youtube-dl > Deselect 'Allow - youtube-dl to create its own archive...') -- If creation of the archive file is nonetheless enabled, Tartube can now - re-download video(s) without problems -- In rare circumstances, Tartube was unable to redraw the video catalogue - (the right-hand side of the Videos tab). Fxied - -v1.3.007 (20 Dec 2019) -------------------------------------------------------------------------------- - -MAJOR FIXES -- v1.3.007 was completely broken when replacing an earlier installation. Fixed -- When Tartube's data directory was copied from one place to another (for - example, from one external drive to another), Tartube did not adapt to the - change very well. The way file paths are stored in Tartube's database has - been changed to eliminate this problem - -MINOR FIXES -- Fixed an invalid time value which (sometimes) prevented a refresh operation - from completing correctly - -v1.3.0 (20 Dec 2019) -------------------------------------------------------------------------------- -MAJOR NEW FEATURES -- Tartube on MS Windows did not recognise FFmpeg or AVConv. You can now tell - Tartube to download and install a compatible version of FFmpeg from the - main menu (Operations > Install FFmpeg). Tartube still cannot recognise - the ordinary version of FFmpeg, and it still does not recognise AVConv at - all. It is unlikely that this situation can be remedied -- A new Output Tab has been added, in which you can see what is happening - internally when you check or download videos, update youtube-dl, install - FFmpeg, or refresh the Tartube database. The amount of information shown - can be customised in the System preferences window. The information can - still be written to STDOUT/STDERR, if required -- For users on other operating systems, the system preferences window - displayed the wrong location of the FFmpeg/AVConv executable. This has now - been fixed -- There are now two simple ways to specify the video resolution you want to - download (for example, 1080p). You can use the download options window - (Edit > General download options... > Formats, and then choose a video - format like 'any format [1080p]'). You can also use the new spinbutton at - the bottom of the Progress Tab. Both of these methods have the same effect, - so it's not necessary to use both of them. Tartube will download videos in - that resolution if possible, or in the next highest available resolution - otherwise -- The download options window has been simplified, with only the most useful - options visible. If you want to see the full range of options, open any - download options window, and in the General tab, click the new 'Show - advanced download options' button - -MINOR NEW FEATURES -- By default, temporary folders are no longer emptied when Tartube shuts down, - but only when Tartube starts up. This means you can continue watching - temporary videos you've downloaded even after shutting down Tartube. If you - want temporary folders to be emptied on shutdown, as before, select Edit > - System preferences... > Videos > Empty temporary folders when Tartube shuts - down - -MAJOR FIXES -- Tartube experienced a whole range of problems when downloading videos to a - hard drive that was running out of space. Tartube now checks the available - disk space before starting to download anything, and continues checking it - throughout the download process. You can specify how much disk space should - be available in the System Preferences window. If the hard drive, despite - your best efforts, actually does run out of space, Tartube is now much more - resilient (and can usually halt the download process, rather than - crashing). The amount of disk space available is now visible in the System - Preferences window -- Adding a channel/playlist/folder whose name included a slash, for example - 'Adam/Eve's Channel', had unfortunate consequences, with Tartube creating - a directory (folder) at the wrong location. Slashes are now automatically - converted to hyphens, which solves the problem -- In Tartube's window, dragging a channel/playlist/folder to a new location in - the tree changes the hard drive, moving a directory (folder) to a new - location in the filesystem. If a directory (folder) with the same name - already existed at that location, an invisible error occurs. Tartube now - displays a visible error so the user can delete the duplicate directory - (folder) manually -- When refreshing the Tartube database (e.g. Operations > Refresh database), - the moviepy module freezes if it encounters a corrupted video file. We - can't fix the moviepy module, but the Tartube code has been made much more - resilient -- When refreshing the Tartube database, Tartube made bad decisions if it was - looking for a video called 'ymca.mp4', but found a video called - 'ymca.webm'. This has been fixed -- Tartube is now able to detect if its data directory (into which videos are - downloaded) doesn't exist. Usually this is because an external hard drive - has not been mounted; the user is now warned about this, so they can mount - it -- On MS Windows, if the user has updated youtube-dl or installed FFmpeg, - Tartube no longer freezes on shutdown -- On MS Windows, Tartube was unable to open a video file in the system's - default media player, if the name contained an ampersand. Fixed - -MINOR FIXES -- When deleting large channels/playlists/folders, sometimes not everything was - deleted, and the user had to delete the item a second time. This was due to - inconsistencies in the Tartube database, which have now been fixed -- Channels/playlists/folders beginning with a number, e.g. '5 Pewdiepie', were - supposed to be displayed in numerical order, rather than in strict - alphabetical order. This did not work as intended (e.g. '11 Pewdiepie' was - listed before '1 T-Series'). Fixed again -- In the 'Delete channel' dialogue window (and so on), the name of the channel - to be deleted is now displayed prominently -- The size of the MS Windows installer has been reduced by about 40% - -v1.2.008 (30 Sep 2019) -------------------------------------------------------------------------------- - -MINOR NEW FEATURES -- Tartube now ignores the YouTube 'WARNING: video doesn't have subtitles' - by default. You can change this setting, if you want to - -MAJOR FIXES -- When moving a channel/playlist/folder to a different place on your - filesystem, or when renaming a channel/playlist/folder, in certain rare - situations data in the Tartube database isn't updated correctly. This may - lead to a freeze or a crash. I'm not sure yet what the cause is, but I have - added temporary code to prevent the problem affecting any user -- Fixed error messages generated when checking/downloading individual - channels/playlists/folders -- Fixed faulty code for importing videos/channels/playlists/folders into the - database - -v1.2.0 (31 Aug 2019) -------------------------------------------------------------------------------- - -MAJOR NEW FEATURES -- Multiple channels, playlists and/or folders can now download their videos to - a single location. The README.rst file explains how it works, and why you - might want to do it -- You can also tell Tartube to download all videos into the 'Unsorted Videos' - or 'Temporary Videos' folders, instead of downloading them into separate - directories/folders for each channel and playlist -- Added automatic deletion of videos, disabled by default. Before enabling it, - you should do a 'Check all' or 'Download all' operation, which will create - the necessary youtube-dl archive files -- Added archiving. A video, channel, playlist or folder that is marked - archived won't be auto-deleted (but can still be deleted manually by the - user) -- You can now disable both checking and downloading a channel, playlist or - folder, if you want to. (It was already possible to just disabled - downloading them) -- Download operations can now be scheduled to take place at regular intervals -- You can now 'Download and watch' a video. The video is opened in your - system's default media player as soon as it has been downloaded -- Tartube can now download a video's annotations file automatically. Warnings - generated by YouTube about the lack of annotations are ignored by default -- For channels/playlists/folders containing many videos, you can now skip to - the first video uploaded after a certain date, using the new button in the - toolbar at the bottom of the Videos Tab -- The lists in the Results Tab can now be right-clicked, so you can change the - order in which videos/channels/playlists/folders are checked/downloaded, - abandon a download, play a video directly from the Results List, delete a - video directly from the Results List, and so on -- The edit window for youtube-dl options has been improved, adding many new - options to the GUI interface -- Plain text exports of Tartube's database can now be re-imported. Some - inconsistencies with the import process (from JSON and plain text files) - have been fixed - -MINOR NEW FEATURES -- Added tooltips in several places. If you don't want to see tooltips above - videos/channels/playlists/folders, you can turn them off -- You can now select multiple videos in the Video Catalogue, and apply an - action to all of them (by right-clicking them) -- You can now switch to smaller icons in the Video Index (on the left side of - the Videos Tab), if you want to -- You can now force the Video Index to expand its tree whenever you click on a - folder, revealing any channels/playlists it contains. This is disabled by - default -- If you only want to check videos, and never download them, you can disable - the 'Download all' buttons. Individual videos/channels/playlists/folders - can still be downloaded by right-clicking them -- Tartube's file structure has changed. If you run it from the command line, - you might need to use a (slightly) different command. See the README.rst - for details of what command to use -- The path to the FFmpeg/AVConv executable can now be specified by the user. - This will be especially helpful for MS Windows users -- Columns in the Progress Tab can now be manually resized -- The Tartube website can now be opened from the main window menu -- XDG has been added as an optional dependency, for the benefit of Debian - packagers - -MAJOR FIXES -- The MS Windows installer should now work for everyone -- Refresh operations are now stable (should not crash) on systems with Gtk 3.22 - or earlier -- When downloads were disabled for a folder, downloads for channels/playlists/ - folders inside that folder were still enabled. This is counter-intuitive, - so disabling downloads for a folder disables downloads for everything it - contains -- Marking/unmarking a video as favourite caused certain problems, which should - now be fixed - -MINOR FIXES -- Fixed some unicode errors in reading JSON and plain text files -- Fixed the wrong page size displayed in the toolbar at the bototm of the - Videos Tab -- Empty lines in a video's description are now preserved when they're displayed - in Tartube's main window - -v1.1.0 (18 Aug 2019) -------------------------------------------------------------------------------- - -MAJOR NEW FEATURES -- You can now create an export of Tartube's database. This export contains - details of videos, channels, playlists and/or folders, but not the videos - themselves (or any of the thumbnail/description/metadata files). The - export can take two forms: JSON data, or plain text. If JSON data, the - exported file can later be imported into another Tartube database (imports - from plain text are not implemented yet). You can export either the - entire database, or just one channel/playlist/folder (and everythng it - contains) -- You can now change the name of a channel, playlist of folder. This doesn't - have any effect on your filesystem; it only changes the name displayed in - the Video Index (the left-hand side of the Videos Tab). This might be - useful for channels and playlists that have weird or very short names. For - example, right-click a folder and select 'Folder actions > Set nickname...' -- You can also rename the channel, playlist or folder, and this action DOES - affect the filesystem, changing the directory/folder on your hard drive - where the channel/playlist/folder videos are stored. For example, right- - click a folder and select 'Filesystem > Rename location...' -- If you change the format of a downloaded video file from the default 'Title' - to, for example, 'Title + ID', the Video catalogue (the right-hand side of - the Videos Tab) will now simply display the video's title (which should be - easier to read). To see the actual filename, you can right-click the video - and select 'Show properties'. This only works if the video's metadata was - downloaded when the video itself was downloaded; this is now turned on by - default for all new users - -MINOR NEW FEATURES -- When adding videos, channels and playlists, the contents of the system's - clipboard was automatically copied into the window. This can now be turned - off, if you wish (Edit > System preferences... > Windows > When adding - videos/channels/playlists, copy URLs from the system clipboard) -- When adding channels/playlists, you can set the dialogue window to stay open, - which makes adding multiple channels/playlists quicker (Edit > - System preferences... > Windows > When adding channels/playlists, keep the - dialogue window open) -- When creating channels/playlists/folders inside an existing parent folder, a - dialogue window which stays open can be told to continuously re-use that - parent folder (Edit > System preferences... > Windows > When adding - channels/playlists, re-use the optional parent folder) -- When checking/downloading videos, the Results List (the bottom half of the - Progress Tab) can now display videos in reverse order, so you don't have to - scroll down to see the video that was just checked/downloaded (Edit > - System Preferences... > Windows > Show results in reverse order) -- The number of ystem error and warning messages displayed in their own tab - is visible in the tab's label. The label is usually reset when the tab - is made visible. You can now disable this behaviour, preserving the numbers - until the 'Clear the list' button is explicitly clicked (Edit > - System preferences... > Windows > Don't remove number of system messages - from tab label until 'Clear' button is clicked) -- Items in the Video Index (on the left-hand side of the Videos Tab) are sorted - alphabetically. The sorting algorithm has been improved to take account of - numbered items, such that '1 Music' will now appear before '11 Comedy' -- For the benefit of package maintainers (such as a Debian package), Tartube - now uses an environment variable which will prevent Tartube from updating - the youtube-dl binary, if specified. See the comments in setup.py - -MAJOR FIXES -- Users whose system Gtk is earlier than v3.24 (this includes many current - Linux distros, but the MS Windows installer) will have experienced graphics - issues, and endless error messages in the terminal window, if open. If your - system Gtk is earlier than v3.24, Tartube will no longer update the Video - Index during a download operation; this should fix the issue at the cost of - disabling real-time updates of the number of videos in each channel, - playlist and folder. Your system's Gtk version is now visible in Tartube's - System Preferences window -- After loading the config file, the download limits were set, but not - displayed in the Progress Tab. Fixed -- Rarely, Tartube crashes (or freezes) when loading a video's JSON metadata - file from your filesystem (but now when downloading it). This should no - longer happen - -MINOR FIXES -- The 'Channel properties', 'Folder properties' (etc) windows used the wrong - icon (displaying a folder in the wrong colour). Fixed -- Fixed a 'list modified during sort' error during a download operation - -v1.0.0 (31 Jul 2019) -------------------------------------------------------------------------------- - -- First beta release -- Fixed some issues with the MS Windows installer -- Some parts of the Tartube window displayed the wrong icons. Fixed - -v0.7.0 (7 Jul 2019) -------------------------------------------------------------------------------- - -- This is the first release candidate for v1.0.0 -- The MS Windows installer has been redesigned again (thanks to slartie for his - generous assistance in getting it working). Some MS Windows 10 users were - still complaining that Tartube would not run; this should be fixed now -- MS Windows should find that the annoying terminal window is no longer visible -- Deleting individual videos, then adding them to the Tartube database again, - could cause problems with the statistics displayed in the Video Index (e.g. - 'All Videos (0, -1)'. Fixed -- Deleting and individual video by right-clicking it removed the video from the - database, but didn't delete the video file itself. Fixed -- If an empty channel/playlist was selected, new videos did not automatically - appear in the Video Catalogue during a download operation. Fixed -- Fixed some issues with the Temporary Videos folder -- Fixed more issues with videos being dislayed in the wrong order in the Video - Catalogue -- Fixed more issues with the scrollbars not resetting themselves when switching - between channels/playlists/folders -- After right-clicking a folder, selecting Mark Videos > New didn't work. Fixed -- When marking videos as new/favourite, you can now do this to all videos - in a folder, including all child channels/playlists/folders, or you can do - it just to the videos actually inside that folder. There are several new - popup menu options for emptying a folder, or for removing all its videos -- Tartube will no longer issue a system error if you drag a folder onto itself - in the Video Index -- Fixed the remaining issues caused by the Gtk graphics libraries -- Confirmed that various Gtk issues present in Gtk3.22 are not present in - Gtk 3.24. Users with Gtk 3.22 on their system will be warned to update it. - These warnings can be disabled, if required - -v0.6.0 (4 Jul 2019) -------------------------------------------------------------------------------- - -- Some MS Windows users, especially on Windows 10, report that they can't run - Tartube at all. In an effort to get around this, the installer has been - redesigned. The way Tartube communicates with youtube-dl on MS Windows has - been changed. youtube-dl update operations should now work flawlessly. - Please report any further problems at our GitHub page; this might be a fix - to issue #10 -- Users on Linux/*BSD can now run Tartube directly from the command line, after - installing it (see the README) -- Occasionally, videos were downloaded (or checked) successfully, but Tartube - failed to notice them. This issue should now be fixed -- When checking videos/channels/playlists/folders, only new videos will now - appear in the Results List (in the 'Progress' Tab) -- The toolbar has been redesigned. MS Windows users won't see labels at all - (so everything should fit). Users on all system can turn labels on or off. - Tooltips have been added to the buttons, in case the labels are turned off -- Fixed yet another problem with button to switch the location of Tartube's - data folder (#6) -- Tartube no longer requires the python 'validators' module -- Tartube can now ignore YouTube copyright messages, and also 'Child process - exited with non-zero code' messages, meaning that they won't appear in - Tartube's Errors/Warnings Tab. (They are not ignored by default) -- Tartube now applies a 60-second timeout when youtube-dl tries to download - a video's metadata (since youtube-dl uses a 10-minute timeout); this can be - turned off, if required (#9) -- Fixed some more issues with the way videos are sorted in the Video Catalogue - (it's still not 100%) -- Users can no longer type in comboboxes -- Tartube now spots when the user adds a channel or playlist URL as a video; - only the first video in the channel/playlist is now downloaded -- You can no longer remove download options from a media data object when the - download options edit window is still open -- When you update youtube-dl, Tartube will now tell you which youtube-dl - version is installed. Tartube will no longer claim the update operation - fails if you're using pip (rather than pip3) - -v0.5.0 (1 Jul 2019) -------------------------------------------------------------------------------- - -- On MS Windows, the fix from v0.4.0 to prevent a crash whenever the user tries - to change the location of Tartube's data directory, did not work. Fixed it - again (#6), and added some dialogue window to make it clearer to the user - what is going on -- Fixed problems with dragging-and-dropping (or otherwise moving) a channel, - playlist or folder to a new location (such as another folder). The videos - were not updated with their new location. The new code will fix any - problems in the Tartube database -- Fixed numerous problems with the code that sorts videos into the right order, - and which displays videos in the Video Catalogue in the right order -- The 'Switch' button no longer resets the page back to the first one -- When switching between channels/playlists/folders, scrollbars are - automatically moved back to the top -- In the toolbar beneath the Video Catalogue, there are two new buttons for - scrolling to the top or bottom of the visible page. Users can now select - a different page just by typing the page number and pressing RETURN. The - same applies to the page size - there is no button to click any more, just - type the new size and press RETURN -- Fixed several problems which were still preventing selection of different - video formats (#3) -- Added bare-bones aac, m4a, mp3, ogg and wav as recognised video formats -- A set of download options can now be completely reset to their default values -- The 'Add Videos' dialogue window, and some others, don't behave well when the - user resizes them. Fixed (#4) -- Added a small folder icon to the the 'Add Videos' dialogue window, and - others, so the user is less likely to forget to set a custom location -- In the 'Add Videos' dialogue window, and others, URLs are now tidied up a - bit because being copied from the clipboard, eliminating leading/trailing - whitespace and empty lines -- The Gtk test window, available for MS Windows after using the installer, - now contains some text (to make it clear that the window is working as - intended) - -v0.4.0 (29 Jun 2019) ------------------------------------------------------------------------------- - -- Drastic improvements to overall performance. Download operations are now much - smoother. You should notice a much lighter burden on your machine's CPU -- The graphics libraries struggled to draw lists containing hundreds (or - thousands!) of videos, so the Video Catalogue has been split into pages. It - typically takes less then a second to show the 'All Videos' folder, if it - contains hundreds of videos, rather than several minutes -- If you just want to find new videos, you can now tell Tartube to stop - checking/downloading channels/playlists as soon as notifications of videos - you've already checked/downloaded start arriving. This works well on - YouTube, which sends the newest videos first, but might not work well on - all websites. The new functionality is turned off by default. Click - 'Edit > System preferences > Performance > Time-saving preferences' to turn - it on -- The installer for 32-bit MS Windows failed under all circumstances. Applied a - fix -- On MS Windows, the uninstaller was invisible. It can now be executed from the - Start Menu -- On MS Windows, fixed a crash whenever the user tried to changed the location - of Tartube's data directory (#6) -- Fixed some problems with the 'Add videos' dialogue window, which tried to add - all sorts of invalid URLs (such as lines containing only empty space). The - other 'Add' dialogue windows were also fixed -- A limit to the length of a channel/playlist/folder name now applies -- Leading/trailing whitespace is now removed from URLs and channel/playlist/ - folder names -- The 'Add videos' dialogue window now checks for duplicate URLs, and when - found, doesn't add them to the database -- Fixed various problems caused by deleting a video/channel/playlist/folder - (such as the numbers visible in the Video Index). If you've been using an - earlier version of Tartube, any such problems with the database will be - automatically repaired for you (#8) -- Videos which haven't actually been downloaded can now be deleted from the - database by right-clicking them -- Added new options for making backups of the Tartube database file, in case it - becomes corrupted. See the settings in - 'Edit > System preferences... > Backups' -- The 'Switch' button now switches between four 'skins' (instead of the - previous two). Two of them show the name of a video's parent channel/ - playlist/folder; the other two show the video's description -- Fixed incorrect formatting in the DASH and 3D file formats (#3) - -v0.3.0 (25 Jun 2019) -------------------------------------------------------------------------------- - -- Tartube will now run on MS Windows -- Fixed some more crashes -- Fixed some issues with video descriptions containing quotes and ampersand - characters - -v0.2.0 (23 Jun 2019) -------------------------------------------------------------------------------- - -- Corrected old Python2 code to work on Python3 -- Greatly expanded the README file -- Fixed the constant crashes -- Fixed some Gtk problems (but others remain unfixed) -- Fixed downloads for users who haven't installed Ffmpeg, and for sites that - don't support Ffmpeg -- Several other minor tweaks/fixes - -v0.1.0 (27 May 2019) -------------------------------------------------------------------------------- - -- This is the first public release of Tartube - +- This is the first public release of GymBob diff --git a/MANIFEST.in b/MANIFEST.in index 2d69820..162d011 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,4 @@ recursive-include docs * recursive-include icons * recursive-include pack * recursive-include screenshots * -recursive-include share * diff --git a/README.rst b/README.rst index d098a9e..78b77a0 100644 --- a/README.rst +++ b/README.rst @@ -1,941 +1,220 @@ -=================================================== -Tartube - The Easy Way To Watch And Download Videos -=================================================== ------------------------------------------------------------- -Works with YouTube, BitChute, and hundreds of other websites ------------------------------------------------------------- +====== +GymBob +====== +---------------------------------------------- +A simple script to prompt you during a workout +---------------------------------------------- -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 +.. image:: screenshots/gymbob.png + :alt: GymBob screenshot * `1 Introduction`_ -* `2 Why should I use Tartube?`_ -* `3 Downloads`_ -* `4 Quick start guide`_ -* `5 Installation`_ -* `6 Using Tartube`_ -* `7. Frequently-Asked Questions`_ -* `8. Contributing`_ -* `9. Authors`_ -* `10. License`_ +* `2 Downloads`_ +* `3 Quick start guide`_ +* `4 Installation`_ +* `5 Using GymBob`_ +* `6 Contributing`_ +* `7 Authors`_ +* `8 License`_ 1 Introduction ============== -**Tartube** is a GUI front-end for `youtube-dl `__, partly based on `youtube-dl-gui `__ and written in Python 3 / Gtk 3. +A typical gym workout consists of a sequence of exercises. Sometimes each exercise is performed several times. (Each repetition is called a 'set'). -It runs on MS Windows, Linux and BSD. It probably works on MacOS, but the authors have not been able to confirm this. +**GymBob** prompts the user, at pre-determined intervals, when to begin each set. The **GymBob** window shows the current set, the next set, and a stopwatch for both. If sound is enabled (which it is by default), the user will hear a sound effect when it's time to start a set. -Problems can be reported at `our GitHub page `__. +**GymBob** follows a workout programme created by you, the user. It's easy to create your own programmes, and you can create as many as you like. -2 Why should I use Tartube? -=========================== +**GymBob** is written in Python 3 / Gtk 3. It runs on Linux and \*BSD. There are no plans to create installers for MS Windows and MacOS. -- You can download individual videos, and even whole channels and playlists, from YouTube and hundreds of other websites (see `here `__ 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 -- 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 +Problems can be reported at `our GitHub page `__. -3 Downloads +2 Downloads =========== -Latest version: **v2.0.0 (29 Feb 2019)** +Latest version: **v1.002 (28 Mar 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 -- `Gentoo ebuild (available in src_prepare-overlay) `__ from Gitlab -- `Source code `__ from Sourceforge -- `Source code `__ and `support `__ from GitHub +- `DEB package (for Debian-based distros, e.g. Ubuntu, Linux Mint) `__ from Sourceforge +- `RPM package (for RHEL-based distros, e.g. Fedora) `__ 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. - -4 Quick start guide +3 Quick start guide =================== -4.1 MS Windows --------------- - -- 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** -- 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) +- Download, install and run **GymBob**, using the links above +- Click **Programmes > New programme...** +- Enter a name, and click the **OK** button +- Specify a time (in seconds), a message to display, and optionally a sound effect +- Repeat that step as many times as you like - Click the **OK** button to close the window -- Click the **Check all** button. **Tartube** will fetch a list of videos in the channel -- Click **All Videos** to see that list -- If you want to download the videos, click the **Download all** button +- Click the **START** button to start the workout programme -4.2 Linux/BSD -~~~~~~~~~~~~~ - -- Install **Tartube** by downloading the DEB or RPM package from the links above. Alternatively, install it from PyPI, using the instructions below -- It's strongly recommended that you install `Ffmpeg `__ or `AVConv `__, too -- Run **Tartube** -- When prompted, choose a directory where **Tartube** can store videos -- Install **youtube-dl** by clicking **Operations > Update youtube-dl** -- 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) -- Click the **OK** button to close the window -- Click the **Check all** button. **Tartube** will fetch a list of videos in the channel -- Click **All Videos** to see that list -- If you want to download the videos, click the **Download all** button - -5 Installation +4 Installation ============== -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. - -If you want to use **FFmpeg**, see `6.4 Setting the location of FFmpeg / AVConv`_. - -From v1.4, the installer includes a copy of `AtomicParsley `__, so there is no need to install it yourself. - -5.1.1 Manual installation - MS Windows -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some users report that **Tartube** will install but won't run. This problem should be fixed as of v1.2.0 but, if you still have problems, you can try performing a manual installation. This takes about 10-30 minutes, depending on your internet speed. - -- This section assumes you have a 64-bit computer -- Download and install MSYS2 from `msys2.org `__. You need the file that looks something like **msys2-x86_64-yyyymmdd.exe** -- MSYS2 wants to install in **C:\\msys64**, so do that -- Open the MINGW64 terminal, which is **C:\\msys64\\mingw64.exe** -- In the MINGW64 terminal, type: - - **pacman -Syu** - -- If the terminal wants to shut down, close it, and then restart it -- Now type the following commands, one by one: - - **pacman -Su** - - **pacman -S mingw-w64-x86_64-python3** - - **pacman -S mingw-w64-x86_64-python3-pip** - - **pacman -S mingw-w64-x86_64-python3-gobject** - - **pacman -S mingw-w64-x86_64-python3-requests** - - **pacman -S mingw-w64-x86_64-gtk3** - - **pacman -S mingw-w64-x86_64-gsettings-desktop-schemas** - -- 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: - - **cd tartube** - - **python3 tartube** - -5.2 Installation - MacOS ------------------------- - -**Tartube** should run on MacOS, but the authors don't have access a MacOS system. If you are a MacOS user, open an issue at our Github page, and we'll work out the installation procedure together. - -5.3 Installation - Linux/BSD ----------------------------- - Linux/BSD users can use any of the following installation methods. -5.3.1 Install using the DEB/RPM/ebuild packages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4.1 Install using the DEB/RPM packages +-------------------------------------- -Linux distributions based on Debian, such as Ubuntu and Linux Mint, can install **Tartube** using the DEB package (see the links above). Linux distributions based on RHEL, such as Fedora, can install **Tartube** using the RPM package (see the links above). Gentoo users can install **Tartube** using the ebuild (see the link above). +Linux distributions based on Debian, such as Ubuntu and Linux Mint, can install **GymBob** using the DEB package (see the links above). Linux distributions based on RHEL, such as Fedora, can install **GymBob** using the RPM package (see the links above). -**Tartube** requires `youtube-dl `__. If it's already installed on your system, then you can start **Tartube** immediately. +4.2 Install using PyPI +---------------------- -Otherwise, if **pip** is already installed on your system, do this: - -1. Run **Tartube** -2. **Tartube** asks you to choose a data directory, so do that -3. Click **Operations > Update youtube-dl** - -If neither **youtube-dl** nor **pip** are installed on your system, then the recommended way to install **youtube-dl** is from the command line, using **pip**. (Software managers usually don't offer the most recent version of **youtube-dl**.) - -This is the procedure on Debian-based distributions, like Ubuntu and Linux Mint. The procedure on other distributions is probably very similar. - -1. Open a terminal window -2. Type: ``sudo apt install python3-pip`` -3. Type: ``pip3 install youtube-dl`` -4. You can now run **Tartube**. - -5.3.2 Install using PyPI -~~~~~~~~~~~~~~~~~~~~~~~~ - -**Tartube** can be installed from `PyPI `__ with or without root privileges. +**GymBob** can be installed from `PyPI `__ with or without root privileges. Here is the procedure for Debian-based distributions, like Ubuntu and Linux Mint. The procedure on other distributions is probably very similar. -5.3.3 Install using PyPI (with root privileges) +4.2.1 Install using PyPI (with root privileges) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -1. Make sure **youtube-dl** has been completely removed from your system -2. Type: ``sudo apt install python3-pip`` -3. Type: ``sudo pip3 install youtube-dl tartube`` -4. Type: ``tartube`` +1. Type: ``sudo apt install python3-pip`` +2. Type: ``sudo pip3 install gymbob`` +3. Type: ``gymbob`` -5.3.4 Install using PyPI (without root privileges) +4.2.2 Install using PyPI (without root privileges) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. Type: ``sudo apt install python3-pip`` -2. Type: ``pip3 install tartube`` -3. The **Tartube** executable is stored in **~/.local/bin** by default. If that is already in your path, you can start **Tartube** by typing ``tartube``. Otherwise, type ``~/.local/bin/tartube`` -4. **Tartube** asks you to choose a data directory, so do that -5. In the **Tartube** main window, click **Edit > System preferences... > youtube-dl** -6. In the box marked **Actual path to use**, select **Use PyPI path (\~/.local/bin/youtube-dl)** -7. Click **OK** to close the dialogue window -8. Click **Operations > Update youtube-dl** -9. Once the update has finished, **Tartube** is ready for use +2. Type: ``pip3 install gymbob`` +3. The **GymBob** executable is stored in **~/.local/bin** by default. If that is already in your path, you can start **GymBob** by typing ``gymbob``. Otherwise, type ``~/.local/bin/gymbob`` -5.3.5 Manual installation -~~~~~~~~~~~~~~~~~~~~~~~~~ +4.3 Manual installation +----------------------- For any other method of installation, the following dependencies are required: - `Python 3 `__ - `Gtk 3 `__ -- `Python Requests module `__ -- `youtube-dl `__ +- `Python playsound module `__ -These dependencies are optional, but recommended: - -- `Python pip `__ - keeping youtube-dl up to date is much simpler when pip is installed -- `Python moviepy module `__ - if the website doesn't tell **Tartube** about the length of its videos, moviepy can work it out -- `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 - -5.3.6 Install from source +4.3.1 Install from source ~~~~~~~~~~~~~~~~~~~~~~~~~ After installing dependencies (see above): 1. Download & extract the source code (see the links above) -2. Change directory into the **Tartube** directory +2. Change directory into the **GymBob** directory 3. Type: ``python3 setup.py install`` -4. Type: ``tartube`` +4. Type: ``gymbob`` -5.3.7 Run without installing +4.3.2 Run without installing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ After installing dependencies (see above): 1. Download & extract the source code (see the links above) -2. Change directory into the **Tartube** directory -3. Type: ``python3 tartube/tartube`` +2. Change directory into the **GymBob** directory +3. Type: ``python3 gymbob/gymbob`` -6 Using Tartube -=============== +5 Using GymBob +============== -* `6.1 Choose where to save videos`_ -* `6.2 Check youtube-dl is updated`_ -* `6.3 Setting youtube-dl's location`_ -* `6.4 Setting the location of FFmpeg / AVConv`_ -* `6.4.1 On MS Windows`_ -* `6.4.2 On Linux/BSD`_ -* `6.5 Introducing system folders`_ -* `6.6 Adding videos`_ -* `6.7 Adding channels and playlists`_ -* `6.8 Adding videos, channels and playlists together`_ -* `6.9 Adding folders`_ -* `6.10 Things you can do`_ -* `6.11 General download options`_ -* `6.12 Other download options`_ -* `6.13 Custom downloads`_ -* `6.13.1 Independent downloads`_ -* `6.13.2 Diverting to HookTube/Invidious`_ -* `6.13.3 Delays between downloads`_ -* `6.14 Watching videos`_ -* `6.15 Filtering and finding videos`_ -* `6.16 Marking videos`_ -* `6.16.1 Bookmarked videos`_ -* `6.16.2 Favourite channels, playlists and folders`_ -* `6.17 Combining channels, playlists and folders`_ -* `6.17.1 Combining one channel and many playlists`_ -* `6.17.2 Combining channels from different websites`_ -* `6.17.3 Download all videos to a single folder`_ -* `6.18 Archiving videos`_ -* `6.19 Managing databases`_ -* `6.19.1 Importing videos from other applications`_ -* `6.19.2 Multiple databases`_ -* `6.19.3 Multiple Tartubes`_ -* `6.19.4 Exporting/importing the database`_ -* `6.20 Converting to audio`_ - -6.1 Choose where to save videos -------------------------------- - -When you first start **Tartube**, you will be asked to choose where **Tartube** should save its videos. - -.. image:: screenshots/example1.png - :alt: Setting Tartube's data folder - -Regardless of which location you select, you can change it later, if you need to - see `6.19 Managing databases`_ - -- In the main menu, click **File > Database preferences...** -- In the new window, check the location of the **Tartube data directory** -- If you want to change it, click the **Change** button - -6.2 Check youtube-dl is updated -------------------------------- - -*If you installed Tartube via a repository such as the official Debian repository, then Tartube may not be allowed to update youtube-dl, in which case this section does not apply.* - -**Tartube** uses **youtube-dl** to interact with websites like YouTube. You should check that **youtube-dl** is also installed and running correctly. - -If you are using MS Windows, you will be prompted to install **youtube-dl**; you should click **Yes**. - -.. image:: screenshots/example2.png - :alt: Installing youtube-dl on MS Windows - -**youtube-dl** is updated every week or so. You can check that **youtube-dl** is installed and up to date: - -.. image:: screenshots/example3.png - :alt: Updating youtube-dl - -- Click **Operations > Update youtube-dl** - -6.3 Setting youtube-dl's location ---------------------------------- - -If the update operation fails on MS Windows, you should `ask the authors for help `__. - -On other systems, users can modify **Tartube**'s settings. There are several locations on your filesystem where **youtube-dl** might have been installed. - -.. image:: screenshots/example4.png - :alt: Updating youtube-dl - -- Click **Edit > System preferences... > youtube-dl** -- Try changing the setting **Actual path to use** -- Try changing the setting **Shell command for update operations** -- Try the update operation again - -6.4 Setting the location of FFmpeg / AVConv -------------------------------------------- - -**youtube-dl** can use the `FFmpeg library `__ or the `AVConv library `__ for various video-processing tasks, such as converting video files to audio, and for handling large resolutions (1080p and higher). If you want to use FFmpeg or AVConv, you should first install them on your system. - -6.4.1 On MS Windows -~~~~~~~~~~~~~~~~~~~ - -On MS Windows, the usual methods of FFmpeg installation will not work. You **must** download a MinGW-compatible version of FFmpeg. The quickest way to do this is from **Tartube**'s main menu: click **Operations > Install FFmpeg**. - -There is no known method of installing a compatible version of AVConv. - -6.4.2 On Linux/BSD -~~~~~~~~~~~~~~~~~~ - -On Linux/BSD, **youtube-dl** might be able to detect FFmpeg/AVConv without any help from you. If not, you can tell **Tartube** where to find FFmpeg/AVConv in this same tab. - -.. image:: screenshots/example5.png - :alt: Updating ffmpeg - -6.5 Introducing system folders ------------------------------- - -On the left side of the **Tartube** window is a list of folders. You can store videos, channels and playlists inside these folders. You can even store folders inside of other folders. - -**Tartube** saves videos on your filesystem using exactly the same structure. - -.. image:: screenshots/example6.png - :alt: Tartube's system folders - -When you start **Tartube**, there are seven folders already visible. You can't remove any of these folders (but you can hide them, if you want). - -- 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 **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 -- The **Unsorted Videos** folder is a useful place to put videos that don't belong to a particular channel or playlist - -6.6 Adding videos ------------------ - -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 - :alt: Adding videos - -Copy and paste the video's URL into the dialogue window. You can copy and paste as many URLs as you like. - -When you're finished, click the **OK** button. - -Finally, click on the **Unsorted Videos** folder to see the videos you've added. - -.. image:: screenshots/example8.png - :alt: Your first added video - -6.7 Adding channels and playlists ---------------------------------- - -You can also add a whole channel by clicking the **'Channel'** button or a whole playlist by clicking the **'Playlist'** button. - -**Tartube** will download all of the videos in the channel or playlist. - -.. image:: screenshots/example9.png - :alt: Adding a channel - -Copy and paste the channel's URL into the dialogue window. You should also give the channel a name. The channel's name is usually the name used on the website (but you can choose any name you like). - -6.8 Adding videos, channels and playlists together --------------------------------------------------- - -When adding a long list of URLs, containing a mixture of channels, playlists and individual videos, it's quicker to add them all at the same time. Click the **'Videos'** button near the top of the window, and paste all the links into the dialogue window. - -**Tartube** doesn't know anything about these links until you actually download them (or check them). If it's expecting an individual video, but receives a channel or a playlist, **Tartube** will the handle the conversion for you. - -By default, **Tartube** converts a link into a channel, when necessary. You can change this behaviour, if you want to. - -- In **Tartube**'s main window, click **Edit > System preferences... > Operations > URL flexibility** -- Select one of the behaviours listed there - -Unfortunately, there is no way for **Tartube** to distinguish a channel from a playlist. Most video websites don't supply that information. - -If your list of URLs contains a mixture of channels and playlists, you can convert one to the other after the download has finished. - -- In **Tartube**'s main window, right-click a channel, and select **Channel actions > Convert to playlist** -- Alternatively, right-click a playlist, and select **Channel actions > Convert to channel** -- After converting, you can set a name for the new channel/playlist by right-clicking it, and selecting **Channel actions > Rename channel...** or **Playlist actions > Rename playlist...** - -6.9 Adding folders ------------------- - -The left-hand side of the window will quickly still filling up. It's a good idea to create some folders, and to store your channels/playlists inside those folders. - -Click the **'Folder'** button near the top of the window, and create a folder called **Comedy**. - -.. image:: screenshots/example10.png - :alt: Adding a folder - -Then repeat that process to create a folder called **Music**. You can then drag-and-drop your channels and playlists into those folders. - -.. image:: screenshots/example11.png - :alt: A channel inside a folder - -6.10 Things you can do ----------------------- - -Once you've finished adding videos, channels, playlists and folders, you can make **Tartube** do something. **Tartube** offers the following operations: - -- **Check** - Fetches information about videos, but don't download them -- **Download** - Actually downloads the videos. If you have disabled downloads for a particular item, **Tartube** will just fetch information about it instead -- **Custom download** - Downloads videos in a non-standard way; see `6.13 Custom downloads`_ -- **Refresh** - Examines your filesystem. If you have manually copied any videos into **Tartube**'s data directory, those videos are added to **Tartube**'s database -- **Update** - Installs or updates **youtube-dl**, as described in `6.2 Check youtube-dl is updated`_. Also installs FFmpeg (on MS Windows only); see `6.4 Setting the location of FFmpeg / AVConv`_ -- **Info** - Fetches information about a particular video: either the available video/audio formats, or the available subtitles -- **Tidy** - Tidies up **Tartube**'s data directory, as well as checking that downloaded videos still exist and are not corrupted - -.. image:: screenshots/example12.png - :alt: The Check and Download buttons - -To **Check** or **Download** videos, channels and playlists, use the main menu, or the buttons near the top of the window, or right-click an individual video, channel or playlist. A **Custom Download** can be started from the main menu or by right-clicking. - -To **Refresh** **Tartube**'s database, use the main menu (or right-click a channel/playlist/folder). - -**Protip:** Do an **'Update'** operation before you do a **'Check'** or **'Download'** operation - -**Protip:** Do a **'Check'** operation before you do **'Refresh'** operation - -To fetch **Info** about a video, right-click it. - -To **Tidy** the data directory, use the main menu (or right-click a channel/playlist/folder). - -6.11 General download options ------------------------------ - -**youtube-dl** offers a large number of download options. This is how to set them. - -.. image:: screenshots/example13.png - :alt: Opening the download options window - -- Click **Edit > General download options...** - -A new window opens. Any changes you make in this window aren't actually applied until you click the **'Apply'** or **'OK'** buttons. - -6.12 Other download options ---------------------------- - -Those are the *default* download options. If you want to apply a *different* set of download options to a particular channel or particular playlist, you can do so. - -At the moment, the general download options apply to *all* the videos, channels, playlists and folders you've added. - -.. image:: screenshots/example14.png - :alt: The window with only general download options applied - -Now, suppose you want to apply some download options to the **Music** folder: - -- Right-click the folder, and select **Apply download options...** - -In the new window, click the **'OK'** button. The options are applied to *everything* in the **Music folder**. A pen icon appears above the folder to remind you of this. - -.. 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. - -- 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. - -.. image:: screenshots/example16.png - :alt: Download options applied to the Village People channel - -6.13 Custom downloads ---------------------- - -By default, **Tartube** downloads videos as quickly as possible using each video's original 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. - -- Click **Edit > System preferences... > Operations > Custom** -- Select one or more of the three options to enable them -- To start the custom download, click **Operations > Custom download all** - -6.13.1 Independent downloads -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, **Tartube** instructs the underlying **youtube-dl** software to download from a channel or a playlist; it doesn't actually supply a list of videos in each channel/playlist. **youtube-dl** is perfectly capable of working out that information for itself. - -If you need to download videos directly, for any reason, you can: - -- Firstly, fetch the list of videos, for example by clicking **Operations > Check all** -- Click **Edit > System preferences... > Operations > Custom** -- 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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If **Tartube** can't download a video from YouTube, it's sometimes possible to obtain it from an alternative website instead. - -- Click **Edit > System preferences... > Operations > Custom** -- Click **In custom downloads, obtain the video from HookTube rather than YouTube** to select it -- Alternatively click **In custom downloads, obtain the video from Invidious rather than YouTube** to select it -- You can now start the custom download - -HookTube/Invidious can only handle requests for videos, not whole channels or playlists. You should normally enable independent downloads as well. - -6.13.3 Delays between downloads -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If a video website is complaining that you are downloading videos too quickly, it's possible to add a delay betwen downloads. The delay can be of a fixed or random duration. - -- Click **Edit > System preferences... > Operations > Custom** -- Click **In custom downloads, apply a delay after each video/channel/playlist download** to select it -- Select the maximum delay -- If you also set a minimum delay, **Tartube** uses a random value between these two numbers -- You can now start the custom download - -The delay is applied after downloading a channel or a playlist. If you want to apply the delay after each video, you should enable independent downloads as well. - -6.14 Watching videos --------------------- - -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 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. - -6.15 Filtering and finding videos ---------------------------------- - -Beneath the videos you'll find a toolbar. The buttons are self-explanatory, except for the one on the right. - -.. image:: screenshots/example18.png - :alt: The video catalogue toolbar - -Click that button, and a second row of buttons is revealed. You can use these buttons to filter out videos, change the order in which videos are displayed, or find a video uploaded at a certain date. - -.. image:: screenshots/example19.png - :alt: The toolbar's hidden buttons revealed - -- Click the **Sort by** button to sort the videos alphabetically -- Click the button again to sort the videos by date of upload -- Click the **Find date** button to select a date. If there are more videos than will fit on a single page, **Tartube** will show the page containing the videos uploaded closest to this date - -You can search for videos by applying a filter. For example, you could search for videos whose name contains the word **PewDiePie**: - -- In the **Filter** box, type **pewdiepie** -- The search is case-insensitive, so it doesn't matter if you type **PewDiePie** or **pewdiepie** -- Click the magnifiying glass button. All matching videos are displayed -- Click the cancel button next it to remove the filter - -You can search using a *regular expression* (regex), too. These searches are also case-insensitive. For example, to find all videos whose name begins with the word "village": - -- In the **Filter** box, type **\^village** -- Click the **Regex** button to select it -- Click the magnifying glass button. All matching videos are displayed -- To search using ordinary text, rather than a regex, de-select the **Regex** button - -6.16 Marking videos -------------------- - -You can mark videos, channels, playlists and folders that you find interesting, or which are important. - -- You can **bookmark** a video -- You can **favourite** a channel, playlist or folder - -Bookmarked and favourite videos shouldn't be confused with archived videos, which are protected from automatic deletion - see `6.18 Archiving videos`_. - -6.16.1 Bookmarked videos -~~~~~~~~~~~~~~~~~~~~~~~~ - -There are several ways to bookmark a video. - -- Right-click a video, and click **Video is bookmarked** to select it -- If the **Bookmarked** label is visible under the video's name, click it -- Right-click a channel, and select **Channel contents > Mark as bookmarked**. This will bookmark every video in the channel, but it won't bookmark videos that are added to the channel later -- (This can also be done with playlists and folders) - -A bookmarked video appears in **Tartube**'s own **Bookmarks** folder, as well as in its usual location. - -6.16.2 Favourite channels, playlists and folders -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When you mark a channel, playlist or folder as a favourite, all of its videos will also be visible in **Tartube**'s own **Favourite Videos** folder. - -If new videos are later added to the channel, playlist or folder, they will automatically appear in the **Favourite Videos** folder. - -(It's possible to mark or unmark an individual video as a favourite, but it's better to use bookmarking for that.) - -- Right-click a channel, and select **Channel contents > Mark as favourite** -- Right-click a playlist, and select **Playlist contents > Mark as favourite** -- Right-click a folder, and select **Folder contents > All contents > Mark as favourite** -- If you just want to mark a folder's videos as favourite, and not any channels or playlists it contains, select **Folder contents > Just folder videos > Mark as favourite** - -6.17 Combining channels, playlists and folders ----------------------------------------------- - -**Tartube** can download videos from several channels and/or playlists into a single directory (folder) on your computer's hard drive. There are three situations in which this might be useful: - -- A channel has several playlists. You have added both the channel and its playlists to **Tartube**'s database, but you don't want to download duplicate videos -- A creator releases their videos on **BitChute** as well as on **YouTube**. You have added both channels, but you don't want to download duplicate videos -- You don't care about keeping videos in separate directories/folders on your filesystem. You just want to download all videos to one place - -6.17.1 Combining one channel and many playlists -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A creator might have a single channel, and several playlists. The playlists contain videos from that channel (but not necessarily *every* video). - -You can add the channel and its playlists in the normal way but, if you do, **Tartube** will download many videos twice. - -The solution is to tell **Tartube** to store all the videos from the channel and its playlists in a single location. In that way, you can still see a list of videos in each playlist, but duplicate videos are not actually downloaded to your filesystem. - -- Click **Media > Add channel**..., and then enter the channel's details -- Click **Media > Add playlist**... for each playlist -- Now, right-click on each playlist in turn and select **Playlist actions > Set download destination...** -- In the dialogue window, click **Choose a different directory/folder**, select the name of the channel, then click the **OK button** - -6.17.2 Combining channels from different websites -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A creator might release their videos on **YouTube**, but also on a site like **BitChute**. Sometimes they will only release a particular video on **BitChute**. - -You can add both channels in the normal way but, if you do, **Tartube** will download many videos twice. - -The solution is to tell **Tartube** to store videos from both channels in a single location. In that way, you can still see a list of videos in each channel, but duplicate videos are not actually downloaded to your filesystem. - -- Click **Media > Add channel**..., and then enter the **YouTube** channel's details -- Click **Media > Add channel**..., and then enter the **BitChute** channel's details -- Right-click the **BitChute** channel and select **Channel actions > Set download destination...** -- In the dialogue window, click **Choose a different directory/folder**, select the name of the **YouTube** channel, then click the **OK button** - -It doesn't matter which of the two channels you use as the download destination. There is also no limit to the number of parallel channels, so if a creator uploads videos to a dozen different websites, you can add them all. - -6.17.3 Download all videos to a single folder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you don't care about keeping videos in separate directories/folders on your filesystem, you can download *all* videos into the **Unsorted videos** folder. Regardless of whether you have added one channel or a thousand, all the videos will be stored in that one place. - -- Click **Edit > General download options... > Files > Filesystem** -- Click the **Download all videos into this folder** button to select it -- In the combo next to it, select **Unsorted Videos** - -Alternatively, you could select **Temporary Videos**. If you do, videos will be deleted when you shut down **Tartube** (and will not be re-downloaded in the future). - -6.18 Archiving videos ---------------------- - -You can tell **Tartube** to automatically delete videos after some period of time. This is useful if you don't have an infinitely large hard drive. - -- Click **Edit > System preferences... > Filesystem > Video Deletion** -- Click the **Automatically delete downloaded videos after this many days** button to select it -- If you want to, change the number of days from 30 to some other value - -If you want to protect your favourite videos from being deleted automatically, you can *archive* them. Only videos that have actually been downloaded can be archived. - -- Right-click a video, and select **Video is archived** - -You can also archive all the videos in a channel, playlist or folder. - -- For example, right-click a folder and select **Channel contents > Mark videos as archived** -- This action applies to *all* videos that are *currently* in the folder, including the contents of any channels and playlists in that folder -- It doesn't apply to any videos you might download in the future - -6.19 Managing databases ------------------------ - -**Tartube** downloads all of its videos into a single directory (folder) - the **Tartube data directory**. The contents of this directory comprise the **Tartube database**. - -*You should not use this directory (folder) for any other purpose*. - -**Tartube** stores important files here, some of which are invisible (by default). Don't let other applications store their files here, too. - -*You can modify the contents of the directory yourself, if you want, but don't do it while **Tartube** is running.* - -It's fine to add new videos to the database, or to remove them. Just be careful that you don't delete any sub-directories (folders), including those which are hidden, and don't modify the **Tartube** database file, **tartube.db**. - -6.19.1 Importing videos from other applications -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Tartube** is a GUI front-end for `youtube-dl `__, but it is not the only one. If you've downloaded videos using another application, this is how to add them to **Tartube**'s database. - -- In **Tartube**'s main window, add each channel and playlist in the normal way -- When you're ready, click the **Check all** button. This adds a list of videos to **Tartube**'s database, without actually downloading the videos themselves -- Copy the video files into **Tartube**'s data directory (folder). For example, copy all your **PewDiePie** videos into **../tartube-data/downloads/PewDiePie** -- In the **Tartube** menu, click **Operations > Refresh database**. **Tartube** will search for video files, and try to match them with the contents of its database -- The whole process might some time, so be patient - -6.19.2 Multiple databases -~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Tartube** can only use one database at a time, but you can create as many as you want. - -For example, if you've just bought an external hard drive, you can create a new database on that hard drive. - -- In the main menu, click **File > Database preferences...** -- In the new window, click the **Change** button -- Another new window appears. Use it to create a directory (folder) on your external hard drive - -**Tartube** remembers the location of the databases it has loaded. To switch back to your original database: - -- In the main menu, click **File > Database preferences...** -- In the list, click the path to the original database to select it -- Click the **Switch** button - -6.19.3 Multiple Tartubes -~~~~~~~~~~~~~~~~~~~~~~~~ - -**Tartube** can't load more than one database, but you can run as many instances of **Tartube** as you want. - -If you have added three databases to the list, and if you have three **Tartube** windows open at the same time, then by default each window will be using a different database. - -By default, the databases are loaded in the order they appear in the list. - -6.19.4 Exporting/importing the database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can export the contents of **Tartube**'s database and, at any time in the future, import that information into a different **Tartube** database, perhaps on a different computer. - -It is important to note that *only a list of videos, channels, playlists, folders are exported*. The videos themselves are not exported, and neither are any thumbnail, description or metadata files. - -- Click **Media > Export from database** -- In the dialogue window, choose what you want to export -- If you want a list of videos, channels and playlists that you can edit by hand, select the **Export as plain text** option -- Click the **OK** button, then select where to save the export file - -It is safe to share this export file with other people. It doesn't contain any personal information. - -This is how to import the data into a different **Tartube** database. - -- Click **Media > Import into database > JSON export file** or **Media > Import into database > Plain text export file** -- Select the export file you created earlier -- A dialogue window will appear. You can choose how much of the database you want to import - -6.20 Converting to audio +5.1 Creating a programme ------------------------ -**Tartube** can automatically extract the audio from its downloaded videos, if that's what you want. +The first step is to create a programme. -The first step is to make sure that either FFmpeg or AVconv is installed on your system - see `6.4 Setting the location of FFmpeg / AVconv`_. +- Click **Programmes > New programme...** +- Enter a name for the programme. Each programme must have a unique name +- Click OK to create the programme -The remaining steps are simple: +5.2 Customising the programme +----------------------------- -- In **Tartube**'s main window, click **Edit > General download options...** +Immediately after creating a programme, a new window appears. In this window you can customise the programme. -In the new window, if the **Sound only** tab is visible, do this: +Add the first set. If you want the first set to begin immediately, do this: -- Click the **Sound Only** tab -- Select the checkbox **Download each video, extract the sound, and then discard the original videos** -- In the boxes below, select an audio format and an audio quality -- Click the **OK** button at the bottom of the window to apply your changes +- In the box marked **Time (in seconds)**, add the number 0 +- In the box marked **Message**, add a message like **Squat set 1** +- If you want to add a sound effect, click the drop-down box and select one of the effects. There are twenty-five to choose from +- Click the **Add message** button -If the **Post-process** tab is visible, do this: +If you want the first set to begin after a delay, do this: -- Click on the **Post-process** tab -- Select the checkbox **Post-process video files to convert them to audio-only files** -- If you want, click the button **Keep video file after post-processing it** to select it -- In the box labelled **Audio format of the post-processed file**, specify what type of audio file you want - **.mp3**, **.wav**, etc -- Click the **OK** button at the bottom of the window to apply your changes +- In the box marked **Time (in seconds)**, add a number in seconds. For example, add 60 for a one-minute delay +- In the box marked **Message**, add a message like **Squat set 1** +- If you want to add a sound effect, click the drop-down box and select one of the effects. There are twenty-five to choose from +- Click the **Add message** button -N.B. Many video websites, such as **YouTube**, allow you to download the audio (in **.m4a** format) directly, without downloading the whole video, and without using FFmpeg or AVconv. +.. image:: screenshots/gymbob2.png + :alt: The edited workout programme -- In **Tartube**'s main window, click **Edit > General download options... > Formats** -- In the list on the left-hand side, select an **.m4a** format -- 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 +You can repeat this step as often as you like. (There is no limit to the length of a programme). + +- In the box marked **Time (in seconds)**, add a non-zero delay +- In the box marked **Message**, add a message like **Squat set 2** +- Click the **Add message** button -7. Frequently-Asked Questions -============================= +5.3 Saving the programme +------------------------ -**Q: I can't install Tartube / I can't run Tartube / Tartube doesn't work properly / Tartube keeps crashing!** +At the bottom of this window you'll see four buttons. -A: Please report any problems to the authors at our `Github page `__ +- The **OK** button saves your changes, and closes the window +- The **Cancel** button ignores your changes, and closes the window +- The **Apply** button saves your changes, but doesn't close the window +- The **Reset** button removes your changes, and doesn't close the window -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. +5.4 Modifying the programme +--------------------------- -If crashes are a problem, you can force **Tartube** to restrict those cosmetic features, regardless of your current Gtk library. +You can edit the current programme at any time (click **Programmes > Edit current programme...**) -- Click **Edit > System preferences... > General > Modules** -- Click **Assume that Gtk is broken, and disable some minor features** to select it +If that programme is currently running (in other words, if you have clicked the **START** button), any changes you make to the programme won't be applied until after you have clicked the **RESET** button, and then the **START** button again. -**Q: When I try to download videos, nothing happens! In the Errors/Warnings tab, I can see "Download did not start"!** +In the edit window: -A: See `6.3 Setting youtube-dl's location`_ +- You can modify any step of the programme by clicking on it, typing new values for the time, message, and/or sound effect, and clicking the **Update message** button +- You can delete a step by clicking on it, and clicking the **Delete message** button +- You can change the order of the steps by clicking on one step, and then by clicking on the **Move up** and **Move down** buttons -**Q: I can't download my favourite video!** +5.5 Deleting a programme +------------------------ -A: Make sure **youtube-dl** is updated; see `6.2 Check youtube-dl is updated`_ +You can delete a programme by clicking **Programmes > Delete programme...** -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: +This deletes not just the programme in memory, but the file saved on your hard drive. -**youtube-dl ** +Deletion is permanent, so it's a good idea to make backup copies of your programmes. **GymBob** stores its programme files in an (invisible) directory called **../.gymbob**. -...where **\** is the address of the video. If the video downloads successfully, then it's a **Tartube** problem that you can report. If it doesn't download, you should submit a bug report to the authors of `youtube-dl `__ instead. +5.6 Setting the current programme +--------------------------------- -Because most people don't like typing, **Tartube** offers a shortcut. +If you've created multiple programmes, **GymBob** will load them all into memory. The *current* programme is the one that comes first in alphabetical order. -- Click **Operations > Test youtube-dl**, or right-click a video, and select **Downloads > Test system command** -- In the dialogue window, enter the address (URL) of the video -- You can add more **youtube-dl** download options, if you want. See `here `__ for a complete list of them -- Click the **OK** button to close the window and begin the test -- 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 +The name of the current programme is visible in the window's title bar. -**Q: After I downloaded some videos, Tartube crashed, and now all my videos are missing!** +To switch to a different programme, click **Programmes > Switch programme...** -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. +5.7 Running a programme +----------------------- -- Open the data directory (folder). If you're not sure where to find **Tartube**'s data directory , you can click **Edit > System preferences... > Filesystem > Database** -- Make sure **Tartube** is not running. The **Tartube** window is sometimes minimised, and sometimes only visible in the system tray. A good way to make sure is to run **Tartube**, then close it by clicking **File > Quit** -- In the data directory is the broken **tartube.db** file. You should rename to something else, in case you want to examine it later -- In the same directory, you might be able to see a directory called **.backups** -- If **.backups** is not visible, then it is hidden. (On many Linux/BSD system, pressing **CTRL + H** will reveal hidden folders) -- Inside the **.backups** directory, you'll find some backup copies of the database file -- Choose the most recent one, copy it into the directory above, and rename the copy as **tartube.db**, replacing the old broken file -- Restart **Tartube** -- Click the **Check All** button. **Tartube** will update its database with any videos you've downloaded that were not in the backup database file +Use the **START** button to start the current programme. -**Tartube** can make more frequent backups of your database file, if you want. See the options in **Edit > System preferences... > Filesystem > Backups**. +Use the **STOP** button to pause the current programme, and then use the **START** button to resume it. -Note that **Tartube** does not create backup copies of the videos you've downloaded. That is your responsibility! +If you want to start the current programme again from the beginning, or if you want to switch to a different programme, first click the **RESET** button. -**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. - -You can drastically reduce the time this takes by telling **Tartube** to stop checking/downloading videos, if it receives (for example) notifications for three videos it has already checked/downloaded. - -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** -- 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!** - -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?** - -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. - -If you installed an earlier version of **Tartube**, and if you want to move your channels and playlists out of **../tartube-data/downloads**, this is how to do it: - -- Open the data directory (folder). If you're not sure where to find **Tartube**'s data directory, you can click **Edit > System preferences... > Filesystem > Database**. -- Make sure **Tartube** is not running. The **Tartube** window is sometimes minimised, and sometimes only visible in the system tray. A good way to make sure is to run **Tartube**, then close it by clicking **File > Quit** -- Now open the **downloads** directory -- Move everything inside that directory into the directory above, e.g. move everything from **../tartube-data/downloads** into **../tartube-data** -- Delete the empty **downloads** directory -- You can now restart **Tartube** - -**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!** - -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** - -A: In the main menu, click **Edit > System preferences... > Windows > Main window > Show smaller icons in the Video Index** to select it - -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!** - -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!** - -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: Why is the Windows installer so big?** - -**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. - -If you're at all suspicious that such a small application uses such a large installer, you are invited to examine the installed files for yourself: - -**C:\\Users\\YOURNAME\\AppData\\Local\\Tartube** - -(You might need to enable hidden folders; this can be done from the Control Panel.) - -Everything is copied into this single folder. The installer doesn't modify the Windows registry, nor does it copy files anywhere else (other than to the desktop and the Start Menu). - -The NSIS scripts used to create the installers can be found here: - -**C:\\Users\\YOURNAME\\AppData\\Local\\Tartube\\msys64\\home\\user\\tartube\\nsis** - -The scripts contain full instructions, so you should be able to create your own installer, and compare it with the official one. - -8. Contributing -=============== +6 Contributing +============== - Report a bug: Use the Github - `issues `__ page + `issues `__ page -9. Authors -========== +7 Authors +========= See the `AUTHORS `__ file. -10. License -=========== +8 License +========= + +**GymBob** is licensed under the `GNU General Public License v3.0 `__. -**Tartube** is licensed under the `GNU General Public License v3.0 `__. ✨🍰✨ diff --git a/__init__.py b/__init__.py index ef26739..3123f42 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -name = "tartube" +name = "gymbob" diff --git a/docs/empty.md b/docs/empty.md index a3e0248..77769a8 100644 --- a/docs/empty.md +++ b/docs/empty.md @@ -1 +1 @@ -#Tartube \ No newline at end of file +#GymBob \ No newline at end of file diff --git a/gymbob/editwin.py b/gymbob/editwin.py new file mode 100644 index 0000000..2c48bd7 --- /dev/null +++ b/gymbob/editwin.py @@ -0,0 +1,758 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 A S Lewis +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + + +"""Edit window classes.""" + + +# Import Gtk modules +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GObject, GdkPixbuf + + +# Import other modules +import re + + +# Import our modules +# ... + + +# Classes + + +class ProgEditWin(Gtk.Window): + + """Python class for an 'edit window' to modify values in a gymprog.GymProg + object. + + Args: + + app_obj (mainapp.TartubeApp): The main application object + + edit_obj (gymprog.GymProg): The object whose attributes will be edited + in this window + + """ + + + def __init__(self, app_obj, edit_obj): + + Gtk.Window.__init__(self, title='Edit \'' + edit_obj.name + '\'') + + # IV list - class objects + # ----------------------- + # The mainapp.GymBobApp object + self.app_obj = app_obj + # The gymprog.GymProg object being edited + self.edit_obj = edit_obj + + + # IV list - Gtk widgets + # --------------------- + self.main_grid = None # Gtk.Grid + self.reset_button = None # Gtk.Button + self.apply_button = None # Gtk.Button + self.ok_button = None # Gtk.Button + self.cancel_button = None # Gtk.Button + + self.treeview = None # Gtk.TreeView + self.liststore = None # Gtk.ListStore + + + # IV list - other + # --------------- + # Size (in pixels) of gaps between edit window widgets + self.spacing_size = self.app_obj.default_spacing_size + + # When the user changes a value, it is not applied to self.edit_obj + # immediately; instead, it is stored temporarily in this dictionary + # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the + # window, the changes are applied to self.edit_obj + # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary + # is emptied and the changes are lost + # The key-value pairs in the dictionary correspond directly to the + # names of attributes, and their values in self.edit_obj + # Key-value pairs are added to this dictionary whenever the user makes + # a change (so if no changes are made when the window is closed, the + # dictionary will still be empty) + self.edit_dict = {} + + + # Code + # ---- + + # Set up the edit window + self.setup() + + + # Public class methods + + + def setup(self): + + """Called by self.__init__(). + + Sets up the edit window when it opens. + """ + + # Set the default window size + self.set_default_size( + self.app_obj.edit_win_width, + self.app_obj.edit_win_height, + ) + + # Set the window's Gtk icon list + self.set_icon_list(self.app_obj.main_win_obj.icon_pixbuf_list) + + # Set up the window's containing box + self.main_grid = Gtk.Grid() + self.add(self.main_grid) + + # Set up main widgets + self.setup_tab() + self.setup_button_strip() + self.setup_gap() + + # Procedure complete + self.show_all() + + # Inform the main window of this window's birth (so that GymBox doesn't + # the clock to start until all configuration windows have closed) + self.app_obj.main_win_obj.add_child_window(self) + # Add a callback so we can inform the main window of this window's + # destruction + self.connect('destroy', self.close) + + + def setup_tab(self): + + """Called by self.setup(). + + Sets up all widgets (except the button strip at the bottom of the + window). + """ + + mini_grid = Gtk.Grid() + self.main_grid.attach(mini_grid, 0, 0, 1, 1) + mini_grid.set_border_width(self.spacing_size) + mini_grid.set_column_spacing(self.spacing_size) + mini_grid.set_row_spacing(self.spacing_size) + + # Add a treeview + frame = Gtk.Frame() + mini_grid.attach(frame, 0, 0, 5, 1) + frame.set_hexpand(True) + frame.set_vexpand(True) + + scrolled = Gtk.ScrolledWindow() + frame.add(scrolled) + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + self.treeview = Gtk.TreeView() + scrolled.add(self.treeview) + self.treeview.set_headers_visible(True) + + for i, column_title in enumerate( ['#', 'Time', 'Message', 'Sound'] ): + + renderer_text = Gtk.CellRendererText() + column_text = Gtk.TreeViewColumn( + column_title, + renderer_text, + text=i, + ) + self.treeview.append_column(column_text) + + self.treeview_reset() + self.treeview_refill() + + # Add editing widgets beneath the treeview + label = Gtk.Label('Time (in seconds)') + mini_grid.attach(label, 0, 1, 1, 1) + + entry = Gtk.Entry() + mini_grid.attach(entry, 1, 1, 2, 1) + entry.set_hexpand(True) + entry.set_max_width_chars(4) + + label2 = Gtk.Label('Message') + mini_grid.attach(label2, 0, 2, 1, 1) + + entry2 = Gtk.Entry() + mini_grid.attach(entry2, 1, 2, 4, 1) + entry2.set_hexpand(True) + + label3 = Gtk.Label('Sound (optional)') + mini_grid.attach(label3, 0, 3, 1, 1) + + combostore = Gtk.ListStore(str) + combostore.append('') + for sound_file in self.app_obj.sound_list: + combostore.append( [sound_file] ) + + combo = Gtk.ComboBox.new_with_model(combostore) + mini_grid.attach(combo, 1, 3, 2, 1) + renderer_text = Gtk.CellRendererText() + combo.pack_start(renderer_text, True) + combo.add_attribute(renderer_text, 'text', 0) + combo.set_entry_text_column(0) + combo.set_active(0) + + button = Gtk.Button('Add message') + mini_grid.attach(button, 0, 4, 1, 1) + button.connect( + 'clicked', + self.on_button_add_clicked, + entry, + entry2, + combo, + ) + + button2 = Gtk.Button('Update message') + mini_grid.attach(button2, 1, 4, 1, 1) + button2.connect( + 'clicked', + self.on_button_update_clicked, + entry, + entry2, + combo, + ) + + button3 = Gtk.Button('Delete message') + mini_grid.attach(button3, 2, 4, 1, 1) + button3.connect('clicked', self.on_button_delete_clicked) + + button4 = Gtk.Button('Move up') + mini_grid.attach(button4, 3, 4, 1, 1) + button4.connect('clicked', self.on_button_move_up_clicked) + + button5 = Gtk.Button('Move down') + mini_grid.attach(button5, 4, 4, 1, 1) + button5.connect('clicked', self.on_button_move_down_clicked) + + + def setup_button_strip(self): + + """Called by self.setup(). + + Creates a strip of buttons at the bottom of the window. Any changes the + user has made are applied by clicking the 'OK' or 'Apply' buttons, and + cancelled by using the 'Reset' or 'Cancel' buttons. + + The window is closed by using the 'OK' and 'Cancel' buttons. + """ + + hbox = Gtk.HBox() + self.main_grid.attach(hbox, 0, 1, 1, 1) + + # 'Reset' button + 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', + ); + self.reset_button.connect('clicked', self.on_button_reset_clicked) + + # 'Apply' button + 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', + ); + self.apply_button.connect('clicked', self.on_button_apply_clicked) + + # 'OK' button + 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.connect('clicked', self.on_button_ok_clicked) + + # 'Cancel' button + 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.connect( + 'clicked', + self.on_button_cancel_clicked, + ) + + + def setup_gap(self): + + """Called by self.setup(). + + Adds an empty box beneath the button strip for aesthetic purposes. + """ + + hbox = Gtk.HBox() + self.main_grid.attach(hbox, 0, 2, 1, 1) + hbox.set_border_width(self.spacing_size) + + + def retrieve_val(self, name): + + """Can be called by anything. + + Any changes the user has made are temporarily stored in self.edit_dict. + + Each key corresponds to an attribute in the object being edited, + self.edit_obj. + + If 'name' exists as a key in that dictionary, retrieve the + corresponding value and return it. Otherwise, the user hasn't yet + modified the value, so retrieve directly from the attribute in the + object being edited. + + Args: + + name (str): The name of the attribute in the object being edited + + Returns: + + The original or modified value of that attribute. + + """ + + if name in self.edit_dict: + return self.edit_dict[name] + else: + attrib = getattr(self.edit_obj, name) + return attrib.copy() + + + def treeview_reset(self): + + """Called by self.setup_tab() and several callback functions. + + Creates a model for the Gtk.TreeView to use, replacing any previous + model in use. + """ + + self.liststore = Gtk.ListStore(int, int, str, str) + self.treeview.set_model(self.liststore) + + + def treeview_refill(self): + + """Called by self.setup_tab() and several callback functions, usually + after a call to self.treeview_reset(). + + Fills the Gtk.Treeview with data. + """ + + msg_group_list = self.retrieve_val('msg_group_list') + + count = 0 + for mini_list in msg_group_list: + + count += 1 + mod_list = mini_list.copy() + mod_list.insert(0, count) + self.liststore.append(mod_list) + + + def check_entries(self, entry, entry2): + + """Called by self.on_button_add_clicked() and + .on_button_update_clicked(). + + Extracts the value of the two Gtk.Entrys. If either value is invalid, + displays an error message. + + Args: + + entry, entry2 (Gtk.Entry): The entry boxes to check + + Return values: + + time (int), msg (str): The contents of the boxes, or (None, None) + if either value is invalid + + """ + + time = entry.get_text() + msg = entry2.get_text() + + if time == '' or not re.search('^\d+$', time): + + msg_dialogue_win = Gtk.MessageDialog( + self, + 0, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, + 'Invalid time value (must\nbe an integer in seconds)', + ) + msg_dialogue_win.run() + msg_dialogue_win.destroy() + return None, None + +# elif msg == '': +# +# msg_dialogue_win = Gtk.MessageDialog( +# self, +# 0, +# Gtk.MessageType.ERROR, +# Gtk.ButtonsType.OK, +# 'Invalid message (must\ncontain some characters)', +# ) +# msg_dialogue_win.run() +# msg_dialogue_win.destroy() +# return None, None + + else: + + return time, msg + + + def apply_changes(self): + + """Called by self.on_button_ok_clicked() and + self.on_button_apply_clicked(). + + Any changes the user has made are temporarily stored in self.edit_dict. + Apply to those changes to the object being edited. + """ + + # Apply any changes the user has made + for key in self.edit_dict.keys(): + setattr(self.edit_obj, key, self.edit_dict[key]) + + # The changes can now be cleared + self.edit_dict = {} + + # Save the programme file + self.app_obj.save_prog(self.edit_obj) + + + def close(self, also_self): + + """Called from callback in self.setup(). + + Inform the main window that this window is closing. + + Args: + + also_self (editwin.ProgEditWin): Another copy of self + + """ + + self.app_obj.main_win_obj.del_child_window(self) + + + # (Callbacks) + + + def on_button_add_clicked(self, button, entry, entry2, combo): + + """Called from a callback in self.setup_tab(). + + Adds a message to the treeview. + + Args: + + button (Gtk.Button): The widget clicked + + entry, entry2 (Gtk.Entry): The contents of these entry boxes are + aded to the treeview (and the gymprog.GymProg programme) + + combo (Gtk.ComboBox): The contents of this combobox is added to the + treeview + + """ + + time, msg = self.check_entries(entry, entry2) + + tree_iter = combo.get_active_iter() + model = combo.get_model() + filename = model[tree_iter][0] + + if time is not None: + + msg_group_list = self.retrieve_val('msg_group_list') + + # mini_list is in the form + # (time_in_seconds, message, optional_sound_file) + mini_list = [int(time), str(msg), str(filename)] + msg_group_list.append(mini_list) + self.edit_dict['msg_group_list'] = msg_group_list + + mini_list2 = [ + len(msg_group_list), int(time), str(msg), str(filename), + ] + self.liststore.append(mini_list2) + + + def on_button_apply_clicked(self, button): + + """Called from a callback in self.setup_button_strip(). + + Applies any changes made by the user, but doesn't close the window. + + Args: + + button (Gtk.Button): The widget clicked + + """ + + # Apply any changes the user has made + self.apply_changes() + + + def on_button_cancel_clicked(self, button): + + """Called from a callback in self.setup_button_strip(). + + Destroys any changes made by the user and closes the window. + + Args: + + button (Gtk.Button): The widget clicked + + """ + + # Destroy the window + self.destroy() + + + def on_button_delete_clicked(self, button): + + """Called from a callback in self.setup_tab(). + + Deletes a message from the treeview. + + Args: + + button (Gtk.Button): The widget clicked + + """ + + selection = self.treeview.get_selection() + (model, iter) = selection.get_selected() + if iter is None: + + # Nothing selected + return + + row_num = model[iter][0] + + msg_group_list = self.retrieve_val('msg_group_list') + count = 0 + mod_list = [] + + # mini_list is in the form + # (time_in_seconds, message, optional_sound_file) + for mini_list in msg_group_list: + + count += 1 + if count != row_num: + mod_list.append(mini_list) + + self.edit_dict['msg_group_list'] = mod_list + + self.treeview_reset() + self.treeview_refill() + + + def on_button_ok_clicked(self, button): + + """Called from a callback in self.setup_button_strip(). + + Destroys any changes made by the user and then closes the window. + + Args: + + button (Gtk.Button): The widget clicked + + """ + + # Apply any changes the user has made + self.apply_changes() + + # Destroy the window + self.destroy() + + + def on_button_move_down_clicked(self, button): + + """Called from a callback in self.setup_tab(). + + Moves a message one place down in the treeview. + + Args: + + button (Gtk.Button): The widget clicked + + """ + + selection = self.treeview.get_selection() + (model, iter) = selection.get_selected() + if iter is None: + + # Nothing selected + return + + row_num = model[iter][0] + msg_group_list = self.retrieve_val('msg_group_list') + + if row_num < len(msg_group_list): + + count = 0 + mod_list = [] + + # mini_list is in the form + # (time_in_seconds, message, optional_sound_file) + for mini_list in msg_group_list: + + count += 1 + if count != row_num: + mod_list.append(mini_list) + else: + insert_list = mini_list.copy() + + mod_list.insert(row_num, insert_list) + + self.edit_dict['msg_group_list'] = mod_list + + self.treeview_reset() + self.treeview_refill() + + selection = self.treeview.get_selection() + selection.select_path(row_num) + + + def on_button_move_up_clicked(self, button): + + """Called from a callback in self.setup_tab(). + + Moves a message one place up in the treeview. + + Args: + + button (Gtk.Button): The widget clicked + + """ + + selection = self.treeview.get_selection() + (model, iter) = selection.get_selected() + if iter is None: + + # Nothing selected + return + + row_num = model[iter][0] + msg_group_list = self.retrieve_val('msg_group_list') + + if row_num > 1: + + count = 0 + mod_list = [] + + # mini_list is in the form + # (time_in_seconds, message, optional_sound_file) + for mini_list in msg_group_list: + + count += 1 + if count != row_num: + mod_list.append(mini_list) + else: + insert_list = mini_list.copy() + + mod_list.insert(row_num - 2, insert_list) + + self.edit_dict['msg_group_list'] = mod_list + + self.treeview_reset() + self.treeview_refill() + + selection = self.treeview.get_selection() + selection.select_path(row_num - 2) + + + def on_button_reset_clicked(self, button): + + """Called from a callback in self.setup_button_strip(). + + Destroys any changes made by the user and updates the window, showing + self.edit_obj's original values + + Args: + + button (Gtk.Button): The widget clicked + + """ + + # Empty self.edit_dict, destroying any changes the user has made + self.edit_dict = {} + + # Reset the treeview + self.treeview_reset() + self.treeview_refill() + + # Render the changes + self.show_all() + + + def on_button_update_clicked(self, button, entry, entry2, combo): + + """Called from a callback in self.setup_tab(). + + Updates the selected message in the treeview. + + Args: + + button (Gtk.Button): The widget clicked + + entry, entry2 (Gtk.Entry): The contents of these entry boxes are + aded to the treeview, updating the selected line (and the + gymprog.GymProg programme) + + combo (Gtk.ComboBox): The contents of this combobox is added to the + treeview + + """ + + selection = self.treeview.get_selection() + (model, iter) = selection.get_selected() + if iter is None: + + # Nothing selected + return + + row_num = model[iter][0] + time, msg = self.check_entries(entry, entry2) + tree_iter = combo.get_active_iter() + model = combo.get_model() + filename = model[tree_iter][0] + + if time is not None: + + msg_group_list = self.retrieve_val('msg_group_list') + + # mini_list is in the form + # (time_in_seconds, message, optional_sound_file) + mini_list = [int(time), str(msg), str(filename)] + msg_group_list[row_num - 1] = mini_list + self.edit_dict['msg_group_list'] = msg_group_list + + self.treeview_reset() + self.treeview_refill() diff --git a/tartube/tartube b/gymbob/gymbob old mode 100755 new mode 100644 similarity index 68% rename from tartube/tartube rename to gymbob/gymbob index 9ac80da..e1a538a --- a/tartube/tartube +++ b/gymbob/gymbob @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# Copyright (C) 2019-2020 A S Lewis +# Copyright (C) 2020 A S Lewis # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software @@ -17,7 +17,7 @@ # this program. If not, see . -"""Tartube main file.""" +"""Gymbob main file.""" # Import Gtk modules @@ -31,23 +31,23 @@ import importlib.util # Add module directory to path to prevent import issues -spec = importlib.util.find_spec('tartube') +spec = importlib.util.find_spec('gymbob') if spec is not None: sys.path.append(os.path.abspath(os.path.dirname(spec.origin))) - + # Import our modules import mainapp # 'Global' variables -__packagename__ = 'tartube' -__prettyname__ = 'Tartube' -__version__ = '2.0.006' -__date__ = '3 Mar 2020' -__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' +__packagename__ = 'gymbob' +__prettyname__ = 'GymBob' +__version__ = '1.002' +__date__ = '28 Mar 2020' +__copyright__ = 'Copyright \xa9 2020 A S Lewis' __license__ = """ -Copyright \xa9 2019-2020 A S Lewis. +Copyright \xa9 2020 A S Lewis. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -64,18 +64,11 @@ this program. If not, see . __author_list__ = [ 'A S Lewis', ] -__description__ = 'A front-end GUI for youtube-dl,\n' \ -+ 'partly based on youtube-dl-gui\n' \ -+ 'and written in Python 3 / Gtk 3' -__website__ = 'http://tartube.sourceforge.io' -__app_id__ = 'io.sourceforge.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 -__pkg_install_flag__ = False -__pkg_strict_install_flag__ = False +__description__ = 'Simple script to prompt the user during a workout' +__website__ = 'http://gymbob.sourceforge.io' +__app_id__ = 'io.sourceforge.gymbob' -# Start Tartube -app = mainapp.TartubeApp() +# Start Gymbob +app = mainapp.GymBobApp() app.run(sys.argv) diff --git a/gymbob/gymprog.py b/gymbob/gymprog.py new file mode 100644 index 0000000..237760d --- /dev/null +++ b/gymbob/gymprog.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 A S Lewis +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + + +"""Workout programme classes.""" + + +# Import Gtk modules +# ... + + +# Import other modules +# ... + + +# Import our modules +# ... + + +# Classes + + +class GymProg(object): + + """Can be called by anything. + + Python class that handles a workout programme. + + Args: + + name (str): A unique programme name + + msg_group_list (list): List of messages to display in the main window, + in the form described below + + """ + + + # Standard class methods + + + def __init__(self, name, msg_group_list=[]): + + # IV list - other + # --------------- + # A unique programme name + self.name = name + + # The programme is made up of a sequence of messages, displayed after + # fixed intervals + # A message group is a list in the form [int, txt, txt]: + # int: Time in seconds between this message and the previous one + # txt: The message itself (or an empty string to overwrite the + # previous message, leaving no message visible) + # txt: Name of the sound file (.wav or .ogg) in the ../sounds + # folder to play when this message is displayed (or an empty + # string to play no sound) + # The message groups are stored in this sequential list + self.msg_group_list = msg_group_list + diff --git a/gymbob/mainapp.py b/gymbob/mainapp.py new file mode 100644 index 0000000..4b2c909 --- /dev/null +++ b/gymbob/mainapp.py @@ -0,0 +1,901 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 A S Lewis +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + + +"""Main application class.""" + + +# Import Gtk modules +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GObject, GdkPixbuf + + +# Import other modules +from gi.repository import Gio +import datetime +import json +import os +import playsound +import re +import subprocess +import sys +import threading +import time + + +# Import our modules +import editwin +import gymprog +import __main__ +import mainwin + + +# Classes + + +class GymBobApp(Gtk.Application): + + """Main python class for the GymBob application.""" + + + def __init__(self, *args, **kwargs): + + super(GymBobApp, self).__init__( + *args, + application_id=None, + flags=Gio.ApplicationFlags.FLAGS_NONE, + **kwargs) + + # Instance variable (IV) list - class objects + # ------------------------------------------- + # The main window object, set as soon as it's created + self.main_win_obj = None + + + # Instance variable (IV) list - other + # ----------------------------------- + # Default window sizes (in pixels) + self.main_win_width = 550 + self.main_win_height = 400 + # Default size of edit windows (in pixels) + self.edit_win_width = 600 + self.edit_win_height = 400 + # Default size (in pixels) of space between various widgets + self.default_spacing_size = 5 + + # For quick lookup, the directory in which the 'gymbob' executable + # file is found, and its parent directory + self.script_dir = sys.path[0] + self.script_parent_dir = os.path.abspath( + os.path.join(self.script_dir, os.pardir), + ) + # The directory in which workout programmes can be stored (as .json + # files) + self.data_dir = os.path.abspath( + os.path.join( + os.path.expanduser('~'), + __main__.__packagename__ + '-data', + ), + ) + + # List of sound files found in the ../sounds directory + self.sound_list = [] + # So that a sound can be played from within its own thread, the name of + # the sound file to be played is stored here (temporarily) before + # self.play_sound() can be called + self.sound_file = None + + # At all times (after initial setup), a GObject timer runs + # The timer's ID + self.script_timer_id = None + # The timer interval time (in milliseconds) + self.script_timer_time = 100 + + # Flag set to True if the clock is running + self.clock_running_flag = False + # The time (matches time.time()) at which the user clicked the START + # button + self.clock_start_time = 0 + # The time at which the STOP button was clicked. If the START button is + # subsequently clicked, the value of self.clock_start_time is + # adjusted + self.clock_stop_time = 0 + + # Dictionary of gymprog.GymProg objects, each one handling a single + # workout programme. Dictionary in the form + # prog_dict[unique_name] = GymProg object + # ...where 'unique_name' is a string with a maximum length of 16 + self.prog_dict = {} + # The current workout programme object + self.current_prog_obj = None + # Flag set to True when the clock is started (from 0), which starts the + # workout programme + self.current_prog_started_flag = False + # When the clock is started (from 0), the contents of the workout + # programme is copied into this IV + self.current_prog_msg_group_list = [] + # The time (matches the clock time, not time.time()) at which the first + # message in self.current_msg_group_list should be displayed. As soon + # as the message is displayed, it is removed from the list + self.current_prog_next_update_time = None + + # Flag set to True if sound should has been muted + self.mute_sound_flag = False + + + def do_startup(self): + + """Gio.Application standard function.""" + + Gtk.Application.do_startup(self) + + # Menu actions + # ------------ + + # 'GymBob' column + quit_menu_action = Gio.SimpleAction.new('quit_menu', None) + quit_menu_action.connect('activate', self.on_menu_quit) + self.add_action(quit_menu_action) + + # 'Programmes' column + new_prog_menu_action = Gio.SimpleAction.new('new_prog_menu', None) + new_prog_menu_action.connect('activate', self.on_menu_new_prog) + self.add_action(new_prog_menu_action) + + switch_prog_menu_action = Gio.SimpleAction.new( + 'switch_prog_menu', + None, + ) + switch_prog_menu_action.connect('activate', self.on_menu_switch_prog) + self.add_action(switch_prog_menu_action) + + edit_prog_menu_action = Gio.SimpleAction.new('edit_prog_menu', None) + edit_prog_menu_action.connect('activate', self.on_menu_edit_prog) + self.add_action(edit_prog_menu_action) + + delete_prog_menu_action = Gio.SimpleAction.new( + 'delete_prog_menu', + None, + ) + delete_prog_menu_action.connect('activate', self.on_menu_delete_prog) + self.add_action(delete_prog_menu_action) + + # 'Help' column + about_menu_action = Gio.SimpleAction.new('about_menu', None) + about_menu_action.connect('activate', self.on_menu_about) + self.add_action(about_menu_action) + + go_website_menu_action = Gio.SimpleAction.new('go_website_menu', None) + go_website_menu_action.connect('activate', self.on_menu_go_website) + self.add_action(go_website_menu_action) + + # Button actions + # -------------- + + start_button_action = Gio.SimpleAction.new('start_button', None) + start_button_action.connect('activate', self.on_button_start) + self.add_action(start_button_action) + + stop_button_action = Gio.SimpleAction.new('stop_button', None) + stop_button_action.connect('activate', self.on_button_stop) + self.add_action(stop_button_action) + + reset_button_action = Gio.SimpleAction.new('reset_button', None) + reset_button_action.connect('activate', self.on_button_reset) + self.add_action(reset_button_action) + + + def do_activate(self): + + """Gio.Application standard function.""" + + self.start() + + + def do_shutdown(self): + + """Gio.Application standard function.""" + + # Stop immediately + Gtk.Application.do_shutdown(self) + + + # Public class methods + + + def start(self): + + """Called by self.do_activate(). + + Performs general initialisation. + """ + + # Create the main window and make it visible + self.main_win_obj = mainwin.MainWin(self) + self.main_win_obj.show_all() + + # Start the GObject timer, which runs continuously, even when the + # visible clock/stopwatch is not running + self.script_timer_id = GObject.timeout_add( + self.script_timer_time, + self.script_timer_callback, + ) + + # Get a list of available sound files, and sort alphabetically + sound_dir = os.path.abspath( + os.path.join(self.script_parent_dir, 'sounds'), + ) + + for (dirpath, dir_list, file_list) in os.walk(sound_dir): + for filename in file_list: + if filename != 'COPYING': + self.sound_list.append(filename) + + self.sound_list.sort() + + # Create the data directory (in which workout programmes are stored as + # .json files), if it doesn't already exist + if not os.path.isdir(self.data_dir): + os.makedirs(self.data_dir) + + # Load any workout programmes found in the data directory + for (dirpath, dir_list, file_list) in os.walk(self.data_dir): + + # The first file (alphabetically) is the new current workout + # programme + file_list.sort() + for filename in file_list: + if re.search('\.json$', filename): + self.load_prog(filename) + + + def stop(self): + + """Called by self.on_menu_quit(). + + Performs a clean shutdown. + """ + + # I'm outta here! + self.quit() + + + def save_prog(self, prog_obj): + + """Called by self.on_menu_new_prog() and + editwin.ProgEditWin.apply_changes(). + + Saves the specified workout programme as .json file. + + Args: + + prog_obj (gymprog.GymProg): The workout programme to save + + """ + + # Prepare the file's data + utc = datetime.datetime.utcfromtimestamp(time.time()) + file_path = os.path.abspath( + os.path.join(self.data_dir, prog_obj.name + '.json'), + ) + + json_dict = { + # Metadata + 'script_name': __main__.__packagename__, + 'script_version': __main__.__version__, + 'save_date': str(utc.strftime('%d %b %Y')), + 'save_time': str(utc.strftime('%H:%M:%S')), + # Data + 'prog_name': prog_obj.name, + 'prog_msg_group_list': prog_obj.msg_group_list, + } + + # Try to save the file + try: + with open(file_path, 'w') as outfile: + json.dump(json_dict, outfile, indent=4) + + except: + + # Save failed + msg_dialogue_win = Gtk.MessageDialog( + self.main_win_obj, + 0, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, + 'Failed to save the \'' + prog_obj.name + '\' programme', + ) + msg_dialogue_win.run() + msg_dialogue_win.destroy() + + + def load_prog(self, filename): + + """Called by self.start(). + + Loads the named workout programme (a .json file). + + Args: + + filename (str): The name of the workout programme to load, matching + a key in self.prog_dict, and the name of a file in the data + directory + + """ + + # Get the full filepath for the specified file + filepath = os.path.abspath( + os.path.join(self.data_dir, filename), + ) + + # Try to load the file, ignoring any failures + try: + with open(filepath) as infile: + json_dict = json.load(infile) + + except: + return + + # Do some basic checks on the loaded data + if not json_dict \ + or not 'script_name' in json_dict \ + or not 'script_version' in json_dict \ + or not 'save_date' in json_dict \ + or not 'save_time' in json_dict \ + or not 'prog_name' in json_dict \ + or not 'prog_msg_group_list' in json_dict \ + or json_dict['script_name'] != __main__.__packagename__: + return + + # Ignore duplicate names (the file name and programme name should be + # the same, but perhaps the user has been editing it by hand...) + if json_dict['prog_name'] in self.prog_dict: + return + + # File loaded; create a gymprog.GymProg object for it + prog_obj = gymprog.GymProg( + json_dict['prog_name'], + json_dict['prog_msg_group_list'], + ) + + self.prog_dict[json_dict['prog_name']] = prog_obj + + # The first file loaded is the new current programme. (Files are loaded + # in alphabetical order) + if not self.current_prog_obj: + self.current_prog_obj = prog_obj + self.main_win_obj.update_win_title(prog_obj.name) + self.main_win_obj.update_buttons_on_current_prog() + self.main_win_obj.update_menu_items_on_prog() + + + def delete_prog(self, prog_obj): + + """Called by self.on_menu_delete_prog(). + + Deletes the .json file corresponding to the specified workout + programme. + + Args: + + prog_obj (gymprog.GymProg): The workout programme to delete + + """ + + file_path = os.path.abspath( + os.path.join(self.data_dir, prog_obj.name + '.json'), + ) + + # Delete the file + try: + os.remove(file_path) + + except: + + # Deletion failed + msg_dialogue_win = Gtk.MessageDialog( + self.main_win_obj, + 0, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, + 'Failed to delete the \'' + prog_obj.name + '\' programme', + ) + msg_dialogue_win.run() + msg_dialogue_win.destroy() + + + def play_sound(self): + + """Called periodically by self.script_timer_callback(). + + If a sound file has been set, play it (unless sound is muted). + """ + + sound_file = self.sound_file + self.sound_file = None + + # (The value might be None or an empty string) + if not self.mute_sound_flag and sound_file: + + path = os.path.abspath( + os.path.join(self.script_parent_dir, 'sounds', sound_file), + ) + + if os.path.isfile(path): + + playsound.playsound(path) + + + # Callback class methods + + + # (Timers) + + + def script_timer_callback(self): + + """Called by GObject timer created by self.start(), ten times a second. + + Returns: + + 1 to keep the timer going, or None to halt it + + """ + + # (Use the same time value to update all main window widgets) + current_time = time.time() + + # If the programme is running... + if self.clock_running_flag: + + # Update the contents of textviews in the main window + # (De)sensitise menu items and/or buttons + clock_time = current_time - self.clock_start_time + self.main_win_obj.update_clock_textview(clock_time) + + if self.current_prog_next_update_time is not None \ + and self.current_prog_next_update_time <= clock_time \ + and self.current_prog_msg_group_list: + + mini_list = self.current_prog_msg_group_list.pop(0) + self.main_win_obj.update_this_info_textview(str(mini_list[1])) + if mini_list[2]: + self.sound_file = mini_list[2] + + if self.current_prog_msg_group_list: + + # Programme not finished; show the next message as well (in + # a different colour) + next_mini_list = self.current_prog_msg_group_list[0] + self.current_prog_next_update_time += next_mini_list[0] + self.main_win_obj.update_next_info_textview( + next_mini_list[1], + ) + + else: + + # Programme finished + self.current_prog_next_update_time = None + self.main_win_obj.update_countdown_textview('') + self.main_win_obj.update_next_info_textview('') + + if self.current_prog_next_update_time is not None: + + next_time = self.current_prog_next_update_time - clock_time + self.main_win_obj.update_countdown_textview(next_time) + + # Play a sound file, if required. In case of long sounds, perform this + # action inside its own thread + if self.sound_file is not None: + thread = threading.Thread(target=self.play_sound) + thread.start() + + # Return 1 to keep the timer going + return 1 + + + # (Widgets) + + + def on_button_start(self, action, par): + + """Called from a callback in self.do_startup(). + + Starts the clock. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + # Update IVs + self.clock_running_flag = True + if not self.clock_start_time: + self.clock_start_time = time.time() + else: + self.clock_start_time += (time.time() - self.clock_stop_time) + + # (De)sensitise buttons + self.main_win_obj.update_buttons_on_start() + + if not self.current_prog_started_flag: + + # Clock is at 0; start the workout programme + self.current_prog_started_flag = True + self.current_prog_msg_group_list \ + = self.current_prog_obj.msg_group_list.copy() + + # (The workout programme might be empty, so we have to check for + # that) + if self.current_prog_msg_group_list: + + # mini_list is in the form + # (time_in_seconds, message, optional_sound_file) + mini_list = self.current_prog_msg_group_list[0] + self.current_prog_next_update_time = mini_list[0] + + if len(self.current_prog_msg_group_list) > 1: + + # Programme not finished; show the next message as well (in + # a different colour) + next_mini_list = self.current_prog_msg_group_list[0] + self.main_win_obj.update_next_info_textview( + next_mini_list[1], + ) + + # (De)sensitise menu items + self.main_win_obj.update_menu_items_on_prog() + + + def on_button_stop(self, action, par): + + """Called from a callback in self.do_startup(). + + Stops the clock. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + # Update IVs + self.clock_running_flag = False + self.clock_stop_time = time.time() + + # (De)sensitise buttons + self.main_win_obj.update_buttons_on_stop() + + # (De)sensitise menu items + self.main_win_obj.update_menu_items_on_prog() + + + def on_button_reset(self, action, par): + + """Called from a callback in self.do_startup(). + + Resets the clock. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + # Update IVs + self.clock_running_flag = False + self.clock_start_time = 0 + self.clock_stop_time = 0 + + self.current_prog_started_flag = False + self.current_prog_msg_group_list = [] + self.current_prog_next_update_time = None + + # Reset textviews + self.main_win_obj.update_clock_textview('') + self.main_win_obj.update_countdown_textview('') + self.main_win_obj.update_this_info_textview('') + self.main_win_obj.update_next_info_textview('') + + # (De)sensitise buttons + self.main_win_obj.update_buttons_on_reset() + + # (De)sensitise menu items + self.main_win_obj.update_menu_items_on_prog() + + + def on_menu_about(self, action, par): + + """Called from a callback in self.do_startup(). + + Show a standard 'about' dialogue window. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + dialogue_win = Gtk.AboutDialog() + dialogue_win.set_transient_for(self.main_win_obj) + dialogue_win.set_destroy_with_parent(True) + + dialogue_win.set_program_name(__main__.__packagename__.title()) + dialogue_win.set_version('v' + __main__.__version__) + dialogue_win.set_copyright(__main__.__copyright__) + dialogue_win.set_license(__main__.__license__) + dialogue_win.set_website(__main__.__website__) + dialogue_win.set_website_label( + __main__.__prettyname__ + ' website' + ) + dialogue_win.set_comments(__main__.__description__) + dialogue_win.set_logo( + self.main_win_obj.icon_pixbuf_dict['icon_64'], + ) + dialogue_win.set_authors(__main__.__author_list__) + dialogue_win.set_title('') + dialogue_win.connect('response', self.on_menu_about_close) + + dialogue_win.show() + + + def on_menu_about_close(self, action, par): + + """Called from a callback in self.on_menu_about(). + + Close the 'about' dialogue window. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + action.destroy() + + + def on_menu_delete_prog(self, action, par): + + """Called from a callback in self.do_startup(). + + Prompts the user to delete a workout programme. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + # Open a dialogue window + dialogue_win = mainwin.DeleteProgDialogue(self.main_win_obj) + response = dialogue_win.run() + + # Retrieve user choices from the dialogue window... + prog_name = dialogue_win.prog_name + # ...before destroying the dialogue window + dialogue_win.destroy() + + if response == Gtk.ResponseType.OK: + + # Remove the workout programme from the registry + del self.prog_dict[prog_name] + + # If it's the current programme, update some things + if self.current_prog_obj is not None \ + and self.current_prog_obj.name == prog_name: + + # Reset the window title + self.main_win_obj.update_win_title() + + # Delete the corresponding file + self.delete_prog(self.current_prog_obj) + self.current_prog_obj = None + + # Artificially click the RESET button to reset the clock and + # empty the main window of text + self.main_win_obj.reset_button.clicked() + + # (De)sensitise buttons + self.main_win_obj.update_buttons_on_current_prog() + + # Desensitise menu items, if there are no workout programmes left + self.main_win_obj.update_menu_items_on_prog() + + + def on_menu_edit_prog(self, action, par): + + """Called from a callback in self.do_startup(). + + Prompts the user to edit the current workout programme. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + # Open the edit window immediately + editwin.ProgEditWin(self, self.current_prog_obj) + + + def on_menu_go_website(self, action, par): + + """Called from a callback in self.do_startup(). + + Opens the GymBob website. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + uri = __main__.__website__ + + # Open the GymBob home page in the user's default browser + if sys.platform == "win32": + os.startfile(uri) + else: + opener ="open" if sys.platform == "darwin" else "xdg-open" + subprocess.call([opener, uri]) + + + def on_menu_new_prog(self, action, par): + + """Called from a callback in self.do_startup(). + + Prompts the user for the name of a new workout programme. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + # Open a dialogue window + dialogue_win = mainwin.NewProgDialogue(self.main_win_obj) + response = dialogue_win.run() + + # Retrieve user choices from the dialogue window... + prog_name = dialogue_win.entry.get_text() + # ...before destroying the dialogue window + dialogue_win.destroy() + + if response != Gtk.ResponseType.OK: + return + + if prog_name in self.prog_dict: + + # Duplicate programme names are not allowed + msg_dialogue_win = Gtk.MessageDialog( + self.main_win_obj, + 0, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, + 'A programme called \'' + prog_name + '\' already exists!', + ) + msg_dialogue_win.run() + msg_dialogue_win.destroy() + return + + # Create the new workout programme + prog_obj = gymprog.GymProg(prog_name) + self.prog_dict[prog_name] = prog_obj + # Set it as the current programme + self.current_prog_obj = prog_obj + # Save the empty programme, creating a .json file + self.save_prog(prog_obj) + + # Update the main window title to show the current programme + self.main_win_obj.update_win_title(prog_obj.name) + # (De)sensitise menu items and buttons + self.main_win_obj.update_buttons_on_current_prog() + self.main_win_obj.update_menu_items_on_prog() + + # Open an edit window for the new programme immediately + editwin.ProgEditWin(self, prog_obj) + + + def on_menu_switch_prog(self, action, par): + + """Called from a callback in self.do_startup(). + + Prompts the user with a list of workout programmes from which to + select. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + # Open a dialogue window + dialogue_win = mainwin.SwitchProgDialogue(self.main_win_obj) + response = dialogue_win.run() + + # Retrieve user choices from the dialogue window... + prog_name = dialogue_win.prog_name + # ...before destroying the dialogue window + dialogue_win.destroy() + + if response == Gtk.ResponseType.OK: + + # Set the current programme + self.current_prog_obj = self.prog_dict[prog_name] + + # (De)sensitise menu items and buttons + self.main_win_obj.update_buttons_on_current_prog() + self.main_win_obj.update_menu_items_on_prog() + + # Artificially click the RESET button to reset the clock and empty + # the main window of text + self.main_win_obj.reset_button.clicked() + + # Update the main window title to show the current programme + self.main_win_obj.update_win_title(self.current_prog_obj.name) + + + def on_menu_quit(self, action, par): + + """Called from a callback in self.do_startup(). + + Terminates the GymBob app. + + Args: + + action (Gio.SimpleAction): Object generated by Gio + + par (None): Ignored + + """ + + # Terminate the script + self.stop() + + + # Set accessors + + + def set_mute_sound_flag(self, flag): + + if not flag: + self.mute_sound_flag = False + else: + self.mute_sound_flag = True diff --git a/gymbob/mainwin.py b/gymbob/mainwin.py new file mode 100644 index 0000000..f157f10 --- /dev/null +++ b/gymbob/mainwin.py @@ -0,0 +1,998 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 A S Lewis +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + + +"""Main window class.""" + + +# Import Gtk modules +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GObject, Gdk, GdkPixbuf + + +# Import other modules +from gi.repository import Gio +import math +import os + + +# Import our modules +import __main__ +import mainapp + + +# Classes + + +class MainWin(Gtk.ApplicationWindow): + + """Called by mainapp.GymBobApp.start(). + + Python class that handles the main window. + + Args: + + app_obj (mainapp.TartubeApp): The main application object + + """ + + + def __init__(self, app_obj): + + super(MainWin, self).__init__( + title=__main__.__prettyname__, + application=app_obj + ) + + # IV list - class objects + # ----------------------- + # The main application + self.app_obj = app_obj + + + # IV list - Gtk widgets + # --------------------- + # (from self.setup_grid) + self.grid = None # Gtk.Grid + # (from self.setup_menubar) + self.menubar = None # Gtk.MenuBar + self.new_prog_menu_item = None # Gtk.MenuItem + self.switch_prog_menu_item = None # Gtk.MenuItem + self.edit_prog_menu_item = None # Gtk.MenuItem + self.delete_prog_menu_item = None # Gtk.MenuItem + # (from self.setup_win) + self.clock_textview = None # Gtk.TextView + self.countdown_textview = None # Gtk.TextView + self.this_info_textview = None # Gtk.TextView + self.next_info_textview = None # Gtk.TextView + self.start_button = None # Gtk.Button + self.pause_button = None # Gtk.Button + self.stop_button = None # Gtk.Button + self.reset_button = None # Gtk.Button + + + # IV list - other + # --------------- + # Colours to use in the upper/lower textviews, and the font size (in + # points, default value is 10) + self.clock_bg_colour = '#000000' + self.clock_text_colour = '#FFFFFF' + self.clock_font_size = 40 + + self.countdown_bg_colour = '#000000' + self.countdown_text_colour = '#FF0000' + self.countdown_font_size = 40 + + self.this_info_bg_colour = '#000000' + self.this_info_text_colour = '#FFFFFF' + self.this_info_font_size = 30 + + self.next_info_bg_colour = '#000000' + self.next_info_text_colour = '#FF0000' + self.next_info_font_size = 30 + + # Gymbob icon files are loaded into pixbufs, ready for use. Dictionary + # in the form: + # key - a string like 'icon_512' + # value - a Gdk.Pixbuf + self.icon_pixbuf_dict = {} + # The same list of pixbufs in sequential order + self.icon_pixbuf_list = [] + + # List of edit windows (editwin.ProgEditWin objects) that are currently + # open. The clock can't be started if any edit windows are open + self.edit_win_list = [] + + # Code + # ---- + + # Set up icon pixbufs + self.setup_icons() + # Set up the main window + self.setup_win() + + + # Public class methods + + + def setup_icons(self): + + """Called by self.__init__(). + + Sets up pixbufs for GymBob icons. + """ + + # The default location for icons is ../icons + # When installed via PyPI, the icons are moved to ../gymbob/icons + # When installed via a Debian/RPM package, the icons are moved to + # /usr/share/gymbob/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', + ), + ), + ) + + icon_dir_list.append( + os.path.join( + '/', 'usr', 'share', __main__.__packagename__, 'icons', + ) + ) + + self.icon_pixbuf_list = [] + for icon_dir_path in icon_dir_list: + if os.path.isdir(icon_dir_path): + + for size in [16, 24, 32, 48, 64, 128, 256, 512]: + + path = os.path.abspath( + os.path.join( + icon_dir_path, + 'win', + __main__.__packagename__ + '_icon_' + str(size) \ + + '.png', + ), + ) + + pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) + + self.icon_pixbuf_list.append(pixbuf) + self.icon_pixbuf_dict['icon_' + str(size)] = pixbuf + + # Pass the list of pixbufs to Gtk + self.set_icon_list(self.icon_pixbuf_list) + + + def setup_win(self): + + """Called by self.__init__(). + + Sets up the main window, calling various function to create its + widgets. + """ + + spacing = self.app_obj.default_spacing_size + + # Set the default window size + self.set_default_size( + self.app_obj.main_win_width, + self.app_obj.main_win_height, + ) + + # Create main window widgets + self.grid = Gtk.Grid() + self.add(self.grid) + + self.setup_menubar() + + self.clock_textview = self.setup_textview( + 1, + self.clock_bg_colour, + self.clock_text_colour, + self.clock_font_size, + 0, 1, 1, 1, + ) + + self.countdown_textview = self.setup_textview( + 2, + self.countdown_bg_colour, + self.countdown_text_colour, + self.countdown_font_size, + 1, 1, 1, 1, + ) + + self.this_info_textview = self.setup_textview( + 3, + self.this_info_bg_colour, + self.this_info_text_colour, + self.this_info_font_size, + 0, 2, 2, 1, + ) + + self.next_info_textview = self.setup_textview( + 4, + self.next_info_bg_colour, + self.next_info_text_colour, + self.next_info_font_size, + 0, 3, 2, 1, + ) + + self.setup_dummy_textview() + + # Separator + self.grid.attach(Gtk.Separator(), 0, 4, 2, 1) + + hbox = Gtk.HBox() + self.grid.attach(hbox, 0, 5, 2, 1) + hbox.set_border_width(spacing * 2) + hbox.set_vexpand(True) + + self.start_button = Gtk.Button('START') + hbox.pack_start(self.start_button, True, True, spacing) + self.start_button.set_action_name('app.start_button') + # (These buttons are desensitised until a programme is loaded/created) + self.start_button.set_sensitive(False) + + self.stop_button = Gtk.Button('STOP') + hbox.pack_start(self.stop_button, True, True, spacing) + self.stop_button.set_sensitive(False) + self.stop_button.set_action_name('app.stop_button') + self.stop_button.set_sensitive(False) + + self.reset_button = Gtk.Button('RESET') + hbox.pack_start(self.reset_button, True, True, spacing) + self.reset_button.set_action_name('app.reset_button') + self.reset_button.set_sensitive(False) + + + def setup_menubar(self): + + """Called by self.setup_win(). + + Sets up a Gtk.Menu at the top of the main window. + """ + + self.menubar = Gtk.MenuBar() + self.grid.attach(self.menubar, 0, 0, 2, 1) + + # GymBob column + file_menu_column = Gtk.MenuItem.new_with_mnemonic( + '_' + __main__.__prettyname__, + ) + self.menubar.add(file_menu_column) + + file_sub_menu = Gtk.Menu() + file_menu_column.set_submenu(file_sub_menu) + + mute_sound_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( + '_Mute sound', + ) + mute_sound_menu_item.set_active(self.app_obj.mute_sound_flag) + mute_sound_menu_item.connect( + 'activate', + self.on_menu_mute_sound, + ) + file_sub_menu.append(mute_sound_menu_item) + + # Separator + file_sub_menu.append(Gtk.SeparatorMenuItem()) + + 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') + + # Programmes column + edit_menu_column = Gtk.MenuItem.new_with_mnemonic('_Programmes') + self.menubar.add(edit_menu_column) + + edit_sub_menu = Gtk.Menu() + edit_menu_column.set_submenu(edit_sub_menu) + + self.new_prog_menu_item = Gtk.MenuItem.new_with_mnemonic( + '_New programme...', + ) + edit_sub_menu.append(self.new_prog_menu_item) + self.new_prog_menu_item.set_action_name('app.new_prog_menu') + + self.switch_prog_menu_item = Gtk.MenuItem.new_with_mnemonic( + '_Switch programme...', + ) + edit_sub_menu.append(self.switch_prog_menu_item) + self.switch_prog_menu_item.set_action_name('app.switch_prog_menu') + + self.edit_prog_menu_item = Gtk.MenuItem.new_with_mnemonic( + '_Edit current programme...', + ) + edit_sub_menu.append(self.edit_prog_menu_item) + self.edit_prog_menu_item.set_action_name('app.edit_prog_menu') + + # Separator + edit_sub_menu.append(Gtk.SeparatorMenuItem()) + + self.delete_prog_menu_item = Gtk.MenuItem.new_with_mnemonic( + '_Delete programme...', + ) + edit_sub_menu.append(self.delete_prog_menu_item) + self.delete_prog_menu_item.set_action_name('app.delete_prog_menu') + + # Help column + 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...') + 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') + help_sub_menu.append(go_website_menu_item) + go_website_menu_item.set_action_name('app.go_website_menu') + + # (Some menu items are desensitised until a programme is loaded/ + # created) + self.edit_prog_menu_item.set_sensitive(False) + self.switch_prog_menu_item.set_sensitive(False) + self.delete_prog_menu_item.set_sensitive(False) + + + def setup_textview(self, widget_id, bg_colour, text_colour, font_size, \ + x_pos, y_pos, width, height): + + """Called by self.setup_win(). + + Creates one of the main window textviews (there are four in all). + + Args: + + widget_id (int): Unique number for this textview (1-4) + + bg_colour, text_colour (str): The colours to use in this textview + (e.g. '#FFFFFF') + + font_size (int): The font size (in points, e.g. 10) + + x_pos, y_pos, width, height (int): Coordinates on the Gtk3.Grid + + Return values: + + The Gtk.TextView created + + """ + + # Add a textview to the grid, using a css style sheet to provide (for + # example) monospaced white text on a black background + scrolled = Gtk.ScrolledWindow() + self.grid.attach(scrolled, x_pos, y_pos, width, height) + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + frame = Gtk.Frame() + scrolled.add_with_viewport(frame) + + style_provider = self.set_textview_css( + '#css_text_id_' + str(widget_id) \ + + ', textview text {\n' \ + + ' background-color: ' + bg_colour + ';\n' \ + + ' color: ' + text_colour + ';\n' \ + + '}\n' \ + + '#css_label_id_' + str(widget_id) \ + + ', textview {\n' \ + + ' font-family: monospace, monospace;\n' \ + + ' font-size: ' + str(font_size) + 'pt;\n' \ + + '}' + ) + + textview = Gtk.TextView() + frame.add(textview) + textview.set_wrap_mode(Gtk.WrapMode.WORD) + textview.set_editable(False) + textview.set_cursor_visible(False) + textview.set_hexpand(True) + textview.set_vexpand(True) + + context = textview.get_style_context() + context.add_provider(style_provider, 600) + + return textview + + + def setup_dummy_textview(self): + + """Called by self.setup_win(), immediately after calls to + self.setup_textview(). + + Resets css properties for the next Gtk.TextView created (presumably by + another application), so it uses the default style, not the css style + specified in the calls to self.setup_textview(). + """ + + # Create a dummy textview that's not visible in the main window + textview = Gtk.TextView() + style_provider = self.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' \ + + '}' + ) + + context = textview.get_style_context() + context.add_provider(style_provider, 600) + + + def set_textview_css(self, css_string): + + """Called by self.setup_upper_textview() and .setup_lower_textview(). + + Applies a CSS style to the current screen, which is used for the + Gtk.TextView that has just been created. + + Called a third time to create a dummy textview with default properties. + + Args: + + css_string (str): The CSS style to apply + + Returns: + + The Gtk.CssProvider created + + """ + + 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 + ) + + return style_provider + + + # (Update widgets) + + + def update_win_title(self, prog_name=None): + + """Called by various functions. + + Changes the title of the main window. If there is a current programme, + display its name alongside the name of the script. + + Args: + + prog_name (str): The name of a workout programme, matching a key + in self.app_obj.prog_dict + + """ + + if prog_name is not None: + self.set_title(__main__.__prettyname__ + ' [' + prog_name + ']') + else: + self.set_title(__main__.__prettyname__) + + + def update_clock_textview(self, time): + + """Called by various functions. + + Updates the main window textview showing the current time. + + Args: + + time (int): The current time (since the programme begun) in seconds + + """ + + if not time: + self.clock_textview.get_buffer().set_text('') + else: + self.clock_textview.get_buffer().set_text( + self.convert_time_to_string(time), + ) + + + def update_countdown_textview(self, time): + + """Called by various functions. + + Updates the main window textview showing the time until the next + message to be displayed. + + Args: + + time (int): The time in seconds + + """ + + if not time: + self.countdown_textview.get_buffer().set_text('') + else: + self.countdown_textview.get_buffer().set_text( + self.convert_time_to_string(math.ceil(time)), + ) + + + def update_this_info_textview(self, msg): + + """Called by various functions. + + Updates the main window textview with the current message (if any). + + Args: + + msg (str): The message to display (use an empty string to clear the + textview) + + """ + + self.this_info_textview.get_buffer().set_text(str(msg)) + + + def update_next_info_textview(self, msg): + + """Called by various functions. + + Updates the main window textview with the next message (if any). + + Args: + + msg (str): The message to display (use an empty string to clear the + textview) + + """ + + self.next_info_textview.get_buffer().set_text(str(msg)) + + + def update_menu_items_on_prog(self): + + """Called by various functions. + + (De)sensitises menu items depending on whether any workout programmes + exist, or not. + """ + + if self.app_obj.current_prog_started_flag: + self.new_prog_menu_item.set_sensitive(False) + else: + self.new_prog_menu_item.set_sensitive(True) + + if not self.app_obj.prog_dict: + self.edit_prog_menu_item.set_sensitive(False) + else: + self.edit_prog_menu_item.set_sensitive(True) + + if not self.app_obj.prog_dict \ + or self.app_obj.current_prog_started_flag: + self.switch_prog_menu_item.set_sensitive(False) + self.delete_prog_menu_item.set_sensitive(False) + else: + self.switch_prog_menu_item.set_sensitive(True) + self.delete_prog_menu_item.set_sensitive(True) + + + def update_buttons_on_start(self): + + """Called by mainapp.GymBobApp.on_button_start(). + + (De)sensitises buttons after the user clicks the START button. + """ + + self.start_button.set_sensitive(False) + self.stop_button.set_sensitive(True) + self.reset_button.set_sensitive(False) + + + def update_buttons_on_stop(self): + + """Called by mainapp.GymBobApp.on_button_stop(). + + (De)sensitises buttons after the user clicks the STOP button. + """ + + self.start_button.set_sensitive(True) + self.stop_button.set_sensitive(False) + self.reset_button.set_sensitive(True) + + + def update_buttons_on_reset(self): + + """Called by mainapp.GymBobApp.on_button_reset(). + + (De)sensitises buttons after the user clicks the RESET button. + """ + + self.start_button.set_sensitive(True) + self.stop_button.set_sensitive(False) + self.reset_button.set_sensitive(True) + + + def update_buttons_on_current_prog(self): + + """Called by various functions. + + (De)sensitises the START, STOP and RESET buttons, depending on whether + there is a current workout programme, or not. + """ + + if not self.app_obj.current_prog_obj: + self.start_button.set_sensitive(False) + self.stop_button.set_sensitive(False) + self.reset_button.set_sensitive(False) + + else: + self.start_button.set_sensitive(True) + self.stop_button.set_sensitive(True) + self.reset_button.set_sensitive(True) + + + # (Support functions) + + + def convert_time_to_string(self, time): + + """Called by various functions. + + Converts a time value (an integer in seconds) into a formatted string, + e.g. '1:27:02'. + + Args: + + time (int): A value in seconds (0 or above) + + Return values: + + The converted string + + """ + + minutes = int(time / 60) + seconds = int(time % 60) + + hours = int(minutes / 60) + minutes = int(minutes % 60) + + time_str = str(minutes).zfill(2) + ':' + str(seconds).zfill(2) + if hours > 0: + time_str = str(hours) + ':' + time_str + + return time_str + + + def add_child_window(self, edit_win_obj): + + """Called by editwin.ProgEditWin.setup(). + + When an edit window opens, add it to our list of such windows. (A + workout programme will not start while the window(s) are open.) + + Args: + + edit_win_obj (edit.ProgEditWin): The window to add + + """ + + # Check that the window isn't already in the list (unlikely, but check + # anyway) + if not edit_win_obj in self.edit_win_list: + + # Update the IV + self.edit_win_list.append(edit_win_obj) + + + def del_child_window(self, edit_win_obj): + + """Called by editwin.ProgEditWin.close(). + + When an edit window closes, remove it to our list of such windows. + + Args: + + edit_win_obj (edit.ProgEditWin): The window to remove + + """ + + # Update the IV + if edit_win_obj in self.edit_win_list: + self.edit_win_list.remove(edit_win_obj) + + + # Callbacks + + + def on_menu_mute_sound(self, checkbutton): + + """Called from a callback in self.setup_menubar(). + + Mutes (or unmutes) sound effects. + + Args: + + checkbutton (Gtk.CheckButton): The widget clicked. + + """ + + self.app_obj.set_mute_sound_flag(checkbutton.get_active()) + + +class DeleteProgDialogue(Gtk.Dialog): + + """Called by mainapp.GymBobApp.on_menu_switch_prog(). + + Python class handling a dialogue window that prompts the user to delete a + workout programme. + + Args: + + main_win_obj (mainwin.MainWin): The parent main window + + """ + + + def __init__(self, main_win_obj): + + # IV list - Gtk widgets + # --------------------- + self.combo = None # Gtk.ComboBox + + + # IV list - other + # --------------- + self.prog_name = None + self.prog_list = [] + + + # Code + # ---- + + Gtk.Dialog.__init__( + self, + 'Delete programme', + main_win_obj, + Gtk.DialogFlags.DESTROY_WITH_PARENT, + ( + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OK, Gtk.ResponseType.OK, + ) + ) + + self.set_modal(True) + + # Set up the dialogue window + box = self.get_content_area() + + grid = Gtk.Grid() + box.add(grid) + grid.set_border_width(main_win_obj.app_obj.default_spacing_size) + grid.set_row_spacing(main_win_obj.app_obj.default_spacing_size) + + label = Gtk.Label('Select the programme to delete') + grid.attach(label, 0, 0, 1, 1) + + # Import and display a sorted list of workout programmes + self.prog_list = list(main_win_obj.app_obj.prog_dict.keys()) + self.prog_list.sort() + self.prog_name = self.prog_list[0] + + listmodel = Gtk.ListStore(str) + for item in self.prog_list: + listmodel.append([item]) + + self.combo = Gtk.ComboBox.new_with_model(listmodel) + grid.attach(self.combo, 0, 1, 1, 1) + self.combo.set_hexpand(True) + + cell = Gtk.CellRendererText() + self.combo.pack_start(cell, False) + self.combo.add_attribute(cell, 'text', 0) + self.combo.set_active(0) + self.combo.connect('changed', self.on_combo_changed) + + # Display the dialogue window + self.show_all() + + + def on_combo_changed(self, combo): + + """Called from callback in self.__init__(). + + Store the combobox's selected item, so the calling function can + retrieve it. + + Args: + + combo (Gtk.ComboBox): The clicked widget + + """ + + self.prog_name = self.prog_list[combo.get_active()] + + +class NewProgDialogue(Gtk.Dialog): + + """Called by mainapp.GymBobApp.on_menu_new_prog(). + + Python class handling a dialogue window that prompts the user for the name + of a new workout programme. + + Args: + + main_win_obj (mainwin.MainWin): The parent main window + + """ + + + def __init__(self, main_win_obj): + + # IV list - Gtk widgets + # --------------------- + self.entry = None # Gtk.Entry + + + # Code + # ---- + + Gtk.Dialog.__init__( + self, + 'New programme', + main_win_obj, + Gtk.DialogFlags.DESTROY_WITH_PARENT, + ( + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OK, Gtk.ResponseType.OK, + ) + ) + + self.set_modal(True) + + # Set up the dialogue window + box = self.get_content_area() + + grid = Gtk.Grid() + box.add(grid) + grid.set_border_width(main_win_obj.app_obj.default_spacing_size) + grid.set_row_spacing(main_win_obj.app_obj.default_spacing_size) + + label = Gtk.Label('Enter the name of a new workout programme') + grid.attach(label, 0, 0, 1, 1) + + self.entry = Gtk.Entry() + grid.attach(self.entry, 0, 1, 1, 1) + self.entry.set_hexpand(True) + self.entry.set_max_length(16) + + # Display the dialogue window + self.show_all() + + +class SwitchProgDialogue(Gtk.Dialog): + + """Called by mainapp.GymBobApp.on_menu_switch_prog(). + + Python class handling a dialogue window that prompts the user to switch to + a new workout programme. + + Args: + + main_win_obj (mainwin.MainWin): The parent main window + + """ + + + def __init__(self, main_win_obj): + + # IV list - Gtk widgets + # --------------------- + self.combo = None # Gtk.ComboBox + + + # IV list - other + # --------------- + self.prog_name = None + self.prog_list = [] + + + # Code + # ---- + + Gtk.Dialog.__init__( + self, + 'Switch programme', + main_win_obj, + Gtk.DialogFlags.DESTROY_WITH_PARENT, + ( + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OK, Gtk.ResponseType.OK, + ) + ) + + self.set_modal(True) + + # Set up the dialogue window + box = self.get_content_area() + + grid = Gtk.Grid() + box.add(grid) + grid.set_border_width(main_win_obj.app_obj.default_spacing_size) + grid.set_row_spacing(main_win_obj.app_obj.default_spacing_size) + + label = Gtk.Label('Set the new workout programme') + grid.attach(label, 0, 0, 1, 1) + + # Import a sorted list of programmes. The current programme should be + # the first item in the list + import_list = list(main_win_obj.app_obj.prog_dict.keys()) + sorted_list = [] + self.prog_name = main_win_obj.app_obj.current_prog_obj.name + + for item in import_list: + if item != self.prog_name: + self.prog_list.append(item) + + self.prog_list.sort() + self.prog_list.insert(0, self.prog_name) + + listmodel = Gtk.ListStore(str) + for item in self.prog_list: + listmodel.append([item]) + + self.combo = Gtk.ComboBox.new_with_model(listmodel) + grid.attach(self.combo, 0, 1, 1, 1) + self.combo.set_hexpand(True) + + cell = Gtk.CellRendererText() + self.combo.pack_start(cell, False) + self.combo.add_attribute(cell, 'text', 0) + self.combo.set_active(0) + self.combo.connect('changed', self.on_combo_changed) + + # Display the dialogue window + self.show_all() + + + def on_combo_changed(self, combo): + + """Called from callback in self.__init__(). + + Store the combobox's selected item, so the calling function can + retrieve it. + + Args: + + combo (Gtk.ComboBox): The clicked widget + + """ + + self.prog_name = self.prog_list[combo.get_active()] + diff --git a/icons/COPYING b/icons/COPYING index ca9d99b..90a29a3 100644 --- a/icons/COPYING +++ b/icons/COPYING @@ -1,39 +1,9 @@ COPYING -All files in the ../dialogue sub-directory -All files in the ../status sub-directory -All files in the ../win sub-directory +All files in ../win sub-directory -Author: Vectorgraphit -Source: https://www.iconfinder.com/icons/199499/ -Author: bekeen studio -Source: https://www.iconfinder.com/bekeenstudio +Author: Carlo Rodriguez +Source: https://www.iconfinder.com/icons/512534/exercise_fitness_gym_gymnasium_icon -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 ../large sub-directory -All files in the ../msg sub-directory -All files in the ../small sub-directory -All files in the ../toolbar sub-directory - -Author: FatCow Web Hosting -Source: https://www.fatcow.com/free-icons - -This work is licensed under the Creative Commons Attribution 3.0 Generic -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 ../locale directory - -Author: Mr. Hopnguyen -Source: https://www.iconfinder.com/icons/2634450/ - -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. +"Free for commercial use" diff --git a/icons/dialogue/system_icon_64.png b/icons/dialogue/system_icon_64.png deleted file mode 100644 index 9b67966..0000000 Binary files a/icons/dialogue/system_icon_64.png and /dev/null differ diff --git a/icons/dialogue/system_icon_xmas_64.png b/icons/dialogue/system_icon_xmas_64.png deleted file mode 100644 index 862c7d9..0000000 Binary files a/icons/dialogue/system_icon_xmas_64.png and /dev/null differ diff --git a/icons/large/channel_both.png b/icons/large/channel_both.png deleted file mode 100644 index 7955e0e..0000000 Binary files a/icons/large/channel_both.png and /dev/null differ diff --git a/icons/large/channel_left.png b/icons/large/channel_left.png deleted file mode 100644 index c15c03c..0000000 Binary files a/icons/large/channel_left.png and /dev/null differ diff --git a/icons/large/channel_none.png b/icons/large/channel_none.png deleted file mode 100644 index 5e29f7c..0000000 Binary files a/icons/large/channel_none.png and /dev/null differ diff --git a/icons/large/channel_right.png b/icons/large/channel_right.png deleted file mode 100644 index 79dbde0..0000000 Binary files a/icons/large/channel_right.png and /dev/null differ diff --git a/icons/large/copy.png b/icons/large/copy.png deleted file mode 100644 index 7e387d6..0000000 Binary files a/icons/large/copy.png and /dev/null differ diff --git a/icons/large/folder_black_both.png b/icons/large/folder_black_both.png deleted file mode 100644 index 2899f22..0000000 Binary files a/icons/large/folder_black_both.png and /dev/null differ diff --git a/icons/large/folder_black_left.png b/icons/large/folder_black_left.png deleted file mode 100644 index 862410b..0000000 Binary files a/icons/large/folder_black_left.png and /dev/null differ diff --git a/icons/large/folder_black_none.png b/icons/large/folder_black_none.png deleted file mode 100644 index b2171ae..0000000 Binary files a/icons/large/folder_black_none.png and /dev/null differ diff --git a/icons/large/folder_black_right.png b/icons/large/folder_black_right.png deleted file mode 100644 index 21f4d5a..0000000 Binary files a/icons/large/folder_black_right.png and /dev/null differ diff --git a/icons/large/folder_blue_both.png b/icons/large/folder_blue_both.png deleted file mode 100644 index f398c78..0000000 Binary files a/icons/large/folder_blue_both.png and /dev/null differ diff --git a/icons/large/folder_blue_left.png b/icons/large/folder_blue_left.png deleted file mode 100644 index a1b915c..0000000 Binary files a/icons/large/folder_blue_left.png and /dev/null differ diff --git a/icons/large/folder_blue_none.png b/icons/large/folder_blue_none.png deleted file mode 100644 index f587424..0000000 Binary files a/icons/large/folder_blue_none.png and /dev/null differ diff --git a/icons/large/folder_blue_right.png b/icons/large/folder_blue_right.png deleted file mode 100644 index 2785875..0000000 Binary files a/icons/large/folder_blue_right.png and /dev/null differ diff --git a/icons/large/folder_green_both.png b/icons/large/folder_green_both.png deleted file mode 100644 index 42f696a..0000000 Binary files a/icons/large/folder_green_both.png and /dev/null differ diff --git a/icons/large/folder_green_left.png b/icons/large/folder_green_left.png deleted file mode 100644 index b159c11..0000000 Binary files a/icons/large/folder_green_left.png and /dev/null differ diff --git a/icons/large/folder_green_none.png b/icons/large/folder_green_none.png deleted file mode 100644 index ad334fb..0000000 Binary files a/icons/large/folder_green_none.png and /dev/null differ diff --git a/icons/large/folder_green_right.png b/icons/large/folder_green_right.png deleted file mode 100644 index 21a660d..0000000 Binary files a/icons/large/folder_green_right.png and /dev/null differ diff --git a/icons/large/folder_red_both.png b/icons/large/folder_red_both.png deleted file mode 100644 index bcc1e15..0000000 Binary files a/icons/large/folder_red_both.png and /dev/null differ diff --git a/icons/large/folder_red_left.png b/icons/large/folder_red_left.png deleted file mode 100644 index f9ec364..0000000 Binary files a/icons/large/folder_red_left.png and /dev/null differ diff --git a/icons/large/folder_red_none.png b/icons/large/folder_red_none.png deleted file mode 100644 index 78be9c2..0000000 Binary files a/icons/large/folder_red_none.png and /dev/null differ diff --git a/icons/large/folder_red_right.png b/icons/large/folder_red_right.png deleted file mode 100644 index 8e25a46..0000000 Binary files a/icons/large/folder_red_right.png and /dev/null differ diff --git a/icons/large/folder_yellow_both.png b/icons/large/folder_yellow_both.png deleted file mode 100644 index 14691ff..0000000 Binary files a/icons/large/folder_yellow_both.png and /dev/null differ diff --git a/icons/large/folder_yellow_left.png b/icons/large/folder_yellow_left.png deleted file mode 100644 index 269a54c..0000000 Binary files a/icons/large/folder_yellow_left.png and /dev/null differ diff --git a/icons/large/folder_yellow_none.png b/icons/large/folder_yellow_none.png deleted file mode 100644 index f37bb3e..0000000 Binary files a/icons/large/folder_yellow_none.png and /dev/null differ diff --git a/icons/large/folder_yellow_right.png b/icons/large/folder_yellow_right.png deleted file mode 100644 index 6d4a851..0000000 Binary files a/icons/large/folder_yellow_right.png and /dev/null differ diff --git a/icons/large/hand_left.png b/icons/large/hand_left.png deleted file mode 100644 index 1f0c882..0000000 Binary files a/icons/large/hand_left.png and /dev/null differ diff --git a/icons/large/hand_right.png b/icons/large/hand_right.png deleted file mode 100644 index 540dcf8..0000000 Binary files a/icons/large/hand_right.png and /dev/null differ diff --git a/icons/large/playlist_both.png b/icons/large/playlist_both.png deleted file mode 100644 index d50f353..0000000 Binary files a/icons/large/playlist_both.png and /dev/null differ diff --git a/icons/large/playlist_left.png b/icons/large/playlist_left.png deleted file mode 100644 index 0c3dc76..0000000 Binary files a/icons/large/playlist_left.png and /dev/null differ diff --git a/icons/large/playlist_none.png b/icons/large/playlist_none.png deleted file mode 100644 index 8755c17..0000000 Binary files a/icons/large/playlist_none.png and /dev/null differ diff --git a/icons/large/playlist_right.png b/icons/large/playlist_right.png deleted file mode 100644 index 7715633..0000000 Binary files a/icons/large/playlist_right.png and /dev/null differ diff --git a/icons/large/question.png b/icons/large/question.png deleted file mode 100644 index e67fe63..0000000 Binary files a/icons/large/question.png and /dev/null differ diff --git a/icons/large/video_both.png b/icons/large/video_both.png deleted file mode 100644 index c38caf4..0000000 Binary files a/icons/large/video_both.png and /dev/null differ diff --git a/icons/large/video_left.png b/icons/large/video_left.png deleted file mode 100644 index 6bbfd32..0000000 Binary files a/icons/large/video_left.png and /dev/null differ diff --git a/icons/large/video_none.png b/icons/large/video_none.png deleted file mode 100644 index 46531e8..0000000 Binary files a/icons/large/video_none.png and /dev/null differ diff --git a/icons/large/video_right.png b/icons/large/video_right.png deleted file mode 100644 index 7951d33..0000000 Binary files a/icons/large/video_right.png and /dev/null differ diff --git a/icons/large/warning.png b/icons/large/warning.png deleted file mode 100644 index 311e726..0000000 Binary files a/icons/large/warning.png and /dev/null differ diff --git a/icons/locale/flag_uk.png b/icons/locale/flag_uk.png deleted file mode 100644 index 49d82e3..0000000 Binary files a/icons/locale/flag_uk.png and /dev/null differ diff --git a/icons/small/archived.png b/icons/small/archived.png deleted file mode 100644 index fd69e8f..0000000 Binary files a/icons/small/archived.png and /dev/null differ diff --git a/icons/small/arrow_down.png b/icons/small/arrow_down.png deleted file mode 100644 index 691f6e0..0000000 Binary files a/icons/small/arrow_down.png and /dev/null differ diff --git a/icons/small/arrow_up.png b/icons/small/arrow_up.png deleted file mode 100644 index 30d005f..0000000 Binary files a/icons/small/arrow_up.png and /dev/null differ diff --git a/icons/small/channel.png b/icons/small/channel.png deleted file mode 100644 index 139a25e..0000000 Binary files a/icons/small/channel.png and /dev/null differ diff --git a/icons/small/check.png b/icons/small/check.png deleted file mode 100644 index 2c41621..0000000 Binary files a/icons/small/check.png and /dev/null differ diff --git a/icons/small/download.png b/icons/small/download.png deleted file mode 100644 index df01f33..0000000 Binary files a/icons/small/download.png and /dev/null differ diff --git a/icons/small/error.png b/icons/small/error.png deleted file mode 100644 index dbfda22..0000000 Binary files a/icons/small/error.png and /dev/null differ diff --git a/icons/small/folder.png b/icons/small/folder.png deleted file mode 100644 index f1ed9ab..0000000 Binary files a/icons/small/folder.png and /dev/null differ diff --git a/icons/small/folder_black.png b/icons/small/folder_black.png deleted file mode 100644 index cb1d414..0000000 Binary files a/icons/small/folder_black.png and /dev/null differ diff --git a/icons/small/folder_blue.png b/icons/small/folder_blue.png deleted file mode 100644 index 696561a..0000000 Binary files a/icons/small/folder_blue.png and /dev/null differ diff --git a/icons/small/folder_green.png b/icons/small/folder_green.png deleted file mode 100644 index 68c3221..0000000 Binary files a/icons/small/folder_green.png and /dev/null differ diff --git a/icons/small/folder_red.png b/icons/small/folder_red.png deleted file mode 100644 index 83cef08..0000000 Binary files a/icons/small/folder_red.png and /dev/null differ diff --git a/icons/small/have_file.png b/icons/small/have_file.png deleted file mode 100644 index 635230d..0000000 Binary files a/icons/small/have_file.png and /dev/null differ diff --git a/icons/small/no_file.png b/icons/small/no_file.png deleted file mode 100644 index 4dbf0fa..0000000 Binary files a/icons/small/no_file.png and /dev/null differ diff --git a/icons/small/playlist.png b/icons/small/playlist.png deleted file mode 100644 index ec93a22..0000000 Binary files a/icons/small/playlist.png and /dev/null differ diff --git a/icons/small/system_error.png b/icons/small/system_error.png deleted file mode 100644 index ecfdd09..0000000 Binary files a/icons/small/system_error.png and /dev/null differ diff --git a/icons/small/system_warning.png b/icons/small/system_warning.png deleted file mode 100644 index 81134af..0000000 Binary files a/icons/small/system_warning.png and /dev/null differ diff --git a/icons/small/video.png b/icons/small/video.png deleted file mode 100644 index d720c4c..0000000 Binary files a/icons/small/video.png and /dev/null differ diff --git a/icons/small/warning.png b/icons/small/warning.png deleted file mode 100644 index 2c39360..0000000 Binary files a/icons/small/warning.png and /dev/null differ diff --git a/icons/status/status_check_icon_64.png b/icons/status/status_check_icon_64.png deleted file mode 100644 index 31f7326..0000000 Binary files a/icons/status/status_check_icon_64.png and /dev/null differ diff --git a/icons/status/status_check_icon_xmas_64.png b/icons/status/status_check_icon_xmas_64.png deleted file mode 100644 index 31f7326..0000000 Binary files a/icons/status/status_check_icon_xmas_64.png and /dev/null differ diff --git a/icons/status/status_default_icon_64.png b/icons/status/status_default_icon_64.png deleted file mode 100644 index 9b67966..0000000 Binary files a/icons/status/status_default_icon_64.png and /dev/null differ diff --git a/icons/status/status_default_icon_xmas_64.png b/icons/status/status_default_icon_xmas_64.png deleted file mode 100644 index 862c7d9..0000000 Binary files a/icons/status/status_default_icon_xmas_64.png and /dev/null differ diff --git a/icons/status/status_download_icon_64.png b/icons/status/status_download_icon_64.png deleted file mode 100644 index 1f57997..0000000 Binary files a/icons/status/status_download_icon_64.png and /dev/null differ diff --git a/icons/status/status_download_icon_xmas_64.png b/icons/status/status_download_icon_xmas_64.png deleted file mode 100644 index 1f57997..0000000 Binary files a/icons/status/status_download_icon_xmas_64.png and /dev/null differ diff --git a/icons/status/status_info_icon_64.png b/icons/status/status_info_icon_64.png deleted file mode 100644 index 3a6ac79..0000000 Binary files a/icons/status/status_info_icon_64.png and /dev/null differ diff --git a/icons/status/status_info_icon_xmas_64.png b/icons/status/status_info_icon_xmas_64.png deleted file mode 100644 index 3a6ac79..0000000 Binary files a/icons/status/status_info_icon_xmas_64.png and /dev/null differ diff --git a/icons/status/status_refresh_icon_64.png b/icons/status/status_refresh_icon_64.png deleted file mode 100644 index 1985013..0000000 Binary files a/icons/status/status_refresh_icon_64.png and /dev/null differ diff --git a/icons/status/status_refresh_icon_xmas_64.png b/icons/status/status_refresh_icon_xmas_64.png deleted file mode 100644 index 1985013..0000000 Binary files a/icons/status/status_refresh_icon_xmas_64.png and /dev/null differ diff --git a/icons/status/status_tidy_icon_64.png b/icons/status/status_tidy_icon_64.png deleted file mode 100644 index 226ecd3..0000000 Binary files a/icons/status/status_tidy_icon_64.png and /dev/null differ diff --git a/icons/status/status_tidy_icon_xmas_64.png b/icons/status/status_tidy_icon_xmas_64.png deleted file mode 100644 index 226ecd3..0000000 Binary files a/icons/status/status_tidy_icon_xmas_64.png and /dev/null differ diff --git a/icons/status/status_update_icon_64.png b/icons/status/status_update_icon_64.png deleted file mode 100644 index 59c1761..0000000 Binary files a/icons/status/status_update_icon_64.png and /dev/null differ diff --git a/icons/status/status_update_icon_xmas_64.png b/icons/status/status_update_icon_xmas_64.png deleted file mode 100644 index 59c1761..0000000 Binary files a/icons/status/status_update_icon_xmas_64.png and /dev/null differ diff --git a/icons/toolbar/channel_large.png b/icons/toolbar/channel_large.png deleted file mode 100644 index 5e29f7c..0000000 Binary files a/icons/toolbar/channel_large.png and /dev/null differ diff --git a/icons/toolbar/channel_small.png b/icons/toolbar/channel_small.png deleted file mode 100644 index 139a25e..0000000 Binary files a/icons/toolbar/channel_small.png and /dev/null differ diff --git a/icons/toolbar/check_large.png b/icons/toolbar/check_large.png deleted file mode 100644 index ac03ac8..0000000 Binary files a/icons/toolbar/check_large.png and /dev/null differ diff --git a/icons/toolbar/check_small.png b/icons/toolbar/check_small.png deleted file mode 100644 index 2c41621..0000000 Binary files a/icons/toolbar/check_small.png and /dev/null differ diff --git a/icons/toolbar/download_large.png b/icons/toolbar/download_large.png deleted file mode 100644 index e3e0377..0000000 Binary files a/icons/toolbar/download_large.png and /dev/null differ diff --git a/icons/toolbar/download_small.png b/icons/toolbar/download_small.png deleted file mode 100644 index df01f33..0000000 Binary files a/icons/toolbar/download_small.png and /dev/null differ diff --git a/icons/toolbar/folder_large.png b/icons/toolbar/folder_large.png deleted file mode 100644 index f37bb3e..0000000 Binary files a/icons/toolbar/folder_large.png and /dev/null differ diff --git a/icons/toolbar/folder_small.png b/icons/toolbar/folder_small.png deleted file mode 100644 index f1ed9ab..0000000 Binary files a/icons/toolbar/folder_small.png and /dev/null differ diff --git a/icons/toolbar/playlist_large.png b/icons/toolbar/playlist_large.png deleted file mode 100644 index 8755c17..0000000 Binary files a/icons/toolbar/playlist_large.png and /dev/null differ diff --git a/icons/toolbar/playlist_small.png b/icons/toolbar/playlist_small.png deleted file mode 100644 index ec93a22..0000000 Binary files a/icons/toolbar/playlist_small.png and /dev/null differ diff --git a/icons/toolbar/quit_large.png b/icons/toolbar/quit_large.png deleted file mode 100644 index ce2730b..0000000 Binary files a/icons/toolbar/quit_large.png and /dev/null differ diff --git a/icons/toolbar/quit_small.png b/icons/toolbar/quit_small.png deleted file mode 100644 index d4e84c2..0000000 Binary files a/icons/toolbar/quit_small.png and /dev/null differ diff --git a/icons/toolbar/stop_large.png b/icons/toolbar/stop_large.png deleted file mode 100644 index 1b20ae0..0000000 Binary files a/icons/toolbar/stop_large.png and /dev/null differ diff --git a/icons/toolbar/stop_small.png b/icons/toolbar/stop_small.png deleted file mode 100644 index 33c876b..0000000 Binary files a/icons/toolbar/stop_small.png and /dev/null differ diff --git a/icons/toolbar/switch_large.png b/icons/toolbar/switch_large.png deleted file mode 100644 index 8320169..0000000 Binary files a/icons/toolbar/switch_large.png and /dev/null differ diff --git a/icons/toolbar/switch_small.png b/icons/toolbar/switch_small.png deleted file mode 100644 index 46f5a7b..0000000 Binary files a/icons/toolbar/switch_small.png and /dev/null differ diff --git a/icons/toolbar/test_large.png b/icons/toolbar/test_large.png deleted file mode 100644 index 2b4cfa5..0000000 Binary files a/icons/toolbar/test_large.png and /dev/null differ diff --git a/icons/toolbar/test_small.png b/icons/toolbar/test_small.png deleted file mode 100644 index 485a136..0000000 Binary files a/icons/toolbar/test_small.png and /dev/null differ diff --git a/icons/toolbar/video_large.png b/icons/toolbar/video_large.png deleted file mode 100644 index 46531e8..0000000 Binary files a/icons/toolbar/video_large.png and /dev/null differ diff --git a/icons/toolbar/video_small.png b/icons/toolbar/video_small.png deleted file mode 100644 index d720c4c..0000000 Binary files a/icons/toolbar/video_small.png and /dev/null differ diff --git a/icons/win/gymbob_icon_128.png b/icons/win/gymbob_icon_128.png new file mode 100644 index 0000000..64b7eb3 Binary files /dev/null and b/icons/win/gymbob_icon_128.png differ diff --git a/icons/win/gymbob_icon_16.png b/icons/win/gymbob_icon_16.png new file mode 100644 index 0000000..cc3fcd4 Binary files /dev/null and b/icons/win/gymbob_icon_16.png differ diff --git a/icons/win/gymbob_icon_24.png b/icons/win/gymbob_icon_24.png new file mode 100644 index 0000000..b86641a Binary files /dev/null and b/icons/win/gymbob_icon_24.png differ diff --git a/icons/win/gymbob_icon_256.png b/icons/win/gymbob_icon_256.png new file mode 100644 index 0000000..44ea533 Binary files /dev/null and b/icons/win/gymbob_icon_256.png differ diff --git a/icons/win/gymbob_icon_32.png b/icons/win/gymbob_icon_32.png new file mode 100644 index 0000000..24a50ca Binary files /dev/null and b/icons/win/gymbob_icon_32.png differ diff --git a/icons/win/gymbob_icon_48.png b/icons/win/gymbob_icon_48.png new file mode 100644 index 0000000..dc8d108 Binary files /dev/null and b/icons/win/gymbob_icon_48.png differ diff --git a/icons/win/gymbob_icon_512.png b/icons/win/gymbob_icon_512.png new file mode 100644 index 0000000..a0175e6 Binary files /dev/null and b/icons/win/gymbob_icon_512.png differ diff --git a/icons/win/gymbob_icon_64.png b/icons/win/gymbob_icon_64.png new file mode 100644 index 0000000..880f07e Binary files /dev/null and b/icons/win/gymbob_icon_64.png differ diff --git a/icons/win/system_icon_128.png b/icons/win/system_icon_128.png deleted file mode 100644 index 73de20b..0000000 Binary files a/icons/win/system_icon_128.png and /dev/null differ diff --git a/icons/win/system_icon_16.png b/icons/win/system_icon_16.png deleted file mode 100644 index 17efd6a..0000000 Binary files a/icons/win/system_icon_16.png and /dev/null differ diff --git a/icons/win/system_icon_24.png b/icons/win/system_icon_24.png deleted file mode 100644 index 10edada..0000000 Binary files a/icons/win/system_icon_24.png and /dev/null differ diff --git a/icons/win/system_icon_256.png b/icons/win/system_icon_256.png deleted file mode 100644 index d0c736d..0000000 Binary files a/icons/win/system_icon_256.png and /dev/null differ diff --git a/icons/win/system_icon_32.png b/icons/win/system_icon_32.png deleted file mode 100644 index 3da1309..0000000 Binary files a/icons/win/system_icon_32.png and /dev/null differ diff --git a/icons/win/system_icon_48.png b/icons/win/system_icon_48.png deleted file mode 100644 index 9d387dd..0000000 Binary files a/icons/win/system_icon_48.png and /dev/null differ diff --git a/icons/win/system_icon_512.png b/icons/win/system_icon_512.png deleted file mode 100644 index 0a222a9..0000000 Binary files a/icons/win/system_icon_512.png and /dev/null differ diff --git a/icons/win/system_icon_64.png b/icons/win/system_icon_64.png deleted file mode 100644 index 9b67966..0000000 Binary files a/icons/win/system_icon_64.png and /dev/null differ diff --git a/icons/win/system_icon_xmas_128.png b/icons/win/system_icon_xmas_128.png deleted file mode 100644 index 9779bdf..0000000 Binary files a/icons/win/system_icon_xmas_128.png and /dev/null differ diff --git a/icons/win/system_icon_xmas_16.png b/icons/win/system_icon_xmas_16.png deleted file mode 100644 index 58b33a3..0000000 Binary files a/icons/win/system_icon_xmas_16.png and /dev/null differ diff --git a/icons/win/system_icon_xmas_24.png b/icons/win/system_icon_xmas_24.png deleted file mode 100644 index 4339d41..0000000 Binary files a/icons/win/system_icon_xmas_24.png and /dev/null differ diff --git a/icons/win/system_icon_xmas_256.png b/icons/win/system_icon_xmas_256.png deleted file mode 100644 index 53d4d5d..0000000 Binary files a/icons/win/system_icon_xmas_256.png and /dev/null differ diff --git a/icons/win/system_icon_xmas_32.png b/icons/win/system_icon_xmas_32.png deleted file mode 100644 index 02325fb..0000000 Binary files a/icons/win/system_icon_xmas_32.png and /dev/null differ diff --git a/icons/win/system_icon_xmas_48.png b/icons/win/system_icon_xmas_48.png deleted file mode 100644 index 377b91d..0000000 Binary files a/icons/win/system_icon_xmas_48.png and /dev/null differ diff --git a/icons/win/system_icon_xmas_512.png b/icons/win/system_icon_xmas_512.png deleted file mode 100644 index bcb7b39..0000000 Binary files a/icons/win/system_icon_xmas_512.png and /dev/null differ diff --git a/icons/win/system_icon_xmas_64.png b/icons/win/system_icon_xmas_64.png deleted file mode 100644 index 862c7d9..0000000 Binary files a/icons/win/system_icon_xmas_64.png and /dev/null differ diff --git a/nsis/README b/nsis/README deleted file mode 100644 index dca5cf7..0000000 --- a/nsis/README +++ /dev/null @@ -1 +0,0 @@ -These files in this directory are used to create the MS Windows installer diff --git a/nsis/license.txt b/nsis/license.txt deleted file mode 100644 index 94a9ed0..0000000 --- a/nsis/license.txt +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/nsis/tartube_32bit.bat b/nsis/tartube_32bit.bat deleted file mode 100644 index 37c9f53..0000000 --- a/nsis/tartube_32bit.bat +++ /dev/null @@ -1 +0,0 @@ -..\..\..\usr\bin\mintty.exe -w hide /bin/env MSYSTEM=MINGW32 /bin/bash -lc /home/user/tartube/tartube_mswin.sh diff --git a/nsis/tartube_64bit.bat b/nsis/tartube_64bit.bat deleted file mode 100644 index b30ffa2..0000000 --- a/nsis/tartube_64bit.bat +++ /dev/null @@ -1 +0,0 @@ -..\..\..\usr\bin\mintty.exe -w hide /bin/env MSYSTEM=MINGW64 /bin/bash -lc /home/user/tartube/tartube_mswin.sh diff --git a/nsis/tartube_header.bmp b/nsis/tartube_header.bmp deleted file mode 100644 index 31814e0..0000000 Binary files a/nsis/tartube_header.bmp and /dev/null differ diff --git a/nsis/tartube_icon.ico b/nsis/tartube_icon.ico deleted file mode 100644 index d0bbd3d..0000000 Binary files a/nsis/tartube_icon.ico and /dev/null differ diff --git a/nsis/tartube_install_32bit.nsi b/nsis/tartube_install_32bit.nsi deleted file mode 100644 index 0802b45..0000000 --- a/nsis/tartube_install_32bit.nsi +++ /dev/null @@ -1,355 +0,0 @@ -# Tartube v2.0.006 installer script for MS Windows -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . -# -# -# Build instructions: -# - These instructions describe how to create an installer for Tartube on a -# 32-bit MS Windows machine, Windows Vista or higher. For 64-bit machines -# see the tartube_install_64bit.nsi file -# -# - Download and install NSIS from -# -# http://nsis.sourceforge.io/Download/ -# -# - Download the 32-bit version of MSYS2. The downloadable file should look -# something like 'msys2-i686-yyyymmdd.exe' -# -# http://www.msys2.org/ -# -# - 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 -# -# - We need to install various dependencies. In the terminal window, type -# this command -# -# pacman -Syu -# -# - 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 -# pacman -S mingw-w64-i686-python3 -# pacman -S mingw-w64-i686-python3-pip -# pacman -S mingw-w64-i686-python3-gobject -# pacman -S mingw-w64-i686-python3-requests -# pacman -S mingw-w64-i686-gtk3 -# pacman -S mingw-w64-i686-gsettings-desktop-schemas -# -# - Optional step: you can check that the dependencies are working by typing -# this command (if you like): -# -# 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 -# necessary. This table shows which files and folders are in the official -# Tartube installer (which is about 90MB). Everything else can be -# deleted: -# -# C:\testme\msys32\dev -# C:\testme\msys32\etc -# C:\testme\msys32\home -# C:\testme\msys32\minwg32\bin -# C:\testme\msys32\minwg32\bin\gdbus* -# C:\testme\msys32\minwg32\bin\gdk* -# C:\testme\msys32\minwg32\bin\gio* -# C:\testme\msys32\minwg32\bin\glib* -# C:\testme\msys32\minwg32\bin\gobject* -# C:\testme\msys32\minwg32\bin\gtk* -# C:\testme\msys32\minwg32\bin\json* -# C:\testme\msys32\minwg32\bin\lib* -# C:\testme\msys32\minwg32\bin\openssl -# C:\testme\msys32\minwg32\bin\pip* -# C:\testme\msys32\minwg32\bin\python* -# C:\testme\msys32\minwg32\bin\pyenv* -# C:\testme\msys32\minwg32\bin\sqlite* -# C:\testme\msys32\minwg32\bin\zlib1.dll -# C:\testme\msys32\minwg32\include\gdk-pixbuf-2.0 -# C:\testme\msys32\minwg32\include\gio-win32-2.0 -# C:\testme\msys32\minwg32\include\glib-2.0 -# C:\testme\msys32\minwg32\include\gsettings-desktop-schemas -# C:\testme\msys32\minwg32\include\gtk-3.0 -# C:\testme\msys32\minwg32\include\json-glib-1.0 -# C:\testme\msys32\minwg32\include\ncurses -# C:\testme\msys32\minwg32\include\ncursesw -# C:\testme\msys32\minwg32\include\openssl -# C:\testme\msys32\minwg32\include\pycairo -# C:\testme\msys32\minwg32\include\pygobject-3.0 -# C:\testme\msys32\minwg32\include\python3.7 -# 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\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\thread2.8.4 -# C:\testme\msys32\minwg32\lib\tk8.6 -# C:\testme\msys32\minwg32\share\gir-1.0 -# C:\testme\msys32\minwg32\share\glib-2.0 -# C:\testme\msys32\minwg32\share\gtk-3.0 -# C:\testme\msys32\minwg32\share\icons -# C:\testme\msys32\minwg32\share\locale\en* -# C:\testme\msys32\minwg32\share\locale\locale.alias -# C:\testme\msys32\minwg32\share\themes -# C:\testme\msys32\minwg32\share\thumbnailers -# C:\testme\msys32\minwg32\ssl -# C:\testme\msys32\tmp -# C:\testme\msys32\usr\bin\bash -# C:\testme\msys32\usr\bin\chmod -# C:\testme\msys32\usr\bin\cygpath -# C:\testme\msys32\usr\bin\cygwin-console-helper -# C:\testme\msys32\usr\bin\dir -# C:\testme\msys32\usr\bin\env -# C:\testme\msys32\usr\bin\find -# C:\testme\msys32\usr\bin\findfs -# C:\testme\msys32\usr\bin\gpg* -# C:\testme\msys32\usr\bin\hostid -# C:\testme\msys32\usr\bin\hostname -# C:\testme\msys32\usr\bin\iconv -# C:\testme\msys32\usr\bin\id -# C:\testme\msys32\usr\bin\ln -# C:\testme\msys32\usr\bin\lndir -# C:\testme\msys32\usr\bin\locale -# C:\testme\msys32\usr\bin\ls -# C:\testme\msys32\usr\bin\mintty -# C:\testme\msys32\usr\bin\mkdir -# C:\testme\msys32\usr\bin\msys-2.0.dll -# C:\testme\msys32\usr\bin\msys-assuan-0.dll -# C:\testme\msys32\usr\bin\msys-bz2-1.dll -# C:\testme\msys32\usr\bin\msys-gcc_s-1.dll -# C:\testme\msys32\usr\bin\msys-gcrypt-20.dll -# C:\testme\msys32\usr\bin\msys-gio-2.0-0.dll -# C:\testme\msys32\usr\bin\msys-glib-2.0-0.dll -# C:\testme\msys32\usr\bin\msys-gobject-2.0-0.dll -# C:\testme\msys32\usr\bin\msys-gpg-error-0.dll -# C:\testme\msys32\usr\bin\msys-gpgme-11.dll -# C:\testme\msys32\usr\bin\msys-gpgmepp-6.dll -# C:\testme\msys32\usr\bin\msys-gthread-2.0-0.dll -# C:\testme\msys32\usr\bin\msys-iconv-2.dll -# C:\testme\msys32\usr\bin\msys-intl-8.dll -# C:\testme\msys32\usr\bin\msys-ncurses++w6.dll -# C:\testme\msys32\usr\bin\msys-ncursesw6.dll -# C:\testme\msys32\usr\bin\msys-readline8.dll -# C:\testme\msys32\usr\bin\msys-sqlite3-0.dll -# C:\testme\msys32\usr\bin\msys-stdc++06.dll -# C:\testme\msys32\usr\bin\msys-z.dll -# C:\testme\msys32\usr\bin\pac* -# C:\testme\msys32\usr\bin\test -# C:\testme\msys32\usr\bin\tzset -# C:\testme\msys32\usr\lib\gio -# C:\testme\msys32\usr\lib\openssl -# C:\testme\msys32\usr\lib\python3.7 -# C:\testme\msys32\usr\share\cygwin -# C:\testme\msys32\usr\share\glib-2.0 -# C:\testme\msys32\usr\share\mintty -# C:\testme\msys32\usr\share\Msys -# C:\testme\msys32\usr\share\pacman -# C:\testme\msys32\usr\share\pactoys -# C:\testme\msys32\usr\ssl -# C:\testme\msys32\var\lib\pacman -# -# - You can optionally install AtomicParsley at 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 -# - 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 -# ------------------------------- - - !include "MUI2.nsh" - !include "Sections.nsh" - -# General -# ------------------------------- - - ;Name and file - Name "Tartube" - OutFile "install-tartube-2.0.006-32bit.exe" - - ;Default installation folder - InstallDir "$LOCALAPPDATA\Tartube" - - ;Get installation folder from registry if available - InstallDirRegKey HKCU "Software\Tartube" "" - - ;Request application privileges for Windows Vista - RequestExecutionLevel user - - ; Extra stuff here - BrandingText " " - -# Variables -# ------------------------------- - -### Var StartMenuFolder - -# Interface settings -# ------------------------------- - - !define MUI_ABORTWARNING - !define MUI_ICON "tartube_icon.ico" - !define MUI_UNICON "tartube_icon.ico" - !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "tartube_header.bmp" - !define MUI_HEADERIMAGE_UNBITMAP "tartube_header.bmp" - !define MUI_WELCOMEFINISHPAGE_BITMAP "tartube_wizard.bmp" - -# Pages -# ------------------------------- - - !insertmacro MUI_PAGE_WELCOME - - !insertmacro MUI_PAGE_LICENSE "license.txt" - - !insertmacro MUI_PAGE_DIRECTORY - - !define MUI_STARTMENUPAGE_REGISTRY_ROOT "SHCTX" - !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Tartube" - !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Startmenu" - !define MUI_STARTMENUPAGE_DEFAULTFOLDER "Tartube" - - !insertmacro MUI_PAGE_INSTFILES - - !define MUI_FINISHPAGE_RUN "$INSTDIR\msys32\home\user\tartube\tartube_32bit.bat" - !define MUI_FINISHPAGE_RUN_TEXT "Run Tartube" - !define MUI_FINISHPAGE_RUN_NOTCHECKED - !define MUI_FINISHPAGE_LINK "Visit the Tartube website for the latest news \ - and support" - !define MUI_FINISHPAGE_LINK_LOCATION "http://tartube.sourceforge.io/" - !insertmacro MUI_PAGE_FINISH - - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - -# Languages -# ------------------------------- - - !insertmacro MUI_LANGUAGE "English" - -# Installer sections -# ------------------------------- - -Section "Tartube" SecClient - - SectionIn RO - SetOutPath "$INSTDIR" - - File "tartube_icon.ico" - File /r msys32 - - SetOutPath "$INSTDIR\msys32\home\user\tartube" - - # Start Menu - CreateDirectory "$SMPROGRAMS\Tartube" - CreateShortCut "$SMPROGRAMS\Tartube\Tartube.lnk" \ - "$INSTDIR\msys32\home\user\tartube\tartube_32bit.bat" \ - "" "$INSTDIR\tartube_icon.ico" "" SW_SHOWMINIMIZED - CreateShortCut "$SMPROGRAMS\Tartube\Uninstall Tartube.lnk" \ - "$INSTDIR\Uninstall.exe" \ - "" "$INSTDIR\tartube_icon.ico" - - # Desktop icon - CreateShortcut "$DESKTOP\Tartube.lnk" \ - "$INSTDIR\msys32\home\user\tartube\tartube_32bit.bat" \ - "" "$INSTDIR\tartube_icon.ico" "" SW_SHOWMINIMIZED - - # Store installation folder - # Commented out from v1.5.0; these instructions don't work, and probably - # aren't necessary anyway -# WriteRegStr HKLM \ -# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "DisplayName" "Tartube" -# WriteRegStr HKLM \ -# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "UninstallString" "$\"$INSTDIR\Uninstall.exe$\"" -# WriteRegStr HKLM \ -# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "Publisher" "A S Lewis" -# WriteRegStr HKLM \ -# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "DisplayVersion" "2.0.006" - - # Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" - -SectionEnd - -# Uninstaller sections -# ------------------------------- - -Section "Uninstall" - - Delete "$SMPROGRAMS\Tartube\Tartube.lnk" - Delete "$SMPROGRAMS\Tartube\Uninstall Tartube.lnk" - Delete "$SMPROGRAMS\Tartube\Gtk graphics test.lnk" - RMDir /r "$SMPROGRAMS\Tartube" - Delete "$DESKTOP\Tartube.lnk" - - RMDir /r "$INSTDIR" - Delete "$INSTDIR\Uninstall.exe" - - DeleteRegKey HKLM \ - "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" - -SectionEnd diff --git a/nsis/tartube_install_64bit.nsi b/nsis/tartube_install_64bit.nsi deleted file mode 100644 index 5221250..0000000 --- a/nsis/tartube_install_64bit.nsi +++ /dev/null @@ -1,356 +0,0 @@ -# Tartube v2.0.006 installer script for MS Windows -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . -# -# -# Build instructions: -# - These instructions describe how to create an installer for Tartube on a -# 64-bit MS Windows machine, Windows Vista or higher. For 32-bit machines -# see the tartube_install_32bit.nsi file -# -# - Download and install NSIS from -# -# http://nsis.sourceforge.io/Download/ -# -# - Download the 64-bit version of MSYS2. The downloadable file should look -# something like 'msys2-x86-64-yyyymmdd.exe' -# -# http://www.msys2.org/ -# -# - 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\msys64 -# -# - Run the mingw64 terminal, i.e. -# -# C:\testme\msys64\mingw64.exe -# -# - We need to install various dependencies. In the terminal window, type -# this command -# -# pacman -Syu -# -# - 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 -# pacman -S mingw-w64-x86_64-python3 -# pacman -S mingw-w64-x86_64-python3-pip -# pacman -S mingw-w64-x86_64-python3-gobject -# pacman -S mingw-w64-x86_64-python3-requests -# pacman -S mingw-w64-x86_64-gtk3 -# pacman -S mingw-w64-x86_64-gsettings-desktop-schemas -# -# - Optional step: you can check that the dependencies are working by typing -# this command (if you like): -# -# 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 -# necessary. This table shows which files and folders are in the official -# Tartube installer (which is about 90MB). Everything else can be -# deleted: -# -# C:\testme\msys64\dev -# C:\testme\msys64\etc -# C:\testme\msys64\home -# C:\testme\msys64\mingw64\bin -# C:\testme\msys64\mingw64\bin\gdbus* -# C:\testme\msys64\mingw64\bin\gdk* -# C:\testme\msys64\mingw64\bin\gio* -# C:\testme\msys64\mingw64\bin\glib* -# C:\testme\msys64\mingw64\bin\gobject* -# C:\testme\msys64\mingw64\bin\gtk* -# C:\testme\msys64\mingw64\bin\json* -# C:\testme\msys64\mingw64\bin\lib* -# C:\testme\msys64\mingw64\bin\openssl -# C:\testme\msys64\mingw64\bin\pip* -# C:\testme\msys64\mingw64\bin\python* -# C:\testme\msys64\mingw64\bin\pyenv* -# C:\testme\msys64\mingw64\bin\sqlite* -# C:\testme\msys64\mingw64\bin\zlib1.dll -# C:\testme\msys64\mingw64\include\gdk-pixbuf-2.0 -# C:\testme\msys64\mingw64\include\gio-win32-2.0 -# C:\testme\msys64\mingw64\include\glib-2.0 -# C:\testme\msys64\mingw64\include\gsettings-desktop-schemas -# C:\testme\msys64\mingw64\include\gtk-3.0 -# C:\testme\msys64\mingw64\include\json-glib-1.0 -# C:\testme\msys64\mingw64\include\ncurses -# C:\testme\msys64\mingw64\include\ncursesw -# 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\readline -# C:\testme\msys64\mingw64\include\tk8.6 -# C:\testme\msys64\mingw64\lib\gdk-pixbuf-2.0 -# 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\thread2.8.4 -# C:\testme\msys64\mingw64\lib\tk8.6 -# C:\testme\msys64\mingw64\share\gir-1.0 -# C:\testme\msys64\mingw64\share\glib-2.0 -# C:\testme\msys64\mingw64\share\gtk-3.0 -# C:\testme\msys64\mingw64\share\icons -# C:\testme\msys64\mingw64\share\locale\en* -# C:\testme\msys64\mingw64\share\locale\locale.alias -# C:\testme\msys64\mingw64\share\themes -# C:\testme\msys64\mingw64\share\thumbnailers -# C:\testme\msys64\mingw64\ssl -# C:\testme\msys64\tmp -# C:\testme\msys64\usr\bin\bash -# C:\testme\msys64\usr\bin\chmod -# C:\testme\msys64\usr\bin\cygpath -# C:\testme\msys64\usr\bin\cygwin-console-helper -# C:\testme\msys64\usr\bin\dir -# C:\testme\msys64\usr\bin\env -# C:\testme\msys64\usr\bin\find -# C:\testme\msys64\usr\bin\findfs -# C:\testme\msys64\usr\bin\gpg* -# C:\testme\msys64\usr\bin\hostid -# C:\testme\msys64\usr\bin\hostname -# C:\testme\msys64\usr\bin\iconv -# C:\testme\msys64\usr\bin\id -# C:\testme\msys64\usr\bin\ln -# C:\testme\msys64\usr\bin\lndir -# C:\testme\msys64\usr\bin\locale -# C:\testme\msys64\usr\bin\ls -# C:\testme\msys64\usr\bin\mintty -# C:\testme\msys64\usr\bin\mkdir -# C:\testme\msys64\usr\bin\msys-2.0.dll -# C:\testme\msys64\usr\bin\msys-assuan-0.dll -# C:\testme\msys64\usr\bin\msys-bz2-1.dll -# C:\testme\msys64\usr\bin\msys-gcc_s-1.dll -# C:\testme\msys64\usr\bin\msys-gcrypt-20.dll -# C:\testme\msys64\usr\bin\msys-gio-2.0-0.dll -# C:\testme\msys64\usr\bin\msys-glib-2.0-0.dll -# C:\testme\msys64\usr\bin\msys-gobject-2.0-0.dll -# C:\testme\msys64\usr\bin\msys-gpg-error-0.dll -# C:\testme\msys64\usr\bin\msys-gpgme-11.dll -# C:\testme\msys64\usr\bin\msys-gpgmepp-6.dll -# C:\testme\msys64\usr\bin\msys-gthread-2.0-0.dll -# C:\testme\msys64\usr\bin\msys-iconv-2.dll -# C:\testme\msys64\usr\bin\msys-intl-8.dll -# C:\testme\msys64\usr\bin\msys-ncurses++w6.dll -# C:\testme\msys64\usr\bin\msys-ncursesw6.dll -# C:\testme\msys64\usr\bin\msys-readline8.dll -# C:\testme\msys64\usr\bin\msys-sqlite3-0.dll -# C:\testme\msys64\usr\bin\msys-stdc++06.dll -# C:\testme\msys64\usr\bin\msys-z.dll -# C:\testme\msys64\usr\bin\pac* -# C:\testme\msys64\usr\bin\test -# C:\testme\msys64\usr\bin\tzset -# C:\testme\msys64\usr\lib\gio -# C:\testme\msys64\usr\lib\openssl -# C:\testme\msys64\usr\lib\python3.7 -# C:\testme\msys64\usr\share\cygwin -# C:\testme\msys64\usr\share\glib-2.0 -# C:\testme\msys64\usr\share\mintty -# C:\testme\msys64\usr\share\Msys -# C:\testme\msys64\usr\share\pacman -# C:\testme\msys64\usr\share\pactoys -# C:\testme\msys64\usr\ssl -# C:\testme\msys64\var\lib\pacman -# -# - You can optionally install AtomicParsley at 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 -# 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 -# ------------------------------- - - !include "MUI2.nsh" - !include "Sections.nsh" - -# General -# ------------------------------- - - ;Name and file - Name "Tartube" - OutFile "install-tartube-2.0.006-64bit.exe" - - ;Default installation folder - InstallDir "$LOCALAPPDATA\Tartube" - - ;Get installation folder from registry if available - InstallDirRegKey HKCU "Software\Tartube" "" - - ;Request application privileges for Windows Vista - RequestExecutionLevel user - - ; Extra stuff here - BrandingText " " - -# Variables -# ------------------------------- - -### Var StartMenuFolder - -# Interface settings -# ------------------------------- - - !define MUI_ABORTWARNING - !define MUI_ICON "tartube_icon.ico" - !define MUI_UNICON "tartube_icon.ico" - !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "tartube_header.bmp" - !define MUI_HEADERIMAGE_UNBITMAP "tartube_header.bmp" - !define MUI_WELCOMEFINISHPAGE_BITMAP "tartube_wizard.bmp" - -# Pages -# ------------------------------- - - !insertmacro MUI_PAGE_WELCOME - - !insertmacro MUI_PAGE_LICENSE "license.txt" - - !insertmacro MUI_PAGE_DIRECTORY - - !define MUI_STARTMENUPAGE_REGISTRY_ROOT "SHCTX" - !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Tartube" - !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Startmenu" - !define MUI_STARTMENUPAGE_DEFAULTFOLDER "Tartube" - - !insertmacro MUI_PAGE_INSTFILES - - !define MUI_FINISHPAGE_RUN "$INSTDIR\msys64\home\user\tartube\tartube_64bit.bat" - !define MUI_FINISHPAGE_RUN_TEXT "Run Tartube" - !define MUI_FINISHPAGE_RUN_NOTCHECKED - !define MUI_FINISHPAGE_LINK "Visit the Tartube website for the latest news \ - and support" - !define MUI_FINISHPAGE_LINK_LOCATION "http://tartube.sourceforge.io/" - !insertmacro MUI_PAGE_FINISH - - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - -# Languages -# ------------------------------- - - !insertmacro MUI_LANGUAGE "English" - -# Installer sections -# ------------------------------- - -Section "Tartube" SecClient - - SectionIn RO - SetOutPath "$INSTDIR" - - File "tartube_icon.ico" - File /r msys64 - - SetOutPath "$INSTDIR\msys64\home\user\tartube" - - # Start Menu - CreateDirectory "$SMPROGRAMS\Tartube" - CreateShortCut "$SMPROGRAMS\Tartube\Tartube.lnk" \ - "$INSTDIR\msys64\home\user\tartube\tartube_64bit.bat" \ - "" "$INSTDIR\tartube_icon.ico" "" SW_SHOWMINIMIZED - CreateShortCut "$SMPROGRAMS\Tartube\Uninstall Tartube.lnk" \ - "$INSTDIR\Uninstall.exe" \ - "" "$INSTDIR\tartube_icon.ico" - - # Desktop icon - CreateShortcut "$DESKTOP\Tartube.lnk" \ - "$INSTDIR\msys64\home\user\tartube\tartube_64bit.bat" \ - "" "$INSTDIR\tartube_icon.ico" "" SW_SHOWMINIMIZED - - # Store installation folder - # Commented out from v1.5.0; these instructions don't work, and probably - # aren't necessary anyway -# WriteRegStr HKLM \ -# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "DisplayName" "Tartube" -# WriteRegStr HKLM \ -# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "UninstallString" "$\"$INSTDIR\Uninstall.exe$\"" -# WriteRegStr HKLM \ -# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "Publisher" "A S Lewis" -# WriteRegStr HKLM \ -# "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" \ -# "DisplayVersion" "2.0.006" - - # Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" - -SectionEnd - -# Uninstaller sections -# ------------------------------- - -Section "Uninstall" - - Delete "$SMPROGRAMS\Tartube\Tartube.lnk" - Delete "$SMPROGRAMS\Tartube\Uninstall Tartube.lnk" - Delete "$SMPROGRAMS\Tartube\Gtk graphics test.lnk" - RMDir /r "$SMPROGRAMS\Tartube" - Delete "$DESKTOP\Tartube.lnk" - - RMDir /r "$INSTDIR" - Delete "$INSTDIR\Uninstall.exe" - - DeleteRegKey HKLM \ - "Software\Microsoft\Windows\CurrentVersion\Uninstall\Tartube" - -SectionEnd diff --git a/nsis/tartube_wizard.bmp b/nsis/tartube_wizard.bmp deleted file mode 100644 index 547e8ba..0000000 Binary files a/nsis/tartube_wizard.bmp and /dev/null differ diff --git a/pack/bin/tartube b/pack/bin/tartube deleted file mode 100755 index 49ccfd0..0000000 --- a/pack/bin/tartube +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Tartube main file.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import os -import sys -import importlib.util - - -# Add module directory to path to prevent import issues -spec = importlib.util.find_spec('tartube') -if spec is not None: - sys.path.append(os.path.abspath(os.path.dirname(spec.origin))) - - -# Import our modules -import mainapp - - -# 'Global' variables -__packagename__ = 'tartube' -__prettyname__ = 'Tartube' -__version__ = '2.0.006' -__date__ = '3 Mar 2020' -__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' -__license__ = """ -Copyright \xa9 2019-2020 A S Lewis. - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" -__author_list__ = [ - 'A S Lewis', -] -__description__ = 'A front-end GUI for youtube-dl,\n' \ -+ 'partly based on youtube-dl-gui\n' \ -+ 'and written in Python 3 / Gtk 3' -__website__ = 'http://tartube.sourceforge.io' -__app_id__ = 'io.sourceforge.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 -__pkg_install_flag__ = True -__pkg_strict_install_flag__ = False - - -# Start Tartube -app = mainapp.TartubeApp() -app.run(sys.argv) diff --git a/pack/bin_strict/tartube b/pack/bin_strict/tartube deleted file mode 100755 index 4be3a0a..0000000 --- a/pack/bin_strict/tartube +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Tartube main file.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import os -import sys -import importlib.util - - -# Add module directory to path to prevent import issues -spec = importlib.util.find_spec('tartube') -if spec is not None: - sys.path.append(os.path.abspath(os.path.dirname(spec.origin))) - - -# Import our modules -import mainapp - - -# 'Global' variables -__packagename__ = 'tartube' -__prettyname__ = 'Tartube' -__version__ = '2.0.006' -__date__ = '3 Mar 2020' -__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' -__license__ = """ -Copyright \xa9 2019-2020 A S Lewis. - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" -__author_list__ = [ - 'A S Lewis', -] -__description__ = 'A front-end GUI for youtube-dl,\n' \ -+ 'partly based on youtube-dl-gui\n' \ -+ 'and written in Python 3 / Gtk 3' -__website__ = 'http://tartube.sourceforge.io' -__app_id__ = 'io.sourceforge.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 -__pkg_install_flag__ = True -__pkg_strict_install_flag__ = True - - -# Start Tartube -app = mainapp.TartubeApp() -app.run(sys.argv) diff --git a/pack/copyright b/pack/copyright index e27b708..8cd67ab 100644 --- a/pack/copyright +++ b/pack/copyright @@ -1,11 +1,7 @@ -Files: tartube/* -Copyright: 2019-2020 A S Lewis +Files: gymbob/* +Copyright: 2020 A S Lewis License: GPL-3+ Files: icons/* -Copyright: A S Lewis - Vectorgraphit https://www.iconfinder.com/icons/199499/ - bekeen studio https://www.iconfinder.com/bekeenstudio - FatCow Web Hosting https://www.fatcow.com/free-icons - Mr. Hopnguyen https://www.iconfinder.com/icons/2634450/ -License: CC-BY +Copyright: Free for commercial use + diff --git a/pack/gymbob.1 b/pack/gymbob.1 new file mode 100644 index 0000000..250d2d4 --- /dev/null +++ b/pack/gymbob.1 @@ -0,0 +1,16 @@ +.TH man 1 "28 Mar 2020" "1.002" "gymbob man page" +.SH NAME +gymbob \- simple script to prompt the user during a workout +.SH SYNOPSIS +gymbob +.SH DESCRIPTION +Designed for use at the gym, GymBob prompts the user (graphically and using +sound effects) at regular intervals during a workout. The workout programmes +are completely customistable. GymBob is written in Python 3 / Gtk 3 and runs +on Linux/*BSD. +.SH OPTIONS +The gymbob executable does not take any options. +.SH BUGS +No known bugs. +.SH AUTHOR +A S Lewis (aslewis@cpan.org) diff --git a/pack/gymbob.desktop b/pack/gymbob.desktop new file mode 100644 index 0000000..405ff07 --- /dev/null +++ b/pack/gymbob.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=GymBob +Version=1.002 +Exec=gymbob +Icon=gymbob +Type=Application +Categories=Network +Comment=Simple script to prompt the user during a workout +Terminal=false +Name[en_GB]=gymbob.desktop diff --git a/pack/gymbob.png b/pack/gymbob.png new file mode 100644 index 0000000..dc8d108 Binary files /dev/null and b/pack/gymbob.png differ diff --git a/pack/gymbob.xpm b/pack/gymbob.xpm new file mode 100644 index 0000000..b53e8ce --- /dev/null +++ b/pack/gymbob.xpm @@ -0,0 +1,122 @@ +/* XPM */ +static char *_091_6610963132c82b91447160691ad6cf614c1b29e6c5e2ac791e763f22db8d483f[] = { +/* columns rows colors chars-per-pixel */ +"32 32 84 1 ", +" c #373C4B", +". c #354555", +"X c #3E4355", +"o c #354858", +"O c #146271", +"+ c #146373", +"@ c #146474", +"# c #146574", +"$ c #156777", +"% c #166D7E", +"& c #166E7F", +"* c #42485B", +"= c #595E6F", +"- c #595F70", +"; c #5A5F70", +": c #576678", +"> c #566779", +", c #616777", +"< c #626979", +"1 c #167081", +"2 c #177082", +"3 c #177183", +"4 c #177284", +"5 c #177385", +"6 c #2E7489", +"7 c #2E768B", +"8 c #2D798F", +"9 c #2D7A90", +"0 c #2D7B91", +"q c #2D7C92", +"w c #6F7C86", +"e c #6E7C87", +"r c #707F89", +"t c #727F89", +"y c #2C879E", +"u c #1D90A6", +"i c #1D91A7", +"p c #1D93AA", +"a c #1E94AA", +"s c #1E96AC", +"d c #1E97AE", +"f c #1E98AF", +"g c #1E99B0", +"h c #2B88A0", +"j c #2B8BA2", +"k c #2C89A1", +"l c #2B8DA5", +"z c #2A8EA6", +"x c #2A8FA7", +"c c #2A90A8", +"v c #2A91A9", +"b c #319DB6", +"n c #319EB8", +"m c #319FB8", +"M c #2FA5BF", +"N c #30A0B9", +"B c #21A6C0", +"V c #21A7C1", +"C c #2FA6C0", +"Z c #2EA7C0", +"A c #21A8C1", +"S c #21A8C2", +"D c #22A9C4", +"F c #22AAC5", +"G c #27AFCA", +"H c #23B0CA", +"J c #23B0CB", +"K c #23B1CC", +"L c #23B2CE", +"P c #23B3CF", +"I c #26B1CC", +"U c #26B1CD", +"Y c #24B3CF", +"T c #25B3CF", +"R c #24B5D1", +"E c #25B6D2", +"W c #25B7D3", +"Q c #52BCD1", +"! c #53BDD1", +"~ c #54BDD1", +"^ c #55BDD1", +"/ c #B3C9CD", +"( c #B0C9CE", +") c None", +/* pixels */ +"))))))))))))%uVYYVi&))))))))))))", +")))))))))&HWWWWWWWWWWH1)))))))))", +")))))))OLWWWWWWWWWWWWWWL+)))))))", +"))))))sWWWWWWWWWWWWWWWWWWd))))))", +")))))BWWWWWWWWWWWWWWWWWWWWA)))))", +"))))BWWWWWWWWWWWWWWWWWWWWWWA))))", +")))sWWWWWWWWWWWWWWWWWWWWWWWWf)))", +"))OWWWWWWWWWWWWWWWWWWWWWWWWWW#))", +"))LWWWWvo9WWWWWWWWWWWWqoxWWWWP))", +")&WWWWWl 6WWWWWWWWWWWW7 jWWWWW4)", +")HWWWETj 6WWWWWWWWWWWW7 hTEWWWJ)", +")WWWWI*X 6WWWWWWWWWWWW7 X*GWWWW)", +"%WWWZN*X 6WWWWWWWWWWWW7 X*nCWWW2", +"uWWW>-*X 6WWWWWWWWWWWW7 X*=:WWWp", +"VWWW>-*X 6WWWWWWWWWWWW7 X*=:WWWD", +"YWQ(<;*X e((((((((((((r X*=,(~WR", +"YW!/<;*X w////////////t X*=,/^WR", +"VWWW>-*X 6WWWWWWWWWWWW7 X*=:WWWF", +"iWWW>-*X 6WWWWWWWWWWWW7 X*=:WWWa", +"&WWWMm*X 6WWWWWWWWWWWW7 X*bMWWW3", +")WWWWI*X 6WWWWWWWWWWWW7 X*GWWWW)", +")HWWWEUk 6WWWWWWWWWWWW7 yUEWWWK)", +")1WWWWWl 6WWWWWWWWWWWW7 jWWWWW5)", +"))LWWWWc.8WWWWWWWWWWWW0.zWWWWP))", +"))@WWWWWWWWWWWWWWWWWWWWWWWWWW$))", +")))dWWWWWWWWWWWWWWWWWWWWWWWWg)))", +"))))AWWWWWWWWWWWWWWWWWWWWWWS))))", +")))))AWWWWWWWWWWWWWWWWWWWWS)))))", +"))))))fWWWWWWWWWWWWWWWWWWg))))))", +")))))))#PWWWWWWWWWWWWWWP$)))))))", +")))))))))4JWWWWWWWWWWK5)))))))))", +"))))))))))))2pDRRFa3))))))))))))" +}; diff --git a/pack/tartube.1 b/pack/tartube.1 deleted file mode 100644 index da0e354..0000000 --- a/pack/tartube.1 +++ /dev/null @@ -1,18 +0,0 @@ -.TH man 1 "3 Mar 2020" "2.0.006" "tartube man page" -.SH NAME -tartube \- GUI front-end for youtube-dl -.SH SYNOPSIS -tartube -.SH DESCRIPTION -Tartube is a GUI front-end for youtube-dl, partly based on youtube-dl-gui and -written in Python 3 / Gtk 3. It's a convenient way to download videos from -YouTube and hundreds of other sites (or just to fetch a list of those videos, -if you prefer). -.SH OPTIONS -The tartube executable does not take any options. -.SH SEE ALSO -youtube-dl(8) -.SH BUGS -No known bugs. -.SH AUTHOR -A S Lewis (aslewis@cpan.org) diff --git a/pack/tartube.desktop b/pack/tartube.desktop deleted file mode 100644 index 4930170..0000000 --- a/pack/tartube.desktop +++ /dev/null @@ -1,9 +0,0 @@ -[Desktop Entry] -Name=Tartube -Version=2.0.006 -Exec=tartube -Icon=tartube -Type=Application -Categories=Network -Comment=GUI front-end for youtube-dl -Terminal=false diff --git a/pack/tartube.png b/pack/tartube.png deleted file mode 100644 index 9d387dd..0000000 Binary files a/pack/tartube.png and /dev/null differ diff --git a/pack/tartube.xpm b/pack/tartube.xpm deleted file mode 100644 index eefd693..0000000 --- a/pack/tartube.xpm +++ /dev/null @@ -1,144 +0,0 @@ -/* XPM */ -static char *c4809e064382417083867074c9f26f42[] = { -/* columns rows colors chars-per-pixel */ -"32 32 106 2 ", -" c None", -". c #612772", -"X c #68297C", -"o c #692A7C", -"O c #6A2A7C", -"+ c #6C2B7F", -"@ c #D22020", -"# c #D32020", -"$ c #D32323", -"% c #D32626", -"& c #D63434", -"* c #D84343", -"= c #DD5F5F", -"- c #DF6969", -"; c #E37373", -": c #6E2B82", -"> c #713B82", -", c #7A3F8C", -"< c #9039AA", -"1 c #923AAD", -"2 c #963BB1", -"3 c #9E3FBA", -"4 c #824496", -"5 c #86469A", -"6 c #88479D", -"7 c #AA44C9", -"8 c #AC44CB", -"9 c #AD45CD", -"0 c #AD5AC7", -"q c #B05BCA", -"w c #B05CCA", -"e c #B447D4", -"r c #B648D8", -"t c #B649D8", -"y c #B749D9", -"u c #B849DA", -"i c #BB4ADD", -"p c #B860D3", -"a c #BF4CE2", -"s c #C24DE5", -"d c #CB51F0", -"f c #CC57F0", -"g c #CD57F0", -"h c #CD58F0", -"j c #CE5BF0", -"k c #CF5FF1", -"l c #C567E2", -"z c #C668E3", -"x c #CA69E8", -"c c #CB6AEA", -"v c #D064F1", -"b c #D165F1", -"n c #D361F7", -"m c #D26EF1", -"M c #D36EF3", -"N c #D56FF5", -"B c #D677F3", -"V c #D770F7", -"C c #D870F8", -"Z c #D971F9", -"A c #D972FA", -"S c #DE74FF", -"D c #DF78FF", -"F c #E48585", -"G c #E58989", -"H c #EAA0A0", -"J c #EAA1A1", -"K c #ECADAD", -"L c #F0BBBB", -"P c #F0BFBF", -"I c #DD8EF5", -"U c #DD82F9", -"Y c #DE90F5", -"T c #E181FF", -"R c #E38CFF", -"E c #E491FF", -"W c #E4A6F7", -"Q c #E5A8F7", -"! c #E9A3FF", -"~ c #EDB6FF", -"^ c #F2C9C9", -"/ c #F5D9D9", -"( c #ECC0F9", -") c #ECC1F9", -"_ c #F2C9FF", -"` c #F3D7FB", -"' c #F3D8FB", -"] c #F7DEFF", -"[ c #F7E2E2", -"{ c #F7E7FC", -"} c #F8E9FD", -"| c #F9EBFD", -" . c #F9ECFD", -".. c #FAEDFF", -"X. c #FBEFFF", -"o. c #FBF3F3", -"O. c #FBF3FE", -"+. c #FCF6FE", -"@. c #FCF7FE", -"#. c #FCF6FF", -"$. c #FDFBFB", -"%. c #FCF8FE", -"&. c #FDF8FF", -"*. c #FEFDFF", -"=. c #FEFEFE", -"-. c white", -/* pixels */ -" 5 q c Z V c 0 4 ", -" 6 N S S S S S S S S S S M 4 ", -" , Z S S S S S S S S S S S S S S N > ", -" p S S S S S S S S S S S S S S S S S S w ", -" c S S S S S S S S S S S S S S S S S S S S z ", -" c S S S S S S S S S S S S S S S S S S S S S n e ", -" p S S S S S S S S S S S S S S S S S S S S S n d d 3 ", -" , S S S S S S S S S S S S S S S S S S S S S n d d d d . ", -" Z S S S S S S S S E R S S S S S S S S S S n d d d d d s ", -" 6 S S S S S S S S S X.-._ T S S S S S S S n d d d d d d d : ", -" N S S S S S S S S S +.-.-.@.~ D S S S S n d d d d d d d d i ", -" S S S S S S S S S S +.-.-.-.-.X.! S S n d d d d d d d d d d ", -"5 S S S S S S S S S S +.-.J J $.-.-.] U d d d d d d d d d d d + ", -"q S S S S S S S S S S +.-.; # * P -.-.-.( v d d d d d d d d d 1 ", -"c S S S S S S S S S S +.-.; @ # @ = / -.-.%.Q g d d d d d d d 8 ", -"Z S S S S S S S S S S +.-.; @ # # @ @ F -.-.-.} j d d d d d d u ", -"C S S S S S S S S S S @.-.; # # # # & K -.-.-.` f d d d d d d e ", -"x S S S S S S S S S S @.-.; # @ % G o.-.-.X.I d d d d d d d d 7 ", -"0 S S S S S S S S S S @.-.; @ - [ -.-.%.Q h d d d d d d d d d < ", -"4 S S S S S S S S S S @.-.L ^ -.-.-.( v d d d d d d d d d d d O ", -" S S S S S S S S S S @.-.-.-.-.' B d d d d d d d d d d d d d ", -" m S S S S S S S S n O.-.-.} I d d d d d d d d d d d d d d y ", -" 4 S S S S S S S n d { @.W f d d d d d d d d d d d d d d d O ", -" V S S S S S n d d v k d d d d d d d d d d d d d d d d a ", -" > S S S S n d d d d d d d d d d d d d d d d d d d d d ", -" w S S n d d d d d d d d d d d d d d d d d d d d d 2 ", -" l n d d d d d d d d d d d d d d d d d d d d d 9 ", -" e d d d d d d d d d d d d d d d d d d d d 9 ", -" 3 d d d d d d d d d d d d d d d d d d 2 ", -" . s d d d d d d d d d d d d d d a ", -" : i d d d d d d d d d d u O ", -" : 1 7 y y 7 < O " -}; diff --git a/screenshots/example1.png b/screenshots/example1.png deleted file mode 100644 index 588ce9a..0000000 Binary files a/screenshots/example1.png and /dev/null differ diff --git a/screenshots/example10.png b/screenshots/example10.png deleted file mode 100644 index 3cb3157..0000000 Binary files a/screenshots/example10.png and /dev/null differ diff --git a/screenshots/example11.png b/screenshots/example11.png deleted file mode 100644 index 5bd2bb0..0000000 Binary files a/screenshots/example11.png and /dev/null differ diff --git a/screenshots/example12.png b/screenshots/example12.png deleted file mode 100644 index 95c0dfb..0000000 Binary files a/screenshots/example12.png and /dev/null differ diff --git a/screenshots/example13.png b/screenshots/example13.png deleted file mode 100644 index 0dc5c7c..0000000 Binary files a/screenshots/example13.png and /dev/null differ diff --git a/screenshots/example14.png b/screenshots/example14.png deleted file mode 100644 index b0ddb0c..0000000 Binary files a/screenshots/example14.png and /dev/null differ diff --git a/screenshots/example15.png b/screenshots/example15.png deleted file mode 100644 index f7267c1..0000000 Binary files a/screenshots/example15.png and /dev/null differ diff --git a/screenshots/example16.png b/screenshots/example16.png deleted file mode 100644 index ac14abf..0000000 Binary files a/screenshots/example16.png and /dev/null differ diff --git a/screenshots/example17.png b/screenshots/example17.png deleted file mode 100644 index ad35e1c..0000000 Binary files a/screenshots/example17.png and /dev/null differ diff --git a/screenshots/example18.png b/screenshots/example18.png deleted file mode 100644 index b403d9f..0000000 Binary files a/screenshots/example18.png and /dev/null differ diff --git a/screenshots/example19.png b/screenshots/example19.png deleted file mode 100644 index 4f340e3..0000000 Binary files a/screenshots/example19.png and /dev/null differ diff --git a/screenshots/example2.png b/screenshots/example2.png deleted file mode 100644 index e28e4d7..0000000 Binary files a/screenshots/example2.png and /dev/null differ diff --git a/screenshots/example3.png b/screenshots/example3.png deleted file mode 100644 index 3e70e7c..0000000 Binary files a/screenshots/example3.png and /dev/null differ diff --git a/screenshots/example4.png b/screenshots/example4.png deleted file mode 100644 index 866ed86..0000000 Binary files a/screenshots/example4.png and /dev/null differ diff --git a/screenshots/example5.png b/screenshots/example5.png deleted file mode 100644 index af3c1c4..0000000 Binary files a/screenshots/example5.png and /dev/null differ diff --git a/screenshots/example6.png b/screenshots/example6.png deleted file mode 100644 index 557673d..0000000 Binary files a/screenshots/example6.png and /dev/null differ diff --git a/screenshots/example7.png b/screenshots/example7.png deleted file mode 100644 index cd9811a..0000000 Binary files a/screenshots/example7.png and /dev/null differ diff --git a/screenshots/example8.png b/screenshots/example8.png deleted file mode 100644 index c253fe8..0000000 Binary files a/screenshots/example8.png and /dev/null differ diff --git a/screenshots/example9.png b/screenshots/example9.png deleted file mode 100644 index bb46880..0000000 Binary files a/screenshots/example9.png and /dev/null differ diff --git a/screenshots/gymbob.png b/screenshots/gymbob.png new file mode 100644 index 0000000..255a667 Binary files /dev/null and b/screenshots/gymbob.png differ diff --git a/screenshots/gymbob2.png b/screenshots/gymbob2.png new file mode 100644 index 0000000..2033245 Binary files /dev/null and b/screenshots/gymbob2.png differ diff --git a/screenshots/tartube.png b/screenshots/tartube.png deleted file mode 100644 index e082f4d..0000000 Binary files a/screenshots/tartube.png and /dev/null differ diff --git a/setup.py b/setup.py index 4919212..c0040c5 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# Copyright (C) 2019-2020 A S Lewis +# Copyright (C) 2019-z2020 A S Lewis # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software @@ -28,76 +28,36 @@ import sys # Set a standard long_description, modified only for Debian/RPM packages long_description=""" -Tartube is a GUI front-end for youtube-dl, partly based on youtube-dl-gui -and written in Python 3 / Gtk 3. - -- You can download individual videos, and even whole channels and -playlists, from YouTube and hundreds of other websites -- You can fetch information about those videos, channels and playlists, -without actually downloading anything -- Tartube will organise your videos into convenient folders -- 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 -""" - -alt_description = """ -Tartube is a GUI front-end for youtube-dl, partly based on youtube-dl-gui -and written in Python 3 / Gtk 3. +Designed for use at the gym, GymBob prompts the user (graphically and using +sound effects) at regular intervals during a workout. The workout programmes +are completely customistable. GymBob is written in Python 3 / Gtk 3 and runs +on Linux/*BSD. """ # data_files for setuptools.setup are added here param_list = [] -# For Debian/RPM packaging, use environment variables -# For example, the package maintainer might use either of the following: -# TARTUBE_PKG=1 python3 setup.py build -# TARTUBE_PKG_STRICT=1 python3 setup.py build -# (Specifying both variables is the same as specifying TARTUBE_PKG_STRICT -# alone) -# -# There are three executables: the default one in ../tartube, and two -# alternative ones in ../pack/bin and ../pack/bin_strict -# If TARTUBE_PKG_STRICT is specified, then ../pack/bin_strict/tartube is the -# executable, which means that youtube-dl updates are disabled. Also, icon -# files are copied into /usr/share/tartube/icons -pkg_strict_var = 'TARTUBE_PKG_STRICT' -pkg_strict_value = os.environ.get( pkg_strict_var, None ) -script_exec = os.path.join('tartube', 'tartube') +# For Debian/RPM packaging, use environment variables: +# GYMBOB_PKG=1 python3 setup.py build +script_exec = os.path.join('gymbob', 'gymbob') icon_path = '/tartube/icons/' + pkg_flag = False - -if pkg_strict_value is not None: - - if pkg_strict_value == '1': - script_exec = os.path.join('pack', 'bin_strict', 'tartube') - sys.stderr.write('youtube-dl updates are disabled in this version\n') - pkg_flag = True - - else: - sys.stderr.write( - "Unrecognised '%s=%s' environment variable!\n" % ( - pkg_strict_var, - pkg_strict_value, - ), - ) - -# If TARTUBE_PKG is specified, then ../pack/bin/tartube is the executable, -# which means that youtube-dl updates are enabled. Also, icon files are -# copied into /usr/share/tartube/icons pkg_var = 'TARTUBE_PKG' pkg_value = os.environ.get( pkg_var, None ) if pkg_value is not None: if pkg_value == '1': - script_exec = os.path.join('pack', 'bin', 'tartube') - pkg_flag = True + + # Icons must be copied into the right place + icon_path = '/usr/share/tartube/icons/' + # Add a desktop file + param_list.append(('share/applications', ['pack/gymbob.desktop'])) + param_list.append(('share/pixmaps', ['pack/gymbob.png'])) + param_list.append(('share/pixmaps', ['pack/gymbob.xpm'])) + # Add a manpage + param_list.append(('share/man/man1', ['pack/gymbob.1'])) else: sys.stderr.write( @@ -107,52 +67,29 @@ if pkg_value is not None: ), ) -# Apply changes if either environment variable was specified -if pkg_flag: - - # Icons must be copied into the right place - icon_path = '/usr/share/tartube/icons/' - # Use a shorter long description, as the standard one tends to cause errors - long_description = alt_description - # Add a desktop file - param_list.append(('share/applications', ['pack/tartube.desktop'])) - param_list.append(('share/pixmaps', ['pack/tartube.png'])) - param_list.append(('share/pixmaps', ['pack/tartube.xpm'])) - # Add a manpage - param_list.append(('share/man/man1', ['pack/tartube.1'])) - # For PyPI installations and Debian/RPM packaging, copy everything in ../icons # into a suitable location -subdir_list = [ - 'dialogue', - 'large', - 'locale', - 'small', - 'status', - 'toolbar', - 'win', -] - +subdir_list = ['win'] for subdir in subdir_list: for path in glob.glob('icons/' + subdir + '/*'): param_list.append((icon_path + subdir + '/', [path])) # Setup setuptools.setup( - name='tartube', - version='2.0.006', - description='GUI front-end for youtube-dl', + name='gymbob', + version='1.002', + description='Simple script to prompt the user during a workout', long_description=long_description, long_description_content_type='text/plain', - url='https://tartube.sourceforge.io', + url='https://gymbob.sourceforge.io', author='A S Lewis', author_email='aslewis@cpan.org', # license=license, license="""GPLv3+""", classifiers=[ - 'Development Status :: 5 - Production/Stable', + 'Development Status :: 4 - Beta', 'Intended Audience :: End Users/Desktop', - 'Topic :: Multimedia :: Video', + 'Topic :: Games/Entertainment', 'License :: OSI Approved' \ + ' :: GNU General Public License v3 or later (GPLv3+)', 'Programming Language :: Python :: 3', @@ -161,17 +98,17 @@ setuptools.setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], - keywords='tartube video download youtube', + keywords='gymbob gym workout', packages=setuptools.find_packages( - exclude=('docs', 'icons', 'nsis', 'tests'), + exclude=('docs', 'icons', 'tests'), ), include_package_data=True, python_requires='>=3.0, <4', - install_requires=['requests'], + install_requires=['playsound'], scripts=[script_exec], project_urls={ - 'Bug Reports': 'https://github.com/axcore/tartube/issues', - 'Source': 'https://github.com/axcore/tartube', + 'Bug Reports': 'https://github.com/axcore/gymbob/issues', + 'Source': 'https://github.com/axcore/gymbob', }, data_files=param_list, ) diff --git a/sounds/COPYING b/sounds/COPYING new file mode 100644 index 0000000..4060891 --- /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 0000000..735d87b 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 0000000..bfbd9bb 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 0000000..a87b7ee 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 0000000..d2f8a52 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 0000000..cbb9ff8 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 0000000..f9bf4f8 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 0000000..fedb4d9 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 0000000..4463dfb 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 0000000..879ce6d 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 0000000..70a52bb 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 0000000..afe0999 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 0000000..d01d599 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 0000000..008fb67 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 0000000..8bc82f8 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 0000000..8dc12ce 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 0000000..e707ea2 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 0000000..b6ebf68 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 0000000..7751f7b 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 0000000..e5638cf 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 0000000..b9e02f4 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 0000000..ee7b8b9 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 0000000..b5ed30b 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 0000000..a6e73e8 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 0000000..4d16e6d 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 0000000..8725560 Binary files /dev/null and b/sounds/teaspoon.mp3 differ diff --git a/tartube/__init__.py b/tartube/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tartube/config.py b/tartube/config.py deleted file mode 100644 index 34ff899..0000000 --- a/tartube/config.py +++ /dev/null @@ -1,10869 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Configuration window classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, GdkPixbuf - - -# Import other modules -import os - - -# Import our modules -import __main__ -import formats -import mainapp -import mainwin -import media -import re -import utils - - -# Classes - - -class GenericConfigWin(Gtk.Window): - - """Generic Python class for windows in which the user can modify various - settings. - - Inherited by two types of window - 'preference windows' (in which changes - are applied immediately), and 'edit windowS' (in which changes are stored - temporarily, and only applied once the user has finished making changes. - """ - - - # Standard class methods - - -# def __init__(): # Provided by child object - - - # Public class methods - - - def setup(self): - - """Called by self.__init__(). - - Sets up the config window when it opens. - """ - - # Set the default window size - self.set_default_size( - self.app_obj.config_win_width, - self.app_obj.config_win_height, - ) - - # Set the window's Gtk icon list - self.set_icon_list(self.app_obj.main_win_obj.win_pixbuf_list) - - # Set up main widgets - self.setup_grid() - self.setup_notebook() - self.setup_button_strip() - self.setup_gap() - - # Set up tabs - self.setup_tabs() - - # Procedure complete - self.show_all() - - # Inform the main window of this window's birth (so that Tartube - # doesn't allow a download/update/refresh/info/tidy operation to - # start until all configuration windows have closed) - self.app_obj.main_win_obj.add_child_window(self) - # Add a callback so we can inform the main window of this window's - # destruction - self.connect('destroy', self.close) - - - def setup_grid(self): - - """Called by self.setup(). - - Sets up a Gtk.Grid, on which a notebook and a button strip will be - placed. (Each of the notebook's tabs also has its own Gtk.Grid.) - """ - - self.grid = Gtk.Grid() - self.add(self.grid) - - - def setup_notebook(self): - - """Called by self.setup(). - - Sets up a Gtk.Notebook, after which self.setup_tabs() is called to fill - it with tabs. - """ - - self.notebook = Gtk.Notebook() - self.grid.attach(self.notebook, 0, 1, 1, 1) - self.notebook.set_border_width(self.spacing_size) - # It shouldn't be necessary to scroll the notebook's tabs, but we'll - # make it possible anyway - self.notebook.set_scrollable(True) - - - def add_notebook_tab(self, name, border_width=None): - - """Called by various functions in the child edit/preference window. - - Adds a tab to the main Gtk.Notebook, creating a Gtk.Grid inside it, on - which the calling function can add more widgets. - - Args: - - name (str): The name of the tab - - border_width (int): If specified, the border width for the - Gtk.Grid contained in this tab (usually specified when an inner - Gtk.Notebook is to be added to this tab). If not specified, a - default width is used - - Returns: - - The tab created (in the form of a Gtk.Box) and its Gtk.Grid. - - """ - - if border_width is None: - border_width = self.spacing_size - - tab = Gtk.Box() - self.notebook.append_page(tab, Gtk.Label.new_with_mnemonic(name)) - tab.set_hexpand(True) - tab.set_vexpand(True) - tab.set_border_width(self.spacing_size) - - scrolled = Gtk.ScrolledWindow() - tab.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - grid = Gtk.Grid() - scrolled.add_with_viewport(grid) - grid.set_border_width(border_width) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - return tab, grid - - - def add_inner_notebook(self, grid): - - """Called by various functions in the child edit/preference window. - - Adds an inner Gtk.Notebook to a tab inside the main Gtk.Notebook. - - Args: - - grid (Gtk.Grid): The widget to which the notebook is added - - Returns: - - Returns the new Gtk.Notebook - - """ - - inner_notebook = Gtk.Notebook() - grid.attach(inner_notebook, 0, 1, 1, 1) - # It shouldn't be necessary to scroll the notebook's tabs, but we'll - # make it possible anyway - inner_notebook.set_scrollable(True) - - return inner_notebook - - - def add_inner_notebook_tab(self, name, notebook): - - """Called by various functions in the child edit/preference window. - - A modified form of self.add_notebook_tab, for tabs to be placd in the - inner notebook created by a call to self.add_inner_notebook. - - Adds a tab to the specified Gtk.Notebook, creating a Gtk.Grid inside - it, on which the calling function can add more widgets. - - Args: - - name (str): The name of the tab - - notebook (Gtk.Notebook): The notebook to which the tab is added - - Returns: - - The tab created (in the form of a Gtk.Box) and its Gtk.Grid. - - """ - - tab = Gtk.Box() - notebook.append_page(tab, Gtk.Label.new_with_mnemonic(name)) - tab.set_hexpand(True) - tab.set_vexpand(True) - - scrolled = Gtk.ScrolledWindow() - tab.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - grid = Gtk.Grid() - scrolled.add_with_viewport(grid) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - return tab, grid - - -# def setup_button_strip(): # Provided by child object - - - def setup_gap(self): - - """Called by self.setup(). - - Adds an empty box beneath the button strip for aesthetic purposes. - """ - - hbox = Gtk.HBox() - self.grid.attach(hbox, 0, 3, 1, 1) - hbox.set_border_width(self.spacing_size) - - - def close(self, also_self): - - """Called from callback in self.setup(). - - Inform the main window that this window is closing. - - Args: - - also_self (an object inheriting from config.GenericConfigWin): - Another copy of self - - """ - - self.app_obj.main_win_obj.del_child_window(self) - - - # (Add widgets) - - - def add_treeview(self, grid, x, y, wid, hei): - - """Called by various functions in the child preference/edit window. - - Adds a Gtk.Treeview to the tab's Gtk.Grid. No callback function is - created by this function; it's up to the calling code to supply one. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - A list containing the treeview widget and liststore created - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - treeview = Gtk.TreeView() - scrolled.add(treeview) - treeview.set_headers_visible(False) - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - '', - renderer_text, - text=0, - ) - treeview.append_column(column_text) - - liststore = Gtk.ListStore(str) - treeview.set_model(liststore) - - return treeview, liststore - - -class GenericEditWin(GenericConfigWin): - - """Generic Python class for windows in which the user can modify various - settings in a class object (such as a media.Video or an - options.OptionsManager object). - - The modifications are stored temporarily, and only applied once the user - has finished making changes. - """ - - - # Standard class methods - - -# def __init__(): # Provided by child object - - - # Public class methods - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - - def setup_button_strip(self): - - """Called by self.setup(). - - Creates a strip of buttons at the bottom of the window. Any changes the - user has made are applied by clicking the 'OK' or 'Apply' buttons, and - cancelled by using the 'Reset' or 'Cancel' buttons. - - The window is closed by using the 'OK' and 'Cancel' buttons. - - If self.multi_button_flag is True, only the 'OK' button is created. - """ - - hbox = Gtk.HBox() - self.grid.attach(hbox, 0, 2, 1, 1) - - if self.multi_button_flag: - - # 'Reset' button - 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', - ); - self.reset_button.connect('clicked', self.on_button_reset_clicked) - - # 'Apply' button - 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', - ); - self.apply_button.connect('clicked', self.on_button_apply_clicked) - - # 'OK' button - 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.connect('clicked', self.on_button_ok_clicked) - - if self.multi_button_flag: - - # 'Cancel' button - 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.connect( - 'clicked', - self.on_button_cancel_clicked, - ) - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - """ - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - setattr(self.edit_obj, self.edit_dict[key]) - - # The changes can now be cleared - self.edit_dict = {} - - - def reset_with_new_edit_obj(self, new_edit_obj): - - """Can be called by anything. - - Resets the object whose values are being edited in this window, i.e. - self.edit_obj, to the specified object. - - Then redraws the window itself, as if the user had clicked the 'Reset' - button at the bottom of the window. This makes new_edit_obj's IVs - visible in the edit window, without the need to destroy the old one and - replace it with a new one. - - Args: - - new_edit_obj (class): The replacement edit object - - """ - - self.edit_obj = new_edit_obj - - # The rest of this function is copied from - # self.on_button_reset_clicked() - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Empty self.edit_dict, destroying any changes the user has made - self.edit_dict = {} - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - def retrieve_val(self, name): - - """Can be called by anything. - - Any changes the user has made are temporarily stored in self.edit_dict. - - Each key corresponds to an attribute in the object being edited, - self.edit_obj. - - If 'name' exists as a key in that dictionary, retrieve the - corresponding value and return it. Otherwise, the user hasn't yet - modified the value, so retrieve directly from the attribute in the - object being edited. - - Args: - - name (str): The name of the attribute in the object being edited - - Returns: - - The original or modified value of that attribute. - - """ - - if name in self.edit_dict: - return self.edit_dict[name] - else: - return getattr(self.edit_obj, name) - - - # (Add widgets) - - - def add_checkbutton(self, grid, text, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.CheckButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (string or None): The text to display in the checkbutton's - label. No label is used if 'text' is an empty string or None - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The checkbutton widget created - - """ - - checkbutton = Gtk.CheckButton() - grid.attach(checkbutton, x, y, wid, hei) - checkbutton.set_hexpand(True) - if text is not None and text != '': - checkbutton.set_label(text) - - if prop is not None: - checkbutton.set_active(self.retrieve_val(prop)) - checkbutton.connect('toggled', self.on_checkbutton_toggled, prop) - - return checkbutton - - - def add_combo(self, grid, combo_list, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a simple Gtk.ComboBox to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - combo_list (list): A list of values to display in the combobox. - This function expects a simple, one-dimensional list. For - something more complex, see self.add_combo_with_data() - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The combobox widget created - - """ - - store = Gtk.ListStore(str) - for string in combo_list: - store.append( [string] ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, x, y, wid, hei) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_entry_text_column(0) - - if prop is not None: - val = self.retrieve_val(prop) - index = combo_list.index(val) - combo.set_active(index) - - combo.connect('changed', self.on_combo_changed, prop) - - return combo - - - def add_combo_with_data(self, grid, combo_list, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a more complex Gtk.ComboBox to the tab's Gtk.Grid. This function - expects a list of values in the form - - [ [val1, val2], [val1, val2], ... ] - - The combobox displays the 'val1' values. If one of them is selected, - the corresponding 'val2' is used to set the attribute described by - 'prop'. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - combo_list (list): The list described above. For something more - simple, see self.add_combo() - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The combobox widget created - - """ - - store = Gtk.ListStore(str, str) - - index_list = [] - for mini_list in combo_list: - store.append( [ mini_list[0], mini_list[1] ] ) - index_list.append(mini_list[1]) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, x, y, wid, hei) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_entry_text_column(0) - - if prop is not None: - val = self.retrieve_val(prop) - index = index_list.index(val) - combo.set_active(index) - - combo.connect('changed', self.on_combo_with_data_changed, prop) - - return combo - - - def add_entry(self, grid, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.Entry to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The entry widget created - - """ - - entry = Gtk.Entry() - grid.attach(entry, x, y, wid, hei) - entry.set_hexpand(True) - - if prop is not None: - value = self.retrieve_val(prop) - if value is not None: - entry.set_text(str(value)) - - entry.connect('changed', self.on_entry_changed, prop) - - 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_label(self, grid, text, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.Label to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (str): Pango markup displayed in the label - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The label widget created - - """ - - label = Gtk.Label() - grid.attach(label, x, y, wid, hei) - label.set_markup(text) - label.set_hexpand(True) - label.set_alignment(0, 0.5) - - return label - - - def add_pixbuf(self, grid, pixbuf_name, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.Image to the tab's Gtk.Grid. A modified version of - self.add_image(), which is called with a path to an image file; this - function is called with one of the pixbuf names specified by - mainwin.MainWin.pixbuf_dict, e.g. 'video_both_large'. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - pixbuf_name (str): One of the keys in - mainwin.MainWin.pixbuf_dict, e.g. 'video_both_large'. - - 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) - - main_win_obj = self.app_obj.main_win_obj - if pixbuf_name in main_win_obj.pixbuf_dict: - image.set_from_pixbuf(main_win_obj.pixbuf_dict[pixbuf_name]) - else: - # Unrecognised pixbuf name - image.set_from_pixbuf( - main_win_obj.pixbuf_dict['question_large'], - ) - - return frame - - - def add_radiobutton(self, grid, prev_button, text, prop, value, x, y, \ - wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.RadioButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - prev_button (Gtk.RadioButton or None): When this is the first - radio button in the group, None. Otherwise, the previous - radio button in the group. Use of this IV links the radio - buttons together, ensuring that only one of them can be active - at any time - - text (string or None): The text to display in the radiobutton's - label. No label is used if 'text' is an empty string or None - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - value (any): When this radiobutton becomes the active one, and if - 'prop' is not None, then 'prop' and 'value' are added as a new - key-value pair to self.edit_dict - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The radiobutton widget created - - """ - - radiobutton = Gtk.RadioButton.new_from_widget(prev_button) - grid.attach(radiobutton, x, y, wid, hei) - radiobutton.set_hexpand(True) - if text is not None and text != '': - radiobutton.set_label(text) - - if prop is not None: - if value is not None and self.retrieve_val(prop) == value: - radiobutton.set_active(True) - - radiobutton.connect( - 'toggled', - self.on_radiobutton_toggled, prop, value, - ) - - return radiobutton - - - def add_spinbutton(self, grid, min_val, max_val, step, prop, x, y, wid, \ - hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.SpinButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - min_val (int): The minimum permitted in the spinbutton - - max_val (int or None): The maximum values permitted in the - spinbutton. If None, this function assigns a very large maximum - value (a billion) - - step (int): Clicking the up/down arrows in the spin button - increments/decrements the value by this much - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. If - None, no changes are made to self.edit_dict; it's up to the - calling function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The spinbutton widget created - - """ - - # If the specified value of 'max_valu' was none, just use a very big - # number (as Gtk.SpinButton won't accept the None argument) - if max_val is None: - max_val = 1000000000 - - spinbutton = Gtk.SpinButton.new_with_range(min_val, max_val, step) - grid.attach(spinbutton, x, y, wid, hei) - spinbutton.set_hexpand(False) - - if prop is not None: - spinbutton.set_value(self.retrieve_val(prop)) - spinbutton.connect( - 'value-changed', - self.on_spinbutton_changed, - prop, - ) - - return spinbutton - - - def add_textview(self, grid, prop, x, y, wid, hei): - - """Called by various functions in the child edit window. - - Adds a Gtk.TextView to the tab's Gtk.Grid. The contents of the textview - are used as a single string (perhaps including newline characters) to - set the value of a string attribute. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - prop (string or None): The name of the attribute in self.edit_obj - whose value will be set to the contents of this widget. The - attribute can be an integer, string, list or tuple. If None, no - changes are made to self.edit_dict; it's up to the calling - function to provide a .connect() - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The textview and textbuffer widgets created - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - textview = Gtk.TextView() - scrolled.add(textview) - - textbuffer = textview.get_buffer() - - if prop is not None: - value = self.retrieve_val(prop) - if value is not None: - if type(value) is list or type(value) is tuple: - textbuffer.set_text(str.join('\n', value)) - else: - textbuffer.set_text(str(value)) - - textbuffer.connect('changed', self.on_textview_changed, prop) - - return textview, textbuffer - - -# def add_treeview # Inherited from GenericConfigWin - - - # Callback class methods - - - def on_button_apply_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Applies any changes made by the user and re-draws the window's tabs, - showing their new values. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Apply any changes the user has made - self.apply_changes() - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - def on_button_cancel_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Destroys any changes made by the user and re-draws the window's tabs, - showing their original values. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Destroy the window - self.destroy() - - - def on_button_ok_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Destroys any changes made by the user and then closes the window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Apply any changes the user has made - self.apply_changes() - - # Destroy the window - self.destroy() - - - def on_button_reset_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Destroys any changes made by the user and re-draws the window's tabs, - showing their original values. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Empty self.edit_dict, destroying any changes the user has made - self.edit_dict = {} - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - def on_checkbutton_toggled(self, checkbutton, prop): - - """Called from a callback in self.add_checkbutton(). - - Adds a key-value pair to self.edit_dict, using True if the button is - selected, False if not. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - if not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - - def on_combo_changed(self, combo, prop): - - """Called from a callback in self.add_combo(). - - Temporarily stores the contents of the widget in self.edit_dict. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.edit_dict[prop] = model[tree_iter][0] - - - def on_combo_with_data_changed(self, combo, prop): - - """Called from a callback in self.add_combo_with_data(). - - Extracts the value visible in the widget, converts it into another - value, and stores the later in self.edit_dict. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.edit_dict[prop] = model[tree_iter][1] - - - def on_entry_changed(self, entry, prop): - - """Called from a callback in self.add_entry(). - - Temporarily stores the contents of the widget in self.edit_dict. - - Args: - - entry (Gtk.Entry): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - self.edit_dict[prop] = entry.get_text() - - - def on_radiobutton_toggled(self, checkbutton, prop, value): - - """Called from a callback in self.add_radiobutton(). - - Adds a key-value pair to self.edit_dict, but only if this radiobutton - (from those in the group) is the selected one. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - value (-): The attribute's new value - - """ - - if radiobutton.get_active(): - self.edit_dict[prop] = value - - - def on_spinbutton_changed(self, spinbutton, prop): - - """Called from a callback in self.add_spinbutton(). - - Temporarily stores the contents of the widget in self.edit_dict. - - Args: - - spinbutton (Gtk.SpinkButton): The widget clicked - - prop (str): The attribute in self.edit_obj to modify - - """ - - self.edit_dict[prop] = int(spinbutton.get_value()) - - - def on_textview_changed(self, textbuffer, prop): - - """Called from a callback in self.add_textview(). - - Temporarily stores the contents of the widget in self.edit_dict. - - Args: - - textbuffer (Gtk.TextBuffer): The widget modified - - prop (str): The attribute in self.edit_obj to modify - - """ - - text = textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - old_value = self.retrieve_val(prop) - - if type(old_value) is list: - self.edit_dict[prop] = text.split() - elif type(old_value) is tuple: - self.edit_dict[prop] = text.split() - else: - self.edit_dict[prop] = text - - - # (Inherited by VideoEditWin, ChannelPlaylistEditWin and FolderEditWin) - - - def add_container_properties(self, grid): - - """Called by VideoEditWin.setup_general_tab(), - ChannelPlaylistEditWin.setup_general_tab() and - FolderEditWin.setup_general_tab(). - - Adds widgets common to those edit windows. - - Args: - - grid (Gtk.Grid): The grid on which widgets are arranged in their - tab - - """ - - entry = self.add_entry(grid, - None, - 0, 1, 1, 1, - ) - entry.set_text('#' + str(self.edit_obj.dbid)) - entry.set_editable(False) - entry.set_hexpand(False) - entry.set_width_chars(8) - - main_win_obj = self.app_obj.main_win_obj - if isinstance(self.edit_obj, media.Video): - icon_path = main_win_obj.icon_dict['video_small'] - elif isinstance(self.edit_obj, media.Channel): - icon_path = main_win_obj.icon_dict['channel_small'] - elif isinstance(self.edit_obj, media.Playlist): - icon_path = main_win_obj.icon_dict['playlist_small'] - else: - - if self.edit_obj.priv_flag: - icon_path = main_win_obj.icon_dict['folder_red_small'] - elif self.edit_obj.temp_flag: - icon_path = main_win_obj.icon_dict['folder_blue_small'] - elif self.edit_obj.fixed_flag: - icon_path = main_win_obj.icon_dict['folder_green_small'] - else: - icon_path = main_win_obj.icon_dict['folder_small'] - - frame = self.add_image(grid, - icon_path, - 1, 1, 1, 1, - ) - # (The frame looks cramped without this. The icon itself is 16x16) - frame.set_size_request( - 16 + (self.spacing_size * 2), - -1, - ) - - entry2 = self.add_entry(grid, - 'name', - 2, 1, 1, 1, - ) - entry2.set_editable(False) - - label = self.add_label(grid, - 'Listed as', - 0, 2, 1, 1, - ) - label.set_hexpand(False) - - entry3 = self.add_entry(grid, - 'nickname', - 2, 2, 1, 1, - ) - entry3.set_editable(False) - - label2 = self.add_label(grid, - 'Contained in', - 0, 3, 1, 1, - ) - label2.set_hexpand(False) - - parent_obj = self.edit_obj.parent_obj - if parent_obj: - if isinstance(parent_obj, media.Channel): - icon_path2 = main_win_obj.icon_dict['channel_small'] - elif isinstance(parent_obj, media.Playlist): - icon_path2 = main_win_obj.icon_dict['playlist_small'] - else: - - if parent_obj.priv_flag: - icon_path2 = main_win_obj.icon_dict['folder_red_small'] - elif parent_obj.temp_flag: - icon_path2 = main_win_obj.icon_dict['folder_blue_small'] - elif parent_obj.fixed_flag: - icon_path2 = main_win_obj.icon_dict['folder_green_small'] - else: - icon_path2 = main_win_obj.icon_dict['folder_small'] - - else: - icon_path2 = main_win_obj.icon_dict['folder_black_small'] - - frame2 = self.add_image(grid, - icon_path2, - 1, 3, 1, 1, - ) - frame2.set_size_request( - 16 + (self.spacing_size * 2), - -1, - ) - - entry4 = self.add_entry(grid, - None, - 2, 3, 1, 1, - ) - entry4.set_editable(False) - if parent_obj: - entry4.set_text(parent_obj.name) - - - def add_source_properties(self, grid): - - """Called by VideoEditWin.setup_general_tab() and - ChannelPlaylistEditWin.setup_general_tab(). - - Adds widgets common to those edit windows. - - Args: - - grid (Gtk.Grid): The grid on which widgets are arranged in their - tab - - """ - - label2 = self.add_label(grid, - utils.upper_case_first(self.media_type) + ' URL', - 0, 4, 1, 1, - ) - label2.set_hexpand(False) - - entry5 = self.add_entry(grid, - 'source', - 1, 4, 2, 1, - ) - entry5.set_editable(False) - - - def add_destination_properties(self, grid): - - """Called by ChannelPlaylistEditWin.setup_general_tab() and - FolderEditWin.setup_general_tab(). - - Adds widgets common to those edit windows. - - Args: - - grid (Gtk.Grid): The grid on which widgets are arranged in their - tab - - """ - - # 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, - ) - label3.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'] - elif isinstance(dest_obj, media.Playlist): - icon_path3 = main_win_obj.icon_dict['playlist_small'] - else: - - if dest_obj.priv_flag: - icon_path3 = main_win_obj.icon_dict['folder_red_small'] - elif dest_obj.temp_flag: - icon_path3 = main_win_obj.icon_dict['folder_blue_small'] - elif dest_obj.fixed_flag: - icon_path3 = main_win_obj.icon_dict['folder_green_small'] - else: - icon_path3 = main_win_obj.icon_dict['folder_small'] - - frame3 = self.add_image(grid2, - icon_path3, - 1, 0, 1, 1, - ) - frame3.set_size_request( - 16 + (self.spacing_size * 2), - -1, - ) - - entry6 = self.add_entry(grid2, - None, - 2, 0, 1, 1, - ) - entry6.set_editable(False) - entry6.set_text(dest_obj.name) - - label5 = self.add_label(grid2, - 'Location on filesystem', - 0, 1, 1, 1, - ) - label5.set_hexpand(False) - - entry7 = self.add_entry(grid2, - None, - 1, 1, 2, 1, - ) - entry7.set_editable(False) - entry7.set_text(self.edit_obj.get_default_dir(self.app_obj)) - - - def setup_download_options_tab(self): - - """Called by VideoEditWin.setup_tabs(), - ChannelPlaylistEditWin.setup_tabs() and FolderEditWin.setup_tabs(). - - Sets up the 'Download options' tab. - """ - - tab, grid = self.add_notebook_tab('Download _options') - - # Download options - self.add_label(grid, - 'Download options', - 0, 0, 2, 1, - ) - - 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') - 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') - grid.attach(self.remove_button, 1, 2, 1, 1) - self.remove_button.connect( - 'clicked', - self.on_button_remove_options_clicked, - ) - - if self.edit_obj.options_obj: - self.apply_options_button.set_sensitive(False) - else: - self.edit_button.set_sensitive(False) - self.remove_button.set_sensitive(False) - - - def on_button_apply_options_clicked(self, button): - - """Called from callback in self.setup_download_options_tab(). - - Apply download options to the media data object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if self.edit_obj.options_obj: - return self.app_obj.system_error( - 401, - 'Download options already applied', - ) - - # Apply download options to the media data object - self.app_obj.apply_download_options(self.edit_obj) - # (De)sensitise buttons appropriately - self.apply_options_button.set_sensitive(False) - self.edit_options_button.set_sensitive(True) - self.remove_options_button.set_sensitive(True) - - - def on_button_edit_options_clicked(self, button): - - """Called from callback in self.setup_download_options_tab(). - - Edit download options for the media data object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if not self.edit_obj.options_obj: - return self.app_obj.system_error( - 402, - 'Download options not already applied', - ) - - # Open an edit window to show the options immediately - OptionsEditWin( - self.app_obj, - self.edit_obj.options_obj, - self.edit_obj, - ) - - - def on_button_remove_options_clicked(self, button): - - """Called from callback in self.setup_download_options_tab(). - - Remove download options from the media data object. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if not self.edit_obj.options_obj: - return self.app_obj.system_error( - 403, - 'Download options not already applied', - ) - - # Remove download options from the media data object - self.app_obj.remove_download_options(self.edit_obj) - # (De)sensitise buttons appropriately - self.apply_options_button.set_sensitive(True) - self.edit_options_button.set_sensitive(False) - self.remove_options_button.set_sensitive(False) - - -class GenericPrefWin(GenericConfigWin): - - """Generic Python class for windows in which the user can modify various - system settings. - - Any modifications are applied immediately (unlike in an 'edit window', in - which the modifications are stored temporarily, and only applied once the - user has finished making changes). - """ - - - # Standard class methods - - -# def __init__(): # Provided by child object - - - # Public class methods - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - - def setup_button_strip(self): - - """Called by self.setup(). - - Creates a strip of buttons at the bottom of the window. For preference - windows, there is only a single 'OK' button, which closes the window. - """ - - hbox = Gtk.HBox() - self.grid.attach(hbox, 0, 2, 1, 1) - - # 'OK' button - 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.connect('clicked', self.on_button_ok_clicked) - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def reset_window(self): - - """Can be called by anything. - - Redraws the window, without the need to destroy the old one and replace - it with a new one. - """ - - # This code is copied from - # config.GenericEditWin.on_button_reset_clicked() - - # Remove all existing tabs from the notebook - number = self.notebook.get_n_pages() - if number: - - for count in range(0, number): - self.notebook.remove_page(0) - - # Re-draw all the tabs - self.setup_tabs() - - # Render the changes - self.show_all() - - - # (Add widgets) - - - def add_checkbutton(self, grid, text, set_flag, mod_flag, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.CheckButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (string or None): The text to display in the checkbutton's - label. No label is used if 'text' is an empty string or None - - set_flag (bool): True if the checkbutton is selected - - mod_flag (bool): True if the checkbutton can be toggled by the user - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The checkbutton widget created - - """ - - checkbutton = Gtk.CheckButton() - grid.attach(checkbutton, x, y, wid, hei) - checkbutton.set_active(set_flag) - checkbutton.set_sensitive(mod_flag) - checkbutton.set_hexpand(True) - if text is not None and text != '': - checkbutton.set_label(text) - - return checkbutton - - - def add_combo(self, grid, combo_list, active_val, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a simple Gtk.ComboBox to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - combo_list (list): A list of values to display in the combobox. - This function expects a simple, one-dimensional list. There is - not generic self.add_combo_with_data() function for preference - windows. - - active_val (string or None): If not None, a value matching one of - the items in combo_list, that should be the active row in the - combobox - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The combobox widget created - - """ - - store = Gtk.ListStore(str) - - count = -1 - active_index = 0 - for string in combo_list: - store.append( [string] ) - - count += 1 - if active_val is not None and active_val == string: - active_index = count - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, x, y, wid, hei) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_entry_text_column(0) - combo.set_active(active_index) - - return combo - - - def add_entry(self, grid, text, edit_flag, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.Entry to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (string or None): The initial contents of the entry. - - edit_flag (bool): True if the contents of the entry can be edited - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The entry widget created - - """ - - entry = Gtk.Entry() - grid.attach(entry, x, y, wid, hei) - entry.set_hexpand(True) - - if text is not None: - entry.set_text(str(text)) - - if not edit_flag: - entry.set_editable(False) - - return entry - - - def add_label(self, grid, text, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.Label to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - text (str): Pango markup displayed in the label - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The label widget created - - """ - - label = Gtk.Label() - grid.attach(label, x, y, wid, hei) - label.set_markup(text) - label.set_hexpand(True) - label.set_alignment(0, 0.5) - - return label - - - def add_radiobutton(self, grid, prev_button, text, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.RadioButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - prev_button (Gtk.RadioButton or None): When this is the first - radio button in the group, None. Otherwise, the previous - radio button in the group. Use of this IV links the radio - buttons together, ensuring that only one of them can be active - at any time - - text (string or None): The text to display in the radiobutton's - label. No label is used if 'text' is an empty string or None - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The radiobutton widget created - - """ - - radiobutton = Gtk.RadioButton.new_from_widget(prev_button) - grid.attach(radiobutton, x, y, wid, hei) - radiobutton.set_hexpand(True) - if text is not None and text != '': - radiobutton.set_label(text) - - return radiobutton - - - def add_spinbutton(self, grid, min_val, max_val, step, val, x, y, wid, \ - hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.SpinButton to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - min_val (int): The minimum permitted in the spinbutton - - max_val (int or None): The maximum values permitted in the - spinbutton. If None, this function assigns a very large maximum - value (a billion) - - step (int): Clicking the up/down arrows in the spin button - increments/decrements the value by this much - - val (int): The current value of the spinbutton - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The spinbutton widget created - - """ - - # If the specified value of 'max_valu' was none, just use a very big - # number (as Gtk.SpinButton won't accept the None argument) - if max_val is None: - max_val = 1000000000 - - spinbutton = Gtk.SpinButton.new_with_range(min_val, max_val, step) - grid.attach(spinbutton, x, y, wid, hei) - spinbutton.set_value(val) - spinbutton.set_hexpand(False) - - return spinbutton - - - def add_textview(self, grid, contents_list, x, y, wid, hei): - - """Called by various functions in the child preference window. - - Adds a Gtk.TextView to the tab's Gtk.Grid. - - Args: - - grid (Gtk.Grid): The grid on which this widget will be placed - - contents_list (list): The initial contents of the textview. Each - item in the list is a line in the textview. - - x, y, wid, hei (int): Position on the grid at which the widget is - placed - - Returns: - - The textview and textbuffer widgets created - - """ - - frame = Gtk.Frame() - grid.attach(frame, x, y, wid, hei) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - - textview = Gtk.TextView() - scrolled.add(textview) - - textbuffer = textview.get_buffer() - - if contents_list: - textbuffer.set_text(str.join('\n', contents_list)) - - return textview, textbuffer - - -# def add_treeview # Inherited from GenericConfigWin - - - # Callback class methods - - - def on_button_ok_clicked(self, button): - - """Called from a callback in self.setup_button_strip(). - - Closes the window. - - Args: - - button (Gtk.Button): The button clicked - - """ - - # Destroy the window - self.destroy() - - -class OptionsEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in an - options.OptionsManager object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (options.OptionsManager): The object whose attributes will be - edited in this window - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder or None): The media data object which is the parent of - the object being edited. None if we're editing the General Options - Manager - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj, media_data_obj=None): - - Gtk.Window.__init__(self, title='Download options') - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The options.OptionManager object being edited - self.edit_obj = edit_obj - # The media data object which is the parent of the options manager - # object. Set to None if we are editing the General Options Manager - self.media_data_obj = media_data_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # The 'embed_subs' option appears in two different places - self.embed_checkbutton = None # Gtk.CheckButton - self.embed_checkbutton2 = None # Gtk.CheckButton - # The Gtk.ListStore containing the user's preferred video/audio formats - # (which must be redrawn when self.apply_changes() is called) - self.formats_liststore = None # Gtk.ListStore - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK' are required, or False if just the 'OK' button is required - self.multi_button_flag = True - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # In this edit window, the key-value pairs directly correspond to those - # in options.OptionsManager.options_dict, rather than corresponding - # directly to attributes in the options.OptionsManager object - # Because of that, we use our own .apply_changes() and .retrieve_val() - # functions, rather than relying on the generic functions - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - # IVs used to keep track of widget changes in the 'Files' tab - # Flag set to to False when that tab's output template widgets are - # desensitised, True when sensitised - self.template_flag = False - # A list of Gtk widgets to (de)sensitise in when the flag changes - self.template_widget_list = [] - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - - def apply_changes(self): - - """Called by self.on_button_ok_clicked() and - self.on_button_apply_clicked(). - - Any changes the user has made are temporarily stored in self.edit_dict. - Apply to those changes to the object being edited. - - In this edit window we apply changes to self.edit_obj.options_dict - (rather than to self.edit_obj's attributes directly, as in the generic - function.) - """ - - # Apply any changes the user has made - for key in self.edit_dict.keys(): - self.edit_obj.options_dict[key] = self.edit_dict[key] - - # The changes can now be cleared - self.edit_dict = {} - - # The user can specify up to 3 video/audio formats. If a mixture of - # both is specified, then video formats must be listed before audio - # formats (or youtube-dl won't donwload them all) - # Tell the options.OptionManager object to rearrange them, if - # necessary - self.edit_obj.rearrange_formats() - # ...then redraw the textview in the Formats tab - self.redraw_formats_list() - - - def retrieve_val(self, name): - - """Can be called by anything. - - Any changes the user has made are temporarily stored in self.edit_dict. - - In the generic function, each key corresponds to an attribute in the - object being edited, self.edit_obj. In this window, it corresponds to a - key in self.edit_obj.options_dict. - - If 'name' exists as a key in that dictionary, retrieve the - corresponding value and return it. Otherwise, the user hasn't yet - modified the value, so retrieve directly from the attribute in the - object being edited. - - Args: - - name (str): The name of the attribute in the object being edited - - Returns: - - The original or modified value of that attribute. - - """ - - if name in self.edit_dict: - return self.edit_dict[name] - elif name in self.edit_obj.options_dict: - return self.edit_obj.options_dict[name] - else: - return self.app_obj.system_error( - 404, - 'Unrecognised property name \'' + 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) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_general_tab() - self.setup_files_tab() - self.setup_formats_tab() - self.setup_downloads_tab() - if not self.app_obj.simple_options_flag: - self.setup_post_process_tab() - else: - self.setup_sound_only_tab() - self.setup_subtitles_tab() - if not self.app_obj.simple_options_flag: - self.setup_advanced_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - tab, grid = self.add_notebook_tab('_General') - - if self.media_data_obj: - parent_type = self.media_data_obj.get_type() - - self.add_label(grid, - 'General options', - 0, 0, 2, 1, - ) - - label = self.add_label(grid, - '', - 0, 1, 2, 1, - ) - - if self.media_data_obj is None: - - 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') - - else: - - label.set_text( - 'These options have been applied to the ' + parent_type + ':', - ) - - entry = self.add_entry(grid, - None, - 0, 2, 1, 1, - ) - entry.set_editable(False) - entry.set_hexpand(False) - entry.set_width_chars(8) - - entry2 = self.add_entry(grid, - None, - 1, 2, 1, 1, - ) - entry2.set_editable(False) - - entry.set_text('#' + str(self.media_data_obj.dbid)) - 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, - ) - - self.add_textview(grid, - 'extra_cmd_string', - 0, 4, 2, 1, - ) - - if self.app_obj.simple_options_flag: - frame = self.add_pixbuf(grid, - 'hand_right_large', - 0, 5, 1, 1, - ) - frame.set_hexpand(False) - - else: - frame = self.add_pixbuf(grid, - 'hand_left_large', - 0, 5, 1, 1, - ) - frame.set_hexpand(False) - - 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') - else: - button.set_label('Show advanced download options') - button.connect('clicked', self.on_simple_options_clicked) - - frame2 = self.add_pixbuf(grid, - 'copy_large', - 0, 6, 1, 1, - ) - frame2.set_hexpand(False) - - button2 = Gtk.Button( - 'Import general download options into this window', - ) - grid.attach(button2, 1, 6, 1, 1) - button2.connect('clicked', self.on_clone_options_clicked) - if self.edit_obj == self.app_obj.general_options_obj: - # No point cloning the General Options Manager onto itself - button2.set_sensitive(False) - - frame3 = self.add_pixbuf(grid, - 'warning_large', - 0, 7, 1, 1, - ) - frame3.set_hexpand(False) - - button3 = Gtk.Button( - 'Completely reset all download options to their default values', - ) - grid.attach(button3, 1, 7, 1, 1) - button3.connect('clicked', self.on_reset_options_clicked) - - - def setup_files_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Files' tab. - """ - - # Add this tab... - tab, grid = self.add_notebook_tab('_Files', 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_files_names_tab(inner_notebook) - self.setup_files_filesystem_tab(inner_notebook) - self.setup_files_write_files_tab(inner_notebook) - self.setup_files_keep_files_tab(inner_notebook) - - - def setup_files_names_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'File names' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('File _names', inner_notebook) - - grid_width = 5 - - # File name options - self.add_label(grid, - 'File name options', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - 'Format for video file names', - 0, 1, grid_width, 1, - ) - - store = Gtk.ListStore(int, str) - num_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] - for num in num_list: - store.append( [num, formats.FILE_OUTPUT_NAME_DICT[num]] ) - - current_format = self.edit_obj.options_dict['output_format'] - current_template = formats.FILE_OUTPUT_CONVERT_DICT[current_format] - if current_template is None: - current_template = self.edit_obj.options_dict['output_template'] - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 0, 2, grid_width, 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(num_list.index(current_format)) - # Signal connect appears below - - self.add_label(grid, - 'youtube-dl file output template', - 0, 3, grid_width, 1, - ) - - entry = self.add_entry(grid, - None, - 0, 4, grid_width, 1, - ) - entry.set_text(current_template) - # Signal connect appears below - - self.add_label(grid, - 'Add to template:', - 0, 5, 1, 1, - ) - - store2 = Gtk.ListStore(str) - for string in ( - 'ID', - 'Title', - 'Ext', - 'Uploader', - 'Resolution', - 'Autonumber', - ): - store2.append( [string] ) - - 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 - - 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 - - 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 - - # 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, - ) - - # 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, - ] - - if current_format == 0: - self.file_tab_sensitise_widgets(True) - else: - self.file_tab_sensitise_widgets(False) - - - def setup_files_filesystem_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Filesystem' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Filesystem', inner_notebook) - - grid_width = 2 - - # Filesystem options - if not self.app_obj.simple_options_flag: - - self.add_label(grid, - 'Filesystem options', - 0, 0, grid_width, 1, - ) - - self.add_checkbutton(grid, - 'Restrict filenames to using ASCII characters', - 'restrict_filenames', - 0, 1, grid_width, 1, - ) - - self.add_checkbutton(grid, - 'Set the file modification time from the server', - 'nomtime', - 0, 2, grid_width, 1, - ) - - # Filesystem overrides - self.add_label(grid, - 'Filesystem overrides', - 0, 3, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Download all videos into this folder', - None, - 0, 4, 1, 1, - ) - # Signal connect below - - # (Currently, only two fixed folders are elligible for this mode, so - # we'll just add them individually) - store = Gtk.ListStore(GdkPixbuf.Pixbuf, str) - pixbuf = self.app_obj.main_win_obj.pixbuf_dict['folder_green_small'] - store.append( [pixbuf, self.app_obj.fixed_misc_folder.name] ) - pixbuf = self.app_obj.main_win_obj.pixbuf_dict['folder_blue_small'] - store.append( [pixbuf, self.app_obj.fixed_temp_folder.name] ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 1, 4, 1, 1) - renderer_pixbuf5 = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf5, False) - combo.add_attribute(renderer_pixbuf5, 'pixbuf', 0) - renderer_text5 = Gtk.CellRendererText() - combo.pack_start(renderer_text5, True) - combo.add_attribute(renderer_text5, 'text', 1) - combo.set_entry_text_column(1) - # Signal connect below - - current_override = self.edit_obj.options_dict['use_fixed_folder'] - if current_override is None: - checkbutton.set_active(False) - combo.set_sensitive(False) - combo.set_active(0) - else: - checkbutton.set_active(True) - combo.set_sensitive(True) - if current_override == self.app_obj.fixed_temp_folder.name: - combo.set_active(1) - else: - # The value should be either None, 'Unsorted Videos' or - # 'Temporary Videos'. In case the value is anything else, - # use 'Unsorted Videos' - combo.set_active(0) - - # Signal connects from above - checkbutton.connect('toggled', self.on_fixed_folder_toggled, combo) - combo.connect('changed', self.on_fixed_folder_changed) - - - def setup_files_write_files_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Write files' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Write files', inner_notebook) - - # Write other files options - self.add_label(grid, - 'Write other file options', - 0, 0, 1, 1, - ) - - self.add_checkbutton(grid, - '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_info', - 0, 2, 1, 1, - ) - - self.add_checkbutton(grid, - '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_thumbnail', - 0, 4, 1, 1, - ) - - - def setup_files_keep_files_tab(self, inner_notebook): - - """Called by self.setup_files_tab(). - - Sets up the 'Write files' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Keep files', inner_notebook) - - script = __main__.__prettyname__ - - # Options during real (not simulated) downloads - self.add_label(grid, - 'Options during real (not simulated) downloads', - 0, 0, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep the description file after ' + script + ' shuts down', - 'keep_description', - 0, 1, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep the metadata file after ' + script + ' shuts down', - 'keep_info', - 0, 2, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep the annotations file after ' + script + ' shuts down', - 'keep_annotations', - 0, 3, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep the thumbnail file after ' + script + ' shuts down', - 'keep_thumbnail', - 0, 4, 1, 1, - ) - - # Options during simulated (not real) downloads - self.add_label(grid, - 'Options during simulated (not real) downloads', - 0, 5, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep the description file after ' + script + ' shuts down', - 'sim_keep_description', - 0, 6, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep the metadata file after ' + script + ' shuts down', - 'sim_keep_info', - 0, 7, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep the annotations file after ' + script + ' shuts down', - 'sim_keep_annotations', - 0, 8, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep the thumbnail file after ' + script + ' shuts down', - 'sim_keep_thumbnail', - 0, 9, 1, 1, - ) - - - def setup_formats_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Formats' tab. - """ - - tab, grid = self.add_notebook_tab('F_ormats') - grid_width = 4 - grid.set_column_homogeneous(True) - - # Format options - self.add_label(grid, - '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, - ) - - treeview, liststore = self.add_treeview(grid, - 0, 3, 2, 1, - ) - - for key in formats.VIDEO_OPTION_LIST: - liststore.append([key]) - - button = Gtk.Button('Add format >>>') - grid.attach(button, 0, 4, 2, 1) - # Signal connect below - - # Right column - label2 = self.add_label(grid, - 'Preference list (up to three formats)', - 2, 2, 2, 1, - ) - - treeview2, self.formats_liststore = self.add_treeview(grid, - 2, 3, 2, 1, - ) - - # (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 - - # There are three video format options, any or all of which might be - # set - self.redraw_formats_list() - - button2 = Gtk.Button('<<< Remove format') - grid.attach(button2, 2, 4, 2, 1) - # Signal connect below - - button3 = Gtk.Button('^ Move up') - grid.attach(button3, 2, 5, 1, 1) - # Signal connect below - - button4 = Gtk.Button('v Move down') - grid.attach(button4, 3, 5, 1, 1) - # Signal connect below - - # Signal connects from above - # 'Add format' - button.connect( - 'clicked', - self.on_formats_tab_add_clicked, - button2, - button3, - button4, - treeview, - ) - # 'Remove format' - button2.connect( - 'clicked', - self.on_formats_tab_remove_clicked, - button, - button3, - button4, - treeview2, - ) - # 'Move up' - button3.connect( - 'clicked', - self.on_formats_tab_up_clicked, - treeview2, - ) - # 'Move down' - button4.connect( - 'clicked', - self.on_formats_tab_down_clicked, - treeview2, - ) - - # Desensitise buttons, as appropriate - format_count = self.formats_tab_count_formats() - if format_count == 0: - button2.set_sensitive(False) - 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: - - self.add_checkbutton(grid, - 'Prefer free video formats, unless one is specified above', - 'prefer_free_formats', - 0, 6, grid_width, 1, - ) - - self.add_checkbutton(grid, - 'Do not download DASH-related data on YouTube videos', - 'yt_skip_dash', - 0, 7, grid_width, 1, - ) - - self.add_label(grid, - 'Output to this format, if merge required', - 0, 8, 2, 1, - ) - - combo_list = ['', 'flv', 'mkv', 'mp4', 'ogg', 'webm'] - self.add_combo(grid, - combo_list, - 'merge_output_format', - 2, 8, 2, 1, - ) - - - def setup_downloads_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Downloads' tab. - """ - - # Simple options only - if self.app_obj.simple_options_flag: - - tab, grid = self.add_notebook_tab('_Downloads') - - row_count = 0 - - # Download options - self.add_label(grid, - 'Download options', - 0, row_count, 1, 1, - ) - - row_count += 1 - row_count = self.downloads_age_widgets(grid, row_count) - row_count = self.downloads_date_widgets(grid, row_count) - row_count = self.downloads_views_widgets(grid, row_count) - - # All options - else: - - # Add this tab... - tab, grid = self.add_notebook_tab('_Downloads', 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_downloads_general_tab(inner_notebook) - self.setup_downloads_playlists_tab(inner_notebook) - self.setup_downloads_size_limits_tab(inner_notebook) - self.setup_downloads_dates_tab(inner_notebook) - self.setup_downloads_views_tab(inner_notebook) - self.setup_downloads_filtering_tab(inner_notebook) - self.setup_downloads_external_tab(inner_notebook) - - - def setup_downloads_general_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'General' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_General', inner_notebook) - - # Download options - self.add_label(grid, - 'Download options', - 0, 0, 1, 1, - ) - - row_count = 1 - row_count = self.downloads_general_widgets(grid, row_count) - row_count = self.downloads_age_widgets(grid, row_count) - - - def setup_downloads_playlists_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Playlists' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Playlists', inner_notebook) - - row_count = self.downloads_playlist_widgets(grid, 0) - - - def setup_downloads_size_limits_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Size limits' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Size limits', inner_notebook) - - row_count = self.downloads_size_limit_widgets(grid, 0) - - - def setup_downloads_dates_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Dates' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Dates', inner_notebook) - - row_count = self.downloads_date_widgets(grid, 0) - - - def setup_downloads_views_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Views' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Views', inner_notebook) - - row_count = self.downloads_views_widgets(grid, 0) - - - def setup_downloads_filtering_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'Filtering' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Filtering', inner_notebook) - - row_count = self.downloads_filtering_widgets(grid, 0) - - - def setup_downloads_external_tab(self, inner_notebook): - - """Called by self.setup_downloads_tab(). - - Sets up the 'External' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_External', inner_notebook) - - row_count = self.downloads_external_widgets(grid, 0) - - - def setup_sound_only_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Sound Only' tab. - """ - - tab, grid = self.add_notebook_tab('_Sound only') - grid_width = 4 - - # Sound only options - self.add_label(grid, - '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' - if os.name != 'nt': - text += '\n(requires that FFmpeg or AVConv is installed on your' \ - + ' system)' - - self.add_checkbutton(grid, - text, - 'extract_audio', - 0, 1, grid_width, 1, - ) - - label = self.add_label(grid, - 'Use this audio format: ', - 0, 2, 1, 1, - ) - label.set_hexpand(False) - - combo_list = formats.AUDIO_FORMAT_LIST.copy() - combo_list.insert(0, '') - combo = self.add_combo(grid, - combo_list, - 'audio_format', - 1, 2, 1, 1, - ) - combo.set_hexpand(True) - - label2 = self.add_label(grid, - 'Use this audio quality: ', - 2, 2, 1, 1, - ) - label2.set_hexpand(False) - - combo2_list = [ - ['High', '0'], - ['Medium', '5'], - ['Low', '9'], - ] - - combo2 = self.add_combo_with_data(grid, - combo2_list, - 'audio_quality', - 3, 2, 1, 1, - ) - combo2.set_hexpand(True) - - - def setup_post_process_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Post-processing' tab. - """ - - 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', - 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', - 0, 2, 1, 1, - ) - if os.name == 'nt': - button.set_sensitive(False) - - button2 = self.add_checkbutton(grid, - 'Prefer ffmpeg over avconv (default)', - 'prefer_ffmpeg', - 1, 2, 1, 1, - ) - if os.name == 'nt': - button2.set_sensitive(False) - - self.add_label(grid, - 'Audio format of the post-processed file', - 0, 3, 1, 1, - ) - - combo_list = formats.AUDIO_FORMAT_LIST.copy() - combo_list.insert(0, '') - self.add_combo(grid, - combo_list, - 'audio_format', - 1, 3, 1, 1, - ) - - self.add_label(grid, - 'Audio quality of the post-processed file', - 0, 4, 1, 1, - ) - - combo2_list = [ - ['High', '0'], - ['Medium', '5'], - ['Low', '9'], - ] - - self.add_combo_with_data(grid, - combo2_list, - 'audio_quality', - 1, 4, 1, 1, - ) - - self.add_label(grid, - 'Encode video to another format, if necessary', - 0, 5, 1, 1, - ) - - combo_list3 = ['', 'avi', 'flv', 'mkv', 'mp4', 'ogg', 'webm'] - self.add_combo(grid, - combo_list3, - 'recode_video', - 1, 5, 1, 1, - ) - - self.add_label(grid, - 'Arguments to pass to postprocessor', - 0, 6, 1, 1, - ) - - self.add_entry(grid, - 'pp_args', - 1, 6, 1, 1, - ) - - self.add_checkbutton(grid, - 'Keep video 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)', - None, - 1, 7, 1, 1, - ) - self.embed_checkbutton.set_active(self.retrieve_val('embed_subs')) - self.embed_checkbutton.connect( - 'toggled', - self.on_embed_checkbutton_toggled, - ) - - self.add_checkbutton(grid, - 'Embed thumbnail in audio file as cover art', - 'embed_thumbnail', - 0, 8, 1, 1, - ) - - self.add_checkbutton(grid, - 'Write metadata to the video file', - 'add_metadata', - 1, 8, 1, 1, - ) - - self.add_label(grid, - 'Automatically correct known faults of the file', - 0, 9, 1, 1, - ) - - combo_list4 = ['', 'never', 'warn', 'detect_or_warn'] - self.add_combo(grid, - combo_list4, - 'fixup_policy', - 1, 9, 1, 1, - ) - - - def setup_subtitles_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Subtitles' tab. - """ - - # Add this tab... - tab, grid = self.add_notebook_tab('S_ubtitles', 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_subtitles_options_tab(inner_notebook) - self.setup_subtitles_more_options_tab(inner_notebook) - - - def setup_subtitles_options_tab(self, inner_notebook): - - """Called by self.setup_subtitles_tab(). - - Sets up the 'Options' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Options', inner_notebook) - - # Subtitles options - self.add_label(grid, - 'Subtitles options', - 0, 0, 2, 1, - ) - - radiobutton = self.add_radiobutton(grid, - None, - 'Don\'t download the subtitles file', - None, - None, - 0, 1, 2, 1, - ) - if self.retrieve_val('write_subs') is False: - radiobutton.set_active(True) - # Signal connect appears below - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - 'Download the automatic subtitles file (YouTube only)', - None, - None, - 0, 2, 2, 1, - ) - if self.retrieve_val('write_subs') is True \ - and self.retrieve_val('write_auto_subs') is True: - radiobutton2.set_active(True) - # Signal connect appears below - - radiobutton3 = self.add_radiobutton(grid, - radiobutton2, - 'Download all available subtitles files', - None, - None, - 0, 3, 2, 1, - ) - if self.retrieve_val('write_subs') is True \ - and self.retrieve_val('write_all_subs') is True: - radiobutton3.set_active(True) - # Signal connect appears below - - radiobutton4 = self.add_radiobutton(grid, - radiobutton3, - 'Download subtitles file for these languages:', - None, - None, - 0, 4, 2, 1, - ) - if self.retrieve_val('write_subs') is True \ - and self.retrieve_val('write_auto_subs') is False \ - and self.retrieve_val('write_all_subs') is False: - radiobutton4.set_active(True) - # Signal connect appears below - - treeview, liststore = self.add_treeview(grid, - 0, 5, 1, 1, - ) - for key in formats.LANGUAGE_CODE_LIST: - liststore.append([key]) - - # We need a reverse dictionary for quick lookup - rev_dict = {} - for key in formats.LANGUAGE_CODE_DICT: - val = formats.LANGUAGE_CODE_DICT[key] - rev_dict[val] = key - - button = Gtk.Button('Add language >>>') - grid.attach(button, 0, 6, 1, 1) - # Signal connect below - - treeview2, liststore2 = self.add_treeview(grid, - 1, 5, 1, 1, - ) - lang_list = self.retrieve_val('subs_lang_list') - # The option stores ISO 639-1 Language Codes like 'en'; convert them to - # language names like 'English' - for lang_code in lang_list: - liststore2.append([rev_dict[lang_code]]) - - button2 = Gtk.Button('<<< Remove language') - grid.attach(button2, 1, 6, 1, 1) - # Signal connect below - - # Desensitise the buttons, if the matching radiobutton isn't active - if not radiobutton4.get_active(): - button.set_sensitive(False) - button2.set_sensitive(False) - - # Signal connects from above - button.connect( - 'clicked', - self.on_subtitles_tab_add_clicked, - treeview, - liststore2, - rev_dict, - ) - button2.connect( - 'clicked', - self.on_subtitles_tab_remove_clicked, - treeview2, - liststore2, - rev_dict, - ) - radiobutton.connect( - 'toggled', - self.on_subtitles_toggled, - button, button2, - 'write_subs', - ) - radiobutton2.connect( - 'toggled', - self.on_subtitles_toggled, - button, button2, - 'write_auto_subs', - ) - radiobutton3.connect( - 'toggled', - self.on_subtitles_toggled, - button, button2, - 'write_all_subs', - ) - radiobutton4.connect( - 'toggled', - self.on_subtitles_toggled, - button, button2, - 'subs_lang', - ) - - - def setup_subtitles_more_options_tab(self, inner_notebook): - - """Called by self.setup_subtitles_tab(). - - Sets up the 'Format' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_More options', - inner_notebook, - ) - - # Subtitle format options - self.add_label(grid, - '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, - ) - - self.add_entry(grid, - 'subs_format', - 0, 2, 1, 1, - ) - - # Post-processing options - self.add_label(grid, - 'Post-processing options', - 0, 3, 1, 1, - ) - - self.add_label(grid, - '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', - None, - 0, 5, 1, 1, - ) - self.embed_checkbutton2.set_active(self.retrieve_val('embed_subs')) - self.embed_checkbutton2.connect( - 'toggled', - self.on_embed_checkbutton_toggled, - ) - - - def setup_advanced_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Advanced' tab. - """ - - # Add this tab... - 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_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): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Authentification' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Authentification', - inner_notebook, - ) - - grid_width = 2 - - # Authentification options - self.add_label(grid, - 'Authentification options', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - 'Username with which to log in', - 0, 1, 1, 1, - ) - - self.add_entry(grid, - 'username', - 1, 1, 1, 1, - ) - - self.add_label(grid, - 'Password with which to log in', - 0, 2, 1, 1, - ) - - self.add_entry(grid, - 'password', - 1, 2, 1, 1, - ) - - self.add_label(grid, - 'Password required for this URL', - 0, 3, 1, 1, - ) - - self.add_entry(grid, - 'video_password', - 1, 3, 1, 1, - ) - - self.add_label(grid, - 'Two-factor authentication code', - 0, 4, 1, 1, - ) - - self.add_entry(grid, - 'two_factor', - 1, 4, 1, 1, - ) - - self.add_checkbutton(grid, - 'Use .netrc authentication data', - 'force_ipv4', - 0, 5, grid_width, 1, - ) - - - def setup_advanced_network_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Network' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Network', inner_notebook) - - grid_width = 2 - - # Network options - self.add_label(grid, - 'Network options', - 0, 6, grid_width, 1, - ) - - self.add_label(grid, - 'Use this HTTP/HTTPS proxy', - 0, 7, 1, 1, - ) - - self.add_entry(grid, - 'proxy', - 1, 7, 1, 1, - ) - - self.add_label(grid, - 'Time to wait for socket connection, before giving up', - 0, 8, 1, 1, - ) - - self.add_entry(grid, - 'socket_timeout', - 1, 8, 1, 1, - ) - - self.add_label(grid, - 'Client-side IP address to which to bind', - 0, 9, 1, 1, - ) - - self.add_entry(grid, - 'source_address', - 1, 9, 1, 1, - ) - - self.add_checkbutton(grid, - 'Make all connections via IPv4', - 'force_ipv4', - 0, 10, 1, 1, - ) - - self.add_checkbutton(grid, - 'Make all connections via IPv6', - 'force_ipv6', - 1, 10, 1, 1, - ) - - - def setup_advanced_georestrict_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Geo-restriction' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Geo-restriction', - inner_notebook, - ) - - grid_width = 2 - - # Geo-restriction options - self.add_label(grid, - 'Geo-restriction options', - 0, 11, grid_width, 1, - ) - - self.add_label(grid, - 'Use this proxy to verify IP address', - 0, 12, 1, 1, - ) - - self.add_entry(grid, - 'geo_verification_proxy', - 1, 12, 1, 1, - ) - - self.add_checkbutton(grid, - 'Bypass via fake X-Forwarded-For HTTP header', - 'geo_bypass', - 0, 13, 1, 1, - ) - - self.add_checkbutton(grid, - 'Don\'t bypass via fake HTTP header', - 'no_geo_bypass', - 1, 13, 1, 1, - ) - - self.add_label(grid, - 'Bypass geo-restriction with ISO 3166-2 country code', - 0, 14, 1, 1, - ) - - self.add_entry(grid, - 'geo_bypass_country', - 1, 14, 1, 1, - ) - - self.add_label(grid, - 'Bypass with explicit IP block in CIDR notation', - 0, 15, 1, 1, - ) - - self.add_entry(grid, - 'geo_bypass_ip_block', - 1, 15, 1, 1, - ) - - - def setup_advanced_workaround_tab(self, inner_notebook): - - """Called by self.setup_advanced_tab(). - - Sets up the 'Workaround' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Workaround', inner_notebook) - - grid_width = 2 - - # Workaround options - self.add_label(grid, - 'Workaround options', - 0, 16, grid_width, 1, - ) - - self.add_label(grid, - 'Custom user agent for youtube-dl', - 0, 17, 1, 1, - ) - - self.add_entry(grid, - 'user_agent', - 1, 17, 1, 1, - ) - - self.add_label(grid, - 'Custom referer if video access has restricted domain', - 0, 18, 1, 1, - ) - - self.add_entry(grid, - 'referer', - 1, 18, 1, 1, - ) - - self.add_label(grid, - 'Force this encoding (experimental)', - 0, 19, 1, 1, - ) - - self.add_entry(grid, - 'force_encoding', - 1, 19, 1, 1, - ) - - self.add_checkbutton(grid, - '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, - ) - - - # (Tab support functions - general) - - - def file_tab_sensitise_widgets(self, flag): - - """Called by self.setup_files_names_tab() and - self.on_file_tab_combo_changed(). - - Sensitises or desensitises a list of widgets in response to the user's - interactions with widgets on that tab. - - Args: - - flag (bool): True to sensitise the widgets, False to desensitise - them - - """ - - self.template_flag = flag - for widget in self.template_widget_list: - widget.set_sensitive(flag) - - - def formats_tab_count_formats(self): - - """Called by several parts of self.setup_formats_tab(). - - Counts the number of video/audio formats that are set. - - Returns: - - An integer in the range 0-3 - - """ - - 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 - - - # (Tab support functions - Downloads tab) - - - def downloads_general_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - grid_width = 3 - - self.add_checkbutton(grid, - 'Prefer HLS (HTTP Live Streaming)', - 'native_hls', - 0, row_count, grid_width, 1, - ) - - self.add_checkbutton(grid, - '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_ads', - 0, (row_count + 2), grid_width, 1, - ) - - self.add_checkbutton(grid, - 'Ignore errors and continue the download operation', - 'ignore_errors', - 0, (row_count + 3), grid_width, 1, - ) - - self.add_label(grid, - 'Number of retries', - 0, (row_count + 4), 1, 1, - ) - - self.add_spinbutton(grid, - 1, 99, 1, - 'retries', - 1, (row_count + 4), 1, 1, - ) - - return row_count + 5 - - - def downloads_age_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - grid_width = 3 - - self.add_label(grid, - 'Download videos suitable for this age', - 0, row_count, 1, 1, - ) - - self.add_entry(grid, - 'age_limit', - 1, row_count, 1, 1, - ) - - return row_count + 2 - - - def downloads_playlist_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - grid_width = 2 - - # Playlist options - self.add_label(grid, - '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', - 0, (row_count + 1), grid_width, 1, - ) - - self.add_label(grid, - 'Start downloading playlist from index', - 0, (row_count + 2), 1, 1, - ) - - self.add_spinbutton(grid, - 1, None, 1, - 'playlist_start', - 1, (row_count + 2), 1, 1, - ) - - self.add_label(grid, - 'Stop downloading playlist at index', - 0, (row_count + 3), 1, 1, - ) - - self.add_spinbutton(grid, - 0, None, 1, - 'playlist_end', - 1, (row_count + 3), 1, 1, - ) - - self.add_label(grid, - 'Abort operation after downloading this many videos', - 0, (row_count + 4), 1, 1, - ) - - self.add_spinbutton(grid, - 0, None, 1, - 'max_downloads', - 1, (row_count + 4), 1, 1, - ) - - self.add_checkbutton(grid, - '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', - 'playlist_reverse', - 0, (row_count + 6), grid_width, 1, - ) - - self.add_checkbutton(grid, - 'Download playlist in random order', - 'playlist_random', - 0, (row_count + 7), grid_width, 1, - ) - - return row_count + 7 - - - def downloads_size_limit_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - grid_width = 3 - - self.add_label(grid, - 'Video size limit options', - 0, row_count, grid_width, 1, - ) - - self.add_label(grid, - 'Minimum file size for video downloads', - 0, (row_count + 1), (grid_width - 2), 1, - ) - - self.add_spinbutton(grid, - 0, None, 1, - 'min_filesize', - (grid_width - 2), (row_count + 1), 1, 1, - ) - - self.add_combo_with_data(grid, - formats.FILE_SIZE_UNIT_LIST, - 'min_filesize_unit', - (grid_width - 1), (row_count + 1), 1, 1, - ) - - self.add_label(grid, - 'Maximum file size for video downloads', - 0, (row_count + 2), (grid_width - 2), 1, - ) - - self.add_spinbutton(grid, - 0, None, 1, - 'max_filesize', - (grid_width - 2), (row_count + 2), 1, 1, - ) - - self.add_combo_with_data(grid, - formats.FILE_SIZE_UNIT_LIST, - 'max_filesize_unit', - (grid_width - 1), (row_count + 2), 1, 1, - ) - - return row_count + 3 - - - def downloads_date_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - grid_width = 3 - - # Video date options - self.add_label(grid, - 'Video date options', - 0, row_count, grid_width, 1, - ) - - self.add_label(grid, - 'Only videos uploaded on this date', - 0, (row_count + 1), (grid_width - 2), 1, - ) - - entry = self.add_entry(grid, - 'date', - (grid_width - 2), (row_count + 1), 1, 1, - ) - entry.set_editable(False) - - button = Gtk.Button('Set') - grid.attach(button, (grid_width - 1), (row_count + 1), 1, 1) - button.connect( - 'clicked', - self.on_button_set_date_clicked, - entry, - 'date', - ) - - self.add_label(grid, - 'Only videos uploaded before this date', - 0, (row_count + 2), (grid_width - 2), 1, - ) - - entry2 = self.add_entry(grid, - 'date_before', - (grid_width - 2), (row_count + 2), 1, 1, - ) - entry2.set_editable(False) - - button2 = Gtk.Button('Set') - grid.attach(button2, (grid_width - 1), (row_count + 2), 1, 1) - button2.connect( - 'clicked', - self.on_button_set_date_clicked, - entry2, - 'date_before', - ) - - self.add_label(grid, - 'Only videos uploaded after this date', - 0, (row_count + 3), (grid_width - 2), 1, - ) - - entry3 = self.add_entry(grid, - 'date_after', - (grid_width - 2), (row_count + 3), 1, 1, - ) - entry3.set_editable(False) - - button3 = Gtk.Button('Set') - grid.attach(button3, (grid_width - 1), (row_count + 3), 1, 1) - button3.connect( - 'clicked', - self.on_button_set_date_clicked, - entry3, - 'date_after', - ) - - return row_count + 4 - - - def downloads_views_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - grid_width = 3 - - # Video views options - self.add_label(grid, - 'Video views options', - 0, row_count, grid_width, 1, - ) - - self.add_label(grid, - 'Minimum number of views', - 0, (row_count + 1), (grid_width - 2), 1, - ) - - spinbutton = self.add_spinbutton(grid, - 0, None, 1, - 'min_views', - (grid_width - 2), (row_count + 1), 1, 1, - ) - - self.add_label(grid, - 'Maximum number of views', - 0, (row_count + 2), (grid_width - 2), 1, - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, None, 1, - 'max_views', - (grid_width - 2), (row_count + 2), 1, 1, - ) - - # (This improves layout a little) - if not self.app_obj.simple_options_flag: - spinbutton.set_hexpand(True) - spinbutton2.set_hexpand(True) - - return row_count + 3 - - - def downloads_filtering_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - grid_width = 3 - - self.add_label(grid, - 'Video filtering options', - 0, row_count, grid_width, 1, - ) - - self.add_label(grid, - 'Download only matching titles (regex or caseless substring)', - 0, (row_count + 1), grid_width, 1, - ) - - self.add_textview(grid, - 'match_title_list', - 0, (row_count + 2), grid_width, 1, - ) - - self.add_label(grid, - 'Don\'t download only matching titles (regex or caseless' \ - + ' substring)', - 0, (row_count + 3), grid_width, 1, - ) - - self.add_textview(grid, - 'reject_title_list', - 0, (row_count + 4), grid_width, 1, - ) - - self.add_label(grid, - 'Generic video filter, for example: like_count > 100', - 0, (row_count + 5), grid_width, 1, - ) - - self.add_entry(grid, - 'match_filter', - 0, (row_count + 6), grid_width, 1, - ) - - return row_count + 7 - - - def downloads_external_widgets(self, grid, row_count): - - """Called by various parts of the Downloads tabs.""" - - grid_width = 2 - - # External downloader options - self.add_label(grid, - 'External downloader options', - 0, row_count, grid_width, 1, - ) - - self.add_label(grid, - 'Use this external downloader', - 0, (row_count + 1), 1, 1, - ) - - ext_list = [ - '', 'aria2c', 'avconv', 'axel', 'curl', 'ffmpeg', 'httpie', - 'wget', - ] - - combo = self.add_combo(grid, - ext_list, - 'external_downloader', - 1, (row_count + 1), 1, 1, - ) - combo.set_hexpand(True) - - self.add_label(grid, - 'Arguments to pass to external downloader', - 0, (row_count + 2), grid_width, 1, - ) - - self.add_entry(grid, - 'external_arg_string', - 0, (row_count + 3), grid_width, 1, - ) - - return row_count + 4 - - - # Callback class methods - - - def on_button_set_date_clicked(self, button, entry, prop): - - """Called by callback in self.downloads_date_widgets(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - prop (str): The attribute in self.edit_dict to modify - - """ - - # Prompt the user for a new calendar date - dialogue_win = mainwin.CalendarDialogue( - self, - self.retrieve_val(prop), - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - if response == Gtk.ResponseType.OK: - date_tuple = dialogue_win.calendar.get_date() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and date_tuple: - - year = str(date_tuple[0]) # e.g. 2011 - month = str(date_tuple[1] + 1) # Values in range 0-11 - day = str(date_tuple[2]) # Values in range 1-31 - - entry.set_text( - year.zfill(4) + month.zfill(2) + day.zfill(2) - ) - - else: - - entry.set_text('') - - - def on_clone_options_clicked(self, button): - - """Called by callback in self.setup_general_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - # Editing an Options Manager object attached to a particular media - # 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?', - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'clone_general_options_manager', - 'data': [self, self.edit_obj], - }, - ) - - - def on_embed_checkbutton_toggled(self, checkbutton): - - """Called by callback in self.setup_post_process_tab() or - setup_subtitles_more_options_tab(). - - The 'embed_subs' option appears in both the Formats and Subtitles tabs. - When one widget is modified, we need to set the other widgets to match - without starting an infinite loop of signal_connects. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - prop = 'embed_subs' - - if checkbutton == self.embed_checkbutton2 \ - and self.embed_checkbutton is None: - - # An easy case; the Formats tab isn't visible, so there is only one - # widget to think about - if not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - else: - - # We get around the infinite loop problem by setting the other - # checkbutton, if it's in the opposite state to this checkbutton - flag = checkbutton.get_active() - - if checkbutton == self.embed_checkbutton: - - if self.embed_checkbutton2.get_active() != flag: - self.embed_checkbutton2.set_active(flag) - elif not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - else: - - if self.embed_checkbutton.get_active() != flag: - self.embed_checkbutton.set_active(flag) - elif not checkbutton.get_active(): - self.edit_dict[prop] = False - else: - self.edit_dict[prop] = True - - - def on_fixed_folder_changed(self, combo): - - """Called by callback in self.setup_files_filesystem_tab(). - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - pixbuf, name = model[tree_iter][:2] - self.edit_dict['use_fixed_folder'] = name - - - def on_fixed_folder_toggled(self, checkbutton, combo): - - """Called by callback in self.setup_files_filesystem_tab(). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - combo (Gtk.ComboBox): Another widget to be modified by this - function - - """ - - if not checkbutton.get_active(): - self.edit_dict['use_fixed_folder'] = None - combo.set_sensitive(False) - - else: - - tree_iter = combo.get_active_iter() - model = combo.get_model() - pixbuf, name = model[tree_iter][:2] - self.edit_dict['use_fixed_folder'] = name - combo.set_sensitive(True) - - - def on_file_tab_button_clicked(self, button, entry, combo): - - """Called by callback in self.setup_files_names_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - combo (Gtk.ComboBox): Another widget to be modified by this - function - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - label = model[tree_iter][0] - - # (Code adapted from youtube-dl-gui's GeneralTab._on_template) - label = label.lower().replace(' ', '_') - if label == "ext": - prefix = '.' - else: - prefix = '-' - - # If the output template is empty or ends with a file path separator, - # remove the prefix - output_template = self.retrieve_val('output_template') - if not output_template or output_template[-1] == os.sep: - prefix = '' - - formatted = '{0}%({1})s'.format(prefix, label) - # (Setting the entry updates self.edit_dict) - entry.set_text(output_template + formatted) - - - def on_file_tab_combo_changed(self, combo, entry): - - """Called by callback in self.setup_files_names_tab(). - - Args: - - combo (Gtk.ComboBox): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - row_id, name = model[tree_iter][:2] - - self.edit_dict['output_format'] = row_id - - # The custom template is associated with the index 0 - if row_id == 0: - self.file_tab_sensitise_widgets(True) - entry.set_text(self.retrieve_val('output_template')) - - else: - self.file_tab_sensitise_widgets(False) - entry.set_text(formats.FILE_OUTPUT_CONVERT_DICT[row_id]) - - - def on_file_tab_entry_changed(self, entry): - - """Called by callback in self.setup_files_names_tab(). - - Args: - - entry (Gtk.Entry): The widget clicked - - """ - - # Only set 'output_template' when option 3 is selected, which is when - # the entry is sensitised - if self.template_flag: - self.edit_dict['output_template'] = entry.get_text() - - - def on_formats_tab_add_clicked(self, add_button, remove_button, \ - up_button, down_button, treeview): - - """Called by callback in self.setup_formats_tab(). - - Args: - - add_button (Gtk.Button): The widget clicked - - remove_button, up_button, down_button (Gtk.Button): Other widgets - to be modified by this function - - treeview (Gtk.TreeView): The treeview on the left side of the tab - - """ - - selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: - - # Nothing selected - return - - else: - - name = model[iter][0] - # 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: - 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 - - # Update the other treeview, adding the format to it (and don't modify - # this treeview) - self.formats_liststore.append([name]) - - # Update other widgets, as required - remove_button.set_sensitive(True) - up_button.set_sensitive(True) - down_button.set_sensitive(True) - - - def on_formats_tab_down_clicked(self, down_button, treeview): - - """Called by callback in self.setup_formats_tab(). - - Args: - - down_button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): Another widget to be modified by this - function - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - else: - - this_iter = model.get_iter(path_list[0]) - name = model[this_iter][0] - # 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: - - if extract_code == val2: - self.edit_dict['second_video_format'] = val3 - self.edit_dict['third_video_format'] = val2 - - elif extract_code == val1: - self.edit_dict['video_format'] = val2 - self.edit_dict['second_video_format'] = val1 - - 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), - ) - - - def on_formats_tab_remove_clicked(self, remove_button, add_button, \ - up_button, down_button, other_treeview): - - """Called by callback in self.setup_formats_tab(). - - Args: - - remove_button (Gtk.Button): The widget clicked - - add_button, up_button, down_button (Gtk.Button): Other widgets to - be modified by this function - - other_treeview (Gtk.TreeView): The treeview on the right side of - the tab - - """ - - selection = other_treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: - - # Nothing selected - return - - else: - - name = model[iter][0] - # 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 right-hand side treeview - model.remove(iter) - - # Update other widgets, as required - add_button.set_sensitive(True) - if self.retrieve_val('video_format') == '0': - - # 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): - - """Called by callback in self.setup_formats_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): Another widget to be modified by this - function - - """ - - selection = treeview.get_selection() - (model, path_list) = selection.get_selected_rows() - if not path_list: - - # Nothing selected - return - - else: - - this_iter = model.get_iter(path_list[0]) - name = model[this_iter][0] - # 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: - - if extract_code == val2: - self.edit_dict['video_format'] = val2 - self.edit_dict['second_video_format'] = val1 - - elif extract_code == val3: - self.edit_dict['second_video_format'] = val3 - self.edit_dict['third_video_format'] = val2 - - 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), - ) - - - def on_reset_options_clicked(self, button): - - """Called by callback in self.setup_general_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - msg = 'This procedure cannot be reversed.' \ - + ' Are you sure you want to continue?', - - if self.media_data_obj is None: - - # Editing the General Options Manager object - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - msg, - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'reset_options_manager', - # (Reset this edit window, if the user clicks 'yes') - 'data': [self], - }, - ) - - else: - - # Editing an Options Manager object attached to a particular media - # data object - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - msg, - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'reset_options_manager', - 'data': [self, self.media_data_obj], - }, - ) - - - def on_simple_options_clicked(self, button): - - """Called by callback in self.setup_general_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if not self.app_obj.simple_options_flag: - - self.app_obj.set_simple_options_flag(True) - - if not self.edit_dict: - # User has not changed any options, so redraw the window to - # show the same options.OptionsManager object - self.reset_with_new_edit_obj(self.edit_obj) - - else: - # User has already changed some options. We don't want to lose - # 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)', - ) - - else: - - self.app_obj.set_simple_options_flag(False) - - if not self.edit_dict: - self.reset_with_new_edit_obj(self.edit_obj) - - else: - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - 'When the window is re-opened, all download options' \ - + ' will bevisible', - 'info', - 'ok', - self, # Parent window is this window - ) - - button.set_label( - 'Hide advanced download options (when window re-opens)', - ) - - - def on_subtitles_tab_add_clicked(self, button, treeview, other_liststore, - rev_dict): - - """Called by callback in self.setup_subtitles_options_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - treeview (Gtk.TreeView): The treeview on the left side of the tab - - other_liststore (Gtk.ListStore): The liststore belonging to the - treeview on the right side of the tab - - rev_dict (dict): A reversed formats.LANGUAGE_CODE_DICT - - """ - - selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: - - # Nothing selected - return - - else: - - lang_name = model[iter][0] - # Convert a language to its ISO 639-1 Language Code, e.g. convert - # 'English' to 'en' - lang_code = formats.LANGUAGE_CODE_DICT[lang_name] - - # Retrieve the existing list of languages - lang_code_list = self.retrieve_val('subs_lang_list') - if not lang_code in lang_code_list: - - lang_code_list.append(lang_code) - - # Sort by language name, not by language code - lang_list = [] - mod_code_list = [] - for this_code in lang_code_list: - lang_list.append(rev_dict[this_code]) - - lang_list.sort() - for this_lang in lang_list: - mod_code_list.append(formats.LANGUAGE_CODE_DICT[this_lang]) - - # Update the option... - self.edit_dict['subs_lang_list'] = mod_code_list - # ...and the textview - other_liststore.clear() - for this_lang in lang_list: - other_liststore.append([this_lang]) - - - def on_subtitles_tab_remove_clicked(self, button, other_treeview, - other_liststore, rev_dict): - - """Called by callback in self.setup_subtitles_options_tab(). - - Args: - - button (Gtk.Button): The widget clicked - - other_treeview (Gtk.TreeView): The treeview on the right side of - the tab - - other_liststore (Gtk.ListStore): The liststore belonging to that - treeview - - rev_dict (dict): A reversed formats.LANGUAGE_CODE_DICT - - """ - - selection = other_treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: - - # Nothing selected - return - - else: - - lang_name = model[iter][0] - # Convert a language to its ISO 639-1 Language Code, e.g. convert - # 'English' to 'en' - lang_code = formats.LANGUAGE_CODE_DICT[lang_name] - - # Retrieve the existing list of languages - lang_code_list = self.retrieve_val('subs_lang_list') - if lang_code in lang_code_list: - - lang_code_list.remove(lang_code) - - # Sort by language name, not by language code - lang_list = [] - mod_code_list = [] - for this_code in lang_code_list: - lang_list.append(rev_dict[this_code]) - - lang_list.sort() - for this_lang in lang_list: - mod_code_list.append(formats.LANGUAGE_CODE_DICT[this_lang]) - - # Update the option... - self.edit_dict['subs_lang_list'] = mod_code_list - # ...and the textview - other_liststore.clear() - for this_lang in lang_list: - other_liststore.append([this_lang]) - - - def on_subtitles_toggled(self, radiobutton, button, button2, prop): - - """Called by callback in self.setup_subtitles_options_tab(). - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - button, button2 (Gtk.Button): Other widgets to be modified by this - function - - prop (str): The attribute in self.edit_dict to modify - - """ - - if radiobutton.get_active(): - - if prop == 'write_subs': - self.edit_dict['write_subs'] = False - self.edit_dict['write_auto_subs'] = False - self.edit_dict['write_all_subs'] = False - button.set_sensitive(False) - button2.set_sensitive(False) - - elif prop == 'write_auto_subs': - self.edit_dict['write_subs'] = True - self.edit_dict['write_auto_subs'] = True - self.edit_dict['write_all_subs'] = False - button.set_sensitive(False) - button2.set_sensitive(False) - - elif prop == 'write_all_subs': - self.edit_dict['write_subs'] = True - self.edit_dict['write_auto_subs'] = False - self.edit_dict['write_all_subs'] = True - button.set_sensitive(False) - button2.set_sensitive(False) - - elif prop == 'subs_lang': - self.edit_dict['write_subs'] = True - self.edit_dict['write_auto_subs'] = False - self.edit_dict['write_all_subs'] = False - button.set_sensitive(True) - button2.set_sensitive(True) - - -class VideoEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in a media.Video - object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (media.Video): The object whose attributes will be edited in - this window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj): - - Gtk.Window.__init__(self, title='Video properties') - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media.Video object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (Non-standard widgets) - self.apply_options_button = None # Gtk.Button - self.edit_options_button = None # Gtk.Button - self.remove_options_button = None # Gtk.Button - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK' are required, or False if just the 'OK' button is required - self.multi_button_flag = False - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # The key-value pairs in the dictionary correspond directly to - # the names of attributes, and their balues in self.edit_obj - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - # String identifying the media type - self.media_type = 'video' - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - -# def apply_changes(): # Inherited from GenericConfigWin - - -# def retrieve_val(): # Inherited from GenericConfigWin - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_general_tab() - self.setup_download_options_tab() - self.setup_descrip_tab() - self.setup_errors_warnings_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - tab, grid = self.add_notebook_tab('_General') - - self.add_label(grid, - 'General properties', - 0, 0, 2, 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, - ) - label3.set_hexpand(False) - - entry6 = self.add_entry(grid, - None, - 1, 5, 2, 1, - ) - entry6.set_editable(False) - if self.edit_obj.file_name: - entry6.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', - 'dl_sim_flag', - 0, 0, 2, 1, - ) - checkbutton.set_sensitive(False) - - label4 = self.add_label(grid3, - 'Duration', - 2, 0, 1, 1, - ) - label4.set_hexpand(False) - - entry7 = self.add_entry(grid3, - None, - 3, 0, 1, 1, - ) - entry7.set_editable(False) - if self.edit_obj.duration is not None: - entry7.set_text( - utils.convert_seconds_to_string(self.edit_obj.duration), - ) - - checkbutton2 = self.add_checkbutton(grid3, - 'Video has been downloaded', - 'dl_flag', - 0, 1, 2, 1, - ) - checkbutton2.set_sensitive(False) - - label5 = self.add_label(grid3, - 'File size', - 2, 1, 1, 1, - ) - label5.set_hexpand(False) - - entry8 = self.add_entry(grid3, - None, - 3, 1, 1, 1, - ) - entry8.set_editable(False) - if self.edit_obj.file_size is not None: - entry8.set_text(self.edit_obj.get_file_size_string()) - - checkbutton3 = self.add_checkbutton(grid3, - 'Video is marked as unwatched', - 'new_flag', - 0, 2, 2, 1, - ) - checkbutton3.set_sensitive(False) - - label6 = self.add_label(grid3, - 'Upload time', - 2, 2, 1, 1, - ) - label6.set_hexpand(False) - - entry9 = self.add_entry(grid3, - None, - 3, 2, 1, 1, - ) - entry9.set_editable(False) - if self.edit_obj.upload_time is not None: - entry9.set_text(self.edit_obj.get_upload_time_string()) - - checkbutton4 = self.add_checkbutton(grid3, - 'Video is archived', - 'archive_flag', - 0, 3, 1, 1, - ) - checkbutton4.set_sensitive(False) - - checkbutton5 = self.add_checkbutton(grid3, - 'Video is bookmarked', - 'bookmark_flag', - 1, 3, 1, 1, - ) - checkbutton5.set_sensitive(False) - - label7 = self.add_label(grid3, - 'Receive time', - 2, 3, 1, 1, - ) - label7.set_hexpand(False) - - entry10 = self.add_entry(grid3, - None, - 3, 3, 1, 1, - ) - entry10.set_editable(False) - if self.edit_obj.receive_time is not None: - entry10.set_text(self.edit_obj.get_receive_time_string()) - - checkbutton6 = self.add_checkbutton(grid3, - 'Video is favourite', - 'fav_flag', - 0, 4, 1, 1, - ) - checkbutton6.set_sensitive(False) - - checkbutton7 = self.add_checkbutton(grid3, - 'Video is in waiting list', - 'waiting_flag', - 1, 4, 1, 1, - ) - checkbutton7.set_sensitive(False) - - -# def setup_download_options_tab(): # Inherited from GenericConfigWin - - - def setup_descrip_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Description' tab. - """ - - tab, grid = self.add_notebook_tab('_Description') - - self.add_label(grid, - 'Video description', - 0, 0, 1, 1, - ) - - self.add_textview(grid, - 'descrip', - 0, 1, 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, - ) - - self.add_label(grid, - 'Error messages produced the last time this video was' \ - + ' checked/downloaded', - 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) - - self.add_label(grid, - 'Warning messages produced the last time this video was' \ - + ' checked/downloaded', - 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) - - - # Callback class methods - - -# def on_button_apply_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_edit_optiosn_clicked(): # Inherited from GenericConfigWin - - -# def on_button_remove_options_clicked(): # Inherited from GenericConfigWin - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass - - -class ChannelPlaylistEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in a media.Channel or - media.Playlist object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (media.Channel, media.Playlist): The object whose attributes - will be edited in this window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj): - - if isinstance(edit_obj, media.Channel): - media_type = 'channel' - win_title = 'Channel properties' - else: - media_type = 'playlist' - win_title = 'Playlist properties' - - Gtk.Window.__init__(self, title=win_title) - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media.Channel or media.Playlist object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (Non-standard widgets) - self.apply_options_button = None # Gtk.Button - self.edit_options_button = None # Gtk.Button - self.remove_options_button = None # Gtk.Button - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK' are required, or False if just the 'OK' button is required - self.multi_button_flag = False - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # The key-value pairs in the dictionary correspond directly to - # the names of attributes, and their balues in self.edit_obj - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - # String set to 'channel' or 'playlist' - self.media_type = media_type - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - -# def apply_changes(): # Inherited from GenericConfigWin - - -# def retrieve_val(): # Inherited from GenericConfigWin - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_general_tab() - self.setup_download_options_tab() - self.setup_errors_warnings_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - tab, grid = self.add_notebook_tab('_General') - - self.add_label(grid, - '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) - self.add_destination_properties(grid) - - # 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, - 'dl_sim_flag', - 0, 0, 1, 1, - ) - checkbutton.set_sensitive(False) - - checkbutton2 = self.add_checkbutton(grid3, - 'Disable checking/downloading for this ' + self.media_type, - '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', - 'fav_flag', - 0, 2, 1, 1, - ) - checkbutton3.set_sensitive(False) - - self.add_label(grid3, - 'Total videos', - 1, 0, 1, 1, - ) - entry8 = self.add_entry(grid3, - 'vid_count', - 2, 0, 1, 1, - ) - entry8.set_editable(False) - entry8.set_width_chars(8) - entry8.set_hexpand(False) - - self.add_label(grid3, - 'New videos', - 1, 1, 1, 1, - ) - entry9 = self.add_entry(grid3, - 'new_count', - 2, 1, 1, 1, - ) - entry9.set_editable(False) - entry9.set_width_chars(8) - entry9.set_hexpand(False) - - self.add_label(grid3, - 'Favourite videos', - 1, 2, 1, 1, - ) - entry10 = self.add_entry(grid3, - 'fav_count', - 2, 2, 1, 1, - ) - entry10.set_editable(False) - entry10.set_width_chars(8) - entry10.set_hexpand(False) - - self.add_label(grid3, - 'Downloaded videos', - 1, 3, 1, 1, - ) - entry11 = self.add_entry(grid3, - 'dl_count', - 2, 3, 1, 1, - ) - entry11.set_editable(False) - entry11.set_width_chars(8) - entry11.set_hexpand(False) - - -# def setup_download_options_tab(): # Inherited from GenericConfigWin - - - 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, - ) - - self.add_label(grid, - 'Error messages produced the last time this ' \ - + self.media_type + ' was checked/downloaded', - 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) - - 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, - ) - textview2.set_editable(False) - textview2.set_wrap_mode(Gtk.WrapMode.WORD) - - - # Callback class methods - - -# def on_button_apply_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_edit_optiosn_clicked(): # Inherited from GenericConfigWin - - -# def on_button_remove_options_clicked(): # Inherited from GenericConfigWin - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass - - -class FolderEditWin(GenericEditWin): - - """Python class for an 'edit window' to modify values in a media.Folder - object. - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - edit_obj (media.Folder): The object whose attributes will be edited in - this window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, edit_obj): - - Gtk.Window.__init__(self, title='Folder properties') - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media.Folder object being edited - self.edit_obj = edit_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.reset_button = None # Gtk.Button - self.apply_button = None # Gtk.Button - self.ok_button = None # Gtk.Button - self.cancel_button = None # Gtk.Button - # (Non-standard widgets) - self.apply_options_button = None # Gtk.Button - self.edit_options_button = None # Gtk.Button - self.remove_options_button = None # Gtk.Button - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between edit window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and - # 'OK' are required, or False if just the 'OK' button is required - self.multi_button_flag = False - - # When the user changes a value, it is not applied to self.edit_obj - # immediately; instead, it is stored temporarily in this dictionary - # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the - # window, the changes are applied to self.edit_obj - # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary - # is emptied and the changes are lost - # The key-value pairs in the dictionary correspond directly to - # the names of attributes, and their balues in self.edit_obj - # Key-value pairs are added to this dictionary whenever the user - # makes a change (so if no changes are made when the window is - # closed, the dictionary will still be empty) - self.edit_dict = {} - - # String identifying the media type - self.media_type = 'folder' - - - # Code - # ---- - - # Set up the edit window - self.setup() - - - # Public class methods - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericEditWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - # (Non-widget functions) - - -# def apply_changes(): # Inherited from GenericConfigWin - - -# def retrieve_val(): # Inherited from GenericConfigWin - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this edit window. - """ - - self.setup_general_tab() - self.setup_download_options_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - tab, grid = self.add_notebook_tab('_General') - - self.add_label(grid, - 'General properties', - 0, 0, 2, 1, - ) - - # The first sets of widgets are shared by multiple edit windows - self.add_container_properties(grid) - self.add_destination_properties(grid) - - # 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', - 'dl_sim_flag', - 0, 0, 1, 1, - ) - checkbutton.set_sensitive(False) - - checkbutton2 = self.add_checkbutton(grid3, - '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', - 'fav_flag', - 0, 2, 1, 1, - ) - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid3, - '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', - 'fixed_flag', - 1, 0, 1, 1, - ) - checkbutton5.set_sensitive(False) - - checkbutton6 = self.add_checkbutton(grid3, - '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', - 'restrict_flag', - 1, 2, 1, 1, - ) - checkbutton7.set_sensitive(False) - - checkbutton8 = self.add_checkbutton(grid3, - 'All contents deleted when ' + __main__.__prettyname__ \ - + ' shuts down', - 'temp_flag', - 1, 3, 1, 1, - ) - checkbutton8.set_sensitive(False) - - -# def setup_download_options_tab(): # Inherited from GenericConfigWin - - - # Callback class methods - - -# def on_button_apply_options_clicked(): # Inherited from GenericConfigWin - - -# def on_button_edit_optiosn_clicked(): # Inherited from GenericConfigWin - - -# def on_button_remove_options_clicked(): # Inherited from GenericConfigWin - - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass - - -class SystemPrefWin(GenericPrefWin): - - """Python class for a 'preference window' to modify various system - settings. - - Args: - - 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 - - """ - - - # Standard class methods - - - def __init__(self, app_obj, switch_db_flag=False): - - - Gtk.Window.__init__(self, title='System preferences') - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - - - # IV list - Gtk widgets - # --------------------- - self.grid = None # Gtk.Grid - self.notebook = None # Gtk.Notebook - self.ok_button = None # Gtk.Button - # (IVs used to handle widget changes in the 'General' tab) - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.radiobutton3 = None # Gtk.RadioButton - self.spinbutton = None # Gtk.SpinButton - self.spinbutton2 = None # Gtk.SpinButton - # (IVs used to handle widget changes in the 'Filesystem' tab) - self.entry = None # Gtk.Entry - self.entry2 = None # Gtk.Entry - self.filesystem_inner_notebook = None # Gtk.Notebook - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between preference window widgets - self.spacing_size = self.app_obj.default_spacing_size - - # Code - # ---- - - # Set up the preference window - self.setup() - if switch_db_flag: - self.select_switch_db_tab() - - - # Public class methods - - -# def setup(): # Inherited from GenericConfigWin - - -# def setup_grid(): # Inherited from GenericConfigWin - - -# def setup_notebook(): # Inherited from GenericConfigWin - - -# def add_notebook_tab(): # Inherited from GenericConfigWin - - -# def setup_button_strip(): # Inherited from GenericPrefWin - - -# def setup_gap(): # Inherited from GenericConfigWin - - - def select_switch_db_tab(self): - - """Can be called by anything. - - Makes the visible tab the one on which the user can set Tartube's - data directory (which contains the Tartube database file). - """ - - self.notebook.set_current_page(1) - self.filesystem_inner_notebook.set_current_page(1) - - - # (Setup tabs) - - - def setup_tabs(self): - - """Called by self.setup(), .on_button_apply_clicked() and - .on_button_reset_clicked(). - - Sets up the tabs for this preference window. - """ - - self.setup_general_tab() - self.setup_filesystem_tab() - self.setup_windows_tab() - self.setup_scheduling_tab() - self.setup_operations_tab() - self.setup_ytdl_tab() - self.setup_output_tab() - - - def setup_general_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'General' tab. - """ - - # Add this tab... - 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_modules_tab(inner_notebook) - self.setup_general_video_matching_tab(inner_notebook) - - - def setup_general_language_tab(self, inner_notebook): - - """Called by self.setup_general_tab(). - - Sets up the 'Language' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Language', inner_notebook) - grid_width = 2 - - # Language preferences - self.add_label(grid, - 'Language preferences', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - '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'] ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 1, 1, (grid_width - 1), 1) - combo.set_hexpand(False) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 1) - - combo.set_active(0) - - - 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 = 3 - - # Gtk support - self.add_label(grid, - 'Gtk support', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - 'Current version of the system\'s Gtk library', - 0, 1, 1, 1 - ) - - entry = self.add_entry(grid, - 'v' + str(self.app_obj.gtk_version_major) + '.' \ - + str(self.app_obj.gtk_version_minor) + '.' \ - + str(self.app_obj.gtk_version_micro), - False, - 1, 1, 2, 1, - ) - entry.set_sensitive(False) - - checkbutton = self.add_checkbutton(grid, - 'Some (minor) 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, - ) - checkbutton.set_hexpand(False) - - checkbutton2 = self.add_checkbutton(grid, - 'Assume that Gtk is broken, and disable some features', - self.app_obj.gtk_emulate_broken_flag, - True, # Can be toggled by user - 0, 3, grid_width, 1, - ) - checkbutton2.set_hexpand(False) - checkbutton2.connect('toggled', self.on_gtk_emulate_button_toggled) - - # Module availability - self.add_label(grid, - 'Module availability', - 0, 4, grid_width, 1, - ) - - self.add_checkbutton(grid, - 'moviepy module is available', - mainapp.HAVE_MOVIEPY_FLAG, - False, # Can't be toggled by user - 0, 5, grid_width, 1, - ) - - self.add_checkbutton(grid, - 'XDG module is available', - mainapp.HAVE_XDG_FLAG, - False, # Can't be toggled by user - 0, 6, grid_width, 1, - ) - - # Module preferences - self.add_label(grid, - 'Module preferences', - 0, 7, grid_width, 1, - ) - - checkbutton3 = 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, - ) - checkbutton3.connect('toggled', self.on_moviepy_button_toggled) - if not mainapp.HAVE_MOVIEPY_FLAG: - checkbutton3.set_sensitive(False) - - self.add_label(grid, - 'Timeout applied when moviepy checks a video file', - 0, 9, grid_width, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 0, - 60, - 1, # Step - self.app_obj.refresh_moviepy_timeout, - 1, 9, 2, 1, - ) - spinbutton.connect( - 'value-changed', - self.on_moviepy_timeout_spinbutton_changed, - ) - - - def setup_general_video_matching_tab(self, inner_notebook): - - """Called by self.setup_general_tab(). - - Sets up the 'Video matching' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Video matching', - inner_notebook, - ) - - grid_width = 2 - - # Video matching preferences - self.add_label(grid, - 'Video matching preferences', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - 'When matching videos on the filesystem:', - 0, 1, grid_width, 1, - ) - - self.radiobutton = self.add_radiobutton(grid, - None, - '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', - 0, 3, (grid_width - 1), 1, - ) - # Signal connect appears below - - self.spinbutton = self.add_spinbutton(grid, - 1, 999, 1, self.app_obj.match_first_chars, - 2, 3, 1, 1, - ) - # Signal connect appears below - - self.radiobutton3 = self.add_radiobutton(grid, - self.radiobutton2, - 'Ignore the last n characters; the remaining name must match' \ - + ' exactly', - 0, 4, (grid_width - 1), 1, - ) - # Signal connect appears below - - self.spinbutton2 = self.add_spinbutton(grid, - 1, 999, 1, self.app_obj.match_ignore_chars, - 2, 4, 1, 1, - ) - # Signal connect appears below - - # (Widgets are sensitised/desensitised, based on the radiobutton) - if self.app_obj.match_method == 'exact_match': - self.spinbutton.set_sensitive(False) - self.spinbutton2.set_sensitive(False) - elif self.app_obj.match_method == 'match_first': - self.radiobutton2.set_active(True) - self.spinbutton2.set_sensitive(False) - else: - self.radiobutton3.set_active(True) - self.spinbutton.set_sensitive(False) - - # Signal connects from above - self.radiobutton.connect('toggled', self.on_match_button_toggled) - self.radiobutton2.connect('toggled', self.on_match_button_toggled) - self.radiobutton3.connect('toggled', self.on_match_button_toggled) - self.spinbutton.connect( - 'value-changed', - self.on_match_spinbutton_changed, - ) - self.spinbutton2.connect( - 'value-changed', - self.on_match_spinbutton_changed, - ) - - - def setup_filesystem_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Filesystem' tab. - """ - - # Add this tab... - tab, grid = self.add_notebook_tab('_Filesystem', 0) - - # ...and an inner notebook... - self.filesystem_inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_filesystem_device_tab(self.filesystem_inner_notebook) - self.setup_filesystem_database_tab(self.filesystem_inner_notebook) - self.setup_filesystem_db_errors_tab(self.filesystem_inner_notebook) - self.setup_filesystem_backups_tab(self.filesystem_inner_notebook) - self.setup_filesystem_video_deletion_tab( - self.filesystem_inner_notebook, - ) - self.setup_filesystem_temp_folders_tab(self.filesystem_inner_notebook) - - - def setup_filesystem_device_tab(self, inner_notebook): - - """Called by self.setup_filesystem_tab(). - - Sets up the 'Device' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Device', inner_notebook) - grid_width = 3 - - # Device preferences - self.add_label(grid, - 'Device preferences', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - 'Size of device (in Mb)', - 0, 3, 1, 1, - ) - - self.entry = self.add_entry(grid, - str(utils.disk_get_total_space(self.app_obj.data_dir)), - False, - 1, 3, 2, 1, - ) - self.entry.set_sensitive(False) - - self.add_label(grid, - 'Free space on device (in Mb)', - 0, 4, 1, 1, - ) - - self.entry2 = self.add_entry(grid, - str(utils.disk_get_free_space(self.app_obj.data_dir)), - False, - 1, 4, 2, 1, - ) - self.entry2.set_sensitive(False) - - checkbutton = self.add_checkbutton(grid, - 'Warn user if disk space is below (Mb)', - self.app_obj.disk_space_warn_flag, - True, # Can be toggled by user - 0, 5, 1, 1, - ) - # (signal_connect appears below) - - spinbutton = self.add_spinbutton(grid, - 0, None, - self.app_obj.disk_space_increment, - self.app_obj.disk_space_warn_limit, - 1, 5, 2, 1, - ) - if not self.app_obj.disk_space_warn_flag: - spinbutton.set_sensitive(False) - # (signal_connect appears below) - - checkbutton2 = self.add_checkbutton(grid, - 'Halt downloads if disk space is below (Mb)', - self.app_obj.disk_space_stop_flag, - True, # Can be toggled by user - 0, 6, 1, 1, - ) - # (signal_connect appears below) - - spinbutton2 = self.add_spinbutton(grid, - 0, None, - self.app_obj.disk_space_increment, - self.app_obj.disk_space_stop_limit, - 1, 6, 2, 1, - ) - if not self.app_obj.disk_space_stop_flag: - spinbutton2.set_sensitive(False) - # (signal_connect appears below) - - # (signal_connect from above) - checkbutton.connect( - 'toggled', - self.on_disk_warn_button_toggled, - spinbutton, - ) - spinbutton.connect( - 'value-changed', - self.on_disk_warn_spinbutton_changed, - ) - checkbutton2.connect( - 'toggled', - self.on_disk_stop_button_toggled, - spinbutton2, - ) - spinbutton2.connect( - 'value-changed', - self.on_disk_stop_spinbutton_changed, - ) - - # Configuration preferences - self.add_label(grid, - 'Configuration preferences', - 0, 7, grid_width, 1, - ) - - self.add_label(grid, - __main__.__prettyname__ + ' configuration file loaded from:', - 0, 8, grid_width, 1, - ) - - if self.app_obj.config_file_xdg_path is not None: - config_path = self.app_obj.config_file_xdg_path - else: - config_path = self.app_obj.config_file_path - - entry3 = self.add_entry(grid, - config_path, - False, - 0, 9, grid_width, 1, - ) - entry3.set_sensitive(False) - - - def setup_filesystem_database_tab(self, inner_notebook): - - """Called by self.setup_filesystem_tab(). - - Sets up the 'Database' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('D_atabase', inner_notebook) - grid_width = 3 - - if os.name == 'nt': - folder = 'folder' - folder_plural = 'folders' - else: - folder = 'directory' - folder_plural = 'directories' - - # Database preferences - self.add_label(grid, - 'Database preferences', - 0, 0, grid_width, 1, - ) - - label = self.add_label(grid, - __main__.__prettyname__ + ' data ' + folder, - 0, 2, 1, 1, - ) - label.set_hexpand(False) - - entry = self.add_entry(grid, - self.app_obj.data_dir, - False, - 1, 2, 1, 1, - ) - entry.set_sensitive(False) - - button = Gtk.Button('Change') - grid.attach(button, 2, 2, 1, 1) - button.set_tooltip_text('Change to a different data ' + folder) - button.connect( - 'clicked', - self.on_data_dir_change_button_clicked, - entry, - ) - - label = self.add_label(grid, - 'Recent data ' + folder_plural, - 0, 3, 1, 1, - ) - label.set_hexpand(False) - - treeview, liststore = self.add_treeview(grid, - 1, 3, 1, 5, - ) - treeview.set_vexpand(False) - for item in self.app_obj.data_dir_alt_list: - liststore.append([item]) - # (signal_connect appears below) - - button2 = Gtk.Button('Switch') - grid.attach(button2, 2, 3, 1, 1) - button2.set_tooltip_text('Switch to the selected data ' + folder) - button2.set_sensitive(False) - button2.connect( - 'clicked', - self.on_data_dir_switch_button_clicked, - button, - treeview, - entry, - ) - - button3 = Gtk.Button('Forget') - grid.attach(button3, 2, 4, 1, 1) - button3.set_tooltip_text( - 'Remove the selected data ' + folder + ' from the list', - ) - button3.set_sensitive(False) - button3.connect( - 'clicked', - self.on_data_dir_forget_button_clicked, - treeview, - ) - - 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)', - ) - if len(self.app_obj.data_dir_alt_list) <= 1: - button4.set_sensitive(False) - button4.connect( - 'clicked', - self.on_data_dir_forget_all_button_clicked, - treeview, - ) - - button5 = Gtk.Button('Move up') - grid.attach(button5, 2, 6, 1, 1) - button5.set_tooltip_text( - 'Move the selected ' + folder + ' up the list', - ) - button5.set_sensitive(False) - button5.connect( - 'clicked', - self.on_data_dir_move_up_button_clicked, - treeview, - liststore, - ) - - 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, - ) - - # (Add a second grid, so widget positioning on the first one isn't - # messed up) - grid2 = Gtk.Grid() - 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, - ) - checkbutton.connect('toggled', self.on_use_first_button_toggled) - - checkbutton2 = self.add_checkbutton(grid2, - '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, - ) - checkbutton2.connect('toggled', self.on_use_list_button_toggled) - - checkbutton3 = self.add_checkbutton(grid2, - '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, - ) - checkbutton3.connect('toggled', self.on_add_from_list_button_toggled) - - # Everything must be desensitised, if load/save is disabled - if self.app_obj.disable_load_save_flag: - button.set_sensitive(False) - button2.set_sensitive(False) - button3.set_sensitive(False) - button4.set_sensitive(False) - button5.set_sensitive(False) - button6.set_sensitive(False) - checkbutton.set_sensitive(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_sensitive(False) - - # (signal_connect from above) - treeview.connect( - 'cursor-changed', - self.on_data_dir_cursor_changed, - button2, # Use - button3, # Forget - button4, # Forget all - button5, # Move up - button6, # Move down - ) - - - def setup_filesystem_db_errors_tab(self, inner_notebook): - - """Called by self.setup_filesystem_tab(). - - Sets up the 'DB Errors' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('DB _Errors', inner_notebook) - grid_width = 2 - - # Database error preferences - self.add_label(grid, - 'Database error preferences', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - 'Check ' + __main__.__prettyname__ \ - + '\'s database for inconsistencies, and fix them', - 0, 1, 1, 1, - ) - - button = Gtk.Button('Check') - grid.attach(button, 1, 1, 1, 1) - if self.app_obj.disable_load_save_flag: - button.set_sensitive(False) - button.set_hexpand(True) - button.connect('clicked', self.on_data_check_button_clicked) - - - def setup_filesystem_backups_tab(self, inner_notebook): - - """Called by self.setup_filesystem_tab(). - - Sets up the 'Backups' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Backups', inner_notebook) - - # Backup preferences - self.add_label(grid, - '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)', - 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': - radiobutton2.set_active(True) - # Signal connect appears below - - 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': - radiobutton3.set_active(True) - # Signal connect appears below - - radiobutton4 = self.add_radiobutton(grid, - radiobutton3, - 'Make a new backup file for every save procedure', - 0, 5, 1, 1, - ) - if self.app_obj.db_backup_mode == 'always': - radiobutton4.set_active(True) - # Signal connect appears below - - # Signal connects from above - radiobutton.connect( - 'toggled', - self.on_backup_button_toggled, - 'default', - ) - - radiobutton2.connect( - 'toggled', - self.on_backup_button_toggled, - 'single', - ) - - radiobutton3.connect( - 'toggled', - self.on_backup_button_toggled, - 'daily', - ) - - radiobutton4.connect( - 'toggled', - self.on_backup_button_toggled, - 'always', - ) - - - def setup_filesystem_video_deletion_tab(self, inner_notebook): - - """Called by self.setup_filesystem_tab(). - - Sets up the 'Video deletion' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Video deletion', - inner_notebook, - ) - - grid_width = 2 - - # Automatic video deletion preferences - self.add_label(grid, - 'Automatic video deletion preferences', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - '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, - ) - # Signal connect appears below - - spinbutton = self.add_spinbutton(grid, - 1, 999, 1, self.app_obj.auto_delete_days, - 2, 1, 1, 1, - ) - # Signal connect appears below - - checkbutton2 = self.add_checkbutton(grid, - '...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, - ) - # Signal connect appears below - if not self.app_obj.auto_delete_flag: - checkbutton2.set_sensitive(False) - - # Signal connects from above - checkbutton.connect( - 'toggled', - self.on_auto_delete_button_toggled, - spinbutton, - checkbutton2, - ) - spinbutton.connect( - 'value-changed', - self.on_auto_delete_spinbutton_changed, - ) - checkbutton2.connect('toggled', self.on_delete_watched_button_toggled) - - - def setup_filesystem_temp_folders_tab(self, inner_notebook): - - """Called by self.setup_filesystem_tab(). - - Sets up the 'Temporary folders' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Temporary folders', - inner_notebook, - ) - - # Temporary folder preferences - self.add_label(grid, - 'Temporary folder preferences', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Empty temporary folders when ' + __main__.__prettyname__ \ - + ' shuts down', - self.app_obj.delete_on_shutdown_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - # signal_connect appears below - - self.add_label(grid, - '(N.B. Temporary folders are always emptied when ' \ - + __main__.__prettyname__ + ' starts up)', - 0, 2, 1, 1, - ) - - checkbutton2 = self.add_checkbutton(grid, - 'Open temporary folders (on the desktop) when ' \ - + __main__.__prettyname__ + ' shuts down', - self.app_obj.open_temp_on_desktop_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton2.connect('toggled', self.on_open_desktop_button_toggled) - if self.app_obj.delete_on_shutdown_flag: - checkbutton2.set_sensitive(False) - - # signal_connects from above - checkbutton.connect( - 'toggled', - self.on_delete_shutdown_button_toggled, - checkbutton2, - ) - - - def setup_windows_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Window' tab. - """ - - # Add this tab... - 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_system_tray_tab(inner_notebook) - self.setup_windows_dialogue_windows_tab(inner_notebook) - self.setup_windows_errors_warnings_tab(inner_notebook) - self.setup_windows_websites_tab(inner_notebook) - - - def setup_windows_main_window_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Main Window' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Main window', inner_notebook) - - # Main window preferences - self.add_label(grid, - 'Main window preferences', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - '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, - ) - checkbutton.connect('toggled', self.on_remember_size_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - 'Don\'t show labels in the toolbar', - self.app_obj.toolbar_squeeze_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.connect('toggled', self.on_squeeze_button_toggled) - - checkbutton3 = self.add_checkbutton(grid, - 'Show tooltips for videos, channels, playlists and folders', - self.app_obj.show_tooltips_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - 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, - ) - 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, - ) - checkbutton5.connect('toggled', self.on_complex_button_toggled) - - checkbutton6 = self.add_checkbutton(grid, - 'After clicking on a folder, automatically expand the Video' \ - + ' Index beneath it', - self.app_obj.auto_expand_video_index_flag, - True, # Can be toggled by user - 0, 6, 1, 1, - ) - checkbutton6.connect('toggled', self.on_expand_tree_toggled) - - checkbutton7 = 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, 7, 1, 1, - ) - checkbutton7.connect('toggled', self.on_disable_dl_all_toggled) - - checkbutton8 = 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, 8, 1, 1, - ) - checkbutton8.connect('toggled', self.on_pretty_date_button_toggled) - - checkbutton9 = self.add_checkbutton(grid, - 'In the Progress Tab, hide finished videos / channels' \ - + ' / playlists', - self.app_obj.progress_list_hide_flag, - True, # Can be toggled by user - 0, 9, 1, 1, - ) - checkbutton9.connect('toggled', self.on_hide_button_toggled) - - checkbutton10 = self.add_checkbutton(grid, - 'In the Progress Tab, show results in reverse order', - self.app_obj.results_list_reverse_flag, - True, # Can be toggled by user - 0, 10, 1, 1, - ) - checkbutton10.connect('toggled', self.on_reverse_button_toggled) - - checkbutton11 = self.add_checkbutton(grid, - 'In the Errors/Warnings Tab, preserve message counts in the' \ - + ' tab label for longer', - self.app_obj.system_msg_keep_totals_flag, - True, # Can be toggled by user - 0, 11, 1, 1, - ) - checkbutton11.connect('toggled', self.on_system_keep_button_toggled) - - - def setup_windows_system_tray_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'System tray' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_System tray', inner_notebook) - - - # System tray preferences - self.add_label(grid, - 'System tray preferences', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Show icon in system tray', - self.app_obj.show_status_icon_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.set_hexpand(False) - # signal connnect appears below - - checkbutton2 = self.add_checkbutton(grid, - 'Close to the tray, rather than closing the application', - self.app_obj.close_to_tray_flag, - True, # Can be toggled by user - 0, 12, 1, 1, - ) - checkbutton2.set_hexpand(False) - checkbutton2.connect('toggled', self.on_close_to_tray_toggled) - if not self.app_obj.show_status_icon_flag: - checkbutton2.set_sensitive(False) - - # signal connect from above - checkbutton.connect( - 'toggled', - self.on_show_status_icon_toggled, - checkbutton2, - ) - - - def setup_windows_dialogue_windows_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Dialogue windows' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Dialogue windows', - inner_notebook, - ) - - # Dialogue window preferences - self.add_label(grid, - 'Dialogue window preferences', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - '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, - ) - checkbutton.set_hexpand(False) - # signal connnect appears below - - checkbutton2 = self.add_checkbutton(grid, - 'When the dialogue window opens, copy URLs from the system' \ - + ' clipboard', - self.app_obj.dialogue_copy_clipboard_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.set_hexpand(False) - checkbutton2.connect('toggled', self.on_clipboard_button_toggled) - if self.app_obj.dialogue_keep_open_flag: - checkbutton2.set_sensitive(False) - - # signal connect from above - checkbutton.connect( - 'toggled', - self.on_keep_open_button_toggled, - checkbutton2, - ) - - - def setup_windows_errors_warnings_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Errors/Warnings' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Errors/Warnings', - inner_notebook, - ) - - grid_width = 2 - - # Errors/Warnings tab preferences - self.add_label(grid, - 'Errors/Warnings tab preferences', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Show ' + __main__.__prettyname__ + ' error messages', - self.app_obj.system_error_show_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.connect('toggled', self.on_system_error_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - 'Show ' + __main__.__prettyname__ + ' warning messages', - self.app_obj.system_warning_show_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.connect('toggled', self.on_system_warning_button_toggled) - - checkbutton3 = self.add_checkbutton(grid, - 'Show server error messages', - self.app_obj.operation_error_show_flag, - True, # Can be toggled by user - 1, 1, 1, 1, - ) - checkbutton3.connect( - 'toggled', - self.on_operation_error_button_toggled, - ) - - checkbutton4 = self.add_checkbutton(grid, - 'Show server warning messages', - self.app_obj.operation_warning_show_flag, - True, # Can be toggled by user - 1, 2, 1, 1, - ) - checkbutton4.connect( - 'toggled', - self.on_operation_warning_button_toggled, - ) - - # youtube-dl error/warning preferences - self.add_label(grid, - 'youtube-dl error/warning preferences', - 0, 3, 1, 1, - ) - - checkbutton5 = self.add_checkbutton(grid, - 'Ignore \'Child process exited with non-zero code\' errors', - self.app_obj.ignore_child_process_exit_flag, - True, # Can be toggled by user - 0, 4, grid_width, 1, - ) - 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, - ) - checkbutton6.connect('toggled', self.on_http_404_button_toggled) - - checkbutton7 = self.add_checkbutton(grid, - '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, - ) - 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, - ) - checkbutton8.connect('toggled', self.on_merge_button_toggled) - - checkbutton9 = self.add_checkbutton(grid, - '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, - ) - checkbutton9.connect('toggled', self.on_missing_format_button_toggled) - - checkbutton10 = self.add_checkbutton(grid, - 'Ignore \'There are no annotations to write\' warnings', - self.app_obj.ignore_no_annotations_flag, - True, # Can be toggled by user - 0, 9, grid_width, 1, - ) - checkbutton10.connect('toggled', self.on_no_annotations_button_toggled) - - checkbutton11 = self.add_checkbutton(grid, - 'Ignore \'Video doesn\'t have subtitles\' warnings', - self.app_obj.ignore_no_subtitles_flag, - True, # Can be toggled by user - 0, 10, grid_width, 1, - ) - checkbutton11.connect('toggled', self.on_no_subtitles_button_toggled) - - - def setup_windows_websites_tab(self, inner_notebook): - - """Called by self.setup_windows_tab(). - - Sets up the 'Websites' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Websites', - inner_notebook, - ) - - grid_width = 2 - - # Youtube error/warning preferences - self.add_label(grid, - 'Youtube error/warning preferences', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Ignore YouTube copyright errors', - self.app_obj.ignore_yt_copyright_flag, - True, # Can be toggled by user - 0, 1, grid_width, 1, - ) - checkbutton.connect('toggled', self.on_copyright_button_toggled) - - checkbutton2 = self.add_checkbutton(grid, - 'Ignore YouTube age-restriction errors', - self.app_obj.ignore_yt_age_restrict_flag, - True, # Can be toggled by user - 0, 2, grid_width, 1, - ) - checkbutton2.connect('toggled', self.on_age_restrict_button_toggled) - - checkbutton3 = self.add_checkbutton(grid, - '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, - ) - checkbutton3.connect('toggled', self.on_uploader_button_toggled) - - # Custom error/warning preferences - self.add_label(grid, - '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)', - 0, 5, grid_width, 1, - ) - - textview, textbuffer = self.add_textview(grid, - self.app_obj.ignore_custom_msg_list, - 0, 6, grid_width, 1 - ) - - radiobutton = self.add_radiobutton(grid, - None, - 'These are ordinary strings', - 0, 7, 1, 1, - ) - # Signal connect appears below - - radiobutton2 = self.add_radiobutton(grid, - radiobutton, - 'These are regular expressions (regexes)', - 1, 7, 1, 1, - ) - if self.app_obj.ignore_custom_regex_flag: - radiobutton2.set_active(True) - # Signal connect appears below - - # Signal connects from above - textbuffer.connect('changed', self.on_custom_textview_changed) - radiobutton.connect( - 'toggled', - self.on_regex_button_toggled, - False, - ) - radiobutton2.connect( - 'toggled', - self.on_regex_button_toggled, - True, - ) - - - def setup_scheduling_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Scheduling' tab. - """ - - # Add this tab... - tab, grid = self.add_notebook_tab('_Scheduling', 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_scheduling_start_tab(inner_notebook) - self.setup_scheduling_stop_tab(inner_notebook) - - - def setup_scheduling_start_tab(self, inner_notebook): - - """Called by self.setup_scheduling_tab(). - - Sets up the 'Start' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Start', inner_notebook) - - grid_width = 2 - - # Scheduled start preferences - self.add_label(grid, - 'Scheduled start preferences', - 0, 0, grid_width, 1, - ) - - self.add_label(grid, - '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'] ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 1, 1, 1, 1) - combo.set_hexpand(True) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 1) - combo.set_entry_text_column(1) - - if self.app_obj.scheduled_dl_mode == 'start': - combo.set_active(1) - elif self.app_obj.scheduled_dl_mode == 'scheduled': - combo.set_active(2) - else: - combo.set_active(0) - # Signal connect appears below - - self.add_label(grid, - 'Time (in hours) between operations', - 0, 2, 1, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 1, 999, 1, self.app_obj.scheduled_dl_wait_hours, - 1, 2, 1, 1, - ) - if self.app_obj.scheduled_dl_mode != 'scheduled': - spinbutton.set_sensitive(False) - # Signal connect appears below - - self.add_label(grid, - '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'] ) - - combo2 = Gtk.ComboBox.new_with_model(store2) - grid.attach(combo2, 1, 3, 1, 1) - combo.set_hexpand(True) - - renderer_text = Gtk.CellRendererText() - combo2.pack_start(renderer_text, True) - combo2.add_attribute(renderer_text, 'text', 1) - combo2.set_entry_text_column(1) - - if self.app_obj.scheduled_check_mode == 'start': - combo2.set_active(1) - elif self.app_obj.scheduled_check_mode == 'scheduled': - combo2.set_active(2) - else: - combo2.set_active(0) - # Signal connect appears below - - self.add_label(grid, - 'Time (in hours) between operations', - 0, 4, 1, 1, - ) - - spinbutton2 = self.add_spinbutton(grid, - 1, 999, 1, self.app_obj.scheduled_check_wait_hours, - 1, 4, 1, 1, - ) - if self.app_obj.scheduled_check_mode != 'scheduled': - spinbutton2.set_sensitive(False) - # Signal connect appears below - - checkbutton = self.add_checkbutton(grid, - 'After an automatic \'Download/Check all\' operation, shut down' \ - + script, - self.app_obj.scheduled_shutdown_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - - # Signal connects from above - combo.connect('changed', self.on_dl_mode_combo_changed, spinbutton) - spinbutton.connect('value-changed', self.on_dl_wait_spinbutton_changed) - combo2.connect( - 'changed', - self.on_check_mode_combo_changed, - spinbutton2, - ) - spinbutton2.connect( - 'value-changed', - self.on_check_wait_spinbutton_changed, - ) - checkbutton.connect('toggled', self.on_scheduled_stop_button_toggled) - - - def setup_scheduling_stop_tab(self, inner_notebook): - - """Called by self.setup_scheduling_tab(). - - Sets up the 'Stop' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('S_top', inner_notebook) - - grid_width = 3 - - # Scheduled stop preferences - self.add_label(grid, - 'Scheduled stop preferences', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Stop all download operations after this much time', - self.app_obj.autostop_time_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - # Signal connect appears below - - spinbutton = self.add_spinbutton(grid, - 1, None, 1, self.app_obj.autostop_time_value, - 1, 1, 1, 1, - ) - 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, - ) - combo.set_active( - formats.TIME_METRIC_LIST.index( - self.app_obj.autostop_time_unit, - ) - ) - if not self.app_obj.autostop_time_flag: - combo.set_sensitive(False) - # Signal connect appears below - - # Signal connects from above - checkbutton.connect( - 'toggled', - self.on_autostop_time_button_toggled, - spinbutton, - combo, - ) - spinbutton.connect( - 'value-changed', - self.on_autostop_time_spinbutton_toggled, - ) - combo.connect('changed', self.on_autostop_time_combo_changed) - - checkbutton2 = self.add_checkbutton(grid, - 'Stop all download operations after this many videos', - self.app_obj.autostop_videos_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - # Signal connect appears below - - spinbutton2 = self.add_spinbutton(grid, - 1, None, 1, self.app_obj.autostop_videos_value, - 1, 2, 1, 1, - ) - if not self.app_obj.autostop_videos_flag: - spinbutton2.set_sensitive(False) - # Signal connect appears below - - # Signal connects from above - checkbutton2.connect( - 'toggled', - self.on_autostop_videos_button_toggled, - spinbutton2, - ) - spinbutton2.connect( - 'value-changed', - self.on_autostop_videos_spinbutton_toggled, - ) - - checkbutton3 = self.add_checkbutton(grid, - '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, - ) - # Signal connect appears below - - spinbutton3 = self.add_spinbutton(grid, - 1, None, 1, self.app_obj.autostop_size_value, - 1, 3, 1, 1, - ) - if not self.app_obj.autostop_size_flag: - spinbutton3.set_sensitive(False) - - combo3 = self.add_combo(grid, - formats.FILESIZE_METRIC_LIST, - None, - 2, 3, 1, 1, - ) - combo3.set_active( - formats.FILESIZE_METRIC_LIST.index( - self.app_obj.autostop_size_unit, - ) - ) - if not self.app_obj.autostop_size_flag: - combo3.set_sensitive(False) - # Signal connect appears below - - # Signal connects from above - checkbutton3.connect( - 'toggled', - self.on_autostop_size_button_toggled, - spinbutton3, - combo3, - ) - spinbutton3.connect( - 'value-changed', - self.on_autostop_size_spinbutton_toggled, - ) - 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\')', - 0, 4, grid_width, 1, - ) - - - def setup_operations_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Operations' tab. - """ - - # Add this tab... - tab, grid = self.add_notebook_tab('_Operations', 0) - - # ...and an inner notebook... - 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) - - - def setup_operations_downloads_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Downloads' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Downloads', inner_notebook) - - # Download operation preferences - self.add_label(grid, - '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, - ) - checkbutton.connect('toggled', self.on_auto_update_button_toggled) - if __main__.__pkg_strict_install_flag__: - 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, - ) - checkbutton2.connect('toggled', self.on_save_button_toggled) - - checkbutton3 = self.add_checkbutton(grid, - 'When applying download options, automatically clone general' \ - + ' download options', - self.app_obj.auto_clone_options_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - 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, - ) - checkbutton4.connect('toggled', self.on_operation_sim_button_toggled) - - - def setup_operations_custom_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Custom' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Custom', inner_notebook) - grid_width = 2 - - # Custom download preferences - self.add_label(grid, - '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, - ) - checkbutton.connect('toggled', self.on_custom_video_button_toggled) - - radiobutton = self.add_radiobutton(grid, - None, - 'In custom downloads, obtain a YouTube video from the original' \ - + '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': - radiobutton2.set_active(True) - # Signal connect appears below - - 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': - radiobutton3.set_active(True) - # Signal connect appears below - - checkbutton2 = self.add_checkbutton(grid, - 'In custom downloads, apply a delay after each video/channel/' \ - + 'playlist download', - self.app_obj.custom_dl_delay_flag, - True, # Can be toggled by user - 0, 5, grid_width, 1, - ) - # signal_connect appears below - - self.add_label(grid, - 'Maximum delay to apply (in minutes)', - 0, 6, 1, 1, - ) - - spinbutton = self.add_spinbutton(grid, - 0.2, - None, - 0.2, # Step - self.app_obj.custom_dl_delay_max, - 1, 6, 1, 1, - ) - # signal_connect appears below - if not self.app_obj.custom_dl_delay_flag: - spinbutton.set_sensitive(False) - - self.add_label(grid, - 'Minimum delay to apply (in minutes; randomises the actual' \ - + ' delay)', - 0, 7, 1, 1, - ) - - spinbutton2 = self.add_spinbutton(grid, - 0, - self.app_obj.custom_dl_delay_max, - 0.2, # Step - self.app_obj.custom_dl_delay_min, - 1, 7, 1, 1, - ) - spinbutton2.connect( - 'value-changed', - self.on_delay_min_spinbutton_changed, - ) - if not self.app_obj.custom_dl_delay_flag: - spinbutton2.set_sensitive(False) - - # signal_connects from above - radiobutton.connect( - 'toggled', - self.on_custom_divert_button_toggled, - 'default', - ) - - radiobutton2.connect( - 'toggled', - self.on_custom_divert_button_toggled, - 'hooktube', - ) - - radiobutton3.connect( - 'toggled', - self.on_custom_divert_button_toggled, - 'invidious', - ) - - checkbutton2.connect( - 'toggled', - self.on_custom_delay_button_toggled, - spinbutton, - spinbutton2, - ) - - spinbutton.connect( - 'value-changed', - self.on_delay_max_spinbutton_changed, - spinbutton2, - ) - - - def setup_operations_notifications_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Notifications' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Notifications', - inner_notebook, - ) - - # Desktop notification preferences - self.add_label(grid, - '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': - radiobutton2.set_active(True) - if os.name == 'nt': - radiobutton2.set_sensitive(False) - # Signal connect appears below - - 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': - radiobutton3.set_active(True) - # Signal connect appears below - - # Signal connects from above - radiobutton.connect( - 'toggled', - self.on_dialogue_button_toggled, - 'dialogue', - ) - radiobutton2.connect( - 'toggled', - self.on_dialogue_button_toggled, - 'desktop', - ) - radiobutton3.connect( - 'toggled', - self.on_dialogue_button_toggled, - 'default', - ) - - - def setup_operations_url_flexibility_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'URL flexibility' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_URL flexibility', - inner_notebook, - ) - - # URL flexibility preferences - self.add_label(grid, - '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', - 0, 2, 1, 1, - ) - if self.app_obj.operation_convert_mode == 'multi': - radiobutton2.set_active(True) - # Signal connect appears below - - 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': - radiobutton3.set_active(True) - # Signal connect appears below - - 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': - radiobutton4.set_active(True) - # Signal connect appears below - - # Signal connects from above - radiobutton.connect( - 'toggled', - self.on_convert_from_button_toggled, - 'disable', - ) - radiobutton2.connect( - 'toggled', - self.on_convert_from_button_toggled, - 'multi', - ) - radiobutton3.connect( - 'toggled', - self.on_convert_from_button_toggled, - 'channel', - ) - radiobutton4.connect( - 'toggled', - self.on_convert_from_button_toggled, - 'playlist', - ) - - - def setup_operations_performance_tab(self, inner_notebook): - - """Called by self.setup_operations_tab(). - - Sets up the 'Performance' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Performance', - inner_notebook, - ) - - grid_width = 3 - - # Performance limits - self.add_label(grid, - 'Performance limits', - 0, 0, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Limit simultaneous downloads to', - self.app_obj.num_worker_apply_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect('toggled', self.on_worker_button_toggled) - - spinbutton = self.add_spinbutton(grid, - self.app_obj.num_worker_min, - self.app_obj.num_worker_max, - 1, # Step - self.app_obj.num_worker_default, - 1, 1, 1, 1, - ) - spinbutton.connect('value-changed', self.on_worker_spinbutton_changed) - - checkbutton2 = self.add_checkbutton(grid, - 'Limit download speed to', - self.app_obj.bandwidth_apply_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.set_hexpand(False) - checkbutton2.connect('toggled', self.on_bandwidth_button_toggled) - - spinbutton2 = self.add_spinbutton(grid, - self.app_obj.bandwidth_min, - self.app_obj.bandwidth_max, - 1, # Step - self.app_obj.bandwidth_default, - 1, 2, 1, 1, - ) - spinbutton2.connect( - 'value-changed', - self.on_bandwidth_spinbutton_changed, - ) - - self.add_label(grid, - 'KiB/s', - 2, 2, 1, 1, - ) - - checkbutton3 = self.add_checkbutton(grid, - 'Limit video resolution (overriding video format options) to', - self.app_obj.video_res_apply_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton3.set_hexpand(False) - checkbutton3.connect('toggled', self.on_video_res_button_toggled) - - combo = self.add_combo(grid, - formats.VIDEO_RESOLUTION_LIST, - None, - 1, 3, 1, 1, - ) - combo.set_active( - formats.VIDEO_RESOLUTION_LIST.index( - self.app_obj.video_res_default, - ) - ) - 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, - ) - - checkbutton = 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, - ) - checkbutton.set_hexpand(False) - # Signal connect appears below - - self.add_label(grid, - 'Stop after this many videos (when checking)', - 0, 2, 1, 1, - ) - - entry = self.add_entry(grid, - self.app_obj.operation_check_limit, - True, - 1, 2, 1, 1, - ) - entry.set_width_chars(4) - entry.connect('changed', self.on_check_limit_changed) - if not self.app_obj.operation_limit_flag: - entry.set_sensitive(False) - - self.add_label(grid, - 'Stop after this many videos (when downloading)', - 0, 3, 1, 1, - ) - - entry2 = self.add_entry(grid, - self.app_obj.operation_download_limit, - True, - 1, 3, 1, 1, - ) - entry2.set_width_chars(4) - entry2.connect('changed', self.on_dl_limit_changed) - if not self.app_obj.operation_limit_flag: - entry2.set_sensitive(False) - - # Signal connect from above - checkbutton.connect( - 'toggled', - self.on_limit_button_toggled, - entry, - entry2, - ) - - - def setup_ytdl_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'youtube-dl' tab. - """ - - tab, grid = self.add_notebook_tab('_youtube-dl') - grid_width = 4 - - # youtube-dl preferences - self.add_label(grid, - 'youtube-dl preferences', - 0, 0, grid_width, 1, - ) - - - label = self.add_label(grid, - 'youtube-dl executable (system-dependant)', - 0, 1, 1, 1, - ) - - entry = self.add_entry(grid, - self.app_obj.ytdl_bin, - False, - 1, 1, (grid_width - 1), 1, - ) - entry.set_sensitive(True) - entry.set_editable(False) - - label2 = self.add_label(grid, - 'Default path to youtube-dl executable', - 0, 2, 1, 1, - ) - - entry2 = self.add_entry(grid, - self.app_obj.ytdl_path_default, - False, - 1, 2, (grid_width - 1), 1, - ) - entry2.set_sensitive(True) - entry2.set_editable(False) - - label3 = self.add_label(grid, - 'Actual path to use', - 0, 3, 1, 1, - ) - - combo_list = [ - [ - 'Use default path (' + self.app_obj.ytdl_path_default \ - + ')', - self.app_obj.ytdl_path_default, - ], - [ - 'Use local path (' + self.app_obj.ytdl_bin + ')', - self.app_obj.ytdl_bin, - ], - ] - if os.name != 'nt': - - combo_list.append( - [ - 'Use PyPI path (' + self.app_obj.ytdl_path_pypi + ')', - self.app_obj.ytdl_path_pypi, - ], - ) - - store = Gtk.ListStore(str, str) - for mini_list in combo_list: - store.append( [ mini_list[0], mini_list[1] ] ) - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 1, 3, (grid_width - 1), 1) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 0) - combo.set_entry_text_column(0) - - if self.app_obj.ytdl_path == self.app_obj.ytdl_path_default: - combo.set_active(0) - elif self.app_obj.ytdl_path == self.app_obj.ytdl_path_pypi: - combo.set_active(2) - else: - combo.set_active(1) - - combo.connect('changed', self.on_ytdl_path_combo_changed) - - label4 = self.add_label(grid, - '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, - ) - 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', - 0, 5, grid_width, 1, - ) - - self.add_label(grid, - 'Path to the ffmpeg/avconv binary', - 0, 6, 1, 1, - ) - - entry3 = self.add_entry(grid, - self.app_obj.ffmpeg_path, - False, - 1, 6, 1, 1, - ) - entry3.set_sensitive(True) - entry3.set_editable(False) - entry3.set_hexpand(True) - - 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') - 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') - button.set_sensitive(False) - button2.set_sensitive(False) - - # Other preferences - self.add_label(grid, - 'Other preferences', - 0, 7, grid_width, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Allow youtube-dl to create its own archive (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, - ) - 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, - ) - checkbutton2.connect('toggled', self.on_json_button_toggled) - - - def setup_output_tab(self): - - """Called by self.setup_tabs(). - - Sets up the 'Output' tab. - """ - - # Add this tab... - tab, grid = self.add_notebook_tab('Out_put', 0) - - # ...and an inner notebook... - inner_notebook = self.add_inner_notebook(grid) - - # ...with its own tabs - self.setup_output_outputtab_tab(inner_notebook) - self.setup_output_terminal_window_tab(inner_notebook) - self.setup_output_both_tab(inner_notebook) - - - def setup_output_outputtab_tab(self, inner_notebook): - - """Called by self.setup_output_tab(). - - Sets up the 'Output Tab' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab('_Output Tab', inner_notebook) - - # Output Tab preferences - self.add_label(grid, - 'Output Tab preferences', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - '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, - ) - checkbutton.set_hexpand(False) - 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', - self.app_obj.ytdl_output_stdout_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.set_hexpand(False) - # Signal connect appears below - - checkbutton3 = self.add_checkbutton(grid, - '...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, - ) - checkbutton3.set_hexpand(False) - checkbutton3.connect('toggled', self.on_output_json_button_toggled) - if not self.app_obj.ytdl_output_stdout_flag: - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid, - '...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, - ) - checkbutton4.set_hexpand(False) - checkbutton4.connect('toggled', self.on_output_progress_button_toggled) - if not self.app_obj.ytdl_output_stdout_flag: - checkbutton4.set_sensitive(False) - - # Signal connect from above - checkbutton2.connect( - 'toggled', - self.on_output_stdout_button_toggled, - checkbutton3, - checkbutton4, - ) - - checkbutton5 = self.add_checkbutton(grid, - '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, - ) - checkbutton5.set_hexpand(False) - checkbutton5.connect('toggled', self.on_output_stderr_button_toggled) - - checkbutton6 = self.add_checkbutton(grid, - 'Empty pages in the Output Tab at the start of every operation', - self.app_obj.ytdl_output_start_empty_flag, - True, # Can be toggled by user - 0, 6, 1, 1, - ) - checkbutton6.set_hexpand(False) - checkbutton6.connect('toggled', self.on_output_empty_button_toggled) - - checkbutton7 = self.add_checkbutton(grid, - 'Show a summary of active threads (changes are applied when ' \ - + __main__.__prettyname__ + ' restarts', - self.app_obj.ytdl_output_show_summary_flag, - True, # Can be toggled by user - 0, 7, 1, 1, - ) - checkbutton7.set_hexpand(False) - checkbutton7.connect('toggled', self.on_output_summary_button_toggled) - - 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, - ) - checkbutton8.set_hexpand(False) - # Signal connect appears below - - checkbutton9 = self.add_checkbutton(grid, - '...also show all non-matching videos', - self.app_obj.refresh_output_verbose_flag, - True, # Can be toggled by user - 0, 9, 1, 1, - ) - checkbutton9.set_hexpand(False) - checkbutton9.connect( - 'toggled', - self.on_refresh_verbose_button_toggled, - ) - if not self.app_obj.refresh_output_videos_flag: - checkbutton8.set_sensitive(False) - - # Signal connect from above - checkbutton8.connect( - 'toggled', - self.on_refresh_videos_button_toggled, - checkbutton9, - ) - - - def setup_output_terminal_window_tab(self, inner_notebook): - - """Called by self.setup_output_tab(). - - Sets up the 'Terminal window' inner notebook tab. - """ - - tab, grid = self.add_inner_notebook_tab( - '_Terminal window', - inner_notebook, - ) - - # Terminal window preferences - self.add_label(grid, - 'Terminal window preferences', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - '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, - ) - checkbutton.set_hexpand(False) - 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', - self.app_obj.ytdl_write_stdout_flag, - True, # Can be toggled by user - 0, 2, 1, 1, - ) - checkbutton2.set_hexpand(False) - # Signal connect appears below - - checkbutton3 = self.add_checkbutton(grid, - '...but don\'t write each video\'s JSON data', - self.app_obj.ytdl_write_ignore_json_flag, - True, # Can be toggled by user - 0, 3, 1, 1, - ) - checkbutton3.set_hexpand(False) - checkbutton3.connect('toggled', self.on_terminal_json_button_toggled) - if not self.app_obj.ytdl_write_stdout_flag: - checkbutton3.set_sensitive(False) - - checkbutton4 = self.add_checkbutton(grid, - '...but don\'t write each video\'s download progress', - self.app_obj.ytdl_write_ignore_progress_flag, - True, # Can be toggled by user - 0, 4, 1, 1, - ) - checkbutton4.set_hexpand(False) - checkbutton4.connect( - 'toggled', - self.on_terminal_progress_button_toggled, - ) - if not self.app_obj.ytdl_write_stdout_flag: - checkbutton4.set_sensitive(False) - - # Signal connect from above - checkbutton2.connect( - 'toggled', - self.on_terminal_stdout_button_toggled, - checkbutton3, - checkbutton4, - ) - - checkbutton5 = self.add_checkbutton(grid, - 'Write output from youtube-dl\'s STDERR to the terminal window', - self.app_obj.ytdl_write_stderr_flag, - True, # Can be toggled by user - 0, 5, 1, 1, - ) - checkbutton5.set_hexpand(False) - checkbutton5.connect( - 'toggled', - self.on_terminal_stderr_button_toggled, - ) - - - def setup_output_both_tab(self, inner_notebook): - - """Called by self.setup_output_tab(). - - Sets up the 'Both' inner notebook tab. - """ - - 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)', - 0, 0, 1, 1, - ) - - checkbutton = self.add_checkbutton(grid, - 'Write verbose output (youtube-dl debugging mode)', - self.app_obj.ytdl_write_verbose_flag, - True, # Can be toggled by user - 0, 1, 1, 1, - ) - checkbutton.set_hexpand(False) - checkbutton.connect('toggled', self.on_verbose_button_toggled) - - - # Callback class methods - - - def on_add_from_list_button_toggled(self, checkbutton): - - """Called from callback in self.setup_filesystem_database_tab(). - - Enables/disables automatic adding of new Tartube data directories to - the list of recent directories. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.data_dir_add_from_list_flag: - self.app_obj.set_data_dir_add_from_list_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.data_dir_add_from_list_flag: - self.app_obj.set_data_dir_add_from_list_flag(False) - - - def on_age_restrict_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_websites_tab(). - - Enables/disables ignoring of YouTube age-restriction error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_yt_age_restrict_flag: - self.app_obj.set_ignore_yt_age_restrict_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_yt_age_restrict_flag: - self.app_obj.set_ignore_yt_age_restrict_flag(False) - - - def on_archive_button_toggled(self, checkbutton): - - """Called from callback in self.setup_ytdl_tab(). - - Enables/disables creation of youtube-dl's archive file, - ytdl-archive.txt. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.allow_ytdl_archive_flag: - self.app_obj.set_allow_ytdl_archive_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.allow_ytdl_archive_flag: - self.app_obj.set_allow_ytdl_archive_flag(False) - - - def on_auto_clone_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables auto-cloning of the General Options Manager. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_clone_options_flag: - self.app_obj.set_auto_clone_options_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_clone_options_flag: - self.app_obj.set_auto_clone_options_flag(False) - - - def on_auto_delete_button_toggled(self, checkbutton, spinbutton, - checkbutton2): - - """Called from callback in self.setup_filesystem_video_deletion_tab(). - - Enables/disables automatic deletion of downloaded videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): A widget to be (de)sensitised - - checkbutton2 (Gtk.CheckButton): Another widget to be - (de)sensitised - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_delete_flag: - self.app_obj.set_auto_delete_flag(True) - spinbutton.set_sensitive(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.auto_delete_flag: - self.app_obj.set_auto_delete_flag(False) - spinbutton.set_sensitive(False) - checkbutton2.set_sensitive(False) - - - def on_auto_delete_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_filesystem_video_deletion_tab(). - - Sets the number of days after which downloaded videos should be - deleted. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_auto_delete_days(spinbutton.get_value()) - - - def on_auto_update_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables automatic update operation before every download - operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.operation_auto_update_flag: - self.app_obj.set_operation_auto_update_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.operation_auto_update_flag: - self.app_obj.set_operation_auto_update_flag(False) - - - def on_autostop_size_button_toggled(self, checkbutton, spinbutton, combo): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Enables/disables auto-stopping a download operation after a certain - amount of disk space. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to modify - - combo (Gtk.ComboBox): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.autostop_size_flag: - self.app_obj.set_autostop_size_flag(True) - spinbutton.set_sensitive(True) - combo.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.autostop_size_flag: - self.app_obj.set_autostop_size_flag(False) - spinbutton.set_sensitive(False) - combo.set_sensitive(False) - - - def on_autostop_size_combo_changed(self, combo): - - """Called from a callback in self.setup_scheduling_stop_tab(). - - Sets the disk space unit at which a download operation is auto-stopped. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_autostop_size_unit(model[tree_iter][0]) - - - def on_autostop_size_spinbutton_toggled(self, spinbutton): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Sets the disk space value at which a download operation is - auto-stopped. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_autostop_size_value(spinbutton.get_value()) - - - def on_autostop_time_button_toggled(self, checkbutton, spinbutton, combo): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Enables/disables auto-stopping a download operation after a certain - time. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to modify - - combo (Gtk.ComboBox): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.autostop_time_flag: - self.app_obj.set_autostop_time_flag(True) - spinbutton.set_sensitive(True) - combo.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.autostop_time_flag: - self.app_obj.set_autostop_time_flag(False) - spinbutton.set_sensitive(False) - combo.set_sensitive(False) - - - def on_autostop_time_combo_changed(self, combo): - - """Called from a callback in self.setup_scheduling_stop_tab(). - - Sets the time unit at which a download operation is auto-stopped. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_autostop_time_unit(model[tree_iter][0]) - - - def on_autostop_time_spinbutton_toggled(self, spinbutton): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Sets the time value at which a download operation is auto-stopped. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_autostop_time_value(spinbutton.get_value()) - - - def on_autostop_videos_button_toggled(self, checkbutton, spinbutton): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Enables/disables auto-stopping a download operation after a certain - number of videos. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to modify - - """ - - if checkbutton.get_active() \ - and not self.app_obj.autostop_videos_flag: - self.app_obj.set_autostop_videos_flag(True) - spinbutton.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.autostop_videos_flag: - self.app_obj.set_autostop_videos_flag(False) - spinbutton.set_sensitive(False) - - - def on_autostop_videos_spinbutton_toggled(self, spinbutton): - - """Called from callback in self.setup_scheduling_stop_tab(). - - Sets the number of videos at which a download operation is - auto-stopped. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_autostop_videos_value(spinbutton.get_value()) - - - def on_backup_button_toggled(self, radiobutton, value): - - """Called from callback in self.setup_filesystem_backups_tab(). - - Updates IVs in the main application. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - value (str): The new value of the IV - - """ - - if radiobutton.get_active(): - self.app_obj.set_db_backup_mode(value) - - - def on_bandwidth_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_performance_tab(). - - Enables/disables the download speed limit. Toggling the corresponding - Gtk.CheckButton in the Progress Tab sets the IV (and makes sure the two - checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - other_flag \ - = self.app_obj.main_win_obj.bandwidth_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - self.app_obj.main_win_obj.bandwidth_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - self.app_obj.main_win_obj.bandwidth_checkbutton.set_active(False) - - - def on_bandwidth_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_performance_tab(). - - Sets the simultaneous download limit. Setting the value of the - corresponding Gtk.SpinButton in the Progress Tab sets the IV (and - makes sure the two spinbuttons have the same value). - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.main_win_obj.bandwidth_spinbutton.set_value( - spinbutton.get_value(), - ) - - - def on_check_limit_changed(self, entry): - - """Called from callback in self.setup_operations_time_saving_tab(). - - Sets the limit at which a download operation will stop checking a - channel or playlist. - - Args: - - entry (Gtk.Entry): The widget changed - - """ - - text = entry.get_text() - if text.isdigit() and int(text) >= 0: - self.app_obj.set_operation_check_limit(int(text)) - - - def on_check_mode_combo_changed(self, combo, spinbutton): - - """Called from a callback in self.setup_scheduling_start_tab(). - - Extracts the value visible in the combobox, converts it into another - value, and uses that value to update the main application's IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to be (de)sensitised - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_scheduled_check_mode(model[tree_iter][0]) - if self.app_obj.scheduled_check_mode != 'scheduled': - spinbutton.set_sensitive(False) - else: - spinbutton.set_sensitive(True) - - - def on_check_wait_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_scheduling_start_tab(). - - Sets the interval between scheduled 'Check all' operations. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_scheduled_check_wait_hours(spinbutton.get_value()) - - - def on_child_process_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables ignoring of child process exit error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_child_process_exit_flag: - self.app_obj.set_ignore_child_process_exit_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_child_process_exit_flag: - self.app_obj.set_ignore_child_process_exit_flag(False) - - - def on_clipboard_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_dialogue_windows_tab(). - - Enables/disables copying from the system clipboard in various dialogue - windows. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.dialogue_copy_clipboard_flag: - self.app_obj.set_dialogue_copy_clipboard_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.dialogue_copy_clipboard_flag: - self.app_obj.set_dialogue_copy_clipboard_flag(False) - - - def on_close_to_tray_toggled(self, checkbutton): - - """Called from a callback in self.setup_windows_system_tray_tab(). - - Enables/disables closing to the system tray, rather than closing the - application. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.close_to_tray_flag: - self.app_obj.set_close_to_tray_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.close_to_tray_flag: - self.app_obj.set_close_to_tray_flag(False) - - - def on_complex_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Switches between simple/complex views in the Video Index. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - redraw_flag = False - if checkbutton.get_active() and not self.app_obj.complex_index_flag: - self.app_obj.set_complex_index_flag(True) - redraw_flag = True - elif not checkbutton.get_active() and self.app_obj.complex_index_flag: - self.app_obj.set_complex_index_flag(False) - redraw_flag = True - - if redraw_flag: - # Redraw the Video Index and the Video Catalogue (since nothing in - # the Video Index will be selected) - self.app_obj.main_win_obj.video_index_catalogue_reset() - - - def on_convert_from_button_toggled(self, radiobutton, mode): - - """Called from callback in self.setup_operations_url_flexibility_tab(). - - Set what happens when downloading a media.Video object whose URL - represents a channel/playlist. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - mode (str): The new value for the IV: 'disable', 'multi', - 'channel' or 'playlist' - - """ - - if radiobutton.get_active(): - self.app_obj.set_operation_convert_mode(mode) - - - def on_copyright_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_websites_tab(). - - Enables/disables ignoring of YouTube copyright errors messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_yt_copyright_flag: - self.app_obj.set_ignore_yt_copyright_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_yt_copyright_flag: - self.app_obj.set_ignore_yt_copyright_flag(False) - - - def on_custom_delay_button_toggled(self, checkbutton, spinbutton, - spinbutton2): - - """Called from callback in self.setup_operations_custom_tab(). - - Enables/disables a delay after downloads of a media data object - (during custom downloads). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton, spinbutton2 (Gtk.SpinButton): Other widgets to be - (de)sensitised - - """ - - if checkbutton.get_active() \ - and not self.app_obj.custom_dl_delay_flag: - self.app_obj.set_custom_dl_delay_flag(True) - spinbutton.set_sensitive(True) - spinbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.custom_dl_delay_flag: - self.app_obj.set_custom_dl_delay_flag(False) - spinbutton.set_sensitive(False) - spinbutton2.set_sensitive(False) - - - def on_custom_divert_button_toggled(self, radiobutton, value): - - """Called from callback in self.setup_operations_custom_tab(). - - Enables/disables diverting downloads of YouTube videos to HookTube or - Invidious. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - value (str): The new value of the IV - - """ - - if radiobutton.get_active(): - self.app_obj.set_custom_dl_divert_mode(value) - - - def on_custom_video_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_custom_tab(). - - Enables/disables downloading videos independently of its channel/ - playlist. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.custom_dl_by_video_flag: - self.app_obj.set_custom_dl_by_video_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.custom_dl_by_video_flag: - self.app_obj.set_custom_dl_by_video_flag(False) - - - def on_custom_textview_changed(self, textbuffer): - - """Called from callback in self.setup_windows_websites_tab(). - - Sets the custom of list of ignorable error messages. - - Args: - - textbuffer (Gtk.TextBuffer): The buffer belonging to the textview - whose contents has been modified - - """ - - text = textbuffer.get_text( - textbuffer.get_start_iter(), - textbuffer.get_end_iter(), - # Don't include hidden characters - False, - ) - - # Filter out empty lines - line_list = text.split("\n") - mod_list = [] - for line in line_list: - if re.search(r'\S', line): - mod_list.append(line) - - # Apply the changes - self.app_obj.set_ignore_custom_msg_list(mod_list) - - - def on_data_block_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables ignoring of data block error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_data_block_error_flag: - self.app_obj.set_ignore_data_block_error_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_data_block_error_flag: - self.app_obj.set_ignore_data_block_error_flag(False) - - - def on_data_check_button_clicked(self, button): - - """Called from callback in self.setup_filesystem_db_errors_tab(). - - Checks the Tartube database for inconsistencies, and fixes them. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - self.app_obj.check_integrity_db() - - - def on_data_dir_change_button_clicked(self, button, entry): - - """Called from callback in self.setup_filesystem_database_tab(). - - Opens a window in which the user can select Tartube's data directoy. - If the user actually selects it, call the main application to take - action. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Additional widget to be modified by this - function - - """ - - dialogue_win = Gtk.FileChooserDialog( - 'Please select ' + __main__.__prettyname__ + '\'s data directory', - self, - Gtk.FileChooserAction.SELECT_FOLDER, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.OK, - ), - ) - - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - dialogue_manager_obj = self.app_obj.dialogue_manager_obj - - # In the past, I accidentally created a new database directory - # just inside an existing one, rather than switching to the - # existing one - # If no database file exists, prompt the user to create a new one - db_path = os.path.abspath( - os.path.join(new_path, self.app_obj.db_file_name), - ) - - 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, - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'switch_db', - 'data': [new_path, self], - }, - ) - - else: - - # Database file already exists, so try to load it now - self.try_switch_db(new_path, button) - - - def on_data_dir_cursor_changed(self, treeview, button2, button3, button4, - button5, button6): - - """Called by self.setup_filesystem_database_tab(). - - When a data directory in the list is selected, (de)sensitise buttons - in response. - - Args: - - treeview (Gtk.TreeView): The widget in which a line was selected. - - button2, button3, button4, button5, button6 (Gtk.Button): Other - widgets to be modified - - """ - - selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is not None and not self.app_obj.disable_load_save_flag: - - data_dir = model[iter][0] - - if data_dir != self.app_obj.data_dir: - button2.set_sensitive(True) - button3.set_sensitive(True) - else: - button2.set_sensitive(False) - button3.set_sensitive(False) - - posn = self.app_obj.data_dir_alt_list.index(data_dir) - if posn > 0: - button5.set_sensitive(True) - else: - button5.set_sensitive(False) - - if posn < (len(self.app_obj.data_dir_alt_list) - 1): - button6.set_sensitive(True) - else: - button6.set_sensitive(False) - - else: - - button2.set_sensitive(False) - button3.set_sensitive(False) - button5.set_sensitive(False) - button6.set_sensitive(False) - - if len(self.app_obj.data_dir_alt_list) <= 1 \ - or self.app_obj.disable_load_save_flag: - button4.set_sensitive(False) - else: - button4.set_sensitive(True) - - - def on_data_dir_forget_button_clicked(self, button, treeview): - - """Called from callback in self.setup_filesystem_database_tab(). - - Removes the selected the data directory from the list of alternative - data directories. - - Args: - - button (Gtk.Button): The widget that was clicked - - treeview (Gtk.TreeView): The widget in which a line was selected. - - """ - - selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: - - # Nothing selected - return - - else: - - data_dir = model[iter][0] - - # Should not be possible to click the button, when the current - # directory is selected, but we'll check anyway - if data_dir == self.app_obj.data_dir: - return - - # 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?', - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'forget_db', - 'data': [data_dir, self], - }, - ) - - - def on_data_dir_forget_all_button_clicked(self, button, treeview): - - """Called from callback in self.setup_filesystem_database_tab(). - - Removes all data directories from the list of alternatives, except for - the current one. - - Args: - - button (Gtk.Button): The widget that was clicked - - treeview (Gtk.TreeView): The widget in which a line was selected. - - """ - - # Should not be possible to click the button, when the list contains - # no alternatives but the current one, but we'll check anyway - if len(self.app_obj.data_dir_alt_list) <= 1: - return - - # 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 - { - 'yes': 'forget_all_db', - 'data': self, - }, - ) - - - def on_data_dir_move_up_button_clicked(self, button, treeview, liststore): - - """Called from callback in self.setup_filesystem_database_tab(). - - Moves the selected data directory up one position in the list of - alternative data directories. - - Args: - - button (Gtk.Button): The widget that was clicked - - treeview (Gtk.TreeView): The widget in which a line was selected - - liststore (Gtk.ListStore): The treeview's liststore - - """ - - selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: - - # Nothing selected - return - - else: - - data_dir = model[iter][0] - - # Update the IV - self.app_obj.reorder_db(data_dir, False) - - # Update the liststore - liststore.clear() - for item in self.app_obj.data_dir_alt_list: - liststore.append([item]) - - - def on_data_dir_move_down_button_clicked(self, button, treeview, \ - liststore): - - """Called from callback in self.setup_filesystem_database_tab(). - - Moves the selected data directory down one position in the list of - alternative data directories. - - Args: - - button (Gtk.Button): The widget that was clicked - - treeview (Gtk.TreeView): The widget in which a line was selected - - liststore (Gtk.ListStore): The treeview's liststore - - """ - - selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: - - # Nothing selected - return - - else: - - data_dir = model[iter][0] - - # Update the IV - self.app_obj.reorder_db(data_dir, True) - - # Update the liststore - liststore.clear() - for item in self.app_obj.data_dir_alt_list: - liststore.append([item]) - - - def on_data_dir_switch_button_clicked(self, button, button2, treeview, \ - entry): - - """Called from callback in self.setup_filesystem_database_tab(). - - Changes the Tartube data directory to the one selected in the - textview. - - Args: - - button (Gtk.Button): The widget clicked - - button2 (Gtk.Button): Another button to be possibly desensitised - - treeview (Gtk.TreeView): A widget in which one file path is - selected (maybe) - - entry (Gtk.Entry): Another widget to be modified - - """ - - selection = treeview.get_selection() - (model, iter) = selection.get_selected() - if iter is None: - - # Nothing selected - return - - else: - - data_dir = model[iter][0] - - # Should not be possible to click the button, when the current - # directory is selected, but we'll check anyway - if data_dir == self.app_obj.data_dir: - return - - # If no database file exists, prompt the user to create a new one - db_path = os.path.abspath( - os.path.join(data_dir, self.app_obj.db_file_name), - ) - - 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?', - 'question', - 'yes-no', - self, # Parent window is this window - { - 'yes': 'switch_db', - 'data': [data_dir, self], - }, - ) - - else: - - # Database file already exists, so try to load it now - self.try_switch_db(data_dir, button2) - - - def on_delay_max_spinbutton_changed(self, spinbutton, spinbutton2): - - """Called from callback in self.setup_operations_custom_tab(). - - Sets the maximum delay between media data object downloads during a - custom download. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - spinbutton2 (Gtk.SpinButton): Another widget to be modified - - """ - - value = spinbutton.get_value() - - self.app_obj.set_custom_dl_delay_max(value) - # Adjust the other spinbutton, so that the minimum value never exceeds - # the maximum value - spinbutton2.set_range(0, value) - if value < self.app_obj.custom_dl_delay_min: - spinbutton2.set_value(value) - - - def on_delay_min_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_custom_tab(). - - Sets the minimum delay between media data object downloads during a - custom download. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_custom_dl_delay_min(spinbutton.get_value()) - - - def on_delete_shutdown_button_toggled(self, checkbutton, checkbutton2): - - """Called from callback in self.setup_filesystem_temp_folders_tab(). - - Enables/disables emptying temporary folders when Tartube shuts down. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another widget to be modified - - """ - - if checkbutton.get_active() \ - and not self.app_obj.delete_on_shutdown_flag: - self.app_obj.set_delete_on_shutdown_flag(True) - checkbutton2.set_sensitive(False) - - elif not checkbutton.get_active() \ - and self.app_obj.delete_on_shutdown_flag: - self.app_obj.set_delete_on_shutdown_flag(False) - checkbutton2.set_sensitive(True) - - - def on_delete_watched_button_toggled(self, checkbutton): - - """Called from callback in self.setup_filesystem_video_deletion_tab(). - - Enables/disables automatic deletion of videos, but only those that have - been watched. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_delete_watched_flag: - self.app_obj.set_auto_delete_watched_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_delete_watched_flag: - self.app_obj.set_auto_delete_watched_flag(False) - - - def on_dialogue_button_toggled(self, radiobutton, mode): - - """Called from callback in self.setup_operations_notifications_tab(). - - Sets whether a desktop notification, dialogue window or neither should - be shown to the user at the end of a download/update/refresh/info/tidy - operation. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - mode (str): The new value for the IV: 'default', 'desktop' or - 'dialogue' - - """ - - if radiobutton.get_active(): - self.app_obj.set_operation_dialogue_mode(mode) - - - def on_disable_dl_all_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables the 'Download all' buttons in the main window toolbar - and in the Videos Tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.disable_dl_all_flag: - self.app_obj.set_disable_dl_all_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.disable_dl_all_flag: - self.app_obj.set_disable_dl_all_flag(False) - - - def on_disk_stop_button_toggled(self, checkbutton, spinbutton): - - """Called from a callback in self.setup_filesystem_device_tab(). - - Enables/disables halting a download operation when the system is - running out of disk space. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.CheckButton): Another widget to be (de)sensitised - - """ - - if checkbutton.get_active() \ - and not self.app_obj.disk_space_stop_flag: - self.app_obj.set_disk_space_stop_flag(True) - spinbutton.set_sensitive(True) - elif not checkbutton.get_active() \ - and self.app_obj.disk_space_stop_flag: - self.app_obj.set_disk_space_stop_flag(False) - spinbutton.set_sensitive(False) - - - def on_disk_stop_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_filesystem_device_tab(). - - Sets the amount of free disk space below which download operations - will be halted. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_disk_space_stop_limit(spinbutton.get_value()) - - - def on_disk_warn_button_toggled(self, checkbutton, spinbutton): - - """Called from a callback in self.setup_filesystem_device_tab(). - - Enables/disables warnings when the system is running out of disk space. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - spinbutton (Gtk.CheckButton): Another widget to be (de)sensitised - - """ - - if checkbutton.get_active() \ - and not self.app_obj.disk_space_warn_flag: - self.app_obj.set_disk_space_warn_flag(True) - spinbutton.set_sensitive(True) - elif not checkbutton.get_active() \ - and self.app_obj.disk_space_warn_flag: - self.app_obj.set_disk_space_warn_flag(False) - spinbutton.set_sensitive(False) - - - def on_disk_warn_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_filesystem_device_tab(). - - Sets the amount of free disk space below which a warning will be - issued. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_disk_space_warn_limit(spinbutton.get_value()) - - - def on_dl_mode_combo_changed(self, combo, spinbutton): - - """Called from a callback in self.setup_scheduling_start_tab(). - - Extracts the value visible in the combobox, converts it into another - value, and uses that value to update the main application's IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - spinbutton (Gtk.SpinButton): Another widget to be (de)sensitised - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_scheduled_dl_mode(model[tree_iter][0]) - if self.app_obj.scheduled_dl_mode != 'scheduled': - spinbutton.set_sensitive(False) - else: - spinbutton.set_sensitive(True) - - - def on_dl_limit_changed(self, entry): - - """Called from callback in self.setup_operations_time_saving_tab(). - - Sets the limit at which a download operation will stop downloading a - channel or playlist. - - Args: - - entry (Gtk.Entry): The widget changed - - """ - - text = entry.get_text() - if text.isdigit() and int(text) >= 0: - self.app_obj.set_operation_download_limit(int(text)) - - - def on_dl_wait_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_scheduling_start_tab(). - - Sets the interval between scheduled 'Download all' operations. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_scheduled_dl_wait_hours(spinbutton.get_value()) - - - def on_expand_tree_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables auto-expansion of the Video Index after a folder is - selected (clicked). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.auto_expand_video_index_flag: - self.app_obj.set_auto_expand_video_index_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.auto_expand_video_index_flag: - self.app_obj.set_auto_expand_video_index_flag(False) - - - def on_gtk_emulate_button_toggled(self, checkbutton): - - """Called from callback in self.setup_general_modules_tab(). - - Enables/disables emulation of a broken Gtk library. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.gtk_emulate_broken_flag: - self.app_obj.set_gtk_emulate_broken_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.gtk_emulate_broken_flag: - self.app_obj.set_gtk_emulate_broken_flag(False) - - - def on_hide_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables hiding finishe media data objects in the Progress - List. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.hide_finished_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.hide_finished_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.hide_finished_checkbutton.set_active(False) - - - def on_http_404_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables ignoring of HTTP 404 error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_http_404_error_flag: - self.app_obj.set_ignore_http_404_error_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_http_404_error_flag: - self.app_obj.set_ignore_http_404_error_flag(False) - - - def on_json_button_toggled(self, checkbutton): - - """Called from callback in self.setup_ytdl_tab(). - - Enables/disables apply a 60-second timeout when fetching a video's JSON - data. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.apply_json_timeout_flag: - self.app_obj.set_apply_json_timeout_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.apply_json_timeout_flag: - self.app_obj.set_apply_json_timeout_flag(False) - - - def on_keep_open_button_toggled(self, checkbutton, checkbutton2): - - """Called from a callback in self.setup_windows_dialogue_windows_tab(). - - Enables/disables keeping the dialogue window open when adding channels/ - playlists/folders. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another checkbutton to sensitise/ - desensitise, according to the new value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.dialogue_keep_open_flag: - self.app_obj.set_dialogue_keep_open_flag(True) - checkbutton2.set_sensitive(False) - - elif not checkbutton.get_active() \ - and self.app_obj.dialogue_keep_open_flag: - self.app_obj.set_dialogue_keep_open_flag(False) - checkbutton2.set_sensitive(True) - - - def on_limit_button_toggled(self, checkbutton, entry, entry2): - - """Called from callback in self.setup_operations_time_saving_tab(). - - Sets the limit at which a download operation will stop downloading a - channel or playlist. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - entry, entry2 (Gtk.Entry): The entry boxes which must be - sensitised/desensitised, according to the new setting of the IV - - """ - - if checkbutton.get_active() and not self.app_obj.operation_limit_flag: - self.app_obj.set_operation_limit_flag(True) - entry.set_sensitive(True) - entry2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.operation_limit_flag: - self.app_obj.set_operation_limit_flag(False) - entry.set_sensitive(False) - entry2.set_sensitive(False) - - - def on_match_button_toggled(self, radiobutton): - - """Called from callback in self.setup_general_video_matching_tab(). - - Updates IVs in the main application and sensities/desensities widgets. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - """ - - default_val = self.app_obj.match_default_chars - - if radiobutton.get_active(): - - if radiobutton == self.radiobutton: - self.app_obj.set_match_method('exact_match') - # (Changing the contents of the widgets automatically updates - # mainapp.TartubeApp IVs) - self.spinbutton.set_value(default_val) - self.spinbutton.set_sensitive(False) - self.spinbutton2.set_value(default_val) - self.spinbutton2.set_sensitive(False) - - elif radiobutton == self.radiobutton2: - self.app_obj.set_match_method('match_first') - self.spinbutton.set_sensitive(True) - self.spinbutton2.set_value(default_val) - self.spinbutton2.set_sensitive(False) - - else: - self.app_obj.set_match_method('ignore_last') - self.spinbutton.set_value(default_val) - self.spinbutton.set_sensitive(False) - self.spinbutton2.set_sensitive(True) - - - def on_match_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_general_video_matching_tab(). - - Updates IVs in the main application and sensities/desensities widgets. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - if spinbutton == self.spinbutton: - self.app_obj.set_match_first_chars(spinbutton.get_value()) - else: - self.app_obj.set_match_ignore_chars(spinbutton.get_value()) - - - def on_merge_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables ignoring of 'Requested formats are incompatible for - merge and will be merged into mkv' warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_merge_warning_flag: - self.app_obj.set_ignore_merge_warning_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_merge_warning_flag: - self.app_obj.set_ignore_merge_warning_flag(False) - - - def on_missing_format_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables ignoring of missing format error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_missing_format_error_flag: - self.app_obj.set_ignore_missing_format_error_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_missing_format_error_flag: - self.app_obj.set_ignore_missing_format_error_flag(False) - - - def on_moviepy_button_toggled(self, checkbutton): - - """Called from callback in self.setup_general_modules_tab(). - - Enables/disables use of the moviepy.editor module. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.use_module_moviepy_flag: - self.app_obj.set_use_module_moviepy_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.use_module_moviepy_flag: - self.app_obj.set_use_module_moviepy_flag(False) - - - def on_moviepy_timeout_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_general_modules_tab(). - - Sets the timeout to apply to threads using the moviepy module. - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.set_refresh_moviepy_timeout( - spinbutton.get_value(), - ) - - - def on_no_annotations_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables ignoring of the 'no annotations' warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_no_annotations_flag: - self.app_obj.set_ignore_no_annotations_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_no_annotations_flag: - self.app_obj.set_ignore_no_annotations_flag(False) - - - def on_no_subtitles_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables ignoring of the 'no subtitles' warning messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_no_subtitles_flag: - self.app_obj.set_ignore_no_subtitles_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_no_subtitles_flag: - self.app_obj.set_ignore_no_subtitles_flag(False) - - - def on_open_desktop_button_toggled(self, checkbutton): - - """Called from callback in self.setup_filesystem_temp_folders_tab(). - - Enables/disables opening temporary folders on the desktop when Tartube - shuts down. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.open_temp_on_desktop_flag: - self.app_obj.set_open_temp_on_desktop_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.open_temp_on_desktop_flag: - self.app_obj.set_open_temp_on_desktop_flag(False) - - - def on_operation_error_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables opeartion errors in the 'Errors/Warnings' tab. - Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab - sets the IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.show_operation_error_checkbutton.get_active() - - main_win_obj = self.app_obj.main_win_obj - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_operation_error_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_operation_error_checkbutton.set_active(False) - - - def on_operation_sim_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables ignoring already-checked videos whose parent is a - media.Folder, if the videos have already been checked. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.operation_sim_shortcut_flag: - self.app_obj.set_operation_sim_shortcut_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.operation_sim_shortcut_flag: - self.app_obj.set_operation_sim_shortcut_flag(False) - - - def on_operation_warning_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables opeartion warnings in the 'Errors/Warnings' tab. - Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab - sets the IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag \ - = main_win_obj.show_operation_warning_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_operation_warning_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_operation_warning_checkbutton.set_active(False) - - - def on_output_empty_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables emptying pages in the Output Tab at the start of every - operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_start_empty_flag: - self.app_obj.set_ytdl_output_start_empty_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_start_empty_flag: - self.app_obj.set_ytdl_output_start_empty_flag(False) - - - def on_output_json_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the Output - Tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_ignore_json_flag: - self.app_obj.set_ytdl_output_ignore_json_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_ignore_json_flag: - self.app_obj.set_ytdl_output_ignore_json_flag(False) - - - def on_output_progress_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the Output - Tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_ignore_progress_flag: - self.app_obj.set_ytdl_output_ignore_progress_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_ignore_progress_flag: - self.app_obj.set_ytdl_output_ignore_progress_flag(False) - - - def on_output_stderr_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing output from youtube-dl's STDERR to the Output - Tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_stderr_flag: - self.app_obj.set_ytdl_output_stderr_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_stderr_flag: - self.app_obj.set_ytdl_output_stderr_flag(False) - - - def on_output_stdout_button_toggled(self, checkbutton, checkbutton2, \ - checkbutton3): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the Output - Tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2, checkbutton3 (Gtk.CheckButton): Additional - checkbuttons to sensitise/desensitise, according to the new - value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_stdout_flag: - self.app_obj.set_ytdl_output_stdout_flag(True) - checkbutton2.set_sensitive(True) - checkbutton3.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_stdout_flag: - self.app_obj.set_ytdl_output_stdout_flag(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_sensitive(False) - - - def on_output_summary_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables displaying a summary page in the Output Tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_show_summary_flag: - self.app_obj.set_ytdl_output_show_summary_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_show_summary_flag: - self.app_obj.set_ytdl_output_show_summary_flag(False) - - - def on_output_system_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables writing youtube-dl system commands to the Output Tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_output_system_cmd_flag: - self.app_obj.set_ytdl_output_system_cmd_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_output_system_cmd_flag: - self.app_obj.set_ytdl_output_system_cmd_flag(False) - - - def on_pretty_date_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables 'today' and 'yesterday' rather than a numerical date - in the Videos Tab. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_pretty_dates_flag: - self.app_obj.set_show_pretty_dates_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_pretty_dates_flag: - self.app_obj.set_show_pretty_dates_flag(False) - - - def on_refresh_verbose_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables displaying non-matching videos in the Output Tab - during a refresh operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.refresh_output_verbose_flag: - self.app_obj.set_refresh_output_verbose_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.refresh_output_verbose_flag: - self.app_obj.set_refresh_output_verbose_flag(False) - - - def on_refresh_videos_button_toggled(self, checkbutton, checkbutton2): - - """Called from a callback in self.setup_output_outputtab_tab(). - - Enables/disables displaying matching videos in the Output Tab during a - refresh operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): A different checkbutton to - sensitise/desensitise, according to the new value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.refresh_output_videos_flag: - self.app_obj.set_refresh_output_videos_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.refresh_output_videos_flag: - self.app_obj.set_refresh_output_videos_flag(False) - checkbutton2.set_sensitive(False) - - - def on_regex_button_toggled(self, radiobutton, flag): - - """Called from callback in self.setup_windows_websites_tab(). - - Sets whether mainapp.TartubeApp.ignore_custom_msg_list contains - ordinary strings or regexes. - - Args: - - radiobutton (Gtk.RadioButton): The widget clicked - - flag (bool): False for ordinary strings, True for regexes - - """ - - if radiobutton.get_active(): - self.app_obj.set_ignore_custom_regex_flag(flag) - - - def on_remember_size_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables remembering the size of the main window. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.main_win_save_size_flag: - self.app_obj.set_main_win_save_size_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.main_win_save_size_flag: - self.app_obj.set_main_win_save_size_flag(False) - - - def on_reset_ffmpeg_button_clicked(self, button, entry): - - """Called from callback in self.setup_ytdl_tab(). - - Resets the path to the ffmpeg binary. - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - self.app_obj.set_ffmpeg_path(None) - entry.set_text('') - - - def on_reverse_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables display of videos in the Results List in the reverse - order. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.reverse_results_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.reverse_results_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.reverse_results_checkbutton.set_active(False) - - - def on_save_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_downloads_tab(). - - Enables/disables automatic saving of files at the end of a download/ - update/refresh/info/tidy operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() and not self.app_obj.operation_save_flag: - self.app_obj.set_operation_save_flag(True) - elif not checkbutton.get_active() and self.app_obj.operation_save_flag: - self.app_obj.set_operation_save_flag(False) - - - def on_scheduled_stop_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_scheduling_start_tab(). - - Enables/disables shutting down Tartube after a scheduled 'Download all' - or 'Check all' operation. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.scheduled_shutdown_flag: - self.app_obj.set_scheduled_shutdown_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.scheduled_shutdown_flag: - self.app_obj.set_scheduled_shutdown_flag(False) - - - def on_set_ffmpeg_button_clicked(self, button, entry): - - """Called from callback in self.setup_ytdl_tab(). - - Opens a window in which the user can select the ffmpeg binary, if it is - installed (and if the user wants it). - - Args: - - button (Gtk.Button): The widget clicked - - entry (Gtk.Entry): Another widget to be modified by this function - - """ - - dialogue_win = Gtk.FileChooserDialog( - 'Please select the ffmpeg executable', - self, - Gtk.FileChooserAction.OPEN, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.OK, - ), - ) - - response = dialogue_win.run() - if response == Gtk.ResponseType.OK: - new_path = dialogue_win.get_filename() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and new_path: - - self.app_obj.set_ffmpeg_path(new_path) - entry.set_text(self.app_obj.ffmpeg_path) - - - def on_show_small_icons_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables smaller icons in the Video Index. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_small_icons_in_index: - self.app_obj.set_show_small_icons_in_index(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_small_icons_in_index: - self.app_obj.set_show_small_icons_in_index(False) - - - def on_show_status_icon_toggled(self, checkbutton, checkbutton2): - - """Called from a callback in self.setup_windows_system_tray_tab(). - - Shows/hides the status icon in the system tray. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2 (Gtk.CheckButton): Another checkbutton to sensitise/ - desensitise, according to the new value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_status_icon_flag: - self.app_obj.set_show_status_icon_flag(True) - checkbutton2.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.show_status_icon_flag: - self.app_obj.set_show_status_icon_flag(False) - checkbutton2.set_sensitive(False) - - - def on_show_tooltips_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables tooltips for videos/channels/playlists/folders. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.show_tooltips_flag: - self.app_obj.set_show_tooltips_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.show_tooltips_flag: - self.app_obj.set_show_tooltips_flag(False) - - - def on_squeeze_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables labels in the main window's main toolbar. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.toolbar_squeeze_flag: - self.app_obj.set_toolbar_squeeze_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.toolbar_squeeze_flag: - self.app_obj.set_toolbar_squeeze_flag(False) - - - def on_system_error_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables system errors in the 'Errors/Warnings' tab. Toggling - the corresponding Gtk.CheckButton in the Errors/Warnings tab sets the - IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.show_system_error_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_system_error_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_system_error_checkbutton.set_active(False) - - - def on_system_keep_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_main_window_tab(). - - Enables/disables keeping the total number of system messages in the tab - label until the clear button is explicitly clicked. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.system_msg_keep_totals_flag: - self.app_obj.set_system_msg_keep_totals_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.system_msg_keep_totals_flag: - self.app_obj.set_system_msg_keep_totals_flag(False) - - - def on_system_warning_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_errors_warnings_tab(). - - Enables/disables system warnings in the 'Errors/Warnings' tab. Toggling - the corresponding Gtk.CheckButton in the Errors/Warnings tab sets the - IV (and makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - main_win_obj = self.app_obj.main_win_obj - other_flag = main_win_obj.show_system_warning_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - main_win_obj.show_system_warning_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - main_win_obj.show_system_warning_checkbutton.set_active(False) - - - def on_terminal_json_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_terminal_window_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_ignore_json_flag: - self.app_obj.set_ytdl_write_ignore_json_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_ignore_json_flag: - self.app_obj.set_ytdl_write_ignore_json_flag(False) - - - def on_terminal_progress_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_terminal_window_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_ignore_progress_flag: - self.app_obj.set_ytdl_write_ignore_progress_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_ignore_progress_flag: - self.app_obj.set_ytdl_write_ignore_progress_flag(False) - - - def on_terminal_stderr_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_terminal_window_tab(). - - Enables/disables writing output from youtube-dl's STDERR to the - terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_stderr_flag: - self.app_obj.set_ytdl_write_stderr_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_stderr_flag: - self.app_obj.set_ytdl_write_stderr_flag(False) - - - def on_terminal_stdout_button_toggled(self, checkbutton, checkbutton2, \ - checkbutton3): - - """Called from a callback in self.setup_output_terminal_window_tab(). - - Enables/disables writing output from youtube-dl's STDOUT to the - terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - checkbutton2, checkbutton3 (Gtk.CheckButton): Additional - checkbuttons to sensitise/desensitise, according to the new - value of the flag - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_stdout_flag: - self.app_obj.set_ytdl_write_stdout_flag(True) - checkbutton2.set_sensitive(True) - checkbutton3.set_sensitive(True) - - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_stdout_flag: - self.app_obj.set_ytdl_write_stdout_flag(False) - checkbutton2.set_sensitive(False) - checkbutton3.set_sensitive(False) - - - def on_terminal_system_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_terminal_window_tab(). - - Enables/disables writing youtube-dl system commands to the terminal. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_system_cmd_flag: - self.app_obj.set_ytdl_write_system_cmd_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_system_cmd_flag: - self.app_obj.set_ytdl_write_system_cmd_flag(False) - - - def on_update_combo_changed(self, combo): - - """Called from a callback in self.setup_ytdl_tab(). - - Extracts the value visible in the combobox, converts it into another - value, and uses that value to update the main application's IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_ytdl_update_current(model[tree_iter][0]) - - - def on_uploader_button_toggled(self, checkbutton): - - """Called from callback in self.setup_windows_websites_tab(). - - Enables/disables ignoring of deletion by uploader error messages. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ignore_yt_uploader_deleted_flag: - self.app_obj.set_ignore_yt_uploader_deleted_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ignore_yt_uploader_deleted_flag: - self.app_obj.set_ignore_yt_uploader_deleted_flag(False) - - - def on_use_first_button_toggled(self, checkbutton): - - """Called from callback in self.setup_filesystem_database_tab(). - - Enables/disables automatic loading of the first database file in the - list. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.data_dir_use_first_flag: - self.app_obj.set_data_dir_use_first_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.data_dir_use_first_flag: - self.app_obj.set_data_dir_use_first_flag(False) - - - def on_use_list_button_toggled(self, checkbutton): - - """Called from callback in self.setup_filesystem_database_tab(). - - Enables/disables automatic loading of an alternative database file, if - the default one is locked. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.data_dir_use_list_flag: - self.app_obj.set_data_dir_use_list_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.data_dir_use_list_flag: - self.app_obj.set_data_dir_use_list_flag(False) - - - def on_verbose_button_toggled(self, checkbutton): - - """Called from a callback in self.setup_output_both_tab(). - - Enables/disables writing verbose output (youtube-dl debugging mode). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - if checkbutton.get_active() \ - and not self.app_obj.ytdl_write_verbose_flag: - self.app_obj.set_ytdl_write_verbose_flag(True) - elif not checkbutton.get_active() \ - and self.app_obj.ytdl_write_verbose_flag: - self.app_obj.set_ytdl_write_verbose_flag(False) - - - def on_worker_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_performance_tab(). - - Enables/disables the simultaneous download limit. Toggling the - corresponding Gtk.CheckButton in the Progress Tab sets the IV (and - makes sure the two checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - other_flag \ - = self.app_obj.main_win_obj.num_worker_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - self.app_obj.main_win_obj.num_worker_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - self.app_obj.main_win_obj.num_worker_checkbutton.set_active(False) - - - def on_worker_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_operations_performance_tab(). - - Sets the simultaneous download limit. Setting the value of the - corresponding Gtk.SpinButton in the Progress Tab sets the IV (and - makes sure the two spinbuttons have the same value). - - Args: - - spinbutton (Gtk.SpinButton): The widget clicked - - """ - - self.app_obj.main_win_obj.num_worker_spinbutton.set_value( - spinbutton.get_value(), - ) - - - def on_video_res_button_toggled(self, checkbutton): - - """Called from callback in self.setup_operations_performance_tab(). - - Enables/disables the video resolution limit. Toggling the corresponding - Gtk.CheckButton in the Progress Tab sets the IV (and makes sure the two - checkbuttons have the same status). - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - """ - - other_flag \ - = self.app_obj.main_win_obj.video_res_checkbutton.get_active() - - if (checkbutton.get_active() and not other_flag): - self.app_obj.main_win_obj.video_res_checkbutton.set_active(True) - elif (not checkbutton.get_active() and other_flag): - self.app_obj.main_win_obj.video_res_checkbutton.set_active(False) - - - def on_video_res_combo_changed(self, combo): - - """Called from a callback in self.setup_operations_performance_tab(). - - Extracts the value visible in the combobox, converts it into another - value, and uses that value to update the main application's IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.main_win_obj.set_video_res(model[tree_iter][0]) - - - def on_ytdl_path_combo_changed(self, combo): - - """Called from a callback in self.setup_ytdl_tab(). - - Extracts the value visible in the combobox, converts it into another - value, and uses that value to update the main application's IV. - - Args: - - combo (Gtk.ComboBox): The widget clicked - - """ - - tree_iter = combo.get_active_iter() - model = combo.get_model() - self.app_obj.set_ytdl_path(model[tree_iter][1]) - - - # (Callback support functions) - - - def try_switch_db(self, data_dir, button): - - """Called by self.on_data_dir_change_button_clicked() and - .on_data_dir_switch_button_clicked(). - - Having confirmed that a database directory specified by the user - actually exists, attempt to load the database file inside it. - - Args: - - data_dir (str): The full path to the data directory - - button (Gtk.Button): A button to be possibly desensitised - - """ - - dialogue_manager_obj = self.app_obj.dialogue_manager_obj - - # Database file already exists, so try to load it now - if not self.app_obj.switch_db([data_dir, self]): - - # Load failed - if self.app_obj.disable_load_save_flag: - button.set_sensitive(False) - - if self.app_obj.disable_load_save_msg is not None: - - dialogue_win = dialogue_manager_obj.show_msg_dialogue( - self.app_obj.disable_load_save_msg, - 'error', - 'ok', - self, # Parent window is this window - ) - - else: - - dialogue_win = dialogue_manager_obj.show_msg_dialogue( - 'Database file not loaded', - 'error', - 'ok', - self, # Parent window is this window - ) - - # When load/save is disabled, this preference window can't be - # opened - # Therefore, if load/save has just been disabled, close this - # window after the dialogue window closes - dialogue_win.set_modal(True) - dialogue_win.run() - dialogue_win.destroy() - if self.app_obj.disable_load_save_flag: - self.destroy() - - else: - - # Load succeeded. Redraw the preference window, opening it at the - # same tab - self.reset_window() - self.select_switch_db_tab() - - if self.app_obj.disable_load_save_msg is not None: - - dialogue_manager_obj.show_msg_dialogue( - self.app_obj.disable_load_save_msg, - 'info', - 'ok', - self, # Parent window is this window - ) - - else: - - dialogue_manager_obj.show_msg_dialogue( - 'Database file loaded', - 'info', - 'ok', - self, # Parent window is this window - ) diff --git a/tartube/dialogue.py b/tartube/dialogue.py deleted file mode 100755 index a54780b..0000000 --- a/tartube/dialogue.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Dialogue manager classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, GdkPixbuf - - -# Import other modules -import os -import threading - - -# Import our modules -import utils - - -# Classes - - -class DialogueManager(threading.Thread): - - """Called by mainapp.TartubeApp.start(). - - Python class to manage message dialogue windows safely (i.e. without - causing a Gtk crash). - - Args: - - app_obj: The mainapp.TartubeApp object - - main_win_obj (mainwin.MainWin): The main window - - """ - - - # Standard class methods - - - def __init__(self, app_obj, main_win_obj): - - super(DialogueManager, self).__init__() - - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The main window - self.main_win_obj = main_win_obj - - - # Public class methods - - - def show_msg_dialogue(self, msg, msg_type, button_type, - parent_win_obj=None, response_dict=None): - - """Can be called by anything. - - Creates a standard Gtk.MessageDialog window. - - Args: - - msg (str): The text to display in the dialogue window - - msg_type (str): The icon to display in the dialogue window: 'info', - 'warning', 'question', 'error' - - button_type (str): The buttons to use in the dialogue window: 'ok', - 'ok-cancel', 'yes-no' - - parent_win_obj (mainwin.MainWin, config.GenericConfigWin or None): - The parent window for the dialogue window. If None, the main - window is used as the parent window - - response_dict (dict or None): A dictionary specified if the calling - code needs a response (e.g., needs to know whether the user - 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 - - Returns: - - Gtk.MessageDialog window - - """ - - if parent_win_obj is None: - parent_win_obj = self.main_win_obj - - # Rationalise the message. First, split the string into a list of - # lines, preserving \n\n (but not a standalone \n) - line_list = msg.split('\n\n') - # In each line, convert any standalone \n characters to whitespace. - # Then add new newline characters, if required, to give a maximum - # length per line - mod_list = [] - for line in line_list: - mod_list.append(utils.tidy_up_long_string(line, 40)) - - # Finally combine everything into a single string, as before - double = '\n\n' - msg = double.join(mod_list) - - # ...and display the message dialogue - dialogue_win = MessageDialogue( - self, - msg, - msg_type, - button_type, - parent_win_obj, - response_dict, - ) - - dialogue_win.create_dialogue() - - return dialogue_win - - -class MessageDialogue(Gtk.MessageDialog): - - """Called by dialogue.DialogueManager.show_msg_dialogue(). - - Creates a standard Gtk.MessageDialog window, and optionally returns a - response. - - Args: - - manager_obj (dialogue.DialogueManager): The parent dialogue manager - - msg (str): The text to display in the dialogue window - - msg_type (str): The icon to display in the dialogue window: 'info', - 'warning', 'question', 'error' - - button_type (str): The buttons to use in the dialogue window: 'ok', - 'ok-cancel', 'yes-no' - - parent_win_obj (mainwin.MainWin, config.GenericConfigWin): The parent - window for the dialogue window - - response_dict (dict or None): A dictionary specified if the calling - code needs a response (e.g., needs to know whether the user 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 - - """ - - - # Standard class methods - - - def __init__(self, manager_obj, msg, msg_type, button_type, parent_win_obj, - response_dict): - - # Prepare arguments - if msg_type == 'warning': - gtk_msg_type = Gtk.MessageType.WARNING - elif msg_type == 'question': - gtk_msg_type = Gtk.MessageType.QUESTION - elif msg_type == 'error': - gtk_msg_type = Gtk.MessageType.ERROR - else: - gtk_msg_type = Gtk.MessageType.INFO - - if button_type == 'ok-cancel': - gtk_button_type = Gtk.ButtonsType.OK_CANCEL - default_response = Gtk.ResponseType.OK - elif button_type == 'yes-no': - gtk_button_type = Gtk.ButtonsType.YES_NO - default_response = Gtk.ResponseType.YES - else: - gtk_button_type = Gtk.ButtonsType.OK - default_response = Gtk.ResponseType.OK - - # Set up the dialogue window - Gtk.MessageDialog.__init__( - self, - parent_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - gtk_msg_type, - gtk_button_type, - msg, - ) - - spacing_size = manager_obj.app_obj.default_spacing_size - - # Set up responses - self.set_default_response(default_response) - self.connect( - 'response', - self.on_clicked, - manager_obj.app_obj, - response_dict, - ) - - - # Public class methods - - - def create_dialogue(self): - - """Called by dialogue.DialogueManager.show_msg_dialogue(). - - Creating the message dialogue window using a Glib timeout keeps this - code thread-safe. - """ - - GObject.timeout_add(0, self.show_dialogue) - - - def show_dialogue(self): - - """Called by the timer created in self.create_dialogue(). - - Creating the message dialogue window using a Glib timeout keeps this - code thread-safe. - """ - - self.show_all() - return False - - - # (Callbacks) - - - def on_clicked(self, widget, response, app_obj, response_dict): - - """Called from a callback in self.__init__(). - - Destroy the dialogue window. If the calling code requires a response, - call the specified function in mainapp.TartubeApp. - - Args: - - widget (Gtk.MessageDialog): This dialogue window - - response (int): The response, matching a Gtk.ResponseType - - app_obj: The mainapp.TartubeApp object - - response_dict (dict or None): A dictionary specified if the calling - code needs a response (e.g., needs to know whether the user - 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 - - """ - - # Destroy the window - self.destroy() - - # If the calling code requires a response, provide it - if response_dict is not None: - - func = None - if response == Gtk.ResponseType.OK and 'ok' in response_dict: - func = response_dict['ok'] - elif response == Gtk.ResponseType.CANCEL \ - and 'cancel' in response_dict: - func = response_dict['cancel'] - elif response == Gtk.ResponseType.YES and 'yes' in response_dict: - func = response_dict['yes'] - elif response == Gtk.ResponseType.NO and 'no' in response_dict: - func = response_dict['no'] - - if func is not None: - # 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 - if 'data' in response_dict: - method(response_dict['data']) - else: - method() diff --git a/tartube/downloads.py b/tartube/downloads.py deleted file mode 100755 index f181d01..0000000 --- a/tartube/downloads.py +++ /dev/null @@ -1,3624 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Download operation classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import GObject - - -# Import other modules -import datetime -import json -import __main__ -import signal -import os -import queue -import random -import re -import requests -import signal -import subprocess -import sys -import threading -import time - - -# Import our modules -import formats -import mainapp -import media -import options -import utils - - -# Debugging flag (calls utils.debug_time at the start of every function) -DEBUG_FUNC_FLAG = False - - -# Decorator to add thread synchronisation to some functions in the -# downloads.DownloadList object -_SYNC_LOCK = threading.RLock() - -def synchronise(lock): - def _decorator(func): - def _wrapper(*args, **kwargs): - lock.acquire() - ret_value = func(*args, **kwargs) - lock.release() - return ret_value - return _wrapper - return _decorator - - -# Classes -class DownloadManager(threading.Thread): - - """Called by mainapp.TartubeApp.download_manager_continue(). - - Based on the DownloadManager class in youtube-dl-gui. - - Python class to manage a download operation. - - Creates one or more downloads.DownloadWorker objects, each of which handles - a single download. - - This object runs on a loop, looking for available workers and, when one is - found, assigning them something to download. The worker completes that - download and then waits for another assignment. - - Args: - - app_obj: The mainapp.TartubeApp object - - operation_type (str): 'sim' if channels/playlists should just be - checked for new videos, 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) - - download_list_obj(downloads.DownloadManager): An ordered list of - media data objects to download, each one represented by a - downloads.DownloadItem object - - """ - - - # Standard class methods - - - def __init__(self, app_obj, operation_type, download_list_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 113 __init__') - - super(DownloadManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # Each instance of this object, which represents a single download - # operation, creates its own options.OptionsParser object. That - # object convert the download options stored in - # downloads.DownloadWorker.options_list into a list of youtube-dl - # command line options - self.options_parser_obj = None - # An ordered list of media data objects to download, each one - # represented by a downloads.DownloadItem object - self.download_list_obj = download_list_obj - # List of downloads.DownloadWorker objects, each one handling one of - # several simultaneous downloads - self.worker_list = [] - - - # IV list - other - # --------------- - # 'sim' if channels/playlists should just be checked for new videos, - # 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) - self.operation_type = operation_type - - # The time at which the download operation began (in seconds since - # epoch) - self.start_time = int(time.time()) - # The time at which the download operation completed (in seconds since - # epoch) - self.stop_time = None - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # 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 - - # On-going counts of how many videos have been downloaded (real or - # simulated), and how much disk space has been consumed (in bytes), - # so that the operation can be auto-stopped, if required - self.total_video_count = 0 - self.total_size_count = 0 - - # If mainapp.TartubeApp.operation_convert_mode is set to any value - # other than 'disable', then a media.Video object whose URL - # represents a channel/playlist is converted into multiple new - # media.Video objects, one for each video actually downloaded - # The original media.Video object is added to this list, via a call to - # self.mark_video_as_doomed(). At the end of the whole download - # operation, any media.Video object in this list is destroyed - self.doomed_video_list = [] - - - # Code - # ---- - - # Create an object for converting download options stored in - # downloads.DownloadWorker.options_list into a list of youtube-dl - # command line options - self.options_parser_obj = options.OptionsParser(self.app_obj) - - # Create a list of downloads.DownloadWorker objects, each one handling - # one of several simultaneous downloads - for i in range(1, self.app_obj.num_worker_default + 1): - self.worker_list.append(DownloadWorker(self)) - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - On a continuous loop, passes downloads.DownloadItem objects to each - downloads.DownloadWorker object, as they become available, until the - download operation is complete. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 206 run') - - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - 'Manager: Starting download operation', - ) - - # (Monitor changes to the number of workers, and number of available - # workers, so that we can display a running total in the Output Tab's - # summary page) - local_worker_available_count = 0 - local_worker_total_count = 0 - - # Perform the download operation until there is nothing left to - # download, or until something has called - # self.stop_download_operation() - while self.running_flag: - - # Send a message to the Output Tab's summary page, if required - available_count = 0 - total_count = 0 - for worker_obj in self.worker_list: - total_count += 1 - if worker_obj.available_flag: - available_count += 1 - - if local_worker_available_count != available_count \ - or local_worker_total_count != total_count: - local_worker_available_count = available_count - local_worker_total_count = total_count - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - 'Manager: Workers: available: ' \ - + str(available_count) + ', total: ' \ - + str(total_count), - ) - - # Auto-stop the download operation, if required - if self.app_obj.autostop_time_flag: - - # Calculate the current time limit, in seconds - time_limit = self.app_obj.autostop_time_value \ - * formats.TIME_METRIC_DICT[self.app_obj.autostop_time_unit] - - if (time.time() - self.start_time) > time_limit: - break - - # Fetch information about the next media data object to be - # downloaded - download_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 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', - ) - - 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) - - # Otherwise, initialise the worker's IVs for the next job - else: - - # 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 + '\'', - ) - - # 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), - ) - - # Pause a moment, before the next iteration of the loop (don't want - # to hog resources) - time.sleep(self.sleep_time) - - # Download operation complete (or has been stopped). Send messages to - # the Output Tab's summary page - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - 'Manager: Downloads complete (or stopped)', - ) - - # Close all the workers - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - 'Manager: Halting all workers', - ) - - for worker_obj in self.worker_list: - worker_obj.close() - - # Join and collect - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - 'Manager: Join and collect threads', - ) - - for worker_obj in self.worker_list: - worker_obj.join() - - self.app_obj.main_win_obj.output_tab_write_stdout( - 0, - 'Manager: 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 Output Tab to display any remaining messages immediately - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.output_tab_update_pages, - ) - - # Any media.Video objects which have been marked as doomed, can now be - # destroyed - for video_obj in self.doomed_video_list: - self.app_obj.delete_video( - video_obj, - True, # Delete any files associated with the video - True, # Don't update the Video Index yet - True, # Don't update the Video Catalogue yet - ) - - # When youtube-dl reports it is finished, there is a short delay before - # the final downloaded video(s) actually exist in the filesystem - # Therefore, mainwin.MainWin.progress_list_display_dl_stats() may not - # have marked the final video(s) as downloaded yet - # 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': - 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 - GObject.timeout_add( - 0, - self.app_obj.download_manager_finished, - ) - - - def change_worker_count(self, number): - - """Called by mainapp.TartubeApp.set_num_worker_default(). - - When the number of simultaneous downloads allowed is changed during a - download operation, this function responds. - - If the number has increased, creates an extra download worker object. - - If the number has decreased, marks the worker as doomed. When its - current download is completed, the download manager destroys it. - - Args: - - number (int): The new value of - mainapp.TartubeApp.num_worker_default - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 417 change_worker_count') - - # How many workers do we have already? - current = len(self.worker_list) - # If this object hasn't set up its worker pool yet, let the setup code - # proceed as normal - # Sanity check: if the specified value is less than 1, or hasn't - # changed, take no action - if not current or number < 1 or current == number: - return - - # Usually, the number of workers goes up or down by one at a time, but - # we'll check for larger leaps anyway - for i in range(1, (abs(current-number) + 1)): - - if number > current: - - # The number has increased. If any workers have marked as - # doomed, they can be unmarked, allowing them to continue - match_flag = False - - for worker_obj in self.worker_list: - if worker_obj.doomed_flag: - worker_obj.set_doomed_flag(True) - match_flag = True - break - - if not match_flag: - # No workers were marked doomed, so create a brand new - # download worker - self.worker_list.append(DownloadWorker(self)) - - else: - - # The number has decreased. The first worker in the list is - # marked as doomed - that is, when it has finished its - # current job, it closes (rather than being given another - # job, as usual) - for worker_obj in self.worker_list: - if not worker_obj.doomed_flag: - worker_obj.set_doomed_flag(True) - break - - - def check_master_slave(self, media_data_obj): - - """Called by VideoDownloader.do_download(). - - When two channels/playlists/folders share a download destination, we - don't want to download both of them at the same time. - - This function is called when media_data_obj is about to be - downloaded. - - Every worker is checked, to see if it's downloading to the same - destination. If so, this function returns True, and - VideoDownloader.do_download() waits a few seconds, before trying - again. - - Otherwise, this function returns False, and - VideoDownloader.do_download() is free to start its download. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): - The media data object that the calling function wants to - download - - Returns: - - True or False, as described above - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 492 check_master_slave') - - for worker_obj in self.worker_list: - - if not worker_obj.available_flag \ - and worker_obj.download_item_obj: - - other_obj = worker_obj.download_item_obj.media_data_obj - - if other_obj.dbid != media_data_obj.dbid \ - and other_obj.dbid == media_data_obj.master_dbid: - return True - - return False - - - def check_workers_all_finished(self): - - """Called by self.run(). - - Based on DownloadManager._jobs_done(). - - Returns: - - True if all downloads.DownloadWorker objects have finished their - jobs, otherwise returns False - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 522 check_workers_all_finished') - - for worker_obj in self.worker_list: - if not worker_obj.available_flag: - return False - - return True - - - def get_available_worker(self): - - """Called by self.run(). - - Based on DownloadManager._get_worker(). - - Returns: - - The first available downloads.DownloadWorker, or None if there are - no available workers. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 545 get_available_worker') - - for worker_obj in self.worker_list: - if worker_obj.available_flag: - return worker_obj - - return None - - - def mark_video_as_doomed(self, video_obj): - - """Called by VideoDownloader.check_dl_is_correct_type(). - - When youtube-dl reports the URL associated with a download item - object contains multiple videos (or potentially contains multiple - videos), then the URL represents a channel or playlist, not a video. - - If the channel/playlist was about to be downloaded into a media.Video - object, then the calling function takes action to prevent it. - - It then calls this function to mark the old media.Video object to be - destroyed, once the download operation is complete. - - Args: - - video_obj (media.Video): The video object whose URL is not a video, - and which must be destroyed - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 576 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 register_video(self): - - """Called by VideoDownloader.confirm_new_video(), when a video is - downloaded, or by .confirm_sim_video(), when a simulated download finds - a new video. - - This function adds the new video to its ongoing total and, if a limit - has been reached, stops the download operation. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 595 register_video') - - self.total_video_count += 1 - - if self.app_obj.autostop_videos_flag \ - and self.total_video_count >= self.app_obj.autostop_videos_value: - self.stop_download_operation() - - - def register_video_size(self, size=None): - - """Called by mainapp.TartubeApp.update_video_when_file_found(). - - Called with the size of a video that's just been downloaded. This - function adds the size to its ongoing total and, if a limit has been - reached, stops the download operation. - - Args: - - size (int): The size of the downloaded video (in bytes) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 619 register_video_size') - - # (In case the filesystem didn't detect the file size, for whatever - # reason, we'll check for a None value) - if size is not None: - - self.total_size_count += size - - if self.app_obj.autostop_size_flag: - - # Calculate the current limit - limit = self.app_obj.autostop_size_value \ - * formats.FILESIZE_METRIC_DICT[self.app_obj.autostop_size_unit] - - if self.total_size_count >= limit: - self.stop_download_operation() - - - def remove_worker(self, worker_obj): - - """Called by self.run(). - - When a worker marked as doomed has completed its download job, this - function is called to remove it from self.worker_list. - - Args: - - worker_obj (downloads.DownloadWorker): The worker object to remove - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 651 remove_worker') - - new_list = [] - - for other_obj in self.worker_list: - if other_obj != worker_obj: - new_list.append(other_obj) - - self.worker_list = new_list - - - def stop_download_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .dl_timer_callback(), .on_button_stop_operation(). - - Also called by mainwin.StatusIcon.on_stop_menu_item(). - - Also called by self.register_video() and .register_video_size(). - - Based on DownloadManager.stop_downloads(). - - Stops the download operation. On the next iteration of self.run()'s - loop, the downloads.DownloadWorker objects are cleaned up. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 678 stop_download_operation') - - self.running_flag = False - - - def stop_download_operation_soon(self): - - """Called by mainwin.MainWin.on_progress_list_stop_all_soon(), after - the user clicks the 'Stop after these videos' option in the Progress - List. - - Stops the download operation, but only after any videos which are - currently being downloaded have finished downloading. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 694 stop_download_operation_soon') - - self.download_list_obj.prevent_fetch_new_items() - for worker_obj in self.worker_list: - if worker_obj.running_flag: - worker_obj.video_downloader_obj.stop_soon() - - -class DownloadWorker(threading.Thread): - - """Called by downloads.DownloadManager.__init__(). - - Based on the Worker class in youtube-dl-gui. - - Python class for managing simultaneous downloads. The parent - downloads.DownloadManager object can create one or more workers, each of - 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. - - Args: - - download_manager_obj (downloads.DownloadManager): The parent download - manager object. - - """ - - - # Standard class methods - - - def __init__(self, download_manager_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 730 __init__') - - super(DownloadWorker, self).__init__() - - # IV list - class objects - # ----------------------- - # The parent downloads.DownloadManager object - 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 - self.video_downloader_obj = None - # The options.OptionsManager object for the current job - self.options_manager_obj = None - - - # IV list - other - # --------------- - # A number identifying this worker, matching the number of the page - # in the Output Tab (so the first worker created is #1) - self.worker_id = len(download_manager_obj.worker_list) + 1 - - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # Flag set to False if self.close() is called - # The False value halts the main loop in self.run() - self.running_flag = True - # Flag set to True when the parent downloads.DownloadManager object - # wants to destroy this worker, having called self.set_doomed_flag() - # to do that - # The worker is not destroyed until its current download is complete - self.doomed_flag = False - - # Options list (used by downloads.VideoDownloader) - # Initialised in the call to self.prepare_download() - self.options_list = [] - # Flag set to True when the worker is available for a new job, False - # when it is already occupied with a job - self.available_flag = True - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Waits until this worker has been assigned a job, at which time we - create a new downloads.VideoDownloader object and wait for the result. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 791 run') - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Handle a job, or wait for the downloads.DownloadManager to assign - # this worker a job - while self.running_flag: - - # If this worker is currently assigned a job... - if not self.available_flag: - - # youtube-dl-gui used a single instance of a - # YoutubeDLDownloader object for each instance of a Worker - # object. - # This causes problems, so Tartube will use a new - # downloads.VideoDownloader object each time - # Set up the new downloads.VideoDownloader object - self.video_downloader_obj = VideoDownloader( - self.download_manager_obj, - self, - self.download_item_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) \ - + ': 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 - if media_data_obj.error_list or media_data_obj.warning_list: - GObject.timeout_add( - 0, - app_obj.main_win_obj.errors_list_add_row, - media_data_obj, - ) - - # In the event of an error, nothing updates the video's row in - # the Video Catalogue, and therefore the error icon won't be - # visible - # Do that now (but don't if mainwin.ComplexCatalogueItem - # objects aren't being used in the Video Catalogue) - if 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': - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_row, - media_data_obj, - ) - - # Call the destructor function of VideoDownloader object - self.video_downloader_obj.close() - - # 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', - ) - - # During custom downloads, apply a delay if one has been - # specified - if self.download_manager_obj.operation_type == 'custom' \ - and app_obj.custom_dl_delay_flag: - - # Set the delay (in seconds), a randomised value if - # required - if app_obj.custom_dl_delay_min: - delay = random.randint( - int(app_obj.custom_dl_delay_min * 60), - int(app_obj.custom_dl_delay_max * 60), - ) - 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 - # to hog resources) - time.sleep(self.sleep_time) - - - def close(self): - - """Called by downloads.DownloadManager.run(). - - This worker object is closed when: - - 1. The download operation is complete (or has been stopped) - 2. The worker has been marked as doomed, and the calling function - is now ready to destroy it - - Tidy up IVs and stop any child processes. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 913 close') - - self.running_flag = False - if self.video_downloader_obj: - self.video_downloader_obj.stop() - - - def prepare_download(self, download_item_obj): - - """Called by downloads.DownloadManager.run(). - - Based on Worker.download(). - - Updates IVs for a new job, so that self.run can initiate the download. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object describing the URL from which youtube-dl should download - video(s). - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 937 prepare_download') - - self.download_item_obj = download_item_obj - self.options_manager_obj = download_item_obj.options_manager_obj - self.options_list = self.download_manager_obj.options_parser_obj.parse( - download_item_obj.media_data_obj, - self.options_manager_obj, - ) - - self.available_flag = False - - - 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') - - self.doomed_flag = flag - - - # Callback class methods - - - def data_callback(self, dl_stat_dict, last_flag=False): - - """Called by downloads.VideoDownloader.do_download() and - .last_data_callback(). - - Based on Worker._data_hook() and ._talk_to_gui(). - - 'dl_stat_dict' holds a dictionary of statistics in a standard format - specified by downloads.VideoDownloader.extract_stdout_data(). - - This callback receives that dictionary and passes it on to the main - window, so the statistics can be displayed there. - - Args: - - dl_stat_dict (dict): The dictionary of statistics described above - - last_flag (bool): True when called by .last_data_callback(), - meaning that the VideoDownloader object has finished, and is - sending this function the final set of statistics - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 986 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, - ) - - -class DownloadList(object): - - """Called by mainapp.TartubeApp.download_manager_continue(). - - Based on the DownloadList class in youtube-dl-gui. - - Python class to keep track of all the media data objects to be downloaded - (for real or in simulation) during a downloaded operation. - - This object contains an ordered list of downloads.DownloadItem objects. - Each of those objects represents a media data object to be downloaded - (media.Video, media.Channel, media.Playlist or media.Folder). - - Videos are downloaded in the order specified by the list. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - operation_type (str): 'sim' if channels/playlists should just be - checked for new videos, 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) - - 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 - - """ - - - # Standard class methods - - - def __init__(self, app_obj, operation_type, media_data_list): - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1039 __init__') - - # IV list - class objects - # ----------------------- - self.app_obj = app_obj - - - # IV list - other - # --------------- - # 'sim' if channels/playlists should just be checked for new videos, - # 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) - 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 - # nothing, preventing any further downloads - self.prevent_fetch_flag = False - - # Number of download.DownloadItem objects created (used to give each a - # unique ID) - self.download_item_count = 0 - - # An ordered list of downloads.DownloadItem items, one for each - # media.Video, media.Channel, media.Playlist or media.Folder object - # This list stores each item's .item_id - self.download_item_list = [] - # Corresponding dictionary of downloads.DownloadItem items for quick - # lookup. Dictionary in the form - # key = download.DownloadItem.item_id - # value = the download.DownloadItem object itself - self.download_item_dict = {} - - - # Code - # ---- - - # For each media data object to be downloaded, created 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 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() - - - # Public class methods - - - @synchronise(_SYNC_LOCK) - def change_item_stage(self, item_id, new_stage): - - """Called by downloads.DownloadManager.run(). - - Based on DownloadList.change_stage(). - - Changes the download stage for the specified downloads.DownloadItem - object. - - Args: - - item_id (int): The specified item's .item_id - - new_stage: The new download stage, one of the values imported from - formats.py (e.g. formats.MAIN_STAGE_QUEUED) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1145 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. - - Creates a downloads.DownloadItem object for media data objects in the - media data registry. - - Doesn't create a download item object for: - - media.Video objects whose parent is not a media.Folder (i.e. - whose parent is a media.Channel or a media.Playlist) - - media.Video objects in any restricted folder - - media.Video objects in the fixed 'Unsorted Videos' folder which - are already marked as downloaded - - media.Video objects which have an ancestor (e.g. a parent - media.Channel) for which checking/downloading is disabled - - media.Video objects whose parent is a media.Folder, and whose - file IVs are set, and for which a thumbnail exists, if - mainapp.TartubeApp.operation_sim_shortcut_flag is set, and if - self.operation_type is set to 'sim' - - media.Channel and media.Playlist objects for which checking/ - downloading are disabled, or which have an ancestor (e.g. a - parent media.folder) for which checking/downloading is disabled - - media.Channel and media.Playlist objects during custom downloads - in which videos are to be downloaded independently - - media.Folder objects - - Adds the resulting downloads.DownloadItem object to this object's IVs. - - Args: - - 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 - - Returns: - - The downloads.DownloadItem object created (or None if no object is - created; only required by calls from - mainapp.TartubeApp.download_watch_videos() ) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1199 create_item') - - # Get the options.OptionsManager object that applies to this media - # data object - # (The manager might be specified by obj itself, or it might be - # specified by obj's parent, or we might use the default - # options.OptionsManager) - options_manager_obj = utils.get_options_manager( - self.app_obj, - media_data_obj, - ) - - # Ignore private folders, and don't download any of their children - # (because they are all children of some other non-private folder) - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: - return None - - # Don't download videos that we already have - # Don't download videos if they're in a channel or playlist (since - # downloading the channel/playlist downloads the videos it contains) - # (Exception: download a single video if that's what the calling code - # has specifically requested) - # (Exception: for custom downloads, do get videos independently of - # 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 - if isinstance(media_data_obj, media.Video): - - if media_data_obj.dl_flag \ - or ( - not isinstance(media_data_obj.parent_obj, media.Folder) \ - and not init_flag - and ( - self.operation_type != 'custom' - or not self.app_obj.custom_dl_by_video_flag - or media_data_obj.dl_flag - ) - ): - return None - - if isinstance(media_data_obj.parent_obj, media.Folder) \ - and self.operation_type == 'sim' \ - and self.app_obj.operation_sim_shortcut_flag \ - and media_data_obj.file_name \ - and utils.find_thumbnail(self.app_obj, media_data_obj): - return None - - # Don't create a download.DownloadItem object if the media data object - # has an ancestor for which checking/downloading is disabled - if isinstance(media_data_obj, media.Video): - dl_disable_flag = False - else: - dl_disable_flag = media_data_obj.dl_disable_flag - - parent_obj = media_data_obj.parent_obj - - while not dl_disable_flag and parent_obj is not None: - dl_disable_flag = parent_obj.dl_disable_flag - parent_obj = parent_obj.parent_obj - - if dl_disable_flag: - return None - - # Don't create a download.DownloadItem object for a media.Folder, - # obviously - # Dont' create a download.DownloadItem object for a media.Channel or - # media.Playlist during a custom download in which videos are to be - # downloaded independently - download_item_obj = None - - if ( - isinstance(media_data_obj, media.Video) - and self.operation_type == 'custom' - and self.app_obj.custom_dl_by_video_flag - and not media_data_obj.dl_flag - ) or ( - isinstance(media_data_obj, media.Video) - and ( - self.operation_type != 'custom' - or not self.app_obj.custom_dl_by_video_flag - ) - ) or ( - ( - isinstance(media_data_obj, media.Channel) \ - or isinstance(media_data_obj, media.Playlist) - ) and ( - self.operation_type != 'custom' - or not self.app_obj.custom_dl_by_video_flag - ) - ): - # Create a new download.DownloadItem object... - self.download_item_count += 1 - download_item_obj = DownloadItem( - self.download_item_count, - media_data_obj, - options_manager_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 - - # If the media data object has children, call this function recursively - # for each of them - if not isinstance(media_data_obj, media.Video): - for child_obj in media_data_obj.child_list: - self.create_item(child_obj) - - # Procedure complete - return download_item_obj - - - @synchronise(_SYNC_LOCK) - def fetch_next_item(self): - - """Called by downloads.DownloadManager.run(). - - Based on DownloadList.fetch_next(). - - Returns: - - The next downloads.DownloadItem object, or None if there are none - left. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1328 fetch_next_item') - - if not self.prevent_fetch_flag: - - for item_id in self.download_item_list: - this_item = self.download_item_dict[item_id] - - # Don't return an item that's marked as - # formats.MAIN_STAGE_ACTIVE - if this_item.stage == formats.MAIN_STAGE_QUEUED: - return this_item - - return None - - - @synchronise(_SYNC_LOCK) - def move_item_to_bottom(self, download_item_obj): - - """Called by mainwin.MainWin.on_progress_list_dl_last(). - - Moves the specified DownloadItem object to the end of - self.download_item_list, so it is assigned a DownloadWorker last - (after all other DownloadItems). - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object to move - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1360 move_item_to_bottom') - - # Move the item to the bottom (end) of the list - if download_item_obj is None \ - or not download_item_obj.item_id in self.download_item_list: - return - else: - self.download_item_list.append( - self.download_item_list.pop( - self.download_item_list.index(download_item_obj.item_id), - ), - ) - - - @synchronise(_SYNC_LOCK) - def move_item_to_top(self, download_item_obj): - - """Called by mainwin.MainWin.on_progress_list_dl_next(). - - Moves the specified DownloadItem object to the start of - self.download_item_list, so it is the next item to be assigned a - DownloadWorker. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object to move - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1391 move_item_to_top') - - # Move the item to the top (beginning) of the list - if download_item_obj is None \ - or not download_item_obj.item_id in self.download_item_list: - return - else: - self.download_item_list.insert( - 0, - self.download_item_list.pop( - self.download_item_list.index(download_item_obj.item_id), - ), - ) - - - @synchronise(_SYNC_LOCK) - def prevent_fetch_new_items(self): - - """Called by DownloadManager.stop_download_operation_soon(). - - Sets the flag that prevents calls to self.fetch_next_item() from - fetching anything new, which allows the download operation to stop as - soon as any ongoing video downloads have finished. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1417 prevent_fetch_new_items') - - self.prevent_fetch_flag = True - - - def reorder_master_slave(self): - - """Called by self.__init__() after the calls to self.create_item() are - finished. - - 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 - media data object can't be both a master and a slave). - - Even if this doesn't reduce the time the 'slaves' spend waiting to - start, it at least makes the download order predictable. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1442 reorder_master_slave') - - master_list = [] - other_list = [] - for item_id in self.download_item_list: - download_item_obj = self.download_item_dict[item_id] - - if isinstance(download_item_obj.media_data_obj, media.Video) \ - or not download_item_obj.media_data_obj.slave_dbid_list: - other_list.append(item_id) - else: - master_list.append(item_id) - - self.download_item_list = [] - self.download_item_list.extend(master_list) - self.download_item_list.extend(other_list) - - -class DownloadItem(object): - - """Called by downloads.DownloadList.create_item(). - - Based on the DownloadItem class in youtube-dl-gui. - - Python class used to track the download status of a media data object - (media.Video, media.Channel, media.Playlist or media.Folder), one of many - in a downloads.DownloadList object. - - Args: - - item_id (int) - The number of downloads.DownloadItem objects created, - 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 - - options_manager_obj (options.OptionsManager): The object which - specifies download options for the media data object - - """ - - - # Standard class methods - - - def __init__(self, item_id, media_data_obj, options_manager_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1490 __init__') - - # IV list - class objects - # ----------------------- - # The media data object to be downloaded - 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 - - - # IV list - other - # --------------- - # A unique ID for this object - self.item_id = item_id - # The current download stage - self.stage = formats.MAIN_STAGE_QUEUED - - -class VideoDownloader(object): - - """Called by downloads.DownloadWorker.run(). - - Based on the YoutubeDLDownloader class in youtube-dl-gui. - - Python class to create a system child process. Uses the child process to - instruct youtube-dl to download all videos associated with the URL - described by a downloads.DownloadItem object (which might be an individual - video, or a channel or playlist). - - Reads from the child process STDOUT and STDERR, having set up a - downloads.PipeReader object to do so in an asynchronous way. - - Sets self.return_code to a value in the range 0-5, described below. The - parent downloads.DownloadWorker object checks that return code once this - object's child process has finished. - - 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 to - interface with youtube-dl, and waits for this object to return a - return code - - download_item_obj (downloads.DownloadItem) - The download item object - describing the URL from which youtube-dl should download video(s) - - 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. - - """ - - - # Attributes - - - # Valid values for self.return_code. The larger the number, the higher in - # the hierarchy of return codes. - # Codes lower in the hierarchy (with a smaller number) cannot overwrite - # higher in the hierarchy (with a bigger number) - # - # 0 - The download operation completed successfully - OK = 0 - # 1 - A warning occured during the download operation - WARNING = 1 - # 2 - An error occured during the download operation - ERROR = 2 - # 3 - The corresponding url video file was larger or smaller from the given - # filesize limit - FILESIZE_ABORT = 3 - # 4 - The video(s) for the specified URL have already been downloaded - ALREADY = 4 - # 5 - The download operation was stopped by the user - STOPPED = 5 - - - # Standard class methods - - - def __init__(self, download_manager_obj, download_worker_obj, \ - download_item_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1582 __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 downloads.DownloadItem object describing the URL from which - # youtube-dl should download video(s) - self.download_item_obj = download_item_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 current return code, using values in the range 0-5, as described - # above - # The value remains set to self.OK unless we encounter any problems - # The larger the number, the higher in the hierarchy of return codes. - # Codes lower in the hierarchy (with a smaller number) cannot - # overwrite higher in the hierarchy (with a bigger number) - self.return_code = self.OK - # The time (in seconds) between iterations of the loop in - # self.do_download() - self.sleep_time = 0.1 - # The time (in seconds) to wait for an existing download, which shares - # a common download destination with this media data object, to - # finish downloading - self.long_sleep_time = 10 - - # 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 - - # 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 - # the next call to self.confirm_new_video(), .confirm_old_video() - # .confirm_sim_video() - self.stop_soon_flag = False - # When self.stop_soon_flag is True, the next call to - # self.confirm_new_video(), .confirm_old_video() or - # .confirm_sim_video() sets this flag to True, informing - # self.do_download() that it can stop the child process - self.stop_now_flag = False - - # youtube-dl is passed a URL, which might represent an individual - # video, a channel or a playlist - # Assume it's an individual video unless youtube-dl reports a - # channel or playlist (in which case, we can update these IVs later) - # For simulated downloads, both IVs are set to the number of - # videos actually found - self.video_num = None - self.video_total = None - # self.extract_stdout_data() detects the completion of a download job - # in one of several ways - # The first time it happens for each individual video, - # self.extract_stdout_data() takes action. It calls - # self.confirm_new_video(), self.confirm_old_video() or - # self.confirm_sim_video() when required - # On subsequent occasions, the completion message is ignored (as - # youtube-dl may pass us more than one completion message for a - # single video) - # Dictionary of videos, used to check for the first completion message - # for each unique video - # Dictionary in the form - # key = the video number (matches self.video_num) - # value = the video name (not actually used by anything at the - # moment) - self.video_check_dict = {} - # The code imported from youtube-dl-gui doesn't recognise a downloaded - # video, if Ffmpeg isn't used to extract it (because Ffmpeg is not - # installed, or because the website doesn't support it, or whatever) - # In this situation, youtube-dl's STDOUT messages don't definitively - # establish when it has finished downloading a video - # When a file destination is announced; it is temporarily stored in - # these IVs. When STDOUT receives a message in the form - # [download] 100% of 2.06MiB in 00:02 - # ...and the filename isn't one that Ffmpeg would use (e.g. - # 'myvideo.f136.mp4' or 'myvideo.f136.m4a', then assume that the - # video has finished downloading - self.temp_path = None - self.temp_filename = None - self.temp_extension = None - - # When checking a channel/playlist, this number is incremented every - # time youtube-dl gives us the details of a video which the Tartube - # database already contains (with a minimum number of IVs already - # set) - # When downloading a channel/playlist, this number is incremented every - # time youtube-dl gives us a 'video already downloaded' message - # (unless the Tartube database hasn't actually marked the video as - # downloaded) - # Every time the value is incremented, we check the limits specified by - # mainapp.TartubeApp.operation_check_limit or - # .operation_download_limit. If the limit has been reached, we stop - # checking/downloading the channel/playlist - # No check is carried out if self.download_item_obj represents an - # individual media.Video object (and not a whole channel or playlist) - self.video_limit_count = 0 - # Git issue #9 describes youtube-dl failing to download the video's - # JSON metadata. We can't do anything about the youtube-dl code, but - # we can apply our own timeout - # This IV is set whenever self.confirm_sim_video() is called. After - # being set, if a certain time has passed without another call to - # self.confirm_sim_video, self.do_download() halts the child process - self.last_sim_video_check_time = None - # The time to wait, in seconds - self.last_sim_video_wait_time = 60 - - # If mainapp.TartubeApp.operation_convert_mode is set to any value - # other than 'disable', then a media.Video object whose URL - # represents a channel/playlist is converted into multiple new - # media.Video objects, one for each video actually downloaded - # Flag set to True when self.download_item_obj.media_data_obj is a - # media.Video object, but a channel/playlist is detected (regardless - # of the value of mainapp.TartubeApp.operation_convert_mode) - self.url_is_not_video_flag = False - - - # Code - # ---- - # Initialise IVs depending on whether this is a real or simulated - # 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 - # 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 - - 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 - - - # Public class methods - - - def do_download(self): - - """Called by downloads.DownloadWorker.run(). - - Based on YoutubeDLDownloader.download(). - - Downloads video(s) from a URL described by self.download_item_obj. - - Returns: - - The final return code, a value in the range 0-5 (as described - above) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 1763 do_download') - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - - # Set the default return code. Everything is OK unless we encounter - # 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 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): - - 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' \ - 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( - app_obj, - self.download_item_obj.media_data_obj, - self.download_worker_obj.options_list, - self.dl_sim_flag, - divert_mode, - ) - - # ...display it in the Output Tab (if required)... - if app_obj.ytdl_output_system_cmd_flag: - space = ' ' - app_obj.main_win_obj.output_tab_write_system_cmd( - self.download_worker_obj.worker_id, - space.join(cmd_list), - ) - - # ...and the terminal (if required)... - if app_obj.ytdl_write_system_cmd_flag: - space = ' ' - print(space.join(cmd_list)) - - # ...and create a new child process using that command - 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, - ) - - # While downloading the video, update the callback function with - # the status of the current job - 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) - - # Read from the child process STDOUT, and convert into unicode for - # Python's convenience - while not self.stdout_queue.empty(): - - stdout = self.stdout_queue.get_nowait().rstrip() - if stdout: - - if os.name == 'nt': - stdout = stdout.decode('cp1252') - else: - stdout = stdout.decode('utf-8') - - # Convert the statistics into a python dictionary in a - # standard format, specified in the comments for - # self.extract_stdout_data() - dl_stat_dict = self.extract_stdout_data(stdout) - # If the job's status is formats.COMPLETED_STAGE_ALREADY - # or formats.ERROR_STAGE_ABORT, set our self.return_code - # IV - self.extract_stdout_status(dl_stat_dict) - # Pass the dictionary on to self.download_worker_obj so the - # main window can be updated - self.download_worker_obj.data_callback(dl_stat_dict) - - # Show output in the Output Tab (if required). For - # simulated downloads, a message is displayed by - # self.confirm_sim_video() instead - if app_obj.ytdl_output_stdout_flag \ - and ( - not app_obj.ytdl_output_ignore_progress_flag \ - or not re.match( - r'\[download\]\s+[0-9\.]+\%\sof\s.*\sat\s.*\sETA', - stdout, - ) - ) and ( - not app_obj.ytdl_output_ignore_json_flag \ - or stdout[:1] != '{' - ): - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - stdout, - ) - - # Show output in the terminal (if required). For simulated - # downloads, a message is displayed by - # self.confirm_sim_video() instead - if app_obj.ytdl_write_stdout_flag \ - and ( - not app_obj.ytdl_write_ignore_progress_flag \ - or not re.match( - r'\[download\]\s+[0-9\.]+\%\sof\s.*\sat\s.*\sETA', - stdout, - ) - ) and ( - not app_obj.ytdl_write_ignore_json_flag \ - or stdout[:1] != '{' - ): - print(stdout) - - # Apply the JSON timeout, if required - if app_obj.apply_json_timeout_flag \ - and self.last_sim_video_check_time is not None \ - and self.last_sim_video_check_time < time.time(): - # Halt the child process, which stops checking this channel/ - # playlist - self.stop() - - GObject.timeout_add( - 0, - app_obj.system_error, - 302, - 'Enforced timeout on youtube-dl because it took too long' \ - + ' to fetch a video\'s JSON data', - ) - - # Stop this video downloader, if required to do so, having just - # finished checking/downloading a video - if self.stop_now_flag: - self.stop() - - - # The child process has finished - while not self.stderr_queue.empty(): - - # Read from the child process STDERR queue (we don't need to read - # it in real time), and convert into unicode for python's - # convenience - stderr = self.stderr_queue.get_nowait().rstrip() - if os.name == 'nt': - stderr = stderr.decode('cp1252') - else: - stderr = stderr.decode('utf-8') - - if not self.is_ignorable(stderr): - - if self.is_warning(stderr): - self.set_return_code(self.WARNING) - self.download_item_obj.media_data_obj.set_warning(stderr) - - elif not self.is_debug(stderr): - self.set_return_code(self.ERROR) - self.download_item_obj.media_data_obj.set_error(stderr) - - # Show output in the Output Tab (if required) - if (app_obj.ytdl_output_stderr_flag): - app_obj.main_win_obj.output_tab_write_stderr( - self.download_worker_obj.worker_id, - stderr, - ) - - # Show output in the terminal (if required) - if (app_obj.ytdl_write_stderr_flag): - print(stderr) - - # We also set the return code to self.ERROR if the download didn't - # start or if the child process return code is greater than 0 - # Original notes from youtube-dl-gui: - # NOTE: In Linux if the called script is just empty Python exits - # normally (ret=0), so we can't detect this or similar cases - # using the code below - # NOTE: In Unix a negative return code (-N) indicates that the child - # was terminated by signal N (e.g. -9 = SIGKILL) - 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', - ) - - elif self.child_process.returncode > 0: - self.set_return_code(self.ERROR) - - 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( - self.child_process.returncode, - ) - ) - - # Pass a dictionary of values to downloads.DownloadWorker, confirming - # the result of the job. The values are passed on to the main - # window - self.last_data_callback() - - # Pass the result back to the parent downloads.DownloadWorker object - return self.return_code - - - def check_dl_is_correct_type(self): - - """Called by self.extract_stdout_data(). - - When youtube-dl reports the URL associated with the download item - object contains multiple videos (or potentially contains multiple - videos), then the URL represents a channel or playlist, not a video. - - This function checks whether a channel/playlist is about to be - downloaded into a media.Video object. If so, it takes action to prevent - that from happening. - - The action taken depends on the value of - mainapp.TartubeApp.operation_convert_mode. - - Returns: - - False if a channel/playlist was about to be downloaded into a - media.Video object, which has since been replaced by a new - media.Channel/media.Playlist object - - True in all other situations (including when a channel/playlist was - about to be downloaded into a media.Video object, which was - not replaced by a new media.Channel/media.Playlist object) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2013 check_dl_is_correct_type') - - app_obj = self.download_manager_obj.app_obj - media_data_obj = self.download_item_obj.media_data_obj - - if isinstance(self.download_item_obj.media_data_obj, media.Video): - - # If the mode is 'disable', or if it the original media.Video - # object is contained in a channel or a playlist, then we must - # stop downloading this URL immediately - if app_obj.operation_convert_mode == 'disable' \ - or not isinstance( - self.download_item_obj.media_data_obj.parent_obj, - media.Folder, - ): - self.url_is_not_video_flag = True - - # Stop downloading this URL - self.stop() - media_data_obj.set_error( - 'The video \'' + media_data_obj.name \ - + '\' has a source URL that points to a channel or a' \ - + ' playlist, not a video', - ) - - # Don't allow self.confirm_sim_video() to be called - return False - - # Otherwise, we can create new media.Video objects for each - # video downloaded/checked. The new objects may be placd into a - # new media.Channel or media.Playlist object - elif not self.url_is_not_video_flag: - - self.url_is_not_video_flag = True - - # Mark the original media.Video object to be destroyed at the - # end of the download operation - self.download_manager_obj.mark_video_as_doomed(media_data_obj) - - if app_obj.operation_convert_mode != 'multi': - - # Create a new media.Channel or media.Playlist object and - # add it to the download manager - # Then halt this job, so the new channel/playlist object - # can be downloaded - self.convert_video_to_container() - - # Don't allow self.confirm_sim_video() to be called - return False - - # Do allow self.confirm_sim_video() to be called - return True - - - def close(self): - - """Called by DownloadWorker.run(). - - Destructor function for this object. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2075 close') - - # Tell the PipeReader objects to shut down, thus joining their threads - self.stdout_reader.join() - self.stderr_reader.join() - - - def confirm_new_video(self, dir_path, filename, extension): - - """Called by self.extract_stdout_data(). - - A successful download is announced in one of several ways. - - When an announcement is detected, this function is called. Use the - first announcement to update self.video_check_dict, and ignore - subsequent announcements. - - Args: - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2104 confirm_new_video') - - if not self.video_num in self.video_check_dict: - - app_obj = self.download_manager_obj.app_obj - self.video_check_dict[self.video_num] = filename - - # Create a new media.Video object for the video - if self.url_is_not_video_flag: - - video_obj = app_obj.convert_video_from_download( - self.download_item_obj.media_data_obj.parent_obj, - self.download_item_obj.options_manager_obj, - dir_path, - filename, - extension, - True, # Don't sort parent containers yet - ) - - else: - - video_obj = app_obj.create_video_from_download( - self.download_item_obj, - dir_path, - filename, - extension, - True, # Don't sort parent containers yet - ) - - # If downloading from a channel/playlist, remember the video's - # index. (The server supplies an index even for a channel, and - # the user might want to convert a channel to a playlist) - if isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist): - video_obj.set_index(self.video_num) - - # Fetch the options.OptionsManager object used for this download - options_manager_obj = self.download_worker_obj.options_manager_obj - - # Update the main window - GObject.timeout_add( - 0, - app_obj.announce_video_download, - self.download_item_obj, - video_obj, - options_manager_obj.options_dict['keep_description'], - options_manager_obj.options_dict['keep_info'], - options_manager_obj.options_dict['keep_annotations'], - options_manager_obj.options_dict['keep_thumbnail'], - ) - - # Register the download with DownloadManager, so that download - # limits can be applied, if required - self.download_manager_obj.register_video() - - # This VideoDownloader can now stop, if required to do so after a video - # has been checked/downloaded - if self.stop_soon_flag: - self.stop_now_flag = True - - - def confirm_old_video(self, dir_path, filename, extension): - - """Called by self.extract_stdout_data(). - - When youtube-dl reports a video has already been downloaded, make sure - the media.Video object is marked as downloaded, and upate the video - catalogue in the main window if necessary. - - Args: - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2185 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): - - if not media_data_obj.dl_flag: - - GObject.timeout_add( - 0, - app_obj.mark_video_downloaded, - media_data_obj, - True, # Video is downloaded - True, # Video is not new - ) - - else: - - # media_data_obj is a media.Channel or media.Playlist object. Check - # its child objects, looking for a matching video - match_obj = media_data_obj.find_matching_video(app_obj, filename) - if match_obj: - - if not match_obj.dl_flag: - - GObject.timeout_add( - 0, - app_obj.mark_video_downloaded, - match_obj, - True, # Video is downloaded - True, # Video is not new - ) - - else: - - # This video applies towards the limit (if any) specified - # by mainapp.TartubeApp.operation_download_limit - self.video_limit_count += 1 - - if not isinstance( - self.download_item_obj.media_data_obj, - media.Video, - ) \ - and app_obj.operation_limit_flag \ - and app_obj.operation_download_limit \ - and self.video_limit_count >= \ - app_obj.operation_download_limit: - # Limit reached; stop downloading videos in this - # channel/playlist - self.stop() - - else: - - # No match found, so create a new media.Video object for the - # video file that already exists on the user's filesystem - self.video_check_dict[self.video_num] = filename - - video_obj = app_obj.create_video_from_download( - self.download_item_obj, - dir_path, - filename, - extension, - ) - - # Fetch the options.OptionsManager object used for this - # download - options_manager_obj \ - = self.download_worker_obj.options_manager_obj - - # Update the main window - if media_data_obj.master_dbid != media_data_obj.dbid: - - # The container is storing its videos in another - # container's sub-directory, which (probably) explains - # why we couldn't find a match. Don't add anything to the - # Results List - GObject.timeout_add( - 0, - app_obj.announce_video_clone, - video_obj, - ) - - else: - - # Do add an entry to the Results List (as well as updating - # the Video Catalogue, as normal) - GObject.timeout_add( - 0, - app_obj.announce_video_download, - self.download_item_obj, - video_obj, - options_manager_obj.options_dict['keep_description'], - options_manager_obj.options_dict['keep_info'], - options_manager_obj.options_dict['keep_annotations'], - options_manager_obj.options_dict['keep_thumbnail'], - ) - - # This VideoDownloader can now stop, if required to do so after a video - # has been checked/downloaded - if self.stop_soon_flag: - self.stop_now_flag = True - - - def confirm_sim_video(self, json_dict): - - """Called by self.extract_stdout_data(). - - After a successful simulated download, youtube-dl presents us with JSON - data for the video. Use that data to update everything. - - Args: - - json_dict (dict): JSON data from STDOUT, converted into a python - dictionary - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2305 confirm_sim_video') - - # Import the main application (for convenience) - app_obj = self.download_manager_obj.app_obj - # Call self.stop(), if the limit described in the comments for - # self.__init__() have been reached - stop_flag = False - - # Set the time at which a JSON timeout should be applied, if no more - # calls to this function have been made - self.last_sim_video_check_time \ - = int(time.time()) + self.last_sim_video_wait_time - - # From the JSON dictionary, extract the data we need - if '_filename' in json_dict: - full_path = json_dict['_filename'] - path, filename, extension = self.extract_filename(full_path) - else: - GObject.timeout_add( - 0, - app_obj.system_error, - 303, - 'Missing filename in JSON data', - ) - - return - - if 'upload_date' in json_dict: - # date_string in form YYYYMMDD - date_string = json_dict['upload_date'] - dt_obj = datetime.datetime.strptime(date_string, '%Y%m%d') - upload_time = dt_obj.timestamp() - else: - upload_time = None - - if 'duration' in json_dict: - duration = json_dict['duration'] - else: - duration = None - - if 'title' in json_dict: - name = json_dict['title'] - else: - name = None - - if 'description' in json_dict: - descrip = json_dict['description'] - else: - descrip = None - - if 'thumbnail' in json_dict: - thumbnail = json_dict['thumbnail'] - else: - thumbnail = None - - if 'webpage_url' in json_dict: - source = json_dict['webpage_url'] - else: - source = None - - if 'playlist_index' in json_dict: - playlist_index = json_dict['playlist_index'] - else: - playlist_index = None - - # Does an existing media.Video object match this video? - media_data_obj = self.download_item_obj.media_data_obj - video_obj = None - - if self.url_is_not_video_flag: - - # media_data_obj has a URL which represents a channel or playlist, - # but media_data_obj itself is a media.Video object - # media_data_obj's parent is a media.Folder object. Check its - # child objects, looking for a matching video - # (video_obj is set to None, if no match is found) - video_obj = media_data_obj.parent_obj.find_matching_video( - app_obj, - filename, - ) - - if not video_obj: - video_obj = media_data_obj.parent_obj.find_matching_video( - app_obj, - name, - ) - - elif isinstance(media_data_obj, media.Video): - - # media_data_obj is a media.Video object - video_obj = media_data_obj - - else: - - # media_data_obj is a media.Channel or media.Playlist object. Check - # its child objects, looking for a matching video - # (video_obj is set to None, if no match is found) - video_obj = media_data_obj.find_matching_video(app_obj, filename) - if not video_obj: - video_obj = media_data_obj.find_matching_video(app_obj, name) - - new_flag = False - update_results_flag = False - if not video_obj: - - # No matching media.Video object found, so create a new one - new_flag = True - update_results_flag = True - - if self.url_is_not_video_flag: - - video_obj = app_obj.convert_video_from_download( - self.download_item_obj.media_data_obj.parent_obj, - self.download_item_obj.options_manager_obj, - path, - filename, - extension, - # Don't sort parent container objects yet; wait for - # mainwin.MainWin.results_list_update_row() to do it - True, - ) - - else: - - video_obj = app_obj.create_video_from_download( - self.download_item_obj, - path, - filename, - extension, - True, - ) - - # Update its IVs with the JSON information we extracted - if filename is not None: - video_obj.set_name(filename) - - if name is not None: - video_obj.set_nickname(name) - elif filename is not None: - video_obj.set_nickname(filename) - - if upload_time is not None: - video_obj.set_upload_time(upload_time) - - if duration is not None: - video_obj.set_duration(duration) - - if source is not None: - video_obj.set_source(source) - - if descrip is not None: - video_obj.set_video_descrip( - descrip, - app_obj.main_win_obj.descrip_line_max_len, - ) - - # If downloading from a channel/playlist, remember the video's - # index. (The server supplies an index even for a channel, and - # the user might want to convert a channel to a playlist) - if isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist): - video_obj.set_index(playlist_index) - - # Now we can sort the parent containers - video_obj.parent_obj.sort_children() - app_obj.fixed_all_folder.sort_children() - if video_obj.bookmark_flag: - app_obj.fixed_bookmark_folder.sort_children() - if video_obj.fav_flag: - app_obj.fixed_fav_folder.sort_children() - if video_obj.new_flag: - app_obj.fixed_new_folder.sort_children() - if video_obj.waiting_flag: - app_obj.fixed_waiting_folder.sort_children() - - else: - - if video_obj.file_name \ - and video_obj.name != app_obj.default_video_name: - - # This video must not be displayed in the Results List, and - # does counts towards the limit (if any) specified by - # mainapp.TartubeApp.operation_check_limit - self.video_limit_count += 1 - - if not isinstance( - self.download_item_obj.media_data_obj, - media.Video, - ) \ - and app_obj.operation_limit_flag \ - and app_obj.operation_check_limit \ - and self.video_limit_count >= app_obj.operation_check_limit: - # Limit reached. When we reach the end of this function, - # stop checking videos in this channel playlist - 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 - update_results_flag = True - - # If the 'Add videos' button was used, the path/filename/extension - # won't be set yet - if not video_obj.file_name and full_path: - video_obj.set_file(filename, extension) - - # Update any video object IVs that are not set - if video_obj.name == app_obj.default_video_name \ - and filename is not None: - video_obj.set_name(filename) - - if video_obj.nickname == app_obj.default_video_name: - if name is not None: - video_obj.set_nickname(name) - elif filename is not None: - video_obj.set_nickname(filename) - - if not video_obj.upload_time and upload_time is not None: - video_obj.set_upload_time(upload_time) - - if not video_obj.duration and duration is not None: - video_obj.set_duration(duration) - - if not video_obj.source and source is not None: - video_obj.set_source(source) - - if not video_obj.descrip and descrip is not None: - video_obj.set_video_descrip( - descrip, - app_obj.main_win_obj.descrip_line_max_len, - ) - - # If downloading from a channel/playlist, remember the video's - # index. (The server supplies an index even for a channel, and - # the user might want to convert a channel to a playlist) - if isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist): - video_obj.set_index(playlist_index) - - # 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 - - if descrip and options_dict['write_description']: - descrip_path = os.path.abspath( - os.path.join(path, filename + '.description'), - ) - if not options_dict['sim_keep_description']: - descrip_path = utils.convert_path_to_temp( - app_obj, - descrip_path, - ) - - # (Don't replace a file that already exists) - if not os.path.isfile(descrip_path): - try: - fh = open(descrip_path, 'wb') - fh.write(descrip.encode('utf-8')) - fh.close() - except: - pass - - if options_dict['write_info']: - json_path = os.path.abspath( - os.path.join(path, filename + '.info.json'), - ) - if not options_dict['sim_keep_info']: - json_path = utils.convert_path_to_temp(app_obj, json_path) - - if not os.path.isfile(json_path): - try: - with open(json_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - except: - pass - - if options_dict['write_annotations']: - xml_path = os.path.abspath( - os.path.join(path, filename + '.annotations.xml'), - ) - if not options_dict['sim_keep_annotations']: - xml_path = utils.convert_path_to_temp(app_obj, xml_path) - - if thumbnail and options_dict['write_thumbnail']: - - # Download the thumbnail, if we don't already have it - # The thumbnail's URL is something like - # 'https://i.ytimg.com/vi/abcdefgh/maxresdefault.jpg' - # When saved to disc by youtube-dl, the file is given the same name - # as the video (but with a different extension) - # Get the thumbnail's extension... - remote_file, remote_ext = os.path.splitext(thumbnail) - - # ...and thus get the filename used by youtube-dl when storing the - # thumbnail locally - thumb_path = video_obj.get_actual_path_by_ext(app_obj, remote_ext) - - if not options_dict['sim_keep_thumbnail']: - thumb_path = utils.convert_path_to_temp(app_obj, thumb_path) - - if not os.path.isfile(thumb_path): - request_obj = requests.get(thumbnail) - - # v1.2.006 This crashes if the directory specified by - # thumb_path doesn't exist, so need to use 'try' - try: - with open(thumb_path, 'wb') as outfile: - outfile.write(request_obj.content) - except: - pass - - # If a new media.Video object was created (or if a video whose name is - # unknown, now has a name), add a line to the Results List, as well - # as updating the Video Catalogue - if update_results_flag: - - GObject.timeout_add( - 0, - app_obj.announce_video_download, - self.download_item_obj, - video_obj, - ) - - else: - - # Otherwise, just update the Video Catalogue - GObject.timeout_add( - 0, - app_obj.main_win_obj.video_catalogue_update_row, - video_obj, - ) - - # For simulated downloads, self.do_download() has not displayed - # anything in the Output Tab/terminal window; so do that now (if - # required) - if (app_obj.ytdl_output_stdout_flag): - - msg = '[' + video_obj.parent_obj.name \ - + '] ' - - if (app_obj.ytdl_output_stdout_flag): - app_obj.main_win_obj.output_tab_write_stdout( - self.download_worker_obj.worker_id, - msg, - ) - - if (app_obj.ytdl_write_stdout_flag): - print(msg) - - # If a new media.Video object was created (or if a video whose name is - # unknown, now has a name), register the simulated download with - # DownloadManager, so that download limits can be applied, if - # required - if update_results_flag: - self.download_manager_obj.register_video() - - # Stop checking videos in this channel/playlist, if a limit has been - # reached - if stop_flag: - self.stop() - - # This VideoDownloader can now stop, if required to do so after a video - # has been checked/downloaded - elif self.stop_soon_flag: - self.stop_now_flag = True - - - def convert_video_to_container (self): - - """Called by self.check_dl_is_correct_type(). - - Creates a new media.Channel or media.Playlist object to replace an - existing media.Video object. The new object is given some of the - properties of the old one. - - This function doesn't destroy the old object; DownloadManager.run() - handles that. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2686 convert_video_to_container') - - app_obj = self.download_manager_obj.app_obj - old_video_obj = self.download_item_obj.media_data_obj - container_obj = old_video_obj.parent_obj - - # Some media.Folder objects cannot contain channels or playlists (for - # example, the 'Unsorted Videos' folder) - # If that is the case, the new channel/playlist is created without a - # parent. Otherwise, it is created at the same location as the - # original media.Video object - if container_obj.restrict_flag: - container_obj = None - - # Decide on a name for the new channel/playlist, e.g. 'channel_1' or - # 'playlist_4'. The name must not already be in use. The user can - # customise the name when they're ready - name = utils.find_available_name( - app_obj, - # e.g. 'channel' - app_obj.operation_convert_mode, - # Allow 'channel_1', if available - 1, - ) - - # (Prevent any possibility of an infinite loop by giving up after - # thousands of attempts) - name = None - new_container_obj = None - - for n in range (1, 9999): - test_name = app_obj.operation_convert_mode + '_' + str(n) - if not test_name in app_obj.media_name_dict: - name = test_name - break - - if name is not None: - - # Create the new channel/playlist. Very unlikely that the old - # media.Video object has its .dl_sim_flag set, but we'll use it - # nonetheless - if app_obj.operation_convert_mode == 'channel': - - new_container_obj = app_obj.add_channel( - name, - container_obj, # May be None - source = old_video_obj.source, - dl_sim_flag = old_video_obj.dl_sim_flag, - ) - - else: - - new_container_obj = app_obj.add_playlist( - name, - container_obj, # May be None - source = old_video_obj.source, - dl_sim_flag = old_video_obj.dl_sim_flag, - ) - - if new_container_obj is None: - - # New channel/playlist could not be created (for some reason), so - # stop downloading from this URL - self.stop() - media_data_obj.set_error( - 'The video \'' + media_data_obj.name \ - + '\' has a source URL that points to a channel or a' \ - + ' playlist, not a video', - ) - - else: - - # Update IVs for the new channel/playlist object - new_container_obj.set_options_obj(old_video_obj.options_obj) - new_container_obj.set_source(old_video_obj.source) - - # Add the new channel/playlist to the Video Index (but don't - # select it) - app_obj.main_win_obj.video_index_add_row(new_container_obj, True) - - # Add the new channel/playlist to the download manager's list of - # things to download... - new_download_item_obj \ - = self.download_manager_obj.download_list_obj.create_item( - new_container_obj, - ) - # ...and add a row the Progress List - app_obj.main_win_obj.progress_list_add_row( - new_download_item_obj.item_id, - new_download_item_obj.media_data_obj, - ) - - # Stop this download job, allowing the replacement one to start - self.stop() - - - def create_child_process(self, cmd_list): - - """Called by self.do_download() immediately after the call to - self.get_system_cmd(). - - 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: - - None on success, or the new value of self.return_code if there's an - error - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2804 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, - ) - - except (ValueError, OSError) as error: - # (There is no need to update the media data object's error list, - # as the code in self.do_download() will notice the child - # process didn't start, and set its own error message) - self.set_return_code(self.ERROR) - - - def extract_filename(self, input_data): - - """Called by self.confirm_sim_video() and .extract_stdout_data(). - - Based on the extract_data() function in youtube-dl-gui's - downloaders.py. - - Extracts various components of a filename. - - Args: - - input_data (str): Full path to a file which has been downloaded - and saved to the filesystem - - Returns: - - Returns the path, filename and extension components of the full - file path. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2855 extract_filename') - - path, fullname = os.path.split(input_data.strip("\"")) - filename, extension = os.path.splitext(fullname) - - return path, filename, extension - - - def extract_stdout_data(self, stdout): - - """Called by self.do_download(). - - Based on the extract_data() function in youtube-dl-gui's - downloaders.py. - - Extracts youtube-dl statistics from the child process. - - Args: - - stdout (str): String that contains a line from the child process - STDOUT (i.e., a message from youtube-dl) - - Returns: - - Python dictionary in a standard format also used by the main window - code. Dictionaries in this format are generally called - 'dl_stat_dict' (or some variation of it). - - The returned dictionary can be empty if there is no data to - extract, otherwise it contains one or more of the following keys: - - 'status' : Contains the status of the download - 'path' : Destination path - 'filename' : The filename without the extension - 'extension' : The file extension - 'percent' : The percentage of the video being downloaded - 'eta' : Estimated time for the completion of the - download - 'speed' : Download speed - 'filesize' : The size of the video file being downloaded - 'playlist_index' : The playlist index of the current video file - being downloaded - 'playlist_size' : The number of videos in the playlist - 'dl_sim_flag' : Flag set to True if we are simulating downloads - for this media data object, or False if we - actually downloading videos (set below) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 2905 extract_stdout_data') - - # Initialise the dictionary with default key-value pairs for the main - # window to display, to be overwritten (if possible) with new key- - # value pairs as this function interprets the STDOUT message - dl_stat_dict = { - 'playlist_index': self.video_num, - 'playlist_size': self.video_total, - 'dl_sim_flag': self.dl_sim_flag, - } - - # If STDOUT has not been received by this function, then the main - # window can be passed just the default key-value pairs - if not stdout: - return dl_stat_dict - - # In some cases, we want to preserve the multiple successive whitespace - # characters in the STDOUT message, in order to extract filenames - # in their original form - # In other cases, we just eliminate multiple successive whitespace - # characters - stdout_with_spaces_list = stdout.split(' ') - stdout_list = stdout.split() - - # Extract the data - stdout_list[0] = stdout_list[0].lstrip('\r') - if stdout_list[0] == '[download]': - - dl_stat_dict['status'] = formats.ACTIVE_STAGE_DOWNLOAD - - # Get path, filename and extension - if stdout_list[1] == 'Destination:': - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[2:]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.set_temp_destination(path, filename, extension) - - # Get progress information - if '%' in stdout_list[1]: - if stdout_list[1] != '100%': - dl_stat_dict['percent'] = stdout_list[1] - dl_stat_dict['eta'] = stdout_list[7] - dl_stat_dict['speed'] = stdout_list[5] - dl_stat_dict['filesize'] = stdout_list[3] - - else: - dl_stat_dict['percent'] = '100%' - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - dl_stat_dict['filesize'] = stdout_list[3] - - # If the most recently-received filename isn't one used by - # Ffmpeg, then this marks the end of a video download - # (See the comments in self.__init__) - if len(stdout_list) > 4 \ - and stdout_list[4] == 'in' \ - and self.temp_filename is not None \ - and not re.match(r'.*\.f\d{1,3}$', self.temp_filename): - - self.confirm_new_video( - self.temp_path, - self.temp_filename, - self.temp_extension, - ) - - self.reset_temp_destination() - - # Get playlist information (when downloading a channel or a - # playlist, this line is received once per video) - if stdout_list[1] == 'Downloading' and stdout_list[2] == 'video': - dl_stat_dict['playlist_index'] = stdout_list[3] - self.video_num = stdout_list[3] - dl_stat_dict['playlist_size'] = stdout_list[5] - self.video_total = stdout_list[5] - - # 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() - - # Remove the 'and merged' part of the STDOUT message when using - # FFmpeg to merge the formats - if stdout_list[-3] == 'downloaded' and stdout_list[-1] == 'merged': - stdout_list = stdout_list[:-2] - stdout_with_spaces_list = stdout_with_spaces_list[:-2] - - dl_stat_dict['percent'] = '100%' - - # Get file already downloaded status - if stdout_list[-1] == 'downloaded': - dl_stat_dict['status'] = formats.COMPLETED_STAGE_ALREADY - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[1:-4]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.reset_temp_destination() - - self.confirm_old_video(path, filename, extension) - - # Get filesize abort status - if stdout_list[-1] == 'Aborting.': - dl_stat_dict['status'] = formats.ERROR_STAGE_ABORT - - elif stdout_list[0] == '[hlsnative]': - - # Get information from the native HLS extractor (see - # https://github.com/rg3/youtube-dl/blob/master/youtube_dl/ - # downloader/hls.py#L54 - dl_stat_dict['status'] = formats.ACTIVE_STAGE_DOWNLOAD - - if len(stdout_list) == 7: - segment_no = float(stdout_list[6]) - current_segment = float(stdout_list[4]) - - # Get the percentage - percent = '{0:.1f}%'.format(current_segment / segment_no * 100) - dl_stat_dict['percent'] = percent - - elif stdout_list[0] == '[ffmpeg]': - - # Using FFmpeg, not the the native HLS extractor - # A successful video download is announced in one of several ways. - # Use the first announcement to update self.video_check_dict, and - # ignore subsequent announcements - dl_stat_dict['status'] = formats.ACTIVE_STAGE_POST_PROCESS - - # Get the final file extension after the merging process has - # completed - if stdout_list[1] == 'Merging': - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[4:]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.reset_temp_destination() - - self.confirm_new_video(path, filename, extension) - - # Get the final file extension after simple FFmpeg post-processing - # (i.e. not after a file merge) - if stdout_list[1] == 'Destination:': - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[2:]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.reset_temp_destination() - - self.confirm_new_video(path, filename, extension) - - # Get final file extension after the recoding process - if stdout_list[1] == 'Converting': - path, filename, extension = self.extract_filename( - ' '.join(stdout_with_spaces_list[8:]), - ) - - dl_stat_dict['path'] = path - dl_stat_dict['filename'] = filename - dl_stat_dict['extension'] = extension - self.reset_temp_destination() - - self.confirm_new_video(path, filename, extension) - - elif stdout_list[0][0] == '{': - - # JSON data, the result of a simulated download. Convert to a - # python dictionary - if self.dl_sim_flag: - - # (Try/except to check for invalid JSON) - try: - json_dict = json.loads(stdout) - - except: - GObject.timeout_add( - 0, - app_obj.system_error, - 304, - 'Invalid JSON data received from server', - ) - - if json_dict: - - # If youtube-dl is about to download a channel or playlist - # into a media.Video object, decide what to do to prevent - # 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 - # for a URL that's an individual video, but > 0 for a URL - # that's actually a channel/playlist - if not self.video_num \ - or self.check_dl_is_correct_type(): - self.confirm_sim_video(json_dict) - - self.video_num += 1 - dl_stat_dict['playlist_index'] = self.video_num - self.video_total += 1 - dl_stat_dict['playlist_size'] = self.video_total - - dl_stat_dict['status'] = formats.ACTIVE_STAGE_CHECKING - - elif stdout_list[0][0] != '[' or stdout_list[0] == '[debug]': - - # (Just ignore this output) - return dl_stat_dict - - else: - - # The download has started - dl_stat_dict['status'] = formats.ACTIVE_STAGE_PRE_PROCESS - - return dl_stat_dict - - - def extract_stdout_status(self, dl_stat_dict): - - """Called by self.do_download() immediately after a call to - self.extract_stdout_data(). - - Based on YoutubeDLDownloader._extract_info(). - - If the job's status is formats.COMPLETED_STAGE_ALREADY or - formats.ERROR_STAGE_ABORT, translate that into a new value for the - return code, and then use that value to actually set self.return_code - (which halts the download). - - Args: - - dl_stat_dict (dict): The Python dictionary returned by the call to - self.extract_stdout_data(), in the standard form described by - the comments for that function - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3151 extract_stdout_status') - - if 'status' in dl_stat_dict: - if dl_stat_dict['status'] == formats.COMPLETED_STAGE_ALREADY: - self.set_return_code(self.ALREADY) - dl_stat_dict['status'] = None - - if dl_stat_dict['status'] == formats.ERROR_STAGE_ABORT: - self.set_return_code(self.FILESIZE_ABORT) - dl_stat_dict['status'] = None - - - def is_child_process_alive(self): - - """Called by self.do_download() and self.stop(). - - Based on YoutubeDLDownloader._proc_is_alive(). - - Called continuously during the self.do_download() 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 3179 is_child_process_alive') - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def is_debug(self, stderr): - - """Called by self.do_download(). - - Based on YoutubeDLDownloader._is_warning(). - - After the child process has terminated with an error of some kind, - checks the STERR message to see if it's an error or just a debug - message (generated then youtube-dl verbose output is turned on). - - Args: - - stderr (str): A message from the child process STDERR - - Returns: - - True if the STDERR message is a youtube-dl debug message, False if - it's an error - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3209 is_debug') - - return stderr.split(' ')[0] == '[debug]' - - - def is_ignorable(self, stderr): - - """Called by self.do_download(). - - Before testing a STDERR message, see if it's one of the frequent - messages which the user has opted to ignore (if any). - - Args: - - stderr (str): A message from the child process STDERR - - Returns: - - True if the STDERR message is ignorable, False if it should be - tested further. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3233 is_ignorable') - - app_obj = self.download_manager_obj.app_obj - - if ( - app_obj.ignore_http_404_error_flag \ - and re.search( - r'unable to download video data\: HTTP Error 404', - stderr, - ) - ) or ( - app_obj.ignore_data_block_error_flag \ - and re.search(r'Did not get any data blocks', stderr) - ) or ( - app_obj.ignore_merge_warning_flag \ - and re.search( - r'Requested formats are incompatible for merge', - stderr, - ) - ) or ( - app_obj.ignore_missing_format_error_flag \ - and re.search( - r'No video formats found; please report this issue', - stderr, - ) - ) or ( - app_obj.ignore_no_annotations_flag \ - and re.search( - r'There are no annotations to write', - stderr, - ) - ) or ( - app_obj.ignore_no_subtitles_flag \ - and re.search( - r'video doesn\'t have subtitles', - stderr, - ) - ) or ( - app_obj.ignore_yt_copyright_flag \ - and ( - re.search( - r'This video contains content from.*copyright grounds', - stderr, - ) or re.search( - r'Sorry about that\.', - stderr, - ) - ) - ) or ( - app_obj.ignore_yt_age_restrict_flag \ - and ( - re.search( - r'ERROR\: Content Warning', - stderr, - ) or re.search( - r'This video may be inappropriate for some users', - stderr, - ) or re.search( - r'Sign in to confirm your age', - stderr, - ) - ) - ) or ( - app_obj.ignore_yt_uploader_deleted_flag \ - and ( - re.search( - r'The uploader has not made this video available', - stderr, - ) - ) - ): - # This message is ignorable - return True - - # Check the custom list of messages - for item in app_obj.ignore_custom_msg_list: - if ( - (not app_obj.ignore_custom_regex_flag) \ - and stderr.find(item) > -1 - ) or ( - app_obj.ignore_custom_regex_flag and re.search(item, stderr) - ): - # This message is ignorable - return True - - # This message is not ignorable - return False - - - def is_warning(self, stderr): - - """Called by self.do_download(). - - Based on YoutubeDLDownloader._is_warning(). - - After the child process has terminated with an error of some kind, - checks the STERR message to see if it's an error or just a warning. - - Args: - - stderr (str): A message from the child process STDERR - - Returns: - - True if the STDERR message is a warning, False if it's an error - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3342 is_warning') - - return stderr.split(':')[0] == 'WARNING' - - - def last_data_callback(self): - - """Called by self.download(). - - Based on YoutubeDLDownloader._last_data_hook(). - - After the child process has finished, creates a new Python dictionary - in the standard form described by self.extract_stdout_data(). - - Sets key-value pairs in the dictonary, then passes it to the parent - downloads.DownloadWorker object, confirming the result of the child - process. - - The new key-value pairs are used to update the main window. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3364 last_data_callback') - - dl_stat_dict = {} - - if self.return_code == self.OK: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_FINISHED - elif self.return_code == self.ERROR: - dl_stat_dict['status'] = formats.MAIN_STAGE_ERROR - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - elif self.return_code == self.WARNING: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_WARNING - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - elif self.return_code == self.STOPPED: - dl_stat_dict['status'] = formats.ERROR_STAGE_STOPPED - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - elif self.return_code == self.ALREADY: - dl_stat_dict['status'] = formats.COMPLETED_STAGE_ALREADY - else: - dl_stat_dict['status'] = formats.ERROR_STAGE_ABORT - - # Use some empty values in dl_stat_dict so that the Progress Tab - # doesn't show arbitrary data from the last file downloaded - dl_stat_dict['filename'] = '' - dl_stat_dict['extension'] = '' - dl_stat_dict['percent'] = '' - dl_stat_dict['eta'] = '' - dl_stat_dict['speed'] = '' - dl_stat_dict['filesize'] = '' - - # The True argument shows that this function is the caller - self.download_worker_obj.data_callback(dl_stat_dict, True) - - - def set_return_code(self, code): - - """Called by self.do_download(), .create_child_process(), - .extract_stdout_status() and .stop(). - - Based on YoutubeDLDownloader._set_returncode(). - - After the child process has terminated with an error of some kind, - sets a new value for self.return_code, but only if the new return code - is higher in the hierarchy of return codes than the current value. - - Args: - - code (int): A return code in the range 0-5 - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3418 set_return_code') - - if code >= self.return_code: - self.return_code = code - - - 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') - - self.temp_path = path - self.temp_filename = filename - self.temp_extension = extension - - - def reset_temp_destination(self): - - """Called by self.extract_stdout_data().""" - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3441 reset_temp_destination') - - self.temp_path = None - self.temp_filename = None - self.temp_extension = None - - - def stop(self): - - """Called by DownloadWorker.close() and also by - mainwin.MainWin.on_progress_list_stop_now(). - - Terminates the child process and sets this object's return code to - self.STOPPED. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3458 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) - - self.set_return_code(self.STOPPED) - - - def stop_soon(self): - - """Can be called by anything. Currently called by - mainwin.MainWin.on_progress_list_stop_soon(). - - Sets the flag that causes this VideoDownloader to stop after the - current video. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3488 stop_soon') - - self.stop_soon_flag = True - - -class PipeReader(threading.Thread): - - """Called by downloads.VideoDownloader.__init__(). - - Based on the PipeReader class in youtube-dl-gui. - - Python class used by downloads.VideoDownloader and updates.UpdateManager to - avoid deadlocks when reading from child process pipes STDOUT and STDERR. - - This class uses python threads and queues in order to read from child - process pipes in an asynchronous way. - - Args: - - queue (queue.Queue): Python queue to store the output of the child - process. - - Warnings: - - All the actions are based on 'str' types. The calling function must - convert the queued items back to 'unicode', if necessary. - - """ - - - # Standard class methods - - - def __init__(self, queue): - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3524 __init__') - - super(PipeReader, self).__init__() - - # IV list - other - # --------------- - # Python queue to store the output of the child process. - self.output_queue = queue - - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - # Flag that is set to False by self.join(), which enables the loop in - # self.run() to terminate - self.running_flag = True - # Set by self.attach_file_descriptor(). The file descriptor for the - # child process STDOUT or STDERR - self.file_descriptor = None - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Reads from STDOUT or STERR using the attached filed descriptor. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3561 run') - - # Use this flag so that the loop can ignore FFmpeg error messsages - # (because the parent VideoDownloader object shouldn't use that as a - # serious error) - ignore_line = False - - while self.running_flag: - - if self.file_descriptor is not None: - - for line in iter(self.file_descriptor.readline, str('')): - - if line == b'': - break - - if str.encode('ffmpeg version') in line: - ignore_line = True - - if not ignore_line: - self.output_queue.put_nowait(line) - - self.file_descriptor = None - ignore_line = False - - time.sleep(self.sleep_time) - - - def attach_file_descriptor(self, filedesc): - - """Called by downloads.VideoDownloader.do_download(). - - Sets the file descriptor for the child process STDOUT or STDERR. - - Args: - - filedesc (filehandle): The open filehandle for STDOUT or STDERR - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3602 attach_file_descriptor') - - self.file_descriptor = filedesc - - - def join(self, timeout=None): - - """Called by downloads.VideoDownloader.close(), which is the destructor - function for that object. - - Join the thread and update IVs. - - Args: - - timeout (-): No calling code sets a timeout - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('dld 3621 join') - - self.running_flag = False - super(PipeReader, self).join(timeout) diff --git a/tartube/files.py b/tartube/files.py deleted file mode 100755 index 5511223..0000000 --- a/tartube/files.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""File manager classes.""" - - -# Import Gtk modules -# ... - - -# Import other modules -from gi.repository import GdkPixbuf -import json -import os -import threading - - -# Import our modules -# ... - - -# Classes - - -class FileManager(threading.Thread): - - """Called by mainapp.TartubeApp.__init__(). - - Python class to manage loading of thumbnail, icon and JSON files safely - (i.e. without causing a Gtk crash). - """ - - - # Standard class methods - - - def __init__(self): - - super(FileManager, self).__init__() - - - # Public class methods - - - def load_json(self, full_path): - - """Can be called by anything. - - Given the full path to a JSON file, loads the file into a Python - dictionary and returns the dictionary. - - Args: - - full_path (str): The full path to the JSON file - - Returns: - - The JSON data, converted to a Python dictionary (an empty - dictionary if the file is missing or can't be loaded) - - """ - - empty_dict = {} - if not os.path.isfile(full_path): - return empty_dict - - with open(full_path, 'r') as json_file: - - try: - json_dict = json.load(json_file) - return json_dict - - except: - return empty_dict - - - def load_text(self, full_path): - - """Can be called by anything. - - Given the full path to a text file, loads it. - - Args: - - full_path (str): The full path to the text file - - Returns: - - The contents of the text file as a string, or or None if the file - is missing or can't be loaded - - """ - - if not os.path.isfile(full_path): - return None - - with open(full_path, 'r') as text_file: - - try: - text = text_file.read() - return text - - except: - return None - - - def load_to_pixbuf(self, full_path, width=None, height=None): - - """Can be called by anything. - - Given the full path to an icon file, loads the icon into a pibxuf, and - returns the pixbuf. - - Args: - - full_path (str): The full path to the icon file - - width, height (int or None): If both are specified, the icon is - scaled to that size - - Returns: - - A GdkPixbuf, or None if the file is missing or can't be loaded - - """ - - if not os.path.isfile(full_path): - return None - - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(full_path) - except: - return None - - if width is not None and height is not None: - pixbuf = pixbuf.scale_simple( - width, - height, - GdkPixbuf.InterpType.BILINEAR, - ) - - return pixbuf diff --git a/tartube/formats.py b/tartube/formats.py deleted file mode 100755 index 52651ab..0000000 --- a/tartube/formats.py +++ /dev/null @@ -1,705 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Constant variables used in various parts of the code.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import datetime - - -# Import our modules -# ... - - -# Some icons are different at Christmas -today = datetime.date.today() -day = today.strftime("%d") -month = today.strftime("%m") -if (int(month) == 12 and int(day) >= 24) \ -or (int(month) == 1 and int(day) <= 5): - xmas_flag = True -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 - -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), -] - -TIME_METRIC_LIST = [] -TIME_METRIC_DICT = {} - -while time_metric_setup_list: - key = time_metric_setup_list.pop(0) - value = time_metric_setup_list.pop(0) - - TIME_METRIC_LIST.append(key) - TIME_METRIC_DICT[key] = value - -KILO_SIZE = 1024.0 -filesize_metric_setup_list = [ - 'B', 1, - 'KiB', int(KILO_SIZE ** 1), - 'MiB', int(KILO_SIZE ** 2), - 'GiB', int(KILO_SIZE ** 3), - 'TiB', int(KILO_SIZE ** 4), - 'PiB', int(KILO_SIZE ** 5), - 'EiB', int(KILO_SIZE ** 6), - 'ZiB', int(KILO_SIZE ** 7), - 'YiB', int(KILO_SIZE ** 8), -] - -FILESIZE_METRIC_LIST = [] -FILESIZE_METRIC_DICT = {} - -while filesize_metric_setup_list: - key = filesize_metric_setup_list.pop(0) - value = filesize_metric_setup_list.pop(0) - - FILESIZE_METRIC_LIST.append(key) - FILESIZE_METRIC_DICT[key] = value - -file_output_setup_list = [ - 0, 'Custom', - None, # (The same as option 2 by default) - 1, 'ID', - '%(id)s.%(ext)s', - 2, 'Title', - '%(title)s.%(ext)s', - 3, 'Title + ID', - '%(title)s-%(id)s.%(ext)s', - 4, 'Title + Quality', - '%(title)s-%(height)sp.%(ext)s', - 5, 'Title + ID + Quality', - '%(title)s-%(id)s-%(height)sp.%(ext)s', - 6, 'Autonumber + Title', - '%(playlist_index)s-%(title)s.%(ext)s', - 7, 'Autonumber + Title + ID', - '%(playlist_index)s-%(title)s-%(id)s.%(ext)s', - 8, 'Autonumber + Title + Quality', - '%(playlist_index)s-%(title)s-%(height)sp.%(ext)s', - 9, 'Autonumber + Title + ID + Quality', - '%(playlist_index)s-%(title)s-%(id)s-%(height)sp.%(ext)s', -] - -FILE_OUTPUT_NAME_DICT = {} -FILE_OUTPUT_CONVERT_DICT = {} - -while file_output_setup_list: - key = file_output_setup_list.pop(0) - value = file_output_setup_list.pop(0) - value2 = file_output_setup_list.pop(0) - - FILE_OUTPUT_NAME_DICT[key] = value - FILE_OUTPUT_CONVERT_DICT[key] = value2 - -video_option_setup_list = [ - # List of YouTube extractor (format) codes, based on the original list in - # youtube-dl-gui, and supplemented by this list: - # - # https://gist.github.com/sidneys/7095afe4da4ae58694d128b1034e01e2 - # - # Unfortunately, as of late September 2019, that list was already out of - # date - # Unfortunately, the list is YouTube-specific, and will not necessarily - # work on other websites - # - # I'm not sure about the meaning of some extractor codes; in those cases, - # I add the code itself to distinguish it from similar codes (e.g. - # compare codes 18 and 396) - # - # Dummy extractor codes - progressive scan resolutions - '144p', 'Any format [144p]', False, - '240p', 'Any format [240p]', False, - '360p', 'Any format [360p]', False, - '480p', 'Any format [480p]', False, - '720p', 'Any format [720p]', False, - '720p60', 'Any format [720p 60fps]', False, - '1080p', 'Any format [1080p]', False, - '1080p60', 'Any format [1080p 60fps]', False, - '1440p', 'Any format [1440p]', False, - '1440p60', 'Any format [1440p 60fps]', False, - '2160p', 'Any format [2160p]', False, - '2160p60', 'Any format [2160p 60fps]', False, - '4320p', 'Any format [4320p]', False, - '4320p60', 'Any format [4320p 60fps]', False, - # Dummy extractor codes - other - '3gp', '3gp', False, - 'flv', 'flv', False, - 'm4a', 'm4a', True, - 'mp4', 'mp4', False, - 'webm', 'webm', False, - # Real extractor codes - '17', '3gp [144p] <17>', False, - '36', '3gp [240p] <36>', False, - '5', 'flv [240p] <5>', False, - '6', 'flv [270p] <6>', False, - '34', 'flv [360p] <34>', False, - '35', 'flv [480p] <35>', False, - # v1.3.037 - not sure whether the HLS format codes should be added here, or - # not. 'hls' has not been added as a dummy extractor code because - # youtube-dl doesn't support that - '151', 'hls [72p] <151>', False, - '132', 'hls [240p] <132>', False, - '92', 'hls [240p] (3D) <92>', False, - '93', 'hls [360p] (3D) <93>', False, - '94', 'hls [480p] (3D) <94>', False, - '95', 'hls [720p] (3D) <95>', False, - '96', 'hls [1080p] <96>', False, - '139', 'm4a 48k (DASH Audio) <139>', True, - '140', 'm4a 128k (DASH Audio) <140>', True, - '141', 'm4a 256k (DASH Audio) <141>', True, - '18', 'mp4 [360p] <18>', False, - '22', 'mp4 [720p] <22>', False, - '37', 'mp4 [1080p] <37>', False, - '38', 'mp4 [4K] <38>', False, - '160', 'mp4 [144p] (DASH Video) <160>', False, - '133', 'mp4 [240p] (DASH Video) <133>', False, - '134', 'mp4 [360p] (DASH Video) <134>', False, - '135', 'mp4 [480p] (DASH Video) <135>', False, - '136', 'mp4 [720p] (DASH Video) <136>', False, - '298', 'mp4 [720p 60fps] (DASH Video) <298>', False, - '137', 'mp4 [1080p] (DASH Video) <137>', False, - '299', 'mp4 [1080p 60fps] (DASH Video) <299>', False, - '264', 'mp4 [1440p] (DASH Video) <264>', False, - '138', 'mp4 [2160p] (DASH Video) <138>', False, - '266', 'mp4 [2160p 60fps] (DASH Video) <266>', False, - '82', 'mp4 [360p] (3D) <82>', False, - '83', 'mp4 [480p] (3D) <83>', False, - '84', 'mp4 [720p] (3D) <84>', False, - '85', 'mp4 [1080p] (3D) <85>', False, - '394', 'mp4 [144p] <394>', False, - '395', 'mp4 [240p] <395>', False, - '396', 'mp4 [360p] <396>', False, - '397', 'mp4 [480p] <397>', False, - '398', 'mp4 [720p] <398>', False, - '399', 'mp4 [1080p] <399>', False, - '400', 'mp4 [1440p] <400>', False, - '401', 'mp4 [2160p] <401>', False, - '402', 'mp4 [2880p] <402>', False, - '43', 'webm [360p] <43>', False, - '44', 'webm [480p] <44>', False, - '45', 'webm [720p] <45>', False, - '46', 'webm [1080p] <46>', False, - '242', 'webm [240p] (DASH Video) <242>', False, - '243', 'webm [360p] (DASH Video) <243>', False, - '244', 'webm [480p] (DASH Video) <244>', False, - '247', 'webm [720p] (DASH Video) <247>', False, - '302', 'webm [720p 60fps] (DASH Video) <302>', False, - '248', 'webm [1080p] (DASH Video) <248>', False, - '303', 'webm [1080p 60fps] (DASH Video) <303>', False, - '271', 'webm [1440p] (DASH Video) <271>', False, - '308', 'webm [1440p 60fps] (DASH Video) <300>', False, - '313', 'webm [2160p] (DASH Video) <313>', False, - '315', 'webm [2160p 60fps] (DASH Video) <315>', False, - '272', 'webm [4320p] (DASH Video) <272>', False, - '100', 'webm [360p] (3D) <100>', False, - '101', 'webm [480p] (3D) <101>', False, - '102', 'webm [720p] (3D) <102>', False, - '330', 'webm [144p 60fps] (HDR) <330>', False, - '331', 'webm [240p 60fps] (HDR) <331>', False, - '332', 'webm [360p 60fps] (HDR) <332>', False, - '333', 'webm [480p 60fps] (HDR) <333>', False, - '334', 'webm [720p 60fps] (HDR) <334>', False, - '335', 'webm [1080p 60fps] (HDR) <335>', False, - '336', 'webm [1440p 60fps] (HDR) <336>', False, - '337', 'webm [2160p 60fps] (HDR) <337>', False, - '600', 'webm (36k Audio) <600>', True, - '249', 'webm (52k Audio) <249>', True, - '250', 'webm (64k Audio) <250>', True, - '251', 'webm (116k Audio) <251>', True, - '219', 'webm [144p] <219>', False, - '278', 'webm [144p] <278>', False, - '167', 'webm [360p] <167>', False, - '168', 'webm [480p] <168>', False, - '218', 'webm [480p] <218>', False, - '245', 'webm [480p] <245>', False, - '246', 'webm [480p] <246>', False, - '169', 'webm [1080p] <169>', False, - '171', 'webm 48k (DASH Audio) <171>', True, - '172', 'webm 256k (DASH Audio) <172>', True, -] - -VIDEO_OPTION_LIST = [] -VIDEO_OPTION_DICT = {} -VIDEO_OPTION_TYPE_DICT = {} - -while video_option_setup_list: - value = video_option_setup_list.pop(0) - key = video_option_setup_list.pop(0) - audio_only_flag = video_option_setup_list.pop(0) - - VIDEO_OPTION_LIST.append(key) - VIDEO_OPTION_DICT[key] = value - VIDEO_OPTION_TYPE_DICT[value] = audio_only_flag - -video_resolution_setup_list = [ - '144p', '144', - '240p', '240', - '360p', '360', - '480p', '480', - '720p', '720', - '720p60', '720', - '1080p', '1080', - '1080p60', '1080', - '1440p', '1440', - '1440p60', '1440', - '2160p', '2160', - '2160p60', '2160', - '4320p', '4320', - '4320p60', '4320', -] - -VIDEO_RESOLUTION_LIST = [] -VIDEO_RESOLUTION_DICT = {} -VIDEO_RESOLUTION_DEFAULT = '720p' - -while video_resolution_setup_list: - key = video_resolution_setup_list.pop(0) - value = video_resolution_setup_list.pop(0) - - VIDEO_RESOLUTION_LIST.append(key) - VIDEO_RESOLUTION_DICT[key] = value - -VIDEO_FPS_DICT = { - # Contains a subset of VIDEO_RESOLUTION_DICT. Only required to distinguish - # 30fps from 60fps formats - '720p60': '60', - '1080p60': '60', - '1440p60': '60', - '2160p60': '60', - '4320p60': '60', -} - -video_format_setup_list = ['mp4', 'flv', 'ogg', 'webm', 'mkv', 'avi'] - -VIDEO_FORMAT_LIST = [] -VIDEO_FORMAT_DICT = {} - -while video_format_setup_list: - key = value = video_format_setup_list.pop(0) - - VIDEO_FORMAT_LIST.append(key) - VIDEO_FORMAT_DICT[key] = value - -audio_setup_list = ['mp3', 'wav', 'aac', 'm4a', 'vorbis', 'opus', 'flac'] - -AUDIO_FORMAT_LIST = [] -AUDIO_FORMAT_DICT = {} - -while audio_setup_list: - key = value = audio_setup_list.pop(0) - - AUDIO_FORMAT_LIST.append(key) - AUDIO_FORMAT_DICT[key] = value - -FILE_SIZE_UNIT_LIST = [ - ['Bytes', ''], - ['Kilobytes', 'k'], - ['Megabytes', 'm'], - ['Gigabytes', 'g'], - ['Terabytes', 't'], - ['Petabytes', 'p'], - ['Exabytes', 'e'], - ['Zetta', 'z'], - ['Yotta', 'y'], -] - -# ISO 639-1 Language Codes -language_setup_list = [ - # English is top of the list, because it's the default setting in - # options.OptionsManager - 'English', 'en', - 'Abkhazian', 'ab', - 'Afar', 'aa', - 'Afrikaans', 'af', - 'Akan', 'ak', - 'Albanian', 'sq', - 'Amharic', 'am', - 'Arabic', 'ar', - 'Aragonese', 'an', - 'Armenian', 'hy', - 'Assamese', 'as', - 'Avaric', 'av', - 'Avestan', 'ae', - 'Aymara', 'ay', - 'Azerbaijani', 'az', - 'Bambara', 'bm', - 'Bashkir', 'ba', - 'Basque', 'eu', - 'Belarusian', 'be', - 'Bengali (Bangla)', 'bn', - 'Bihari', 'bh', - 'Bislama', 'bi', - 'Bosnian', 'bs', - 'Breton', 'br', - 'Bulgarian', 'bg', - 'Burmese', 'my', - 'Catalan', 'ca', - 'Chamorro', 'ch', - 'Chechen', 'ce', - 'Chichewa, Chewa, Nyanja', 'ny', - 'Chinese', 'zh', - 'Chinese (Simplified)', 'zh-Hans', - 'Chinese (Traditional)', 'zh-Hant', - 'Chuvash', 'cv', - 'Cornish', 'kw', - 'Corsican', 'co', - 'Cree', 'cr', - 'Croatian', 'hr', - 'Czech', 'cs', - 'Danish', 'da', - 'Divehi, Dhivehi, Maldivian', 'dv', - 'Dutch', 'nl', - 'Dzongkha', 'dz', - 'Esperanto', 'eo', - 'Estonian', 'et', - 'Ewe', 'ee', - 'Faroese', 'fo', - 'Fijian', 'fj', - 'Finnish', 'fi', - 'French', 'fr', - 'Fula, Fulah, Pulaar, Pular', 'ff', - 'Galician', 'gl', - 'Gaelic (Scottish)', 'gd', - 'Gaelic (Manx)', 'gv', - 'Georgian', 'ka', - 'German', 'de', - 'Greek', 'el', - 'Greenlandic, Kalaallisut', 'kl', - 'Guarani', 'gn', - 'Gujarati', 'gu', - 'Haitian Creole', 'ht', - 'Hausa', 'ha', - 'Hebrew', 'he', - 'Herero', 'hz', - 'Hindi', 'hi', - 'Hiri Motu', 'ho', - 'Hungarian', 'hu', - 'Icelandic', 'is', - 'Ido', 'io', - 'Igbo', 'ig', - 'Indonesian', 'id', - 'Interlingua', 'ia', - 'Interlingue', 'ie', - 'Inuktitut', 'iu', - 'Inupiak', 'ik', - 'Irish', 'ga', - 'Italian', 'it', - 'Japanese', 'ja', - 'Javanese', 'jv', - 'Kannada', 'kn', - 'Kanuri', 'kr', - 'Kashmiri', 'ks', - 'Kazakh', 'kk', - 'Khmer', 'km', - 'Kikuyu', 'ki', - 'Kinyarwanda (Rwanda)', 'rw', - 'Kirundi', 'rn', - 'Klingon', 'tlh', # Actually ISO 639-2 - 'Kyrgyz', 'ky', - 'Komi', 'kv', - 'Kongo', 'kg', - 'Korean', 'ko', - 'Kurdish', 'ku', - 'Kwanyama', 'kj', - 'Lao', 'lo', - 'Latin', 'la', - 'Latvian (Lettish)', 'lv', - 'Limburgish ( Limburger)', 'li', - 'Lingala', 'ln', - 'Lithuanian', 'lt', - 'Luga-Katanga', 'lu', - 'Luganda, Ganda', 'lg', - 'Luxembourgish', 'lb', - 'Macedonian', 'mk', - 'Malagasy', 'mg', - 'Malay', 'ms', - 'Malayalam', 'ml', - 'Maltese', 'mt', - 'Maori', 'mi', - 'Marathi', 'mr', - 'Marshallese', 'mh', - 'Moldavian', 'mo', - 'Mongolian', 'mn', - 'Nauru', 'na', - 'Navajo', 'nv', - 'Ndonga', 'ng', - 'Northern Ndebele', 'nd', - 'Nepali', 'ne', - 'Norwegian', 'no', - 'Norwegian bokmål', 'nb', - 'Norwegian nynorsk', 'nn', - 'Occitan', 'oc', - 'Ojibwe', 'oj', - 'Old Church Slavonic, Old Bulgarian', 'cu', - 'Oriya', 'or', - 'Oromo (Afaan Oromo)', 'om', - 'Ossetian', 'os', - 'Pāli', 'pi', - 'Pashto, Pushto', 'ps', - 'Persian (Farsi)', 'fa', - 'Polish', 'pl', - 'Portuguese', 'pt', - 'Punjabi (Eastern)', 'pa', - 'Quechua', 'qu', - 'Romansh', 'rm', - 'Romanian', 'ro', - 'Russian', 'ru', - 'Sami', 'se', - 'Samoan', 'sm', - 'Sango', 'sg', - 'Sanskrit', 'sa', - 'Serbian', 'sr', - 'Serbo-Croatian', 'sh', - 'Sesotho', 'st', - 'Setswana', 'tn', - 'Shona', 'sn', - 'Sichuan Yi, Nuoso', 'ii', - 'Sindhi', 'sd', - 'Sinhalese', 'si', - 'Swati, Siswati', 'ss', - 'Slovak', 'sk', - 'Slovenian', 'sl', - 'Somali', 'so', - 'Southern Ndebele', 'nr', - 'Spanish', 'es', - 'Sundanese', 'su', - 'Swahili (Kiswahili)', 'sw', - 'Swedish', 'sv', - 'Tagalog', 'tl', - 'Tahitian', 'ty', - 'Tajik', 'tg', - 'Tamil', 'ta', - 'Tatar', 'tt', - 'Telugu', 'te', - 'Thai', 'th', - 'Tibetan', 'bo', - 'Tigrinya', 'ti', - 'Tonga', 'to', - 'Tsonga', 'ts', - 'Turkish', 'tr', - 'Turkmen', 'tk', - 'Twi', 'tw', - 'Uyghur', 'ug', - 'Ukrainian', 'uk', - 'Urdu', 'ur', - 'Uzbek', 'uz', - 'Venda', 've', - 'Vietnamese', 'vi', - 'Volapük', 'vo', - 'Wallon', 'wa', - 'Welsh', 'cy', - 'Wolof', 'wo', - 'Western Frisian', 'fy', - 'Xhosa', 'xh', - 'Yiddish', 'yi', - 'Yoruba', 'yo', - 'Zhuang, Chuang', 'za', - 'Zulu', 'zu', -] - -LANGUAGE_CODE_LIST = [] -LANGUAGE_CODE_DICT = {} - -while language_setup_list: - key = language_setup_list.pop(0) - value = language_setup_list.pop(0) - - LANGUAGE_CODE_LIST.append(key) - LANGUAGE_CODE_DICT[key] = value - -if not xmas_flag: - DIALOGUE_ICON_DICT = { - 'system_icon': 'system_icon_64.png', - } -else: - DIALOGUE_ICON_DICT = { - 'system_icon': 'system_icon_xmas_64.png', - } - -if not xmas_flag: - STATUS_ICON_DICT = { - 'default_icon': 'status_default_icon_64.png', - 'check_icon': 'status_check_icon_64.png', - 'download_icon': 'status_download_icon_64.png', - 'update_icon': 'status_update_icon_64.png', - 'refresh_icon': 'status_refresh_icon_64.png', - 'info_icon': 'status_info_icon_64.png', - 'tidy_icon': 'status_tidy_icon_64.png', - } -else: - STATUS_ICON_DICT = { - 'default_icon': 'status_default_icon_xmas_64.png', - 'check_icon': 'status_check_icon_xmas_64.png', - 'download_icon': 'status_download_icon_xmas_64.png', - 'update_icon': 'status_update_icon_xmas_64.png', - 'refresh_icon': 'status_refresh_icon_xmas_64.png', - 'info_icon': 'status_info_icon_xmas_64.png', - 'tidy_icon': 'status_tidy_icon_xmas_64.png', - } - -TOOLBAR_ICON_DICT = { - 'tool_channel_large': 'channel_large.png', - 'tool_channel_small': 'channel_small.png', - 'tool_check_large': 'check_large.png', - 'tool_check_small': 'check_small.png', - 'tool_download_large': 'download_large.png', - 'tool_download_small': 'download_small.png', - 'tool_folder_large': 'folder_large.png', - 'tool_folder_small': 'folder_small.png', - 'tool_playlist_large': 'playlist_large.png', - 'tool_playlist_small': 'playlist_small.png', - 'tool_quit_large': 'quit_large.png', - 'tool_quit_small': 'quit_small.png', - 'tool_stop_large': 'stop_large.png', - 'tool_stop_small': 'stop_small.png', - 'tool_switch_large': 'switch_large.png', - 'tool_switch_small': 'switch_small.png', - 'tool_test_large': 'test_large.png', - 'tool_test_small': 'test_small.png', - 'tool_video_large': 'video_large.png', - 'tool_video_small': 'video_small.png', -} - -LARGE_ICON_DICT = { - 'video_both_large': 'video_both.png', - 'video_left_large': 'video_left.png', - 'video_none_large': 'video_none.png', - 'video_right_large': 'video_right.png', - - 'channel_both_large': 'channel_both.png', - 'channel_left_large': 'channel_left.png', - 'channel_none_large': 'channel_none.png', - 'channel_right_large': 'channel_right.png', - - 'playlist_both_large': 'playlist_both.png', - 'playlist_left_large': 'playlist_left.png', - 'playlist_none_large': 'playlist_none.png', - 'playlist_right_large': 'playlist_right.png', - - 'folder_both_large': 'folder_yellow_both.png', - 'folder_left_large': 'folder_yellow_left.png', - 'folder_none_large': 'folder_yellow_none.png', - 'folder_right_large': 'folder_yellow_right.png', - - 'folder_private_both_large': 'folder_red_both.png', - 'folder_private_left_large': 'folder_red_left.png', - 'folder_private_none_large': 'folder_red_none.png', - 'folder_private_right_large': 'folder_red_right.png', - - 'folder_fixed_both_large': 'folder_green_both.png', - 'folder_fixed_left_large': 'folder_green_left.png', - 'folder_fixed_none_large': 'folder_green_none.png', - 'folder_fixed_right_large': 'folder_green_right.png', - - 'folder_temp_both_large': 'folder_blue_both.png', - 'folder_temp_left_large': 'folder_blue_left.png', - 'folder_temp_none_large': 'folder_blue_none.png', - 'folder_temp_right_large': 'folder_blue_right.png', - - 'folder_no_parent_both_large': 'folder_black_both.png', - 'folder_no_parent_left_large': 'folder_black_left.png', - 'folder_no_parent_none_large': 'folder_black_none.png', - 'folder_no_parent_right_large': 'folder_black_right.png', - - 'copy_large': 'copy.png', - 'hand_left_large': 'hand_left.png', - 'hand_right_large': 'hand_right.png', - 'question_large': 'question.png', - 'warning_large': 'warning.png', -} - -SMALL_ICON_DICT = { - 'video_small': 'video.png', - 'channel_small': 'channel.png', - 'playlist_small': 'playlist.png', - 'folder_small': 'folder.png', - - 'archived_small': 'archived.png', - 'arrow_up_small': 'arrow_up.png', - 'arrow_down_small': 'arrow_down.png', - 'check_small': 'check.png', - 'download_small': 'download.png', - 'error_small': 'error.png', - 'folder_black_small': 'folder_black.png', - 'folder_blue_small': 'folder_blue.png', - 'folder_green_small': 'folder_green.png', - 'folder_red_small': 'folder_red.png', - 'have_file_small': 'have_file.png', - 'no_file_small': 'no_file.png', - 'system_error_small': 'system_error.png', - 'system_warning_small': 'system_warning.png', - 'warning_small': 'warning.png', -} - -if not xmas_flag: - WIN_ICON_LIST = [ - 'system_icon_16.png', - 'system_icon_24.png', - 'system_icon_32.png', - 'system_icon_48.png', - 'system_icon_64.png', - 'system_icon_128.png', - 'system_icon_256.png', - 'system_icon_512.png', - ] -else: - WIN_ICON_LIST = [ - 'system_icon_xmas_16.png', - 'system_icon_xmas_24.png', - 'system_icon_xmas_32.png', - 'system_icon_xmas_48.png', - 'system_icon_xmas_64.png', - 'system_icon_xmas_128.png', - 'system_icon_xmas_256.png', - 'system_icon_xmas_512.png', - ] diff --git a/tartube/info.py b/tartube/info.py deleted file mode 100755 index 2548f26..0000000 --- a/tartube/info.py +++ /dev/null @@ -1,461 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Info operation classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import GObject - - -# Import other modules -import os -import queue -import re -import signal -import subprocess -import threading - - -# Import our modules -import downloads -import utils - - -# Debugging flag (calls utils.debug_time at the start of every function) -DEBUG_FUNC_FLAG = False - - -# Classes - - -class InfoManager(threading.Thread): - - """Called by mainapp.TartubeApp.info_manager_start(). - - Python class to create a system child process, to do one of three jobs: - - 1. Fetch a list of available formats for a video, directly from youtube-dl - - 2. Fetch a list of available subtitles for a video, directly from - youtube-dl - - 3. Test youtube-dl with specified download options; everything is - downloaded into a temporary folder - - 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 - - info_type (str): The type of information to fetch: 'formats' for a list - of video formats, 'subs' for a list of subtitles, or 'test_ytdl' - to test youtube-dl with specified options - - media_data_obj (media.Video): For 'formats' and 'subs', the media.Video - object for which formats/subtitles should be fetched. For - 'test_ytdl', set to None - - url_string (str): For 'test_ytdl', the video URL to download (can be - None or an empty string, if no download is required, for example - 'youtube-dl --version'. For 'formats' and 'subs', set to None - - options_string (str): For 'test_ytdl', a string containing one or more - youtube-dl download options. The string, generated by a - Gtk.TextView, typically contains newline and/or multiple whitespace - characters; the info.InfoManager code deals with that. Can be None - or an empty string, if no download options are required. For - 'formats' and 'subs', set to None - - """ - - - # Standard class methods - - - def __init__(self, app_obj, info_type, media_data_obj, url_string, - options_string): - - if DEBUG_FUNC_FLAG: - utils.debug_time('iop 100 __init__') - - super(InfoManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The video for which information will be fetched (None if - # self.info_type is 'test_ytdl') - self.video_obj = media_data_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 = downloads.PipeReader(self.stdout_queue) - self.stderr_reader = downloads.PipeReader(self.stderr_queue) - - # The child process created by self.create_child_process() - self.child_process = None - - - # IV list - other - # --------------- - # The type of information to fetch: 'formats' for a list of video - # formats, 'subs' for a list of subtitles, or 'test_ytdl' to test - # youtube-dl with specified options - self.info_type = info_type - # For 'test_ytdl', the video URL to download (can be None or an empty - # string, if no download is required, for example - # 'youtube-dl --version'. For 'formats' and 'subs', set to None - self.url_string = url_string - # For 'test_ytdl', a string containing one or more youtube-dl download - # options. The string, generated by a Gtk.TextView, typically - # contains newline and/or multiple whitespace characters; the - # info.InfoManager code deals with that. Can be None or an empty - # string, if no download options are required. For 'formats' and - # 'subs', set to None - self.options_string = options_string - - # Flag set to True if the info operation succeeds, False if it fails - self.success_flag = False - - # The list of formats/subtitles extracted from STDOUT - self.output_list = [] - - # (For debugging purposes, store any STDOUT/STDERR messages received; - # otherwise we would just set a flag if a STDERR message was - # received) - self.stdout_list = [] - self.stderr_list = [] - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Creates a child process to run the youtube-dl system command. - - Reads from the child process STDOUT and STDERR, and calls the main - application with the result of the process (success or failure). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('iop 178 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' - - else: - if self.info_type == 'formats': - msg += 'fetching list of video/audio formats' - else: - msg += 'fetching list of subtitles' - - msg += ' for \'' + self.video_obj.name + '\'' - - self.app_obj.main_win_obj.output_tab_write_stdout(1, msg) - - # Convert a path beginning with ~ (not on MS Windows) - ytdl_path = self.app_obj.ytdl_path - if os.name != 'nt': - ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path) - - # Prepare the system command - if self.info_type == 'formats': - - cmd_list = [ - ytdl_path, - '--list-formats', - self.video_obj.source, - ] - - elif self.info_type == 'subs': - - cmd_list = [ - ytdl_path, - '--list-subs', - self.video_obj.source, - ] - - else: - - cmd_list = [ytdl_path] - - if self.options_string is not None \ - and self.options_string != '': - - # Parse the string into a list. It was obtained from a - # Gtk.TextView, so it can contain newline and/or multiple - # whitepsace characters. Whitespace characters within - # double quotes "..." must be preserved - option_list = utils.parse_ytdl_options(self.options_string) - for item in option_list: - cmd_list.append(item) - - if self.url_string is not None \ - and self.url_string != '': - - cmd_list.append('-o') - cmd_list.append( - os.path.join( - self.app_obj.temp_test_dir, - '%(title)s.%(ext)s', - ), - ) - - cmd_list.append(self.url_string) - - # Create the new child process - self.create_child_process(cmd_list) - - # Show the system command in the Output Tab - space = ' ' - self.app_obj.main_win_obj.output_tab_write_system_cmd( - 1, - space.join(cmd_list), - ) - - # So that we can read from the child process STDOUT and STDERR, attach - # 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, - ) - - while self.is_child_process_alive(): - - # Read from the child process STDOUT, and convert into unicode for - # Python's convenience - while not self.stdout_queue.empty(): - - stdout = self.stdout_queue.get_nowait().rstrip() - if stdout: - - if os.name == 'nt': - stdout = stdout.decode('cp1252') - else: - stdout = stdout.decode('utf-8') - - self.output_list.append(stdout) - self.stdout_list.append(stdout) - - # Show command line output in the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - stdout, - ) - - # The child process has finished - while not self.stderr_queue.empty(): - - # Read from the child process STDERR queue (we don't need to read - # it in real time), and convert into unicode for python's - # convenience - stderr = self.stderr_queue.get_nowait().rstrip() - if os.name == 'nt': - stderr = stderr.decode('cp1252') - else: - stderr = stderr.decode('utf-8') - - if stderr: - - # While testing youtube-dl, don't treat anything as an error - if self.info_type == 'test_ytdl': - self.stdout_list.append(stderr) - - # When fetching subtitles from a video that has none, don't - # treat youtube-dl WARNING: messages as something that - # makes the info operation fail - elif self.info_type == 'subs': - - if not re.match('WARNING\:', stderr): - self.stderr_list.append(stderr) - - # When fetching formats, recognise all warnings as errors - else: - self.stderr_list.append(stderr) - - # Show command line output in the Output Tab - self.app_obj.main_win_obj.output_tab_write_stderr( - 1, - stderr, - ) - - # (Generate our own error messages for debugging purposes, in certain - # situations) - if self.child_process is None: - - msg = 'youtube-dl process did not start' - self.stderr_list.append(msg) - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - msg, - ) - - elif self.child_process.returncode > 0: - - msg = 'Child process exited with non-zero code: {}'.format( - self.child_process.returncode, - ) - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - msg, - ) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.info_manager_finished - if not self.stderr_list: - self.success_flag = True - - # Show a confirmation in the the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - 'Info operation finished', - ) - - # Let the timer run for a few more seconds to prevent Gtk errors (for - # systems with Gtk < 3.24) - GObject.timeout_add( - 0, - self.app_obj.info_manager_halt_timer, - ) - - - def create_child_process(self, cmd_list): - - """Called by self.run(). - - Based on code from downloads.VideoDownloader.create_child_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. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('iop 382 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, - ) - - 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') - - - def is_child_process_alive(self): - - """Called by self.run() and .stop_info_operation(). - - Based on code from downloads.VideoDownloader.is_child_process_alive(). - - Called continuously during the self.run() 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('iop 427 is_child_process_alive') - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def stop_info_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Based on code from downloads.VideoDownloader.stop(). - - Terminates the child process. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('iop 446 stop_info_operation') - - 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) diff --git a/tartube/mainapp.py b/tartube/mainapp.py deleted file mode 100755 index 710963a..0000000 --- a/tartube/mainapp.py +++ /dev/null @@ -1,13890 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Main application class.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, GdkPixbuf - - -# Import Python standard modules -from gi.repository import Gio -import datetime -import json -import math -import os -import pickle -import re -import shutil -import sys -import threading -import time - - -# Import other Python modules -try: - import moviepy.editor - HAVE_MOVIEPY_FLAG = True -except: - HAVE_MOVIEPY_FLAG = False - -if os.name != 'nt': - try: - from xdg_tartube import XDG_CONFIG_HOME - HAVE_XDG_FLAG = True - except: - HAVE_XDG_FLAG = False -else: - HAVE_XDG_FLAG = False - -# Import our modules -import __main__ -import config -import dialogue -import downloads -import files -import formats -import info -import mainwin -import media -import options -import refresh -import testing -import tidy -import updates -import utils - - -# Debugging flag (calls utils.debug_time at the start of every function) -DEBUG_FUNC_FLAG = False -# ...(but don't call utils.debug_time from the timer functions such as -# self.script_slow_timer_callback() ) -DEBUG_NO_TIMER_FUNC_FLAG = False - - -# Classes - - -class TartubeApp(Gtk.Application): - - """Main python class for the Tartube application.""" - - - # Standard class methods - - - def __init__(self, *args, **kwargs): - - if DEBUG_FUNC_FLAG: - utils.debug_time('ap 98 __init__') - - super(TartubeApp, self).__init__( - *args, - application_id=None, - flags=Gio.ApplicationFlags.FLAGS_NONE, - **kwargs) - - # Debugging flags - # --------------- - # After installation, don't show the dialogue windows prompting the - # user to choose Tartube's data directory; just use the default - # location - self.debug_no_dialogue_flag = False - # When loading a config/database file, if a lockfile is present, load - # the config/database file anyway (i.e., ignore lockfiles) - self.debug_ignore_lockfile_flag = False - # In the main window's menu, show a menu item for adding a set of - # media data objects for testing - self.debug_test_media_menu_flag = False - # In the main window's toolbar, show a toolbar item for adding a set of - # media data objects for testing - self.debug_test_media_toolbar_flag = False - # 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 - self.debug_open_pref_win_flag = False - # Automatically open the general download options window on startup - self.debug_open_options_win_flag = False - # Hide all the system folders (this is not reversible by setting the - # flag back to False) - self.debug_hide_folders_flag = False - - - # Instance variable (IV) list - class objects - # ------------------------------------------- - # The main window object, set as soon as it's created - self.main_win_obj = None - # The system tray icon (a mainapp.StatusIcon object, inheriting from - # Gtk.StatusIcon) - self.status_icon_obj = None - # - # At the moment, there are five operations - the download, update, - # refresh, info and tidy operations - # Only one operation can be in progress at a time. When an operation is - # in progress, many functions (such as opening configuration windows) - # are not possible - # - # A download operation is handled by a downloads.DownloadManager - # object. It downloads files from a server (for example, it downloads - # videos from YouTube) - # Although its not possible to run more than one download - # operation at a time, a single download operation can handle - # multiple simultaneous downloads - # The current downloads.DownloadManager object, if a download operation - # is in progress (or None, if not) - self.download_manager_obj = None - # An update operation (to update youtube-dl) is handled by an - # updates.UpdateManager object. It updates youtube-dl to the latest - # version - # The current updates.UpdateManager object, if an upload operation is - # in progress (or None, if not) - self.update_manager_obj = None - # A refresh operation compares the media registry with the contents of - # Tartube's data directories, adding new videos to the media registry - # and marking missing videos as not downloaded, as appropriate - # The current refresh.RefreshManager object, if a refresh operation is - # in progress (or None, if not) - self.refresh_manager_obj = None - # An info operation fetches information about a particular video; - # currently, its available formats and available subtitles - # The current info.InfoManager object, if an info operation is in - # progress (or None, if not) - self.info_manager_obj = None - # A tidy operation can check that videos still exist and aren't - # corrupted, or can remove all videos, or all thumbnails, and so on - # The current tidy.TidyManager object, if a tidy operation is in - # progress (or None, if not) - self.tidy_manager_obj = None - # - # When any operation is in progress, the manager object is stored here - # (so code can quickly check if an operation is in progress, or not) - self.current_manager_obj = None - # - # The file manager, files.FileManager, for loading thumbnail, icon - # and JSON files safely (i.e. without causing a Gtk crash) - self.file_manager_obj = files.FileManager() - # The message dialogue manager, dialogue.DialogueManager, for showing - # message dialogue windows safely (i.e. without causing a Gtk crash) - self.dialogue_manager_obj = None - # - # Media data classes are those specified in media.py. Those class - # objects are media.Video (for individual videos), media.Channel, - # media.Playlist and media.Folder (reprenting a sub-directory inside - # Tartube's data directory) - # Some media data objects have a list of children which are themselves - # media data objects. In that way, the user can organise their videos - # in convenient folders - # media.Folder objects can have any media data objects as their - # children (including other media.Folder objects). media.Channel and - # media.Playlist objects can have media.Video objects as their - # children. media.Video objects don't have any children - # (Media data objects are stored in IVs below) - # - # During a download operation, youtube-dl is supplied with a set of - # download options. Those options are specified by an - # options.OptionsManager object - # Each media data object may have its own options.OptionsManager - # object. If not, it uses the options.OptionsManager object of its - # parent (or of its parent's parent, and so on) - # If this chain of family relationships doesn't provide an - # options.OptionsManager object, then this default object, known as - # the General Options Manager, is used - self.general_options_obj = None - - - # Instance variable (IV) list - other - # ----------------------------------- - # Default window sizes (in pixels) - self.main_win_width = 800 - self.main_win_height = 600 - self.config_win_width = 650 - self.config_win_height = 450 - self.paned_min_size = 200 - # Default size (in pixels) of space between various widgets - self.default_spacing_size = 5 - - # Custom window sizes - # Flag set to True if Tartube should remember the main window size - # when saving the config file, and then use that size when - # re-starting tartube - self.main_win_save_size_flag = False - # The size of the main window, when the config file was last saved... - self.main_win_save_width = self.main_win_width - self.main_win_save_height = self.main_win_height - # ...and the position of the divider separating the Video Index and - # Video Catalogue in the Videos tab (the default value is also the - # minimum value saved) - self.main_win_save_posn = self.paned_min_size - - # The current Gtk version - self.gtk_version_major = Gtk.get_major_version() - self.gtk_version_minor = Gtk.get_minor_version() - self.gtk_version_micro = Gtk.get_micro_version() - # 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 (mostly) fixed by Gtk v3.24.* - # Flag set to True by self.start() if Tartube is being run before - # Gtk v3.24, in which case some cosmetic functions (mostly related - # to sorting the Video Index and Video Catalogue) are disabled - self.gtk_broken_flag = False - # The flag above is set automatically, but the user can set this flag - # themselves. If True, Tartube behaves as if self.gtk_broken_flag was - # set (on all systems) - if os.name != 'nt': - self.gtk_emulate_broken_flag = True - else: - self.gtk_emulate_broken_flag = False - - # IVs used to place a lock on the loaded database file, so that - # competing instances of Tartube don't try to use it at the same time - # Time to wait (in seconds) to save the config file, if a lockfile - # exists for it - self.config_lock_time = 5 - # The path to the database lockfile created by this instance of - # Tartube (None if no lockfile has been created) - self.db_lock_file_path = None - - # At all times (after initial setup), two GObject timers run - a fast - # one and a slow one - # The slow timer's ID - self.script_slow_timer_id = None - # The slow timer interval time (in milliseconds) - self.script_slow_timer_time = 60000 - # The fast timer's ID - self.script_fast_timer_id = None - # The fast timer interval time (in milliseconds) - self.script_fast_timer_time = 1000 - - # Flag set to True if the main toolbar should be compressed (by - # removing the labels); ideal if the toolbar's contents won't fit in - # the standard-sized window (as it almost certainly won't on MS - # Windows) - if os.name != 'nt': - self.toolbar_squeeze_flag = False - else: - self.toolbar_squeeze_flag = True - # Flag set to True if tooltips should be visible in the Video Index - # and the Video Catalogue - self.show_tooltips_flag = True - # Flag set to True if small icons should be used in the Video Index, - # False if large icons should be used - self.show_small_icons_in_index = False - # Flag set to True if the Video Index treeview should auto-expand - # when an item is clicked, to show its children (only folders - # have children visible in the Video Index, though) - self.auto_expand_video_index_flag = False - # Flag set to True if the 'Download all' buttons in the main window - # toolbar and in the Videos tab should be disabled (in case the u - # user is sure they only want to do simulated downloads) - self.disable_dl_all_flag = False - # Flag set to True if we should use 'Today' and 'Yesterday' in the - # Video Index, rather than a date - self.show_pretty_dates_flag = True - - # Flag set to True if an icon should be displayed in the system tray - self.show_status_icon_flag = True - # Flag set to True if the main window should close to the tray, rather - # than halting the application altogether. Ignore if - # self.show_status_icon_flag is False - self.close_to_tray_flag = True - - # Flag set to True if rows in the Progress List should be hidden once - # the download operation has finished with the corresponding media - # data object (so the user can see the media data objects currently - # being downloaded more easily) - self.progress_list_hide_flag = False - # Flag set to True if new rows should be added to the Results List - # at the top, False if they should be added at the bottom - self.results_list_reverse_flag = False - - # Flag set to True if system error messages should be shown in the - # Errors/Warnings tab - self.system_error_show_flag = True - # Flag set to True if system warning messages should be shown in the - # Errors/Warnings tab - self.system_warning_show_flag = True - # Flag set to True if operation error messages should be shown in the - # Errors/Warnings tab - self.operation_error_show_flag = True - # Flag set to True if operation warning messages should be shown in the - # Errors/Warnings tab - self.operation_warning_show_flag = True - # Flag set to True if the total number of system error/warning messages - # shown in the tab label is not reset until the 'Clear the list' - # button is explicitly clicked (normally, the total numbers are - # reset when the user switches to a different tab) - self.system_msg_keep_totals_flag = False - - # For quick lookup, the directory in which the 'tartube' executable - # file is found, and its parent directory - self.script_dir = sys.path[0] - self.script_parent_dir = os.path.abspath( - os.path.join(self.script_dir, os.pardir), - ) - - # Tartube's data directory (platform-dependant), 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 - self.default_data_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - ), - ) - self.data_dir = self.default_data_dir - # A list of data directories used recently by the user. The list - # includes the current value of self.data_dir, and can be - # customised by the user (to forget directories no longer needed) - # Multiple instances of Tartube can use the same config file, but - # they cannot use the same database file at the same time - # When Tartube starts, if the database file in the directory - # self.data_dir is locked, Tartube will try other directories in this - # list, in order, until finding one that isn't locked - self.data_dir_alt_list = [ self.data_dir ] - # self.data_dir records the path to the database file that was in - # memory, when the config file was last saved. Flag set to False to - # use this path (meaning that, on startup, the same database file is - # loaded), or True if the first path in self.data_dir_alt_list is - # loaded instead - self.data_dir_use_first_flag = True - # On startup (but not when switching databases), if the database file - # in self.data_dir is locked, when this flag is True Tartube will try - # other directories in self.data_dir_alt_list (as described above). - # If False, only self.data_dir is tried - self.data_dir_use_list_flag = True - # When switching to a new database file, the data directory (containing - # the file) is added to the list, if the flag it True - self.data_dir_add_from_list_flag = True - - # The data directory is structured like this: - # /tartube-data - # tartube.db [the Tartube database file] - # /.backups - # tartube_BU.db [any number of database file backups] - # /.temp [temporary directory, deleted on startup] - # /pewdiepie [example of a custom media.Channel] - # /Temporary Videos [standard media.Folder] - # /Unsorted Videos [standard media.Folder] - # Before v1.3.099, the data directory was structured like this: - # /tartube-data - # tartube.db - # tartube_BU.db - # /.temp - # /downloads - # /pewdiepie - # /Temporary Videos - # /Unsorted Videos - # Tartube can read from both stcuctures although, when creating a new - # data directory, only the new structure is created - # - # The sub-directory into which videos are downloaded (new and old - # style) - self.downloads_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - ), - ) - self.alt_downloads_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - 'downloads', - ), - ) - # A hidden directory, used for storing backups of the Tartube database - # file - self.backup_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - '.backups', - ), - ) - - # A temporary directory, deleted when Tartube starts and stops - self.temp_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - '.temp', - ), - ) - # Inside the temporary directory, a downloads folder, replicating the - # layout of self.downloads_dir, and used for storing description, - # JSON and thumbnail files which the user doesn't want to store in - # self.downloads_dir - self.temp_dl_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - '.temp', - 'downloads', - ), - ) - # Inside the temporary directory, a test folder into which an info - # operation can allow youtube-dl to download files - self.temp_test_dir = os.path.abspath( - os.path.join( - os.path.expanduser('~'), - __main__.__packagename__ + '-data', - '.temp', - 'ytdl-test', - ), - ) - - # 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 - # whether XDG is available, or not - self.config_file_dir = os.path.abspath(self.script_parent_dir) - self.config_file_path = os.path.abspath( - os.path.join(self.script_parent_dir, self.config_file_name), - ) - - if not HAVE_XDG_FLAG: - self.config_file_xdg_dir = None - self.config_file_xdg_path = None - else: - self.config_file_xdg_dir = os.path.abspath( - os.path.join( - XDG_CONFIG_HOME, - __main__.__packagename__, - ), - ) - - self.config_file_xdg_path = os.path.abspath( - os.path.join( - XDG_CONFIG_HOME, - __main__.__packagename__, - self.config_file_name, - ), - ) - - # Name of the Tartube database file (storing media data objects). The - # database file is always found in self.data_dir - self.db_file_name = __main__.__packagename__ + '.db' - # Names of the database export files (one for JSON, for for plain text) - self.export_json_file_name \ - = __main__.__packagename__ + '_db_export.json' - self.export_text_file_name \ - = __main__.__packagename__ + '_db_export.txt' - # How Tartube should make backups of its database file: - # 'default' - make a backup file during a save procedure, but delete - # it when the save procedure is complete - # 'single' - make a backup file during a save procedure, replacing - # any existing backup file, and don't delete it when the save - # procedure is complete - # 'daily' - make a backup file once per day, the first time a save - # procedure is performed in that day. The file is labelled with - # the date, so backup files from previous days are not - # overwritten - # 'always' - always make a backup file, labelled with the date and - # time, so that no backup file is ever overwritten - self.db_backup_mode = 'single' - # If loading/saving of a config or database file fails, this flag is - # set to True, which disables all loading/saving for the rest of the - # session - self.disable_load_save_flag = False - # Optional error message generated when self.disable_load_save_flag - # was set to True - self.disable_load_save_msg = None - # If loading a database file (only) fails because of a lock file, this - # flag is set to True, so the user is prompted to remove the possibly - # stale lock file. If the user declines, the error message stored in - # self.disable_load_save_msg is then displayed - self.disable_load_save_lock_flag = False - # Users have reported that the Tartube database file was corrupted. On - # inspection, it was almost completely empty, presumably because - # self.save_db had been called before .load_db - # As the corruption was catastrophic, make sure that can never happen - # again with this flag, set to False until the code has either - # 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 - # 'youtube-dl.exe', depending on the platform. The default value is - # set by self.start() - self.ytdl_bin = None - # The default path to the youtube-dl binary. The value is set by - # self.start(). On MSWin, it is 'youtube-dl.exe'. On Linux, it is - # '/usr/bin/youtube-dl' - self.ytdl_path_default = None - # The path to the youtube-dl binary, after installation using PyPI. - # Not used on MS Windows. The initial ~ character must be substituted - # for os.path.expanduser('~'), before use - self.ytdl_path_pypi = '~/.local/bin/youtube-dl' - # The actual path to use in the shell command during a download or - # update operation. Initially given the same value as - # self.ytdl_path_default - # On MSWin, this value doesn't change. On Linux, depending on how - # youtube-dl was installed, it might be '/usr/bin/youtube-dl' or just - # 'youtube-dl' - 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() - # Dictionary in the form - # key: description of the update method - # 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 - # combobox in config.SystemPrefWin is in a standard order) - self.ytdl_update_list = [] - # The user's choice of shell command; one of the keys in - # self.ytdl_update_dict, set by self.start() - self.ytdl_update_current = None - - # Flag set to True if youtube-dl system commands should be displayed in - # the Output Tab - self.ytdl_output_system_cmd_flag = True - # Flag set to True if youtube-dl's STDOUT should be displayed in the - # Output Tab - self.ytdl_output_stdout_flag = True - # Flag set to True if we should ignore JSON output when displaying text - # in the Output Tab (ignored if self.ytdl_output_stdout_flag is - # False) - self.ytdl_output_ignore_json_flag = True - # Flag set to True if we should ignore download progress (as a - # percentage) when displaying text in the Output Tab (ignored if - # self.ytdl_output_stdout_flag is False) - self.ytdl_output_ignore_progress_flag = True - # Flag set to True if youtube-dl's STDERR should be displayed in the - # Output Tab - self.ytdl_output_stderr_flag = True - # Flag set to True if pages in the Output Tab should be emptied at the - # start of each operation - self.ytdl_output_start_empty_flag = True - # Flag set to True if a summary page should be visible in the Output - # Tab. Changes to this flag are applied when Tartube restarts - self.ytdl_output_show_summary_flag = False - - # Flag set to True if youtube-dl system commands should be written to - # the terminal window - self.ytdl_write_system_cmd_flag = False - # Flag set to True if youtube-dl's STDOUT should be written to the - # terminal window - self.ytdl_write_stdout_flag = False - # Flag set to True if we should ignore JSON output when writing to the - # terminal window (ignored if self.ytdl_write_stdout_flag is False) - self.ytdl_write_ignore_json_flag = True - # Flag set to True if we should ignore download progress (as a - # percentage) when writing to the terminal window (ignored if - # self.ytdl_write_stdout_flag is False) - self.ytdl_write_ignore_progress_flag = True - # Flag set to True if youtube-dl's STDERR should be written to the - # terminal window - self.ytdl_write_stderr_flag = False - - # Flag set to True if youtube-dl should show verbose output (using the - # --verbose option). The setting applies to both the Output Tab and - # the terminal window - self.ytdl_write_verbose_flag = False - - # Flag set to True if, during a refresh operation, videos should be - # displayed in the Output Tab. Set to False if only channels, - # playlists and folders should be displayed there - self.refresh_output_videos_flag = True - # Flag set to True if, during a refresh operation, non-matching videos - # should be displayed in the Output Tab. Set to False if only - # matching videos should be displayed there. Ignore if - # self.refresh_output_videos_flag is False - self.refresh_output_verbose_flag = False - # The moviepy module hangs indefinitely, if it is used to open a - # corrupted video file - # (see https://github.com/Zulko/moviepy/issues/639) - # To counter this, self.update_video_from_filesystem() moves the - # procedure into a thread, and applies a timeout to that thread - # The timeout (in seconds) to apply. Must be an integer, 0 or above. - # If 0, the moviepy procedure is allowed to hang indefinitely - self.refresh_moviepy_timeout = 10 - - # Path to the ffmpeg/avconv binary (or the directory containing the - # binary). If set to any value besides None, - # downloads.VideoDownloader will pass the value to youtube-dl using - # its --ffmpeg-location option - self.ffmpeg_path = None - - # Flag set to True if the General Options Manager - # (self.general_options_obj) should be cloned whenever the user - # applies a new options manager to a media data object (e.g. by - # right-clicking a channel in the Video Index, and selecting - # Downloads > Apply options manager) - self.auto_clone_options_flag = True - - # 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 - # files at regular intervals, too - # The timer's ID (None when no timer is running) - self.dl_timer_id = None - # The timer interval time (in milliseconds) - self.dl_timer_time = 500 - # At the end of the download operation, the timer continues running for - # a few seconds, to give new files a chance to appear in the - # filesystem. The maximum time to wait (in seconds) - self.dl_timer_final_time = 10 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.dl_timer_check_time = None - - # During a download operation, we periodically check whether the device - # containing self.data_dir is running out of space - # The check interval time (in seconds) - self.dl_timer_disk_space_time = 60 - # The time (matchs time.time()) at which the next check takes place - self.dl_timer_disk_space_check_time = None - - # Flag set to True if Tartube should warn if the system is running out - # of disk space (on the drive containing self.data_dir), False if - # not. The warning is issued at the start of a download operation - self.disk_space_warn_flag = True - # The amount of free disk space (in Mb) below which the warning is - # issued. If 0, no warning is issued. Ignored if - # self.disk_space_warn_flag is False - self.disk_space_warn_limit = 1000 - # Flag set to True if Tartube should refuse to start a download - # operation, and halt an existing download operation, if the system - # is running out of disk space (on the drive containing - # self.data_dir), False if not - self.disk_space_stop_flag = True - # The amount of free disk space (in Mb) below which the refusal/halt - # is enacted. If 0, a download operation will continue downloading - # files until the device actually runs out of space. Ignored if - # self.disk_space_stop_flag is False - self.disk_space_stop_limit = 500 - # The IVs above can be set to any number (0 or above), but the - # Gtk.SpinButtons in the system preferences window increment/ - # decrement the value by this many Mb at a time - self.disk_space_increment = 100 - # An absolute minimum of disk space, below which a download operation - # will not start, or will halt, regardless of the values of the IVs - # above (in Mb) - self.disk_space_abs_limit = 50 - - # Custom download operation settings - # If True, during a custom download, download every video which is - # marked as not downloaded (often after a 'Check all' operation); - # don't download channels/playlists directly - self.custom_dl_by_video_flag = False - # During a custom download, any videos whose source URL is YouTube can - # be diverted to another website - # 'default' - Use the original YouTube URL - # 'hooktube' - Divert to hooktube.com - # 'invidious' - Divert to invidio.us - self.custom_dl_divert_mode = 'default' - # If True, during a custom download, a delay (in minutes) is applied - # between media data object downloads. When applied to a - # channel/playlist, the delay occurs after the whole channel/ - # playlist. When applied directly to videos, the delay occurs after - # each video - self.custom_dl_delay_flag = False - # The maximum delay to apply (in minutes, minimum value 0.2). - # Ignored if self.custom_dl_delay_flag is False - self.custom_dl_delay_max = 5 - # The minimum delay to apply (in minutes, minimum value 0, maximum - # value self.custom_dl_delay_max). If specified, the delay is a - # random length of time between this value and - # self.custom_dl_delay_max. Ignored if self.custom_dl_delay_flag is - # False - self.custom_dl_delay_min = 0 - - # During an update operation, a separate GObject timer runs, so that - # the Output Tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.update_timer_id = None - # The timer interval time (in milliseconds) - self.update_timer_time = 500 - # At the end of the update operation, the timer continues running for - # a few seconds, to prevent various Gtk errors (and occasionally - # crashes) for systems with Gtk < 3.24. The maximum time to wait (in - # seconds) - self.update_timer_final_time = 5 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.update_timer_check_time = None - - # During a refresh operation, a separate GObject timer runs, so that - # the Output Tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.refresh_timer_id = None - # The timer interval time (in milliseconds) - self.refresh_timer_time = 500 - # At the end of the refresh operation, the timer continues running for - # a few seconds, to prevent various Gtk errors (and occasionally - # crashes) for systems with Gtk < 3.24. The maximum time to wait (in - # seconds) - self.refresh_timer_final_time = 5 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.refresh_timer_check_time = None - - # During an info operation, a separate GObject timer runs, so that - # the Output Tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.info_timer_id = None - # The timer interval time (in milliseconds) - self.info_timer_time = 500 - # At the end of the info operation, the timer continues running for - # a few seconds, to prevent various Gtk errors (and occasionally - # crashes) for systems with Gtk < 3.24. The maximum time to wait (in - # seconds) - # (Shorter wait time than other operations, because this type of - # operation finishes quickly) - self.info_timer_final_time = 2 - # Once that extra time has been applied, the time (matches time.time()) - # at which to stop waiting - self.info_timer_check_time = None - - # During a tidy operation, a separate GObject timer runs, so that - # the Output Tab can be updated at regular intervals - # The timer's ID (None when no timer is running) - self.tidy_timer_id = None - # The timer interval time (in milliseconds) - self.tidy_timer_time = 500 - # At the end of the tidy operation, the timer continues running for - # a few seconds, to prevent various Gtk errors (and occasionally - # crashes) for systems with Gtk < 3.24. The maximum time to wait (in - # seconds) - # (Shorter wait time than other operations, because this type of - # operation might finish quickly) - self.tidy_timer_final_time = 2 - # Once that extra time has been applied, the time (matches time.time()) - # 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 - self.operation_halted_flag = False - # During a download operation, a flag set to True if Tartube must shut - # down when the operation is finished - self.halt_after_operation_flag = False - # During a download operation, a flag set to True if no dialogue - # window must be shown at the end of that operation (but not - # necessarily any future download operations) - self.no_dialogue_this_time_flag = False - - # For a channel/playlist containing hundreds (or more!) videos, a - # download operation will take a very long time, even though we might - # only want to check for new videos - # Flag set to True if the download operation should give up checking a - # channel or playlist when its starts receiving details of videos - # about which it already knows (from a previous download operation) - # This works well if the website sends video in order, youngest first - # (as YouTube does), but won't work at all otherwise - self.operation_limit_flag = False - # During simulated video downloads (e.g. after clicking the 'Check all' - # button), stop checking the channel/playlist after receiving details - # for this many videos, when a media.Video object exists for them - # and the object's .file_name and .name IVs are set - # Must be an positive integer or 0. If 0, no limit applies. Ignored if - # self.operation_limit_flag is False - self.operation_check_limit = 3 - # During actual video downloads (e.g. after clicking the 'Download all' - # button), stop downloading the channel/playlist after receiving - # this many 'video already downloaded' messages, when a media.Video - # objects exists for them and the object's .dl_flag is set - # Must be an positive integer or 0. If 0, no limit applies. Ignored if - # self.operation_limit_flag is False - self.operation_download_limit = 3 - - # The media data registry - # Every media data object has a unique .dbid (which is an integer). The - # number of media data objects ever created (including any that have - # been deleted), used to give new media data objects their .dbid - self.media_reg_count = 0 - # A dictionary containing all media data objects (but not those which - # have been deleted) - # Dictionary in the form - # key = media data object's unique .dbid - # value = the media data object itself - self.media_reg_dict = {} - # media.Channel, media.Playlist and media.Folder objects must have - # unique .name IVs - # (A channel and a playlist can't have the same name. Videos within a - # single channel, playlist or folder can't have the same name. - # Videos with different parent objects CAN have the same name) - # A dictionary used to check that media.Channel, media.Playlist and - # media.Folder objects have unique .name IVs (and to look up names - # quickly) - # Dictionary in the form - # key = media data object's .name - # value = media data object's unique .dbid - self.media_name_dict = {} - # An ordered list of media.Channel, media.Playlist and media.Folder - # objects which have no parents (in the order they're displayed) - # This list, combined with each media data object's child list, is - # used to construct a family tree. A typical family tree looks - # something like this: - # Folder - # Channel - # Video - # Video - # Channel - # Video - # Video - # Folder - # Folder - # Playlist - # Video - # Video - # Folder - # Playlist - # Video - # Video - # Folder - # Video - # Video - # A list of .dbid IVs for all top-level media.Channel, media.Playlist - # and media.Folder objects - self.media_top_level_list = [] - # The maximum depth of the media registry. The diagram above shows - # channels on the 2nd level and playlists on the third level. - # Container objects cannot be added beyond the following level - self.media_max_level = 8 - # Standard name for a media.Video object, when the actual name of the - # video is not yet known - self.default_video_name = '(video with no name)' - # The maximum length of channel, playlist and folder names (does not - # apply to video names) - self.container_name_max_len = 64 - # Forbidden names for channels, playlists and folders. This is to - # prevent the user overwriting directories in self.data_dir, that - # Tartube uses for its own purposes, and to prevent the user fooling - # Tartube into thinking that the old file structure is being used - # Every item in this list is a regex; a name for a channel, playlist - # or folder must not match any item in the list. (media.Video - # objects can still have any name) - self.illegal_name_regex_list = [ - r'^\.', - r'^downloads$', - __main__.__packagename__, - ] - - # 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 - # Private folder containing all videos (users cannot add anything to a - # private folder, because it's used by Tartube for special purposes) - self.fixed_all_folder = None - # Private folder containing only bookmarked videos - self.fixed_bookmark_folder = None - # Private folder containing only favourite videos - self.fixed_fav_folder = None - # Private folder containing only new videos - self.fixed_new_folder = None - # Private folder containing only playlist videos (when the user - # watches one, online or locally, the video is removed from the - # playlist) - self.fixed_waiting_folder = None - # Public folder that's used as the second one in the 'Add video' - # dialogue window, in which the user can store any individual videos - # that are automatically deleted when Tartube shuts down - self.fixed_temp_folder = None - # 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 - - # 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 - # self.watch_after_dl_list(), and removed by a call to - # self.announce_video_download() - self.watch_after_dl_list = [] - - # Automatic 'Download all' download operations - 'none' to disable, - # 'start' to perform the operation whenever Tartube starts, or - # 'scheduled' to perform the operation at regular intervals - self.scheduled_dl_mode = 'none' - # The time (in hours) between 'scheduled' 'Download all' operations, if - # enabled (can be fractional) - self.scheduled_dl_wait_hours = 2 - # 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 - - # Automatic 'Check all' download operations - 'none' to disable, - # 'start' to perform the operation whenever Tartube starts, or - # 'scheduled' to perform the operation at regular intervals - self.scheduled_check_mode = 'none' - # The time (in hours) between 'scheduled' 'Check all' operations, if - # enabled (can be fractional) - self.scheduled_check_wait_hours = 2 - # 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 - - # 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 a download operation should auto-stop after a - # certain period of time (applies to both real and simulated - # downloads) - self.autostop_time_flag = False - # Auto-stop after this amount of time (minimum value 1)... - self.autostop_time_value = 1 - # ...in this many units (any of the values in - # formats.TIME_METRIC_LIST) - self.autostop_time_unit = 'hours' - # Flag set to True if a download operation should auto-stop after a - # certain number of videos (applies to both real and simulated - # downloads) - self.autostop_videos_flag = False - # Auto-stop after this many videos (minimum value 1) - self.autostop_videos_value = 100 - # Flag set to True if a download operation should auto-stop after - # downloading videos of a certain combined size (applies to real - # downloads only; the specified size is approximate, because it - # relies on th video size reported by youtube-dl, and doesn't take - # account of thumbnails, JSON data, and so on) - self.autostop_size_flag = False - # Auto-stop after this amount of diskspace (minimum value 1)... - self.autostop_size_value = 1 - # ...in this many units (any of the values in - # formats.FILESIZE_METRIC_LIST) - self.autostop_size_unit = 'GiB' - - # Flag set to True if an update operation should be automatically - # started before the beginning of every download operation - self.operation_auto_update_flag = False - # When that flag is True, the following IVs are set by the initial - # call to self.download_manager_start(), reminding - # self.update_manager_finished() to start a download operation, and - # supplying it with the arguments from the original call to - # self.download_manager_start() - self.operation_waiting_flag = False - self.operation_waiting_type = None - self.operation_waiting_list = [] - # Flag set to True if files should be saved at the end of every - # operation - self.operation_save_flag = True - # Flag set to True if, during download operations using simulated - # downloads, videos whose parent is a media.Folder (i.e. videos not - # in channels/playlists) should not be added to the downlist list, - # unless the location of the video file is not set and no thumbnail - # has been downloaded. If False, those videos are always added to - # the download list - # (This does not affect real downloads, in which such videos are never - # added to the download list) - self.operation_sim_shortcut_flag = True - # How to notify the user at the end of each download/update/refresh - # operation: 'dialogue' to use a dialogue window, 'desktop' to use a - # desktop notification, or 'default' to do neither - # NB Desktop notifications don't work on MS Windows - self.operation_dialogue_mode = 'dialogue' - # What to do when the user creates a media.Video object whose URL - # represents a channel or playlist - # 'channel' to create a new media.Channel object, and place all the - # downloaded videos inside it (the original media.Video object is - # destroyed) - # 'playlist' to create a new media.Playlist object, and place all the - # downloaded videos inside it (the original media.Video object is - # destroyed) - # 'multi' to create a new media.Video object for each downloaded video, - # placed in the same folder as the original media.Video object (the - # original is destroyed) - # 'disable' to download nothing from the URL - # There are some restrictions. If the original media.Video object is - # contained in a folder whose .restrict_flag is False, and if the - # mode is 'channel' or 'playlist', then the new channel/playlist is - # not created in that folder. If the original media.Video object is - # contained in a channel or playlist, all modes to default to - # 'disable' - self.operation_convert_mode = 'channel' - # Flag set to True if self.update_video_from_filesystem() should get - # the video duration, if not already known, using the moviepy.editor - # module (an optional dependency) - self.use_module_moviepy_flag = True - - # Flag set to True if dialogue windows for adding videos, channels and - # playlists should copy the contents of the system clipboard - self.dialogue_copy_clipboard_flag = True - # Flag set to True if dialogue windows for adding channels and - # playlists should continually re-open, whenever the use clicks the - # OK button (so multiple channels etc can be added quickly) - self.dialogue_keep_open_flag = False - - # Flag set to True if, when downloading videos, youtube-dl should be - # passed, --download-archive, creating the file ytdl-archive.txt - # If the file exists, youtube-dl won't re-download a video a user has - # deleted - self.allow_ytdl_archive_flag = True - # If self.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. 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 - # 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) - self.apply_json_timeout_flag = True - - # Flag set to True if 'Child process exited with non-zero code' - # messages, generated by Tartube, should be ignored (in the - # Errors/Warnings tab) - self.ignore_child_process_exit_flag = True - # Flag set to True if 'unable to download video data: HTTP Error 404' - # messages from youtube-dl should be ignored (in the Errors/Warnings - # tab) - self.ignore_http_404_error_flag = False - # Flag set to True if 'Did not get any data blocks' messages from - # youtube-dl should be ignored (in the Errors/Warnings tab) - self.ignore_data_block_error_flag = False - # Flag set to True if 'Requested formats are incompatible for merge and - # will be merged into mkv' messages from youtube-dl should be ignored - # (in the Errors/Warnings tab) - self.ignore_merge_warning_flag = False - # Flag set to True if 'No video formats found; please report this - # issue on...' messages from youtube-dl should be ignored (in the - # Errors/Warnings tab) - self.ignore_missing_format_error_flag = False - # Flag set to True if 'There are no annotations to write' messages - # should be ignored (in the Errors/Warnings tab) - self.ignore_no_annotations_flag = True - # Flag set to True if 'video doesn't have subtitles' errors should be - # ignored (in the Errors/Warnings tab) - self.ignore_no_subtitles_flag = True - - # Flag set to True if YouTube copyright messages should be ignored (in - # the Errors/Warnings tab) - self.ignore_yt_copyright_flag = False - # Flag set to True if YouTube age-restriction messages should be - # ignored (in the Errors/Warnings tab) - self.ignore_yt_age_restrict_flag = False - # Flag set to True if 'The uploader has not made this video available' - # messages should be ignored (in the Errors/Warnings tab) - self.ignore_yt_uploader_deleted_flag = False - - # Websites other than YouTube typically use different error messages - # A custom list of strings or regexes, which are matched against error - # messages. Any matching error messages are not displayed in the - # Errors/Warnings tab. The user can add - self.ignore_custom_msg_list = [] - # Flag set to True if the contents of the list are regexes, False if - # they are ordinary strings - self.ignore_custom_regex_flag = False - - # During a download operation, the number of simultaneous downloads - # allowed. (An instruction to youtube-dl to download video(s) from a - # single URL is called a download job) - # NB Because Tartube just passes a set of instructions to youtube-dl - # and then waits for the results, an increase in this number is - # applied to a download operation immediately, but a decrease is not - # applied until one of the download jobs has finished - self.num_worker_default = 2 - # (Absoute minimum and maximum values) - self.num_worker_max = 10 - self.num_worker_min = 1 - # Flag set to True when the limit is actually applied, False when not - self.num_worker_apply_flag = True - - # During a download operation, the bandwith limit (in KiB/s) - # NB Because Tartube just passes a set of instructions to youtube-dl, - # any change in this value is not applied until one of the download - # jobs has finished - self.bandwidth_default = 500 - # (Absolute minimum and maximum values) - self.bandwidth_max = 10000 - self.bandwidth_min = 1 - # Flag set to True when the limit is currently applied, False when not - self.bandwidth_apply_flag = False - - # During a download operation, the maximum video resolution to - # download. Must be one of the keys in formats.VIDEO_RESOLUTION_DICT - # (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) - self.video_res_apply_flag = False - - # The method of matching downloaded videos against existing - # media.Video objects: - # 'exact_match' - The video name must match exactly - # 'match_first' - The first n characters of the video name must - # match exactly - # 'ignore_last' - All characters before the last n characters of - # the video name must match exactly - self.match_method = 'exact_match' - # Default values for self.match_first_chars and .match_ignore_chars - self.match_default_chars = 10 - # For 'match_first', the number of characters (n) to use. Set to the - # default value when self.match_method is not 'match_first'; range - # 1-999 - self.match_first_chars = self.match_default_chars - # For 'ignore_last', the number of characters (n) to ignore. Set to the - # default value of when self.match_method is not 'ignore_last'; range - # 1-999 - self.match_ignore_chars = self.match_default_chars - - # Automatic video deletion. Applies only to downloaded videos (not to - # checked videos) - # Flag set to True if videos should be deleted after a certain time - self.auto_delete_flag = False - # Flag set to True if videos are automatically deleted after a certain - # time, but only if they have been watched (media.Video.dl_flag is - # True, media.Video.new_flag is False; ignored if - # self.auto_delete_old_flag is False) - self.auto_delete_watched_flag = False - # Videos are automatically deleted after this many days (must be an - # integer, minimum value 1; ignored if self.auto_delete_old_flag is - # False) - self.auto_delete_days = 30 - - # Temporary folder emptying (applies to all media.Folder objects whose - # .temp_flag is True) - # Temporary folders are always emptied when Tartube starts. Flag set to - # True if they should be emptied when Tartube shuts down, as well - self.delete_on_shutdown_flag = False - # Flag set to True if temporary folders should be opened (on the - # desktop) when Tartube shuts down, so the user can more conveniently - # copy things out of it (but only if videos actually exist in the - # folder(s). Ignored if self.delete_on_shutdown_flag is True - self.open_temp_on_desktop_flag = False - - # How much information to show in the Video Index. False to show - # minimal video stats, True to show full video stats - self.complex_index_flag = False - # The Video Catalogue has two 'skins', a simple view (without - # thumbnails) and a more complex view (with thumbnails) - # Each skin can be set to show the name of the parent channel/playlist/ - # folder, or not - # The current Video Catalogue mode: - # 'simple_hide_parent' - No thumbnail, show description - # 'simple_show_parent' - No thumbnail, show parent - # 'complex_hide_parent' - Thumbnail, show description - # 'complex_hide_parent_ext' - Thumbnail, description & extra labels - # 'complex_show_parent' - Thumbnail, show parent - # 'complex_show_parent_ext' - Thumbnail, parent & extra labels - self.catalogue_mode = 'complex_show_parent' - # 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, or 0 to always use a single page - self.catalogue_page_size = 50 - # Flag set to True if the Video Catalogue toolbar should show an - # extra row, containing video filter options - self.catalogue_show_filter_flag = False - # Flag set to True if videos in the catalogue are sorted alphabetically - # or False if they are sorted by date (default) - self.catalogue_alpha_sort_flag = False - # Flag set to True if the 'Regex' button is toggled on, meaning that - # when the searching the catalogue, we match videos using a regex, - # rather than a simple string - self.catologue_use_regex_flag = False - - # Flag set to True if a smaller set of options should be shown in the - # download options edit window (for inexperienced users) - self.simple_options_flag = True - - - def do_startup(self): - - """Gio.Application standard function.""" - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 1215 do_startup') - - GObject.threads_init() - Gtk.Application.do_startup(self) - - # Menu actions - # ------------ - - # 'File' column - change_db_menu_action = Gio.SimpleAction.new('change_db_menu', None) - change_db_menu_action.connect('activate', self.on_menu_change_db) - self.add_action(change_db_menu_action) - - save_db_menu_action = Gio.SimpleAction.new('save_db_menu', None) - save_db_menu_action.connect('activate', self.on_menu_save_db) - self.add_action(save_db_menu_action) - - save_all_menu_action = Gio.SimpleAction.new('save_all_menu', None) - save_all_menu_action.connect('activate', self.on_menu_save_all) - self.add_action(save_all_menu_action) - - close_tray_menu_action = Gio.SimpleAction.new('close_tray_menu', None) - close_tray_menu_action.connect('activate', self.on_menu_close_tray) - self.add_action(close_tray_menu_action) - - quit_menu_action = Gio.SimpleAction.new('quit_menu', None) - quit_menu_action.connect('activate', self.on_menu_quit) - self.add_action(quit_menu_action) - - # 'Edit' column - system_prefs_action = Gio.SimpleAction.new('system_prefs_menu', None) - system_prefs_action.connect( - 'activate', - self.on_menu_system_preferences, - ) - self.add_action(system_prefs_action) - - gen_options_action = Gio.SimpleAction.new('gen_options_menu', None) - gen_options_action.connect('activate', self.on_menu_general_options) - self.add_action(gen_options_action) - - # 'Media' column - add_video_menu_action = Gio.SimpleAction.new('add_video_menu', None) - add_video_menu_action.connect('activate', self.on_menu_add_video) - self.add_action(add_video_menu_action) - - add_channel_menu_action = Gio.SimpleAction.new( - 'add_channel_menu', - None, - ) - add_channel_menu_action.connect('activate', self.on_menu_add_channel) - self.add_action(add_channel_menu_action) - - add_playlist_menu_action = Gio.SimpleAction.new( - 'add_playlist_menu', - None, - ) - add_playlist_menu_action.connect( - 'activate', - self.on_menu_add_playlist, - ) - self.add_action(add_playlist_menu_action) - - add_folder_menu_action = Gio.SimpleAction.new('add_folder_menu', None) - add_folder_menu_action.connect('activate', self.on_menu_add_folder) - self.add_action(add_folder_menu_action) - - export_db_menu_action = Gio.SimpleAction.new('export_db_menu', None) - export_db_menu_action.connect('activate', self.on_menu_export_db) - self.add_action(export_db_menu_action) - - import_json_menu_action = Gio.SimpleAction.new( - 'import_json_menu', - None, - ) - import_json_menu_action.connect('activate', self.on_menu_import_json) - self.add_action(import_json_menu_action) - - import_text_menu_action = Gio.SimpleAction.new( - 'import_text_menu', - None, - ) - import_text_menu_action.connect( - 'activate', - self.on_menu_import_plain_text, - ) - self.add_action(import_text_menu_action) - - switch_view_menu_action = Gio.SimpleAction.new( - 'switch_view_menu', - None, - ) - switch_view_menu_action.connect('activate', self.on_button_switch_view) - self.add_action(switch_view_menu_action) - - show_hidden_menu_action = Gio.SimpleAction.new( - 'show_hidden_menu', - None, - ) - show_hidden_menu_action.connect('activate', self.on_menu_show_hidden) - self.add_action(show_hidden_menu_action) - - if self.debug_test_media_menu_flag: - test_menu_action = Gio.SimpleAction.new('test_menu', None) - test_menu_action.connect('activate', self.on_menu_test) - self.add_action(test_menu_action) - - # 'Operations' column - check_all_menu_action = Gio.SimpleAction.new('check_all_menu', None) - check_all_menu_action.connect( - 'activate', - self.on_menu_check_all, - ) - self.add_action(check_all_menu_action) - - download_all_menu_action = Gio.SimpleAction.new( - 'download_all_menu', - None, - ) - download_all_menu_action.connect( - 'activate', - self.on_menu_download_all, - ) - self.add_action(download_all_menu_action) - - custom_dl_all_menu_action = Gio.SimpleAction.new( - 'custom_dl_all_menu', - None, - ) - custom_dl_all_menu_action.connect( - 'activate', - self.on_menu_custom_dl_all, - ) - self.add_action(custom_dl_all_menu_action) - - refresh_db_menu_action = Gio.SimpleAction.new('refresh_db_menu', None) - refresh_db_menu_action.connect('activate', self.on_menu_refresh_db) - self.add_action(refresh_db_menu_action) - - ytdl_menu_action = Gio.SimpleAction.new('update_ytdl_menu', None) - ytdl_menu_action.connect('activate', self.on_menu_update_ytdl) - self.add_action(ytdl_menu_action) - - ytdl_test_menu_action = Gio.SimpleAction.new('test_ytdl_menu', None) - ytdl_test_menu_action.connect('activate', self.on_menu_test_ytdl) - self.add_action(ytdl_test_menu_action) - - ffmpeg_menu_action = Gio.SimpleAction.new('install_ffmpeg_menu', None) - ffmpeg_menu_action.connect('activate', self.on_menu_install_ffmpeg) - self.add_action(ffmpeg_menu_action) - - tidy_up_menu_action = Gio.SimpleAction.new('tidy_up_menu', None) - tidy_up_menu_action.connect('activate', self.on_menu_tidy_up) - self.add_action(tidy_up_menu_action) - - stop_operation_menu_action = Gio.SimpleAction.new( - 'stop_operation_menu', - None, - ) - stop_operation_menu_action.connect( - 'activate', - self.on_button_stop_operation, - ) - self.add_action(stop_operation_menu_action) - - # 'Help' column - about_menu_action = Gio.SimpleAction.new('about_menu', None) - about_menu_action.connect('activate', self.on_menu_about) - self.add_action(about_menu_action) - - go_website_menu_action = Gio.SimpleAction.new('go_website_menu', None) - go_website_menu_action.connect('activate', self.on_menu_go_website) - self.add_action(go_website_menu_action) - - # Main toolbar actions - # -------------------- - - add_video_toolbutton_action = Gio.SimpleAction.new( - 'add_video_toolbutton', - None, - ) - add_video_toolbutton_action.connect( - 'activate', - self.on_menu_add_video, - ) - self.add_action(add_video_toolbutton_action) - - add_channel_toolbutton_action = Gio.SimpleAction.new( - 'add_channel_toolbutton', - None, - ) - add_channel_toolbutton_action.connect( - 'activate', - self.on_menu_add_channel, - ) - self.add_action(add_channel_toolbutton_action) - - add_playlist_toolbutton_action = Gio.SimpleAction.new( - 'add_playlist_toolbutton', - None, - ) - add_playlist_toolbutton_action.connect( - 'activate', - self.on_menu_add_playlist, - ) - self.add_action(add_playlist_toolbutton_action) - - add_folder_toolbutton_action = Gio.SimpleAction.new( - 'add_folder_toolbutton', - None, - ) - add_folder_toolbutton_action.connect( - 'activate', - self.on_menu_add_folder, - ) - self.add_action(add_folder_toolbutton_action) - - check_all_toolbutton_action = Gio.SimpleAction.new( - 'check_all_toolbutton', - None, - ) - check_all_toolbutton_action.connect( - 'activate', - self.on_menu_check_all, - ) - self.add_action(check_all_toolbutton_action) - - download_all_toolbutton_action = Gio.SimpleAction.new( - 'download_all_toolbutton', - None, - ) - download_all_toolbutton_action.connect( - 'activate', - self.on_menu_download_all, - ) - self.add_action(download_all_toolbutton_action) - - stop_operation_button_action = Gio.SimpleAction.new( - 'stop_operation_toolbutton', - None, - ) - stop_operation_button_action.connect( - 'activate', - self.on_button_stop_operation, - ) - self.add_action(stop_operation_button_action) - - switch_view_button_action = Gio.SimpleAction.new( - 'switch_view_toolbutton', - None, - ) - switch_view_button_action.connect( - 'activate', - self.on_button_switch_view, - ) - self.add_action(switch_view_button_action) - - if self.debug_test_media_toolbar_flag: - test_button_action = Gio.SimpleAction.new('test_toolbutton', None) - test_button_action.connect('activate', self.on_menu_test) - self.add_action(test_button_action) - - quit_button_action = Gio.SimpleAction.new('quit_toolbutton', None) - quit_button_action.connect('activate', self.on_menu_quit) - self.add_action(quit_button_action) - - # Video catalogue toolbar actions - # ------------------------------- - - first_page_toolbutton_action = Gio.SimpleAction.new( - 'first_page_toolbutton', - None, - ) - first_page_toolbutton_action.connect( - 'activate', - self.on_button_first_page, - ) - self.add_action(first_page_toolbutton_action) - - previous_page_toolbutton_action = Gio.SimpleAction.new( - 'previous_page_toolbutton', - None, - ) - previous_page_toolbutton_action.connect( - 'activate', - self.on_button_previous_page, - ) - self.add_action(previous_page_toolbutton_action) - - next_page_toolbutton_action = Gio.SimpleAction.new( - 'next_page_toolbutton', - None, - ) - next_page_toolbutton_action.connect( - 'activate', - self.on_button_next_page, - ) - self.add_action(next_page_toolbutton_action) - - last_page_toolbutton_action = Gio.SimpleAction.new( - 'last_page_toolbutton', - None, - ) - last_page_toolbutton_action.connect( - 'activate', - self.on_button_last_page, - ) - self.add_action(last_page_toolbutton_action) - - scroll_up_toolbutton_action = Gio.SimpleAction.new( - 'scroll_up_toolbutton', - None, - ) - scroll_up_toolbutton_action.connect( - 'activate', - self.on_button_scroll_up, - ) - self.add_action(scroll_up_toolbutton_action) - - scroll_down_toolbutton_action = Gio.SimpleAction.new( - 'scroll_down_toolbutton', - None, - ) - scroll_down_toolbutton_action.connect( - 'activate', - self.on_button_scroll_down, - ) - self.add_action(scroll_down_toolbutton_action) - - show_filter_toolbutton_action = Gio.SimpleAction.new( - 'show_filter_toolbutton', - None, - ) - show_filter_toolbutton_action.connect( - 'activate', - self.on_button_show_filter, - ) - self.add_action(show_filter_toolbutton_action) - - # (Second row) - - sort_type_toolbutton_action = Gio.SimpleAction.new( - 'sort_type_toolbutton', - None, - ) - sort_type_toolbutton_action.connect( - 'activate', - self.on_button_sort_type, - ) - self.add_action(sort_type_toolbutton_action) - - use_regex_togglebutton_action = Gio.SimpleAction.new( - 'use_regex_togglebutton', - None, - ) - use_regex_togglebutton_action.connect( - 'activate', - self.on_button_use_regex, - ) - self.add_action(use_regex_togglebutton_action) - - apply_filter_button_action = Gio.SimpleAction.new( - 'apply_filter_toolbutton', - None, - ) - apply_filter_button_action.connect( - 'activate', - self.on_button_apply_filter, - ) - self.add_action(apply_filter_button_action) - - cancel_filter_button_action = Gio.SimpleAction.new( - 'cancel_filter_toolbutton', - None, - ) - cancel_filter_button_action.connect( - 'activate', - self.on_button_cancel_filter, - ) - self.add_action(cancel_filter_button_action) - - find_date_toolbutton_action = Gio.SimpleAction.new( - 'find_date_toolbutton', - None, - ) - find_date_toolbutton_action.connect( - 'activate', - self.on_button_find_date, - ) - self.add_action(find_date_toolbutton_action) - - # Videos Tab actions - # ------------------ - - # Buttons - - check_all_button_action = Gio.SimpleAction.new( - 'check_all_button', - None, - ) - check_all_button_action.connect('activate', self.on_menu_check_all) - self.add_action(check_all_button_action) - - download_all_button_action = Gio.SimpleAction.new( - 'download_all_button', - None, - ) - download_all_button_action.connect( - 'activate', - self.on_menu_download_all, - ) - self.add_action(download_all_button_action) - - - def do_activate(self): - - """Gio.Application standard function.""" - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 1634 do_activate') - - # Only allow a single main window (raise any existing main windows) - if not self.main_win_obj: - 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) - - else: - self.main_win_obj.present() - - # Show a warning dialogue window, if the debugging flag is set - if self.debug_warn_multiple_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - __main__.__prettyname__ + ' is already running!', - 'warning', - 'ok', - ) - - - def do_shutdown(self): - - """Gio.Application standard function. - - Clean shutdowns (for example, from the main window's toolbar) are - handled by self.stop(). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 1671 do_shutdown') - - # Stop the GObject timers immediately - if self.script_slow_timer_id: - GObject.source_remove(self.script_slow_timer_id) - if self.script_fast_timer_id: - GObject.source_remove(self.script_fast_timer_id) - if self.dl_timer_id: - GObject.source_remove(self.dl_timer_id) - if self.update_timer_id: - GObject.source_remove(self.update_timer_id) - if self.refresh_timer_id: - GObject.source_remove(self.refresh_timer_id) - if self.info_timer_id: - GObject.source_remove(self.info_timer_id) - if self.tidy_timer_id: - GObject.source_remove(self.tidy_timer_id) - - # Don't prompt the user before halting a download/update/refresh/info/ - # tidy operation, as we would do in calls to self.stop() - if self.download_manager_obj: - self.download_manager_obj.stop_download_operation() - elif self.update_manager_obj: - self.update_manager_obj.stop_update_operation() - elif self.refresh_manager_obj: - self.refresh_manager_obj.stop_refresh_operation() - elif self.info_manager_obj: - self.info_manager_obj.stop_info_operation() - elif self.tidy_manager_obj: - self.tidy_manager_obj.stop_tidy_operation() - - # If there is a lock on the database file, release it - self.remove_db_lock_file() - - # Stop immediately - Gtk.Application.do_shutdown(self) - if os.name == 'nt': - # Under MS Windows, all methods of shutting down after an update - # operation fail - except this method - os._exit(0) - - # Still here? Do a brute-force exit - exit() - - - # Public class methods - - - def start(self): - - """Called by self.do_activate(). - - Performs general initialisation. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 1727 start') - - # 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() - - # 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' - else: - # 32-bit MS Windows - descrip = 'Windows 32-bit update (recommended)' - 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: [ - python_path, - pip_path, - 'install', - '--upgrade', - 'youtube-dl', - ], - 'Update using pip3': [ - 'pip3', 'install', '--upgrade', 'youtube-dl', - ], - 'Update using pip': [ - 'pip', 'install', '--upgrade', 'youtube-dl', - ], - 'Update using default youtube-dl path': [ - self.ytdl_path_default, '-U', - ], - 'Update using local youtube-dl 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', - ] - self.ytdl_update_current = descrip - - elif __main__.__pkg_strict_install_flag__: - - self.ytdl_bin = 'youtube-dl' - self.ytdl_path_default = os.path.abspath( - os.path.join(os.sep, 'usr', 'bin', self.ytdl_bin), - ) - self.ytdl_path = self.ytdl_path_pypi - - self.ytdl_update_dict = { - 'youtube-dl updates are disabled': [], - } - self.ytdl_update_list = [ - 'youtube-dl updates are disabled', - ] - self.ytdl_update_current = 'youtube-dl updates are disabled' - - else: - - self.ytdl_bin = 'youtube-dl' - self.ytdl_path_default = os.path.abspath( - os.path.join(os.sep, 'usr', 'bin', self.ytdl_bin), - ) - - if __main__.__pkg_install_flag__: - self.ytdl_path = self.ytdl_path_pypi - else: - self.ytdl_path = 'youtube-dl' - - self.ytdl_update_dict = { - 'Update using pip3 (recommended)': [ - 'pip3', 'install', '--upgrade', '--user', 'youtube-dl', - ], - 'Update using pip3 (omit --user option)': [ - 'pip3', 'install', '--upgrade', 'youtube-dl', - ], - 'Update using pip': [ - 'pip', 'install', '--upgrade', '--user', 'youtube-dl', - ], - 'Update using pip (omit --user option)': [ - 'pip', 'install', '--upgrade', 'youtube-dl', - ], - 'Update using default youtube-dl path': [ - self.ytdl_path_default, '-U', - ], - 'Update using local youtube-dl path': [ - 'youtube-dl', '-U', - ], - 'Update using PyPI youtube-dl 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', - ] - self.ytdl_update_current = 'Update using pip3 (recommended)' - - # Make sure the directory containing the config file exists - config_dir = None - if ( - self.config_file_xdg_dir is not None - and not os.path.isdir(self.config_file_xdg_dir) - ): - config_dir = self.config_file_xdg_dir - - elif ( - self.config_file_xdg_dir is None - and not os.path.isdir(self.config_file_dir) - ): - config_dir = self.config_file_dir - - 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', - ) - - # If the config file exists, load it. If not, create it - new_config_flag = False - if ( - self.config_file_xdg_path is not None \ - and os.path.isfile(self.config_file_xdg_path) - ) or ( - self.config_file_xdg_path is None \ - and os.path.isfile(self.config_file_path) - ): - self.load_config() - - elif self.debug_no_dialogue_flag: - self.save_config() - new_config_flag = True - - elif not self.disable_load_save_flag: - - # 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() - - # On MS Windows, tell the user that they must set the location of - # the data directory, self.data_dir. On other operating systems, - # ask the user if they want to use the default location, or - # choose a custom one - 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() - - # All done; create the config file, whether Tartube's data - # directory has been changed, or not - self.save_config() - - # Multiple instances of Tartube can share the same config file, but not - # the same database file - # If the database file specified by the config file we've just loaded - # is locked (meaning it's in use by another instance), we might be - # able to use an alternative data directory - if self.data_dir_use_list_flag and not new_config_flag: - self.choose_alt_db() - - # Check that the data directory specified by self.data_dir actually - # exists. If not, the most common reason is that the user has - # forgotten to mount an external drive - if not new_config_flag \ - and not self.debug_no_dialogue_flag \ - and not os.path.exists(self.data_dir): - - # Ask the user what to do next. The False argument tells the - # dialogue window that it's a missing directory - dialogue_win = mainwin.MountDriveDialogue( - self.main_win_obj, - False, - ) - dialogue_win.run() - - # If the data directory now exists, or can be created in principle - # by the code just below (because the user wants to use the - # default location), then available_flag will be True - available_flag = dialogue_win.available_flag - dialogue_win.destroy() - - if not available_flag: - - # The user opted to shut down Tartube. Destroying the main - # window calls self.do_shutdown() - return self.main_win_obj.destroy() - - # Create Tartube's data directories (if they don't already exist) - if not os.path.isdir(self.data_dir): - - # React to a 'Permission denied' error by asking the user what to - # do next. If necessary, shut down Tartube - if not self.make_directory(self.data_dir): - return self.main_win_obj.destroy() - - # Create the directory for database file backups - if not os.path.isdir(self.backup_dir): - if not self.make_directory(self.backup_dir): - return self.main_win_obj.destroy() - - # Create the temporary data directories (or empty them, if they already - # exist) - if os.path.isdir(self.temp_dir): - try: - shutil.rmtree(self.temp_dir) - - except: - if not self.make_directory(self.temp_dir): - return self.main_win_obj.destroy() - else: - shutil.rmtree(self.temp_dir) - - if not os.path.isdir(self.temp_dir): - if not self.make_directory(self.temp_dir): - return self.main_win_obj.destroy() - - if not os.path.isdir(self.temp_dl_dir): - if not self.make_directory(self.temp_dl_dir): - return self.main_win_obj.destroy() - - # If the database file exists, load it. If not, create it - db_path = os.path.abspath( - os.path.join(self.data_dir, self.db_file_name), - ) - - if os.path.isfile(db_path): - - self.load_db() - - else: - - # 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() - - # Populate the Video Index - self.main_win_obj.video_index_populate() - - # Create the database file - 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() - - # 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', - ) - - 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', - ) - - # 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.disable_load_save_lock_flag: - - dialogue_win = mainwin.RemoveLockFileDialogue( - self.main_win_obj, - ) - - dialogue_win.run() - remove_flag = dialogue_win.remove_flag - dialogue_win.destroy() - - if remove_flag: - self.remove_stale_lock_file() - # (Don't need to display the error messages just below) - self.disable_load_save_lock_flag = False - - self.file_error_dialogue( - 'The ' + __main__.__prettyname__ \ - + ' database file was not loaded, but is no' \ - + ' longer protected\n\nRestart ' \ - + __main__.__prettyname__ + ' to load it', - ) - - if not remove_flag: - - if self.disable_load_save_msg is None: - - self.file_error_dialogue( - 'Because of an error, file load/save has been' \ - + ' disabled', - ) - - else: - - self.file_error_dialogue( - self.disable_load_save_msg + '\n\nBecause of the' \ - + ' error, file load/save has been disabled', - ) - - # Start the script's GObject slow timer - self.script_slow_timer_id = GObject.timeout_add( - self.script_slow_timer_time, - self.script_slow_timer_callback, - ) - - # Start the script's GObject fast timer - self.script_fast_timer_id = GObject.timeout_add( - self.script_fast_timer_time, - self.script_fast_timer_callback, - ) - - if not self.disable_load_save_flag: - - # For new installations, MS Windows must be prompted to perform an - # update operation, which installs youtube-dl on their system - if new_config_flag and os.name == 'nt': - - 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?', - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'update_manager_start', - # Install youtube-dl, not FFmpeg - 'data': 'ytdl', - }, - ) - - # If a download operation (real or simulated) is scheduled to occur - # on startup, then initiate it - elif self.scheduled_dl_mode == 'start': - self.download_manager_start( - 'real', # 'Download all' - True, # This function is the calling function - ) - - elif self.scheduled_check_mode == 'start': - self.download_manager_start( - 'sim', # 'Check all' - True, # This function is the calling function - ) - - - def stop(self): - - """Called by self.on_menu_quit() and - mainwin.MainWin.on_quit_menu_item(). - - Before terminating the Tartube app, gets confirmation from the user (if - a download/update/refresh/info/tidy operation is in progress). - - If no operation is in progress, calls self.stop_continue() to terminate - the app now. Otherwise, self.stop_continue() is only called when the - clicks the dialogue window's 'Yes' button. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 2161 stop') - - # If a download/update/refresh/info/tidy operation is in progress, get - # confirmation before stopping - if self.current_manager_obj: - - if self.download_manager_obj: - string = 'a download' - elif self.update_manager_obj: - string = 'an update' - elif self.refresh_manager_obj: - string = 'a refresh' - elif self.info_manager_obj: - string = 'an info' - else: - string = 'a tidy' - - # 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__ \ - + '?', - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'stop_continue', - } - ) - - # No confirmation required, so call self.stop_continue() now - else: - self.stop_continue() - - - def stop_continue(self): - - """Called by self.stop() or self.download_manager_finished(). - - Terminates the Tartube app. Forced shutdowns (for example, by clicking - the X in the top corner of the window) are handled by - self.do_shutdown(). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 2207 stop_continue') - - if self.download_manager_obj: - self.download_manager_obj.stop_download_operation() - - elif self.update_manager_obj: - self.update_manager_obj.stop_update_operation() - - elif self.refresh_manager_obj: - self.refresh_manager_obj.stop_refresh_operation() - - elif self.info_manager_obj: - self.info_manager_obj.stop_info_operation() - - elif self.tidy_manager_obj: - self.tidy_manager_obj.stop_tidy_operation() - - # Stop the GObject timers immediately. So this action is not repeated - # in the standard call to self.do_shutdown, reset the IVs - if self.script_slow_timer_id: - GObject.source_remove(self.script_slow_timer_id) - self.script_slow_timer_id = None - - if self.script_fast_timer_id: - GObject.source_remove(self.script_fast_timer_id) - self.script_fast_timer_id = None - - if self.dl_timer_id: - GObject.source_remove(self.dl_timer_id) - self.dl_timer_id = None - - if self.update_timer_id: - GObject.source_remove(self.update_timer_id) - self.update_timer_id = None - - if self.refresh_timer_id: - GObject.source_remove(self.refresh_timer_id) - self.refresh_timer_id = None - - if self.info_timer_id: - GObject.source_remove(self.info_timer_id) - self.info_timer_id = None - - if self.tidy_timer_id: - GObject.source_remove(self.tidy_timer_id) - self.tidy_timer_id = None - - # Empty any temporary folders from the database (if allowed; those - # temporary folders are always deleted when Tartube starts) - # Otherwise, open the temporary folders on the desktop, if allowd - if self.delete_on_shutdown_flag: - self.delete_temp_folders() - elif self.open_temp_on_desktop_flag: - self.open_temp_folders() - - # Delete Tartube's temporary folder from the filesystem - if os.path.isdir(self.temp_dir): - shutil.rmtree(self.temp_dir) - - # Save the config and database files for the final time, and release - # the database lockfile - self.save_config() - self.save_db() - self.remove_db_lock_file() - - # I'm outta here! - self.quit() - - - def system_error(self, error_code, msg): - - """Can be called by anything. - - Wrapper function for mainwin.MainWin.errors_list_add_system_error(). - - Args: - - error_code (int): An error code in the range 100-999 - - msg (str): A system error message to display in the main window's - Errors List. - - Notes: - - Error codes for this function and for self.system_warning are - currently assigned thus: - - 100-199: mainapp.py (in use: 101-153) - 200-299: mainwin.py (in use: 201-248) - 300-399: downloads.py (in use: 301-304) - 400-499: config.py (in use: 401-404) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 2302 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) - else: - # Emergency fallback: display in the terminal window - print('SYSTEM ERROR ' + str(error_code) + ': ' + msg) - - - def system_warning(self, error_code, msg): - - """Can be called by anything. - - Wrapper function for mainwin.MainWin.errors_list_add_system_warning(). - - Args: - - error_code (int): An error code in the range 100-999. This function - and self.system_error() share the same error codes - - msg (str): A system error message to display in the main window's - Errors List. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 2328 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) - else: - # Emergency fallback: display in the terminal window - print('SYSTEM WARNING ' + str(error_code) + ': ' + msg) - - - # (Config/database files load/save) - - - def load_config(self): - - """Called by self.start() (only). - - Loads the Tartube config file. If loading fails, disables all file - loading/saving. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 2349 load_config') - - # The config file can be stored at one of two locations, depending on - # whether xdg is available, or not - # v2.0.003. The user can force Tartube to use the config file in the - # script's directory (rather than the one in the location described - # by xdg) by placing a 'settings.json' file there. If that file is - # created when Tartube is already running, it can be an empty file - # (because Tartube overwrites it). Otherwise, it should be a copy of - # a legitimate config file - if self.config_file_xdg_path is None \ - or ( - os.path.isfile(self.config_file_path) \ - and not __main__.__pkg_strict_install_flag__ - ): - config_file_path = self.config_file_path - else: - config_file_path = self.config_file_xdg_path - - # Sanity check - if self.current_manager_obj \ - or not os.path.isfile(config_file_path) \ - or self.disable_load_save_flag: - return - - # In case a competing instance of Tartube is saving the same config - # file, check for the lockfile and, if it exists, wait a reasonable - # time for it to be released - if not self.debug_ignore_lockfile_flag: - - lock_path = config_file_path + '.lock' - if os.path.isfile(lock_path): - - check_time = time.time() + self.config_lock_time - while time.time < check_time and os.path.isfile(lock_path): - 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', - ) - - return - - # Try to load the config file - try: - with open(config_file_path) as infile: - 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', - ) - - return False - - # Do some basic checks on the loaded data - if not json_dict \ - or not 'script_name' in json_dict \ - or not 'script_version' in json_dict \ - or not 'save_date' in json_dict \ - or not 'save_time' in json_dict \ - or json_dict['script_name'] != __main__.__packagename__: - - self.disable_load_save( - 'The ' + __main__.__prettyname__ \ - + ' config file is invalid (missing data)', - ) - - return False - - # Convert a version, e.g. 1.234.567, into a simple number, e.g. - # 1234567, that can be compared with other versions - version = self.convert_version(json_dict['script_version']) - # Now check that the config file wasn't written by a more recent - # version of Tartube (which this older version might not be able to - # 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__, - ) - - return False - - # Since v1.0.008, config files have identified their file type - if version >= 1000008 \ - and ( - 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)', - ) - - return False - - # 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'] - self.main_win_save_width = json_dict['main_win_save_width'] - self.main_win_save_height = json_dict['main_win_save_height'] - self.main_win_save_posn = json_dict['main_win_save_posn'] - - if version >= 1003122: # v1.3.122 - self.gtk_emulate_broken_flag = json_dict['gtk_emulate_broken_flag'] - - if version >= 5024: # v0.5.024 - self.toolbar_squeeze_flag = json_dict['toolbar_squeeze_flag'] - if version >= 1001064: # v1.1.064 - self.show_tooltips_flag = json_dict['show_tooltips_flag'] - if version >= 1001075: # v1.1.075 - self.show_small_icons_in_index \ - = json_dict['show_small_icons_in_index'] - if version >= 1001077: # v1.1.077 - self.auto_expand_video_index_flag \ - = json_dict['auto_expand_video_index_flag'] - if version >= 1001064: # v1.1.064 - self.disable_dl_all_flag = json_dict['disable_dl_all_flag'] - if version >= 1004011: # v1.4.011 - self.show_pretty_dates_flag = json_dict['show_pretty_dates_flag'] - - if version >= 1003024: # v1.3.024 - 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'], - ) - if version >= 1000029: # v1.0.029 - self.main_win_obj.reverse_results_checkbutton.set_active( - 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'], - ) - if version >= 6006: # v0.6.006 - self.main_win_obj.show_system_warning_checkbutton.set_active( - 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'], - ) - - if version >= 1000007: # v1.0.007 - self.system_msg_keep_totals_flag \ - = json_dict['system_msg_keep_totals_flag'] - - self.data_dir = json_dict['data_dir'] - - if version >= 1004069: # v1.4.069: - self.data_dir_alt_list = json_dict['data_dir_alt_list'] - self.data_dir_use_first_flag = json_dict['data_dir_use_first_flag'] - self.data_dir_use_list_flag = json_dict['data_dir_use_list_flag'] - self.data_dir_add_from_list_flag \ - = json_dict['data_dir_add_from_list_flag'] - else: - self.data_dir_alt_list = [ self.data_dir ] - - 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 >= 1003074: # v1.3.074 - self.ytdl_output_system_cmd_flag \ - = json_dict['ytdl_output_system_cmd_flag'] - if version >= 1002030: # v1.2.030 - self.ytdl_output_stdout_flag = json_dict['ytdl_output_stdout_flag'] - self.ytdl_output_ignore_json_flag \ - = json_dict['ytdl_output_ignore_json_flag'] - self.ytdl_output_ignore_progress_flag \ - = json_dict['ytdl_output_ignore_progress_flag'] - self.ytdl_output_stderr_flag = json_dict['ytdl_output_stderr_flag'] - self.ytdl_output_start_empty_flag \ - = json_dict['ytdl_output_start_empty_flag'] - if version >= 1003064: # v1.3.064 - self.ytdl_output_show_summary_flag \ - = json_dict['ytdl_output_show_summary_flag'] - - if version >= 1003074: # v1.3.074 - self.ytdl_write_system_cmd_flag \ - = json_dict['ytdl_write_system_cmd_flag'] - self.ytdl_write_stdout_flag = json_dict['ytdl_write_stdout_flag'] - if version >= 5004: # v0.5.004 - self.ytdl_write_ignore_json_flag \ - = json_dict['ytdl_write_ignore_json_flag'] - if version >= 1002030: # v1.2.030 - self.ytdl_write_ignore_progress_flag \ - = json_dict['ytdl_write_ignore_progress_flag'] - self.ytdl_write_stderr_flag = json_dict['ytdl_write_stderr_flag'] - - self.ytdl_write_verbose_flag = json_dict['ytdl_write_verbose_flag'] - - if version >= 1002024: # v1.2.024 - self.refresh_output_videos_flag \ - = json_dict['refresh_output_videos_flag'] - if version >= 1002027: # v1.2.027 - self.refresh_output_verbose_flag \ - = json_dict['refresh_output_verbose_flag'] - if version >= 1003012: # v1.3.012 - self.refresh_moviepy_timeout = json_dict['refresh_moviepy_timeout'] - - if version >= 1003032: # v1.3.032 - self.auto_clone_options_flag = json_dict['auto_clone_options_flag'] - - if version >= 1002030: # v1.2.037 - self.disk_space_warn_flag = json_dict['disk_space_warn_flag'] - self.disk_space_warn_limit = json_dict['disk_space_warn_limit'] - self.disk_space_stop_flag = json_dict['disk_space_stop_flag'] - self.disk_space_stop_limit = json_dict['disk_space_stop_limit'] - - if version >= 1004024: # v1.4.024 - self.custom_dl_by_video_flag = json_dict['custom_dl_by_video_flag'] - - if version >= 1004052: # v1.4.052 - self.custom_dl_divert_mode = json_dict['custom_dl_divert_mode'] - elif version >= 1004024: # v1.4.024 - if json_dict['custom_dl_divert_hooktube_flag']: - self.custom_dl_divert_mode = 'hooktube' - - if version >= 1004024: # v1.4.024 - self.custom_dl_delay_flag = json_dict['custom_dl_delay_flag'] - self.custom_dl_delay_max = json_dict['custom_dl_delay_max'] - self.custom_dl_delay_min = json_dict['custom_dl_delay_min'] - - if version >= 1001054: # v1.1.054 - self.ffmpeg_path = json_dict['ffmpeg_path'] - - if version >= 3029: # v0.3.029 - self.operation_limit_flag = json_dict['operation_limit_flag'] - self.operation_check_limit = json_dict['operation_check_limit'] - self.operation_download_limit \ - = json_dict['operation_download_limit'] - - if version >= 1001067: # v1.0.067 - self.scheduled_dl_mode = json_dict['scheduled_dl_mode'] - self.scheduled_dl_wait_hours = json_dict['scheduled_dl_wait_hours'] - self.scheduled_dl_last_time = json_dict['scheduled_dl_last_time'] - - self.scheduled_check_mode = json_dict['scheduled_check_mode'] - self.scheduled_check_wait_hours \ - = json_dict['scheduled_check_wait_hours'] - self.scheduled_check_last_time \ - = json_dict['scheduled_check_last_time'] - - # Renamed in v1.3.120 - if 'scheduled_stop_flag' in json_dict: - self.scheduled_shutdown_flag = json_dict['scheduled_stop_flag'] - else: - self.scheduled_shutdown_flag \ - = json_dict['scheduled_shutdown_flag'] - - if version >= 1003112: # v1.3.112 - self.autostop_time_flag = json_dict['autostop_time_flag'] - self.autostop_time_value = json_dict['autostop_time_value'] - self.autostop_time_unit = json_dict['autostop_time_unit'] - self.autostop_videos_flag = json_dict['autostop_videos_flag'] - self.autostop_videos_value = json_dict['autostop_videos_value'] - self.autostop_size_flag = json_dict['autostop_size_flag'] - self.autostop_size_value = json_dict['autostop_size_value'] - self.autostop_size_unit = json_dict['autostop_size_unit'] - - self.operation_auto_update_flag \ - = json_dict['operation_auto_update_flag'] - self.operation_save_flag = json_dict['operation_save_flag'] - if version >= 1004003: # v1.4.003 - self.operation_sim_shortcut_flag \ - = json_dict['operation_sim_shortcut_flag'] -# # Removed v1.3.028 -# self.operation_dialogue_flag = json_dict['operation_dialogue_flag'] - if version >= 1003028: # v1.3.028 - self.operation_dialogue_mode = json_dict['operation_dialogue_mode'] - if version >= 1003060: # v1.3.060 - self.operation_convert_mode = json_dict['operation_convert_mode'] - - self.use_module_moviepy_flag = json_dict['use_module_moviepy_flag'] -# # Removed v0.5.003 -# self.use_module_validators_flag \ -# = json_dict['use_module_validators_flag'] - - if version >= 1000006: # v1.0.006 - self.dialogue_copy_clipboard_flag \ - = json_dict['dialogue_copy_clipboard_flag'] - self.dialogue_keep_open_flag \ - = json_dict['dialogue_keep_open_flag'] - # Removed v1.3.022 -# self.dialogue_keep_container_flag \ -# = json_dict['dialogue_keep_container_flag'] - - if version >= 1003018: # v1.3.018 - self.allow_ytdl_archive_flag \ - = json_dict['allow_ytdl_archive_flag'] - if version >= 5004: # v0.5.004 - self.apply_json_timeout_flag \ - = json_dict['apply_json_timeout_flag'] - - if version >= 5004: # v0.5.004 - self.ignore_child_process_exit_flag \ - = json_dict['ignore_child_process_exit_flag'] - if version >= 1003088: # v1.3.088 - self.ignore_http_404_error_flag \ - = json_dict['ignore_http_404_error_flag'] - self.ignore_data_block_error_flag \ - = json_dict['ignore_data_block_error_flag'] - if version >= 1027: # v0.1.028 - self.ignore_merge_warning_flag \ - = json_dict['ignore_merge_warning_flag'] - if version >= 1003088: # v1.3.088 - self.ignore_missing_format_error_flag \ - = json_dict['ignore_missing_format_error_flag'] - if version >= 1001077: # v1.1.077 - self.ignore_no_annotations_flag \ - = json_dict['ignore_no_annotations_flag'] - if version >= 1002004: # v1.2.004 - self.ignore_no_subtitles_flag \ - = json_dict['ignore_no_subtitles_flag'] - - if version >= 5004: # v0.5.004 - self.ignore_yt_copyright_flag \ - = json_dict['ignore_yt_copyright_flag'] - if version >= 1003084: # v1.3.084 - self.ignore_yt_age_restrict_flag \ - = json_dict['ignore_yt_age_restrict_flag'] - if version >= 1003088: # v1.3.088 - self.ignore_yt_age_restrict_flag \ - = json_dict['ignore_yt_uploader_deleted_flag'] - - if version >= 1003090: # v1.3.090 - self.ignore_custom_msg_list \ - = json_dict['ignore_custom_msg_list'] - 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.main_win_obj.bandwidth_spinbutton.set_value( - json_dict['bandwidth_default'], - ) - self.main_win_obj.bandwidth_checkbutton.set_active( - 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.match_method = json_dict['match_method'] - self.match_first_chars = json_dict['match_first_chars'] - self.match_ignore_chars = json_dict['match_ignore_chars'] - - if version >= 1001029: # v1.1.029 - self.auto_delete_flag = json_dict['auto_delete_flag'] - self.auto_delete_watched_flag \ - = json_dict['auto_delete_watched_flag'] - self.auto_delete_days = json_dict['auto_delete_days'] - - if version >= 1002041: # v1.2.041 - self.delete_on_shutdown_flag = json_dict['delete_on_shutdown_flag'] - if version >= 1004027: # v1.4.027 - self.open_temp_on_desktop_flag \ - = json_dict['open_temp_on_desktop_flag'] - - self.complex_index_flag = json_dict['complex_index_flag'] - if version >= 3019: # v0.3.019 - self.catalogue_mode = json_dict['catalogue_mode'] - if version >= 3023: # v0.3.023 - self.catalogue_page_size = json_dict['catalogue_page_size'] - if version >= 1004005: # v1.4.005 - self.catalogue_show_filter_flag \ - = json_dict['catalogue_show_filter_flag'] - self.catalogue_alpha_sort_flag \ - = json_dict['catalogue_alpha_sort_flag'] - self.catologue_use_regex_flag \ - = json_dict['catologue_use_regex_flag'] - - if version >= 1002013: # v1.2.013 - self.simple_options_flag = json_dict['simple_options_flag'] - - # Having loaded the config file, set various file paths... - if self.data_dir_use_first_flag: - self.data_dir = self.data_dir_alt_list[0] - - self.downloads_dir = self.data_dir - self.alt_downloads_dir = os.path.abspath( - os.path.join(self.data_dir, 'downloads'), - ) - self.backup_dir = os.path.abspath( - os.path.join(self.data_dir, '.backups'), - ) - self.temp_dir = os.path.abspath(os.path.join(self.data_dir, '.temp')) - self.temp_dl_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp', 'downloads'), - ) - self.temp_test_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp', 'ytdl-test'), - ) - - # ...and update various widgets - - # If the tray icon should be visible, make it visible - if self.show_status_icon_flag: - self.status_icon_obj.show_icon() - - # 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() - - # 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() - - # Update widgets in the Video Catalogue toolbar - self.main_win_obj.catalogue_size_entry.set_text( - str(self.catalogue_page_size), - ) - - self.main_win_obj.update_show_filter_widgets() - self.main_win_obj.update_alpha_sort_widgets() - self.main_win_obj.update_use_regex_widgets() - - # 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, - ) - - - def save_config(self): - - """Called by self.start(), .stop_continue(), switch_db(), - .download_manager_finished(), .update_manager_finished(), - .refresh_manager_finished(), .info_manager_finished(), - .tidy_manager_finished(), .on_menu_save_all(), - - Saves the Tartube config file. If saving fails, disables all file - loading/saving. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 2854 save_config') - - # The config file can be stored at one of two locations, depending on - # whether xdg is available, or not - # v2.0.003. The user can force Tartube to use the config file in the - # script's directory (rather than the one in the location described - # by xdg) by placing a 'settings.json' file there. If that file is - # created when Tartube is already running, it can be an empty file - # (because Tartube overwrites it). Otherwise, it should be a copy of - # a legitimate config file - if self.config_file_xdg_path is None \ - or ( - os.path.isfile(self.config_file_path) \ - and not __main__.__pkg_strict_install_flag__ - ): - config_file_path = self.config_file_path - else: - config_file_path = self.config_file_xdg_path - - # Sanity check - if self.current_manager_obj or self.disable_load_save_flag: - return - - # Prepare values - utc = datetime.datetime.utcfromtimestamp(time.time()) - - # Remember the size of the main window, if required. The minimum - # 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: - (width, height) = self.main_win_obj.get_size() - posn = self.main_win_obj.videos_paned.get_position() - - if width >= int(self.main_win_width / 2): - self.main_win_save_width = width - else: - self.main_win_save_width = self.main_win_width - - if height >= int(self.main_win_height / 2): - self.main_win_save_height = height - else: - self.main_win_save_height = self.main_win_height - - if posn >= self.paned_min_size: - self.main_win_save_posn = posn - else: - self.main_win_save_posn = self.paned_min_size - - # Prepare a dictionary of data to save as a JSON file - json_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(utc.strftime('%d %b %Y')), - 'save_time': str(utc.strftime('%H:%M:%S')), - 'file_type': 'config', - # Data - '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, - 'main_win_save_posn': self.main_win_save_posn, - - 'gtk_emulate_broken_flag': self.gtk_emulate_broken_flag, - - 'toolbar_squeeze_flag': self.toolbar_squeeze_flag, - 'show_tooltips_flag': self.show_tooltips_flag, - 'show_small_icons_in_index': self.show_small_icons_in_index, - 'auto_expand_video_index_flag': self.auto_expand_video_index_flag, - 'disable_dl_all_flag': self.disable_dl_all_flag, - 'show_pretty_dates_flag': self.show_pretty_dates_flag, - - 'show_status_icon_flag': self.show_status_icon_flag, - 'close_to_tray_flag': self.close_to_tray_flag, - - 'progress_list_hide_flag': self.progress_list_hide_flag, - 'results_list_reverse_flag': self.results_list_reverse_flag, - - 'system_error_show_flag': self.system_error_show_flag, - 'system_warning_show_flag': self.system_warning_show_flag, - 'operation_error_show_flag': self.operation_error_show_flag, - 'operation_warning_show_flag': self.operation_warning_show_flag, - 'system_msg_keep_totals_flag': self.system_msg_keep_totals_flag, - - 'data_dir': self.data_dir, - 'data_dir_alt_list': self.data_dir_alt_list, - 'data_dir_use_first_flag': self.data_dir_use_first_flag, - 'data_dir_use_list_flag': self.data_dir_use_list_flag, - 'data_dir_add_from_list_flag': self.data_dir_add_from_list_flag, - - 'db_backup_mode': self.db_backup_mode, - - 'ytdl_bin': self.ytdl_bin, - 'ytdl_path_default': self.ytdl_path_default, - 'ytdl_path': self.ytdl_path, - 'ytdl_update_dict': self.ytdl_update_dict, - 'ytdl_update_list': self.ytdl_update_list, - 'ytdl_update_current': self.ytdl_update_current, - - 'ytdl_output_system_cmd_flag': self.ytdl_output_system_cmd_flag, - 'ytdl_output_stdout_flag': self.ytdl_output_stdout_flag, - 'ytdl_output_ignore_json_flag': self.ytdl_output_ignore_json_flag, - 'ytdl_output_ignore_progress_flag': \ - self.ytdl_output_ignore_progress_flag, - 'ytdl_output_stderr_flag': self.ytdl_output_stderr_flag, - 'ytdl_output_start_empty_flag': self.ytdl_output_start_empty_flag, - 'ytdl_output_show_summary_flag': \ - self.ytdl_output_show_summary_flag, - - 'ytdl_write_system_cmd_flag': self.ytdl_write_system_cmd_flag, - 'ytdl_write_stdout_flag': self.ytdl_write_stdout_flag, - 'ytdl_write_ignore_json_flag': self.ytdl_write_ignore_json_flag, - 'ytdl_write_ignore_progress_flag': \ - self.ytdl_write_ignore_progress_flag, - 'ytdl_write_stderr_flag': self.ytdl_write_stderr_flag, - - 'ytdl_write_verbose_flag': self.ytdl_write_verbose_flag, - - 'refresh_output_videos_flag': self.refresh_output_videos_flag, - 'refresh_output_verbose_flag': self.refresh_output_verbose_flag, - 'refresh_moviepy_timeout': self.refresh_moviepy_timeout, - - 'auto_clone_options_flag': self.auto_clone_options_flag, - - 'disk_space_warn_flag': self.disk_space_warn_flag, - 'disk_space_warn_limit': self.disk_space_warn_limit, - 'disk_space_stop_flag': self.disk_space_stop_flag, - 'disk_space_stop_limit': self.disk_space_stop_limit, - - 'custom_dl_by_video_flag': self.custom_dl_by_video_flag, - 'custom_dl_divert_mode': self.custom_dl_divert_mode, - 'custom_dl_delay_flag': self.custom_dl_delay_flag, - 'custom_dl_delay_max': self.custom_dl_delay_max, - 'custom_dl_delay_min': self.custom_dl_delay_min, - - 'ffmpeg_path': self.ffmpeg_path, - - 'operation_limit_flag': self.operation_limit_flag, - 'operation_check_limit': self.operation_check_limit, - 'operation_download_limit': self.operation_download_limit, - - 'scheduled_dl_mode': self.scheduled_dl_mode, - 'scheduled_dl_wait_hours': self.scheduled_dl_wait_hours, - 'scheduled_dl_last_time': self.scheduled_dl_last_time, - - 'scheduled_check_mode': self.scheduled_check_mode, - 'scheduled_check_wait_hours': self.scheduled_check_wait_hours, - 'scheduled_check_last_time': self.scheduled_check_last_time, - - 'scheduled_shutdown_flag': self.scheduled_shutdown_flag, - - 'autostop_time_flag': self.autostop_time_flag, - 'autostop_time_value': self.autostop_time_value, - 'autostop_time_unit': self.autostop_time_unit, - 'autostop_videos_flag': self.autostop_videos_flag, - 'autostop_videos_value': self.autostop_videos_value, - 'autostop_size_flag': self.autostop_size_flag, - 'autostop_size_value': self.autostop_size_value, - 'autostop_size_unit': self.autostop_size_unit, - - 'operation_auto_update_flag': self.operation_auto_update_flag, - 'operation_save_flag': self.operation_save_flag, - 'operation_sim_shortcut_flag': self.operation_sim_shortcut_flag, - 'operation_dialogue_mode': self.operation_dialogue_mode, - 'operation_convert_mode': self.operation_convert_mode, - 'use_module_moviepy_flag': self.use_module_moviepy_flag, - - 'dialogue_copy_clipboard_flag': self.dialogue_copy_clipboard_flag, - 'dialogue_keep_open_flag': self.dialogue_keep_open_flag, - - 'allow_ytdl_archive_flag': self.allow_ytdl_archive_flag, - 'apply_json_timeout_flag': self.apply_json_timeout_flag, - - 'ignore_child_process_exit_flag': \ - self.ignore_child_process_exit_flag, - 'ignore_http_404_error_flag': self.ignore_http_404_error_flag, - 'ignore_data_block_error_flag': self.ignore_data_block_error_flag, - 'ignore_merge_warning_flag': self.ignore_merge_warning_flag, - 'ignore_missing_format_error_flag': \ - self.ignore_missing_format_error_flag, - 'ignore_no_annotations_flag': self.ignore_no_annotations_flag, - 'ignore_no_subtitles_flag': self.ignore_no_subtitles_flag, - - 'ignore_yt_copyright_flag': self.ignore_yt_copyright_flag, - 'ignore_yt_age_restrict_flag': self.ignore_yt_age_restrict_flag, - 'ignore_yt_uploader_deleted_flag': \ - self.ignore_yt_uploader_deleted_flag, - - 'ignore_custom_msg_list': self.ignore_custom_msg_list, - 'ignore_custom_regex_flag': self.ignore_custom_regex_flag, - - 'num_worker_default': self.num_worker_default, - 'num_worker_apply_flag': self.num_worker_apply_flag, - - 'bandwidth_default': self.bandwidth_default, - 'bandwidth_apply_flag': self.bandwidth_apply_flag, - - 'video_res_default': self.video_res_default, - 'video_res_apply_flag': self.video_res_apply_flag, - - 'match_method': self.match_method, - 'match_first_chars': self.match_first_chars, - 'match_ignore_chars': self.match_ignore_chars, - - 'auto_delete_flag': self.auto_delete_flag, - 'auto_delete_watched_flag': self.auto_delete_watched_flag, - 'auto_delete_days': self.auto_delete_days, - - 'delete_on_shutdown_flag': self.delete_on_shutdown_flag, - 'open_temp_on_desktop_flag': self.open_temp_on_desktop_flag, - - 'complex_index_flag': self.complex_index_flag, - 'catalogue_mode': self.catalogue_mode, - 'catalogue_page_size': self.catalogue_page_size, - 'catalogue_show_filter_flag': self.catalogue_show_filter_flag, - 'catalogue_alpha_sort_flag': self.catalogue_alpha_sort_flag, - 'catologue_use_regex_flag': self.catologue_use_regex_flag, - - 'simple_options_flag': self.simple_options_flag, - } - - # In case a competing instance of Tartube is saving the same config - # file, check for the lockfile and, if it exists, wait a reasonable - # time for it to be released - if not self.debug_ignore_lockfile_flag: - - lock_path = config_file_path + '.lock' - if os.path.isfile(lock_path): - - check_time = time.time() + self.config_lock_time - while time.time < check_time and os.path.isfile(lock_path): - 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', - ) - - return - - # Place our own lock on the config file - if not self.debug_ignore_lockfile_flag: - - try: - fh = open(lock_path, 'a').close() - - except: - - self.disable_load_save( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' config file (file already in use)', - ) - - return - - # Try to save the config file - try: - with open(config_file_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - - 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', - ) - - # Procedure successful; remove the lock - if not self.debug_ignore_lockfile_flag: - os.remove(lock_path) - - - def load_db(self): - - """Called by self.start() and .switch_db(). - - Loads the Tartube database file. If loading fails, disables all file - loading/saving. - - Returns: - - True on success, False on failure - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 3134 load_db') - - # Sanity check - path = os.path.abspath(os.path.join(self.data_dir, self.db_file_name)) - if self.current_manager_obj \ - or not os.path.isfile(path) \ - or self.disable_load_save_flag: - return False - - # If a lockfile already exists, then another competing instance of - # Tartube is already using this database file - if not self.debug_ignore_lockfile_flag: - - lock_path = path + '.lock' - if os.path.isfile(lock_path): - - # (The True argument signals that the user should be prompted - # to artificially remove the lockfile) - self.disable_load_save( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' database file', - True, - ) - - return False - - else: - - # Place our own lock on the database file - try: - fh = open(lock_path, 'a').close() - self.db_lock_file_path = lock_path - - except: - - # (The True argument signals that the user should be - # prompted to artificially remove the lockfile) - self.disable_load_save( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' database file', - True, - ) - - return False - - # Reset main window tabs now so the user can't manipulate their widgets - # during the load - 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 - # loaded - self.main_win_obj.sensitise_widgets_if_database(False) - - # Try to load the database file - try: - fh = open(path, 'rb') - load_dict = pickle.load(fh) - fh.close() - - except: - self.remove_db_lock_file() - self.disable_load_save( - 'Failed to load the ' + __main__.__prettyname__ \ - + ' database file', - ) - - return False - - # Do some basic checks on the loaded data - if not load_dict \ - or not 'script_name' in load_dict \ - or not 'script_version' in load_dict \ - or not 'save_date' in load_dict \ - or not 'save_time' in load_dict \ - or load_dict['script_name'] != __main__.__packagename__: - - self.remove_db_lock_file() - self.file_error_dialogue( - 'The ' + __main__.__prettyname__ + ' database file is invalid', - ) - - return False - - # Convert a version, e.g. 1.234.567, into a simple number, e.g. - # 1234567, that can be compared with other versions - version = self.convert_version(load_dict['script_version']) - # Now check that the database file wasn't written by a more recent - # version of Tartube (which this older version might not be able to - # read) - if version is None \ - or version > self.convert_version(__main__.__version__): - - self.remove_db_lock_file() - self.disable_load_save( - 'Database file can\'t be read\nby this version of ' \ - + __main__.__prettyname__, - ) - - return False - - # Before v1.3.099, self.data_dir and self.downloads_dir were different - # If a /downloads directory exists, then the data directory is using - # the old structure - old_flag = False - if os.path.isdir(self.alt_downloads_dir): - - # Use the old location of self.downloads_dir - old_flag = True - self.downloads_dir = self.alt_downloads_dir - # Move any database backup files to their new location - self.move_backup_files() - - else: - - # Use the new location - self.downloads_dir = self.data_dir - - # Set IVs to their new values - self.general_options_obj = load_dict['general_options_obj'] - self.media_reg_count = load_dict['media_reg_count'] - 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'] - 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'] - self.fixed_temp_folder = load_dict['fixed_temp_folder'] - self.fixed_misc_folder = load_dict['fixed_misc_folder'] - if version >= 1004028: # v1.4.028 - self.fixed_bookmark_folder = load_dict['fixed_bookmark_folder'] - self.fixed_waiting_folder = load_dict['fixed_waiting_folder'] - - # 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 - # To prevent problems when that happens, preemptively rename any media - # data object called 'downloads' - if old_flag and 'downloads' in self.media_name_dict: - - dbid = self.media_name_dict['downloads'] - media_data_obj = self.media_reg_dict[dbid] - - # Generate a new name; the function returns None on failure - new_name = utils.find_available_name(self, 'downloads') - if new_name is not None: - self.rename_container_silently(media_data_obj, new_name) - - # 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 self.debug_hide_folders_flag: - self.fixed_all_folder.set_hidden_flag(True) - self.fixed_fav_folder.set_hidden_flag(True) - self.fixed_new_folder.set_hidden_flag(True) - self.fixed_temp_folder.set_hidden_flag(True) - self.fixed_misc_folder.set_hidden_flag(True) - - # Now that a database file has been loaded, most main window widgets - # can be sensitised... - self.main_win_obj.sensitise_widgets_if_database(True) - # ...and saving the database file is now allowed - self.allow_db_save_flag = True - - # Repopulate the Video Index, showing the new data - if self.main_win_obj: - self.main_win_obj.video_index_populate() - - return True - - - def update_db(self, version): - - """Called by self.load_db(). - - When the Tartube database created by a previous version of Tartube is - loaded, update IVs as required. - - Args: - - version (int): The version of Tartube that created the database, - already converted to a simple integer by self.convert_version() - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 3389 update_db') - - # (self.fixed_bookmark_folder and self.fixed_waiting_folder, having - # been added later, are not required by this list) - fixed_folder_list = [ - self.fixed_all_folder, - self.fixed_fav_folder, - self.fixed_new_folder, - ] - - options_obj_list = [self.general_options_obj] - for media_data_obj in self.media_reg_dict.values(): - if media_data_obj.options_obj is not None \ - and not media_data_obj.options_obj in options_obj_list: - options_obj_list.append(media_data_obj.options_obj) - - if version < 3012: # v0.3.012 - - # This version fixed some problems, in which the deletion of media - # data objects was not handled correctly - # Repair the media data registry, as required - for folder_obj in fixed_folder_list: - - # Check that videos in 'All Videos', 'New Videos' and - # 'Favourite Videos' still exist in the media data registry - copy_list = folder_obj.child_list.copy() - for child_obj in copy_list: - if isinstance(child_obj, media.Video) \ - and not child_obj.parent_obj.dbid in self.media_reg_dict: - folder_obj.del_child(child_obj) - - # Video counts in 'All Videos', 'New Videos' and 'Favourite - # Videos' might be wrong - vid_count = new_count = fav_count = dl_count = 0 - - for child_obj in folder_obj.child_list: - if isinstance(child_obj, media.Video): - vid_count += 1 - - if child_obj.new_flag: - new_count += 1 - - if child_obj.fav_flag: - fav_count += 1 - - if child_obj.dl_flag: - dl_count += 1 - - folder_obj.reset_counts( - vid_count, - 0, - dl_count, - fav_count, - new_count, - 0, - ) - - if version < 4003: # v0.4.002 - - # This version fixes video format options, which were stored - # incorrectly in options.OptionsManager - key_list = [ - 'video_format', - 'second_video_format', - 'third_video_format', - ] - - for options_obj in options_obj_list: - for key in key_list: - - val = options_obj.options_dict[key] - if val != '0': - - if val in formats.VIDEO_OPTION_DICT: - # Invert the key-value pair used before v0.4.002 - options_obj.options_dict[key] \ - = formats.VIDEO_OPTION_DICT[val] - - else: - # Completely invalid format description, so - # just reset it - options_obj.options_dict[key] = '0' - -# if version < 4004: # v0.4.004 -# -# # This version fixes a bug in which moving a channel, playlist or -# # folder to a new location in the media data registry's tree -# # failed to update all the videos that moved with it -# # To be safe, update every video in the registry -# for media_data_obj in self.media_reg_dict.values(): -# if isinstance(media_data_obj, media.Video): -# media_data_obj.reset_file_dir() - - if version < 4015: # v0.4.015 - - # 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) - container_list = [ - self.fixed_all_folder, - self.fixed_new_folder, - self.fixed_fav_folder, - self.fixed_misc_folder, - self.fixed_temp_folder, - ] - - for dbid in self.media_name_dict.values(): - container_list.append(self.media_reg_dict[dbid]) - - for container_obj in container_list: - container_obj.sort_children() - - if version < 4022: # v0.4.022 - - # This version fixes a rare issue in which media.Video.index was - # set to a string, rather than int, value - # Update all existing videos - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.index is not None: - media_data_obj.index = int(media_data_obj.index) - - if version < 6003: # v0.6.003 - - # This version fixes an issue in which deleting an individual video - # and then re-adding the same video, downloading it then deleting - # it a second time, messes up the parent container's count IVs - # Nothing for it but to recalculate them all, just in case - for dbid in self.media_name_dict.values(): - container_obj = self.media_reg_dict[dbid] - - vid_count = new_count = fav_count = dl_count = 0 - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - vid_count += 1 - - if child_obj.new_flag: - new_count += 1 - - if child_obj.fav_flag: - fav_count += 1 - - if child_obj.dl_flag: - dl_count += 1 - - container_obj.reset_counts( - vid_count, - 0, - dl_count, - fav_count, - new_count, - 0, - ) - - if version < 1000013: # v1.0.013 - - # This version adds nicknames to channels, playlists and folders - for dbid in self.media_name_dict.values(): - container_obj = self.media_reg_dict[dbid] - container_obj.nickname = container_obj.name - - if version < 1000031: # v1.0.031 - - # This version adds nicknames to videos. If the database is large, - # warn the user before continuing - 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.', - 'info', - 'ok', - self.main_win_obj, - ) - - dialogue_win.set_modal(True) - - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - - media_data_obj.nickname = media_data_obj.name - - # If the video's JSON data has been saved, we can use that - # to set the nickname - json_path = media_data_obj.get_actual_path_by_ext( - self, - '.info.json', - ) - - if os.path.isfile(json_path): - json_dict = self.file_manager_obj.load_json(json_path) - if 'title' in json_dict: - media_data_obj.nickname = json_dict['title'] - - - if version < 1001031: # v1.1.031 - - # This version adds the ability to disable checking/downloading for - # media data objects - for dbid in self.media_name_dict.values(): - media_data_obj = self.media_reg_dict[dbid] - media_data_obj.dl_disable_flag = False - - if version < 1001032: # v1.1.032 - - # This version adds video archiving. Archived videos cannot be - # auto-deleted - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - media_data_obj.archive_flag = False - - if version < 1001037: # v1.1.037 - - # This version adds alternative destination directories for a - # channel's/playlist's/folder's videos, thumbnails (etc) - for dbid in self.media_name_dict.values(): - media_data_obj = self.media_reg_dict[dbid] - media_data_obj.master_dbid = media_data_obj.dbid - media_data_obj.slave_dbid_list = [] - - if version < 1001045: # v1.1.045 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['use_fixed_folder'] = None - - if version < 1001060: # v1.1.060 - - # This version adds new options to options.OptionsManager - for options_obj in options_obj_list: - options_obj.options_dict['abort_on_error'] = False - - options_obj.options_dict['socket_timeout'] = '' - options_obj.options_dict['source_address'] = '' - options_obj.options_dict['force_ipv4'] = False - options_obj.options_dict['force_ipv6'] = False - - options_obj.options_dict['geo_verification_proxy'] = '' - options_obj.options_dict['geo_bypass'] = False - options_obj.options_dict['no_geo_bypass'] = False - options_obj.options_dict['geo_bypass_country'] = '' - options_obj.options_dict['geo_bypass_ip_block'] = '' - - options_obj.options_dict['match_title_list'] = [] - options_obj.options_dict['reject_title_list'] = [] - - options_obj.options_dict['date'] = '' - options_obj.options_dict['date_before'] = '' - options_obj.options_dict['date_after'] = '' - options_obj.options_dict['min_views'] = 0 - options_obj.options_dict['max_views'] = 0 - options_obj.options_dict['match_filter'] = '' - options_obj.options_dict['age_limit'] = '' - options_obj.options_dict['include_ads'] = False - - options_obj.options_dict['playlist_reverse'] = False - options_obj.options_dict['playlist_random'] = False - options_obj.options_dict['prefer_ffmpeg'] = False - options_obj.options_dict['external_downloader'] = '' - options_obj.options_dict['external_arg_string'] = '' - - options_obj.options_dict['force_encoding'] = '' - options_obj.options_dict['no_check_certificate'] = False - options_obj.options_dict['prefer_insecure'] = False - - options_obj.options_dict['all_formats'] = False - options_obj.options_dict['prefer_free_formats'] = False - options_obj.options_dict['yt_skip_dash'] = False - options_obj.options_dict['merge_output_format'] = '' - - options_obj.options_dict['subs_format'] = '' - - options_obj.options_dict['two_factor'] = '' - options_obj.options_dict['net_rc'] = False - - options_obj.options_dict['recode_video'] = '' - options_obj.options_dict['pp_args'] = '' - options_obj.options_dict['fixup_policy'] = '' - options_obj.options_dict['prefer_avconv'] = False - options_obj.options_dict['prefer_ffmpeg'] = False - - options_obj.options_dict['write_annotations'] = True - options_obj.options_dict['keep_annotations'] = False - options_obj.options_dict['sim_keep_annotations'] = False - - # (Also rename one option) - options_obj.options_dict['extract_audio'] \ - = options_obj.options_dict['to_audio'] - options_obj.options_dict.pop('to_audio') - -# if version < 1003004: # v1.3.004 -# -# # The way that directories are stored in media.VideoObj.file_dir -# # has changed. Reset those values for all video objects -# for media_data_obj in self.media_reg_dict.values(): -# if isinstance(media_data_obj, media.Video): -# -# media_data_obj.reset_file_dir() - - if version < 1003009: # v1.3.009 - - # In earlier versions, - # refresh.RefreshManager.refresh_from_default_destination() set a - # video's .name, but not its .nickname - # The .refresh_from_default_destination() is already fixed, but we - # need to check every video in the database, and set its - # .nickname if not set - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - if ( - media_data_obj.nickname is None \ - or media_data_obj.nickname == self.default_video_name - ) and media_data_obj.name is not None \ - and media_data_obj.name != self.default_video_name: - media_data_obj.nickname = media_data_obj.name - - if version < 1003017: # v1.3.017 - - for options_obj in options_obj_list: - - # In earlier versions, the 'prefer_ffmpeg' and - # 'hls_prefer_ffmpeg' download options had been confused - options_obj.options_dict['hls_prefer_ffmpeg'] = False - - # In earlier versions, MS Windows users could set the - # 'prefer_ffmpeg' and 'prefer_avconv' options, even though - # the MS Windows installer does not provide AVConv. Reset - # both values - options_obj.options_dict['prefer_ffmpeg'] = False - options_obj.options_dict['prefer_avconv'] = False - - # In earlier versions, the download options 'video_format', - # 'second_video_format' and/or 'third_video_format' could - # incorrectly be set to a sound format like 'mp3'. This is - # not the way youtube-dl-gui was supposed to implement its - # formats; remove them, if the user has specified them - if not options_obj.options_dict['third_video_format'] \ - in formats.VIDEO_OPTION_DICT: - options_obj.options_dict['third_video_format'] = '0' - - if not options_obj.options_dict['second_video_format'] \ - in formats.VIDEO_OPTION_DICT: - options_obj.options_dict['second_video_format'] = '0' - if options_obj.options_dict['third_video_format'] != '0': - options_obj.options_dict['second_video_format'] \ - = options_obj.options_dict['third_video_format'] - options_obj.options_dict['third_video_format'] = '0' - - if not options_obj.options_dict['video_format'] \ - in formats.VIDEO_OPTION_DICT: - options_obj.options_dict['video_format'] = '0' - if options_obj.options_dict['second_video_format'] != '0': - options_obj.options_dict['video_format'] \ - = options_obj.options_dict['second_video_format'] - options_obj.options_dict['second_video_format'] \ - = options_obj.options_dict['third_video_format'] - - if version < 1003106: # v1.3.106 - - # This version adds a new option to options.OptionsManager - for options_obj in options_obj_list: - if options_obj.options_dict['subs_lang'] == '': - options_obj.options_dict['subs_lang_list'] = [] - else: - options_obj.options_dict['subs_lang_list'] \ - = [ options_obj.options_dict['subs_lang'] ] - - if version < 1003110: # v1.3.110 - - # Before this version, the 'output_template' in - # options.OptionManager was completely broken, containing both - # the filepath to this file, and an '%(uploader)s string that - # broke the structure of Tartube's data directory - # Reset the value if it seems to contain either - for options_obj in options_obj_list: - output_template = options_obj.options_dict['output_template'] - if re.search(sys.path[0], output_template) \ - or re.search('\%\(uploader\)s', output_template): - options_obj.options_dict['output_template'] \ - = '%(title)s.%(ext)s' - - if version < 1003111: # v1.3.111 - - # In this version, formats.py.FILE_OUTPUT_NAME_DICT and - # .FILE_OUTPUT_CONVERT_DICT, so that the custom format's index - # is 0 (was 3) - for options_obj in options_obj_list: - output_format = options_obj.options_dict['output_format'] - if output_format == 3: - options_obj.options_dict['output_format'] = 0 - elif output_format < 3: - 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 < 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 - container_obj.recalculate_counts() - - if version < 1004043: # v1.4.043 - - # This version removes an IV from media.Video objects - for media_data_obj in self.media_reg_dict.values(): - if isinstance(media_data_obj, media.Video): - del media_data_obj.file_dir - - - def save_db(self): - - """Called by self.start(), .stop_continue(), .switch_db(), - .fix_integrity_db(), .download_manager_finished(), - .update_manager_finished(), .refresh_manager_finished(), - .info_manager_finished(), .tidy_manager_finished(), - .move_container_to_top_continue(), .move_container_continue(), - .rename_container(), .on_menu_save_all() and .on_menu_save_db(). - - Saves the Tartube database file. If saving fails, disables all file - loading/saving. - - Returns: - - True on success, False on failure - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 3839 save_db') - - # Sanity check - if self.current_manager_obj \ - or self.disable_load_save_flag \ - or not self.allow_db_save_flag: - return False - - # Prepare values - utc = datetime.datetime.utcfromtimestamp(time.time()) - path = os.path.abspath(os.path.join(self.data_dir, self.db_file_name)) - bu_path = os.path.abspath( - os.path.join( - self.backup_dir, - __main__.__packagename__ + '_BU.db', - ), - ) - temp_bu_path = os.path.abspath( - os.path.join( - self.backup_dir, - __main__.__packagename__ + '_TEMP_BU.db', - ), - ) - - # Prepare a dictionary of data to save, using Python pickle - save_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(utc.strftime('%d %b %Y')), - 'save_time': str(utc.strftime('%H:%M:%S')), - # Data - 'general_options_obj' : self.general_options_obj, - 'media_reg_count': self.media_reg_count, - 'media_reg_dict': self.media_reg_dict, - 'media_name_dict': self.media_name_dict, - 'media_top_level_list': self.media_top_level_list, - 'fixed_all_folder': self.fixed_all_folder, - 'fixed_bookmark_folder': self.fixed_bookmark_folder, - 'fixed_fav_folder': self.fixed_fav_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, - } - - # Back up any existing file - if os.path.isfile(path): - try: - shutil.copyfile(path, temp_bu_path) - - 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', - ) - - return False - - # If there is no lock already in place (for example, because this is a - # new database file), then create a lockfile - if not self.debug_ignore_lockfile_flag: - - if self.db_lock_file_path is None: - - lock_path = path + '.lock' - if os.path.isfile(lock_path): - - self.system_error( - 101, - 'Database file \'' + lock_path + '\' already exists,' \ - + ' and is locked', - ) - - return False - - else: - - # Place our own lock on the database file - try: - fh = open(lock_path, 'a').close() - self.db_lock_file_path = lock_path - - except: - - self.disable_load_save( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' database file (file already in use)', - ) - - return False - - # Try to save the database file - try: - fh = open(path, 'wb') - pickle.dump(save_dict, fh) - fh.close() - - except: - - self.disable_load_save() - - 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', - ) - - else: - self.file_error_dialogue( - 'Failed to save the ' + __main__.__prettyname__ \ - + ' database file\n\nFile load/save has been' \ - + ' disabled', - ) - - return False - - # In the event that there was no database file to backup, then the - # following code isn't necessary - if os.path.isfile(temp_bu_path): - - # Make the backup file permanent, or not, depending on settings - if self.db_backup_mode == 'default': - os.remove(temp_bu_path) - - elif self.db_backup_mode == 'single': - - # (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(temp_bu_path, bu_path) - - elif self.db_backup_mode == 'daily': - - daily_bu_path = os.path.abspath( - os.path.join( - self.backup_dir, - __main__.__packagename__ + '_BU_' \ - + str(utc.strftime('%Y_%m_%d')) + '.db', - ), - ) - - # Only make a new backup file once per day - if not os.path.isfile(daily_bu_path): - - if os.path.isfile(daily_bu_path): - os.remove(daily_bu_path) - - shutil.move(temp_bu_path, daily_bu_path) - - else: - - os.remove(temp_bu_path) - - elif self.db_backup_mode == 'always': - - always_bu_path = os.path.abspath( - os.path.join( - self.backup_dir, - __main__.__packagename__ + '_BU_' \ - + str(utc.strftime('%Y_%m_%d_%H_%M_%S')) + '.db', - ), - ) - - if os.path.isfile(always_bu_path): - os.remove(always_bu_path) - - shutil.move(temp_bu_path, always_bu_path) - - # Saving a database file, in order to create a new file, is much like - # loading one: main window widgets can now be sensitised - self.main_win_obj.sensitise_widgets_if_database(True) - - # Save succeeded - return True - - - def switch_db(self, data_list): - - """Called by config.SystemPrefWin.try_switch_db(). - - When the user selects a new location for a data directory, first save - our existing database. - - Then load the database at the new location, if exists, or create a new - database there, if not. - - Args: - - data_list (list): A list containing two items: the full file path - to the location of the new data directory, and the system - preferences window (config.SystemPrefWin) that the user has - open - - Returns: - - True on success, False on failure - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4050 switch_db') - - # Extract values from the argument list - path = data_list.pop(0) - pref_win_obj = data_list.pop(0) - - # Sanity check - if self.current_manager_obj or self.disable_load_save_flag: - return False - - # If the old path is the same as the new one, we don't need to do - # anything - if path == self.data_dir: - return False - - # Save the existing database, and release its lockfile - if not self.save_db(): - return False - else: - self.remove_db_lock_file() - - # Delete Tartube's temporary folder from the filesystem - if os.path.isdir(self.temp_dir): - shutil.rmtree(self.temp_dir) - - # Update IVs for the new location of the data directory - self.data_dir = path - self.downloads_dir = path - self.alt_downloads_dir = os.path.abspath( - os.path.join(path, 'downloads'), - ) - self.backup_dir = os.path.abspath(os.path.join(path, '.backups')) - self.temp_dir = os.path.abspath(os.path.join(path, '.temp')) - self.temp_dl_dir = os.path.abspath( - os.path.join(path, '.temp', 'downloads'), - ) - self.temp_test_dir = os.path.abspath( - os.path.join(path, '.temp', 'ytdl-test'), - ) - - if self.data_dir_add_from_list_flag \ - and not self.data_dir in self.data_dir_alt_list: - self.data_dir_alt_list.append(self.data_dir) - - # Before v1.3.099, self.data_dir and self.downloads_dir were different - # If a /downloads directory exists, then the data directory is using - # the old structure - if os.path.isdir(self.alt_downloads_dir): - - # Use the old location of self.downloads_dir - self.downloads_dir = self.alt_downloads_dir - - else: - - # Use the new location - self.downloads_dir = self.data_dir - - # Any of those directories that don't exist should be created - if not os.path.isdir(self.data_dir): - # React to a 'Permission denied' error by asking the user what to - # do next. If necessary, shut down Tartube - # The True argument means that the drive is unwriteable - if not self.make_directory(self.data_dir): - return False - - if not os.path.isdir(self.backup_dir): - if not self.make_directory(self.backup_dir): - return False - - # (The temporary data directory should be emptied, if it already - # exists) - if os.path.isdir(self.temp_dir): - try: - shutil.rmtree(self.temp_dir) - - except: - if not self.make_directory(self.temp_dir): - return False - else: - shutil.rmtree(self.temp_dir) - - if not os.path.isdir(self.temp_dir): - if not self.make_directory(self.temp_dir): - return self.main_win_obj.destroy() - - if not os.path.isdir(self.temp_dl_dir): - if not self.make_directory(self.temp_dl_dir): - return self.main_win_obj.destroy() - - # If the database file itself exists; load it. If not, create it - db_path = os.path.abspath( - os.path.join(self.data_dir, self.db_file_name), - ) - if not os.path.isfile(db_path): - - # Reset main window widgets - 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() - - # Create a new database file - self.save_db() - - # Save the config file, to preserve the new location of the data - # directory - self.save_config() - - # Repopulate the Video Index, showing the new data - self.main_win_obj.video_index_populate() - - # If the system preferences window is open, reset it to show the - # new data directory - if pref_win_obj and pref_win_obj.is_visible(): - - pref_win_obj.reset_window() - pref_win_obj.select_switch_db_tab() - - self.dialogue_manager_obj.show_msg_dialogue( - 'Database file created', - 'info', - 'ok', - pref_win_obj, - ) - - else: - - # (Parent window is the main window) - self.dialogue_manager_obj.show_msg_dialogue( - 'Database file created', - 'info', - 'ok', - ) - - return True - - else: - - if not self.load_db(): - - return False - - else: - - # Save the config file, to preserve the new location of the - # data directory - self.save_config() - return True - - - def choose_alt_db(self): - - """Called by self.start() (only), shortly after loading (or creating) - the config file. - - Multiple instances of Tartube can share the same config file, but not - the same database file. - - If the database file specified by the config file we've just loaded - is locked (meaning it's in use by another instance), we might be - able to use one of the alternative data directories specified by the - user. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4219 choose_alt_db') - - db_file_path = os.path.abspath( - os.path.join(self.data_dir, self.db_file_name), - ) - - lock_file_path = db_file_path + '.lock' - - if not os.path.exists(self.data_dir) \ - or not os.path.isfile(db_file_path) \ - or ( - os.path.isfile(lock_file_path) \ - and not self.debug_ignore_lockfile_flag - ): - for alt_data_dir in self.data_dir_alt_list: - - alt_db_file_path = os.path.abspath( - os.path.join(alt_data_dir, self.db_file_name), - ) - - alt_lock_file_path = alt_db_file_path + '.lock' - - if os.path.exists(alt_data_dir) \ - and os.path.isfile(alt_db_file_path) \ - and ( - not os.path.isfile(alt_lock_file_path) \ - or self.debug_ignore_lockfile_flag - ): - # Try loading this database instead - self.data_dir = alt_data_dir - # (Update other IVs to match) - self.downloads_dir = self.data_dir - - self.alt_downloads_dir = os.path.abspath( - os.path.join(self.data_dir, 'downloads'), - ) - self.backup_dir = os.path.abspath( - os.path.join(self.data_dir, '.backups'), - ) - self.temp_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp'), - ) - self.temp_dl_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp', 'downloads'), - ) - self.temp_test_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp', 'ytdl-test'), - ) - - return - - - def forget_db(self, data_list): - - """Called by config.SystemPrefWin.on_data_dir_forget_button_clicked(). - - When the user selects a data directory to be forgotten (i.e. removed - from self.data_dir_alt_list), perform that action. - - Args: - - data_list (list): A list containing two items: the full file path - to the location of the selected data directory, and the system - preferences window (config.SystemPrefWin) that the user has - open - - Returns: - - True on success, False on failure - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4292 forget_db') - - # Extract values from the argument list - path = data_list.pop(0) - pref_win_obj = data_list.pop(0) - - # Sanity check. It shouldn't be possible to select the current data - # directory, but we'll check anyway - if self.current_manager_obj \ - or self.disable_load_save_flag \ - or path == self.data_dir: - return False - - # Update the IV - if path in self.data_dir_alt_list: - self.data_dir_alt_list.remove(path) - - # If the system preferences window is open, reset it to show the new - # contents of the IV - if pref_win_obj and pref_win_obj.is_visible(): - pref_win_obj.reset_window() - pref_win_obj.select_switch_db_tab() - - # Procedure complete - return True - - - def forget_all_db(self, pref_win_obj=None): - - """Called by - config.SystemPrefWin.on_data_dir_forget_all_button_clicked(). - - When the user wants to forget all data directories except the current - one, perform that action. - - Args: - - pref_win_obj (config.SystemPrefWin): The system preferences window - that the user has open, if any - - Returns: - - True on success, False on failure - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4339 forget_all_db') - - # Sanity check - if self.current_manager_obj or self.disable_load_save_flag: - return False - - # Update the IV - self.data_dir_alt_list = [ self.data_dir ] - - # If the system preferences window is open, reset it to show the new - # contents of the IV - if pref_win_obj and pref_win_obj.is_visible(): - pref_win_obj.reset_window() - pref_win_obj.select_switch_db_tab() - - # Procedure complete - 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(). - - Resets media registry IVs, so that a new Tartube database file can be - created. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4410 reset_db') - - # Reset IVs to their default states - self.general_options_obj = options.OptionsManager() - self.media_reg_count = 0 - self.media_reg_dict = {} - self.media_name_dict = {} - self.media_top_level_list = [] - self.fixed_all_folder = None - self.fixed_bookmark_folder = None - self.fixed_fav_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 - # self.fixed_all_folder, etc) - self.create_system_folders() - - - def check_integrity_db(self): - - """Called by config.SystemPrefWin.on_data_check_button_clicked(). - - In case the Tartube database contains inconsistencies of any kind (for - example, an earlier failure in mainwin.DeleteContainerDialogue left - some channel/playlist/folder objects in a half-deleted state), check - the database for inconsistencies. - - If inconsistencies are found, prompt the user for permission to - repair them. The repair process only updates Tartube IVs; it doesn't - delete any files or folders in the filesystem. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4446 check_integrity_db') - - # Basic checks - if self.disable_load_save_flag: - - self.system_error( - 102, - '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', - 'error', - 'ok', - ) - - return - - # Check the database, looking for: media.Video, media.Channel, - # media.Playlist and media.Folder objects (or their .dbids) that, - # due to some problem or other, appear in one IV but not another - # If inconsistencies are found, add them to this dictionary, and - # then apply the fixes once we've finished checking everything - error_reg_dict = {} - # (Two additional dictionaries for recording any errors in the - # .master_dbid and .slave_dbid_list IVs, which are fixed separately) - error_master_dict = {} - error_slave_dict = {} - - # Check that entries in self.media_name_dict appear in - # self.media_reg_dict - for dbid in self.media_name_dict.values(): - if not dbid in self.media_reg_dict: - error_reg_dict[dbid] = None - - # Check that entries in self.media_top_level_list appear in - # self.media_reg_dict - for dbid in self.media_top_level_list: - 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 - # list of all containers - toplevel_container_obj_list = [] - for dbid in self.media_top_level_list: - if not dbid in error_reg_dict: - toplevel_container_obj_list.append(self.media_reg_dict[dbid]) - - full_container_obj_list = [] - for container_obj in toplevel_container_obj_list: - - full_container_obj_list.extend( - container_obj.compile_all_containers( [] ), - ) - - # Make a copy of self.media_reg_dict... - check_reg_dict = self.media_reg_dict.copy() - # ...then compare the list of containers (and their child videos), - # looking for any which don't appear in self.media_reg_dict - for container_obj in full_container_obj_list: - - if container_obj.dbid in self.media_reg_dict: - - # Container OK - if container_obj.dbid in check_reg_dict: - del check_reg_dict[container_obj.dbid] - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - - if child_obj.dbid in self.media_reg_dict: - # Child video OK - if child_obj.dbid in check_reg_dict: - del check_reg_dict[child_obj.dbid] - - else: - # Child video not OK - error_reg_dict[child_obj.dbid] = child_obj - - else: - # Container not OK - error_reg_dict[container_obj.dbid] = container_obj - - # Anything left in check_reg_dict shouldn't be there - for dbid in check_reg_dict: - error_reg_dict[dbid] = check_reg_dict[dbid] - - # Check every media data object's parent - for media_data_obj in self.media_reg_dict.values(): - if media_data_obj.parent_obj is not None \ - and ( - not media_data_obj.parent_obj.dbid in self.media_reg_dict \ - or isinstance(media_data_obj.parent_obj, media.Video) \ - or not media_data_obj in media_data_obj.parent_obj.child_list - ): - error_reg_dict[media_data_obj.dbid] = media_data_obj - - # Check every media data object's children (but don't check private - # folders, as their children are also stored in a different - # channel/playlist/folder) - for media_data_obj in self.media_reg_dict.values(): - - if not isinstance(media_data_obj, media.Video) \ - and ( - not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.priv_flag - ): - for child_obj in media_data_obj.child_list: - if child_obj.parent_obj is None \ - or child_obj.parent_obj != media_data_obj: - error_reg_dict[child_obj.dbid] = child_obj - - # Check alternative download destinations for each channel/playlist/ - # folder - for media_data_obj in self.media_reg_dict.values(): - if not isinstance(media_data_obj, media.Video): - - # (Check the destination still exists in the media data - # registry) - if media_data_obj.master_dbid is not None \ - and not media_data_obj.master_dbid in self.media_reg_dict: - - error_master_dict[media_data_obj.dbid] = media_data_obj - - for slave_dbid in media_data_obj.slave_dbid_list: - if not slave_dbid in self.media_reg_dict: - error_slave_dict[media_data_obj.dbid] = media_data_obj - - # Initial check complete. Any media data object in error_reg_dict - # must have its children added too (we can't remove an object from - # the database, and not its children) - for dbid in error_reg_dict: - - media_data_obj = error_reg_dict[dbid] - if media_data_obj is not None \ - and not isinstance(media_data_obj, media.Video): - - descendant_list = media_data_obj.compile_all_containers( [] ) - for descendant_obj in descendant_list: - - error_reg_dict[descendant_obj.dbid] = descendant_obj - for child_obj in descendant_obj.child_list: - if isinstance(child_obj, media.Video): - error_reg_dict[child_obj.dbid] = child_obj - - # Failsafe check: it shouldn't be possible for system folders to be - # in error_reg_dict, but check anyway, and remove them if found - mod_error_reg_dict = {} - for dbid in error_reg_dict.keys(): - - media_data_obj = error_reg_dict[dbid] - - # (The corresponding media.Video, media.Channel, media.Playlist or - # media.Folder may be known, or not) - if media_data_obj is None \ - or not isinstance(media_data_obj, media.Folder) \ - or not media_data_obj.fixed_flag: - mod_error_reg_dict[dbid] = media_data_obj - - # Check complete - if not mod_error_reg_dict \ - and not error_master_dict \ - and not error_slave_dict: - - self.dialogue_manager_obj.show_msg_dialogue( - 'Database check complete, no inconsistencies found', - 'info', - 'ok', - ) - - return - - else: - - total = len(error_reg_dict) + len(error_master_dict) \ - + len(error_slave_dict) - - # 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)', - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .fix_integrity_db() - { - 'yes': 'fix_integrity_db', - 'data': [ - mod_error_reg_dict, - error_master_dict, - error_slave_dict, - ], - }, - ) - - - def fix_integrity_db(self, data_list): - - """Called by self.check_integrity_db(). - - After the user has given permission to fix inconsistencies in the - Tartube database, perform the repairs, and save files. - - The repair process only updates Tartube IVs; it doesn't delete any - files or folders in the filesystem. - - Args: - - data_list (list): A list containing three dictionaries; in the - form: - - error_reg_dict[dbid] = media_data_obj - error_reg_dict[dbid] = None - - (A general dictionary of errors to fix. All references to - the media data objects in this dictionary are removed from - all IVs) - - error_master_dict[dbid] = media_data_obj - - (A dictionary of errors in a channel/playlist/folder's - .master_dbid IV, which are fixed separately) - - error_slave_dict[dbid] = media_data_obj - - (A dictionary of errors in a channel/playlist/folder's - .slave_dbid_list IV, which are fixed separately) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4686 fix_integrity_db') - - # Extract the arguments - error_reg_dict = data_list.pop(0) - error_master_dict = data_list.pop(0) - error_slave_dict = data_list.pop(0) - - # Update mainapp.TartubeApp IVs - for dbid in error_reg_dict.keys(): - - # (The corresponding media.Video, media.Channel, media.Playlist or - # media.Folder may be known, or not) - error_obj = error_reg_dict[dbid] - - if dbid in self.media_reg_dict: - del self.media_reg_dict[dbid] - - if error_obj is not None \ - and error_obj.name in self.media_name_dict: - del self.media_name_dict[error_obj.name] - - if dbid in self.media_top_level_list: - self.media_top_level_list.remove(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(): - if not isinstance(media_data_obj, media.Video): - - remove_list = [] - for child_obj in media_data_obj.child_list: - - if child_obj.dbid in error_reg_dict: - remove_list.append(child_obj) - - for child_obj in remove_list: - media_data_obj.child_list.remove(child_obj) - - # Recalculate counts for all channels/playlists/folders - for dbid in self.media_name_dict.values(): - media_data_obj = self.media_reg_dict[dbid] - media_data_obj.recalculate_counts() - - # Deal with alternative download destinations - for media_data_obj in error_master_dict.values(): - - if not media_data_obj.master_dbid in self.media_reg_dict: - media_data_obj.set_master_dbid(self, media_data_obj.dbid) - - for media_data_obj in error_slave_dict.values(): - - del_list = [] - for slave_dbid in media_data_obj.slave_dbid_list: - if not slave_dbid in self.media_reg_dict: - del_list.append(slave_dbid) - - for slave_dbid in del_list: - media_data_obj.del_slave_dbid(slave_dbid) - - # Save the database file (unless load/save has been disabled very - # recently) - if not self.disable_load_save_flag: - self.save_db() - - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - # Show confirmation - self.dialogue_manager_obj.show_msg_dialogue( - 'Database inconsistencies repaired', - 'info', - 'ok', - ) - - - def auto_delete_old_videos(self): - - """Called by self.load_db(). - - After loading the Tartube database, auto-delete any old downloaded - videos (if auto-deletion is enabled) - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4770 auto_delete_old_videos') - - if not self.auto_delete_flag: - return - - # Calculate the system time before which any downloaded videos can be - # deleted - time_limit = int(time.time()) - (self.auto_delete_days * 24 * 60 * 60) - - # Import a list of media data objects (as self.media_reg_dict will be - # modified during this procedure) - media_list = list(self.media_reg_dict.values()) - - # Auto-delete any videos as required - for media_data_obj in media_list: - - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.dl_flag \ - and not media_data_obj.archive_flag \ - and media_data_obj.receive_time < time_limit \ - and ( - not self.auto_delete_watched_flag \ - or not media_data_obj.new_flag - ): - # Ddelete this video - self.delete_video(media_data_obj, True, True, True) - - - def convert_version(self, version): - - """Can be called by anything, but mostly called by self.load_config() - and load_db(). - - Converts a Tartube version number, a string in the form '1.234.567', - into a simple integer in the form 1234567. - - The calling function can then compare the version number for this - installation of Tartube with the version number that created the file. - - Args: - - version (str): A string in the form '1.234.567' - - Returns: - - The simple integer, or None if the 'version' argument was invalid - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4820 convert_version') - - num_list = version.split('.') - if len(num_list) != 3: - return None - else: - return (int(num_list[0]) * 1000000) + (int(num_list[1]) * 1000) \ - + int(num_list[2]) - - - def create_system_folders(self): - - """Called by self.start() and .reset_db(). - - Creates the fixed (system) media.Folder objects that can't be - destroyed by the user. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4839 create_system_folders') - - self.fixed_all_folder = self.add_folder( - 'All Videos', - 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_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_fav_folder = self.add_folder( - 'Favourite Videos', - 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_fav_folder.set_fav_flag(True) - - self.fixed_new_folder = self.add_folder( - 'New Videos', - 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 - ) - - self.fixed_temp_folder = self.add_folder( - 'Temporary Videos', - None, # No parent folder - False, # Allow downloads - True, # Fixed (folder cannot be removed) - False, # Public - False, # Can contain any media data object - True, # Temporary - ) - - self.fixed_misc_folder = self.add_folder( - 'Unsorted Videos', - None, # No parent folder - False, # Allow downloads - True, # Fixed (folder cannot be removed) - False, # Public - True, # Can only contain videos - False, # Not temporary - ) - - - def delete_temp_folders(self): - - """Called by self.stop_continue() and self.load_db(). - - Deletes the contents of any folders marked temporary, such as the - 'Temporary Videos' folder. (The folders themselves are not deleted). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4922 delete_temp_folders') - - # (Must compile a list of top-level container objects first, or Python - # will complain about the dictionary changing size) - obj_list = [] - for dbid in self.media_name_dict.values(): - obj_list.append(self.media_reg_dict[dbid]) - - for media_data_obj in obj_list: - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.temp_flag: - - # Delete all child objects - for child_obj in list(media_data_obj.child_list.copy()): - if isinstance(child_obj, media.Video): - self.delete_video(child_obj) - else: - self.delete_container(child_obj) - - # Remove files from the filesystem, leaving an empty directory - dir_path = media_data_obj.get_default_dir(self) - if os.path.isdir(dir_path): - shutil.rmtree(dir_path) - - os.makedirs(dir_path) - - - def open_temp_folders(self): - - """Called by self.stop_continue(). - - Checks all folders marked temporary. Any of them that contain videos - are opened on the desktop (so the user can more conveniently copy - things out of them, before they are deleted.) - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4960 open_temp_folders') - - for dbid in self.media_name_dict.values(): - media_data_obj = self.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.temp_flag \ - and media_data_obj.child_list: - - utils.open_file(media_data_obj.get_default_dir(self)) - - - def disable_load_save(self, error_msg=None, lock_flag=False): - - """Called by self.load_config(), .save_config(), load_db() and - .save_db(). - - After an error, disables loading/saving, and desensitises many widgets - in the main window. - - Args: - - error_msg (str or None): An optional error message that can be# - retrieved later, if required - - lock_flag (bool): True when the error was caused by being unable to - load a database file because of a lockfile; in which the user - is prompted if they want to remove it, or not - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 4992 disable_load_save') - - # Ignore subsequent calls to this function; only the initial error - # is of interest - if not self.disable_load_save_flag: - - self.disable_load_save_flag = True - self.allow_db_save_flag = False - self.disable_load_save_msg = error_msg - self.disable_load_save_lock_flag = lock_flag - - if self.main_win_obj is not None: - self.main_win_obj.sensitise_widgets_if_database(False) - - - def remove_db_lock_file(self): - - """Called by self.do_shutdown(), .stop_continue(), .load_db() and - .switch_db(). - - Removes the lockfile protecting the Tartube database file, and updates - IVs. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5017 remove_db_lock_file') - - if self.db_lock_file_path is not None: - - if os.path.isfile(self.db_lock_file_path): - os.remove(self.db_lock_file_path) - - self.db_lock_file_path = None - - - def remove_stale_lock_file(self): - - """Called by self.start() (only), after a call to - mainwin.RemoveLockFileDialogue. - - The user has confirmed that the lockfile protecting a Tartube database - file is stale, and can be removed; so remove it. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5037 remove_stale_lock_file') - - lock_path = os.path.abspath( - os.path.join(self.data_dir, self.db_file_name + '.lock'), - ) - - if os.path.exists(lock_path): - os.remove(lock_path) - - - def file_error_dialogue(self, msg): - - """Called by self.start(), load_config(), .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. - - Args: - - msg (str): The message to display - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5062 file_error_dialogue') - - if self.main_win_obj and self.dialogue_manager_obj: - self.dialogue_manager_obj.show_msg_dialogue(msg, 'error', 'ok') - - else: - # Main window not open yet, so remove any newline characters - # (which look weird when printed to the terminal) - msg = re.sub( - r'\n', - ' ', - msg, - ) - - print('FILE ERROR: ' + msg) - - - def make_directory(self, dir_path): - - """Called by self.start() and .switch_db(). - - The call to os.makedirs() might fail with a 'Permission denied' error, - meaning that the specified directory is unwriteable. - - Convenience function to intercept the error, and display a Tartube - dialogue instead. - - Args: - - dir_path (str): The path to the directory to be created with a - call to os.makedirs() - - Returns: - - True if the directory was created, False if not - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5101 make_directory') - - try: - os.makedirs(dir_path) - return True - - except: - - # The True argument tells the dialogue window that it's an - # unwriteable directory - dialogue_win = mainwin.MountDriveDialogue(self.main_win_obj, True) - dialogue_win.run() - available_flag = dialogue_win.available_flag - dialogue_win.destroy() - - return available_flag - - - def move_backup_files(self): - - """Called by self.load_db(). - - Before v1.3.099, Tartube's data directory used a different structure, - with the database backup files stored in self.data_dir itself. - - After v1.3.099, they are stored in self.backup_dir. - - The calling function has detected that the old file structure is being - used. As a convenience to the user, move all the backup files to their - new location. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5134 move_backup_files') - - for filename in os.listdir(path=self.data_dir): - if re.search(r'^tartube_BU_.*\.db$', filename): - - old_path = os.path.abspath( - os.path.join(self.data_dir, filename), - ) - - new_path = os.path.abspath( - os.path.join(self.backup_dir, filename), - ) - - shutil.move(old_path, new_path) - - - def notify_user_of_data_dir(self): - - """Called by self.start(). - - On MS Windows, tell the user that they must set the location of the - Tartube data directory, self.data_dir. On other operating systems, ask - the user if they want to use the default location, or choose a custom - one. - - Returns: - - True to choose a custom location for the data directory, False to - use the default location. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5167 notify_user_of_data_dir') - - if os.name == 'nt': - - # On MS Windows, Cygwin creates a Tartube data directory at - # 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, - ) - - dialogue_win.run() - dialogue_win.destroy() - - return True - - else: - - # On Linux/BSD, offer the user a choice between using the default - # data directory specified by self.data_dir, or specifying their - # own data directory - dialogue_win = mainwin.SetDirectoryDialogue_LinuxBSD( - self.main_win_obj, - self.data_dir, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying - # it - custom_flag = False - if response == Gtk.ResponseType.OK \ - and dialogue_win.button2.get_active(): - custom_flag = True - - dialogue_win.destroy() - - return custom_flag - - - def prompt_user_for_data_dir(self): - - """Called by self.start(), immediately after a call to - self.notify_user_of_data_dir(). - - Also called by mainwin.MountDriveDialogue.do_select_dir(). - - When Tartube starts for the first time, and the user wants to specify - a non-default location for Tartube's data directory, prompt the user to - select/create a directory. - - Returns: - - True if the user selects a location, False if they do not. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5225 prompt_user_for_data_dir') - - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' - - file_chooser_win = Gtk.FileChooserDialog( - 'Please select ' + __main__.__prettyname__ + '\'s data ' + 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() - if response == Gtk.ResponseType.OK: - - self.data_dir = file_chooser_win.get_filename() - self.data_dir_alt_list = [ self.data_dir ] - - self.downloads_dir = os.path.abspath( - os.path.join(self.data_dir), - ) - - self.alt_downloads_dir = os.path.abspath( - os.path.join(self.data_dir, 'downloads'), - ) - - self.backup_dir = os.path.abspath( - os.path.join(self.data_dir, '.backups'), - ) - - self.temp_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp'), - ) - - self.temp_dl_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp', 'downloads'), - ) - - self.temp_test_dir = os.path.abspath( - os.path.join(self.data_dir, '.temp', 'ytdl-test'), - ) - - file_chooser_win.destroy() - if response == Gtk.ResponseType.OK: - - # Location selected; the remaining code in self.start() will - # create the data directory, if necessary - return True - - else: - - # Location not selected. Tartube will now shut down - return False - - - # (Download/Update/Refresh/Info/Tidy operations) - - - def download_manager_start(self, operation_type, \ - automatic_flag=False, media_data_list=[]): - - """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. - - Args: - - operation_type (str): 'sim' if channels/playlists should just be - checked for new videos, 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 - self.custom_dl_by_video_flag) - - automatic_flag (bool): True when called by self.start() or - self.script_slow_timer_callback(). If the download operation - does not start, no dialogue window is displayed (as it normally - would be) - - 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 - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5324 download_manager_start') - - if self.current_manager_obj: - - # Download/update/refresh/info/tidy operation already in progress - if not automatic_flag: - self.system_error( - 103, - 'Download, update, refresh, info or tidy operation' \ - + ' already in progress', - ) - - return - - elif self.main_win_obj.config_win_list: - - # Download operation is not allowed when a configuration window is - # 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', - ) - - return - - # If the device containing self.data_dir is running low on space, - # warn the user before proceeding - disk_space = utils.disk_get_free_space(self.data_dir) - total_space = utils.disk_get_total_space(self.data_dir) - - if ( - self.disk_space_stop_flag \ - and self.disk_space_stop_limit != 0 \ - and disk_space <= self.disk_space_stop_limit - ) or disk_space < self.disk_space_abs_limit: - - # 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', - ) - - return - - elif self.disk_space_warn_flag \ - and self.disk_space_warn_limit != 0 \ - and disk_space <= self.disk_space_warn_limit: - - if automatic_flag: - - # Don't perform a schedules download operation if disk space is - # below the limit at which a warning would normally be issued - return - - else: - - # 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?', - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .download_manager_continue() - { - 'yes': 'download_manager_continue', - 'data': [ - operation_type, - automatic_flag, - media_data_list, - ], - }, - ) - - else: - - # Start the download operation immediately - self.download_manager_continue( - [operation_type, automatic_flag, media_data_list], - ) - - - def download_manager_continue(self, arg_list): - - """Called by self.download_manager_start() and - .update_manager_finished(). - - Having obtained confirmation from the user (if required), start the - download operation. - - Args: - - arg_list (list): List of arguments originally supplied to - self.download_manager_start(). A list in the form - - [ operation_type, automatic_flag, media_data_list ] - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5432 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) - - # The media data registry consists of a collection of media data - # objects (media.Video, media.Channel, media.Playlist and - # media.Folder) - # If a list of media data objects was specified by the calling - # function, those media data object and all of their descendants are - # are assigned a downloads.DownloadItem object - # Otherwise, all media data objects are assigned a - # downloads.DownloadItem object - # Those downloads.DownloadItem objects are collectively stored in a - # downloads.DownloadList object - download_list_obj = downloads.DownloadList( - self, - operation_type, - media_data_list, - ) - - if not download_list_obj.download_item_list: - - if not automatic_flag: - if operation_type == 'sim': - msg = 'There is nothing to check!' - else: - msg = 'There is nothing to download!' - - self.dialogue_manager_obj.show_msg_dialogue(msg, 'error', 'ok') - - return - - # If the flag is set, do an update operation before starting the - # download operation - if self.operation_auto_update_flag and not self.operation_waiting_flag: - - self.update_manager_start('ytdl') - # These IVs tells self.update_manager_finished to start a download - # operation - self.operation_waiting_flag = True - self.operation_waiting_type = operation_type - self.operation_waiting_list = media_data_list - return - - # For the benefit of future scheduled download operations, set the - # time at which this operation began - if not media_data_list: - if operation_type == 'sim': - self.scheduled_check_last_time = int(time.time()) - else: - self.scheduled_dl_last_time = int(time.time()) - - # If Tartube should shut down after this download operation, set a - # flag that self.download_manager_finished() can check - if automatic_flag: - if self.scheduled_shutdown_flag: - self.halt_after_operation_flag = True - else: - 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') - else: - self.main_win_obj.show_progress_bar('download') - - # Reset the Progress List - self.main_win_obj.progress_list_reset() - # Reset the Results 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) - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(False) - # Make the widget changes visible - self.main_win_obj.show_all() - - # 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 - # files at regular intervals, too - # Create the timer - self.dl_timer_id = GObject.timeout_add( - self.dl_timer_time, - self.dl_timer_callback, - ) - - # Initiate the download operation. Any code can check whether a - # download, update or refresh operation is in progress, or not, by - # checking this IV - self.current_manager_obj = downloads.DownloadManager( - self, - operation_type, - download_list_obj, - ) - self.download_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def download_manager_halt_timer(self): - - """Called by downloads.DownloadManager.run() when that function has - finished. - - During a download operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5551 download_manager_halt_timer') - - if self.dl_timer_id: - self.dl_timer_check_time \ - = int(time.time()) + self.dl_timer_final_time - - - def download_manager_finished(self): - - """Called by self.dl_timer_callback() and - downloads.DownloadManager.run(). - - The download operation has finished, so update IVs and main window - widgets. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5568 download_manager_finished') - - # Get the time taken by the download operation, so we can convert it - # into a nice string below (e.g. '05:15') - # For refresh operations, RefreshManager.stop_time() might not have - # been set at this point (for some reason), so we need to check for - # the equivalent problem - if self.download_manager_obj.stop_time is not None: - time_num = int( - self.download_manager_obj.stop_time \ - - self.download_manager_obj.start_time - ) - else: - time_num = int(time.time() - self.download_manager_obj.start_time) - - # Any code can check whether a download/update/refresh/info/tidy - # operation is in progress, or not, by checking this IV - self.current_manager_obj = None - self.download_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.dl_timer_id) - self.dl_timer_id = None - self.dl_timer_check_time = None - # (All videos marked to be launched in the system's default media - # player should have been launched already, but just to be safe, - # empty this list) - self.watch_after_dl_list = [] - - # After a download operation, save files, if allowed - if 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 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: - - # 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 - # mainwin.MainWin.on_video_catalogue_re_download() ), restore the - # archive file's original name - self.reset_backup_archive() - - # If Tartube is due to shut down, then shut it down - if self.halt_after_operation_flag: - self.stop_continue() - - # Otherwise, show a dialogue window or desktop notification, if allowed - elif not self.no_dialogue_this_time_flag: - - if not self.operation_halted_flag: - msg = 'Download operation complete' - else: - msg = 'Download operation halted' - - if time_num >= 10: - msg += '\n\nTime taken: ' \ - + utils.convert_seconds_to_string(time_num, True) - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # In any case, reset those IVs - self.halt_after_operation_flag = False - self.no_dialogue_this_time_flag = False - - - def update_manager_start(self, update_type): - - """Can be called by anything. - - Initiates an update operation to do one of two jobs: - - 1. Install FFmpeg (on MS Windows only) - - 2. Install youtube-dl, or update it to its most recent version. - - Creates a new updates.UpdateManager object to handle the update - operation. When the operation is complete, - self.update_manager_finished() is called. - - Args: - - update_type (str): 'ffmpeg' to install FFmpeg, or 'ytdl' to - install/update youtube-dl - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5682 update_manager_start') - - if self.current_manager_obj: - # Download/update/refresh/info/tidy operation already in progress - return self.system_error( - 104, - 'Download, update, refresh, info or tidy operation already' \ - + ' in progress', - ) - - elif self.main_win_obj.config_win_list: - # 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', - ) - - return - - elif __main__.__pkg_strict_install_flag__: - # Update operation is disabled in the Debian/RPM package. It should - # 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__, - ) - - elif update_type == 'ffmpeg' and os.name != 'nt': - # The Update operation can only install FFmpeg on the MS Windows - # installation of Tartube. It should not be possible to call this - # function, but we'll show an error message anyway - return self.system_error( - 106, - 'Update operation cannot install FFmpeg on your operating' \ - + ' system', - ) - - # During an update operation, certain widgets are modified and/or - # desensitised - self.main_win_obj.output_tab_reset_pages() - self.main_win_obj.sensitise_check_dl_buttons(False, update_type) - - # During an update operation, a GObject timer runs, so that the Output - # Tab can be updated at regular intervals - # Create the timer - self.update_timer_id = GObject.timeout_add( - self.update_timer_time, - self.update_timer_callback, - ) - - # Initiate the update operation. Any code can check whether a - # download, update or refresh operation is in progress, or not, by - # checking this IV - self.current_manager_obj = updates.UpdateManager(self, update_type) - self.update_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def update_manager_halt_timer(self): - - """Called by updates.UpdateManager.install_ffmpeg() or - .install_ytdl() when those functions have finished. - - During an update operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5757 update_manager_halt_timer') - - if self.update_timer_id: - self.update_timer_check_time \ - = int(time.time()) + self.update_timer_final_time - - - def update_manager_finished(self): - - """Called by self.update_timer_callback(). - - The update operation has finished, so update IVs and main window - widgets. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5773 update_manager_finished') - - # Import IVs from updates.UpdateManager, before it is destroyed - update_type = self.update_manager_obj.update_type - success_flag = self.update_manager_obj.success_flag - ytdl_version = self.update_manager_obj.ytdl_version - - # Any code can check whether a download/update/refresh/info/tidy - # operation is in progress, or not, by checking this IV - self.current_manager_obj = None - self.update_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.update_timer_id) - self.update_timer_id = None - self.update_timer_check_time = None - - # After an update operation, save files, if allowed - if self.operation_save_flag: - self.save_config() - self.save_db() - - # During an update operation, certain widgets are modified and/or - # desensitised; restore them to their original state - self.main_win_obj.sensitise_check_dl_buttons(True) - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - # Then show a dialogue window/desktop notification, if allowed (and if - # a download operation is not waiting to start) - if self.operation_dialogue_mode != 'default' \ - and not self.operation_waiting_flag: - - if update_type == 'ffmpeg': - - if not success_flag: - msg = 'Installation failed' - else: - msg = 'Installation complete' - - else: - if not success_flag: - msg = 'Update operation failed' - elif self.operation_halted_flag: - msg = 'Update operation halted' - else: - msg = 'Update operation complete' - if ytdl_version is not None: - msg += '\n\nyoutube-dl version: ' + ytdl_version - else: - msg += '\n\nyoutube-dl version: (unknown)' - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - - # If a download operation is waiting to start, start it - if self.operation_waiting_flag: - self.download_manager_continue( - [ - self.operation_waiting_type, - False, - self.operation_waiting_list, - ], - ) - - # Reset those IVs, ready for any future download operations - self.operation_waiting_flag = False - self.operation_waiting_type = None - self.operation_waiting_list = [] - - - def refresh_manager_start(self, media_data_obj=None): - - """Can be called by anything. - - Initiates a refresh operation to compare Tartube's data directory with - the media registry, updating the registry as appropriate. - - Creates a new refresh.RefreshManager object to handle the refresh - operation. When the operation is complete, - self.refresh_manager_finished() is called. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder or - None): If specified, only this channel/playlist/folder is - refreshed. If not specified, the entire media registry is - refreshed - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5870 refresh_manager_start') - - if self.current_manager_obj: - # Download/update/refresh/info/tidy operation already in progress - return self.system_error( - 107, - 'Download, update, refresh, info or tidy operation already' \ - + ' in progress', - ) - - elif media_data_obj is not None \ - and isinstance(media_data_obj, media.Video): - return self.system_error( - 108, - 'Refresh operation cannot be applied to an individual video', - ) - - elif self.main_win_obj.config_win_list: - # Refresh operation is not allowed when a configuration window is - # open - self.dialogue_manager_obj.show_msg_dialogue( - 'A refresh operation cannot start if one or more' \ - + ' configuration windows are still open', - 'error', - 'ok', - ) - - return - - # 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' - - if not media_data_obj: - string = 'click the \'Check all\' button in the main window.\n\n' - elif isinstance(media_data_obj, media.Channel): - string = ' right-click the channel and select \'Check channel\'' \ - + '.\n\n' - elif isinstance(media_data_obj, media.Playlist): - string = ' right-click the playlist and select \'Check' \ - + ' playlist\'.\n\n' - else: - string = ' right-click the folder and select \'Check folder\'' \ - + '.\n\n' - - 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?', - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .move_container_to_top_continue() - { - 'yes': 'refresh_manager_continue', - 'data': media_data_obj, - }, - ) - - - def refresh_manager_continue(self, media_data_obj=None): - - """Called by self.refresh_manager_start(). - - Having obtained confirmation from the user, start the refresh - operation. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder or - None): If specified, only this channel/playlist/folder is - refreshed. If not specified, the entire media registry is - refreshed - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 5956 refresh_manager_continue') - - # For earlier versions of Gtk, refresh operations on a channel/ - # playlist/folder cause frequent crashes. We can work around that by - # resetting the Video Index and Video Catalogue - if self.gtk_broken_flag or self.gtk_emulate_broken_flag: - - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - # During a refresh operation, show a progress bar in the Videos Tab - self.main_win_obj.show_progress_bar('refresh') - # Reset the Output Tab - self.main_win_obj.output_tab_reset_pages() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(False, True) - # Make the widget changes visible - self.main_win_obj.show_all() - - # During a refresh operation, a GObject timer runs, so that the Output - # Tab can be updated at regular intervals - # Create the timer - self.refresh_timer_id = GObject.timeout_add( - self.refresh_timer_time, - self.refresh_timer_callback, - ) - - # Initiate the refresh operation. Any code can check whether a - # download, update or refresh operation is in progress, or not, by - # checking this IV - self.current_manager_obj = refresh.RefreshManager(self, media_data_obj) - self.refresh_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def refresh_manager_halt_timer(self): - - """Called by refresh.RefreshManager.run() when that function has - finished. - - During a refresh operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6003 refresh_manager_halt_timer') - - if self.refresh_timer_id: - self.refresh_timer_check_time \ - = int(time.time()) + self.refresh_timer_final_time - - - def refresh_manager_finished(self): - - """Called by self.refresh_timer_callback(). - - The refresh operation has finished, so update IVs and main window - widgets. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6019 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') - # For some reason, RefreshManager.stop_time() might not be set, so we - # need to check for that - if self.refresh_manager_obj.stop_time is not None: - time_num = int( - self.refresh_manager_obj.stop_time \ - - self.refresh_manager_obj.start_time - ) - else: - time_num = int(time.time() - self.refresh_manager_obj.start_time) - - # Any code can check whether a download/update/refresh/info/tidy - # operation is in progress, or not, by checking this IV - self.current_manager_obj = None - self.refresh_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.refresh_timer_id) - self.refresh_timer_id = None - self.refresh_timer_check_time = None - - # After a refresh operation, save files, if allowed - if self.operation_save_flag: - self.save_config() - self.save_db() - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - # Remove the progress bar in the Videos Tab - self.main_win_obj.hide_progress_bar() - # Any remaining messages generated by refresh.RefreshManager should be - # shown in the Output Tab immediately - self.main_win_obj.output_tab_update_pages() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(True) - # Make the widget changes visible (not necessary if the main window has - # been closed to the system tray) - if self.main_win_obj.is_visible(): - self.main_win_obj.show_all() - - # 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: - - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - # Then show a dialogue window/desktop notification, if allowed - if self.operation_dialogue_mode != 'default': - - if not self.operation_halted_flag: - msg = 'Refresh operation complete' - else: - msg = 'Refresh operation halted' - - if time_num >= 10: - msg += '\n\nTime taken: ' \ - + utils.convert_seconds_to_string(time_num, True) - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - - - def info_manager_start(self, info_type, media_data_obj=None, - url_string=None, options_string=None): - - """Can be called by anything. - - Initiates an info operation to do one of three jobs: - - 1. Fetch a list of available formats for a video, directly from - youtube-dl - - 2. Fetch a list of available subtitles for a video, directly from - youtube-dl - - 3. Test youtube-dl with specified download options; everything is - downloaded into a temporary folder - - Creates a new info.InfoManager object to handle the info operation. - When the operation is complete, self.info_manager_finished() is - called. - - Args: - - info_type (str): 'formats' to fetch a list of formats, 'subs' to - fetch a list of subtitles, or 'test_ytdl' to test youtube-dl - with specified options - - media_data_obj (media.Video): For 'formats' and 'subs', the - media.Video object for which formats/subtitles should be - fetched. For 'test_ytdl', set to None - - url_string (str): For 'test_ytdl', the video URL to download (can - be None or an empty string, if no download is required, for - example 'youtube-dl --version'. For 'formats' and 'subs', - set to None - - options_string (str): For 'test_ytdl', a string containing one or - more youtube-dl download options. The string, generated by a - Gtk.TextView, typically contains newline and/or multiple - whitespace characters; the info.InfoManager code deals with - that. Can be None or an empty string, if no download options - are required. For 'formats' and 'subs', set to None - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6136 info_manager_start') - - if self.current_manager_obj: - # Download/update/refresh/info/tidy operation already in progress - return self.system_error( - 109, - 'Download, update, refresh, info or tidy operation already' \ - + ' in progress', - ) - - elif info_type != 'formats' \ - and info_type != 'subs' \ - and info_type != 'test_ytdl': - # Unrecognised argument - return self.system_error( - 110, - 'Invalid info operation argument', - ) - - elif media_data_obj is not None \ - and ( - not isinstance(media_data_obj, media.Video) - or not media_data_obj.source - ): - # Unusable media data object - return self.system_error( - 111, - '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', - ) - - # During an info operation, certain widgets are modified and/or - # desensitised - self.main_win_obj.output_tab_reset_pages() - self.main_win_obj.sensitise_check_dl_buttons(False, info_type) - - # During an info operation, a GObject timer runs, so that the Output - # Tab can be updated at regular intervals - # Create the timer - self.info_timer_id = GObject.timeout_add( - self.info_timer_time, - self.info_timer_callback, - ) - - # If testing youtube-dl, empty the temporary directory into which - # anything is downloaded - if info_type == 'test_ytdl': - - if os.path.isdir(self.temp_test_dir): - try: - shutil.rmtree(self.temp_test_dir) - os.makedirs(self.temp_test_dir) - except: - pass - - # Initiate the info operation. Any code can check whether a - # download/update/refresh/info/tidy operation is in progress, or not, - # by checking this IV - self.current_manager_obj = info.InfoManager( - self, - info_type, - media_data_obj, - url_string, - options_string, - ) - - self.info_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def info_manager_halt_timer(self): - - """Called by info.InfoManager.run() when that function has finished. - - During an info operation, a GObject timer was running. Let it - continue running for a few seconds more. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6227 info_manager_halt_timer') - - if self.info_timer_id: - self.info_timer_check_time \ - = int(time.time()) + self.info_timer_final_time - - - def info_manager_finished(self): - - """Called by self.info_timer_callback(). - - The info operation has finished, so update IVs and main window widgets. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6242 info_manager_finished') - - # Import IVs from info.InfoManager, before it is destroyed - info_type = self.info_manager_obj.info_type - success_flag = self.info_manager_obj.success_flag - output_list = self.info_manager_obj.output_list.copy() - url_string = self.info_manager_obj.url_string - - # Any code can check whether a download/update/refresh/info/tidy - # operation is in progress, or not, by checking this IV - self.current_manager_obj = None - self.info_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.info_timer_id) - self.info_timer_id = None - self.info_timer_check_time = None - - # After an info operation, save files, if allowed - if self.operation_save_flag: - self.save_config() - self.save_db() - - # During an info operation, certain widgets are modified and/or - # desensitised; restore them to their original state - self.main_win_obj.sensitise_check_dl_buttons(True) - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - # When testing youtube-dl, and a source URL was specified by the user, - # open the temporary directory so the user can see what (if - # anything) was downloaded - if url_string is not None and url_string != '': - utils.open_file(self.temp_test_dir) - - # Then show a dialogue window/desktop notification, if allowed - if self.operation_dialogue_mode != 'default': - - if not success_flag: - msg = 'Operation failed' - else: - msg = 'Operation complete' - - msg += '\n\nClick the Output Tab to see the results' - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - - - def tidy_manager_start(self, choices_dict): - - """Can be called by anything. - - Initiates a tidy operation to tidy up the directories used by each of - one or more media.Channel, media.Playlist and media.Folder objects. - The tidy-up process consists of one or more of the following jobs: - - 1. Check video files are not corrupted (and optionally delete any - that are) - - 2. Check that video files which should exist, actually do (and - vice-versa) - - 3. Delete video files, audio files, description files, metadata (JSON) - files, annotation files, thumbnail files and/or youtube-dl - archive files - - Creates a new tidy.TidyManager object to handle the tidy operation. - When the operation is complete, self.tidy_manager_finished() is - called. - - Args: - - choices_dict (dict): A dictionary specifying the choices made by - the user in mainwin.TidyDialogue. The dictionary is in the - following format: - - media_data_obj: A media.Channel, media.Playlist or media.Folder - object, or None if all channels/playlists/folders are to be - tidied up. If specified, the channel/playlist/folder and - all of its descendants are checked - - corrupt_flag: True if video files should be checked for - corruption - - del_corrupt_flag: True if corrupted video files should be - deleted - - exist_Flag: True if video files that should exist should be - checked, in case they don't (and vice-versa) - - del_video_flag: True if downloaded video files should be - deleted - - del_others_flag: True if all video/audio files with the same - name should be deleted (as artefacts of post-processing - with FFmpeg or AVConv) - - del_descrip_flag: True if all description files should be - deleted - - del_json_flag: True if all metadata (JSON) files should be - deleted - - del_xml_flag: True if all annotation files should be deleted - - del_thumb_flag: True if all thumbnail files should be deleted - - del_archive_flag: True if all youtube-dl archive files should - be deleted - - """ - - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6362 tidy_manager_start') - - if self.current_manager_obj: - # Download/update/refresh/info/tidy operation already in progress - return self.system_error( - 112, - 'Download, update, refresh, info or tidy operation already' \ - + ' in progress', - ) - - elif self.main_win_obj.config_win_list: - - # 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', - ) - - # 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 - if self.gtk_broken_flag or self.gtk_emulate_broken_flag: - - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - # During a tidy operation, show a progress bar in the Videos Tab - self.main_win_obj.show_progress_bar('tidy') - # Reset the Output Tab - self.main_win_obj.output_tab_reset_pages() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(False, True) - # Make the widget changes visible - self.main_win_obj.show_all() - - # During a tidy operation, a GObject timer runs, so that the Output Tab - # can be updated at regular intervals - # Create the timer - self.tidy_timer_id = GObject.timeout_add( - self.tidy_timer_time, - self.tidy_timer_callback, - ) - - # Initiate the tidy operation. Any code can check whether a - # download/update/refresh/info/tidy operation is in progress, or not, - # by checking this IV - self.current_manager_obj = tidy.TidyManager(self, choices_dict) - self.tidy_manager_obj = self.current_manager_obj - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - - - def tidy_manager_halt_timer(self): - - """Called by tidy.TidyManager.run() when that function has finished. - - During a tidy operation, a GObject timer was running. Let it continue - running for a few seconds more. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6427 tidy_manager_halt_timer') - - if self.tidy_timer_id: - self.tidy_timer_check_time \ - = int(time.time()) + self.tidy_timer_final_time - - - def tidy_manager_finished(self): - - """Called by self.tidy_timer_callback(). - - The tidy operation has finished, so update IVs and main window widgets. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6442 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') - # For some reason, TidyManager.stop_time() might not be set, so we need - # to check for that - if self.tidy_manager_obj.stop_time is not None: - time_num = int( - self.tidy_manager_obj.stop_time \ - - self.tidy_manager_obj.start_time - ) - else: - time_num = int(time.time() - self.tidy_manager_obj.start_time) - - # Any code can check whether a download/update/refresh/info/tidy - # operation is in progress, or not, by checking this IV - self.current_manager_obj = None - self.tidy_manager_obj = None - - # Stop the timer and reset IVs - GObject.source_remove(self.tidy_timer_id) - self.tidy_timer_id = None - self.tidy_timer_check_time = None - - # After a tidy operation, save files, if allowed - if self.operation_save_flag: - self.save_config() - self.save_db() - - # Update the status icon in the system tray - self.status_icon_obj.update_icon() - # Remove the progress bar in the Videos Tab - self.main_win_obj.hide_progress_bar() - # Any remaining messages generated by tidy.TidyManager should be shown - # in the Output Tab immediately - self.main_win_obj.output_tab_update_pages() - # (De)sensitise other widgets, as appropriate - self.main_win_obj.sensitise_operation_widgets(True) - # Make the widget changes visible (not necessary if the main window has - # been closed to the system tray) - if self.main_win_obj.is_visible(): - self.main_win_obj.show_all() - - # 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: - - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - # ...but if not, the Video Catalogue must be redrawn anyway - else: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - ) - - # Show a dialogue window/desktop notification, if allowed - if self.operation_dialogue_mode != 'default': - - if not self.operation_halted_flag: - msg = 'Tidy operation complete' - else: - msg = 'Tidy operation halted' - - if time_num >= 10: - msg += '\n\nTime taken: ' \ - + utils.convert_seconds_to_string(time_num, True) - - if self.operation_dialogue_mode == 'dialogue': - self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') - elif self.operation_dialogue_mode == 'desktop': - self.main_win_obj.notify_desktop(None, msg) - - # Reset operation IVs - self.operation_halted_flag = False - - - # (Download operation support functions) - - def create_video_from_download(self, download_item_obj, dir_path, \ - filename, extension, no_sort_flag=False): - - """Called downloads.VideoDownloader.confirm_new_video(), - .confirm_old_video() and .confirm_sim_video(). - - When an individual video has been downloaded, this function is called - to create a new media.Video object. - - Args: - - download_item_obj (downloads.DownloadItem) - The object used to - track the download status of a media data object (media.Video, - media.Channel or media.Playlist) - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - no_sort_flag (bool): True when called by - downloads.VideoDownloader.confirm_sim_video(), because the - video's parent containers (including the 'All Videos' folder) - should delay sorting their lists of child objects until that - calling function is ready. False when called by anything else - - Returns: - - video_obj (media.Video) - The video object created - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6557 create_video_from_download') - - # The downloads.DownloadItem handles a download for a video, a channel - # or a playlist - media_data_obj = download_item_obj.media_data_obj - - if isinstance(media_data_obj, media.Video): - - # The downloads.DownloadItem object is handling a single video - video_obj = media_data_obj - # If the video was added manually (for example, using the 'Add - # videos' button), then its filepath won't be set yet - if not video_obj.file_name: - video_obj.set_file(filename, extension) - - else: - - # The downloads.DownloadItem object is handling a channel or - # playlist - # Does a media.Video object already exist? - video_obj = None - for child_obj in media_data_obj.child_list: - - child_file_dir = None - if child_obj.file_name is not None: - child_file_dir = media_data_obj.get_actual_dir(self) - - if isinstance(child_obj, media.Video) \ - and child_file_dir \ - and child_file_dir == dir_path \ - and child_obj.file_name \ - and child_obj.file_name == filename: - video_obj = child_obj - - if video_obj is None: - - # Create a new media data object for the video - options_manager_obj = download_item_obj.options_manager_obj - override_name \ - = options_manager_obj.options_dict['use_fixed_folder'] - if override_name is not None \ - and override_name in self.media_name_dict: - - other_dbid = self.media_name_dict[override_name] - other_parent_obj = self.media_reg_dict[other_dbid] - - video_obj = self.add_video( - other_parent_obj, - None, - False, - no_sort_flag, - ) - - else: - video_obj = self.add_video( - media_data_obj, - None, - False, - no_sort_flag, - ) - - # Since we have them to hand, set the video's file path IVs - # immediately - video_obj.set_file(filename, extension) - - # 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 - # IV - # (If not, the IV is set by media.Video.set_dl_flag when the video is - # actually downloaded) - if isinstance(video_obj.parent_obj, media.Channel) \ - or isinstance(video_obj.parent_obj, media.Playlist): - video_obj.set_receive_time() - - return video_obj - - - def convert_video_from_download(self, container_obj, options_manager_obj, - dir_path, filename, extension, no_sort_flag=False): - - """Called downloads.VideoDownloader.confirm_new_video() and - .confirm_sim_video(). - - A modified version of self.create_video_from_download, called when - youtube-dl is about to download a channel or playlist into a - media.Video object. - - Args: - - container_obj (media.Folder): The folder into which a replacement - media.Video object is to be created - - options_manager_obj (options.OptionsManager): The download options - for this media data object - - dir_path (str): The full path to the directory in which the video - is saved, e.g. '/home/yourname/tartube/downloads/Videos' - - filename (str): The video's filename, e.g. 'My Video' - - extension (str): The video's extension, e.g. '.mp4' - - no_sort_flag (bool): True when called by - downloads.VideoDownloader.confirm_sim_video(), because the - video's parent containers (including the 'All Videos' folder) - should delay sorting their lists of child objects until that - calling function is ready. False when called by anything else - - Returns: - - video_obj (media.Video) - The video object created - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6673 convert_video_from_download') - - # Does the container object already contain this video? - video_obj = None - for child_obj in container_obj.child_list: - - child_file_dir = None - if child_obj.file_dir is not None: - child_file_dir = container_obj.get_actual_dir(self) - - if isinstance(child_obj, media.Video) \ - and child_file_dir \ - and child_file_dir == dir_path \ - and child_obj.file_name \ - and child_obj.file_name == filename: - video_obj = child_obj - - if video_obj is None: - - # Create a new media data object for the video - override_name \ - = options_manager_obj.options_dict['use_fixed_folder'] - if override_name is not None \ - and override_name in self.media_name_dict: - - other_dbid = self.media_name_dict[override_name] - other_container_obj = self.media_reg_dict[other_dbid] - - video_obj = self.add_video( - other_container_obj, - None, - False, - no_sort_flag, - ) - - else: - video_obj = self.add_video( - container_obj, - None, - False, - no_sort_flag, - ) - - # Since we have them to hand, set the video's file path IVs - # immediately - video_obj.set_file(filename, extension) - - return video_obj - - - def announce_video_download(self, download_item_obj, video_obj, \ - keep_description=None, keep_info=None, keep_annotations=None, - keep_thumbnail=None): - - """Called by downloads.VideoDownloader.confirm_new_video(), - .confirm_old_video() and .confirm_sim_video(). - - Updates the main window. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object describing the URL from which youtube-dl should download - video(s). - - video_obj (media.Video): The video object for the downloaded video - - keep_description (True, False, None): - keep_info (True, False, None): - keep_annotations (True, False, None): - keep_thumbnail (True, False, None): - Settings from the options.OptionsManager object used to - download the video (set to 'None' for a simulated download) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6750 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 - # for the downloaded video - self.main_win_obj.video_catalogue_update_row(video_obj) - - # Update the Results List - self.main_win_obj.results_list_add_row( - download_item_obj, - video_obj, - keep_description, - keep_info, - keep_annotations, - keep_thumbnail, - ) - - - def update_video_when_file_found(self, video_obj, video_path, temp_dict, \ - mkv_flag=False): - - """Called by mainwin.MainWin.results_list_update_row(). - - When youtube-dl reports it is finished, there is a short delay before - the final downloaded video(s) actually exist in the filesystem. - - Once the calling function has confirmed the file exists, it calls this - function to update the media.Video object's IVs. - - Args: - - video_obj (media.Video): The video object to update - - video_path (str): The full filepath to the video file that has been - confirmed to exist - - temp_dict (dict): Dictionary of values used to update the video - object, in the form: - - 'video_obj': not required by this function, as we already have - it - 'row_num': not required by this function - 'keep_description', 'keep_info', 'keep_annotations', - 'keep_thumbnail': flags from the options.OptionsManager - object used for to download the video (not added to the - dictionary at all for simulated downloads) - - mkv_flag (bool): If the warning 'Requested formats are incompatible - for merge and will be merged into mkv' has been seen, the - calling function has found an .mkv file rather than the .mp4 - file it was expecting, and has set this flag to True - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6805 update_video_when_file_found') - - # Only set the .name IV if the video is currently unnamed - if video_obj.name == self.default_video_name: - video_obj.set_name(video_obj.file_name) - # (The video's title, stored in the .nickname IV, will be updated - # from the JSON data in a momemnt) - video_obj.set_nickname(video_obj.file_name) - - # If it's an .mkv file because of a failed merge, update the IV - if mkv_flag: - video_obj.set_mkv() - - # Set the file size - video_obj.set_file_size(os.path.getsize(video_path)) - - # If the JSON file was downloaded, we can extract video statistics from - # it - self.update_video_from_json(video_obj) - - # For any of those statistics that haven't been set (because the JSON - # file was missing or didn't contain the right statistics), set them - # directly - self.update_video_from_filesystem(video_obj, video_path) - - # Delete the description, JSON, annotations and thumbnail files, if - # required to do so - if 'keep_description' in temp_dict \ - and not temp_dict['keep_description']: - - old_path = video_obj.get_actual_path_by_ext(self, '.description') - - if os.path.isfile(old_path): - utils.convert_path_to_temp( - self, - old_path, - True, # Move the file - ) - - if 'keep_info' in temp_dict and not temp_dict['keep_info']: - - old_path = video_obj.get_actual_path_by_ext(self, '.info.json') - - if os.path.isfile(old_path): - utils.convert_path_to_temp( - self, - old_path, - True, # Move the file - ) - - if 'keep_annotations' in temp_dict \ - and not temp_dict['keep_annotations']: - - old_path = video_obj.get_actual_path_by_ext( - self, - '.annotations.xml', - ) - - if os.path.isfile(old_path): - utils.convert_path_to_temp( - self, - old_path, - True, # Move the file - ) - - if 'keep_thumbnail' in temp_dict and not temp_dict['keep_thumbnail']: - - old_path = utils.find_thumbnail(self, video_obj) - - if old_path is not None: - utils.convert_path_to_temp( - self, - old_path, - True, # Move the file - ) - - # Mark the video as (fully) downloaded (and update everything else) - self.mark_video_downloaded(video_obj, True) - - # Register the video's size with the download manager, so that disk - # space limits can be applied, if required - if self.download_manager_obj and video_obj.dl_flag: - self.download_manager_obj.register_video_size(video_obj.file_size) - - # If required, launch this video in the system's default media player - if video_obj in self.watch_after_dl_list: - - self.watch_after_dl_list.remove(video_obj) - self.watch_video_in_player(video_obj) - self.mark_video_new(video_obj, False) - if video_obj.waiting_flag: - self.mark_video_waiting(video_obj, False) - - - def announce_video_clone(self, video_obj): - - """Called by downloads.VideoDownloader.confirm_old_video(). - - This is a modified version of self.update_video_when_file_found(), - called when a channel/playlist/folder is using an alternative - download destination for its videos (in which case, - self.update_video_when_file_found() can't be called). - - Args: - - video_obj (media.Video): The video which already exists on the - user's filesystem (in the alternative download destination) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6916 announce_video_clone') - - video_path = video_obj.get_actual_path(self) - - # Only set the .name IV if the video is currently unnamed - if video_obj.name == self.default_video_name: - video_obj.set_name(video_obj.file_name) - # (The video's title, stored in the .nickname IV, will be updated - # from the JSON data in a momemnt) - video_obj.set_nickname(video_obj.file_name) - - # Set the file size - video_obj.set_file_size(os.path.getsize(video_path)) - - # If the JSON file was downloaded, we can extract video statistics from - # it - self.update_video_from_json(video_obj) - - # For any of those statistics that haven't been set (because the JSON - # file was missing or didn't contain the right statistics), set them - # directly - self.update_video_from_filesystem(video_obj, video_path) - - # Mark the video as (fully) downloaded (and update everything else) - self.mark_video_downloaded(video_obj, True) - - - def update_video_from_json(self, video_obj): - - """Called by self.update_video_when_file_found(), - .announce_video_clone() and - refresh.RefreshManager.refresh_from_default_destination(). - - If a video's JSON file exists, extract video statistics from it, and - use them to update the video object. - - Args: - - video_obj (media.Video): The video object to update - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 6959 update_video_from_json') - - json_path = video_obj.get_actual_path_by_ext(self, '.info.json') - - if os.path.isfile(json_path): - - json_dict = self.file_manager_obj.load_json(json_path) - - if 'title' in json_dict: - video_obj.set_nickname(json_dict['title']) - - if 'upload_date' in json_dict: - # date_string in form YYYYMMDD - date_string = json_dict['upload_date'] - dt_obj = datetime.datetime.strptime(date_string, '%Y%m%d') - video_obj.set_upload_time(dt_obj.timestamp()) - - if 'duration' in json_dict: - video_obj.set_duration(json_dict['duration']) - - if 'webpage_url' in json_dict: - video_obj.set_source(json_dict['webpage_url']) - - if 'description' in json_dict: - video_obj.set_video_descrip( - json_dict['description'], - self.main_win_obj.descrip_line_max_len, - ) - - - def update_video_from_filesystem(self, video_obj, video_path): - - """Called by self.update_video_when_file_found(), - .announce_video_clone() and - refresh.RefreshManager.refresh_from_default_destination(). - - If a video's JSON file does not exist, or did not contain the - statistics we were looking for, we can set some of them directly from - the filesystem. - - Args: - - video_obj (media.Video): The video object to update - - video_path (str): The full path to the video's file - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7008 update_video_from_filesystem') - - if video_obj.upload_time is None: - video_obj.set_upload_time(os.path.getmtime(video_path)) - - if video_obj.duration is None \ - and HAVE_MOVIEPY_FLAG \ - and self.use_module_moviepy_flag: - - # When the video file is corrupted, moviepy freezes indefinitely - # Instead, let's try placing the procedure inside a thread (unless - # the user has specified a timeout of zero; in which case, don't - # use a thread and let moviepy freeze indefinitely) - if not self.refresh_moviepy_timeout: - - clip = moviepy.editor.VideoFileClip(video_path) - video_obj.set_duration(clip.duration) - - else: - - this_thread = threading.Thread( - target=self.set_duration_from_moviepy, - args=(video_obj, video_path,), - ) - - this_thread.daemon = True - this_thread.start() - this_thread.join(self.refresh_moviepy_timeout) - if this_thread.is_alive(): - self.system_error( - 113, - '\'' + video_obj.parent_obj.name \ - + '\': moviepy module' \ - + 'failed to fetch duration of video \'' \ - + video_obj.name + '\'', - ) - - # (Can't set the video source directly) - - if video_obj.descrip is None: - video_obj.read_video_descrip( - self, - self.main_win_obj.descrip_line_max_len, - ) - - - def set_duration_from_moviepy(self, video_obj, video_path): - - """Called by self.update_video_from_filesystem(). - - When we call moviepy.editor.VideoFileClip() on a corrupted video file, - moviepy freezes indefinitely. - - This function is called inside a thread, so a timeout of (by default) - ten seconds can be applied. - - Args: - - video_obj (media.Video): The video object being updated - - video_path (str): The path to the video file itself - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7073 set_duration_from_moviepy') - - try: - clip = moviepy.editor.VideoFileClip(video_path) - video_obj.set_duration(clip.duration) - except: - self.system_error( - 114, - '\'' + video_obj.parent_obj.name + '\': moviepy module' \ - + 'failed to fetch duration of video \'' \ - + video_obj.name + '\'', - ) - - - def set_backup_archive(self, media_data_obj): - - """Called by mainwin.MainWin.on_video_catalogue_re_download(). - - If self.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. - - Change the name of the archive file temporarily. After the download - operation is complete, self.reset_backup_archive() is called to - restore its original name. - - Args: - - media_data_obj (media.Video): The video object to be re-downloaded - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7108 set_backup_archive') - - archive_path = os.path.abspath( - os.path.join( - media_data_obj.parent_obj.get_default_dir(self), - 'ytdl-archive.txt', - ) - ) - - if os.path.isfile(archive_path): - - bu_path = os.path.abspath( - os.path.join( - media_data_obj.parent_obj.get_default_dir(self), - 'bu_archive.txt', - ) - ) - - # (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(archive_path, bu_path) - - # Store both paths, so self.reset_backup_archive() can retrieve - # them - self.ytdl_archive_path = archive_path - self.ytdl_archive_backup_path = 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 DEBUG_FUNC_FLAG: - utils.debug_time('app 7151 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): - - # (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) - - # (os.rename sometimes fails on external hard drives; this is - # safer) - shutil.move( - self.ytdl_archive_backup_path, - self.ytdl_archive_path, - ) - - # Regardless of whether a backup archive file was created during a - # re-download operation, or not, reset the IVs - self.ytdl_archive_path = None - self.ytdl_archive_backup_path = None - - - # (Add media data objects) - - - def add_video(self, parent_obj, source=None, dl_sim_flag=False, - no_sort_flag=False): - - """Can be called by anything. - - Creates a new media.Video object, and updates IVs. - - Args: - - parent_obj (media.Channel, media.Playlist or media.Folder): The - media data object for which the new media.Video object is the - child (all videos have a parent) - - source (str): The video's source URL, if known - - dl_sim_flag (bool): If True, the video object's .dl_sim_flag IV is - set to True, which forces simulated downloads - - no_sort_flag (bool): True when - self.create_video_from_download() is called by - downloads.VideoDownloader.confirm_sim_video(), because the - video's parent containers (including the 'All Videos' folder) - should delay sorting their lists of child objects until that - calling function is ready. False when called by anything else - - Returns: - - The new media.Video object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7210 add_video') - - # Videos can't be placed inside other videos - if parent_obj and isinstance(parent_obj, media.Video): - return self.system_error( - 115, - 'Videos cannot be placed inside other videos', - ) - - # Videos can't be added directly to a private folder - elif parent_obj and isinstance(parent_obj, media.Folder) \ - and parent_obj.priv_flag: - return self.system_error( - 116, - 'Videos cannot be placed inside a private folder', - ) - - # Create a new media.Video object - video_obj = media.Video( - self.media_reg_count, - self.default_video_name, - parent_obj, - None, # Use default download options - no_sort_flag, - ) - - if source is not None: - video_obj.set_source(source) - - if dl_sim_flag: - video_obj.set_dl_sim_flag(True) - - # Update IVs - self.media_reg_count += 1 - self.media_reg_dict[video_obj.dbid] = video_obj - - # The private 'All Videos' folder also has this video as a child object - self.fixed_all_folder.add_child(video_obj, no_sort_flag) - - # Update the row in the Video Index for both the parent and private - # folder - self.main_win_obj.video_index_update_row_text(video_obj.parent_obj) - self.main_win_obj.video_index_update_row_text(self.fixed_all_folder) - - # If the video's parent is the one visible in the Video Catalogue (or - # if 'Unsorted Videos' or 'Temporary Videos', etc, is the one visible - # in the Video Catalogue), the new video itself won't be visible - # there yet - # Make sure the video is visible, if appropriate - self.main_win_obj.video_catalogue_update_row(video_obj) - - return video_obj - - - def add_channel(self, name, parent_obj=None, source=None, \ - dl_sim_flag=None): - - """Can be called by anything. - - Creates a new media.Channel object, and updates IVs. - - Args: - - name (str): The channel name - - parent_obj (media.Folder): The media data object for which the new - media.Channel object is a child (if any) - - source (str): The channel's source URL, if known - - dl_sim_flag (bool): True if we should simulate downloads for videos - in this channel, False if we should actually download them - (when allowed) - - Returns: - - The new media.Channel object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7291 add_channel') - - # Channels can only be placed inside an unrestricted media.Folder - # object (if they have a parent at all) - if parent_obj \ - and ( - not isinstance(parent_obj, media.Folder) \ - or parent_obj.restrict_flag - ): - return self.system_error( - 117, - 'Channels cannot be added to a restricted folder', - ) - - # There is a limit to the number of levels allowed in the media - # registry - if parent_obj and parent_obj.get_depth() >= self.media_max_level: - return self.system_error( - 118, - 'Channel exceeds maximum depth of media registry', - ) - - # Some names are not allowed at all - if name is None \ - or re.match('\s*$', name) \ - or not self.check_container_name_is_legal(name): - return self.system_error( - 119, - 'Illegal channel name', - ) - - # Create a new media.Channel object - channel_obj = media.Channel( - self, - self.media_reg_count, - name, - parent_obj, - None, # Use default download options - ) - - if source is not None: - channel_obj.set_source(source) - - if dl_sim_flag is not None: - channel_obj.set_dl_sim_flag(dl_sim_flag) - - # Update IVs - self.media_reg_count += 1 - self.media_reg_dict[channel_obj.dbid] = channel_obj - self.media_name_dict[channel_obj.name] = channel_obj.dbid - if not parent_obj: - self.media_top_level_list.append(channel_obj.dbid) - - # Create the directory used by this channel (if it doesn't already - # exist) - dir_path = channel_obj.get_default_dir(self) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - return channel_obj - - - def add_playlist(self, name, parent_obj=None, source=None, \ - dl_sim_flag=None): - - """Can be called by anything. - - Creates a new media.Playlist object, and updates IVs. - - Args: - - name (str): The playlist name - - parent_obj (media.Folder): The media data object for which the new - media.Playlist object is a child (if any) - - source (str): The playlist's source URL, if known - - dl_sim_flag (bool): True if we should simulate downloads for videos - in this playlist, False if we should actually download them - (when allowed) - - Returns: - - The new media.Playlist object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7380 add_playlist') - - # Playlists can only be place inside an unrestricted media.Folder - # object (if they have a parent at all) - if parent_obj \ - and ( - not isinstance(parent_obj, media.Folder) \ - or parent_obj.restrict_flag - ): - return self.system_error( - 120, - 'Playlists cannot be added to a restricted folder', - ) - - # There is a limit to the number of levels allowed in the media - # registry - if parent_obj and parent_obj.get_depth() >= self.media_max_level: - return self.system_error( - 121, - 'Playlist exceeds maximum depth of media registry', - ) - - # Some names are not allowed at all - if name is None \ - or re.match('\s*$', name) \ - or not self.check_container_name_is_legal(name): - return self.system_error( - 122, - 'Illegal playlist name', - ) - - # Create a new media.Playlist object - playlist_obj = media.Playlist( - self, - self.media_reg_count, - name, - parent_obj, - None, # Use default download options - ) - - if source is not None: - playlist_obj.set_source(source) - - if dl_sim_flag is not None: - playlist_obj.set_dl_sim_flag(dl_sim_flag) - - # Update IVs - self.media_reg_count += 1 - self.media_reg_dict[playlist_obj.dbid] = playlist_obj - self.media_name_dict[playlist_obj.name] = playlist_obj.dbid - if not parent_obj: - self.media_top_level_list.append(playlist_obj.dbid) - - # Create the directory used by this playlist (if it doesn't already - # exist) - dir_path = playlist_obj.get_default_dir(self) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - # Procedure complete - return playlist_obj - - - def add_folder(self, name, parent_obj=None, dl_sim_flag=False, - fixed_flag=False, priv_flag=False, restrict_flag=False, temp_flag=False): - - """Can be called by anything. - - Creates a new media.Folder object, and updates IVs. - - Args: - - name (str): The folder name - - parent_obj (media.Folder): The media data object for which the new - media.Channel object is a child (if any) - - dl_sim_flag (bool): If True, the folders .dl_sim_flag IV is set to - True, which forces simulated downloads for any videos, - channels or playlists contained in the folder - - fixed_flag, priv_flag, restrict_flag, temp_flag (bool): Flags sent - to the object's .__init__() function - - Returns: - - The new media.Folder object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7471 add_folder') - - # Folders can only be placed inside an unrestricted media.Folder object - # (if they have a parent at all) - if parent_obj \ - and ( - not isinstance(parent_obj, media.Folder) \ - or parent_obj.restrict_flag - ): - return self.system_error( - 123, - 'Folders cannot be added to another restricted folder', - ) - - # There is a limit to the number of levels allowed in the media - # registry - if parent_obj and parent_obj.get_depth() >= self.media_max_level: - return self.system_error( - 124, - 'Folder exceeds maximum depth of media registry', - ) - - # Some names are not allowed at all - if name is None \ - or re.match('\s*$', name) \ - or not self.check_container_name_is_legal(name): - return self.system_error( - 125, - 'Illegal folder name', - ) - - folder_obj = media.Folder( - self, - self.media_reg_count, - name, - parent_obj, - None, # Use default download options - fixed_flag, - priv_flag, - restrict_flag, - temp_flag, - ) - - if dl_sim_flag: - folder_obj.set_dl_sim_flag(True) - - # Update IVs - self.media_reg_count += 1 - self.media_reg_dict[folder_obj.dbid] = folder_obj - self.media_name_dict[folder_obj.name] = folder_obj.dbid - if not parent_obj: - self.media_top_level_list.append(folder_obj.dbid) - - # Create the directory used by this folder (if it doesn't already - # exist) - # Obviously don't do that for private folders - dir_path = folder_obj.get_default_dir(self) - if not folder_obj.priv_flag and not os.path.exists(dir_path): - os.makedirs(dir_path) - - # Procedure complete - return folder_obj - - - # (Move media data objects) - - - def move_container_to_top(self, media_data_obj): - - """Called by mainwin.MainWin.on_video_index_move_to_top(). - - Before moving a channel, playlist or folder, get confirmation from the - user. - - After getting confirmation, call self.move_container_to_top_continue() - to move the channel, playlist or folder to the top level (in other - words, removes its parent folder). - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - moving media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7557 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, - 'Move container to top request failed sanity check', - ) - - # Check that the target directory doesn't already exist (unlikely, but - # possible if the user has been copying files manually) - target_path = os.path.abspath( - os.path.join( - self.downloads_dir, - media_data_obj.name, - ), - ) - - 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.', - 'error', - 'ok', - ) - - return - - # 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() - - 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', - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .move_container_to_top_continue() - { - 'yes': 'move_container_to_top_continue', - 'data': media_data_obj, - }, - ) - - - def move_container_to_top_continue(self, media_data_obj): - - """Called by self.move_container_to_top(). - - Moves a channel, playlist or folder to the top level (in other words, - removes its parent folder). - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - moving media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7634 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) - - # Update IVs - media_data_obj.parent_obj.del_child(media_data_obj) - media_data_obj.set_parent_obj(None) - self.media_top_level_list.append(media_data_obj.dbid) - - # Save the database (because, if the user terminates Tartube and then - # restarts it, then tries to perform a download operation, a load of - # Python error messages will be generated, complaining that - # directories don't exist) - self.save_db() - - # Remove the moving object from the Video Index, and put it back there - # at its new location - self.main_win_obj.video_index_delete_row(media_data_obj) - self.main_win_obj.video_index_add_row(media_data_obj) - - # Select the moving object, which redraws the Video Catalogue - self.main_win_obj.video_index_select_row(media_data_obj) - - - def move_container(self, source_obj, dest_obj): - - """Called by mainwin.MainWin.on_video_index_drag_data_received(). - - Before moving a channel, playlist or folder, get confirmation from the - user. - - After getting confirmation, call self.move_container_continue() to move - the channel, playlist or folder into another folder. - - Args: - - source_obj (media.Channel, media.Playlist, media.Folder): The - moving media data object - - dest_obj (media.Folder): The destination folder - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7679 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, - 'Move container request failed sanity check', - ) - - elif source_obj == dest_obj: - # No need for a system error message if the user drags a folder - # onto itself; just do nothing - return - - # Ignore Video Index drag-and-drop during an download/update/refresh/ - # info/tidy operation - elif self.current_manager_obj: - return - - 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', - ) - - return - - 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)', - 'error', - 'ok', - ) - - return - - elif dest_obj.restrict_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - 'The folder \'' + dest_obj.name \ - + '\' can only contain videos', - 'error', - 'ok', - ) - - return - - # Check that the target directory doesn't already exist (unlikely, but - # possible if the user has been copying files manually) - target_path = os.path.abspath( - os.path.join( - dest_obj.get_default_dir(self), - source_obj.name, - ), - ) - - 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.', - 'error', - 'ok', - ) - - return - - # Prompt the user for confirmation - source_type = source_obj.get_type() - - if not dest_obj.temp_flag: - temp_string = '' - else: - temp_string = '\n\nWARNING: The destination folder is marked' \ - + ' as temporary, so everything inside it will be DELETED when ' \ - + __main__.__prettyname__ + ' shuts down!', - - # 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, - 'question', - 'yes-no', - None, # Parent window is main window - # Arguments passed directly to .move_container_continue() - { - 'yes': 'move_container_continue', - 'data': [source_obj, dest_obj], - }, - ) - - - def move_container_continue(self, media_list): - - """Called by self.move_container(). - - Moves a channel, playlist or folder into another folder. - - Args: - - media_list (list): List in the form (destination, source), where - both are media.Folder objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7807 move_container_continue') - - source_obj = media_list[0] - dest_obj = media_list[1] - - # Move the sub-directories to their new location - shutil.move( - source_obj.get_default_dir(self), - dest_obj.get_default_dir(self), - ) - - # Update both media data objects' IVs - if source_obj.parent_obj: - source_obj.parent_obj.del_child(source_obj) - - dest_obj.add_child(source_obj) - source_obj.set_parent_obj(dest_obj) - - if source_obj.dbid in self.media_top_level_list: - index = self.media_top_level_list.index(source_obj.dbid) - del self.media_top_level_list[index] - - # Save the database (because, if the user terminates Tartube and then - # restarts it, then tries to perform a download operation, a load of - # Python error messages will be generated, complaining that - # directories don't exist) - self.save_db() - - # Remove the moving object from the Video Index, and put it back there - # at its new location - self.main_win_obj.video_index_delete_row(source_obj) - self.main_win_obj.video_index_add_row(source_obj) - # Select the moving object, which redraws the Video Catalogue - self.main_win_obj.video_index_select_row(source_obj) - - - # (Convert channels to playlists, and vice-versa) - - - def convert_remote_container(self, old_obj): - - """Called by mainwin.MainWin.on_video_index_convert_container(). - - Converts a media.Channel object into a media.Playlist object, or vice- - versa. - - Usually called after the user has copy-pasted a list of URLs into the - mainwin.AddVideoDialogue window, some of which actually represent - channels or playlists, not individual videos. During the next - download operation, new channels or playlists can be automatically - created (depending on the value of self.operation_convert_mode - - The user can then convert a channel to a playlist, and back again, as - required. - - Args: - - old_obj (media.Channel, media.Playlist): The media data object to - convert - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7870 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, - 'Convert container request failed sanity check', - ) - - # If old_obj is a media.Channel, create a playlist. If old_obj is - # a media.Playlist, create a channel - if isinstance(old_obj, media.Channel): - - new_obj = self.add_playlist( - old_obj.name, - old_obj.parent_obj, - old_obj.source, - old_obj.dl_sim_flag, - ) - - elif isinstance(old_obj, media.Playlist): - - new_obj = self.add_channel( - old_obj.name, - old_obj.parent_obj, - old_obj.source, - old_obj.dl_sim_flag, - ) - - # Move any children from the old object to the new one - for child_obj in old_obj.child_list: - - # The True argument means to delay sorting the child list - new_obj.add_child(child_obj, True) - child_obj.set_parent_obj(new_obj) - - # Deal with alternative download destinations - if old_obj.master_dbid: - new_obj.set_master_dbid(self, old_obj.master_dbid) - master_obj = self.media_reg_dict[old_obj.master_dbid] - master_obj.del_slave_dbid(old_obj.dbid) - - for slave_dbid in old_obj.slave_dbid_list: - slave_obj = self.media_reg_dict[slave_dbid] - slave_obj.set_master_dbid(self, new_obj.dbid) - - # Copy remaining properties from the old object to the new one - new_obj.clone_properties(old_obj) - - # Remove the old object from the media data registry. - # self.media_name_dict should already be updated - del self.media_reg_dict[old_obj.dbid] - if old_obj.dbid in self.media_top_level_list: - self.media_top_level_list.remove(old_obj.dbid) - - # Remove the old object from the Video Index... - self.main_win_obj.video_index_delete_row(old_obj) - # ...and add the new one, selecting it at the same time - self.main_win_obj.video_index_add_row(new_obj) - - - # (Delete media data objects) - - - def delete_video(self, video_obj, delete_files_flag=False, - no_update_index_flag=False, no_update_catalogue_flag=False): - - """Can be called by anything. - - Deletes a video object from the media registry. - - Args: - - video_obj (media.Video): The media.Video object to delete - - delete_files_flag (bool): True when called by - mainwin.MainWin.on_video_catalogue_delete_video, in which case - the video and its associated files are deleted from the - filesystem - - no_update_index_flag (bool): True when called by - self.delete_old_videos() or self.delete_container(), in which - case the Video Index is not updated - - no_update_catalogue_flag (bool): True when called by - self.delete_old_videos(), in which case the Video Catalogue is - not updated - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 7963 delete_video') - - if not isinstance(video_obj, media.Video): - return self.system_error( - 129, - 'Delete video request failed sanity check', - ) - - # Remove the video from its parent object - video_obj.parent_obj.del_child(video_obj) - - # Remove the corresponding entry in private folder's child lists - update_list = [video_obj.parent_obj] - if self.fixed_all_folder.del_child(video_obj): - update_list.append(self.fixed_all_folder) - - if self.fixed_bookmark_folder.del_child(video_obj): - update_list.append(self.fixed_bookmark_folder) - - if self.fixed_fav_folder.del_child(video_obj): - update_list.append(self.fixed_fav_folder) - - if self.fixed_new_folder.del_child(video_obj): - update_list.append(self.fixed_new_folder) - - if self.fixed_waiting_folder.del_child(video_obj): - update_list.append(self.fixed_waiting_folder) - - # Remove the video from our IVs - # v1.2.017 When deleting folders containing thousands of videos, I - # noticed that a small number of video DBIDs didn't exist in the - # registry. Not sure what the cause is, but the following lines - # prevent a python error - if video_obj.dbid in self.media_reg_dict: - del self.media_reg_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 - # files because another channel/playlist/folder might be using them - if delete_files_flag \ - and video_obj.file_name \ - and video_obj.parent_obj.dbid == video_obj.parent_obj.master_dbid: - - # There might be thousands of files in the directory, so using - # os.walk() or something like that might be too expensive - # Also, post-processing might create various artefacts, all of - # which must be deleted - ext_list = [ - 'description', - 'info.json', - 'annotations.xml', - ] - ext_list.extend(formats.VIDEO_FORMAT_LIST) - ext_list.extend(formats.AUDIO_FORMAT_LIST) - - for ext in ext_list: - - file_path = video_obj.get_default_path_by_ext(self, ext) - if os.path.isfile(file_path): - os.remove(file_path) - - # (Thumbnails might be in one of two locations, so are handled - # separately) - thumb_path = utils.find_thumbnail(self, video_obj) - if thumb_path and os.path.isfile(thumb_path): - os.remove(thumb_path) - - # Remove the video from the catalogue, if present - if not no_update_catalogue_flag: - self.main_win_obj.video_catalogue_delete_row(video_obj) - - # Update rows in the Video Index, first checking that the parent - # container object is currently drawn there (which it might not be, - # if emptying temporary folders on startup) - if not no_update_index_flag: - for container_obj in update_list: - - if container_obj.name \ - in self.main_win_obj.video_index_row_dict: - self.main_win_obj.video_index_update_row_text( - container_obj, - ) - - - def delete_container(self, media_data_obj, empty_flag=False): - - """Can be called by anything. - - Before deleting a channel, playlist or folder object from the media - data registry, get confirmation from the user. - - The process is split across three functions. - - This functions obtains confirmation from the user. If deleting files, - a second confirmation is required, and self.delete_container_continue() - is called in response. - - In either case, self.delete_container_complete() is then called to - update the media data registry. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): - The container media data object - - empty_flag (bool): If True, the container media data object is to - be emptied, rather than being deleted - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 8075 delete_container') - - # Check this isn't a video or a fixed folder (which cannot be removed) - if isinstance(media_data_obj, media.Video) \ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.fixed_flag - ): - return self.system_error( - 130, - 'Delete container request failed sanity check', - ) - - # Prompt the user for confirmation, even if the container object has no - # children - # (Even though there are no children, we can't guarantee that the - # sub-directories in Tartube's data directory are empty) - # Exception: don't prompt for confirmation if media_data_obj is - # somewhere inside a temporary folder - confirm_flag = True - delete_file_flag = False - parent_obj = media_data_obj.parent_obj - - while parent_obj is not None: - if isinstance(parent_obj, media.Folder) and parent_obj.temp_flag: - # The media data object is somewhere inside a temporary folder; - # no need to prompt for confirmation - confirm_flag = False - - parent_obj = parent_obj.parent_obj - - if confirm_flag: - - # Prompt the user for confirmation - dialogue_win = mainwin.DeleteContainerDialogue( - self.main_win_obj, - media_data_obj, - empty_flag, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - if dialogue_win.button2.get_active(): - delete_file_flag = True - else: - delete_file_flag = False - - # ...before destroying it - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - # Get a second confirmation, if required to delete files - 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 - # Arguments passed directly to .delete_container_continue() - { - 'yes': 'delete_container_continue', - 'data': [media_data_obj, empty_flag], - } - ) - - # No second confirmation required, so we can proceed directly to the - # call to self.delete_container_complete() - else: - self.delete_container_complete(media_data_obj, empty_flag) - - - def delete_container_continue(self, data_list): - - """Called by self.delete_container(). - - When deleting a container, after the user has specified that files - should be deleted too, this function is called to delete those files. - - Args: - - data_list (list): A list of two items. The first is the container - media data object; the second is a flag set to True if the - container should be emptied, rather than being deleted - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 8167 delete_container_continue') - - # Unpack the arguments - media_data_obj = data_list[0] - empty_flag = data_list[1] - - # Confirmation obtained, so delete the files - container_dir = media_data_obj.get_default_dir(self) - if os.path.isdir(container_dir): - shutil.rmtree(container_dir) - - # If emptying the container rather than deleting it, just create a - # replacement (empty) directory on the filesystem - if empty_flag: - os.makedirs(container_dir) - - # Now call self.delete_container_complete() to handle the media data - # registry - self.delete_container_complete(media_data_obj, empty_flag) - - - def delete_container_complete(self, media_data_obj, empty_flag, - recursive_flag=False): - - """Called by self.delete_container() and .delete_container_continue(). - Subsequently called by this function recursively. - - Deletes a channel, playlist or folder object from the media data - registry. - - This function calls itself recursively to delete all of the container - object's descendants. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): - The container media data object - - empty_flag (bool): If True, the container media data object is to - be emptied, rather than being deleted - - recursive_flag (bool): Set to False on the initial call to this - function from some other part of the code, and True when this - function calls itself recursively - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 8215 delete_container_complete') - - # Confirmation has been obtained, and any files have been deleted (if - # required), so now deal with the media data registry - - # Recursively remove all of the container object's children. The code - # doesn't work as intended, unless we make a copy of the list of - # child objects first - copy_list = media_data_obj.child_list.copy() - for child_obj in copy_list: - if isinstance(child_obj, media.Video): - self.delete_video(child_obj, False, True) - else: - self.delete_container_complete(child_obj, False, True) - - if not empty_flag or recursive_flag: - - # Remove the container object from its own parent object (if it has - # one) - if media_data_obj.parent_obj: - media_data_obj.parent_obj.del_child(media_data_obj) - - # Reset alternative download destinations - media_data_obj.set_master_dbid(self, media_data_obj.dbid) - - # Remove the media data object from our IVs - del self.media_reg_dict[media_data_obj.dbid] - del self.media_name_dict[media_data_obj.name] - if media_data_obj.dbid in self.media_top_level_list: - index = self.media_top_level_list.index(media_data_obj.dbid) - del self.media_top_level_list[index] - - # During the initial call to this function, delete the container - # object from the Video Index (which automatically resets the Video - # Catalogue) - # (If deleting the contents of temporary folders while loading a - # Tartube database, the Video Index may not yet have been drawn, so - # we have to check for that) - if not recursive_flag and not empty_flag \ - and media_data_obj.name in self.main_win_obj.video_index_row_dict: - - self.main_win_obj.video_index_delete_row(media_data_obj) - - # Also redraw the private folders in the Video Index, to show the - # correct number of downloaded/new videos, etc - self.main_win_obj.video_index_update_row_text( - self.fixed_all_folder, - ) - - self.main_win_obj.video_index_update_row_text( - self.fixed_bookmark_folder, - ) - - self.main_win_obj.video_index_update_row_text( - self.fixed_fav_folder, - ) - - self.main_win_obj.video_index_update_row_text( - self.fixed_new_folder, - ) - - self.main_win_obj.video_index_update_row_text( - self.fixed_waiting_folder, - ) - - elif not recursive_flag and empty_flag: - - # When emptying the container, the quickest way to update the Video - # Index is just to redraw it from scratch - self.main_win_obj.video_index_catalogue_reset() - - - # (Change media data object settings, updating all related things) - - - def prepare_mark_video(self, data_list): - - """Called by self.mark_container_favourite(), .mark_container_new() - and mainwin.MainWin.on_video_index_mark_bookmark(), etc. - - The operation to mark a container's video as bookmarked or not - bookmarked (etc) can take a very long time, especially if there are - thousands of videos. - - This function takes some shortcuts to reduce the time to a few - seconds. - - Args: - - data_list (list): List in the form - - (action_type, action_flag, container_obj, video_list) - - ...where 'action_type' is one of the strings 'bookmark', - 'favourite', 'new' or 'waiting', 'action_flag' is True (e,g. to - bookmark a video) or False (e.g. to unbookmark a video), - 'container_obj' is a media.Channel, media.Playlist or - media.Folder object, and 'video_list' is a list of media.Video - objects to update (only specified when 'action_type' is - 'favourite' or 'new' - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 8319 prepare_mark_video') - - action_type = data_list.pop(0) - action_flag = data_list.pop(0) - container_obj = data_list.pop(0) - if action_type == 'favourite' or action_type == 'new': - video_list = data_list.pop(0) - else: - video_list = container_obj.child_list - - # Take some shortcuts - for child_obj in video_list: - - if isinstance(child_obj, media.Video): - - if action_type == 'bookmark': - self.mark_video_bookmark( - child_obj, - action_flag, # Mark video bookmarked - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - elif action_type == 'favourite': - - self.mark_video_favourite( - child_obj, - action_flag, # Mark video favourite (or not) - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - elif action_type == 'new': - - self.mark_video_new( - child_obj, - action_flag, # Mark video favourite (or not) - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - elif action_type == 'waiting': - - self.mark_video_waiting( - child_obj, - action_flag, # Mark video waiting (or not) - True, # Don't update the Video Index - True, # Don't update the Video Catalogue - True, # Don't sort the child list each time - ) - - # Now we can sort the system folder's child list... - if action_type == 'bookmark': - self.fixed_bookmark_folder.sort_children() - elif action_type == 'favourite': - self.fixed_fav_folder.sort_children() - elif action_type == 'new': - self.fixed_new_folder.sort_children() - elif action_type == 'waiting': - self.fixed_waiting_folder.sort_children() - - # ...and then can redraw the Video Index and Video Catalogue, - # re-selecting the current selection, if any - self.main_win_obj.video_index_catalogue_reset(True) - - - def mark_video_bookmark(self, video_obj, bookmark_flag, \ - no_update_index_flag=False, no_update_catalogue_flag=False, \ - no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as bookmarked or not bookmarked. - - The video object's .bookmark_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - bookmark_flag (bool): True to mark the video as bookmarked, False - to mark it as not bookmarked - - no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'Bookmarks' 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 8420 mark_video_bookmark') - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_bookmark_folder] - if not no_update_index_flag: - update_list.append(video_obj.parent_obj) - update_list.append(self.fixed_all_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 bookmarked or not bookmarked - if not isinstance(video_obj, media.Video): - return self.system_error( - 131, - 'Mark video as bookmarked request failed sanity check', - ) - - elif not bookmark_flag: - - # Mark video as not bookmarked - if not video_obj.bookmark_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_bookmark_flag(False) - # Update the parent object - video_obj.parent_obj.dec_bookmark_count() - - # Remove this video from the private 'Bookmarks' folder (the - # folder's count IVs are automatically updated) - self.fixed_bookmark_folder.del_child(video_obj) - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'Bookmarks' 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_bookmark_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_bookmark_count() - self.fixed_bookmark_folder.dec_bookmark_count() - if video_obj.fav_flag: - self.fixed_fav_folder.dec_bookmark_count() - if video_obj.new_flag: - self.fixed_new_folder.dec_bookmark_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_bookmark_count() - - else: - - # Mark video as bookmarked - if video_obj.bookmark_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_bookmark_flag(True) - # Update the parent object - video_obj.parent_obj.inc_bookmark_count() - - # Add this video to the private 'Bookmarks' folder - self.fixed_bookmark_folder.add_child(video_obj, no_sort_flag) - self.fixed_bookmark_folder.inc_bookmark_count() - if video_obj.dl_flag: - self.fixed_bookmark_folder.inc_dl_count() - if video_obj.fav_flag: - self.fixed_bookmark_folder.inc_fav_count() - if video_obj.new_flag: - self.fixed_bookmark_folder.inc_new_count() - if video_obj.waiting_flag: - self.fixed_bookmark_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 - self.fixed_all_folder.inc_bookmark_count() - if video_obj.fav_flag: - self.fixed_fav_folder.inc_bookmark_count() - if video_obj.new_flag: - self.fixed_new_folder.inc_bookmark_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_bookmark_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_downloaded(self, video_obj, dl_flag, not_new_flag=False): - - """Can be called by anything. - - Marks a video object as downloaded (i.e. the video file exists on the - user's filesystem) or not downloaded. - - The video object's .dl_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark. - - dl_flag (bool): True to mark the video as downloaded, False to mark - it as not downloaded. - - not_new_flag (bool): Set to True when called by - downloads.confirm_old_video(). The video is downloaded, but not - new - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 8549 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] - - # Mark the video as downloaded or not downloaded - if not isinstance(video_obj, media.Video): - return self.system_error( - 132, - 'Mark video as downloaded request failed sanity check', - ) - - elif not dl_flag: - - # Mark video as not downloaded - if not video_obj.dl_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_dl_flag(False) - # (A video that is not downloaded cannot be marked archived) - video_obj.set_archive_flag(False) - # Update the parent container object - video_obj.parent_obj.dec_dl_count() - # Update private folders - self.fixed_all_folder.dec_dl_count() - self.fixed_new_folder.dec_dl_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_dl_count() - update_list.append(self.fixed_bookmark_folder) - if video_obj.fav_flag: - self.fixed_fav_folder.dec_dl_count() - update_list.append(self.fixed_fav_folder) - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_dl_count() - update_list.append(self.fixed_waiting_folder) - - # Also mark the video as not new - if not not_new_flag: - self.mark_video_new(video_obj, False, True) - - else: - - # Mark video as downloaded - if video_obj.dl_flag: - - # Already marked - return - - else: - - # If any ancestor channels, playlists or folders are marked as - # favourite, the video must be marked favourite as well - if video_obj.ancestor_is_favourite(): - self.mark_video_favourite(video_obj, True, True) - - # Update the video object's IVs - video_obj.set_dl_flag(True) - # Update the parent container object - video_obj.parent_obj.inc_dl_count() - # Update private folders - self.fixed_all_folder.inc_dl_count() - self.fixed_new_folder.inc_dl_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_dl_count() - update_list.append(self.fixed_bookmark_folder) - if video_obj.fav_flag: - self.fixed_fav_folder.inc_dl_count() - update_list.append(self.fixed_fav_folder) - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_dl_count() - update_list.append(self.fixed_waiting_folder) - - # Also mark the video as new - if not not_new_flag: - self.mark_video_new(video_obj, True, True) - - # 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_favourite(self, video_obj, fav_flag, \ - no_update_index_flag=False, no_update_catalogue_flag=False, - no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as favourite or not favourite. - - The video object's .fav_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - fav_flag (bool): True to mark the video as favourite, False to mark - it as not favourite - - no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'Favourite Videos' 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 8667 mark_video_favourite') - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_fav_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.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 favourite or not favourite - if not isinstance(video_obj, media.Video): - return self.system_error( - 133, - 'Mark video as favourite request failed sanity check', - ) - - elif not fav_flag: - - # Mark video as not favourite - if not video_obj.fav_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_fav_flag(False) - # Update the parent object - video_obj.parent_obj.dec_fav_count() - - # Remove this video from the private 'Favourite Videos' folder - # (the folder's count IVs are automatically updated) - self.fixed_fav_folder.del_child(video_obj) - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'Favourite Videos' 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_fav_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_fav_count() - self.fixed_fav_folder.dec_fav_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_fav_count() - if video_obj.new_flag: - self.fixed_new_folder.dec_fav_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_fav_count() - - else: - - # Mark video as favourite - if video_obj.fav_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_fav_flag(True) - # Update the parent object - video_obj.parent_obj.inc_fav_count() - - # Add this video to the private 'Favourite Videos' folder - self.fixed_fav_folder.add_child(video_obj, no_sort_flag) - self.fixed_fav_folder.inc_fav_count() - if video_obj.bookmark_flag: - self.fixed_fav_folder.inc_bookmark_count() - if video_obj.dl_flag: - self.fixed_fav_folder.inc_dl_count() - if video_obj.new_flag: - self.fixed_fav_folder.inc_new_count() - if video_obj.waiting_flag: - self.fixed_fav_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 - self.fixed_all_folder.inc_fav_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_fav_count() - if video_obj.new_flag: - self.fixed_new_folder.inc_fav_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_fav_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): - - """Can be called by anything. - - Marks a video object as new (i.e. unwatched by the user), or as not - new (already watched by the user). - - The video object's .new_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - new_flag (bool): True to mark the video as new, False to mark it as - not new - - no_update_index_flag (bool): True if the Video Index should not be - updated (except for the system 'New Videos' 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 8806 mark_video_new') - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_new_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.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, - 'Mark video as new request failed sanity check', - ) - - elif not new_flag: - - # Mark video as not new - if not video_obj.new_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_new_flag(False) - # Update the parent object - video_obj.parent_obj.dec_new_count() - - # Remove this video from the private 'New Videos' folder - # (the folder's count IVs are automatically updated) - self.fixed_new_folder.del_child(video_obj) - self.fixed_new_folder.dec_new_count() - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'New Videos' 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_new_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_new_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_new_count() - if video_obj.fav_flag: - self.fixed_fav_folder.dec_new_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.dec_new_count() - - else: - - # Mark video as new - if video_obj.new_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_new_flag(True) - # Update the parent object - video_obj.parent_obj.inc_new_count() - - # Add this video to the private 'New Videos' folder - self.fixed_new_folder.add_child(video_obj, no_sort_flag) - self.fixed_new_folder.inc_new_count() - if video_obj.bookmark_flag: - self.fixed_new_folder.inc_bookmark_count() - if video_obj.fav_flag: - self.fixed_new_folder.inc_fav_count() - if video_obj.waiting_flag: - self.fixed_new_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 - self.fixed_all_folder.inc_new_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_new_count() - if video_obj.fav_flag: - self.fixed_fav_folder.inc_new_count() - if video_obj.waiting_flag: - self.fixed_waiting_folder.inc_new_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_waiting(self, video_obj, waiting_flag, \ - no_update_index_flag=False, no_update_catalogue_flag=False, \ - no_sort_flag=False): - - """Can be called by anything. - - Marks a video object as in the waiting list or not in the waiting list. - - The video object's .waiting_flag IV is updated. - - Args: - - video_obj (media.Video): The media.Video object to mark - - waiting_flag (bool): True to mark the video as in the waiting list, - 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), - 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 8941 mark_video_waiting') - - # (List of Video Index rows to update, at the end of this function) - update_list = [self.fixed_waiting_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) - - # 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, - 'Mark video as in waiting list request failed sanity check', - ) - - elif not waiting_flag: - - # Mark video as not in the waiting list - if not video_obj.waiting_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_waiting_flag(False) - # Update the parent object - video_obj.parent_obj.dec_waiting_count() - - # Remove this video from the private 'Waiting Videos' folder - # (the folder's count IVs are automatically updated) - self.fixed_waiting_folder.del_child(video_obj) - # Update the Video Catalogue, if that folder is the visible one - # (deleting the row, if the 'Waiting Videos' 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_waiting_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_waiting_count() - self.fixed_waiting_folder.dec_waiting_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.dec_waiting_count() - if video_obj.fav_flag: - self.fixed_fav_folder.dec_waiting_count() - if video_obj.new_flag: - self.fixed_new_folder.dec_waiting_count() - - else: - - # Mark video as in the waiting list - if video_obj.waiting_flag: - - # Already marked - return - - else: - - # Update the video object's IVs - video_obj.set_waiting_flag(True) - # Update the parent object - video_obj.parent_obj.inc_waiting_count() - - # Add this video to the private 'Waiting Videos' folder - self.fixed_waiting_folder.add_child(video_obj, no_sort_flag) - self.fixed_waiting_folder.inc_waiting_count() - if video_obj.bookmark_flag: - self.fixed_waiting_folder.inc_bookmark_count() - if video_obj.dl_flag: - self.fixed_waiting_folder.inc_dl_count() - if video_obj.fav_flag: - self.fixed_waiting_folder.inc_fav_count() - if video_obj.new_flag: - self.fixed_waiting_folder.inc_new_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 - self.fixed_all_folder.inc_waiting_count() - if video_obj.bookmark_flag: - self.fixed_bookmark_folder.inc_waiting_count() - if video_obj.fav_flag: - self.fixed_fav_folder.inc_waiting_count() - if video_obj.new_flag: - self.fixed_new_folder.inc_waiting_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_folder_hidden(self, folder_obj, flag): - - """Called by callbacks in self.on_menu_show_hidden() and - mainwin.MainWin.on_video_index_hide_folder(). - - Marks a folder as hidden (not visible in the Video Index) or not - hidden (visible in the Video Index, although the user might be - required to expand the tree to see it). - - Args: - - folder_obj (media.Folder): The folder object to mark - - flag (bool): True to mark the folder as hidden, False to mark it as - not hidden - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9067 mark_folder_hidden') - - if not isinstance(folder_obj, media.Folder): - return self.system_error( - 136, - 'Mark folder as hidden request failed sanity check', - ) - - if not flag: - - # Mark folder as not hidden - if not folder_obj.hidden_flag: - - # Already marked - return - - else: - - # Update the folder object's IVs - folder_obj.set_hidden_flag(False) - # Update the Video Index - self.main_win_obj.video_index_add_row(folder_obj) - - else: - - # Mark video as hidden - if folder_obj.hidden_flag: - - # Already marked - return - - else: - - # Update the folder object's IVs - folder_obj.set_hidden_flag(True) - # Update the Video Index - self.main_win_obj.video_index_delete_row(folder_obj) - - - def mark_container_archived(self, media_data_obj, archive_flag, - only_child_videos_flag): - - """Called by mainwin.MainWin.on_video_index_mark_archived() and - .on_video_index_mark_not_archived(). - - Marks any descedant videos as archived. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The container object to update - - archive_flag (bool): True to mark as archived, False to mark as not - archived - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if the container object and all - its descendants should be marked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9129 mark_container_archived') - - if isinstance(media_data_obj, media.Video): - return self.system_error( - 137, - 'Mark container as archived request failed sanity check', - ) - - # Special arrangements for private folders - if media_data_obj == self.fixed_all_folder: - - # Check every video - for other_obj in list(self.media_reg_dict.values()): - - if isinstance(other_obj, media.Video) and other_obj.dl_flag: - other_obj.set_archive_flag(archive_flag) - - elif not archive_flag and media_data_obj == self.fixed_bookmark_folder: - - # Check videos in this folder - for other_obj in self.fixed_bookmark_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.bookmark_flag: - other_obj.set_archive_flag(archive_flag) - - elif not archive_flag and media_data_obj == self.fixed_fav_folder: - - # Check videos in this folder - for other_obj in self.fixed_fav_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.fav_flag: - other_obj.set_archive_flag(archive_flag) - - elif media_data_obj == self.fixed_new_folder: - - # Check videos in this folder - for other_obj in self.fixed_new_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.new_flag: - other_obj.set_archive_flag(archive_flag) - - elif not archive_flag and media_data_obj == self.fixed_waiting_folder: - - # Check videos in this folder - for other_obj in self.fixed_waiting_folder.child_list: - - if isinstance(other_obj, media.Video) and other_obj.dl_flag \ - and other_obj.waiting_flag: - other_obj.set_archive_flag(archive_flag) - - elif only_child_videos_flag: - - # Check videos in this channel/playlist/folder - for other_obj in media_data_obj.child_list: - - if isinstance(other_obj, media.Video): - other_obj.set_archive_flag(archive_flag) - - else: - - # Check videos in this channel/playlist/folder, and in any - # descendant channels/playlists/folders - for other_obj in media_data_obj.compile_all_videos( [] ): - - if isinstance(other_obj, media.Video) and other_obj.dl_flag: - other_obj.set_archive_flag(archive_flag) - - # In all cases, update the row on the Video Index - self.main_win_obj.video_index_update_row_icon(media_data_obj) - self.main_win_obj.video_index_update_row_text(media_data_obj) - # If this container is the one visible in the Video Catalogue, redraw - # the Video Catalogue - if self.main_win_obj.video_index_current == media_data_obj.name: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - ) - - - def mark_container_favourite(self, media_data_obj, fav_flag, - only_child_videos_flag): - - """Called by mainwin.MainWin.on_video_index_mark_favourite() and - .on_video_index_mark_not_favourite(). - - Marks this channel, playlist or folder as favourite (or not favourite). - Also marks any descendant videos as (not) favourite (but not descendent - channels, playlists or folders). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The container object to update - - fav_flag (bool): True to mark as favourite, False to mark as not - favourite - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if the container object and all - its descendants should be marked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9235 mark_container_favourite') - - if isinstance(media_data_obj, media.Video): - return self.system_error( - 138, - 'Mark container as favourite request failed sanity check', - ) - - # Special arrangements for private folders. Mark the videos as - # favourite, but don't modify their parent channels, playlists and - # folders - # (For the private 'Favourite Videos' folder, don't need to do anything - # if 'fav_flag' is True, because the popup menu item is desensitised) - video_list = [] - - if media_data_obj == self.fixed_all_folder: - - # Check every video - for other_obj in list(self.media_reg_dict.values()): - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - elif media_data_obj == self.fixed_bookmark_folder: - - # Check videos in this folder - for other_obj in self.fixed_bookmark_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.bookmark_flag: - video_list.append(other_obj) - - elif not flag and media_data_obj == self.fixed_fav_folder: - - # Check videos in this folder - for other_obj in self.fixed_fav_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.fav_flag: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_new_folder: - - # Check videos in this folder - for other_obj in self.fixed_new_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.new_flag: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_waiting_folder: - - # Check videos in this folder - for other_obj in self.fixed_waiting_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.waiting_flag: - video_list.append(other_obj) - - elif only_child_videos_flag: - - # Check only videos that are children of the specified media data - # object - for other_obj in media_data_obj.child_list: - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - else: - - # Check only video objects that are descendants of the specified - # media data object - for other_obj in media_data_obj.compile_all_videos( [] ): - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - else: - # For channels, playlists and folders, we can set the IV - # directly - other_obj.set_fav_flag(fav_flag) - - # The channel, playlist or folder itself is also marked as - # favourite (obviously, we don't do that for private folders) - media_data_obj.set_fav_flag(fav_flag) - - # Take action, depending on how many videos there are - count = len(video_list) - - if not count: - - # Just update the row on the Video Index - self.main_win_obj.video_index_update_row_icon(media_data_obj) - self.main_win_obj.video_index_update_row_text(media_data_obj) - - elif count < self.main_win_obj.mark_video_lower_limit: - - # The operation should be quick - for child_obj in video_list: - self.mark_video_favourite(child_obj, fav_flag) - - elif count < self.main_win_obj.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.prepare_mark_video( - ['favourite', fav_flag, media_data_obj, video_list], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - 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?', - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': \ - ['favourite', fav_flag, media_data_obj, video_list], - }, - ) - - - def mark_container_new(self, media_data_obj, new_flag, - only_child_videos_flag): - - """Called by mainwin.MainWin.on_video_index_mark_new() and - .on_video_index_mark_not_new(). - - Marks videos in this channel, playlist or folder as new (or not new). - Also marks any descendant videos as (not) new (but not descendent - channels, playlists or folders). - - Unlike self.mark_container_favourite, the container itself is not - marked as new. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The container object to update - - new_flag (bool): True to mark as new, False to mark as not - new - - only_child_videos_flag (bool): Set to True if only child video - objects should be marked; False if the container object and all - its descendants should be marked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9390 mark_container_new') - - if isinstance(media_data_obj, media.Video): - return self.system_error( - 139, - 'Mark container as new request failed sanity check', - ) - - # Special arrangements for private folders - # (For the private 'Favourite Videos' folder, don't need to do anything - # if 'new_flag' is True, because the popup menu item is desensitised) - video_list = [] - - if media_data_obj == self.fixed_all_folder: - - # Check every video - for other_obj in list(self.media_reg_dict.values()): - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - elif media_data_obj == self.fixed_bookmark_folder: - - # Check videos in this folder - for other_obj in self.fixed_bookmark_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.bookmark_flag: - video_list.append(other_obj) - - elif not flag and media_data_obj == self.fixed_fav_folder: - - # Check videos in this folder - for other_obj in self.fixed_fav_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.fav_flag: - video_list.append(other_obj) - - elif media_data_obj == self.fixed_waiting_folder: - - # Check videos in this folder - for other_obj in self.fixed_waiting_folder.child_list: - - if isinstance(other_obj, media.Video) \ - and other_obj.waiting_flag: - video_list.append(other_obj) - - elif only_child_videos_flag: - - # Check only videos that are children of the specified media data - # object - for other_obj in media_data_obj.child_list: - - if isinstance(other_obj, media.Video): - video_list.append(other_obj) - - else: - - # Check only video objects that are descendants of the specified - # media data object - for other_obj in media_data_obj.compile_all_videos( [] ): - - # (Only downloaded videos can be marked as new) - if not new_flag or other_obj.dl_flag: - video_list.append(other_obj) - - # Take action, depending on how many videos there are - count = len(video_list) - - if not count: - - # Just update the row on the Video Index - self.main_win_obj.video_index_update_row_icon(media_data_obj) - self.main_win_obj.video_index_update_row_text(media_data_obj) - - elif count < self.main_win_obj.mark_video_lower_limit: - - # The operation should be quick - for child_obj in video_list: - self.mark_video_new(child_obj, new_flag) - - elif count < self.main_win_obj.mark_video_higher_limit: - - # This will take a few seconds, so don't prompt the user - self.prepare_mark_video( - ['new', new_flag, media_data_obj, video_list], - ) - - else: - - # This might take a few tens of seconds, so prompt the user for - # confirmation first - 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?', - 'question', - 'yes-no', - None, # Parent window is main window - { - 'yes': 'prepare_mark_video', - # Specified options - 'data': ['new', new_flag, media_data_obj, video_list], - }, - ) - - - def rename_container(self, media_data_obj): - - """Called by mainwin.MainWin.on_video_index_rename_location(). - - Renames a channel, playlist or folder. Also renames the corresponding - directory in Tartube's data directory. - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - media data object to be renamed - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9513 rename_container') - - # Do some basic checks - if media_data_obj is None or isinstance(media_data_obj, media.Video) \ - or self.current_manager_obj or self.main_win_obj.config_win_list \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag - ): - return self.system_error( - 140, - 'Rename container request failed sanity check', - ) - - # Prompt the user for a new name - dialogue_win = mainwin.RenameContainerDialogue( - self.main_win_obj, - media_data_obj, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - new_name = dialogue_win.entry.get_text() - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and new_name != '' \ - and new_name != media_data_obj.name: - - # Check that the name is legal - if new_name is None \ - 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', - 'error', - 'ok', - ) - - # Check that an existing channel/playlist/folder isn't already - # 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', - 'error', - 'ok', - ) - - # 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) - - except: - return self.dialogue_manager_obj.show_msg_dialogue( - 'Failed to rename \'' + media_data_obj.name + '\'', - 'error', - 'ok', - ) - - # Filesystem updated, so now update the media data object itself. - # This call also updates the object's .nickname IV - old_name = media_data_obj.name - media_data_obj.set_name(new_name) - # Update the media data registry - del self.media_name_dict[old_name] - self.media_name_dict[new_name] = media_data_obj.dbid - - # Reset the Video Index and the Video Catalogue (this prevents a - # lot of problems) - self.main_win_obj.video_index_catalogue_reset() - - # Save the database file (since the filesystem itself has changed) - self.save_db() - - - def rename_container_silently(self, media_data_obj, new_name): - - """Called by self.load_db(). - - A modified form of self.rename_container. No dialogue windows are used, - no widgets are updated or desensitised, and the Tartube database file - is not saved. - - No checks are carried out; it's up to the calling function to check - this function's return value, and respond appropriately. - - Renames a channel, playlist or folder. Also renames the corresponding - directory in Tartube's data directory. - - - Args: - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - media data object to be renamed - - 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') - - # 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, - '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) - - except: - return False - - # Filesystem updated, so now update the media data object itself. This - # call also updates the object's .nickname IV - old_name = media_data_obj.name - media_data_obj.set_name(new_name) - # Update the media data registry - del self.media_name_dict[old_name] - self.media_name_dict[new_name] = media_data_obj.dbid - - return True - - - def apply_download_options(self, media_data_obj): - - """Called by mainwin.MainWin.on_video_index_apply_options() and - config.GenericEditWin.on_button_apply_options_clicked(). - - Applies a download options object (options.OptionsManager) to a media - data object, and also to any of its descendants (unless they too have - an applied download options object). - - The download options are passed to youtube-dl during a download - operation. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist or - media.Folder): The media data object to which the download - options are applied. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9671 apply_download_options') - - if self.current_manager_obj \ - or media_data_obj.options_obj\ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ): - return self.system_error( - 142, - 'Apply download options request failed sanity check', - ) - - # Apply download options to the media data object - media_data_obj.set_options_obj(options.OptionsManager()) - # If required, clone download options from the General Options Manager - # into the new download options manager - if self.auto_clone_options_flag: - media_data_obj.options_obj.clone_options( - self.general_options_obj, - ) - - # Update the row in the Video Index - self.main_win_obj.video_index_update_row_icon(media_data_obj) - - - def remove_download_options(self, media_data_obj): - - """Called by callbacks in - mainwin.MainWin.on_video_index_remove_options() and - GenericEditWin.on_button_remove_clicked(). - - Removes a download options object (options.OptionsManager) from a media - data object, an action which also affects its descendants (unless they - too have an applied download options object). - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist or - media.Folder): The media data object from which the download - options are removed. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9716 remove_download_options') - - if self.current_manager_obj or not media_data_obj.options_obj: - return self.system_error( - 143, - 'Remove download options request failed sanity check', - ) - - # Remove download options from the media data object - media_data_obj.set_options_obj(None) - # Update the row in the Video Index - self.main_win_obj.video_index_update_row_icon(media_data_obj) - - - def check_container_name_is_legal(self, name): - - """Can be called by anything. - - Checks that the name of a channel, playlist or folder is legal, i.e. - that it doesn't match one of the regexes in - self.illegal_name_regex_list. - - Does not check whether an existing container is already using the name; - that's the responsibility of the calling code. - - Args: - - name (str): A proposed name for a media.Channel, media.Playlist or - media.Folder object - - Returns: - - True if the name is legal, False if it is illegal - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9753 check_container_name_is_legal') - - for regex in self.illegal_name_regex_list: - if re.search(regex, name, re.IGNORECASE): - # Illegal name - return False - - # Legal name - return True - - - # (Export/import data to/from the Tartube database) - - def export_from_db(self, media_list): - - """Called by self.on_menu_export_db() and - mainwin.MainWin.on_video_index_export(). - - Exports a summary of the Tartube database to an export file - either a - structured JSON file, or a plain text file, at the user's option. - - The export file typically contains a list of videos, channels, - playlists and folders, but not any downloaded files (videos, - thumbnails, etc). - - The export file is not the same as a Tartube database file (usually - tartube.db) and cannot be loaded as a database file. However, the - export file can be imported into an existing database. - - Args: - - media_list (list): A list of media data objects. If specified, only - those objects (and any media data objects they contain) are - included in the export. If an empty list is passed, the whole - database is included. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 9792 export_from_db') - - # If the specified list is empty, a summary of the whole database is - # exported - if not media_list: - whole_flag = True - else: - whole_flag = False - - # Prompt the user for which kinds of media data object should be - # included in the export, and which type of file (JSON or plain text) - # should be created - dialogue_win = mainwin.ExportDialogue(self.main_win_obj, whole_flag) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - include_video_flag = dialogue_win.checkbutton.get_active() - include_channel_flag = dialogue_win.checkbutton2.get_active() - include_playlist_flag = dialogue_win.checkbutton3.get_active() - preserve_folder_flag = dialogue_win.checkbutton4.get_active() - plain_text_flag = dialogue_win.checkbutton5.get_active() - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - # Prompt the user for the file path to use - file_chooser_win = Gtk.FileChooserDialog( - 'Select where to save the database export', - self.main_win_obj, - Gtk.FileChooserAction.SAVE, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.OK, - ), - ) - - if not plain_text_flag: - file_chooser_win.set_current_name(self.export_json_file_name) - else: - file_chooser_win.set_current_name(self.export_text_file_name) - - response = file_chooser_win.run() - if response != Gtk.ResponseType.OK: - file_chooser_win.destroy() - return - - file_path = file_chooser_win.get_filename() - file_chooser_win.destroy() - if not file_path: - return - - # Compile a dictionary of data to export, representing the contents of - # the database (in whole or in part) - # Throughout the export/import code, dictionaries in this form are - # called 'db_dict' - # Depending on the user's choices, the dictionary preserves the folder - # structure of the database (or not) - # - # Key-value pairs in the dictionary are in the form - # - # dbid: mini_dict - # - # 'dbid' is each media data object's .dbid - # 'mini_dict' is a dictionary of values representing a media data - # object - # - # The same 'mini_dict' structure is used during export and - # import procedures. Its keys are: - # - # type - set to 'video', 'channel', 'playlist' or 'folder' - # dbid - set to the media data object's .dbid - # name - set to the media data object's .name IV - # nickname - set to the media data object's .nickname IV (or - # None for videos) - # source - set to the media data object's .source IV (or - # None for folders) - # db_dict - the children of this media data object, stored in - # the form described above - # - # The import process adds some extra keys to a 'mini_dict' while - # processing it, but only for channels/playlists/folders. The extra - # keys are: - # - # display_name - # - the media data object's name, indented for display - # in mainwin.ImportDialogueWin - # video_count - # - the number of videos this media data object contains - # import_flag - # - True if the user has selected this media data object - # to be imported, False if they have deselected it - db_dict = {} - - # Compile the contents of the 'db_dict' to export - # If the media_list argument is empty, use the whole database. - # Otherwise, use only the specified media data objects (and any media - # data objects they contain) - if preserve_folder_flag and not plain_text_flag: - - if media_list: - - for media_data_obj in media_list: - - mini_dict = media_data_obj.prepare_export( - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - if mini_dict: - db_dict[media_data_obj.dbid] = mini_dict - - else: - - for dbid in self.media_top_level_list: - - media_data_obj = self.media_reg_dict[dbid] - - mini_dict = media_data_obj.prepare_export( - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - if mini_dict: - db_dict[media_data_obj.dbid] = mini_dict - - else: - - if media_list: - - for media_data_obj in media_list: - - db_dict = media_data_obj.prepare_flat_export( - db_dict, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - else: - - for dbid in self.media_top_level_list: - - media_data_obj = self.media_reg_dict[dbid] - - db_dict = media_data_obj.prepare_flat_export( - db_dict, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - if not db_dict: - - return self.dialogue_manager_obj.show_msg_dialogue( - 'There is nothing to export!', - 'error', - 'ok', - ) - - # Export a JSON file - if not plain_text_flag: - - # The exported JSON file has the same metadata as a config file, - # with only the 'file_type' being different - - # Prepare values - utc = datetime.datetime.utcfromtimestamp(time.time()) - - # Prepare a dictionary of data to save as a JSON file - json_dict = { - # Metadata - 'script_name': __main__.__packagename__, - 'script_version': __main__.__version__, - 'save_date': str(utc.strftime('%d %b %Y')), - 'save_time': str(utc.strftime('%H:%M:%S')), - 'file_type': 'db_export', - # Data - 'db_dict': db_dict, - } - - # Try to save the file - try: - with open(file_path, 'w') as outfile: - json.dump(json_dict, outfile, indent=4) - - except: - return self.dialogue_manager_obj.show_msg_dialogue( - 'Failed to save the database export file', - 'error', - 'ok', - ) - - # Export a plain text file - else: - - # The text file contains lines, in groups of three, in the - # following format: - # - # @type - # - # - # - # ...where '@type' is one of '@video', '@channel' or '@playlist' - # (the folder structure is never preserved in a plain text - # export) - # A video belongs to the channel/playlist above it - - # Prepare the list of lines - line_list = [] - - for dbid in db_dict.keys(): - - media_data_obj = self.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Channel): - line_list.append('@channel') - line_list.append(media_data_obj.name) - line_list.append(media_data_obj.source) - - elif isinstance(media_data_obj, media.Playlist): - line_list.append('@playlist') - line_list.append(media_data_obj.name) - line_list.append(media_data_obj.source) - - else: - continue - - if include_video_flag: - - for child_obj in media_data_obj.child_list: - # (Nothing but videos should be in this list, but we'll - # check anyway) - if isinstance(child_obj, media.Video): - line_list.append('@video') - line_list.append(child_obj.name) - line_list.append(child_obj.source) - - # Try to save the file - try: - with open(file_path, 'w') as outfile: - for line in line_list: - outfile.write(line + '\n') - - except: - return self.dialogue_manager_obj.show_msg_dialogue( - '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, - 'info', - 'ok', - ) - - - def import_into_db(self, json_flag): - - """Called by self.on_menu_import_json() and - .on_menu_import_plain_text(). - - Imports the contents of a JSON export file or a plain text export file - generated by a call to self.export_from_db(). - - After prompting the user, creates new media.Video, media.Channel, - media.Playlist and/or media.Folder objects. Checks for duplicates and - handles them appropriately. - - A JSON export file contains a dictionary, 'db_dict', containing further - dictionaries, 'mini_dict', whose formats are described in the comments - in self.export_from_db(). - - A plain text export file contains lines in groups of three, in the - format described in the comments in self.export_from_db(). - - Args: - - json_flag (bool): True if a JSON export file should be imported, - False if a plain text export file should be imported - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 10081 import_into_db') - - # Prompt the user for the export file to load - file_chooser_win = Gtk.FileChooserDialog( - 'Select the database export', - self.main_win_obj, - Gtk.FileChooserAction.OPEN, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.OK, - ), - ) - - response = file_chooser_win.run() - if response != Gtk.ResponseType.OK: - file_chooser_win.destroy() - return - - file_path = file_chooser_win.get_filename() - file_chooser_win.destroy() - if not file_path: - return - - # Try to load the export file - if not 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', - 'error', - 'ok', - ) - - # Parse the text file, creating a db_dict in the form described in - # the comments in self.export_from_db() - db_dict = self.parse_text_import(text) - - else: - - 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', - 'error', - 'ok', - ) - - # Do some basic checks on the loaded data - # (At the moment, JSON export files are compatible with all - # versions of Tartube after v1.0.0; this may change in future) - if not json_dict \ - or not 'script_name' in json_dict \ - or not 'script_version' in json_dict \ - or not 'save_date' in json_dict \ - or not 'save_time' in json_dict \ - or not 'file_type' in json_dict \ - 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', - 'error', - 'ok', - ) - - # Retrieve the database data itself. db_dict is in the form - # described in the comments in self.export_from_db() - db_dict = json_dict['db_dict'] - - if not db_dict: - return self.dialogue_manager_obj.show_msg_dialogue( - 'The database export file is invalid (or empty)', - 'error', - 'ok', - ) - - # Prompt the user to allow them to select which videos/channels/ - # playlists/folders to actually import, and how to deal with - # duplicate channels/playlists/folders - dialogue_win = mainwin.ImportDialogue(self.main_win_obj, db_dict) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying the - # dialogue window - # 'flat_db_dict' is a flattened version of the imported 'db_dict' (i.e. - # with its folder structure removed), and with additional key-value - # pairs added to each 'mini_dict'. (The new key-value pairs are also - # described in the comments in self.export_from_db() ) - import_videos_flag = dialogue_win.checkbutton.get_active() - merge_duplicates_flag = dialogue_win.checkbutton.get_active() - flat_db_dict = dialogue_win.flat_db_dict - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - return - - # Process the imported 'db_dict', creating new videos/channels/ - # playlists/folders as required, and dealing appropriately with - # any duplicates - (video_count, channel_count, playlist_count, folder_count) \ - = self.process_import( - db_dict, # The imported data - flat_db_dict, # The flattened version of that dictionary - None, # No parent 'mini_dict' yet - import_videos_flag, - merge_duplicates_flag, - 0, # video_count - 0, # channel_count - 0, # playlist count - 0, # folder_count - ) - - 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', - 'error', - 'ok', - ) - - else: - - # Update the Video Catalogue, in case any new videos have been - # imported into it - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - ) - - # 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) - - self.dialogue_manager_obj.show_msg_dialogue(msg, 'info', 'ok') - - - def parse_text_import(self, text): - - """Called by self.import_into_db(). - - Given the contents of a plain text database export, which has been - loaded into memory, convert the contents into the db_dict format - described in the comments in self.export_from_db(), as if a JSON - database export had been loaded. - - The text file contains lines, in groups of three, in the following - format: - - @type - - - - ...where '@type' is one of '@video', '@channel' or '@playlist' (the - folder structure is never preserved in a plain text export). - - A video belongs to the channel/playlist above it. - - Args: - - text (str): The contents of the loaded plain text file - - Returns: - - db_dict (dict): The converted data in the form described in the - comments in self.export_from_db() - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 10252 parse_text_import') - - db_dict = {} - dbid = 0 - last_container_mini_dict = None - - # Split text into separate lines - line_list = text.split('\n') - - # Remove all empty lines (including those containing only whitespace) - mod_list = [] - for line in line_list: - if re.search('\S', line): - mod_list.append(line) - - # Extract each group of three lines, and check they are valid - # If a group of three is invalid (or if we reach the end of the file - # in the middle of a group of 3), ignore that group and any - # subsequent groups, and just use the data already extracted - while len(mod_list) > 2: - - media_type = mod_list[0] - name = mod_list[1] - source = mod_list[2] - - mod_list = mod_list[3:] - - if media_type is None \ - or ( - media_type != '@video' and media_type != '@channel' \ - and media_type != '@playlist' - ) \ - or name is None or name == '' \ - or source is None or not utils.check_url(source): - break - - # A valid group of three; add an entry to db_dict using a fake dbid - dbid += 1 - - mini_dict = { - 'type': None, - 'dbid': dbid, - 'name': name, - 'nickname': name, - 'source': source, - 'db_dict': {}, - } - - if media_type == '@video': - mini_dict['type'] = 'video' - # A video belongs to the previous channel or playlist (if any) - if last_container_mini_dict is not None: - last_container_mini_dict['db_dict'][dbid] = mini_dict - - elif media_type == '@channel': - mini_dict['type'] = 'channel' - last_container_mini_dict = mini_dict - - else: - mini_dict['type'] = 'playlist' - last_container_mini_dict = mini_dict - - db_dict[dbid] = mini_dict - - # Procedure complete - return db_dict - - - def process_import(self, db_dict, flat_db_dict, parent_obj, - import_videos_flag, merge_duplicates_flag, video_count, channel_count, - playlist_count, folder_count): - - """Called by self.import_into_db(). Subsequently called by this - function recursively. - - Process a 'db_dict' (in the format described in the comments in - self.export_from_db() ). - - Create new videos/channels/playlists/folders as required, and deal - appropriately with any duplicates. - - Args: - - db_dict (dict): The dictionary described in self.export_from_db(); - if called from self.import_into_db(), the original imported - dictionary; if called recursively, a dictionary from somewhere - inside the original imported dictionary - - flat_db_dict (dict): A flattened version of the original imported - 'db_dict' (not necessarily the same 'db_dict' provided by the - argument above). Flattened means that the folder structure has - been removed, and additional key-value pairs have been added to - each 'mini_dict' - - parent_obj (media.Channel, media.Playlist, media.Folder or None): - The contents of db_dict are all children of this parent media - data object - - import_videos_flag (bool): If True, any video objects are imported. - If False, video objects are ignored - - merge_duplicates_flag (bool): If True, imported channels/playlists/ - folders with the same name (and source URL) as an existing - channel/playlist/folder are merged with them. If False, the - imported channel/playlist/folder is renamed - - video_count, channel_count, playlist_count, folder_count (int): The - total number of videos/channels/playlists/folders imported so - far - - Returns: - - video_count, channel_count, playlist_count, folder_count (int): The - updated counts after importing videos/channels/playlists/ - folders - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 10371 process_import') - - # To optimise the code below, compile a dictionary for quick lookup, - # containing the source URLs for all videos in the parent channel/ - # playlist/folder - url_check_dict = {} - if parent_obj: - for child_obj in parent_obj.child_list: - if isinstance(child_obj, media.Video) \ - and child_obj.source is not None: - url_check_dict[child_obj.source] = None - - # Deal in turn with each video/channel/playlist/folder stored at the - # top level of 'db_dict' - # The dbid is the one used in the database from which the export file - # was generated. Once imported into our database, the new media data - # object will be given a different dbid - # (In other words, we can't compare this dbid with those used in - # self.media_reg_dict) - for dbid in db_dict.keys(): - - media_data_obj = None - - # Each 'mini_dict' contains details for a single video/channel/ - # playlist/folder - mini_dict = db_dict[dbid] - - # Check whether the user has marked this item to be imported, or - # not - if int(dbid) in flat_db_dict: - - check_dict = flat_db_dict[int(dbid)] - if not check_dict['import_flag']: - - # Don't import this one - continue - - # This item is marked to be imported - if mini_dict['type'] == 'video': - - if import_videos_flag: - - # Check that a video with the same URL doesn't already - # exist in the parent channel/playlist/folder. If so, - # don't import this duplicate video - if not mini_dict['source'] in url_check_dict: - - # This video isn't a duplicate, so we can import it - video_obj = self.add_video( - parent_obj, - mini_dict['source'], - ) - - if video_obj: - video_count += 1 - video_obj.set_name(mini_dict['name']) - - else: - - if mini_dict['name'] in self.media_name_dict: - - old_dbid = self.media_name_dict[mini_dict['name']] - old_obj = self.media_reg_dict[old_dbid] - - # A channel/playlist/folder with the same name already - # exists in our database. Rename it if the user wants - # that, or if the two have different source URLs - if not merge_duplicates_flag \ - or old_obj.source != mini_dict['source']: - - # Rename the imported channel/playlist/folder - mini_dict['name'] = self.rename_imported_container( - mini_dict['name'], - ) - - mini_dict['nickname'] = self.rename_imported_container( - mini_dict['nickname'], - ) - - else: - - # Use the existing channel/playlist/folder of the same - # name, thereby merging the two - old_dbid = self.media_name_dict[mini_dict['name']] - media_data_obj = self.media_reg_dict[old_dbid] - - # Import the channel/playlist/folder - if mini_dict['type'] == 'channel': - media_data_obj = self.add_channel( - mini_dict['name'], - parent_obj, - mini_dict['source'], - ) - - if media_data_obj: - channel_count += 1 - - elif mini_dict['type'] == 'playlist': - media_data_obj = self.add_playlist( - mini_dict['name'], - parent_obj, - mini_dict['source'], - ) - - if media_data_obj: - playlist_count += 1 - - elif mini_dict['type'] == 'folder': - media_data_obj = self.add_folder( - mini_dict['name'], - parent_obj, - ) - - if media_data_obj: - folder_count += 1 - - # If the channel/playlist/folder was successfully imported, - # set its nickname, update the Video Index, then deal with - # any children by calling this function recursively - if media_data_obj is not None: - - media_data_obj.set_nickname(mini_dict['nickname']) - - self.main_win_obj.video_index_add_row(media_data_obj) - - if mini_dict['db_dict']: - - ( - video_count, channel_count, playlist_count, - folder_count, - ) = self.process_import( - mini_dict['db_dict'], - flat_db_dict, - media_data_obj, - import_videos_flag, - merge_duplicates_flag, - video_count, - channel_count, - playlist_count, - folder_count, - ) - - # Procedure complete - return video_count, channel_count, playlist_count, folder_count - - - def rename_imported_container(self, name): - - """Called by self.process_import(). - - When importing a channel/playlist/folder whose name is the same as an - existing channel/playlist/folder, this function is called to rename - the imported one (when necessary). - - For example, converts 'Comedy' to 'Comedy (2)'. - - Args: - - name (str): The name of the imported channel/playlist/folder - - Returns: - - The converted name - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 10538 rename_imported_container') - - count = 1 - while True: - - count += 1 - new_name = name + ' (' + str(count) + ')' - - if not new_name in self.media_name_dict: - return new_name - - - # (Interact with media data objects) - - - def watch_video_in_player(self, video_obj): - - """Can be called by anything. - - Watch a video using the system's default media player, first checking - that a file actually exists. - - Args: - - video_obj (media.Video): The video to watch - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 10567 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!', - 'error', - 'ok', - ) - - else: - utils.open_file(path) - - - def download_watch_videos(self, video_list, watch_flag=True): - - """Can be called by anything. - - Download the specified videos and, when they have been downloaded, - launch them in the system's default media player. - - Args: - - video_list (list): List of media.Video objects to download and - watch - - watch_flag (bool): If False, the video(s) are not launched in the - system's default media player after being downloaded - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 10602 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, - 'Download and watch video request failed sanity check', - ) - - # Add the video to the list of videos to be launched in the system's - # default media player, the next time a download operation finishes - if watch_flag: - for video_obj in video_list: - self.watch_after_dl_list.append(video_obj) - - if self.download_manager_obj: - - # Download operation already in progress. Add these videos to its - # list - for video_obj in video_list: - 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, - ) - - else: - - # Start a new download operation to download this video - self.download_manager_start('real', False, video_list) - - - # (Options manager objects) - - - def clone_general_options_manager(self, data_list): - - """Called by config.OptionsEditWin.on_clone_options_clicked(). - - (Not called by self.apply_download_options(), which can handle its own - cloning). - - Clones youtube-dl download options from the General Options manager - into the specified download options manager. - - Args: - - data_list (list): List of values supplied by the dialogue window. - The first is the edit window for the download options object - (which must be reset). The second value is the download options - manager object, into which new options will be cloned. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 10666 clone_general_options_manager') - - edit_win_obj = data_list.pop(0) - options_obj = data_list.pop(0) - - # Clone values from the general download options manager - options_obj.clone_options(self.general_options_obj) - # Reset the edit window to display the new (cloned) values - edit_win_obj.reset_with_new_edit_obj(options_obj) - - - def reset_options_manager(self, data_list): - - """Called by config.OptionsEditWin.on_reset_options_clicked(). - - Resets the specified download options manager object, setting its - options to their default values. - - Args: - - data_list (list): List of values supplied by the dialogue window. - The first is the edit window for the download options object - (which must be reset). The second optional value is the media - data object to which the download options object belongs. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 10694 reset_options_manager') - - edit_win_obj = data_list.pop(0) - - # Replace the old object with a new one, which has the effect of - # resetting its download options to the default values - options_obj = options.OptionsManager() - - if data_list: - - # The Download Options object belongs to the specified media data - # object - media_data_obj = data_list.pop(0) - media_data_obj.set_options_obj(options_obj) - - else: - - # The General Download Options object - self.general_options_obj = options_obj - - # Reset the edit window to display the new (default) values - edit_win_obj.reset_with_new_edit_obj(options_obj) - - - # Callback class methods - - - # (Timers) - - - 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. - - Returns: - - 1 to keep the timer going, or None to halt it - - """ - - if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10738 script_slow_timer_callback') - - if not self.disable_load_save_flag \ - and not self.current_manager_obj \ - and not self.main_win_obj.config_win_list: - - if self.scheduled_dl_mode == 'scheduled': - - 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 - ) - - 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 - - - def script_fast_timer_callback(self): - - """Called by GObject timer created by self.start(). - - Once a second, check whether there are any mainwin.Catalogue objects to - add to the Video Catalogue and, if so, add them. - - Returns: - - 1 to keep the timer going, or None to halt it - - """ - - if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10780 script_fast_timer_callback') - - self.main_win_obj.video_catalogue_retry_insert_items() - - # Return 1 to keep the timer going - return 1 - - - 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. - - 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. - - During download operations, youtube-dl output is temporarily stored - (because Gtk widgets cannot be updated from within a thread). This - function calls mainwin.MainWin.output_tab_update_pages() to display - that output in the Output Tab. - - If required, this function periodically checks whether the device - containing self.data_dir is running out of space (and halts the - operation, if so.) - - Returns: - - 1 to keep the timer going, or None to halt it - - """ - - if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10816 dl_timer_callback') - - # Periodically check (if required) whether the device is running out of - # disk space - if self.dl_timer_disk_space_check_time is None: - # First check occurs 60 seconds after the operation begins - self.dl_timer_disk_space_check_time \ - = time.time() + self.dl_timer_disk_space_time - - elif self.dl_timer_disk_space_check_time < time.time(): - - self.dl_timer_disk_space_check_time \ - = time.time() + self.dl_timer_disk_space_time - - disk_space = utils.disk_get_free_space(self.data_dir) - - if ( - self.disk_space_stop_flag \ - and self.disk_space_stop_limit != 0 \ - and disk_space <= self.disk_space_stop_limit - ) or disk_space < self.disk_space_abs_limit: - - # Stop the download operation - self.system_error( - 145, - 'Download operation halted because the device is running' \ - + ' out of space', - ) - - self.download_manager_obj.stop_download_operation() - # Return 1 to keep the timer going, which allows the operation - # to finish naturally - 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() - - # Download operation still in progress, return 1 to keep the timer - # going - return 1 - - 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: - self.main_win_obj.progress_list_check_hide_rows() - - if 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 - - # The download operation has finished. The call to - # self.download_manager_finished() destroys the timer - self.download_manager_finished() - - - def update_timer_callback(self): - - """Called by GObject timer created by self.update_manager_start(). - - During an update operation, a GObject timer runs, so that the Output - Tab can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the update operation. - - During update operations, messages generated by updates.UpdateManager - are temporarily stored (because Gtk widgets cannot be updated from - within a thread). This function calls - mainwin.MainWin.output_tab_update_pages() to display those messages in - the Output Tab. - - Returns: - - 1 to keep the timer going - - """ - - if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10902 update_timer_callback') - - if self.update_timer_check_time is None: - - self.main_win_obj.output_tab_update_pages() - # Update operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.update_timer_check_time > time.time(): - - self.main_win_obj.output_tab_update_pages() - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The update operation has finished. The call to - # self.update_manager_finished() destroys the timer - self.update_manager_finished() - - - def refresh_timer_callback(self): - - """Called by GObject timer created by self.refresh_manager_continue(). - - During a refresh operation, a GObject timer runs, so that the Output - Tab can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the refresh operation. - - During refresh operations, messages generated by refresh.RefreshManager - are temporarily stored (because Gtk widgets cannot be updated from - within a thread). This function calls - mainwin.MainWin.output_tab_update_pages() to display those messages in - the Output Tab. - - Returns: - - 1 to keep the timer going - - """ - - if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10946 refresh_timer_callback') - - if self.refresh_timer_check_time is None: - - self.main_win_obj.output_tab_update_pages() - # Refresh operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.refresh_timer_check_time > time.time(): - - self.main_win_obj.output_tab_update_pages() - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The refresh operation has finished. The call to - # self.refresh_manager_finished() destroys the timer - self.refresh_manager_finished() - - - def info_timer_callback(self): - - """Called by GObject timer created by self.info_manager_start(). - - During an info operation, a GObject timer runs, so that the Output - Tab can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the info operation. - - During info operations, messages generated by info.InfoManager - are temporarily stored (because Gtk widgets cannot be updated from - within a thread). This function calls - mainwin.MainWin.output_tab_update_pages() to display those messages in - the Output Tab. - - Returns: - - 1 to keep the timer going - - """ - - if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 10990 info_timer_callback') - - if self.info_timer_check_time is None: - - self.main_win_obj.output_tab_update_pages() - # Info operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.info_timer_check_time > time.time(): - - self.main_win_obj.output_tab_update_pages() - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The info operation has finished. The call to - # self.info_manager_finished() destroys the timer - self.info_manager_finished() - - - def tidy_timer_callback(self): - - """Called by GObject timer created by self.tidy_manager_start(). - - During a tidy operation, a GObject timer runs, so that the Output - Tab can be updated at regular intervals. - - For the benefit of systems with Gtk < 3.24, the timer continues running - for a few seconds at the end of the tidy operation. - - During tidy operations, messages generated by tidy.TidyManager - are temporarily stored (because Gtk widgets cannot be updated from - within a thread). This function calls - mainwin.MainWin.output_tab_update_pages() to display those messages in - the Output Tab. - - Returns: - - 1 to keep the timer going - - """ - - if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('app 11034 tidy_timer_callback') - - if self.tidy_timer_check_time is None: - - self.main_win_obj.output_tab_update_pages() - # Tidy operation still in progress, return 1 to keep the timer - # going - return 1 - - elif self.tidy_timer_check_time > time.time(): - - self.main_win_obj.output_tab_update_pages() - # Cooldown time not yet finished, return 1 to keep the timer going - return 1 - - else: - # The tidy operation has finished. The call to - # self.tidy_manager_finished() destroys the timer - self.tidy_manager_finished() - - - # (Menu item and toolbar button callbacks) - - - def on_button_apply_filter(self, action, par): - - """Called from a callback in self.do_startup(). - - Applies a filter to the Video Catalogue, hiding any videos which don't - match the search text specified by the user. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11074 on_button_apply_filter') - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 146, - 'Apply filter request failed sanity check', - ) - - # Apply the filter - self.main_win_obj.video_catalogue_apply_filter() - - - 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 11102 on_button_cancel_filter') - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 147, - 'Cancel filter request failed sanity check', - ) - - # Cancel the filter - self.main_win_obj.video_catalogue_cancel_filter() - - - def on_button_find_date(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the first one containing a video - whose upload time is the first one on or after date specified by the - user. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11132 on_button_find_date') - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 148, - 'Find videos by date request failed sanity check', - ) - - # Prompt the user for a new calendar date - dialogue_win = mainwin.CalendarDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - if response == Gtk.ResponseType.OK: - date_tuple = dialogue_win.calendar.get_date() - - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK and date_tuple: - - year = date_tuple[0] # e.g. 2011 - month = date_tuple[1] + 1 # Values in range 0-11 - day = date_tuple[2] # Values in range 1-31 - - # Convert the specified date into the epoch time at the start of - # that day - epoch_time = datetime.datetime(year, month, day, 0, 0).timestamp() - - # Get the channel, playlist or folder currently visible in the - # Video Catalogue - dbid = self.media_name_dict[self.main_win_obj.video_index_current] - container_obj = self.media_reg_dict[dbid] - - count = 0 - for child_obj in container_obj.child_list: - - if isinstance(child_obj, media.Video) \ - and child_obj.upload_time is not None \ - and child_obj.upload_time < epoch_time: - break - - else: - count += 1 - - # Find the corresponding page in the Video Catalogue... - page_num = math.ceil(count / self.catalogue_page_size) - # ...and make it visible - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - page_num, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_button_first_page(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the first one. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11203 on_button_first_page') - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - 1, - ) - - - def on_button_last_page(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the last one. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11226 on_button_last_page') - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - self.main_win_obj.catalogue_toolbar_last_page, - ) - - - def on_button_next_page(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the next one. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11249 on_button_next_page') - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - self.main_win_obj.catalogue_toolbar_current_page + 1, - ) - - - def on_button_previous_page(self, action, par): - - """Called from a callback in self.do_startup(). - - Changes the Video Catalogue page to the previous one. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11272 on_button_previous_page') - - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - self.main_win_obj.catalogue_toolbar_current_page - 1, - ) - - - def on_button_scroll_down(self, action, par): - - """Called from a callback in self.do_startup(). - - Scrolls the Video Catalogue page to the bottom. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11295 on_button_scroll_down') - - adjust = self.main_win_obj.catalogue_scrolled.get_vadjustment() - adjust.set_value(adjust.get_upper()) - - - def on_button_scroll_up(self, action, par): - - """Called from a callback in self.do_startup(). - - Scrolls the Video Catalogue page to the top. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11316 on_button_scroll_up') - - self.main_win_obj.catalogue_scrolled.get_vadjustment().set_value(0) - - - def on_button_show_filter(self, action, par): - - """Called from a callback in self.do_startup(). - - Reveals or hides another toolbar just below the Video Catalogue. The - additional toolbar contains filter options. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11337 on_button_show_filter') - - if not self.catalogue_show_filter_flag: - self.catalogue_show_filter_flag = True - else: - self.catalogue_show_filter_flag = False - - # Update the button in the Video Catalogue's toolbar - self.main_win_obj.update_show_filter_widgets() - - - def on_button_sort_type(self, action, par): - - """Called from a callback in self.do_startup(). - - Sets the type of sorting applied to the Video Catalogue: alphabetically - or by date. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11364 on_button_sort_type') - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 149, - 'Change catalogue sort type request failed sanity check', - ) - - # Toggle the flag, and update the icon on the toolbutton - if not self.catalogue_alpha_sort_flag: - self.catalogue_alpha_sort_flag = True - else: - self.catalogue_alpha_sort_flag = False - - self.main_win_obj.update_alpha_sort_widgets() - - # Redraw the Video Catalogue, switching to the first page - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - 1, - True, # Reset scrollbars - True, # Don't cancel the filter, if applied - ) - - - def on_button_stop_operation(self, action, par): - - """Called from a callback in self.do_startup(). - - Stops the current download/update/refresh operation. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11405 on_button_stop_operation') - - self.operation_halted_flag = True - - if self.download_manager_obj: - self.download_manager_obj.stop_download_operation() - elif self.update_manager_obj: - self.update_manager_obj.stop_update_operation() - elif self.refresh_manager_obj: - self.refresh_manager_obj.stop_refresh_operation() - elif self.info_manager_obj: - self.info_manager_obj.stop_info_operation() - elif self.tidy_manager_obj: - self.tidy_manager_obj.stop_tidy_operation() - - - def on_button_switch_view(self, action, par): - - """Called from a callback in self.do_startup(). - - Toggles between simple and complex views in the Video Catalogue, and - between showing the names of each video's parent channel/playlist/ - folder - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11438 on_button_switch_view') - - # There are four modes in a fixed sequence; switch to the next mode in - # the sequence - if self.catalogue_mode == 'simple_hide_parent': - self.catalogue_mode = 'simple_show_parent' - elif self.catalogue_mode == 'simple_show_parent': - self.catalogue_mode = 'complex_hide_parent' - elif self.catalogue_mode == 'complex_hide_parent': - self.catalogue_mode = 'complex_hide_parent_ext' - elif self.catalogue_mode == 'complex_hide_parent_ext': - self.catalogue_mode = 'complex_show_parent' - elif self.catalogue_mode == 'complex_show_parent': - self.catalogue_mode = 'complex_show_parent_ext' - else: - self.catalogue_mode = 'simple_hide_parent' - - # Redraw the Video Catalogue, but only if something was already drawn - # there (and keep the current page number) - if self.main_win_obj.video_index_current is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - self.main_win_obj.catalogue_toolbar_current_page, - ) - - - def on_button_use_regex(self, action, par): - - """Called from a callback in self.do_startup(). - - When the user clicks the Regex togglebutton in the toolbar just below - the Video Catalogue, updates IVs. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11480 on_button_use_regex') - - # Sanity check - if not self.main_win_obj.video_catalogue_dict: - return self.system_error( - 150, - 'Use regex request failed sanity check', - ) - - if not self.main_win_obj.catalogue_regex_togglebutton.get_active(): - self.catologue_use_regex_flag = False - else: - self.catologue_use_regex_flag = True - - - def on_menu_about(self, action, par): - - """Called from a callback in self.do_startup(). - - Show a standard 'about' dialogue window. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11510 on_menu_about') - - dialogue_win = Gtk.AboutDialog() - dialogue_win.set_transient_for(self.main_win_obj) - dialogue_win.set_destroy_with_parent(True) - - dialogue_win.set_program_name(__main__.__packagename__.title()) - dialogue_win.set_version('v' + __main__.__version__) - dialogue_win.set_copyright(__main__.__copyright__) - dialogue_win.set_license(__main__.__license__) - dialogue_win.set_website(__main__.__website__) - dialogue_win.set_website_label( - __main__.__packagename__.title() + ' website' - ) - dialogue_win.set_comments(__main__.__description__) - dialogue_win.set_logo( - self.main_win_obj.pixbuf_dict['system_icon'], - ) - dialogue_win.set_authors(__main__.__author_list__) - dialogue_win.set_title('') - dialogue_win.connect('response', self.on_menu_about_close) - - dialogue_win.show() - - - def on_menu_about_close(self, action, par): - - """Called from a callback in self.do_startup(). - - Close the 'about' dialogue window. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11550 on_menu_about_close') - - action.destroy() - - - def on_menu_add_channel(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify a new channel. - If the user specifies a channel, creates a media.Channel object. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11571 on_menu_add_channel') - - keep_open_flag = True - dl_sim_flag = False - monitor_flag = False - - # If a folder (but not a channel/playlist) is selected in the Video - # Index, use that as the dialogue window's suggested parent folder - suggest_parent_name = None - if self.main_win_obj.video_index_current: - dbid = self.media_name_dict[self.main_win_obj.video_index_current] - container_obj = self.media_reg_dict[dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.fixed_flag \ - and not container_obj.restrict_flag: - suggest_parent_name = container_obj.name - - while keep_open_flag: - - dialogue_win = mainwin.AddChannelDialogue( - self.main_win_obj, - suggest_parent_name, - dl_sim_flag, - monitor_flag, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - name = dialogue_win.entry.get_text() - source = dialogue_win.entry2.get_text() - dl_sim_flag = dialogue_win.radiobutton2.get_active() - monitor_flag = dialogue_win.checkbutton.get_active() - - # ...and find the name of the parent media data object (a - # media.Folder), if one was specified... - parent_name = None - if hasattr(dialogue_win, 'parent_name'): - parent_name = dialogue_win.parent_name - elif suggest_parent_name is not None: - parent_name = suggest_parent_name - - # ...and halt the timer, if running - if dialogue_win.clipboard_timer_id: - GObject.source_remove(dialogue_win.clipboard_timer_id) - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - - keep_open_flag = False - - else: - - if name is None or re.match('\s*$', name): - - keep_open_flag = False - self.dialogue_manager_obj.show_msg_dialogue( - 'You must give the channel a name', - 'error', - 'ok', - ) - - elif not self.check_container_name_is_legal(name): - - keep_open_flag = False - self.dialogue_manager_obj.show_msg_dialogue( - 'The name \'' + name + '\' is not allowed', - 'error', - 'ok', - ) - - elif not source or not utils.check_url(source): - - keep_open_flag = False - self.dialogue_manager_obj.show_msg_dialogue( - 'You must enter a valid URL', - 'error', - 'ok', - ) - - elif name in self.media_name_dict: - - # Another channel, playlist or folder is already using this - # name - keep_open_flag = False - self.reject_container_name(name) - - else: - - keep_open_flag = self.dialogue_keep_open_flag - - # Remove leading/trailing whitespace from the name; make - # sure the name is not excessively long - name = utils.tidy_up_container_name( - name, - self.container_name_max_len, - ) - - # Find the parent media data object (a media.Folder), if - # specified - parent_obj = None - if parent_name and parent_name in self.media_name_dict: - dbid = self.media_name_dict[parent_name] - parent_obj = self.media_reg_dict[dbid] - - if self.dialogue_keep_open_flag \ - and self.dialogue_keep_container_flag: - suggest_parent_name = parent_name - - # Create the new channel - channel_obj = self.add_channel( - name, - parent_obj, - source, - dl_sim_flag, - ) - - # Add the channel to Video Index - if channel_obj: - - if suggest_parent_name is not None \ - and suggest_parent_name \ - == self.main_win_obj.video_index_current: - # The channel has been added to the currently - # selected folder; the True argument tells the - # function not to select the channel - self.main_win_obj.video_index_add_row( - channel_obj, - True, - ) - - else: - # Do select the new channel - self.main_win_obj.video_index_add_row(channel_obj) - - - def on_menu_add_folder(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify a new folder. - If the user specifies a folder, creates a media.Folder object. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11725 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 - suggest_parent_name = None - if self.main_win_obj.video_index_current: - dbid = self.media_name_dict[self.main_win_obj.video_index_current] - container_obj = self.media_reg_dict[dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.fixed_flag \ - and not container_obj.restrict_flag: - suggest_parent_name = container_obj.name - - dialogue_win = mainwin.AddFolderDialogue( - self.main_win_obj, - suggest_parent_name, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - name = dialogue_win.entry.get_text() - dl_sim_flag = dialogue_win.radiobutton2.get_active() - - # ...and find the name of the parent media data object (a - # media.Folder), if one was specified... - parent_name = None - if hasattr(dialogue_win, 'parent_name'): - parent_name = dialogue_win.parent_name - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - if name is None or re.match('\s*$', name): - - self.dialogue_manager_obj.show_msg_dialogue( - 'You must give the folder a name', - 'error', - 'ok', - ) - - elif not self.check_container_name_is_legal(name): - - self.dialogue_manager_obj.show_msg_dialogue( - 'The name \'' + name + '\' is not allowed', - 'error', - 'ok', - ) - - elif name in self.media_name_dict: - - # Another channel, playlist or folder is already using this - # name - self.reject_container_name(name) - - else: - - # Remove leading/trailing whitespace from the name; make sure - # the name is not excessively long - name = utils.tidy_up_container_name( - name, - self.container_name_max_len, - ) - - # Find the parent media data object (a media.Folder), if - # specified - parent_obj = None - if parent_name and parent_name in self.media_name_dict: - dbid = self.media_name_dict[parent_name] - parent_obj = self.media_reg_dict[dbid] - - # Create the new folder - folder_obj = self.add_folder(name, parent_obj, dl_sim_flag) - - # Add the folder to the Video Index - if folder_obj: - - if self.main_win_obj.video_index_current: - # The new folder has been added inside the currently - # selected folder; the True argument tells the - # function not to select the new folder - self.main_win_obj.video_index_add_row( - folder_obj, - True, - ) - - else: - # Do select the new folder - self.main_win_obj.video_index_add_row(folder_obj) - - - def on_menu_add_playlist(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify a new playlist. - If the user specifies a playlist, creates a media.PLaylist object. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11834 on_menu_add_playlist') - - keep_open_flag = True - dl_sim_flag = False - monitor_flag = False - - # If a folder (but not a channel/playlist) is selected in the Video - # Index, use that as the dialogue window's suggested parent folder - suggest_parent_name = None - if self.main_win_obj.video_index_current: - dbid = self.media_name_dict[self.main_win_obj.video_index_current] - container_obj = self.media_reg_dict[dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.fixed_flag \ - and not container_obj.restrict_flag: - suggest_parent_name = container_obj.name - - while keep_open_flag: - - dialogue_win = mainwin.AddPlaylistDialogue( - self.main_win_obj, - suggest_parent_name, - dl_sim_flag, - monitor_flag, - ) - - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - name = dialogue_win.entry.get_text() - source = dialogue_win.entry2.get_text() - dl_sim_flag = dialogue_win.radiobutton2.get_active() - monitor_flag = dialogue_win.checkbutton.get_active() - - # ...and find the name of the parent media data object (a - # media.Folder), if one was specified... - parent_name = None - if hasattr(dialogue_win, 'parent_name'): - parent_name = dialogue_win.parent_name - elif suggest_parent_name is not None: - parent_name = suggest_parent_name - - # ...and halt the timer, if running - if dialogue_win.clipboard_timer_id: - GObject.source_remove(dialogue_win.clipboard_timer_id) - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response != Gtk.ResponseType.OK: - - keep_open_flag = False - - else: - - if name is None or re.match('\s*$', name): - - keep_open_flag = False - self.dialogue_manager_obj.show_msg_dialogue( - 'You must give the playlist a name', - 'error', - 'ok', - ) - - elif not self.check_container_name_is_legal(name): - - keep_open_flag = False - self.dialogue_manager_obj.show_msg_dialogue( - 'The name \'' + name + '\' is not allowed', - 'error', - 'ok', - ) - - elif not source or not utils.check_url(source): - - keep_open_flag = False - self.dialogue_manager_obj.show_msg_dialogue( - 'You must enter a valid URL', - 'error', - 'ok', - ) - - elif name in self.media_name_dict: - - # Another channel, playlist or folder is already using this - # name - keep_open_flag = False - self.reject_container_name(name) - - else: - - keep_open_flag = self.dialogue_keep_open_flag - - # Remove leading/trailing whitespace from the name; make - # sure the name is not excessively long - name = utils.tidy_up_container_name( - name, - self.container_name_max_len, - ) - - # Find the parent media data object (a media.Folder), if - # specified - parent_obj = None - if parent_name and parent_name in self.media_name_dict: - dbid = self.media_name_dict[parent_name] - parent_obj = self.media_reg_dict[dbid] - - if self.dialogue_keep_open_flag \ - and self.dialogue_keep_container_flag: - suggest_parent_name = parent_name - - # Create the playlist - playlist_obj = self.add_playlist( - name, - parent_obj, - source, - dl_sim_flag, - ) - - # Add the playlist to the Video Index - if playlist_obj: - - if suggest_parent_name is not None \ - and suggest_parent_name \ - == self.main_win_obj.video_index_current: - # The playlist has been added to the currently - # selected folder; the True argument tells the - # function not to select the playlist - self.main_win_obj.video_index_add_row( - playlist_obj, - True, - ) - - else: - # Do select the new playlist - self.main_win_obj.video_index_add_row(playlist_obj) - - - def on_menu_add_video(self, action, par): - - """Called from a callback in self.do_startup(). - - Creates a dialogue window to allow the user to specify one or more - videos. If the user supplies some URLs, creates media.Video objects. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 11988 on_menu_add_video') - - dialogue_win = mainwin.AddVideoDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - text = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - False, - ) - - dl_sim_flag = dialogue_win.radiobutton2.get_active() - - # ...and find the parent media data object (a media.Channel, - # media.Playlist or media.Folder)... - parent_name = self.fixed_misc_folder.name - if hasattr(dialogue_win, 'parent_name'): - parent_name = dialogue_win.parent_name - - dbid = self.media_name_dict[parent_name] - parent_obj = self.media_reg_dict[dbid] - - # ...and halt the timer, if running - if dialogue_win.clipboard_timer_id: - GObject.source_remove(dialogue_win.clipboard_timer_id) - - # ...before destroying the dialogue window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # Split text into a list of lines and filter out invalid URLs - video_list = [] - duplicate_list = [] - for line in text.split('\n'): - - # Remove leading/trailing whitespace - line = utils.strip_whitespace(line) - - # Perform checks on the URL. If it passes, remove leading/ - # trailing whitespace - if utils.check_url(line): - video_list.append(utils.strip_whitespace(line)) - - # Check everything in the list against other media.Video objects - # with the same parent folder - for line in video_list: - if parent_obj.check_duplicate_video(line): - duplicate_list.append(line) - else: - self.add_video(parent_obj, line, dl_sim_flag) - - # In the Video Index, select the parent media data object, which - # updates both the Video Index and the Video Catalogue - self.main_win_obj.video_index_select_row(parent_obj) - - # If any duplicates were found, inform the user - if duplicate_list: - - msg = 'The following videos are duplicates:\n\n' - for line in duplicate_list: - msg += '\n\n' + line - - self.dialogue_manager_obj.show_msg_dialogue( - msg, - 'warning', - 'ok', - ) - - - def on_menu_change_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens the preference window at the right tab, so that the user can - switch databases. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12075 on_menu_change_db') - - config.SystemPrefWin(self, True) - - - def on_menu_check_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new download operation (if allowed). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12095 on_menu_check_all') - - self.download_manager_start('sim') - - - def on_menu_close_tray(self, action, par): - - """Called from a callback in self.do_startup(). - - Closes the main window to the system tray. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12115 on_menu_close_tray') - - self.main_win_obj.toggle_visibility() - - - def on_menu_custom_dl_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new (custom) download operation (if - allowed). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12136 on_menu_custom_dl_all') - - self.download_manager_start('custom') - - - def on_menu_download_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Call a function to start a new download operation (if allowed). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12156 on_menu_download_all') - - self.download_manager_start('real') - - - def on_menu_export_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Exports data from the Tartube database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12176 on_menu_export_db') - - self.export_from_db( [] ) - - - def on_menu_general_options(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens an edit window for the General Options Manager. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12196 on_menu_general_options') - - config.OptionsEditWin(self, self.general_options_obj, None) - - - def on_menu_go_website(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens the Tartube website. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12216 on_menu_go_website') - - utils.open_file(__main__.__website__) - - - def on_menu_import_json(self, action, par): - - """Called from a callback in self.do_startup(). - - Imports data into from a JSON export file into the Tartube database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12236 on_menu_import_json') - - self.import_into_db(True) - - - def on_menu_import_plain_text(self, action, par): - - """Called from a callback in self.do_startup(). - - Imports data into from a plain text export file into the Tartube - database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12257 on_menu_import_plain_text') - - self.import_into_db(False) - - - def on_menu_install_ffmpeg(self, action, par): - - """Called from a callback in self.do_startup(). - - Start an update operation to install FFmpeg (on MS Windows only). - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12277 on_menu_install_ffmpeg') - - self.update_manager_start('ffmpeg') - - - def on_menu_refresh_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Starts a refresh operation. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12297 on_menu_refresh_db') - - self.refresh_manager_start() - - - def on_menu_save_all(self, action, par): - - """Called from a callback in self.do_startup(). - - Save the config file, and then the Tartube database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12317 on_menu_save_all') - - if not self.disable_load_save_flag: - self.save_config() - if not self.disable_load_save_flag: - self.save_db() - - # Show a dialogue window for confirmation (unless file load/save has - # been disabled, in which case a dialogue has already appeared) - if not self.disable_load_save_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - 'Data saved', - 'info', - 'ok', - ) - - - def on_menu_save_db(self, action, par): - - """Called from a callback in self.do_startup(). - - Save the Tartube database. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12350 on_menu_save_db') - - self.save_db() - - # Show a dialogue window for confirmation (unless file load/save has - # been disabled, in which case a dialogue has already appeared) - if not self.disable_load_save_flag: - - self.dialogue_manager_obj.show_msg_dialogue( - 'Database saved', - 'info', - 'ok', - ) - - - def on_menu_show_hidden(self, action, par): - - """Called from a callback in self.do_startup(). - - Un-hides all hidden media.Folder objects (and their children) - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12380 on_menu_show_hidden') - - for name in self.media_name_dict: - - dbid = self.media_name_dict[name] - media_data_obj = self.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - self.mark_folder_hidden(media_data_obj, False) - - - def on_menu_system_preferences(self, action, par): - - """Called from a callback in self.do_startup(). - - Opens a preference window to edit system preferences. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12407 on_menu_system_preferences') - - config.SystemPrefWin(self) - - - def on_menu_test(self, action, par): - - """Called from a callback in self.do_startup(). - - Add a set of media data objects for testing. This function can only be - called if the debugging flags are set. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12428 on_menu_test') - - # Add media data objects for testing: videos, channels, playlists and/ - # or folders - testing.add_test_media(self) - - # Clicking the Test button more than once just adds illegal duplicate - # channels/playlists/folders (and non-illegal duplicate videos), so - # just disable the button and the menu item - self.main_win_obj.desensitise_test_widgets() - - # Redraw the video catalogue, if a Video Index row is selected - if self.main_win_obj.video_index_current is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - ) - - - def on_menu_test_ytdl(self, action, par): - - """Called from a callback in self.do_startup(). - - Start an info operation to test a youtube-dl command. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12461 on_menu_test_ytdl') - - # Prompt the user for what should be tested - dialogue_win = mainwin.TestCmdDialogue(self.main_win_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - url_string = dialogue_win.entry.get_text() - options_string = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - False, - ) - - # ...before destroying it - dialogue_win.destroy() - - # If the user specified either (or both) a URL and youtube-dl options, - # then we can proceed - if response == Gtk.ResponseType.OK \ - and (url_string != '' or options_string != ''): - - # Start the info operation, which issues the youtube-dl command - # with the specified options - self.info_manager_start( - 'test_ytdl', - None, # No media.Video object in this case - url_string, # Use the source, if specified - options_string, # Use download options, if specified - ) - - - def on_menu_tidy_up(self, action, par): - - """Called from a callback in self.do_startup(). - - Start a tidy operation to tidy up Tartube's data directory. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12508 on_menu_tidy_up') - - # Prompt the user to specify which actions should be applied to - # Tartube's data directory - dialogue_win = mainwin.TidyDialogue(self.main_win_obj) - response = dialogue_win.run() - - if response == Gtk.ResponseType.OK: - - # Retrieve user choices from the dialogue window - choices_dict = { - 'media_data_obj': None, - 'corrupt_flag': dialogue_win.checkbutton.get_active(), - 'del_corrupt_flag': dialogue_win.checkbutton2.get_active(), - 'exist_flag': dialogue_win.checkbutton3.get_active(), - 'del_video_flag': dialogue_win.checkbutton4.get_active(), - 'del_others_flag': dialogue_win.checkbutton5.get_active(), - 'del_descrip_flag': dialogue_win.checkbutton6.get_active(), - 'del_json_flag': dialogue_win.checkbutton7.get_active(), - 'del_xml_flag': dialogue_win.checkbutton8.get_active(), - 'del_thumb_flag': dialogue_win.checkbutton9.get_active(), - 'del_archive_flag': dialogue_win.checkbutton10.get_active(), - } - - # Now destroy the window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # If nothing was selected, then there is nothing to do - # (Don't need to check 'del_others_flag' here) - if not choices_dict['corrupt_flag'] \ - and not choices_dict['exist_flag'] \ - and not choices_dict['del_video_flag'] \ - and not choices_dict['del_descrip_flag'] \ - and not choices_dict['del_json_flag'] \ - and not choices_dict['del_xml_flag'] \ - and not choices_dict['del_thumb_flag'] \ - and not choices_dict['del_archive_flag']: - return - - # Prompt the user for confirmation, before deleting any files - if choices_dict['del_corrupt_flag'] \ - or choices_dict['del_video_flag'] \ - or choices_dict['del_descrip_flag'] \ - or choices_dict['del_json_flag'] \ - or choices_dict['del_xml_flag'] \ - or choices_dict['del_thumb_flag'] \ - 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 - { - 'yes': 'tidy_manager_start', - # Specified options - 'data': choices_dict, - }, - ) - - else: - - # Start the tidy operation now - self.tidy_manager_start(choices_dict) - - - def on_menu_update_ytdl(self, action, par): - - """Called from a callback in self.do_startup(). - - Start an update operation to update the system's youtube-dl. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12592 on_menu_update_ytdl') - - self.update_manager_start('ytdl') - - - def on_menu_quit(self, action, par): - - """Called from a callback in self.do_startup(). - - Terminates the Tartube app. - - Args: - - action (Gio.SimpleAction): Object generated by Gio - - par (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12612 on_menu_quit') - - self.stop() - - - # (Callback support functions) - - - def reject_container_name(self, name): - - """Called by self.on_menu_add_channel(), .on_menu_add_playlist() - and .on_menu_add_folder(). - - If the user specifies a name for a channel, playlist or folder that's - already in use by a channel, playlist or folder, tell them why they - can't use it. - - Args: - - name (str): The name specified by the user - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12636 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() - - self.dialogue_manager_obj.show_msg_dialogue( - 'There is already a ' + media_type + ' with that name ' \ - + '(so please choose a different name)', - 'error', - 'ok', - ) - - - # Set accessors - - - def set_allow_ytdl_archive_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12657 set_allow_ytdl_archive_flag') - - if not flag: - self.allow_ytdl_archive_flag = False - else: - self.allow_ytdl_archive_flag = True - - - def set_apply_json_timeout_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12668 set_apply_json_timeout_flag') - - if not flag: - self.apply_json_timeout_flag = False - else: - self.apply_json_timeout_flag = True - - - def set_auto_clone_options_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12679 set_auto_clone_options_flag') - - if not flag: - self.auto_clone_options_flag = False - else: - self.auto_clone_options_flag = True - - - def set_auto_delete_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12690 set_auto_delete_flag') - - if not flag: - self.auto_delete_flag = False - else: - self.auto_delete_flag = True - - - def set_auto_delete_days(self, days): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12701 set_auto_delete_days') - - self.auto_delete_days = days - - - def set_auto_delete_watched_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12709 set_auto_delete_watched_flag') - - if not flag: - self.auto_delete_watched_flag = False - else: - self.auto_delete_watched_flag = True - - - def set_auto_expand_video_index_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12720 set_auto_expand_video_index_flag') - - if not flag: - self.auto_expand_video_index_flag = False - else: - self.auto_expand_video_index_flag = True - - - def set_autostop_size_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12731 set_autostop_size_flag') - - if not flag: - self.autostop_size_flag = False - else: - self.autostop_size_flag = True - - - def set_autostop_size_unit(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12742 set_autostop_size_unit') - - self.autostop_size_unit = value - - - def set_autostop_size_value(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12750 set_autostop_size_value') - - self.autostop_size_value = value - - - def set_autostop_time_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12758 set_autostop_time_flag') - - if not flag: - self.autostop_time_flag = False - else: - self.autostop_time_flag = True - - - def set_autostop_time_unit(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12769 set_autostop_time_unit') - - self.autostop_time_unit = value - - - def set_autostop_time_value(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12777 set_autostop_time_value') - - self.autostop_time_value = value - - - def set_autostop_videos_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12785 set_autostop_videos_flag') - - if not flag: - self.autostop_videos_flag = False - else: - self.autostop_videos_flag = True - - - def set_autostop_videos_value(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12796 set_autostop_videos_value') - - self.autostop_videos_value = value - - - def set_bandwidth_apply_flag(self, flag): - - """Called by mainwin.MainWin.on_bandwidth_checkbutton_changed(). - - Applies or releases the bandwidth limit. If a download operation is in - progress, the new setting is applied to the next download job. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12810 set_bandwidth_apply_flag') - - if not flag: - self.bandwidth_apply_flag = False - else: - self.bandwidth_apply_flag = True - - - def set_bandwidth_default(self, value): - - """Called by mainwin.MainWin.on_bandwidth_spinbutton_changed(). - - Sets the new bandwidth limit. If a download operation is in progress, - the new value is applied to the next download job. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12827 set_bandwidth_default') - - if value < self.bandwidth_min or value > self.bandwidth_max: - return self.system_error( - 151, - 'Set bandwidth request failed sanity check', - ) - - self.bandwidth_default = value - - - def set_catalogue_page_size(self, size): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12841 set_catalogue_page_size') - - self.catalogue_page_size = size - - - def set_close_to_tray_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12849 set_close_to_tray_flag') - - if not flag: - self.close_to_tray_flag = False - else: - self.close_to_tray_flag = True - - - def set_complex_index_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12860 set_complex_index_flag') - - if not flag: - self.complex_index_flag = False - else: - self.complex_index_flag = True - - - def set_custom_dl_by_video_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12871 set_custom_dl_by_video_flag') - - if not flag: - self.custom_dl_by_video_flag = False - else: - self.custom_dl_by_video_flag = True - - - def set_custom_dl_delay_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12882 set_custom_dl_delay_flag') - - if not flag: - self.custom_dl_delay_flag = False - else: - self.custom_dl_delay_flag = True - - - def set_custom_dl_delay_max(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12893 set_custom_dl_delay_max') - - self.custom_dl_delay_max = value - - - def set_custom_dl_delay_min(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12901 set_custom_dl_delay_min') - - self.custom_dl_delay_min = value - - - def set_custom_dl_divert_mode(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12909 set_custom_dl_divert_mode') - - self.custom_dl_divert_mode = value - - - def set_data_dir(self, path): - - """Called by mainwin.MountDriveDialogue.on_button_clicked() only; - everything else should call self.switch_db(). - - The call to this function resets the value of self.data_dir without - actually loading the database. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12924 set_data_dir') - - self.data_dir = path - - - def reset_data_dir(self): - - """Called by mainwin.MountDriveDialogue.on_button_clicked() only; - everything else should call self.switch_db(). - - The call to this function resets the value of self.data_dir without - actually loading the database. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12939 reset_data_dir') - - self.data_dir = self.default_data_dir - - - 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') - - if not flag: - self.data_dir_add_from_list_flag = False - else: - self.data_dir_add_from_list_flag = True - - - def set_data_dir_use_first_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12958 set_data_dir_use_first_flag') - - if not flag: - self.data_dir_use_first_flag = False - else: - self.data_dir_use_first_flag = True - - - def set_data_dir_use_list_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12969 set_data_dir_use_list_flag') - - if not flag: - self.data_dir_use_list_flag = False - else: - self.data_dir_use_list_flag = True - - - def set_db_backup_mode(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12980 set_db_backup_mode') - - self.db_backup_mode = value - - - def set_delete_on_shutdown_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12988 set_delete_on_shutdown_flag') - - if not flag: - self.delete_on_shutdown_flag = False - else: - self.delete_on_shutdown_flag = True - - - def set_dialogue_copy_clipboard_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 12999 set_dialogue_copy_clipboard_flag') - - if not flag: - self.dialogue_copy_clipboard_flag = False - else: - self.dialogue_copy_clipboard_flag = True - - - def set_dialogue_keep_open_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13010 set_dialogue_keep_open_flag') - - if not flag: - self.dialogue_keep_open_flag = False - else: - self.dialogue_keep_open_flag = True - - - def set_disable_dl_all_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13021 set_disable_dl_all_flag') - - if not flag: - self.disable_dl_all_flag = False - self.main_win_obj.enable_dl_all_buttons() - - else: - self.disable_dl_all_flag = True - self.main_win_obj.disable_dl_all_buttons() - - - def set_disk_space_stop_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13035 set_disk_space_stop_flag') - - if not flag: - self.disk_space_stop_flag = False - else: - self.disk_space_stop_flag = True - - - def set_disk_space_stop_limit(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13046 set_disk_space_stop_limit') - - self.disk_space_stop_limit = value - - - def set_disk_space_warn_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13054 set_disk_space_warn_flag') - - if not flag: - self.disk_space_warn_flag = False - else: - self.disk_space_warn_flag = True - - - def set_disk_space_warn_limit(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13065 set_disk_space_warn_limit') - - self.disk_space_warn_limit = value - - - def set_ffmpeg_path(self, path): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13073 set_ffmpeg_path') - - self.ffmpeg_path = path - - - def set_gtk_emulate_broken_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13081 set_gtk_emulate_broken_flag') - - if not flag: - self.gtk_emulate_broken_flag = False - else: - self.gtk_emulate_broken_flag = True - - - def set_ignore_child_process_exit_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13092 set_ignore_child_process_exit_flag') - - if not flag: - self.ignore_child_process_exit_flag = False - else: - self.ignore_child_process_exit_flag = True - - - def set_ignore_custom_msg_list(self, custom_list): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13103 set_ignore_custom_msg_list') - - self.ignore_custom_msg_list = custom_list.copy() - - - def set_ignore_custom_regex_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13111 set_ignore_custom_regex_flag') - - if not flag: - self.ignore_custom_regex_flag = False - else: - self.ignore_custom_regex_flag = True - - - def set_ignore_data_block_error_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13122 set_ignore_data_block_error_flag') - - if not flag: - self.ignore_data_block_error_flag = False - else: - self.ignore_data_block_error_flag = True - - - def set_ignore_http_404_error_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13133 set_ignore_http_404_error_flag') - - if not flag: - self.ignore_http_404_error_flag = False - else: - self.ignore_http_404_error_flag = True - - - def set_ignore_merge_warning_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13144 set_ignore_merge_warning_flag') - - if not flag: - self.ignore_merge_warning_flag = False - else: - self.ignore_merge_warning_flag = True - - - def set_ignore_missing_format_error_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13155 set_ignore_missing_format_error_flag') - - if not flag: - self.ignore_missing_format_error_flag = False - else: - self.ignore_missing_format_error_flag = True - - - def set_ignore_no_annotations_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13166 set_ignore_no_annotations_flag') - - if not flag: - self.ignore_no_annotations_flag = False - else: - self.ignore_no_annotations_flag = True - - - def set_ignore_no_subtitles_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13177 set_ignore_no_subtitles_flag') - - if not flag: - self.ignore_no_subtitles_flag = False - else: - self.ignore_no_subtitles_flag = True - - - def set_ignore_yt_age_restrict_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13188 set_ignore_yt_age_restrict_flag') - - if not flag: - self.ignore_yt_age_restrict_flag = False - else: - self.ignore_yt_age_restrict_flag = True - - - def set_ignore_yt_copyright_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13199 set_ignore_yt_copyright_flag') - - if not flag: - self.ignore_yt_copyright_flag = False - else: - self.ignore_yt_copyright_flag = True - - - def set_ignore_yt_uploader_deleted_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13210 set_ignore_yt_uploader_deleted_flag') - - if not flag: - self.ignore_yt_uploader_deleted_flag = False - else: - self.ignore_yt_uploader_deleted_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') - - if not flag: - self.main_win_save_size_flag = False - self.main_win_save_width = self.main_win_width - self.main_win_save_height = self.main_win_height - self.main_win_save_posn = self.paned_min_size - - else: - self.main_win_save_size_flag = True - - - def set_match_first_chars(self, num_chars): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13236 set_match_first_chars') - - self.match_first_chars = num_chars - - - def set_match_ignore_chars(self, num_chars): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13244 set_match_ignore_chars') - - self.match_ignore_chars = num_chars - - - def set_match_method(self, method): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13253 set_match_method') - - self.match_method = method - - - def set_num_worker_apply_flag(self, flag): - - """Called by mainwin.MainWin.on_num_worker_checkbutton_changed(). - - Applies or releases the simultaneous download limit. If a download - operation is in progress, the new setting is applied to the next - download job. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13267 set_num_worker_apply_flag') - - if not flag: - self.bandwidth_apply_flag = False - else: - self.bandwidth_apply_flag = True - - - def set_num_worker_default(self, value): - - """Called by mainwin.MainWin.on_num_worker_spinbutton_changed() and - .on_num_worker_checkbutton_changed(). - - Sets the new value for the number of simultaneous downloads allowed. If - a download operation is in progress, informs the download manager - object, so the number of download workers can be adjusted. Also - increases the number of pages in the Output Tab, if necessary. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13287 set_num_worker_default') - - if value < self.num_worker_min or value > self.num_worker_max: - return self.system_error( - 152, - 'Set simultaneous downloads request failed sanity check', - ) - - old_value = self.num_worker_default - self.num_worker_default = value - - if old_value != value and self.download_manager_obj: - self.download_manager_obj.change_worker_count(value) - - if value > self.main_win_obj.output_page_count: - self.main_win_obj.output_tab_setup_pages() - - - def set_open_temp_on_desktop_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13308 set_open_temp_on_desktop_flag') - - if not flag: - self.open_temp_on_desktop_flag = False - else: - self.open_temp_on_desktop_flag = True - - - def set_operation_auto_update_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13319 set_operation_auto_update_flag') - - if not flag: - self.operation_auto_update_flag = False - else: - self.operation_auto_update_flag = True - - - def set_operation_check_limit(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13330 set_operation_check_limit') - - self.operation_check_limit = value - - - def set_operation_convert_mode(self, mode): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13338 set_operation_convert_mode') - - if mode == 'disable' or mode == 'multi' or mode == 'channel' \ - or mode == 'playlist': - self.operation_convert_mode = mode - - - def set_operation_dialogue_mode(self, mode): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13348 set_operation_dialogue_mode') - - if mode == 'default' or mode == 'desktop' or mode == 'dialogue': - self.operation_dialogue_mode = mode - - - def set_operation_download_limit(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13357 set_operation_download_limit') - - self.operation_download_limit = value - - - def set_operation_error_show_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13365 set_operation_error_show_flag') - - if not flag: - self.operation_error_show_flag = False - else: - self.operation_error_show_flag = True - - - def set_operation_halted_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13376 set_operation_halted_flag') - - if not flag: - self.operation_halted_flag = False - else: - self.operation_halted_flag = True - - - def set_operation_limit_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13387 set_operation_limit_flag') - - if not flag: - self.operation_limit_flag = False - else: - self.operation_limit_flag = True - - - def set_operation_save_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13398 set_operation_save_flag') - - if not flag: - self.operation_save_flag = False - else: - self.operation_save_flag = True - - - def set_operation_sim_shortcut_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13409 set_operation_sim_shortcut_flag') - - if not flag: - self.operation_sim_shortcut_flag = False - else: - self.operation_sim_shortcut_flag = True - - - def set_operation_warning_show_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13420 set_operation_warning_show_flag') - - if not flag: - self.operation_warning_show_flag = False - else: - self.operation_warning_show_flag = True - - - def set_progress_list_hide_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13431 set_progress_list_hide_flag') - - if not flag: - self.progress_list_hide_flag = False - else: - self.progress_list_hide_flag = True - # If a download operation is in progress, hide any hideable rows - # immediately - if self.download_manager_obj: - self.main_win_obj.progress_list_check_hide_rows(True) - - - def set_refresh_moviepy_timeout(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13446 set_refresh_moviepy_timeout') - - self.refresh_moviepy_timeout = value - - - def set_refresh_output_verbose_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13454 set_refresh_output_verbose_flag') - - if not flag: - self.refresh_output_verbose_flag = False - else: - self.refresh_output_verbose_flag = True - - - def set_refresh_output_videos_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13465 set_refresh_output_videos_flag') - - if not flag: - self.refresh_output_videos_flag = False - else: - self.refresh_output_videos_flag = True - - - def set_results_list_reverse_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13476 set_results_list_reverse_flag') - - if not flag: - self.results_list_reverse_flag = False - else: - self.results_list_reverse_flag = True - - - def set_scheduled_check_mode(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13487 set_scheduled_check_mode') - - self.scheduled_check_mode = value - - - def set_scheduled_check_wait_hours(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13495 set_scheduled_check_wait_hours') - - self.scheduled_check_wait_hours = value - - - def set_scheduled_dl_mode(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13503 set_scheduled_dl_mode') - - self.scheduled_dl_mode = value - - - def set_scheduled_dl_wait_hours(self, value): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13511 set_scheduled_dl_wait_hours') - - self.scheduled_dl_wait_hours = value - - - def set_scheduled_shutdown_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13519 set_scheduled_shutdown_flag') - - if not flag: - self.scheduled_shutdown_flag = False - else: - self.scheduled_shutdown_flag = True - - - def set_simple_options_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13530 set_simple_options_flag') - - if not flag: - self.simple_options_flag = False - else: - self.simple_options_flag = True - - - def set_show_pretty_dates_flag(self, flag): - - """Called by config.SystemPrefWin.on_pretty_date_button_toggled(). - - Shows/hides the status icon in the system tray. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13546 set_show_pretty_dates_flag') - - if not flag: - self.show_pretty_dates_flag = False - else: - self.show_status_icon_flag = True - - # Redraw the Video Catalogue, but only if something was already drawn - # there (and keep the current page number) - if self.main_win_obj.video_index_current is not None: - self.main_win_obj.video_catalogue_redraw_all( - self.main_win_obj.video_index_current, - self.main_win_obj.catalogue_toolbar_current_page, - ) - - - def set_show_small_icons_in_index(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13565 set_show_small_icons_in_index') - - if not flag: - self.show_small_icons_in_index = False - else: - self.show_small_icons_in_index = True - - # Redraw the Video Index and Video Catalogue - self.main_win_obj.video_index_catalogue_reset() - - - def set_show_status_icon_flag(self, flag): - - """Called by config.SystemPrefWin.on_show_status_icon_toggled(). - - Shows/hides the status icon in the system tray. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13584 set_show_status_icon_flag') - - if not flag: - self.show_status_icon_flag = False - if self.status_icon_obj: - self.status_icon_obj.hide_icon() - - else: - self.show_status_icon_flag = True - if self.status_icon_obj: - self.status_icon_obj.show_icon() - - - def set_show_tooltips_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13600 set_show_tooltips_flag') - - if not flag: - self.show_tooltips_flag = False - # (The True argument forces the Video Catalogue to be redrawn) - self.main_win_obj.disable_tooltips(True) - - else: - self.show_tooltips_flag = True - self.main_win_obj.enable_tooltips(True) - - - def set_system_error_show_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13615 set_system_error_show_flag') - - if not flag: - self.system_error_show_flag = False - else: - self.system_error_show_flag = True - - - def set_system_msg_keep_totals_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13626 set_system_msg_keep_totals_flag') - - if not flag: - self.system_msg_keep_totals_flag = False - else: - self.system_msg_keep_totals_flag = True - - - def set_system_warning_show_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13637 set_system_warning_show_flag') - - if not flag: - self.system_warning_show_flag = False - else: - self.system_warning_show_flag = True - - - def set_toolbar_squeeze_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13648 set_toolbar_squeeze_flag') - - if not flag: - self.toolbar_squeeze_flag = False - else: - self.toolbar_squeeze_flag = True - - if self.main_win_obj and self.main_win_obj.main_toolbar: - self.main_win_obj.redraw_main_toolbar() - - - def set_use_module_moviepy_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13662 set_use_module_moviepy_flag') - - if not flag: - self.use_module_moviepy_flag = False - else: - self.use_module_moviepy_flag = True - - - def set_video_res_apply_flag(self, flag): - - """Called by mainwin.MainWin.on_video_res_checkbutton_changed(). - - Applies or releases the video resolution limit. If a download operation - is in progress, the new setting is applied to the next download job. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13679 set_video_res_apply_flag') - - if not flag: - self.video_res_apply_flag = False - else: - self.video_res_apply_flag = True - - - def set_video_res_default(self, value): - - """Called by mainwin.MainWin.set_video_res_limit() and - .on_video_res_combobox_changed()(). - - Sets the new video resolution limit. If a download operation is in - progress, the new value is applied to the next download job. - - Args: - - value (str): The new video resolution limit (a key in - formats.VIDEO_RESOLUTION_DICT, e.g. '720p') - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13703 set_video_res_default') - - if not value in formats.VIDEO_RESOLUTION_DICT: - return self.system_error( - 153, - 'Set video resolution request failed sanity check', - ) - - self.video_res_default = 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') - - if not flag: - self.ytdl_output_ignore_json_flag = False - else: - self.ytdl_output_ignore_json_flag = True - - - def set_ytdl_output_ignore_progress_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13728 set_ytdl_output_ignore_progress_flag') - - if not flag: - self.ytdl_output_ignore_progress_flag = False - else: - self.ytdl_output_ignore_progress_flag = True - - - def set_ytdl_output_show_summary_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13739 set_ytdl_output_show_summary_flag') - - if not flag: - self.ytdl_output_show_summary_flag = False - else: - self.ytdl_output_show_summary_flag = True - - - def set_ytdl_output_start_empty_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13750 set_ytdl_output_start_empty_flag') - - if not flag: - self.ytdl_output_start_empty_flag = False - else: - self.ytdl_output_start_empty_flag = True - - - def set_ytdl_output_stderr_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13761 set_ytdl_output_stderr_flag') - - if not flag: - self.ytdl_output_stderr_flag = False - else: - self.ytdl_output_stderr_flag = True - - - def set_ytdl_output_stdout_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13772 set_ytdl_output_stdout_flag') - - if not flag: - self.ytdl_output_stdout_flag = False - else: - self.ytdl_output_stdout_flag = True - - - def set_ytdl_output_system_cmd_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13783 set_ytdl_output_system_cmd_flag') - - if not flag: - self.ytdl_output_system_cmd_flag = False - else: - self.ytdl_output_system_cmd_flag = True - - - def set_ytdl_path(self, path): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13794 set_ytdl_path') - - self.ytdl_path = path - - - def set_ytdl_update_current(self, string): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13802 set_ytdl_update_current') - - self.ytdl_update_current = 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') - - if not flag: - self.ytdl_write_ignore_json_flag = False - else: - self.ytdl_write_ignore_json_flag = True - - - def set_ytdl_write_ignore_progress_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13821 set_ytdl_write_ignore_progress_flag') - - if not flag: - self.ytdl_write_ignore_progress_flag = False - else: - self.ytdl_write_ignore_progress_flag = True - - - def set_ytdl_write_stderr_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13832 set_ytdl_write_stderr_flag') - - if not flag: - self.ytdl_write_stderr_flag = False - else: - self.ytdl_write_stderr_flag = True - - - def set_ytdl_write_stdout_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13843 set_ytdl_write_stdout_flag') - - if not flag: - self.ytdl_write_stdout_flag = False - else: - self.ytdl_write_stdout_flag = True - - - def set_ytdl_write_system_cmd_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13854 set_ytdl_write_system_cmd_flag') - - if not flag: - self.ytdl_write_system_cmd_flag = False - else: - self.ytdl_write_system_cmd_flag = True - - - def set_ytdl_write_verbose_flag(self, flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('app 13865 set_ytdl_write_verbose_flag') - - if not flag: - self.ytdl_write_verbose_flag = False - else: - self.ytdl_write_verbose_flag = True diff --git a/tartube/mainwin.py b/tartube/mainwin.py deleted file mode 100755 index aa428bf..0000000 --- a/tartube/mainwin.py +++ /dev/null @@ -1,17343 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Main window classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, Gdk, GdkPixbuf - - -# Import other modules -import datetime -from gi.repository import Gio -import os -from gi.repository import Pango -import re -import sys -import threading -import time -# (Desktop notifications don't work on MS Windows, so no need to import Notify) -if os.name != 'nt': - gi.require_version('Notify', '0.7') - from gi.repository import Notify - - -# Import our modules -import config -import formats -import html -import __main__ -import mainapp -import media -import options -import utils - - -# Debugging flag (calls utils.debug_time at the start of every function) -DEBUG_FUNC_FLAG = False -# ...(but don't call utils.debug_time from anything called by the -# mainapp.TartubeApp timer functions, e.g. -# self.video_catalogue_retry_insert_items() -DEBUG_NO_TIMER_FUNC_FLAG = False - - -# Classes -class MainWin(Gtk.ApplicationWindow): - - """Called by mainapp.TartubeApp.start(). - - Python class that handles the main window. - - The main window has three tabs - the Videos Tab, the Progress Tab and the - Errors tab. - - In the Videos Tab, the Video Index is visible on the left, and the Video - Catalogue is visible on the right. - - In the Progress Tab, the Progress List is visible at the top, and the - Results List is visible at the bottom. - - In the Errors Tab, any errors generated by youtube-dl are displayed. (The - display is not reset at the beginning of every download operation). - - Args: - - app_obj (mainapp.TartubeApp): The main application object - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 95 __init__') - - super(MainWin, self).__init__( - title=__main__.__packagename__.title() + ' v' \ - + __main__.__version__, - application=app_obj - ) - - # IV list - class objects - # ----------------------- - # The main application - self.app_obj = app_obj - - - # IV list - Gtk widgets - # --------------------- - # (from self.setup_grid) - self.grid = None # Gtk.Grid - # (from self.setup_menubar) - self.menubar = None # Gtk.MenuBar - self.change_db_menu_item = None # Gtk.MenuItem - self.save_db_menu_item = None # Gtk.MenuItem - self.save_all_menu_item = None # Gtk.MenuItem - self.system_prefs_menu_item = None # Gtk.MenuItem - self.gen_options_menu_item = None # Gtk.MenuItem - self.add_video_menu_item = None # Gtk.MenuItem - self.add_channel_menu_item = None # Gtk.MenuItem - self.add_playlist_menu_item = None # Gtk.MenuItem - self.add_folder_menu_item = None # Gtk.MenuItem - self.export_db_menu_item = None # Gtk.MenuItem - self.import_db_menu_item = None # Gtk.MenuItem - self.switch_view_menu_item = None # Gtk.MenuItem - self.test_menu_item = None # Gtk.MenuItem - self.show_hidden_menu_item = None # Gtk.MenuItem - self.check_all_menu_item = None # Gtk.MenuItem - self.download_all_menu_item = None # Gtk.MenuItem - self.refresh_db_menu_item = None # Gtk.MenuItem - self.update_ytdl_menu_item = None # Gtk.MenuItem - self.test_ytdl_menu_item = None # Gtk.MenuItem - self.install_ffmpeg_menu_item = None # Gtk.MenuItem - self.tidy_up_menu_item = None # Gtk.MenuItem - self.stop_operation_menu_item = None # Gtk.MenuItem - # (from self.setup_main_toolbar) - self.main_toolbar = None # Gtk.Toolbar - self.add_video_toolbutton = None # Gtk.ToolButton - self.add_channel_toolbutton = None # Gtk.ToolButton - self.add_playlist_toolbutton = None # Gtk.ToolButton - self.add_folder_toolbutton = None # Gtk.ToolButton - self.check_all_toolbutton = None # Gtk.ToolButton - self.download_all_toolbutton = None # Gtk.ToolButton - self.stop_operation_toolbutton = None # Gtk.ToolButton - self.switch_view_toolbutton = None # Gtk.ToolButton - self.test_toolbutton = None # Gtk.ToolButton - # (from self.setup_notebook) - self.notebook = None # Gtk.Notebook - self.videos_tab = None # Gtk.Box - self.videos_label = None # Gtk.Label - self.progress_tab = None # Gtk.Box - self.progress_label = None # Gtk.Label - self.output_tab = None # Gtk.Box - self.output_label = None # Gtk.Label - self.errors_tab = None # Gtk.Box - self.errors_label = None # Gtk.Label - # (from self.setup_videos_tab) - self.video_index_vbox = None # Gtk.VBox - self.videos_paned = None # Gtk.HPaned - self.video_index_scrolled = None # Gtk.ScrolledWindow - self.video_index_frame = None # Gtk.Frame - self.video_index_treeview = None # Gtk.TreeView - self.video_index_treestore = None # Gtk.TreeStore - self.video_index_sortmodel = None # Gtk.TreeModelSort - self.video_index_tooltip_column = 2 - self.button_box = None # Gtk.VBox - self.check_media_button = None # Gtk.Button - self.download_media_button = None # Gtk.Button - self.progress_box = None # Gtk.HBox - self.progress_bar = None # Gtk.ProgressBar - self.progress_label = None # Gtk.Label - self.video_catalogue_vbox = None # Gtk.VBox - self.catalogue_scrolled = None # Gtk.ScrolledWindow - self.catalogue_frame = None # Gtk.Frame - self.catalogue_listbox = None # Gtk.ListBox - self.catalogue_toolbar = None # Gtk.Toolbar - self.catalogue_page_entry = None # Gtk.Entry - self.catalogue_last_entry = None # Gtk.Entry - self.catalogue_size_entry = None # Gtk.Entry - self.catalogue_first_button = None # Gtk.ToolButton - self.catalogue_back_button = None # Gtk.ToolButton - self.catalogue_forwards_button = None # Gtk.ToolButton - self.catalogue_last_button = None # Gtk.ToolButton - self.catalogue_scroll_up_button = None # Gtk.ToolButton - self.catalogue_scroll_down_button = None - # Gtk.ToolButton - self.catalogue_show_filter_button = None - # Gtk.ToolButton - self.catalogue_toolbar2 = None # Gtk.Toolbar - self.catalogue_sort_button = None # Gtk.ToolButton - self.catalogue_filter_entry = None # Gtk.Entry - self.catalogue_regex_togglebutton = None - # Gtk.ToggleButton - self.catalogue_apply_filter_button = None - # Gtk.ToolButton - self.catalogue_cancel_filter_button = None - # Gtk.ToolButton - self.catalogue_find_date_button = None # Gtk.ToolButton - # (from self.setup_progress_tab) - self.progress_paned = None # Gtk.VPaned - self.progress_list_scrolled = None # Gtk.ScrolledWindow - self.progress_list_treeview = None # Gtk.TreeView - self.progress_list_liststore = None # Gtk.ListStore - self.progress_list_tooltip_column = 2 - self.results_list_scrolled = None # Gtk.Frame - self.results_list_treeview = None # Gtk.TreeView - self.results_list_liststore = None # Gtk.ListStore - self.results_list_tooltip_column = 1 - self.num_worker_checkbutton = None # Gtk.CheckButton - self.num_worker_spinbutton = None # Gtk.SpinButton - self.bandwidth_checkbutton = None # Gtk.CheckButton - self.bandwidth_spinbutton = None # Gtk.SpinButton - self.video_res_checkbutton = None # Gtk.CheckButton - self.video_res_combobox = None # Gtk.ComboBox - self.hide_finished_checkbutton = None # Gtk.CheckButton - self.reverse_results_checkbutton = None # Gtk.CheckButton - # (from self.setup_output_tab) - self.output_notebook = None # Gtk.Notebook - # (from self.setup_errors_tab) - self.errors_list_scrolled = None # Gtk.ScrolledWindow - self.errors_list_treeview = None # Gtk.TreeView - self.errors_list_liststore = None # Gtk.ListStore - self.show_system_error_checkbutton = None - # Gtk.CheckButton - self.show_system_warning_checkbutton = None - # Gtk.CheckButton - self.show_operation_error_checkbutton = None - # Gtk.CheckButton - self.show_operation_warning_checkbutton = None - # Gtk.CheckButton - self.error_list_button = None # Gtk.Button - - - # IV list - other - # --------------- - # Size (in pixels) of gaps between main window widgets - self.spacing_size = self.app_obj.default_spacing_size - # Standard size of video thumbnails in the Video Catalogue, in pixels, - # assuming that the actual thumbnail file is 1280x720 - self.thumb_width = 128 - self.thumb_height = 76 - - # Paths to Tartube standard icon files. 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 'video_both_large' (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 - - # Standard limits for the length of strings displayed in various - # widgets - self.very_long_string_max_len = 64 - self.long_string_max_len = 48 - self.quite_long_string_max_len = 40 - self.medium_string_max_len = 32 - self.short_string_max_len = 24 - self.tiny_string_max_len = 16 - # Use a separate IV for video descriptions (so we can tweak it - # specifically) - # The value is low, because descriptions in ALL CAPS are too big for - # the Video Catalogue, otherwise - self.descrip_line_max_len = 50 - # Use a separate IV for tooltips in the Video Index/Video Catalogue - self.tooltip_max_len = 60 - # Limits (number of videos) at which the code will prompt the user - # before bookmarking videos (etc) - # Take shortcuts, but don't prompt the user - self.mark_video_lower_limit = 50 - # Take shortcuts, and prompt the user - self.mark_video_higher_limit = 1000 - - # Videos Tab IVs - # The Video Index is the left-hand side of the main window, and - # displays only channels, playlists and folders - # The Video Index uses a Gtk.TreeView to display media data objects - # (channels, playlist and folders, but not videos). This dictionary - # keeps track of which row in the Gtk.TreeView is displaying which - # media data object - # Dictionary in the form - # key = name of the media data object (stored in its .name IV) - # value = Gtk.TreeRowReference - self.video_index_row_dict = {} - # The call to self.video_index_add_row() causes the auto-sorting - # function self.video_index_auto_sort() to be called before we're - # ready, due to some Gtk problem I don't understand - # Temporary solution is to disable auto-sorting during calls to that - # function - self.video_index_no_sort_flag = False - # The name of the channel, playlist or folder currently visible in the - # Video Catalogue (None if no channel, playlist or folder is - # selected) - self.video_index_current = None - # Flag set to True when the currently visible item is a private folder - # (media.Folder.priv_flag is True), set to False at all other times - self.video_index_current_priv_flag = False - # Don't update the Video Catalogue during certain procedures, such as - # removing a row from the Video Index (in which case, this flag will - # be set to True - self.ignore_video_index_select_flag = False - - # The Video Catalogue is the right-hand side of the main window. When - # the user clicks on a channel, playlist or folder, all the videos - # it contains are displayed in the Video catalogue (replacing any - # previous contents) - # Dictionary of mainwin.SimpleCatalogueItem or - # mainwin.ComplexCatalogueItem objects (depending on the current - # value of self.catalogue_mode) - # There is one catalogue item object for each row that's currently - # visible in the Video Catalogue - # Dictionary in the form - # key = dbid (of the mainWin.SimpleCatalogueItem or - # mainWin.ComplexCatalogueItem, which matches the dbid of its - # media.Video object) - # value = the catalogue item itself - self.video_catalogue_dict = {} - # Rows are added to the catalogue in a call to - # self.video_catalogue_insert_item() - # If Gtk issues a warning, complaining that the Gtk.ListBox is being - # sorted, the row (actually a CatalogueRow object) is added to this - # list temporarily, and then periodic calls to - # self.video_catalogue_retry_insert_items() try again, until the - # list is empty - self.video_catalogue_temp_list = [] - # Flag set to True if a filter is currently applied to the Video - # Catalogue, hiding some videos, and showing only videos that match - # the search text; False if not - self.video_catalogue_filtered_flag = False - # When the filter is applied, a list of video objects to show (may be - # an empty list) - self.video_catalogue_filtered_list = [] - - # 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 - # mainapp.TartubeApp.catalogue_page_size - # The current page number (minimum 1, maximum 9999) - self.catalogue_toolbar_current_page = 1 - # The number of pages currently in use (minimum 1, maximum 9999) - self.catalogue_toolbar_last_page = 1 - - # Progress Tab IVs - # The Progress List uses a Gtk.TreeView display download jobs, whether - # they are waiting to start, currently in progress, or finished. This - # dictionary keeps track of which row in the Gtk.TreeView is handling - # which download job - # Dictionary in the form - # key = The downloads.DownloadItem.item_id for the download item - # handling the media data object - # value = the row number (0 is the first row) - self.progress_list_row_dict = {} - # The number of rows added to the treeview - self.progress_list_row_count = 0 - # During a download operation, self.progress_list_receive_dl_stats() is - # called every time youtube-dl writes some output to STDOUT. This can - # happen many times a second - # Updating data displayed in the Progress List several times a second, - # and irregularly, doesn't look very nice. Instead, we only update - # the displayed data at fixed intervals - # Thus, when self.progress_list_receive_dl_stats() is called, it - # 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() - # Then, during calls at fixed intervals to - # self.progress_list_display_dl_stats(), those download statistics - # are displayed - # Dictionary of download statistics yet to be displayed, emptied after - # every call to self.progress_list_display_dl_stats() - # 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.progress_list_temp_dict = {} - # During a download operation, we keep track of rows that are finished, - # so they can be hidden, if required - # Dictionary in the form - # key = The downloads.DownloadItem.item_id for the download item - # handling the media data object - # value = The time at which it should be hidden (matches time.time()) - # (As soon as a row is hidden, all of these IVs are updated, removing - # them from all three dictionaries) - self.progress_list_finish_dict = {} - # The time (in seconds) after which a row which can be hidden, should - # actually be hidden - # (The code assumes it is at least twice the value of - # mainapp.TartubeApp.dl_timer_time) - self.progress_list_hide_time = 3 - - # Whenever a video is downloaded (in reality, or just in simulation), - # a row is added to Gtk.TreeView in the Results List - # The number of rows added to the treeview - self.results_list_row_count = 0 - # At the instant youtube-dl reports that a video has been downloaded, - # the file doesn't yet exist in Tartube's data directory (so the - # Python test for the existence of the file fails) - # Therefore, self.results_list_add_row() adds a temporary entry to this - # list. Items in the list are checked by - # self.results_list_update_row() and removed from the list, as soon - # as the file is confirmed to exist, at which time the Results List - # is updated - # (For simulated downloads, the entry is checked by - # self.results_list_update_row() just once. For real downloads, it - # is checked many times until either the file exists or the - # download operation halts) - # List of python dictionaries, one for each downloaded video. Each of - # those dictionaries are in the form: - # 'video_obj': a media.Video object - # 'row_num': the row on the treeview, matching - # self.results_list_row_count - # 'keep_description', 'keep_info', 'keep_annotations', - # 'keep_thumbnail': flags from the options.OptionsManager - # object used for to download the video (not added to the - # dictionary at all for simulated downloads) - self.results_list_temp_list = [] - - # 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 - # mainapp.TartubeApp.ytdl_output_show_summary_flag is False) - self.output_tab_summary_flag = False - # The number of pages in the Output Tab's notebook (not including the - # summary tab). The number matches the highest value of - # mainapp.TartubeApp.num_worker_default during this session (i.e. if - # the user increases the value, new page(s) are created, but if the - # user reduces the value, no pages are destroyed) - self.output_page_count = 0 - # Dictionary of Gtk.TextView objects created in the Output Tab; one for - # each page - # Dictionary in the form - # key = The page number (the summary page is #0, the first page for a - # thread is #1, regardless of whether the summary page is - # visible) - # value = The corresponding Gtk.TextView object - self.output_textview_dict = {} - # When youtube-dl generates output, that text cannot be displayed in - # the Output Tab's pages immediately (because Gtk widgets cannot be - # updated from within a thread) - # Instead, values are appended to this list - # During a download operation, mainapp.TartubeApp.dl_timer_callback() - # calls self.output_tab_update() regularly to display the output in - # the Output Tab (which empties the list) - # List in groups of 3, in the form - # (page_number, mssage, type...) - # ...where 'page_number' matches a key in self.output_textview_dict, - # 'msg' is a string to display, and 'type' is 'system_cmd' for a - # system command (displayed in yellow, by default), 'error_warning' - # for an error/warning message (displayed in cyan, by default) and - # 'default' for everything else - self.output_tab_insert_list = [] - # Colours used in the output tab - self.output_tab_bg_colour = '#000000' - self.output_tab_text_colour = '#FFFFFF' - self.output_tab_stderr_colour = 'cyan' - self.output_tab_system_cmd_colour = 'yellow' - - # Errors / Warnings Tab IVs - # The number of errors added to the Error List, since this tab was the - # visible one (updated by self.errors_list_add_row() or - # self.errors_list_add_system_error(), and reset back to zero by - # self.on_notebook_switch_page() when the tab becomes the visible one - # again) - self.tab_error_count = 0 - # The number of warnings added to the Error List, since this tab was - # the visible one - self.tab_warning_count = 0 - # The number of the tab in self.notebook that is currently visible - # (only required to test whether the Errors/Warnings tab is the - # visible one) - self.visible_tab_num = 0 - - # List of configuration windows (anything inheriting from - # config.GenericConfigWin) that are currently open. A download/ - # update/refresh/info/tidy operation cannot start when one of these - # windows are open (and the windows cannot be opened during such an - # operation) - self.config_win_list = [] - - # Dialogue window IVs - # The SetDestinationDialogue dialogue window displays a list of - # channels/playlists/folders. When opening it repeatedly, it's handy - # to display the previous selection at the top of the list - # The .dbid of the previous channel/playlist/folder selected (or None, - # if SetDestinationDialogue hasn't been used yet) - # The value is set/reset by a call to self.set_previous_alt_dest_dbid() - self.previous_alt_dest_dbid = None - - # Code - # ---- - - # Create GdkPixbuf.Pixbufs for all Tartube standard icons - self.setup_pixbufs() - # Set up the main window - self.setup_win() - - - # Public class methods - - - def setup_pixbufs(self): - - """Called by self.__init__(). - - Populates self.icon_dict and self.pixbuf.dict from the lists provided - by formats.py. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 522 setup_pixbufs') - - # 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', - ), - ), - ) - - icon_dir_list.append( - os.path.join( - '/', 'usr', 'share', __main__.__packagename__, 'icons', - ) - ) - - 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 - - for key in formats.TOOLBAR_ICON_DICT: - rel_path = formats.TOOLBAR_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'toolbar', rel_path), - ) - self.icon_dict[key] = full_path - - for key in formats.LARGE_ICON_DICT: - rel_path = formats.LARGE_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'large', rel_path), - ) - self.icon_dict[key] = full_path - - for key in formats.SMALL_ICON_DICT: - rel_path = formats.SMALL_ICON_DICT[key] - full_path = os.path.abspath( - os.path.join(icon_dir_path, 'small', rel_path), - ) - 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 - - # 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) - - 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), - ) - - # Store the correct icon_dir_path, so that StatusIcon can use - # it - self.icon_dir_path = icon_dir_path - - return - - # No icons directory found; this is a fatal error - print( - __main__.__prettyname__ + ' cannot start because it cannot find' \ - + ' its icons directory (folder)', - file=sys.stderr, - ) - - self.app_obj.do_shutdown() - - - def setup_win(self): - - """Called by self.__init__(). - - Sets up the main window, calling various function to create its - widgets. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 631 setup_win') - - # Set the default window size - self.set_default_size( - self.app_obj.main_win_width, - self.app_obj.main_win_height, - ) - - # Set the window's Gtk icon list - self.set_icon_list(self.win_pixbuf_list) - - # Intercept the user's attempts to close the window, so we can close to - # the system tray, if required - self.connect('delete_event', self.on_delete_event) - - # 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) - 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) - # (Continuing) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Set up desktop notifications. Notifications can be sent by calling - # self.notify_desktop() - if os.name != 'nt': - Notify.init(__main__.__prettyname__) - - # Create main window widgets - self.setup_grid() - self.setup_menubar() - self.setup_main_toolbar() - self.setup_notebook() - self.setup_videos_tab() - self.setup_progress_tab() - self.setup_output_tab() - self.setup_errors_tab() - - - # (Create main window widgets) - - - def setup_grid(self): - - """Called by self.setup_win(). - - Sets up a Gtk.Grid on which all the main window's widgets are placed. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 683 setup_grid') - - self.grid = Gtk.Grid() - self.add(self.grid) - - - def setup_menubar(self): - - """Called by self.setup_win(). - - Sets up a Gtk.Menu at the top of the main window. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 697 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') - 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...', - ) - file_sub_menu.append(self.change_db_menu_item) - self.change_db_menu_item.set_action_name('app.change_db_menu') - - # Separator - file_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.save_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_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', - ) - file_sub_menu.append(self.save_all_menu_item) - self.save_all_menu_item.set_action_name('app.save_all_menu') - - # Separator - file_sub_menu.append(Gtk.SeparatorMenuItem()) - - 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') - 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') - 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...', - ) - 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...', - ) - 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') - 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...', - ) - 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...', - ) - 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...', - ) - 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...', - ) - media_sub_menu.append(self.add_folder_menu_item) - self.add_folder_menu_item.set_action_name('app.add_folder_menu') - - # Separator - media_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.export_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Export from database', - ) - media_sub_menu.append(self.export_db_menu_item) - self.export_db_menu_item.set_action_name('app.export_db_menu') - - import_sub_menu = Gtk.Menu() - - import_json_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_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', - ) - 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' - ) - self.import_db_menu_item.set_submenu(import_sub_menu) - media_sub_menu.append(self.import_db_menu_item) - - # Separator - media_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.switch_view_menu_item = \ - 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') - media_sub_menu.append(self.show_hidden_menu_item) - self.show_hidden_menu_item.set_action_name('app.show_hidden_menu') - - if self.app_obj.debug_test_media_menu_flag: - - # Separator - media_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.test_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Add test media', - ) - media_sub_menu.append(self.test_menu_item) - self.test_menu_item.set_action_name('app.test_menu') - - # Operations column - 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') - 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') - 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') - ops_sub_menu.append(self.custom_dl_all_menu_item) - self.custom_dl_all_menu_item.set_action_name('app.custom_dl_all_menu') - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.refresh_db_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Refresh database...', - ) - ops_sub_menu.append(self.refresh_db_menu_item) - self.refresh_db_menu_item.set_action_name('app.refresh_db_menu') - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic( - '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...', - ) - ops_sub_menu.append(self.test_ytdl_menu_item) - self.test_ytdl_menu_item.set_action_name('app.test_ytdl_menu') - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.install_ffmpeg_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Install FFmpeg', - ) - ops_sub_menu.append(self.install_ffmpeg_menu_item) - self.install_ffmpeg_menu_item.set_action_name( - 'app.install_ffmpeg_menu', - ) - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.tidy_up_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Tidy up _files...', - ) - ops_sub_menu.append(self.tidy_up_menu_item) - self.tidy_up_menu_item.set_action_name( - 'app.tidy_up_menu', - ) - - # Separator - ops_sub_menu.append(Gtk.SeparatorMenuItem()) - - self.stop_operation_menu_item = \ - 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', - ) - - # Help column - 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...') - 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') - help_sub_menu.append(go_website_menu_item) - go_website_menu_item.set_action_name('app.go_website_menu') - - - def setup_main_toolbar(self): - - """Called by self.setup_win(). Also called by - self.redraw_main_toolbar(). - - Sets up a Gtk.Toolbar near the top of the main window, below the menu, - replacing the previous one, if it exists. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 947 setup_main_toolbar') - - # If a toolbar already exists, destroy it to make room for the new one - if self.main_toolbar: - self.grid.remove(self.main_toolbar) - - # Create a new toolbar - self.main_toolbar = Gtk.Toolbar() - self.grid.attach(self.main_toolbar, 0, 1, 1, 1) - - # Toolbar items. If mainapp.TartubeApp.toolbar_squeeze_flag is True, - # we don't display labels in the toolbuttons - squeeze_flag = self.app_obj.toolbar_squeeze_flag - - if not squeeze_flag: - self.add_video_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_video_small'], - ), - ) - self.add_video_toolbutton.set_label('Videos') - self.add_video_toolbutton.set_is_important(True) - else: - self.add_video_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_video_large'], - ), - ) - - 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_action_name('app.add_video_toolbutton') - - if not squeeze_flag: - self.add_channel_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_channel_small'], - ), - ) - self.add_channel_toolbutton.set_label('Channel') - self.add_channel_toolbutton.set_is_important(True) - else: - self.add_channel_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_channel_large'], - ), - ) - - 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_action_name( - 'app.add_channel_toolbutton', - ) - - if not squeeze_flag: - self.add_playlist_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_playlist_small'], - ), - ) - self.add_playlist_toolbutton.set_label('Playlist') - self.add_playlist_toolbutton.set_is_important(True) - else: - self.add_playlist_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_playlist_large'], - ), - ) - - 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_action_name( - 'app.add_playlist_toolbutton', - ) - - if not squeeze_flag: - self.add_folder_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_folder_small'], - ), - ) - self.add_folder_toolbutton.set_label('Folder') - self.add_folder_toolbutton.set_is_important(True) - else: - self.add_folder_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_folder_large'], - ), - ) - - 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_action_name('app.add_folder_toolbutton') - - # (Conversely, if there are no labels, then we have enough room for a - # separator) - if squeeze_flag: - self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1) - - if not squeeze_flag: - self.check_all_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_check_small'], - ), - ) - self.check_all_toolbutton.set_label('Check') - self.check_all_toolbutton.set_is_important(True) - else: - self.check_all_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_check_large'], - ), - ) - - self.main_toolbar.insert(self.check_all_toolbutton, -1) - self.check_all_toolbutton.set_tooltip_text( - 'Check all videos, channels, playlists and folders', - ) - self.check_all_toolbutton.set_action_name('app.check_all_toolbutton') - - if not squeeze_flag: - self.download_all_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_download_small'], - ), - ) - self.download_all_toolbutton.set_label('Download') - self.download_all_toolbutton.set_is_important(True) - else: - self.download_all_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_download_large'], - ), - ) - - self.main_toolbar.insert(self.download_all_toolbutton, -1) - self.download_all_toolbutton.set_tooltip_text( - 'Download all videos, channels, playlists and folders', - ) - self.download_all_toolbutton.set_action_name( - 'app.download_all_toolbutton', - ) - - if squeeze_flag: - self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1) - - if not squeeze_flag: - self.stop_operation_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_stop_small'], - ), - ) - self.stop_operation_toolbutton.set_label('Stop') - self.stop_operation_toolbutton.set_is_important(True) - else: - self.stop_operation_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_stop_large'], - ), - ) - - 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', - ) - self.stop_operation_toolbutton.set_action_name( - 'app.stop_operation_toolbutton', - ) - - if not squeeze_flag: - self.switch_view_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_switch_small'], - ), - ) - self.switch_view_toolbutton.set_label('Switch') - self.switch_view_toolbutton.set_is_important(True) - else: - self.switch_view_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_switch_large'], - ), - ) - - self.main_toolbar.insert(self.switch_view_toolbutton, -1) - self.switch_view_toolbutton.set_tooltip_text( - 'Switch between simple and complex views', - ) - self.switch_view_toolbutton.set_action_name( - 'app.switch_view_toolbutton', - ) - - if self.app_obj.debug_test_media_toolbar_flag: - - if not squeeze_flag: - self.test_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_test_small'], - ), - ) - self.test_toolbutton.set_label('Test') - self.test_toolbutton.set_is_important(True) - else: - self.test_toolbutton = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_test_large'], - ), - ) - - self.main_toolbar.insert(self.test_toolbutton, -1) - self.test_toolbutton.set_tooltip_text( - 'Add test media data objects', - ) - self.test_toolbutton.set_action_name('app.test_toolbutton') - - if squeeze_flag: - self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1) - - if not squeeze_flag: - quit_button = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_quit_small'], - ), - ) - quit_button.set_label('Quit') - quit_button.set_is_important(True) - else: - quit_button = Gtk.ToolButton.new( - Gtk.Image.new_from_pixbuf( - self.pixbuf_dict['tool_quit_large'], - ), - ) - - self.main_toolbar.insert(quit_button, -1) - quit_button.set_tooltip_text('Close ' + __main__.__prettyname__) - quit_button.set_action_name('app.quit_toolbutton') - - - def setup_notebook(self): - - """Called by self.setup_win(). - - Sets up a Gtk.Notebook occupying all the space below the menu and - toolbar. Creates two tabs, the Videos Tab and the Progress Tab. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1195 setup_notebook') - - self.notebook = Gtk.Notebook() - self.grid.attach(self.notebook, 0, 2, 1, 1) - self.notebook.set_border_width(self.spacing_size) - self.notebook.connect('switch-page', self.on_notebook_switch_page) - - # Videos Tab - self.videos_tab = Gtk.Box() - 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) - self.videos_tab.set_border_width(self.spacing_size) - - # Progress Tab - self.progress_tab = Gtk.Box() - 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) - - # Output Tab - self.output_tab = Gtk.Box() - 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) - self.output_tab.set_border_width(self.spacing_size) - - # Errors Tab - self.errors_tab = Gtk.Box() - 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) - self.errors_tab.set_border_width(self.spacing_size) - - - def setup_videos_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Videos Tab. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1243 setup_videos_tab') - - self.videos_paned = Gtk.HPaned() - self.videos_tab.pack_start(self.videos_paned, True, True, 0) - self.videos_paned.set_position(self.app_obj.paned_min_size) - self.videos_paned.set_wide_handle(True) - - # Left-hand side - self.video_index_vbox = Gtk.VBox() - self.videos_paned.add1(self.video_index_vbox) - - self.video_index_frame = Gtk.Frame() - self.video_index_vbox.pack_start( - self.video_index_frame, - True, - True, - 0, - ) - - self.video_index_scrolled = Gtk.ScrolledWindow() - self.video_index_frame.add(self.video_index_scrolled) - self.video_index_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # Video index - self.video_index_reset() - - # 'Check all' and 'Download all' buttons - self.button_box = Gtk.VBox() - self.video_index_vbox.pack_start(self.button_box, False, False, 0) - - self.check_media_button = Gtk.Button() - self.button_box.pack_start( - self.check_media_button, - True, - True, - self.spacing_size, - ) - self.check_media_button.set_label('Check all') - self.check_media_button.set_tooltip_text( - '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_tooltip_text( - 'Download all videos, channels, playlists and folders', - ) - self.download_media_button.set_action_name('app.download_all_button') - - # Right-hand side - self.video_catalogue_vbox = Gtk.VBox() - self.videos_paned.add2(self.video_catalogue_vbox) - - # Video catalogue - self.catalogue_frame = Gtk.Frame() - self.video_catalogue_vbox.pack_start( - self.catalogue_frame, - True, - True, - 0, - ) - - self.catalogue_scrolled = Gtk.ScrolledWindow() - self.catalogue_frame.add(self.catalogue_scrolled) - self.catalogue_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # (An invisible VBox adds a bit of space between the Video Catalogue - # and its toolbar) - self.video_catalogue_vbox.pack_start( - Gtk.VBox(), - False, - False, - self.spacing_size / 2, - ) - - # Video catalogue toolbar - self.catalogue_toolbar_frame = Gtk.Frame() - self.video_catalogue_vbox.pack_start( - self.catalogue_toolbar_frame, - False, - False, - 0, - ) - - self.catalogue_toolbar_vbox = Gtk.VBox() - self.catalogue_toolbar_frame.add(self.catalogue_toolbar_vbox) - - self.catalogue_toolbar = Gtk.Toolbar() - self.catalogue_toolbar_vbox.pack_start( - self.catalogue_toolbar, - False, - False, - 0, - ) - - toolitem = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem, -1) - label = Gtk.Label('Page ') - toolitem.add(label) - - toolitem2 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem2, -1) - self.catalogue_page_entry = Gtk.Entry() - toolitem2.add(self.catalogue_page_entry) - self.catalogue_page_entry.set_text( - str(self.catalogue_toolbar_current_page), - ) - 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.connect( - 'activate', - self.on_video_catalogue_page_entry_activated, - ) - - toolitem3 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem3, -1) - label2 = Gtk.Label(' of ') - toolitem3.add(label2) - - toolitem4 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem4, -1) - self.catalogue_last_entry = Gtk.Entry() - toolitem4.add(self.catalogue_last_entry) - self.catalogue_last_entry.set_text( - str(self.catalogue_toolbar_last_page), - ) - self.catalogue_last_entry.set_width_chars(4) - self.catalogue_last_entry.set_sensitive(False) - self.catalogue_last_entry.set_editable(False) - - toolitem5 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem5, -1) - label3 = Gtk.Label(' Size ') - toolitem5.add(label3) - - toolitem6 = Gtk.ToolItem.new() - self.catalogue_toolbar.insert(toolitem6, -1) - self.catalogue_size_entry = Gtk.Entry() - toolitem6.add(self.catalogue_size_entry) - self.catalogue_size_entry.set_text( - 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.connect( - 'activate', - self.on_video_catalogue_size_entry_activated, - ) - - # Separator - self.catalogue_toolbar.insert(Gtk.SeparatorToolItem(), -1) - - self.catalogue_first_button \ - = 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_action_name( - 'app.first_page_toolbutton', - ) - - self.catalogue_back_button \ - = 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_action_name( - 'app.previous_page_toolbutton', - ) - - self.catalogue_forwards_button \ - = 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_action_name( - 'app.next_page_toolbutton', - ) - - self.catalogue_last_button \ - = 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_action_name( - 'app.last_page_toolbutton', - ) - - self.catalogue_scroll_up_button \ - = 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_action_name( - 'app.scroll_up_toolbutton', - ) - - self.catalogue_scroll_down_button \ - = 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_action_name( - 'app.scroll_down_toolbutton', - ) - - self.catalogue_show_filter_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SORT_ASCENDING) - 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', - ) - self.catalogue_show_filter_button.set_action_name( - 'app.show_filter_toolbutton', - ) - - # Second toolbar, which is not actually added to the VBox until the - # call to self.update_show_filter_widgets() - self.catalogue_toolbar2 = Gtk.Toolbar() - self.catalogue_toolbar2.set_visible(False) - - toolitem7 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem7, -1) - 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_action_name( - 'app.sort_type_toolbutton', - ) - - # Separator - self.catalogue_toolbar2.insert(Gtk.SeparatorToolItem(), -1) - - toolitem8 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem8, -1) - label5 = Gtk.Label('Filter ') - toolitem8.add(label5) - - toolitem9 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem9, -1) - self.catalogue_filter_entry = Gtk.Entry() - 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') - - toolitem10 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem10, -1) - self.catalogue_regex_togglebutton \ - = Gtk.ToggleButton('Regex') - toolitem10.add(self.catalogue_regex_togglebutton) - self.catalogue_regex_togglebutton.set_sensitive(False) - self.catalogue_regex_togglebutton.set_tooltip_text( - 'Select if search text is a regex', - ) - self.catalogue_regex_togglebutton.set_action_name( - 'app.use_regex_togglebutton', - ) - - self.catalogue_apply_filter_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND) - 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', - ) - self.catalogue_apply_filter_button.set_action_name( - 'app.apply_filter_toolbutton', - ) - - self.catalogue_cancel_filter_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CANCEL) - 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', - ) - self.catalogue_cancel_filter_button.set_action_name( - 'app.cancel_filter_toolbutton', - ) - - # Separator - self.catalogue_toolbar2.insert(Gtk.SeparatorToolItem(), -1) - - toolitem11 = Gtk.ToolItem.new() - self.catalogue_toolbar2.insert(toolitem11, -1) - label6 = Gtk.Label('Find date') - toolitem11.add(label6) - - self.catalogue_find_date_button \ - = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND) - 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', - ) - self.catalogue_find_date_button.set_action_name( - 'app.find_date_toolbutton', - ) - - # Video catalogue - self.video_catalogue_reset() - - - def setup_progress_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Progress Tab. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1570 setup_progress_tab') - - vbox = Gtk.VBox() - self.progress_tab.pack_start(vbox, True, True, 0) - - self.progress_paned = Gtk.VPaned() - vbox.pack_start(self.progress_paned, True, True, 0) - self.progress_paned.set_position(self.app_obj.paned_min_size) - self.progress_paned.set_wide_handle(True) - - # Upper half - frame = Gtk.Frame() - self.progress_paned.add1(frame) - - self.progress_list_scrolled = Gtk.ScrolledWindow() - frame.add(self.progress_list_scrolled) - self.progress_list_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # Progress List - 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) - self.progress_list_treeview.set_tooltip_column( - self.progress_list_tooltip_column, - ) - # (Detect right-clicks on the treeview) - self.progress_list_treeview.connect( - 'button-press-event', - self.on_progress_list_right_click, - ) - - for i, column_title in enumerate( - [ - 'hide', 'hide', 'hide', '', 'Source', '#', 'Status', - 'Incoming file', 'Ext', '%', 'Speed', 'ETA', 'Size', - ] - ): - if not column_title: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - '', - renderer_pixbuf, - pixbuf=i, - ) - self.progress_list_treeview.append_column(column_pixbuf) - column_pixbuf.set_resizable(False) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.progress_list_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.progress_list_liststore = Gtk.ListStore( - int, int, str, - GdkPixbuf.Pixbuf, - str, str, str, str, str, str, str, str, str, - ) - self.progress_list_treeview.set_model(self.progress_list_liststore) - - # Lower half - frame2 = Gtk.Frame() - self.progress_paned.add2(frame2) - - self.results_list_scrolled = Gtk.ScrolledWindow() - frame2.add(self.results_list_scrolled) - self.results_list_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - # Results List - 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) - self.results_list_treeview.set_tooltip_column( - self.results_list_tooltip_column, - ) - # (Detect right-clicks on the treeview) - self.results_list_treeview.connect( - 'button-press-event', - self.on_results_list_right_click, - ) - - for i, column_title in enumerate( - [ - 'hide', 'hide', '', 'New videos', 'Duration', 'Size', 'Date', - 'File', '', 'Downloaded to', - ] - ): - if not column_title: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - column_title, - renderer_pixbuf, - pixbuf=i, - ) - self.results_list_treeview.append_column(column_pixbuf) - column_pixbuf.set_resizable(False) - - elif column_title == 'File': - renderer_toggle = Gtk.CellRendererToggle() - column_toggle = Gtk.TreeViewColumn( - column_title, - renderer_toggle, - active=i, - ) - self.results_list_treeview.append_column(column_toggle) - column_toggle.set_resizable(False) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.results_list_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.results_list_liststore = Gtk.ListStore( - int, str, - GdkPixbuf.Pixbuf, - str, str, str, str, - bool, - GdkPixbuf.Pixbuf, - str, - ) - self.results_list_treeview.set_model(self.results_list_liststore) - - # Strip of widgets at the bottom, arranged in a grid - grid = Gtk.Grid() - vbox.pack_start(grid, False, False, 0) - grid.set_vexpand(False) - grid.set_border_width(self.spacing_size) - grid.set_column_spacing(self.spacing_size) - grid.set_row_spacing(self.spacing_size) - - 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_active( - self.app_obj.num_worker_apply_flag, - ) - self.num_worker_checkbutton.connect( - 'toggled', - self.on_num_worker_checkbutton_changed, - ) - - self.num_worker_spinbutton = Gtk.SpinButton.new_with_range( - self.app_obj.num_worker_min, - self.app_obj.num_worker_max, - 1, - ) - grid.attach(self.num_worker_spinbutton, 1, 0, 1, 1) - self.num_worker_spinbutton.set_value(self.app_obj.num_worker_default) - self.num_worker_spinbutton.connect( - 'value-changed', - self.on_num_worker_spinbutton_changed, - ) - - 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_active( - self.app_obj.bandwidth_apply_flag, - ) - # (Making this widget expandable guarantees the whole grid is always - # full) - self.bandwidth_checkbutton.set_hexpand(True) - self.bandwidth_checkbutton.connect( - 'toggled', - self.on_bandwidth_checkbutton_changed, - ) - - self.bandwidth_spinbutton = Gtk.SpinButton.new_with_range( - self.app_obj.bandwidth_min, - self.app_obj.bandwidth_max, - 1, - ) - grid.attach(self.bandwidth_spinbutton, 3, 0, 1, 1) - self.bandwidth_spinbutton.set_value(self.app_obj.bandwidth_default) - self.bandwidth_spinbutton.connect( - 'value-changed', - self.on_bandwidth_spinbutton_changed, - ) - - 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_active( - self.app_obj.video_res_apply_flag, - ) - self.video_res_checkbutton.connect( - 'toggled', - self.on_video_res_checkbutton_changed, - ) - - store = Gtk.ListStore(str) - for string in formats.VIDEO_RESOLUTION_LIST: - store.append( [string] ) - - self.video_res_combobox = Gtk.ComboBox.new_with_model(store) - grid.attach(self.video_res_combobox, 5, 0, 1, 1) - renderer_text = Gtk.CellRendererText() - 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 - self.video_res_combobox.connect( - 'changed', - self.on_video_res_combobox_changed, - ) - - 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', - ) - self.hide_finished_checkbutton.set_active( - self.app_obj.progress_list_hide_flag, - ) - self.hide_finished_checkbutton.connect( - 'toggled', - self.on_hide_finished_checkbutton_changed, - ) - - 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') - self.reverse_results_checkbutton.set_active( - self.app_obj.results_list_reverse_flag, - ) - self.reverse_results_checkbutton.connect( - 'toggled', - self.on_reverse_results_checkbutton_changed, - ) - - - def setup_output_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Output Tab. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1837 setup_output_tab') - - vbox = Gtk.VBox() - self.output_tab.pack_start(vbox, True, True, 0) - - # During a download operation, each page in the Output Tab's - # Gtk.Notebook displays output from a single downloads.DownloadWorker - # object - # The pages are added later, via a call to - # self.output_tab_setup_pages() - self.output_notebook = Gtk.Notebook() - vbox.pack_start(self.output_notebook, True, True, 0) - self.output_notebook.set_border_width(0) - - # When the user switches between notebook pages, scroll the visible - # page's textview to the bottom (otherwise it gets confusing) - self.output_notebook.connect( - 'switch-page', - self.on_output_notebook_switch_page, - ) - - - def setup_errors_tab(self): - - """Called by self.setup_win(). - - Creates widgets for the Errors/Warnings Tab. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1867 setup_errors_tab') - - vbox = Gtk.VBox() - self.errors_tab.pack_start(vbox, True, True, 0) - - # Errors List - frame = Gtk.Frame() - vbox.pack_start(frame, True, True, 0) - - self.errors_list_scrolled = Gtk.ScrolledWindow() - frame.add(self.errors_list_scrolled) - self.errors_list_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - self.errors_list_treeview = Gtk.TreeView() - 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']): - - if not column_title: - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - '', - renderer_pixbuf, - pixbuf=i, - ) - self.errors_list_treeview.append_column(column_pixbuf) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - column_title, - renderer_text, - text=i, - ) - self.errors_list_treeview.append_column(column_text) - - self.errors_list_liststore = Gtk.ListStore( - GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, - str, str, str, - ) - self.errors_list_treeview.set_model(self.errors_list_liststore) - - # Strip of widgets at the bottom - hbox = Gtk.HBox() - vbox.pack_start(hbox, False, False, self.spacing_size) - hbox.set_border_width(self.spacing_size) - - 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', - ) - self.show_system_error_checkbutton.set_active( - self.app_obj.system_error_show_flag, - ) - self.show_system_error_checkbutton.connect( - 'toggled', - self.on_system_error_checkbutton_changed, - ) - - 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', - ) - self.show_system_warning_checkbutton.set_active( - self.app_obj.system_warning_show_flag, - ) - self.show_system_warning_checkbutton.connect( - 'toggled', - self.on_system_warning_checkbutton_changed, - ) - - 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', - ) - self.show_operation_error_checkbutton.set_active( - self.app_obj.operation_error_show_flag, - ) - self.show_operation_error_checkbutton.connect( - 'toggled', - self.on_operation_error_checkbutton_changed, - ) - - self.show_operation_warning_checkbutton = Gtk.CheckButton() - hbox.pack_start( - self.show_operation_warning_checkbutton, - False, - False, - 0, - ) - self.show_operation_warning_checkbutton.set_label( - 'Show server warnings', - ) - self.show_operation_warning_checkbutton.set_active( - self.app_obj.operation_warning_show_flag, - ) - self.show_operation_warning_checkbutton.connect( - 'toggled', - self.on_operation_warning_checkbutton_changed, - ) - - 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.connect( - 'clicked', - self.on_errors_list_clear, - ) - - - # (Moodify main window widgets) - - - def toggle_visibility(self): - - """Called by self.on_delete_event, StatusIcon.on_button_press_event and - mainapp.TartubeApp.on_menu_close_tray(). - - Toggles the main window's visibility (usually after the user has left- - clicked the status icon in the system tray). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 1997 toggle_visibility') - - if self.is_visible(): - self.set_visible(False) - else: - self.set_visible(True) - - - 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. - - 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') - - self.setup_main_toolbar() - - if self.app_obj.disable_dl_all_flag: - self.download_all_menu_item.set_sensitive(False) - - self.show_all() - - - def sensitise_widgets_if_database(self, sens_flag): - - """Called by mainapp.TartubeApp.start(), .load_db(), .save_db() and - .disable_load_save(). - - When no database file has been loaded into memory, most main window - widgets should be desensitised. This function is called to sensitise - or desensitise the widgets after a change in state. - - Args: - - sens_flag (bool): True to sensitise most widgets, False to - desensitise most widgets - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2042 sensitise_widgets_if_database') - - # Menu items - self.change_db_menu_item.set_sensitive(sens_flag) - self.save_db_menu_item.set_sensitive(sens_flag) - self.save_all_menu_item.set_sensitive(sens_flag) - - self.system_prefs_menu_item.set_sensitive(sens_flag) - self.gen_options_menu_item.set_sensitive(sens_flag) - - self.add_video_menu_item.set_sensitive(sens_flag) - self.add_channel_menu_item.set_sensitive(sens_flag) - self.add_playlist_menu_item.set_sensitive(sens_flag) - self.add_folder_menu_item.set_sensitive(sens_flag) - - self.export_db_menu_item.set_sensitive(sens_flag) - self.import_db_menu_item.set_sensitive(sens_flag) - self.switch_view_menu_item.set_sensitive(sens_flag) - self.show_hidden_menu_item.set_sensitive(sens_flag) - - self.check_all_menu_item.set_sensitive(sens_flag) - self.download_all_menu_item.set_sensitive(sens_flag) - self.refresh_db_menu_item.set_sensitive(sens_flag) - - if __main__.__pkg_strict_install_flag__: - self.update_ytdl_menu_item.set_sensitive(False) - else: - self.update_ytdl_menu_item.set_sensitive(sens_flag) - - self.test_ytdl_menu_item.set_sensitive(sens_flag) - - if os.name != 'nt': - self.install_ffmpeg_menu_item.set_sensitive(False) - else: - self.install_ffmpeg_menu_item.set_sensitive(sens_flag) - - self.stop_operation_menu_item.set_sensitive(False) - - if self.test_menu_item: - self.test_menu_item.set_sensitive(sens_flag) - - # Toolbuttons - self.add_video_toolbutton.set_sensitive(sens_flag) - self.add_channel_toolbutton.set_sensitive(sens_flag) - self.add_playlist_toolbutton.set_sensitive(sens_flag) - self.add_folder_toolbutton.set_sensitive(sens_flag) - - self.check_all_toolbutton.set_sensitive(sens_flag) - self.download_all_toolbutton.set_sensitive(sens_flag) - self.stop_operation_toolbutton.set_sensitive(False) - self.switch_view_toolbutton.set_sensitive(sens_flag) - - if self.test_toolbutton: - self.test_toolbutton.set_sensitive(sens_flag) - - # Videos Tab - if self.check_media_button: - self.check_media_button.set_sensitive(sens_flag) - if self.download_media_button: - if self.app_obj.disable_dl_all_flag: - self.download_media_button.set_sensitive(False) - else: - self.download_media_button.set_sensitive(sens_flag) - - # Progress tab - self.num_worker_checkbutton.set_sensitive(sens_flag) - self.num_worker_spinbutton.set_sensitive(sens_flag) - self.bandwidth_checkbutton.set_sensitive(sens_flag) - self.bandwidth_spinbutton.set_sensitive(sens_flag) - self.video_res_checkbutton.set_sensitive(sens_flag) - self.video_res_combobox.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) - self.show_operation_error_checkbutton.set_sensitive(sens_flag) - self.show_operation_warning_checkbutton.set_sensitive(sens_flag) - - - def desensitise_test_widgets(self): - - """Called by mainapp.TartubeApp.on_menu_test(). - - Clicking the Test menu item / toolbutton more than once just adds - illegal duplicate channels/playlists/folders (and non-illegal duplicate - videos), so this function is called to just disable both widgets. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2131 desensitise_test_widgets') - - if self.test_menu_item: - self.test_menu_item.set_sensitive(False) - if self.test_toolbutton: - self.test_toolbutton.set_sensitive(False) - - - def sensitise_operation_widgets(self, sens_flag, \ - not_dl_operation_flag=False): - - """Can by called by anything. - - (De)sensitises widgets that must not be sensitised during a download/ - update/refresh/info/tidy operation. - - Args: - - sens_flag (bool): False to desensitise widget at the start of an - operation, True to re-sensitise widgets at the end of the - operation - - not_dl_operation_flag (True, False or None): False when called by - download operation functions, True when called by everything - else - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2160 sensitise_operation_widgets') - - self.system_prefs_menu_item.set_sensitive(sens_flag) - self.gen_options_menu_item.set_sensitive(sens_flag) - self.export_db_menu_item.set_sensitive(sens_flag) - self.import_db_menu_item.set_sensitive(sens_flag) - self.check_all_menu_item.set_sensitive(sens_flag) - - if not self.app_obj.disable_dl_all_flag: - self.download_all_menu_item.set_sensitive(sens_flag) - else: - self.download_all_menu_item.set_sensitive(False) - - self.refresh_db_menu_item.set_sensitive(sens_flag) - self.check_all_toolbutton.set_sensitive(sens_flag) - - if not self.app_obj.disable_dl_all_flag: - self.download_all_toolbutton.set_sensitive(sens_flag) - else: - self.download_all_toolbutton.set_sensitive(False) - - if not __main__.__pkg_strict_install_flag__: - self.update_ytdl_menu_item.set_sensitive(sens_flag) - - self.test_ytdl_menu_item.set_sensitive(sens_flag) - self.install_ffmpeg_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) - if not_dl_operation_flag: - self.add_video_menu_item.set_sensitive(sens_flag) - self.add_channel_menu_item.set_sensitive(sens_flag) - self.add_playlist_menu_item.set_sensitive(sens_flag) - self.add_folder_menu_item.set_sensitive(sens_flag) - self.add_video_toolbutton.set_sensitive(sens_flag) - self.add_channel_toolbutton.set_sensitive(sens_flag) - self.add_playlist_toolbutton.set_sensitive(sens_flag) - self.add_folder_toolbutton.set_sensitive(sens_flag) - - # (The 'Change database', etc menu items must remain desensitised if - # file load/save is disabled) - if not self.app_obj.disable_load_save_flag: - self.change_db_menu_item.set_sensitive(sens_flag) - self.save_db_menu_item.set_sensitive(sens_flag) - self.save_all_menu_item.set_sensitive(sens_flag) - - # (The 'Stop' button/menu item are only sensitised during a download/ - # update/refresh/info/tidy operation) - if not sens_flag: - self.stop_operation_menu_item.set_sensitive(True) - self.stop_operation_toolbutton.set_sensitive(True) - else: - self.stop_operation_menu_item.set_sensitive(False) - self.stop_operation_toolbutton.set_sensitive(False) - - - def show_progress_bar(self, operation_type): - - """Called by mainapp.TartubeApp.download_manager_continue(), - .refresh_manager_continue(), .tidy_manager_start(). - - At the start of a download/refresh/tidy operation, replace - self.download_media_button with a progress bar (and a label just above - it). - - Args: - - operation_type (str): The type of operation: 'download' for a - download operation, 'check' for a download operation with - simulated downloads, 'refresh' for a refresh operation, or - 'tidy' for a tidy operation - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2236 show_progress_bar') - - if self.progress_bar: - return self.app_obj.system_error( - 201, - 'Videos Tab progress bar is already visible', - ) - - elif operation_type != 'check' \ - and operation_type != 'download' \ - and operation_type != 'refresh' \ - and operation_type != 'tidy': - return self.app_obj.system_error( - 202, - 'Invalid operation type supplied to progress bar', - ) - - # Remove existing widgets. In previous code, we simply changed the - # label on on self.check_media_button, but this causes frequent - # crashes - # Get around the crashes by destroying the old widgets and creating new - # ones - self.button_box.remove(self.check_media_button) - self.check_media_button = None - self.button_box.remove(self.download_media_button) - self.download_media_button = None - - # 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_action_name('app.check_all_button') - self.check_media_button.set_sensitive(False) - if operation_type == 'check': - self.check_media_button.set_label('Checking...') - elif operation_type == 'download': - self.check_media_button.set_label('Downloading...') - elif operation_type == 'refresh': - self.check_media_button.set_label('Refreshing...') - else: - 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) - self.progress_box = Gtk.HBox() - self.button_box.pack_start( - self.progress_box, - True, - True, - (self.spacing_size * 2), - ) - - self.progress_bar = Gtk.ProgressBar() - self.progress_box.pack_start( - self.progress_bar, - True, - True, - (self.spacing_size * 2), - ) - self.progress_bar.set_fraction(0) - self.progress_bar.set_show_text(True) - if operation_type == 'check': - self.progress_bar.set_text('Checking...') - elif operation_type == 'download': - self.progress_bar.set_text('Downloading...') - elif operation_type == 'refresh': - self.progress_bar.set_text('Refreshing...') - else: - self.progress_bar.set_text('Tidying...') - - # Make the changes visible - self.button_box.show_all() - - - def hide_progress_bar(self): - - """Called by mainapp.TartubeApp.download_manager_finished(), - .refresh_manager_finished() and .tidy_manager_finished(). - - At the end of a download operation, replace self.progress_list with the - original button. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2319 hide_progress_bar') - - if not self.progress_bar: - return self.app_obj.system_error( - 203, - 'Videos Tab progress bar is not already visible', - ) - - # Remove existing widgets. In previous code, we simply changed the - # label on on self.check_media_button, but this causes frequent - # crashes - # Get around the crashes by destroying the old widget and creating a - # new one - self.button_box.remove(self.check_media_button) - self.check_media_button = None - self.button_box.remove(self.progress_box) - self.progress_box = None - self.progress_bar = None - - # 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_tooltip_text( - '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_tooltip_text( - 'Download all videos, channels, playlists and folders', - ) - self.download_media_button.set_action_name('app.download_all_button') - - # (For some reason, the button must be desensitised after setting the - # action name) - if not self.app_obj.disable_dl_all_flag: - self.download_media_button.set_sensitive(True) - else: - self.download_media_button.set_sensitive(False) - - # Make the changes visible - self.button_box.show_all() - - - def update_progress_bar(self, text, count, total): - - """Called by downloads.DownloadManager.run(), - refresh.RefreshManager.refresh_from_default_destination(), - .refresh_from_actual_destination() and - tidy.TidyManager.tidy_directory(). - - During a download/refresh/tidy operation, updates the progress bar just - below the Video Index. - - Args: - - text (str): The text of the progress bar's label, matching the name - of the media data object which has just been passed to - youtube-dl - - count (int): The number of media data objects passed to youtube-dl - so far. Note that a channel or a playlist counts as one media - data object, as far as youtube-dl is concerned - - total (int): The total number of media data objects to be passed - to youtube-dl - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2392 update_progress_bar') - - if not self.progress_bar: - return self.app_obj.system_error( - 204, - 'Videos Tab progress bar is missing and cannot be updated', - ) - - # (The 0.5 guarantees that the progress bar is never empty. If - # downloading a single video, the progress bar is half full. If - # downloading the first out of 3 videos, it is 16% full, and so on) - self.progress_bar.set_fraction(float(count - 0.5) / total) - self.progress_bar.set_text( - utils.shorten_string(text, self.short_string_max_len) \ - + ' ' + str(count) + '/' + str(total) - ) - - - def sensitise_check_dl_buttons(self, finish_flag, operation_type=None): - - """Called by mainapp.TartubeApp.update_manager_start(), - .update_manager_finished(), .info_manager_start() and - .info_manager_finished(). - - Modify and de(sensitise) widgets during an update or info operation. - - Args: - - finish_flag (bool): False at the start of the update operation, - True at the end of it - - operation_type (str): 'ffmpeg' for an update operation to install - FFmpeg, 'ytdl' for an update operation to install/update - youtube-dl, 'formats' for an info operation to fetch available - video formats, 'subs' for an info operation to fetch - available subtitles, 'test_ytdl' for an info operation in which - youtube-dl is tested, or None when finish_flag is True - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2433 sensitise_check_dl_buttons') - - if operation_type is not None \ - and operation_type != 'ffmpeg' and operation_type != 'ytdl' \ - and operation_type != 'formats' and operation_type != 'subs' \ - and operation_type != 'test_ytdl': - return self.app_obj.system_error( - 205, - 'Invalid update/info operation argument', - ) - - # Remove existing widgets. In previous code, we simply changed the - # label on on self.check_media_button, but this causes frequent - # crashes - # Get around the crashes by destroying the old widgets and creating new - # ones - self.button_box.remove(self.check_media_button) - self.check_media_button = None - self.button_box.remove(self.download_media_button) - self.download_media_button = None - - # 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_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_action_name('app.download_all_button') - - if not finish_flag: - - if operation_type == 'ffmpeg': - self.check_media_button.set_label('Installing') - self.download_media_button.set_label('FFmpeg') - elif operation_type == 'ytdl': - self.check_media_button.set_label('Updating') - self.download_media_button.set_label('youtube-dl') - elif operation_type == 'formats': - 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.download_media_button.set_label('subtitle list') - else: - self.check_media_button.set_label('Testing') - self.download_media_button.set_label('youtube-dl') - - self.check_media_button.set_sensitive(False) - self.download_media_button.set_sensitive(False) - - self.sensitise_operation_widgets(False, True) - - else: - 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', - ) - - self.download_media_button.set_label('Download all') - - self.download_media_button.set_tooltip_text( - 'Download all videos, channels, playlists and folders', - ) - - if not self.app_obj.disable_dl_all_flag: - self.download_media_button.set_sensitive(True) - else: - self.download_media_button.set_sensitive(False) - - self.sensitise_operation_widgets(True, True) - - # Make the widget changes visible - self.show_all() - - - 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). - - Args: - - update_catalogue_flag (bool): True when called by - .set_show_tooltips_flag(), in which case the Video Catalogue - must be redrawn - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2525 enable_tooltips') - - # Update the Video Index - self.video_index_treeview.set_tooltip_column( - self.video_index_tooltip_column, - ) - - # Update the Video Catalogue, if a playlist/channel/folder is selected - if update_catalogue_flag and self.video_index_current: - self.video_catalogue_redraw_all( - self.video_index_current, - self.catalogue_toolbar_current_page, - ) - - # Update the Progress List - self.progress_list_treeview.set_tooltip_column( - self.progress_list_tooltip_column, - ) - - # Update the Results List - self.results_list_treeview.set_tooltip_column( - self.results_list_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). - - Args: - - update_catalogue_flag (bool): True when called by - .set_show_tooltips_flag(), in which case the Video Catalogue - must be redrawn - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2566 disable_tooltips') - - # Update the Video Index. Using a dummy column makes the tooltips - # invisible - self.video_index_treeview.set_tooltip_column(-1) - - # Update the Video Catalogue, if a playlist/channel/folder is selected - if update_catalogue_flag and self.video_index_current: - self.video_catalogue_redraw_all( - self.video_index_current, - self.catalogue_toolbar_current_page, - ) - - # Update the Progress List - self.progress_list_treeview.set_tooltip_column(-1) - - # Update the Results List - self.results_list_treeview.set_tooltip_column(-1) - - - def enable_dl_all_buttons(self): - - """Called by mainapp.TartubeApp.set_disable_dl_all_flag(). - - Enables (sensitises) the 'Download all' buttons and menu items. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2594 enable_dl_all_buttons') - - # This setting doesn't apply during a download/update/refresh/info/tidy - # operation - if not self.app_obj.current_manager_obj: - self.download_all_menu_item.set_sensitive(True) - self.download_all_toolbutton.set_sensitive(True) - self.download_media_button.set_sensitive(True) - - - def disable_dl_all_buttons(self): - - """Called by mainapp.TartubeApp.load_config() 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') - - # This setting doesn't apply during a download/update/refresh/info/tidy - # operation - if not self.app_obj.current_manager_obj: - self.download_all_menu_item.set_sensitive(False) - self.download_all_toolbutton.set_sensitive(False) - 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): - - """Can be called by anything. - - Creates a desktop notification. - - Args: - - title (str): The notification title. If None, __prettyname__ is - used - - msg (str): The message to show. If None, __prettyname__ is used - - icon_path (str): The absolute path to the icon file to use. If - None, a default icon is used - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2673 notify_desktop') - - # Desktop notifications don't work on MS Windows - if os.name != 'nt': - - if title is None: - title = __main__.__prettyname__ - - if msg is None: - # Emergency fallback - better than an empty message - msg = __main__.__prettyname__ - - if icon_path is None: - icon_path = os.path.abspath( - os.path.join( - self.icon_dir_path, - 'dialogue', - formats.DIALOGUE_ICON_DICT['system_icon'], - ), - ) - - notify_obj = Notify.Notification.new(title, msg, icon_path) - notify_obj.show() - - - def update_show_filter_widgets(self): - - """Called by mainapp.TartubeApp.load_config() 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, - as required. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2709 update_show_filter_widgets') - - if not self.app_obj.catalogue_show_filter_flag: - - # Hide the second row - self.catalogue_show_filter_button.set_stock_id( - Gtk.STOCK_SORT_ASCENDING, - ) - - self.catalogue_show_filter_button.set_tooltip_text( - 'Show filter options', - ) - - if self.catalogue_toolbar2 \ - in self.catalogue_toolbar_vbox.get_children(): - self.catalogue_toolbar_vbox.remove(self.catalogue_toolbar2) - self.catalogue_toolbar_vbox.show_all() - - else: - - # Show the second row - self.catalogue_show_filter_button.set_stock_id( - Gtk.STOCK_SORT_DESCENDING, - ) - - self.catalogue_show_filter_button.set_tooltip_text( - 'Hide filter options', - ) - - if not self.catalogue_toolbar2 \ - in self.catalogue_toolbar_vbox.get_children(): - self.catalogue_toolbar_vbox.pack_start( - self.catalogue_toolbar2, - False, - False, - 0, - ) - - self.catalogue_toolbar_vbox.show_all() - - # After the parent self.catalogue_toolbar2 is added to its - # VBox, the 'Regex' button is not desensitised correctly - # (for reasons unknown) - # Desensitise it, if it should be desensitised - if self.video_index_current is None \ - or not self.video_catalogue_dict: - self.catalogue_regex_togglebutton.set_sensitive(False) - - - def update_alpha_sort_widgets(self): - - """Called by mainapp.TartubeApp.load_config() 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 - widgets themselves. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2769 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') - - else: - self.catalogue_sort_button.set_stock_id( - Gtk.STOCK_INDEX, - ) - - self.catalogue_sort_button.set_tooltip_text('Sort by date') - - - def update_use_regex_widgets(self): - - """Called by mainapp.TartubeApp.load_config(). - - After loading the config file, toggle the 'Regex' button in the toolbar - just below the Video Catalogue. - """ - - 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(). - - Automatically sorts rows in the Video Index. - - Args: - - treestore (Gtk.TreeStore): Rows in the Video Index are stored in - this treestore. - - row_iter1, row_iter2 (Gtk.TreeIter): Iters pointing at two rows - in the treestore, one of which must be sorted before the other - - data (None): Ignored - - Returns: - -1 if row_iter1 comes before row_iter2, 1 if row_iter2 comes before - row_iter1, 0 if their order should not be changed - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2829 video_index_auto_sort') - - # If auto-sorting is disabled temporarily, we can prevent the list - # being sorted by returning -1 for all cases - if self.video_index_no_sort_flag: - return -1 - - # Get the names of the media data objects on each row - sort_column, sort_type \ - = self.video_index_sortmodel.get_sort_column_id() - name1 = treestore.get_value(row_iter1, sort_column) - name2 = treestore.get_value(row_iter2, sort_column) - - # Get corresponding media data objects - id1 = self.app_obj.media_name_dict[name1] - obj1 = self.app_obj.media_reg_dict[id1] - - id2 = self.app_obj.media_name_dict[name2] - obj2 = self.app_obj.media_reg_dict[id2] - - # Do sort. Treat media.Channel and media.Playlist objects as the same - # type of thing, so that all folders appear first (sorted - # alphabetically), followed by all channels/playlists (sorted - # alphabetically) - if str(obj1.__class__) == str(obj2.__class__) \ - or ( - isinstance(obj1, media.GenericRemoteContainer) \ - and isinstance(obj2, media.GenericRemoteContainer) - ): - # Private folders are shown first, then (public) fixed folders, - # then user-created folders - if isinstance(obj1, media.Folder): - if obj1.priv_flag and not obj2.priv_flag: - return -1 - elif not obj1.priv_flag and obj2.priv_flag: - return 1 - elif obj1.fixed_flag and not obj2.fixed_flag: - return -1 - elif not obj1.fixed_flag and obj2.fixed_flag: - return 1 - - # Media data objects can't have the same name, but they might have - # the same nickname - # If two nicknames both start with an index, e.g. '1 Music' and - # '11 Comedy' then make sure the one with the lowest index comes - # first - index1_list = re.findall(r'^(\d+)', obj1.nickname) - index2_list = re.findall(r'^(\d+)', obj2.nickname) - if index1_list and index2_list: - if int(index1_list[0]) < int(index2_list[0]): - return -1 - else: - return 1 - elif obj1.nickname.lower() < obj2.nickname.lower(): - return -1 - else: - return 1 - - else: - - # (Folders displayed first, channels/playlists next, and of course - # videos aren't displayed here at all) - if isinstance(obj1, media.Folder): - return -1 - elif isinstance(obj2, media.Folder): - return 1 - else: - return 0 - - - def video_catalogue_auto_sort(self, row1, row2, data, notify): - - """Sorting function created by self.video_catalogue_reset(). - - Automatically sorts rows in the Video Catalogue, by date (default) or - alphabetically, depending on settings. - - Args: - - row1, row2 (mainwin.CatalogueRow): Two rows in the liststore, one - of which must be sorted before the other - - data (None): Ignored - - notify (False): Ignored - - Returns: - -1 if row1 comes before row2, 1 if row2 comes before row1, 0 if - their order should not be changed - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 2922 video_catalogue_auto_sort') - - # Get the media.Video objects displayed on each row - obj1 = row1.video_obj - obj2 = row2.video_obj - - # 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 - # 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) \ - 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: - if obj1.index < obj2.index: - return -1 - else: - return 1 - elif obj1.upload_time is not None and obj2.upload_time is not None: - if obj1.upload_time > obj2.upload_time: - return -1 - elif obj1.upload_time < obj2.upload_time: - return 1 - elif obj1.receive_time is not None \ - and obj2.receive_time is not None: - # In private folders, the most recently received video goes - # to the top of the list - if self.video_index_current_priv_flag: - if obj1.receive_time > obj2.receive_time: - return -1 - elif obj1.receive_time < obj2.receive_time: - return 1 - else: - return 0 - # ...but for everything else, the sorting algorithm is the - # same as for media.GenericRemoteContainer.do_sort(), in - # which we assume the website is sending us videos, - # newest first - else: - if obj1.receive_time < obj2.receive_time: - return -1 - elif obj1.receive_time > obj2.receive_time: - return 1 - else: - return 0 - else: - return 0 - else: - return 0 - - # Sort alphabetically - else: - if obj1.name.lower() < obj2.name.lower(): - return -1 - elif obj1.name.lower() > obj2.name.lower(): - return 1 - else: - return 0 - - - # (Popup menu functions for main window widgets) - - - 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. - - Args: - - event (Gdk.EventButton): The mouse click event - - name (str): The name of the clicked media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 3004 video_index_popup_menu') - - # Find the right-clicked media data object (and a string to describe - # its type) - dbid = self.app_obj.media_name_dict[name] - media_data_obj = self.app_obj.media_reg_dict[dbid] - media_type = media_data_obj.get_type() - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Check/download/refresh items - check_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Check ' + media_type, - ) - check_menu_item.connect( - 'activate', - self.on_video_index_check, - media_data_obj, - ) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ): - check_menu_item.set_sensitive(False) - popup_menu.append(check_menu_item) - - download_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Download ' + media_type, - ) - download_menu_item.connect( - 'activate', - self.on_video_index_download, - media_data_obj, - ) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_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 ' + media_type, - ) - custom_dl_menu_item.connect( - 'activate', - self.on_video_index_custom_dl, - media_data_obj, - ) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ): - custom_dl_menu_item.set_sensitive(False) - popup_menu.append(custom_dl_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Contents - contents_submenu = Gtk.Menu() - - if not isinstance(media_data_obj, media.Folder): - - self.video_index_setup_contents_submenu( - contents_submenu, - media_data_obj, - False, - ) - - else: - - # All contents - all_contents_submenu = Gtk.Menu() - - self.video_index_setup_contents_submenu( - all_contents_submenu, - media_data_obj, - False, - ) - - # Separator - all_contents_submenu.append(Gtk.SeparatorMenuItem()) - - empty_folder_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Empty folder', - ) - empty_folder_menu_item.connect( - 'activate', - self.on_video_index_empty_folder, - media_data_obj, - ) - all_contents_submenu.append(empty_folder_menu_item) - if not media_data_obj.child_list or media_data_obj.priv_flag: - empty_folder_menu_item.set_sensitive(False) - - all_contents_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_All contents', - ) - all_contents_menu_item.set_submenu(all_contents_submenu) - contents_submenu.append(all_contents_menu_item) - - # Just folder videos - just_videos_submenu = Gtk.Menu() - - self.video_index_setup_contents_submenu( - just_videos_submenu, - media_data_obj, - True, - ) - - # Separator - just_videos_submenu.append(Gtk.SeparatorMenuItem()) - - empty_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Remove videos', - ) - empty_videos_menu_item.connect( - 'activate', - self.on_video_index_remove_videos, - media_data_obj, - ) - just_videos_submenu.append(empty_videos_menu_item) - if not media_data_obj.child_list or media_data_obj.priv_flag: - empty_videos_menu_item.set_sensitive(False) - - just_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_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', - ) - contents_menu_item.set_submenu(contents_submenu) - popup_menu.append(contents_menu_item) - if not media_data_obj.child_list: - contents_menu_item.set_sensitive(False) - - # Actions - actions_submenu = Gtk.Menu() - - move_top_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Move to top level', - ) - move_top_menu_item.connect( - 'activate', - self.on_video_index_move_to_top, - media_data_obj, - ) - actions_submenu.append(move_top_menu_item) - if not media_data_obj.parent_obj \ - or self.app_obj.current_manager_obj: - move_top_menu_item.set_sensitive(False) - - # Separator - 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' - else: - convert_text = None - - if convert_text: - - convert_menu_item = Gtk.MenuItem.new_with_mnemonic(convert_text) - convert_menu_item.connect( - 'activate', - self.on_video_index_convert_container, - media_data_obj, - ) - actions_submenu.append(convert_menu_item) - if self.app_obj.current_manager_obj: - convert_menu_item.set_sensitive(False) - - # Separator - actions_submenu.append(Gtk.SeparatorMenuItem()) - - if isinstance(media_data_obj, media.Folder): - - hide_folder_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Hide folder', - ) - hide_folder_menu_item.connect( - 'activate', - self.on_video_index_hide_folder, - media_data_obj, - ) - actions_submenu.append(hide_folder_menu_item) - - rename_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Rename ' + media_type + '...', - ) - rename_location_menu_item.connect( - 'activate', - self.on_video_index_rename_location, - media_data_obj, - ) - actions_submenu.append(rename_location_menu_item) - if self.app_obj.current_manager_obj or self.config_win_list \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag - ): - rename_location_menu_item.set_sensitive(False) - - set_nickname_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Set _nickname...', - ) - set_nickname_menu_item.connect( - 'activate', - self.on_video_index_set_nickname, - media_data_obj, - ) - actions_submenu.append(set_nickname_menu_item) - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: - set_nickname_menu_item.set_sensitive(False) - - set_destination_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Set _download destination...', - ) - set_destination_menu_item.connect( - 'activate', - self.on_video_index_set_destination, - media_data_obj, - ) - actions_submenu.append(set_destination_menu_item) - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag: - set_destination_menu_item.set_sensitive(False) - - # Separator - actions_submenu.append(Gtk.SeparatorMenuItem()) - - export_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Export ' + media_type + '...', - ) - export_menu_item.connect( - 'activate', - self.on_video_index_export, - media_data_obj, - ) - actions_submenu.append(export_menu_item) - 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, - ) - refresh_menu_item.connect( - 'activate', - self.on_video_index_refresh, - media_data_obj, - ) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ): - refresh_menu_item.set_sensitive(False) - actions_submenu.append(refresh_menu_item) - - tidy_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Tidy up ' + media_type, - ) - tidy_menu_item.connect( - 'activate', - self.on_video_index_tidy, - media_data_obj, - ) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag - ): - 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', - ) - actions_menu_item.set_submenu(actions_submenu) - popup_menu.append(actions_menu_item) - - # Apply/remove/edit download options, disable downloads - downloads_submenu = Gtk.Menu() - - # (Desensitise these menu items, if an edit window is already open) - no_options_flag = False - for win_obj in self.config_win_list: - if isinstance(win_obj, config.OptionsEditWin) \ - and media_data_obj.options_obj == win_obj.edit_obj: - no_options_flag = True - break - - if not media_data_obj.options_obj: - - apply_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Apply download options...', - ) - apply_options_menu_item.connect( - 'activate', - self.on_video_index_apply_options, - media_data_obj, - ) - downloads_submenu.append(apply_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ): - apply_options_menu_item.set_sensitive(False) - - else: - - remove_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Remove download options', - ) - remove_options_menu_item.connect( - 'activate', - self.on_video_index_remove_options, - media_data_obj, - ) - downloads_submenu.append(remove_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) - and media_data_obj.priv_flag - ): - remove_options_menu_item.set_sensitive(False) - - edit_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Edit download options...', - ) - edit_options_menu_item.connect( - 'activate', - self.on_video_index_edit_options, - media_data_obj, - ) - downloads_submenu.append(edit_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj \ - or not media_data_obj.options_obj: - edit_options_menu_item.set_sensitive(False) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - show_system_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Show system command', - ) - show_system_menu_item.connect( - 'activate', - self.on_video_index_show_system_cmd, - media_data_obj, - ) - downloads_submenu.append(show_system_menu_item) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - disable_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - '_Disable checking/downloading', - ) - disable_menu_item.set_active(media_data_obj.dl_disable_flag) - disable_menu_item.connect( - 'activate', - self.on_video_index_dl_disable, - media_data_obj, - ) - downloads_submenu.append(disable_menu_item) - # (Widget sensitivity set below) - - enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - '_Just disable downloading', - ) - enforce_check_menu_item.set_active(media_data_obj.dl_sim_flag) - enforce_check_menu_item.connect( - 'activate', - self.on_video_index_enforce_check, - media_data_obj, - ) - downloads_submenu.append(enforce_check_menu_item) - if self.app_obj.current_manager_obj or media_data_obj.dl_disable_flag \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag - ): - enforce_check_menu_item.set_sensitive(False) - - # (Widget sensitivity from above) - if self.app_obj.current_manager_obj \ - or ( - isinstance(media_data_obj, media.Folder) \ - and media_data_obj.fixed_flag - ): - 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.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...', - ) - show_properties_menu_item.connect( - 'activate', - self.on_video_index_show_properties, - media_data_obj, - ) - show_submenu.append(show_properties_menu_item) - if self.app_obj.current_manager_obj: - show_properties_menu_item.set_sensitive(False) - - # Separator - show_submenu.append(Gtk.SeparatorMenuItem()) - - show_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Default location', - ) - show_location_menu_item.connect( - 'activate', - self.on_video_index_show_location, - media_data_obj, - ) - show_submenu.append(show_location_menu_item) - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: - show_location_menu_item.set_sensitive(False) - - show_destination_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Actual location', - ) - show_destination_menu_item.connect( - 'activate', - self.on_video_index_show_destination, - media_data_obj, - ) - show_submenu.append(show_destination_menu_item) - if isinstance(media_data_obj, media.Folder) \ - 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.set_submenu(show_submenu) - popup_menu.append(show_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Delete items - delete_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'D_elete ' + media_type, - ) - delete_menu_item.connect( - 'activate', - self.on_video_index_delete_container, - media_data_obj, - ) - if self.app_obj.current_manager_obj: - delete_menu_item.set_sensitive(False) - popup_menu.append(delete_menu_item) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - 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- - sensitive popup menu. - - Args: - - event (Gdk.EventButton): The mouse click event - - video_obj (media.Video): The video object displayed in the clicked - row - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 3502 video_catalogue_popup_menu') - - # Use a different popup menu for multiple selected rows - # Because of Gtk weirdness, check that the clicked row is actually - # one of those selected - catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid] - row_list = self.catalogue_listbox.get_selected_rows() - if catalogue_item_obj.catalogue_row in row_list \ - and len(row_list) > 1: - - return self.video_catalogue_multi_popup_menu(event, row_list) - - else: - - # Otherwise, right-clicking a row selects (and unselects everything - # else) - self.catalogue_listbox.unselect_all() - self.catalogue_listbox.select_row(catalogue_item_obj.catalogue_row) - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Check/download videos - check_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Check video' - ) - check_menu_item.connect( - 'activate', - self.on_video_catalogue_check, - video_obj, - ) - if self.app_obj.current_manager_obj: - 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_menu_item.connect( - 'activate', - self.on_video_catalogue_download, - video_obj, - ) - if self.app_obj.current_manager_obj: - 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' - ) - download_menu_item.connect( - 'activate', - self.on_video_catalogue_re_download, - video_obj, - ) - if self.app_obj.current_manager_obj: - 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' - ) - custom_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_custom_dl, - video_obj, - ) - if self.app_obj.current_manager_obj: - custom_dl_menu_item.set_sensitive(False) - popup_menu.append(custom_dl_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Watch video - self.add_watch_video_menu_items(popup_menu, video_obj) - - # Apply/remove/edit download options, show system command, disable - # downloads - downloads_submenu = Gtk.Menu() - - # (Desensitise these menu items, if an edit window is already open) - no_options_flag = False - for win_obj in self.config_win_list: - if isinstance(win_obj, config.OptionsEditWin) \ - and video_obj.options_obj == win_obj.edit_obj: - no_options_flag = True - break - - if not video_obj.options_obj: - - apply_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Apply download options...', - ) - apply_options_menu_item.connect( - 'activate', - self.on_video_catalogue_apply_options, - video_obj, - ) - downloads_submenu.append(apply_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj: - apply_options_menu_item.set_sensitive(False) - - else: - - remove_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Remove download options', - ) - remove_options_menu_item.connect( - 'activate', - self.on_video_catalogue_remove_options, - video_obj, - ) - downloads_submenu.append(remove_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj: - remove_options_menu_item.set_sensitive(False) - - edit_options_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Edit download options...', - ) - edit_options_menu_item.connect( - 'activate', - self.on_video_catalogue_edit_options, - video_obj, - ) - downloads_submenu.append(edit_options_menu_item) - if no_options_flag or self.app_obj.current_manager_obj \ - or not video_obj.options_obj: - edit_options_menu_item.set_sensitive(False) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - show_system_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Show system _command', - ) - show_system_menu_item.connect( - 'activate', - self.on_video_catalogue_show_system_cmd, - video_obj, - ) - downloads_submenu.append(show_system_menu_item) - - test_dl_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Test system command', - ) - test_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_test_dl, - video_obj, - ) - downloads_submenu.append(test_dl_menu_item) - if self.app_obj.current_manager_obj: - test_dl_menu_item.set_sensitive(False) - - # Separator - downloads_submenu.append(Gtk.SeparatorMenuItem()) - - enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - '_Disable downloads', - ) - enforce_check_menu_item.set_active(video_obj.dl_sim_flag) - enforce_check_menu_item.connect( - 'activate', - self.on_video_catalogue_enforce_check, - video_obj, - ) - downloads_submenu.append(enforce_check_menu_item) - # (Don't allow the user to change the setting of - # media.Video.dl_sim_flag if the video is in a channel or playlist, - # since media.Channel.dl_sim_flag or media.Playlist.dl_sim_flag - # applies instead) - if self.app_obj.current_manager_obj \ - or not isinstance(video_obj.parent_obj, media.Folder): - enforce_check_menu_item.set_sensitive(False) - - downloads_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Down_loads', - ) - downloads_menu_item.set_submenu(downloads_submenu) - popup_menu.append(downloads_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Mark video - mark_video_submenu = Gtk.Menu() - - archive_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is _archived', - ) - archive_video_menu_item.set_active(video_obj.archive_flag) - archive_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_archived_video, - video_obj, - ) - mark_video_submenu.append(archive_video_menu_item) - if not video_obj.dl_flag: - archive_video_menu_item.set_sensitive(False) - - bookmark_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is _bookmarked', - ) - bookmark_video_menu_item.set_active(video_obj.bookmark_flag) - bookmark_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_bookmark_video, - video_obj, - ) - mark_video_submenu.append(bookmark_video_menu_item) - - fav_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is _favourite', - ) - fav_video_menu_item.set_active(video_obj.fav_flag) - fav_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_favourite_video, - video_obj, - ) - mark_video_submenu.append(fav_video_menu_item) - - new_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is _new', - ) - new_video_menu_item.set_active(video_obj.new_flag) - new_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_new_video, - video_obj, - ) - mark_video_submenu.append(new_video_menu_item) - if not video_obj.dl_flag: - new_video_menu_item.set_sensitive(False) - - playlist_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic( - 'Video is in _waiting list', - ) - playlist_video_menu_item.set_active(video_obj.waiting_flag) - playlist_video_menu_item.connect( - 'toggled', - self.on_video_catalogue_toggle_waiting_video, - video_obj, - ) - mark_video_submenu.append(playlist_video_menu_item) - - mark_video_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Mark video', - ) - mark_video_menu_item.set_submenu(mark_video_submenu) - popup_menu.append(mark_video_menu_item) - - # Show location/properties - show_submenu = Gtk.Menu() - - show_location_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Location', - ) - show_location_menu_item.connect( - 'activate', - self.on_video_catalogue_show_location, - video_obj, - ) - show_submenu.append(show_location_menu_item) - - show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Properties...', - ) - show_properties_menu_item.connect( - 'activate', - self.on_video_catalogue_show_properties, - video_obj, - ) - show_submenu.append(show_properties_menu_item) - if self.app_obj.current_manager_obj: - show_properties_menu_item.set_sensitive(False) - - show_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Show video', - ) - show_menu_item.set_submenu(show_submenu) - popup_menu.append(show_menu_item) - - # Fetch formats/subtitles - fetch_submenu = Gtk.Menu() - - fetch_formats_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Available _formats', - ) - fetch_formats_menu_item.connect( - 'activate', - self.on_video_catalogue_fetch_formats, - video_obj, - ) - fetch_submenu.append(fetch_formats_menu_item) - - fetch_subs_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Available _subtitles', - ) - fetch_subs_menu_item.connect( - 'activate', - self.on_video_catalogue_fetch_subs, - video_obj, - ) - fetch_submenu.append(fetch_subs_menu_item) - - fetch_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Fetch', - ) - fetch_menu_item.set_submenu(fetch_submenu) - popup_menu.append(fetch_menu_item) - if not video_obj.source or self.app_obj.current_manager_obj: - fetch_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Delete video - delete_menu_item = Gtk.MenuItem.new_with_mnemonic('D_elete video') - delete_menu_item.connect( - 'activate', - self.on_video_catalogue_delete_video, - video_obj, - ) - popup_menu.append(delete_menu_item) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - 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. - - Args: - - event (Gdk.EventButton): The mouse click event - - row_list (list): List of mainwin.CatalogueRow objects that are - currently selected (each one corresponding to a single - media.Video object) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 3856 video_catalogue_multi_popup_menu') - - # Convert row_list, a list of mainwin.CatalogueRow objects, into a - # list of media.Video objects - video_list = [] - for row in row_list: - video_list.append(row.video_obj) - - # So we can desensitise some menu items, work out in advance whether - # any of the selected videos are marked as downloaded, or have a - # source URL, or are in a temporary folder - dl_flag = False - for video_obj in video_list: - if video_obj.dl_flag: - dl_flag = True - break - - not_dl_flag = False - for video_obj in video_list: - if not video_obj.dl_flag: - not_dl_flag = True - break - - source_flag = False - for video_obj in video_list: - if video_obj.source is not None: - source_flag = True - break - - temp_folder_flag = False - for video_obj in video_list: - if isinstance(video_obj.parent_obj, media.Folder) \ - and video_obj.parent_obj.temp_flag: - temp_folder_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.connect( - 'activate', - self.on_video_catalogue_check_multi, - video_list, - ) - if self.app_obj.current_manager_obj: - check_menu_item.set_sensitive(False) - popup_menu.append(check_menu_item) - - download_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Download videos' - ) - download_menu_item.connect( - 'activate', - self.on_video_catalogue_download_multi, - video_list, - ) - if self.app_obj.current_manager_obj: - 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' - ) - custom_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_custom_dl_multi, - video_list, - ) - if self.app_obj.current_manager_obj: - custom_dl_menu_item.set_sensitive(False) - popup_menu.append(custom_dl_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Watch video in player/download and watch - if not_dl_flag: - - dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'D_ownload and watch', - ) - dl_watch_menu_item.connect( - 'activate', - self.on_video_catalogue_dl_and_watch_multi, - video_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: - dl_watch_menu_item.set_sensitive(False) - - else: - - watch_player_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch in _player', - ) - watch_player_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_video_multi, - video_list, - ) - popup_menu.append(watch_player_menu_item) - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _website', - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website_multi, - video_list, - ) - if not source_flag: - watch_website_menu_item.set_sensitive(False) - popup_menu.append(watch_website_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # 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_temp_dl_menu_item.connect( - 'activate', - self.on_video_catalogue_mark_temp_dl_multi, - video_list, - ) - 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_multi, - video_list, - 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_multi, - video_list, - 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) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Mark videos - mark_videos_submenu = Gtk.Menu() - - archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Archived' - ) - archive_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_archived_video_multi, - True, - video_list, - ) - if not dl_flag: - archive_menu_item.set_sensitive(False) - mark_videos_submenu.append(archive_menu_item) - - not_archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not a_rchived' - ) - not_archive_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_archived_video_multi, - False, - video_list, - ) - if not dl_flag: - not_archive_menu_item.set_sensitive(False) - mark_videos_submenu.append(not_archive_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Bookmarked' - ) - bookmark_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_bookmark_video_multi, - True, - video_list, - ) - if not dl_flag: - bookmark_menu_item.set_sensitive(False) - mark_videos_submenu.append(bookmark_menu_item) - - not_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not b_ookmarked' - ) - not_bookmark_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_bookmark_video_multi, - False, - video_list, - ) - if not dl_flag: - not_bookmark_menu_item.set_sensitive(False) - mark_videos_submenu.append(not_bookmark_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Favourite' - ) - fav_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_favourite_video_multi, - True, - video_list, - ) - if not dl_flag: - fav_menu_item.set_sensitive(False) - mark_videos_submenu.append(fav_menu_item) - - not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not fa_vourite' - ) - not_fav_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_favourite_video_multi, - False, - video_list, - ) - if not dl_flag: - not_fav_menu_item.set_sensitive(False) - mark_videos_submenu.append(not_fav_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - new_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_New' - ) - new_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_new_video_multi, - True, - video_list, - ) - if not dl_flag: - new_menu_item.set_sensitive(False) - mark_videos_submenu.append(new_menu_item) - - not_new_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not n_ew' - ) - not_new_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_new_video_multi, - False, - video_list, - ) - if not dl_flag: - not_new_menu_item.set_sensitive(False) - mark_videos_submenu.append(not_new_menu_item) - - # Separator - mark_videos_submenu.append(Gtk.SeparatorMenuItem()) - - playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'In _waiting list' - ) - playlist_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_waiting_video_multi, - True, - video_list, - ) - if not dl_flag: - playlist_menu_item.set_sensitive(False) - mark_videos_submenu.append(playlist_menu_item) - - not_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Not in w_aiting list' - ) - not_playlist_menu_item.connect( - 'activate', - self.on_video_catalogue_toggle_waiting_video_multi, - False, - video_list, - ) - if not dl_flag: - not_playlist_menu_item.set_sensitive(False) - mark_videos_submenu.append(not_playlist_menu_item) - - mark_videos_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Mark videos', - ) - mark_videos_menu_item.set_submenu(mark_videos_submenu) - popup_menu.append(mark_videos_menu_item) - - # Show properties - show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Show p_roperties...', - ) - show_properties_menu_item.connect( - 'activate', - self.on_video_catalogue_show_properties_multi, - video_list, - ) - popup_menu.append(show_properties_menu_item) - if self.app_obj.current_manager_obj: - show_properties_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Delete videos - delete_menu_item = Gtk.MenuItem.new_with_mnemonic('D_elete videos') - delete_menu_item.connect( - 'activate', - self.on_video_catalogue_delete_video_multi, - video_list, - ) - popup_menu.append(delete_menu_item) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - 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- - sensitive popup menu. - - Args: - - event (Gdk.EventButton): The mouse click event - - item_id (int): The .item_id of the clicked downloads.DownloadItem - object - - dbid (int): The .dbid of the corresponding media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4228 progress_list_popup_menu') - - # Find the downloads.VideoDownloader which is currently handling the - # clicked media data object (if any) - download_manager_obj = self.app_obj.download_manager_obj - download_list_obj = None - download_item_obj = None - worker_obj = None - video_downloader_obj = None - - if download_manager_obj: - - download_list_obj = download_manager_obj.download_list_obj - download_item_obj = download_list_obj.download_item_dict[item_id] - - for this_worker_obj in download_manager_obj.worker_list: - if this_worker_obj.running_flag \ - and this_worker_obj.download_item_obj == download_item_obj \ - and this_worker_obj.video_downloader_obj is not None: - worker_obj = this_worker_obj - video_downloader_obj = this_worker_obj.video_downloader_obj - break - - # Find the media data object itself. If the download operation has - # finished, the variables just above will not be set - media_data_obj = None - if dbid in self.app_obj.media_reg_dict: - media_data_obj = self.app_obj.media_reg_dict[dbid] - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Stop check/download - stop_now_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Stop now', - ) - stop_now_menu_item.connect( - 'activate', - self.on_progress_list_stop_now, - download_item_obj, - worker_obj, - video_downloader_obj, - ) - popup_menu.append(stop_now_menu_item) - if not download_manager_obj \ - or video_downloader_obj is None: - stop_now_menu_item.set_sensitive(False) - - stop_soon_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Stop after this _video', - ) - stop_soon_menu_item.connect( - 'activate', - self.on_progress_list_stop_soon, - download_item_obj, - worker_obj, - video_downloader_obj, - ) - popup_menu.append(stop_soon_menu_item) - if not download_manager_obj \ - or video_downloader_obj is None: - stop_soon_menu_item.set_sensitive(False) - - stop_all_soon_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Stop after these v_ideos', - ) - stop_all_soon_menu_item.connect( - 'activate', - self.on_progress_list_stop_all_soon, - ) - popup_menu.append(stop_all_soon_menu_item) - if not download_manager_obj: - stop_all_soon_menu_item.set_sensitive(False) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Check/download next/last - dl_next_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Download _next', - ) - dl_next_menu_item.connect( - 'activate', - self.on_progress_list_dl_next, - download_item_obj, - ) - popup_menu.append(dl_next_menu_item) - if not download_manager_obj or worker_obj: - dl_next_menu_item.set_sensitive(False) - - dl_last_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Download _last', - ) - dl_last_menu_item.connect( - 'activate', - self.on_progress_list_dl_last, - download_item_obj, - ) - popup_menu.append(dl_last_menu_item) - if not download_manager_obj or worker_obj: - dl_last_menu_item.set_sensitive(False) - - # Watch on website - if media_data_obj \ - and isinstance(media_data_obj, media.Video) \ - and media_data_obj.source: - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # For YouTube videos, offer three websites (as usual) - if utils.is_youtube(media_data_obj.source): - - watch_youtube_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _YouTube', - ) - watch_youtube_menu_item.connect( - 'activate', - self.on_progress_list_watch_website, - media_data_obj, - ) - popup_menu.append(watch_youtube_menu_item) - - watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _HookTube', - ) - watch_hooktube_menu_item.connect( - 'activate', - self.on_progress_list_watch_hooktube, - media_data_obj, - ) - popup_menu.append(watch_hooktube_menu_item) - - watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _Invidious', - ) - watch_invidious_menu_item.connect( - 'activate', - self.on_progress_list_watch_invidious, - media_data_obj, - ) - popup_menu.append(watch_invidious_menu_item) - - else: - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _Website', - ) - watch_website_menu_item.connect( - 'activate', - self.on_progress_list_watch_website, - media_data_obj, - ) - popup_menu.append(watch_website_menu_item) - - # Create the popup menu - popup_menu.show_all() - popup_menu.popup(None, None, None, None, event.button, event.time) - - - 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- - 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 4406 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: - return - - video_obj = self.app_obj.media_reg_dict[dbid] - if not isinstance(video_obj, media.Video): - return - - # Set up the popup menu - popup_menu = Gtk.Menu() - - # Watch video - self.add_watch_video_menu_items(popup_menu, video_obj) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Delete video - delete_menu_item = Gtk.MenuItem.new_with_mnemonic('_Delete video') - delete_menu_item.connect( - 'activate', - self.on_results_list_delete_video, - video_obj, - path, - ) - popup_menu.append(delete_menu_item) - if self.app_obj.current_manager_obj: - delete_menu_item.set_sensitive(False) - - # 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): - - """Called by self.video_index_popup_menu(). - - Sets up a submenu for handling the contents of a channel, playlist - or folder. - - Args: - - submenu (Gtk.Menu): The submenu to set up, currently empty - - media_data_obj (media.Channel, media.Playlist, media.Folder): The - channel, playlist or folder whose contents should be modified - by items in the sub-menu - - only_child_videos_flag (bool): Set to True when only a folder's - child videos (not anything in its child channels, playlists or - folders) should be modified by items in the sub-menu; False if - all child objects should be modified - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4466 video_index_setup_contents_submenu') - - mark_archived_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as _archived', - ) - mark_archived_menu_item.connect( - 'activate', - self.on_video_index_mark_archived, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_archived_menu_item) - - mark_not_archive_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as not a_rchived', - ) - mark_not_archive_menu_item.connect( - 'activate', - self.on_video_index_mark_not_archived, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_not_archive_menu_item) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as _bookmarked', - ) - mark_bookmark_menu_item.connect( - 'activate', - self.on_video_index_mark_bookmark, - media_data_obj, - ) - submenu.append(mark_bookmark_menu_item) - if media_data_obj == self.app_obj.fixed_bookmark_folder: - mark_bookmark_menu_item.set_sensitive(False) - - mark_not_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as not b_ookmarked', - ) - mark_not_bookmark_menu_item.connect( - 'activate', - self.on_video_index_mark_not_bookmark, - media_data_obj, - ) - submenu.append(mark_not_bookmark_menu_item) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as _favourite', - ) - mark_fav_menu_item.connect( - 'activate', - self.on_video_index_mark_favourite, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_fav_menu_item) - if media_data_obj == self.app_obj.fixed_fav_folder: - mark_fav_menu_item.set_sensitive(False) - - mark_not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as not fa_vourite', - ) - mark_not_fav_menu_item.connect( - 'activate', - self.on_video_index_mark_not_favourite, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_not_fav_menu_item) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_new_menu_item = Gtk.MenuItem.new_with_mnemonic('Mark as _new') - mark_new_menu_item.connect( - 'activate', - self.on_video_index_mark_new, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_new_menu_item) - 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.connect( - 'activate', - self.on_video_index_mark_not_new, - media_data_obj, - only_child_videos_flag, - ) - submenu.append(mark_old_menu_item) - - # Separator - submenu.append(Gtk.SeparatorMenuItem()) - - mark_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Mark as in _waiting list', - ) - mark_playlist_menu_item.connect( - 'activate', - self.on_video_index_mark_waiting, - media_data_obj, - ) - submenu.append(mark_playlist_menu_item) - if media_data_obj == self.app_obj.fixed_waiting_folder: - 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_not_playlist_menu_item.connect( - 'activate', - self.on_video_index_mark_not_waiting, - media_data_obj, - ) - submenu.append(mark_not_playlist_menu_item) - - - def add_watch_video_menu_items(self, popup_menu, video_obj): - - """Called by self.video_catalogue_popup_menu() and - self.results_list_popup_menu(). - - Adds common menu items to the popup menu. - - Args: - - popup_menu (Gtk.Menu): The popup menu - - video_obj (media.Video): The video object that was right-clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4607 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', - ) - dl_watch_menu_item.connect( - 'activate', - self.on_video_catalogue_dl_and_watch, - 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: - dl_watch_menu_item.set_sensitive(False) - - else: - - watch_player_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch in _player', - ) - watch_player_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_video, - video_obj, - ) - popup_menu.append(watch_player_menu_item) - - # Watch video online. For YouTube URLs, offer alternative websites - if not video_obj.source: - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _website', - ) - watch_website_menu_item.set_sensitive(False) - popup_menu.append(watch_website_menu_item) - - else: - - if not utils.is_youtube(video_obj.source): - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'Watch on _website', - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website, - video_obj, - ) - popup_menu.append(watch_website_menu_item) - - else: - - alt_submenu = Gtk.Menu() - - watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_YouTube', - ) - watch_website_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_website, - video_obj, - ) - alt_submenu.append(watch_website_menu_item) - - watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_HookTube', - ) - watch_hooktube_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_hooktube, - video_obj, - ) - alt_submenu.append(watch_hooktube_menu_item) - - watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic( - '_Invidious', - ) - watch_invidious_menu_item.connect( - 'activate', - self.on_video_catalogue_watch_invidious, - video_obj, - ) - alt_submenu.append(watch_invidious_menu_item) - - alt_menu_item = Gtk.MenuItem.new_with_mnemonic( - 'W_atch on', - ) - alt_menu_item.set_submenu(alt_submenu) - popup_menu.append(alt_menu_item) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # Download to Temporary Videos - 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 - ): - temp_menu_item.set_sensitive(False) - - - # (Video Index) - - - def video_index_catalogue_reset(self, reselect_flag=False): - - """Can be called by anything. - - A convenient way to redraw the Video Index and Video Catalogue with a - one-line call. - - Args: - - reselect_flag (bool): If True, the currently selected channel/ - playlist/folder in the Video Index is re-selected, which draws - any child videos in the Video Catalogue - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4772 video_index_catalogue_reset') - - video_index_current = self.video_index_current - - # Reset the Video Index and Video Catalogue - self.video_index_reset() - self.video_catalogue_reset() - self.video_index_populate() - - # Re-select the old selection, if required - if reselect_flag and video_index_current is not None: - - dbid = self.app_obj.media_name_dict[video_index_current] - self.video_index_select_row(self.app_obj.media_reg_dict[dbid]) - - - def video_index_reset(self): - - """Can be called by anything. - - On the first call, sets up the widgets for the Video Index. - - On subsequent calls, replaces those widgets, ready for them to be - filled with new data. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4799 video_index_reset') - - # Reset IVs - self.video_index_current = None - if self.video_index_treeview: - - self.video_index_row_dict = {} - - # Remove the old widgets - if self.video_index_frame.get_child(): - self.video_index_frame.remove( - self.video_index_frame.get_child(), - ) - - # Set up the widgets - self.video_index_scrolled = Gtk.ScrolledWindow() - self.video_index_frame.add(self.video_index_scrolled) - self.video_index_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - self.video_index_treeview = Gtk.TreeView() - 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) - self.video_index_treeview.set_tooltip_column( - self.video_index_tooltip_column, - ) - # (Detect right-clicks on the treeview) - self.video_index_treeview.connect( - 'button-press-event', - self.on_video_index_right_click, - ) - # (Setup up drag and drop) - drag_target_list = [('video index', 0, 0)] - self.video_index_treeview.enable_model_drag_source( - # Mask of mouse buttons allowed to start a drag - Gdk.ModifierType.BUTTON1_MASK, - # Table of targets the drag procedure supports, and array length - drag_target_list, - # Bitmask of possible actions for a drag from this widget - Gdk.DragAction.MOVE, - ) - self.video_index_treeview.enable_model_drag_dest( - # Table of targets the drag procedure supports, and array length - drag_target_list, - # Bitmask of possible actions for a drag from this widget - Gdk.DragAction.DEFAULT, - ) - self.video_index_treeview.connect( - 'drag-drop', - self.on_video_index_drag_drop, - ) - self.video_index_treeview.connect( - 'drag-data-received', - self.on_video_index_drag_data_received, - ) - - self.video_index_treestore = Gtk.TreeStore( - int, - str, str, - GdkPixbuf.Pixbuf, - str, - ) - self.video_index_sortmodel = Gtk.TreeModelSort( - self.video_index_treestore - ) - self.video_index_treeview.set_model(self.video_index_sortmodel) - self.video_index_sortmodel.set_sort_column_id(1, 0) - self.video_index_sortmodel.set_sort_func( - 1, - self.video_index_auto_sort, - None, - ) - - count = -1 - for item in ['hide', 'hide', 'hide', 'pixbuf', 'show']: - - count += 1 - - if item == 'pixbuf': - - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - None, - renderer_pixbuf, - pixbuf=count, - ) - self.video_index_treeview.append_column(column_pixbuf) - - else: - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - None, - renderer_text, - text=count, - ) - self.video_index_treeview.append_column(column_text) - if item == 'hide': - column_text.set_visible(False) - else: - column_text.set_cell_data_func( - renderer_text, - self.video_index_render_text, - ) - - selection = self.video_index_treeview.get_selection() - selection.connect('changed', self.on_video_index_selection_changed) - - # Make the changes visible - self.video_index_frame.show_all() - - - def video_index_populate(self): - - """Can be called by anything. - - Repopulates the Video Index (assuming that it is already empty, either - because Tartube has just started, or because of an earlier call to - self.video_index_reset() ). - - After the call to this function, new rows can be added via a call to - self.self.video_index_add_row(). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4929 video_index_populate') - - for dbid in self.app_obj.media_top_level_list: - - media_data_obj = self.app_obj.media_reg_dict[dbid] - if not media_data_obj: - return self.app_obj.system_error( - 206, - 'Video Index initialisation failure', - ) - - else: - self.video_index_setup_row(media_data_obj, None) - - # Make the changes visible - self.video_index_treeview.show_all() - - - def video_index_setup_row(self, media_data_obj, parent_pointer=None): - - """Called by self.video_index_populate()Subsequently called by this - function recursively. - - Adds a row to the Video Index. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object for this row - - parent_pointer (Gtk.TreeIter): None if the media data object has no - parent. Otherwise, a pointer to the position of the parent - object in the treeview - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 4966 video_index_setup_row') - - # Don't show a hidden folder, or any of its children - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - return - - # Prepare the icon - pixbuf = self.video_index_get_icon(media_data_obj) - if not pixbuf: - return self.app_obj.system_error( - 207, - 'Video index setup row request failed sanity check', - ) - - # Add a row to the treeview - new_pointer = self.video_index_treestore.append( - parent_pointer, - [ - media_data_obj.dbid, - media_data_obj.name, - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - pixbuf, - self.video_index_get_text(media_data_obj), - ], - ) - - # Create a reference to the row, so we can find it later - tree_ref = Gtk.TreeRowReference.new( - self.video_index_treestore, - self.video_index_treestore.get_path(new_pointer), - ) - self.video_index_row_dict[media_data_obj.name] = tree_ref - - # Call this function recursively for any child objects that are - # channels, playlists or folders (videos are not displayed in the - # Video Index) - for child_obj in media_data_obj.child_list: - - if not(isinstance(child_obj, media.Video)): - self.video_index_setup_row(child_obj, new_pointer) - - - def video_index_add_row(self, media_data_obj, no_select_flag=False): - - """Can be called by anything. - - Adds a row to the Video Index. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object for this row - - no_select_flag (bool): True if the new row should NOT be - automatically selected, as if ordinarily would be - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5029 video_index_add_row') - - # Don't add a hidden folder, or any of its children - if media_data_obj.is_hidden(): - return - - # Prepare the icon - pixbuf = self.video_index_get_icon(media_data_obj) - if not pixbuf: - return self.app_obj.system_error( - 208, - 'Video index setup row request failed sanity check', - ) - - # Add a row to the treeview - if media_data_obj.parent_obj: - - # This media data object has a parent, so we add a row inside the - # parent's row - - # Fetch the treeview reference to the parent media data object... - parent_ref \ - = self.video_index_row_dict[media_data_obj.parent_obj.name] - # ...and add the new object inside its parent - tree_iter = self.video_index_treestore.get_iter( - parent_ref.get_path(), - ) - - new_pointer = self.video_index_treestore.append( - tree_iter, - [ - media_data_obj.dbid, - media_data_obj.name, - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - pixbuf, - self.video_index_get_text(media_data_obj), - ], - ) - - else: - - # The media data object has no parent, so add a row to the - # treeview's top level - new_pointer = self.video_index_treestore.append( - None, - [ - media_data_obj.dbid, - media_data_obj.name, - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - pixbuf, - self.video_index_get_text(media_data_obj), - ], - ) - - # Create a reference to the row, so we can find it later - tree_ref = Gtk.TreeRowReference.new( - self.video_index_treestore, - self.video_index_treestore.get_path(new_pointer), - ) - self.video_index_row_dict[media_data_obj.name] = tree_ref - - if media_data_obj.parent_obj: - - # Expand rows to make the new media data object visible... - self.video_index_treeview.expand_to_path( - self.video_index_sortmodel.convert_child_path_to_path( - parent_ref.get_path(), - ), - ) - - # Select the row (which clears the Video Catalogue) - if not no_select_flag: - selection = self.video_index_treeview.get_selection() - selection.select_path( - self.video_index_sortmodel.convert_child_path_to_path( - tree_ref.get_path(), - ), - ) - - # Make the changes visible - self.video_index_treeview.show_all() - - - def video_index_delete_row(self, media_data_obj): - - """Can be called by anything. - - Removes a row from the Video Index. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object for this row - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5132 video_index_delete_row') - - # Videos can't be shown in the Video Index - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 209, - 'Video index delete row request failed sanity check', - ) - - # During this procedure, ignore any changes to the selected row (i.e. - # don't allow self.on_video_index_selection_changed() to redraw the - # catalogue) - self.ignore_video_index_select_flag = True - - # Remove the treeview row - tree_ref = self.video_index_row_dict[media_data_obj.name] - tree_path = tree_ref.get_path() - tree_iter = self.video_index_treestore.get_iter(tree_path) - self.video_index_treestore.remove(tree_iter) - - self.ignore_video_index_select_flag = False - - # If the deleted row was the previously selected one, the new selected - # row is the one just above/below that - # In this situation, unselect the row and then redraw the Video - # Catalogue - if self.video_index_current is not None \ - and self.video_index_current == media_data_obj.name: - - selection = self.video_index_treeview.get_selection() - selection.unselect_all() - - self.video_index_current = None - self.video_catalogue_reset() - - # Make the changes visible - self.video_index_treeview.show_all() - - - def video_index_select_row(self, media_data_obj): - - """Can be called by anything. - - Selects a row in the Video Index, as if the user had clicked it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be selected - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5182 video_index_select_row') - - # Cannot select a hidden folder, or any of its children - if isinstance(media_data_obj, media.Video) \ - or media_data_obj.is_hidden(): - return self.app_obj.system_error( - 210, - 'Video Index select row request failed sanity check', - ) - - # Select the row, expanding the treeview path to make it visible, if - # necessary - if media_data_obj.parent_obj: - - # Expand rows to make the new media data object visible... - parent_ref \ - = self.video_index_row_dict[media_data_obj.parent_obj.name] - - self.video_index_treeview.expand_to_path( - self.video_index_sortmodel.convert_child_path_to_path( - parent_ref.get_path(), - ), - ) - - # Select the row - tree_ref = self.video_index_row_dict[media_data_obj.name] - - selection = self.video_index_treeview.get_selection() - selection.select_path( - self.video_index_sortmodel.convert_child_path_to_path( - tree_ref.get_path(), - ), - ) - - - def video_index_update_row_icon(self, media_data_obj): - - """Can be called by anything. - - The icons used in the Video Index must be changed when a media data - object is marked (or unmarked) favourite, and when download options - are applied/removed. - - This function updates a row in the Video Index to show the right icon. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be updated - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5235 video_index_update_row_icon') - - # Videos can't be shown in the Video Index - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 211, - 'Video index update row request failed sanity check', - ) - - # If media_data_obj is a hidden folder, then there's nothing to update - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - return - - # Because of Gtk issues, we don't update the Video Index during a - # download/refresh/tidy operation if the flag is set - if ( - self.app_obj.gtk_broken_flag \ - or self.app_obj.gtk_emulate_broken_flag - ) and ( - self.app_obj.download_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.tidy_manager_obj - ): - return - - # Update the treeview row - tree_ref = self.video_index_row_dict[media_data_obj.name] - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - model.set(tree_iter, 3, self.video_index_get_icon(media_data_obj)) - - # Make the changes visible - self.video_index_treeview.show_all() - - - def video_index_update_row_text(self, media_data_obj): - - """Can be called by anything. - - The text used in the Video Index must be changed when a media data - object is updated, including when a child video object is added or - removed. - - This function updates a row in the Video Index to show the new text. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be updated - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5290 video_index_update_row_text') - - # Videos can't be shown in the Video Index - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 212, - 'Video index update row request failed sanity check', - ) - - # If media_data_obj is a hidden folder, then there's nothing to update - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - return - - # Because of Gtk issues, we don't update the Video Index during a - # download/refresh/tidy operation if the flag is set - if ( - self.app_obj.gtk_broken_flag \ - or self.app_obj.gtk_emulate_broken_flag - ) and ( - self.app_obj.download_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.tidy_manager_obj - ): - return - - # Update the treeview row - tree_ref = self.video_index_row_dict[media_data_obj.name] - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - model.set(tree_iter, 4, self.video_index_get_text(media_data_obj)) - - # Make the changes visible - self.video_index_treeview.show_all() - - - def video_index_update_row_tooltip(self, media_data_obj): - - """Can be called by anything. - - The tooltips used in the Video Index must be changed when a media data - object is updated. - - This function updates the (hidden) row in the Video Index containing - the text for tooltips. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be updated - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5345 video_index_update_row_tooltip') - - # Videos can't be shown in the Video Index - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 213, - 'Video index update row request failed sanity check', - ) - - # If media_data_obj is a hidden folder, then there's nothing to update - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.hidden_flag: - return - - # Because of Gtk issues, we don't update the Video Index during a - # download/refresh/tidy operation if the flag is set - if ( - self.app_obj.gtk_broken_flag \ - or self.app_obj.gtk_emulate_broken_flag - ) and ( - self.app_obj.download_manager_obj \ - or self.app_obj.refresh_manager_obj \ - or self.app_obj.tidy_manager_obj - ): - return - - # Update the treeview row - tree_ref = self.video_index_row_dict[media_data_obj.name] - model = tree_ref.get_model() - tree_path = tree_ref.get_path() - tree_iter = model.get_iter(tree_path) - model.set( - tree_iter, - 2, - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - ) - - # Make the changes visible - self.video_index_treeview.show_all() - - - def video_index_get_icon(self, media_data_obj): - - """Called by self.video_index_setup_row(), - .video_index_add_row() and .video_index_update_row_icon(). - - Finds the icon to display on a Video Index row for the specified media - data object. - - Looks up the GdkPixbuf which has already been created for that icon - and returns it (or None, if the icon file is missing or if no - corresponding pixbuf can be found.) - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose row should be updated - - Returns: - - A GdkPixbuf or None. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5413 video_index_get_icon') - - icon = None - if not self.app_obj.show_small_icons_in_index: - - # Large icons, bigger selection - if isinstance(media_data_obj, media.Channel): - - if media_data_obj.fav_flag and media_data_obj.options_obj: - icon = 'channel_both_large' - elif media_data_obj.fav_flag: - icon = 'channel_left_large' - elif media_data_obj.options_obj: - icon = 'channel_right_large' - else: - icon = 'channel_none_large' - - elif isinstance(media_data_obj, media.Playlist): - - if media_data_obj.fav_flag and media_data_obj.options_obj: - icon = 'playlist_both_large' - elif media_data_obj.fav_flag: - icon = 'playlist_left_large' - elif media_data_obj.options_obj: - icon = 'playlist_right_large' - else: - icon = 'playlist_none_large' - - elif isinstance(media_data_obj, media.Folder): - - if media_data_obj.priv_flag: - if media_data_obj.fav_flag and media_data_obj.options_obj: - icon = 'folder_private_both_large' - elif media_data_obj.fav_flag: - icon = 'folder_private_left_large' - elif media_data_obj.options_obj: - icon = 'folder_private_right_large' - else: - icon = 'folder_private_none_large' - - elif media_data_obj.temp_flag: - if media_data_obj.fav_flag and media_data_obj.options_obj: - icon = 'folder_temp_both_large' - elif media_data_obj.fav_flag: - icon = 'folder_temp_left_large' - elif media_data_obj.options_obj: - icon = 'folder_temp_right_large' - else: - icon = 'folder_temp_none_large' - - elif media_data_obj.fixed_flag: - if media_data_obj.fav_flag and media_data_obj.options_obj: - icon = 'folder_fixed_both_large' - elif media_data_obj.fav_flag: - icon = 'folder_fixed_left_large' - elif media_data_obj.options_obj: - icon = 'folder_fixed_right_large' - else: - icon = 'folder_fixed_none_large' - - else: - if media_data_obj.fav_flag and media_data_obj.options_obj: - icon = 'folder_both_large' - elif media_data_obj.fav_flag: - icon = 'folder_left_large' - elif media_data_obj.options_obj: - icon = 'folder_right_large' - else: - icon = 'folder_none_large' - - else: - - # Small icons, smaller selection - if isinstance(media_data_obj, media.Channel): - icon = 'channel_small' - elif isinstance(media_data_obj, media.Playlist): - icon = 'playlist_small' - elif isinstance(media_data_obj, media.Folder): - if media_data_obj.priv_flag: - icon = 'folder_red_small' - elif media_data_obj.temp_flag: - icon = 'folder_blue_small' - elif media_data_obj.fixed_flag: - icon = 'folder_green_small' - else: - icon = 'folder_small' - - if icon is not None and icon in self.icon_dict: - return self.pixbuf_dict[icon] - else: - # Invalid 'icon', or file not found - return None - - - def video_index_get_text(self, media_data_obj): - - """Called by self.video_index_setup_row(), .video_index_add_row() and - .video_index_update_row_text(). - - Sets the text to display on a Video Index row for the specified media - data object. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - A media data object visible in the Video Index - - Returns: - - A string. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5527 video_index_get_text') - - text = utils.shorten_string( - media_data_obj.nickname, - self.short_string_max_len, - ) - - if not self.app_obj.complex_index_flag: - - if media_data_obj.dl_count: - text += ' (' + str(media_data_obj.new_count) + '/' \ - + str(media_data_obj.dl_count) + ')' - - else: - - 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) - - if not isinstance(media_data_obj, media.Folder) \ - and (media_data_obj.error_list or media_data_obj.warning_list): - - if not media_data_obj.vid_count: - text += '\n' - else: - text += ' ' - - text += 'E:' + str(len(media_data_obj.error_list)) \ - + ' W:' + str(len(media_data_obj.warning_list)) - - return text - - - def video_index_render_text(self, col, renderer, model, tree_iter, data): - - """Called by self.video_index_reset(). - - Cell renderer function. When the text column of the Video Index is - about to be rendered, set the font to normal, bold or italic, depending - on the media data object's IVs. - - Args: - - col (Gtk.TreeViewColumn): The treeview column about to be rendered. - - renderer (Gtk.CellRendererText): The Gtk object handling the - rendering. - - model (Gtk.TreeModelSort): The treeview's row data is stored here. - - tree_iter (Gtk.TreeIter): A pointer to the row containing the cell - to be rendered. - - data (None): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5589 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 - if ( - not self.app_obj.gtk_broken_flag \ - and not self.app_obj.gtk_emulate_broken_flag - ) or ( - not self.app_obj.download_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj - ): - dbid = model.get_value(tree_iter, 0) - media_data_obj = self.app_obj.media_reg_dict[dbid] - - # If marked new (unwatched), show as bold text - if media_data_obj.new_count: - renderer.set_property('weight', Pango.Weight.BOLD) - else: - renderer.set_property('weight', Pango.Weight.NORMAL) - - # If downloads disabled, show as italic text - if media_data_obj.dl_disable_flag: - renderer.set_property('style', Pango.Style.ITALIC) - renderer.set_property('underline', True) - elif media_data_obj.dl_sim_flag: - renderer.set_property('style', Pango.Style.ITALIC) - renderer.set_property('underline', False) - else: - renderer.set_property('style', Pango.Style.NORMAL) - renderer.set_property('underline', False) - - else: - - # Using default weight/style/underline doesn't seem to cause the - # same Gtk issues - # Forcing normal weight/style prevents the whole Video Index being - # drawn bold (occasionally) - renderer.set_property('weight', Pango.Weight.NORMAL) - renderer.set_property('style', Pango.Style.NORMAL) - renderer.set_property('underline', False) - - - # (Video Catalogue) - - - def video_catalogue_reset(self): - - """Can be called by anything. - - On the first call, sets up the widgets for the Video Catalogue. On - subsequent calls, replaces those widgets, ready for them to be filled - with new data. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5645 video_catalogue_reset') - - # If not called by self.setup_videos_tab()... - if self.catalogue_frame.get_child(): - self.catalogue_frame.remove(self.catalogue_frame.get_child()) - - # Reset IVs (when called by anything) - self.video_catalogue_dict = {} - - # Set up the widgets - self.catalogue_scrolled = Gtk.ScrolledWindow() - self.catalogue_frame.add(self.catalogue_scrolled) - self.catalogue_scrolled.set_policy( - Gtk.PolicyType.AUTOMATIC, - Gtk.PolicyType.AUTOMATIC, - ) - - self.catalogue_listbox = Gtk.ListBox() - self.catalogue_scrolled.add(self.catalogue_listbox) - self.catalogue_listbox.set_can_focus(False) - self.catalogue_listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE) - # (Without this line, it's not possible to unselect rows by clicking - # on one of them) - self.catalogue_listbox.set_activate_on_single_click(False) - - self.catalogue_listbox.set_sort_func( - self.video_catalogue_auto_sort, - None, - False, - ) - - # Make the changes visible - self.catalogue_frame.show_all() - - - def video_catalogue_redraw_all(self, name, page_num=1, - reset_scroll_flag=False, no_cancel_filter_flag=False): - - """Can be called by anything. - - When the user clicks on a media data object in the Video Index (a - channel, playlist or folder), this function is called to replace the - contents of the Video Catalogue with some or all of the video objects - stored as children in that channel, playlist or folder. - - Depending on the value of self.catalogue_mode, the Video Catalogue - consists of a list of mainwin.SimpleCatalogueItem or - mainwin.ComplexCatalogueItem objects, one for each row in the - Gtk.ListBox (corresponding to a single video). - - The video catalogue splits its video list into pages (as Gtk struggles - with a list of hundreds, or thousands, of videos). Only videos on the - specified page (or on the current page, if no page is specified) are - drawn. If mainapp.TartubeApp.catalogue_page_size is set to zero, all - videos are drawn on a single page. - - If a filter has been applied, only videos matching the search text - are visible in the catalogue. - - This function clears the previous contents of the Gtk.ListBox and - resets IVs. - - Then, it adds new rows to the Gtk.ListBox and creates a new - mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem object for - each video on the page. - - Args: - - name (str): The selected media data object's name; one of the keys - in self.media_name_dict - - page_num (int): The number of the page to be drawn (a value in the - range 1 to self.catalogue_toolbar_last_page) - - reset_scroll_flag (bool): Set to True when called by - self.on_video_index_selection_changed(). The scrollbars must - always be reset when switching between channels/playlist/ - folders - - no_cancel_filter_flag (bool): By default, if the filter is applied, - it is cancelled by this function. Set to True if the calling - function doesn't want that (for example, because it has just - set up the filter, and wants to show only matching videos) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5732 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 - # scrollbars later in the function - if not reset_scroll_flag: - if self.video_index_current is None \ - or self.video_index_current != name \ - or self.catalogue_toolbar_current_page != page_num: - reset_scroll_flag = True - - # The parent media data object is a media.Channel, media.playlist or - # media.Folder object - dbid = self.app_obj.media_name_dict[name] - parent_obj = self.app_obj.media_reg_dict[dbid] - - # Sanity check - the selected item in the Video Index should not be a - # media.Video object - if not parent_obj or (isinstance(parent_obj, media.Video)): - return self.system_error( - 214, - 'Videos should not appear in the Video Index', - ) - - # Reset the previous contents of the Video Catalogue, if any, and reset - # IVs - self.video_catalogue_reset() - # Temporarily reset widgets in the Video Catalogue toolbar (in case - # something goes wrong, or in case drawing the page takes a long - # time) - self.video_catalogue_toolbar_reset() - # If a filter had recently been applied, reset IVs to cancel it (unless - # the calling function doesn't want that) - # This makes sure that the filter is always reset when the user clicks - # on a different channel/playlist/folder in the Video Index - if not no_cancel_filter_flag: - self.video_catalogue_filtered_flag = False - self.video_catalogue_filtered_list = [] - - # The parent media data object has any number of child media data - # objects, but this function is only interested in those that are - # media.Video objects - video_count = 0 - page_size = self.app_obj.catalogue_page_size - # If the filter has been applied, use the prepared list of child videos - # specified by the IV; otherwise, use all child videos - if self.video_catalogue_filtered_flag: - child_list = self.video_catalogue_filtered_list.copy() - else: - child_list = parent_obj.child_list.copy() - - for child_obj in child_list: - if isinstance(child_obj, media.Video): - - # (We need the number of child videos when we update widgets in - # the toolbar) - video_count += 1 - - # Only draw videos on this page. If the page size is zero, all - # videos are drawn on a single page - if page_size \ - and ( - video_count <= ((page_num - 1) * page_size) \ - or video_count > (page_num * page_size) - ): - # Don't draw the video on this page - continue - - # Create a new catalogue item object for the video - if self.app_obj.catalogue_mode == 'simple_hide_parent' \ - or self.app_obj.catalogue_mode == 'simple_show_parent': - catalogue_item_obj = SimpleCatalogueItem( - self, - child_obj, - ) - - else: - catalogue_item_obj = ComplexCatalogueItem( - self, - child_obj, - ) - - self.video_catalogue_dict[catalogue_item_obj.dbid] = \ - catalogue_item_obj - - # Add a row to the Gtk.ListBox - - # Instead of using Gtk.ListBoxRow directly, use a wrapper class - # so we can quickly retrieve the video displayed on each row - wrapper_obj = CatalogueRow(child_obj) - self.catalogue_listbox.add(wrapper_obj) - - # Populate the row with widgets... - catalogue_item_obj.draw_widgets(wrapper_obj) - # ...and give them their initial appearance - catalogue_item_obj.update_widgets() - - # Update widgets in the toolbar, now that we know the number of child - # videos - self.video_catalogue_toolbar_update(page_num, video_count) - - # In all cases, sensitise the scroll up/down toolbar buttons - self.catalogue_scroll_up_button.set_sensitive(True) - self.catalogue_scroll_down_button.set_sensitive(True) - # Reset the scrollbar, if required - if reset_scroll_flag: - self.catalogue_scrolled.get_vadjustment().set_value(0) - - # Procedure complete - self.catalogue_listbox.show_all() - - - def video_catalogue_update_row(self, video_obj): - - """Can be called by anything. - - This function is called with a media.Video object. If that video is - already visible in the Video Catalogue, updates the corresponding - mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem (which - updates the widgets in the Gtk.ListBox). - - If the video is now yet visible in the Video Catalogue, but should be - drawn on the current page, creates a new mainwin.SimpleCatalogueItem or - mainwin.ComplexCatalogueItem object and adds a row to the Gtk.ListBox, - removing an existing catalogue item to make room, if necessary. - - Args: - - video_obj (media.Video) - The video to update - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 5865 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 - if ( - self.app_obj.gtk_broken_flag - or self.app_obj.gtk_emulate_broken_flag - ) and ( - 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 - ): - return - - # Does a mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem - # object already exist for this video? - if video_obj.dbid in self.video_catalogue_dict: - - # 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: - - # 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: - - # Get the media.Video object - missing_obj = app_obj.media_reg_dict[dbid] - - # Create a new catalogue item - self.video_catalogue_insert_item(missing_obj) - - else: - - # Page is not full, so just create a new catalogue item - self.video_catalogue_insert_item(video_obj) - - # Update widgets in the toolbar - self.video_catalogue_toolbar_update( - self.catalogue_toolbar_current_page, - video_count, - ) - - # Force the Gtk.ListBox to sort its rows, so that videos are displayed - # in the correct order - # v1.3.112 this call is suspected of causing occasional crashes due to - # Gtk issues. Disable it, if a download/refresh/tidy operation is in - # progress - if ( - not app_obj.gtk_broken_flag \ - and not app_obj.gtk_emulate_broken_flag - ) or ( - not app_obj.download_manager_obj \ - and not app_obj.refresh_manager_obj \ - and not app_obj.tidy_manager_obj - ): - self.catalogue_listbox.invalidate_sort() - - # Procedure complete - self.catalogue_listbox.show_all() - - - def video_catalogue_insert_item(self, video_obj): - - """Called by self.video_catalogue_update_row() (only). - - Adds a new mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem - to the Video Catalogue. - - Args: - - video_obj (media.Video): The video for which a new catalogue item - should be created - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6033 video_catalogue_insert_item') - - # Create the new catalogue item - if self.app_obj.catalogue_mode == 'simple_hide_parent' \ - or self.app_obj.catalogue_mode == 'simple_show_parent': - catalogue_item_obj = SimpleCatalogueItem( - self, - video_obj, - ) - - else: - catalogue_item_obj = ComplexCatalogueItem( - self, - video_obj, - ) - - self.video_catalogue_dict[video_obj.dbid] = catalogue_item_obj - - # Add a row to the Gtk.ListBox - - # Instead of using Gtk.ListBoxRow directly, use a wrapper - # class so we can quickly retrieve the video displayed on - # each row - wrapper_obj = CatalogueRow(video_obj) - - # On rare occasions, the line below sometimes causes a warning, - # 'Accessing a sequence while it is being sorted or seached is not - # allowed' - # If this happens, add it to a temporary list of rows to be added to - # the listbox by self.video_catalogue_retry_insert_items() - try: - self.catalogue_listbox.add(wrapper_obj) - except: - self.video_catalogue_temp_list.append(wrapper_obj) - - # Populate the row with widgets... - catalogue_item_obj.draw_widgets(wrapper_obj) - # ...and give them their initial appearance - catalogue_item_obj.update_widgets() - - - def video_catalogue_retry_insert_items(self): - - """Called by mainapp.TartubeApp.script_fast_timer_callback(). - - If an earlier call to self.video_catalogue_insert_item() failed, one - or more CatalogueRow objects are waiting to be added to the Video - Catalogue. Add them, if so. - """ - - if DEBUG_FUNC_FLAG and not DEBUG_NO_TIMER_FUNC_FLAG: - utils.debug_time('mwn 6084 video_catalogue_retry_insert_items') - - if self.video_catalogue_temp_list: - - while self.video_catalogue_temp_list: - - wrapper_obj = self.video_catalogue_temp_list.pop() - - try: - self.catalogue_listbox.add(wrapper_obj) - except: - # Still can't add the row; try again later - self.video_catalogue_temp_list.append(wrapper_obj) - return - - # All items added. Force the Gtk.ListBox to sort its rows, so that - # videos are displayed in the correct order - # v1.3.112 this call is suspected of causing occasional crashes due - # to Gtk issues. Disable it, if a download/refresh/tidy operation - # is in progress - if ( - not self.app_obj.gtk_broken_flag \ - and not self.app_obj.gtk_emulate_broken_flag - ) or ( - not self.app_obj.download_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj - ): - self.catalogue_listbox.invalidate_sort() - - # Procedure complete - self.catalogue_listbox.show_all() - - - def video_catalogue_delete_row(self, video_obj): - - """Can be called by anything. - - This function is called with a media.Video object. If that video is - already visible in the Video Catalogue, removes the corresponding - mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem . - - Args: - - video_obj (media.Video) - The video to remove - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6133 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 - # displayed in the Video Catalogue - app_obj = self.app_obj - - if self.video_index_current is None: - 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 - ): - return - - # Does a mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem - # object exist for this video? - if video_obj.dbid in self.video_catalogue_dict: - - # Remove the catalogue item object and its mainwin.CatalogueRow - # object (the latter being a wrapper for Gtk.ListBoxRow) - catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid] - - # Remove the row from the Gtk.ListBox - self.catalogue_listbox.remove(catalogue_item_obj.catalogue_row) - - # Update IVs - del self.video_catalogue_dict[video_obj.dbid] - - # If the current page is not the last one, we can create a new - # catalogue item to replace the removed one - move_obj = None - dbid = app_obj.media_name_dict[self.video_index_current] - container_obj = app_obj.media_reg_dict[dbid] - video_count = 0 - - if self.video_catalogue_dict \ - and self.catalogue_toolbar_current_page \ - < self.catalogue_toolbar_last_page: - - # Get the last mainwin.CatalogueRow object directly from the - # Gtk listbox, as it is auto-sorted frequently - row_list = self.catalogue_listbox.get_children() - last_row = row_list[-1] - if last_row: - last_obj = last_row.video_obj - - # Find the video object that would be drawn after that, if the - # videos were all drawn on a single page - # At the same time, count the number of remaining child video - # objects so we can update the toolbar - next_flag = False - - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - video_count += 1 - if child_obj.dbid == last_obj.dbid: - # (Use the next video after this one) - next_flag = True - - elif next_flag == True: - # (Use this video) - move_obj = child_obj - next_flag = False - - # Create the new catalogue item - if move_obj: - self.video_catalogue_update_row(move_obj) - - else: - - # We're already on the last (or only) page, so no need to - # replace anything. Just count the number of remaining child - # video objects - for child_obj in container_obj.child_list: - if isinstance(child_obj, media.Video): - video_count += 1 - - # Update widgets in the Video Catalogue toolbar - self.video_catalogue_toolbar_update( - self.catalogue_toolbar_current_page, - video_count, - ) - - # Procedure complete - self.catalogue_listbox.show_all() - - - def video_catalogue_toolbar_reset(self): - - """Called by self.video_catalogue_redraw_all(). - - Just before completely redrawing the Video Catalogue, temporarily reset - widgets in the Video Catalogue toolbar (in case something goes wrong, - or in case drawing the page takes a long time). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6243 video_catalogue_toolbar_reset') - - self.catalogue_toolbar_current_page = 1 - self.catalogue_toolbar_last_page = 1 - - self.catalogue_page_entry.set_sensitive(True) - self.catalogue_page_entry.set_text( - str(self.catalogue_toolbar_current_page), - ) - - self.catalogue_last_entry.set_sensitive(True) - self.catalogue_last_entry.set_text( - str(self.catalogue_toolbar_last_page), - ) - - self.catalogue_first_button.set_sensitive(False) - self.catalogue_back_button.set_sensitive(False) - self.catalogue_forwards_button.set_sensitive(False) - self.catalogue_last_button.set_sensitive(False) - - self.catalogue_show_filter_button.set_sensitive(False) - - self.catalogue_sort_button.set_sensitive(False) - self.catalogue_filter_entry.set_sensitive(False) - self.catalogue_regex_togglebutton.set_sensitive(False) - self.catalogue_apply_filter_button.set_sensitive(False) - self.catalogue_cancel_filter_button.set_sensitive(False) - self.catalogue_find_date_button.set_sensitive(False) - - - def video_catalogue_toolbar_update(self, page_num, video_count): - - """Called by self.video_catalogue_redraw_all(), - self.video_catalogue_update_row() and - self.video_catalogue_delete_row(). - - After the Video Catalogue is redrawn or updated, update widgets in the - Video Catalogue toolbar. - - Args: - - page_num (int): The page number to draw (a value in the range 1 to - self.catalogue_toolbar_last_page) - - video_count (int): The number of videos that are children of the - selected channel, playlist or folder (may be 0) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6293 video_catalogue_toolbar_update') - - self.catalogue_toolbar_current_page = page_num - - # If the page size is 0, then all videos are drawn on one page - if not self.app_obj.catalogue_page_size: - self.catalogue_toolbar_last_page = page_num - else: - self.catalogue_toolbar_last_page \ - = int((video_count - 1) / self.app_obj.catalogue_page_size) + 1 - - self.catalogue_page_entry.set_sensitive(True) - self.catalogue_page_entry.set_text( - str(self.catalogue_toolbar_current_page), - ) - - self.catalogue_last_entry.set_sensitive(True) - self.catalogue_last_entry.set_text( - str(self.catalogue_toolbar_last_page), - ) - - if page_num == 1: - self.catalogue_first_button.set_sensitive(False) - self.catalogue_back_button.set_sensitive(False) - else: - self.catalogue_first_button.set_sensitive(True) - self.catalogue_back_button.set_sensitive(True) - - if page_num == self.catalogue_toolbar_last_page: - self.catalogue_forwards_button.set_sensitive(False) - self.catalogue_last_button.set_sensitive(False) - else: - self.catalogue_forwards_button.set_sensitive(True) - self.catalogue_last_button.set_sensitive(True) - - self.catalogue_show_filter_button.set_sensitive(True) - - # These widgets are sensitised when the filter is applied even if - # there are no matching videos - # (If not, the user would not be able to click the 'Cancel filter' - # button) - if not video_count and not self.video_catalogue_filtered_flag: - self.catalogue_sort_button.set_sensitive(False) - self.catalogue_filter_entry.set_sensitive(False) - self.catalogue_regex_togglebutton.set_sensitive(False) - self.catalogue_apply_filter_button.set_sensitive(False) - self.catalogue_cancel_filter_button.set_sensitive(False) - self.catalogue_find_date_button.set_sensitive(False) - else: - self.catalogue_sort_button.set_sensitive(True) - self.catalogue_filter_entry.set_sensitive(True) - self.catalogue_regex_togglebutton.set_sensitive(True) - self.catalogue_apply_filter_button.set_sensitive(True) - self.catalogue_cancel_filter_button.set_sensitive(False) - self.catalogue_find_date_button.set_sensitive(True) - - - def video_catalogue_apply_filter(self): - - """Called by mainapp.TartubeApp.on_button_apply_filter(). - - Applies a filter, so that all videos not matching the search text are - hidden in the Video Catalogue. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6359 video_catalogue_apply_filter') - - # Sanity check - something must be selected in the Video Index, and it - # must not be a media.Video object - parent_obj = None - if self.video_index_current is not None: - dbid = self.app_obj.media_name_dict[self.video_index_current] - parent_obj = self.app_obj.media_reg_dict[dbid] - - if not parent_obj or (isinstance(parent_obj, media.Video)): - return self.system_error( - 215, - 'Tried to apply filter, but no channel/playlist/folder' \ - + ' selected in the Video Index', - ) - - # Get the search text from the entry box - search_text = self.catalogue_filter_entry.get_text() - if search_text is None or search_text == '': - # Apply an empty filter is the same as clicking the cancel filter - # button - return self.video_catalogue_cancel_filter() - - # Get a list of media.Video objects which are children of the - # currently selected channel, playlist or folder - # Then filter out every video whose name doesn't match the filter text - # Also filter out any videos that don't have an individual name set) - video_list = [] - regex_flag = self.app_obj.catologue_use_regex_flag - for child_obj in parent_obj.child_list: - if isinstance(child_obj, media.Video): - - if child_obj.name != self.app_obj.default_video_name \ - and ( - ( - not regex_flag \ - and child_obj.name.lower().find(search_text.lower()) \ - > -1 - ) or ( - regex_flag \ - and re.search( - search_text, - child_obj.name, - re.IGNORECASE, - ) - ) - ): - video_list.append(child_obj) - - # Set IVs... - self.video_catalogue_filtered_flag = True - self.video_catalogue_filtered_list = video_list.copy() - # ...and redraw the Video Catalogue - self.video_catalogue_redraw_all( - self.video_index_current, - 1, # Display the first page - True, # Reset scrollbars - True, # This function is the caller - ) - - # Sensitise widgets, as appropriate - self.catalogue_apply_filter_button.set_sensitive(False) - self.catalogue_cancel_filter_button.set_sensitive(True) - - - def video_catalogue_cancel_filter(self): - - """Called by mainapp.TartubeApp.on_button_cancel_filter() and - self.video_catalogue_apply_filter(). - - Cancels the filter, so that all videos which are children of the - currently selected channel/playlist/folder are shown in the Video - Catalogue. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6435 video_catalogue_cancel_filter') - - # Reset IVs... - self.video_catalogue_filtered_flag = False - self.video_catalogue_filtered_list = [] - # ...and redraw the Video Catalogue - self.video_catalogue_redraw_all(self.video_index_current) - - # Sensitise widgets, as appropriate - self.catalogue_apply_filter_button.set_sensitive(True) - self.catalogue_cancel_filter_button.set_sensitive(False) - - - # (Progress List) - - - def progress_list_reset(self): - - """Can be called by anything. - - Empties the Gtk.TreeView in the Progress List, ready for it to be - refilled. - - Also resets related IVs. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6462 progress_list_reset') - - # Reset widgets - self.progress_list_liststore = Gtk.ListStore( - int, int, str, - GdkPixbuf.Pixbuf, - str, str, str, str, str, str, str, str, str, - ) - self.progress_list_treeview.set_model(self.progress_list_liststore) - - # Reset IVs - self.progress_list_row_dict = {} - self.progress_list_row_count = 0 - self.progress_list_temp_dict = {} - self.progress_list_finish_dict = {} - - - def progress_list_init(self, download_list_obj): - - """Called by mainapp.TartubeApp.download_manager_continue(). - - At the start of the download operation, a downloads.DownloadList - object is created, listing all the media data objects (channels, - playlists and videos) from which videos are to be downloaded. - - This function is then called to add each of those media data objects to - the Progress List. - - As the download operation progresses, - downloads.DownloadWorker.talk_to_mainwin() calls - self.progress_list_receive_dl_stats() to update the contents of the - Progress List. - - Args: - - download_list_obj (downloads.DownloadList): The download list - object that has just been created - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6503 progress_list_init') - - # For each download item object, add a row to the treeview, and store - # the download item's .dbid IV so that - # self.progress_list_receive_dl_stats() can update the correct row - for item_id in download_list_obj.download_item_list: - - download_item_obj = download_list_obj.download_item_dict[item_id] - - self.progress_list_add_row( - item_id, - download_item_obj.media_data_obj, - ) - - - def progress_list_add_row(self, item_id, media_data_obj): - - """Called by self.progress_list_init(), - mainapp.TartubeApp.download_watch_videos() and - downloads.VideoDownloader.convert_video_to_container(). - - Adds a row to the Progress List. - - Args: - - item_id (int): The downloads.DownloadItem.item_id - - media_data_obj (media.Video, media.Channel or media.Playlist): - The media data object for which a row should be added - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6536 progress_list_add_row') - - # Prepare the icon - if isinstance(media_data_obj, media.Channel): - pixbuf = self.pixbuf_dict['channel_small'] - elif isinstance(media_data_obj, media.Playlist): - pixbuf = self.pixbuf_dict['playlist_small'] - elif isinstance(media_data_obj, media.Folder): - pixbuf = self.pixbuf_dict['folder_small'] - else: - pixbuf = self.pixbuf_dict['video_small'] - - # Prepare the new row in the treeview - row_list = [] - - row_list.append(item_id) # Hidden - row_list.append(media_data_obj.dbid) # Hidden - row_list.append( # Hidden - html.escape( - media_data_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - ), - ) - row_list.append(pixbuf) - row_list.append( - utils.shorten_string( - media_data_obj.name, - 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) - - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.progress_list_treeview.show_all() - self.progress_list_liststore.append(row_list) - - # Store the row's details so we can update it later - self.progress_list_row_dict[item_id] \ - = self.progress_list_row_count - self.progress_list_row_count += 1 - - - def progress_list_receive_dl_stats(self, download_item_obj, dl_stat_dict, - finish_flag=False): - - """Called by downloads.DownloadWorker.data_callback(). - - During a download operation, this function is called every time - youtube-dl writes some output to STDOUT. - - Updating data displayed in the Progress List several times a second, - and irregularly, doesn't look very nice. Instead, we only update the - displayed data at fixed intervals. - - 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() ). - - We store that dictionary temporarily. During periodic calls to - self.progress_list_display_dl_stats(), the contents of any stored - dictionaries are displayed and then the dictionaries themselves are - destroyed. - - Args: - - download_item_obj (downloads.DownloadItem): The download item - object handling a download for a media data object - - dl_stat_dict (dict): The dictionary of download statistics - described above - - finish_flag (bool): True if the worker has finished with its - media data object, meaning that dl_stat_dict is the final set - of statistics, and that the progress list row can be hidden, - if required - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6625 progress_list_receive_dl_stats') - - # Check that the Progress List actually has a row for the specified - # downloads.DownloadItem object - if not download_item_obj.item_id in self.progress_list_row_dict: - return self.app_obj.system_error( - 216, - 'Missing row in Progress List', - ) - - # Temporarily store the dictionary of download statistics - if not download_item_obj.item_id in self.progress_list_temp_dict: - new_dl_stat_dict = {} - else: - new_dl_stat_dict \ - = self.progress_list_temp_dict[download_item_obj.item_id] - - for key in dl_stat_dict: - new_dl_stat_dict[key] = dl_stat_dict[key] - - self.progress_list_temp_dict[download_item_obj.item_id] \ - = new_dl_stat_dict - - # If it's the final set of download statistics, set the time at which - # the row can be hidden (if required) - if finish_flag: - self.progress_list_finish_dict[download_item_obj.item_id] \ - = time.time() + self.progress_list_hide_time - - - def progress_list_display_dl_stats(self): - - """Called by downloads.DownloadManager.run() and - mainapp.TartubeApp.dl_timer_callback(). - - As the download operation progresses, youtube-dl writes statistics to - its STDOUT. Those statistics have been interpreted and stored in - self.progress_list_temp_dict, waiting for periodic calls to this - function to display them. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6667 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 - temp_dict = self.progress_list_temp_dict - self.progress_list_temp_dict = {} - - # For each media data object displayed in the Progress List... - for item_id in temp_dict: - - # 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() - dl_stat_dict = temp_dict[item_id] - - # Get the corresponding treeview row - tree_path = Gtk.TreePath(self.progress_list_row_dict[item_id]) - - # Update statistics displayed in that row - # (Columns 0-4 are not modified, once the row has been added to the - # treeview) - column = 4 - - for key in ( - 'playlist_index', - 'status', - 'filename', - 'extension', - 'percent', - 'speed', - 'eta', - 'filesize', - ): - column += 1 - - if key in dl_stat_dict: - - if key == 'playlist_index': - - 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']) - - 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' - - else: - string = utils.shorten_string( - dl_stat_dict[key], - self.medium_string_max_len, - ) - - self.progress_list_liststore.set( - self.progress_list_liststore.get_iter(tree_path), - column, - string, - ) - - - def progress_list_check_hide_rows(self, force_flag=False): - - """Called by mainapp.TartubeApp.download_manager_finished, - .dl_timer_callback() and .set_progress_list_hide_flag(). - - Called only when mainapp.TartubeApp.progress_list_hide_flag is True. - - Any rows in the Progress List which are finished are stored in - self.progress_list_finish_dict. When a row is finished, it is given a - time (three seconds afterwards, by default) at which the row can be - deleted. - - Check each row, and if it's time to delete it, do so. - - Args: - - force_flag (bool): Set to True if all finished rows should be - hidden immediately, rather than waiting for the (by default) - three seconds - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6757 progress_list_check_hide_rows') - - current_time = time.time() - hide_list = [] - - for item_id in self.progress_list_finish_dict.keys(): - finish_time = self.progress_list_finish_dict[item_id] - - if force_flag or current_time > finish_time: - hide_list.append(item_id); - - # Now we've finished walking the dictionary, we can hide rows - for item_id in hide_list: - self.progress_list_do_hide_row(item_id) - - - def progress_list_do_hide_row(self, item_id): - - """Called by self.progress_list_check_hide_rows(). - - If it's time to delete a row in the Progress List, delete the row and - update IVs. - - Args: - - item_id (int): The downloads.DownloadItem.item_id that was - displaying statistics in the row to be deleted - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6788 progress_list_do_hide_row') - - row_num = self.progress_list_row_dict[item_id] - - # Prepare new values for Progress List IVs. Everything after this row - # must have its row number decremented by one - row_dict = {} - for this_item_id in self.progress_list_row_dict.keys(): - this_row_num = self.progress_list_row_dict[this_item_id] - - if this_row_num > row_num: - row_dict[this_item_id] = this_row_num - 1 - elif this_row_num < row_num: - row_dict[this_item_id] = this_row_num - - row_count = self.progress_list_row_count - 1 - - # Remove the row - path = Gtk.TreePath(row_num), - iter = self.progress_list_liststore.get_iter(path) - self.progress_list_liststore.remove(iter) - - # Apply updated IVs - self.progress_list_row_dict = row_dict.copy() - if item_id in self.progress_list_temp_dict: - del self.progress_list_temp_dict[item_id] - if item_id in self.progress_list_finish_dict: - del self.progress_list_finish_dict[item_id] - - - # (Results List) - - - def results_list_reset(self): - - """Can be called by anything. - - Empties the Gtk.TreeView in the Results List, ready for it to be - refilled. (There are no IVs to reset.) - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6830 results_list_reset') - - # Reset widgets - self.results_list_liststore = Gtk.ListStore( - int, str, - GdkPixbuf.Pixbuf, - str, str, str, str, - bool, - GdkPixbuf.Pixbuf, - str, - ) - self.results_list_treeview.set_model(self.results_list_liststore) - - # Reset IVs - self.results_list_row_count = 0 - self.results_list_temp_list = [] - - - def results_list_add_row(self, download_item_obj, video_obj, \ - keep_description=None, keep_info=None, keep_annotations=None, - keep_thumbnail=None): - - """Called by mainapp.TartubeApp.announce_video_download(). - - At the instant when youtube-dl completes a video download, the standard - python test for the existence of a file fails. - - Therefore, when this function is called, we display the downloaded - video in the Results List immediately, but we also add the video to a - temporary list. - - Thereafter, periodic calls to self.results_list_update_row() check - whether the file actually exists yet, and updates the Results List - accordingly. - - 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 - - keep_description (True, False, None): - keep_info (True, False, None): - keep_annotations (True, False, None): - keep_thumbnail (bool): Settings from the options.OptionsManager - object used to download the video (all of them set to 'None' - for a simulated download) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 6883 results_list_add_row') - - # Prepare the icons - if 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: - pixbuf = self.pixbuf_dict['download_small'] - - if isinstance(video_obj.parent_obj, media.Channel): - pixbuf2 = self.pixbuf_dict['channel_small'] - elif isinstance(video_obj.parent_obj, media.Playlist): - pixbuf2 = self.pixbuf_dict['playlist_small'] - elif isinstance(video_obj.parent_obj, media.Folder): - pixbuf2 = self.pixbuf_dict['folder_small'] - else: - return self.app_obj.system_error( - 217, - 'Results List add row request failed sanity check', - ) - - # Prepare the new row in the treeview - row_list = [] - - # Set the row's initial contents - row_list.append(video_obj.dbid) # Hidden - row_list.append( # Hidden - html.escape( - video_obj.fetch_tooltip_text( - self.app_obj, - self.tooltip_max_len, - ), - ), - ) - row_list.append(pixbuf) - row_list.append( - utils.shorten_string( - video_obj.nickname, - self.medium_string_max_len, - ), - ) - - # (For a simulated download, the video duration (etc) will already be - # available, so we can display those values) - if video_obj.duration is not None: - row_list.append( - utils.convert_seconds_to_string(video_obj.duration), - ) - else: - row_list.append(None) - - if video_obj.file_size is not None: - row_list.append(video_obj.get_file_size_string()) - else: - row_list.append(None) - - if video_obj.upload_time is not None: - row_list.append(video_obj.get_upload_date_string()) - else: - row_list.append(None) - - row_list.append(video_obj.dl_flag) - row_list.append(pixbuf2) - row_list.append( - utils.shorten_string( - video_obj.parent_obj.name, - self.medium_string_max_len, - ), - ) - - # Create a new row in the treeview. Doing the .show_all() first - # prevents a Gtk error (for unknown reasons) - self.results_list_treeview.show_all() - if not self.app_obj.results_list_reverse_flag: - self.results_list_liststore.append(row_list) - else: - self.results_list_liststore.prepend(row_list) - - # Store some information about this download so that periodic calls to - # self.results_list_update_row() can retrieve it, and check whether - # the file exists yet - temp_dict = { - 'video_obj': video_obj, - 'row_num': self.results_list_row_count, - } - - if keep_description is not None: - temp_dict['keep_description'] = keep_description - - if keep_info is not None: - temp_dict['keep_info'] = keep_info - - if keep_annotations is not None: - temp_dict['keep_annotations'] = keep_annotations - - if keep_thumbnail is not None: - temp_dict['keep_thumbnail'] = keep_thumbnail - - # Update IVs - self.results_list_temp_list.append(temp_dict) - # (The number of rows has just increased, so increment the IV for the - # next call to this function) - self.results_list_row_count += 1 - - - def results_list_update_row(self): - - """Called by mainapp.TartubeApp.dl_timer_callback(). - - self.results_list_temp_list contains a set of dictionaries, one for - each video download whose file has not yet been confirmed to exist. - - Go through each of those dictionaries. If the file still doesn't exist, - re-insert the dictionary back into self.results_list_temp_list, ready - for it to be checked by the next call to this function. - - If the file does now exist, update the corresponding media.Video - object. Then update the Video Catalogue and the Progress List. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7004 results_list_update_row') - - new_temp_list = [] - - while self.results_list_temp_list: - - temp_dict = self.results_list_temp_list.pop(0) - - # For convenience, retrieve the media.Video object, leaving the - # other values in the dictionary until we need them - video_obj = temp_dict['video_obj'] - # Get the video's full file path now, as we use it several times - video_path = video_obj.get_actual_path(self.app_obj) - - # Because of the 'Requested formats are incompatible for merge and - # will be merged into mkv' warning, we have to check for that - # extension, too - mkv_flag = False - if not os.path.isfile(video_path) and video_obj.file_ext == '.mp4': - - mkv_flag = True - video_path = video_obj.get_actual_path_by_ext( - self.app_obj, - '.mkv', - ) - - # Does the downloaded file now exist on the user's hard drive? - if os.path.isfile(video_path): - - # Update the media.Video object using the temporary dictionary - self.app_obj.update_video_when_file_found( - video_obj, - video_path, - temp_dict, - mkv_flag, - ) - - # The parent container objects can now be sorted - video_obj.parent_obj.sort_children() - self.app_obj.fixed_all_folder.sort_children() - if video_obj.bookmark_flag: - self.app_obj.fixed_bookmark_folder.sort_children() - if video_obj.fav_flag: - self.app_obj.fixed_fav_folder.sort_children() - if video_obj.new_flag: - self.app_obj.fixed_new_folder.sort_children() - if video_obj.waiting_flag: - self.app_obj.fixed_waiting_folder.sort_children() - - # Update the video catalogue in the 'Videos' tab - self.video_catalogue_update_row(video_obj) - - # Prepare icons - if isinstance(video_obj.parent_obj, media.Channel): - pixbuf = self.pixbuf_dict['channel_small'] - elif isinstance(video_obj.parent_obj, media.Channel): - pixbuf = self.pixbuf_dict['playlist_small'] - else: - pixbuf = self.pixbuf_dict['folder_small'] - - # Update the corresponding row in the Results List - tree_path = Gtk.TreePath(temp_dict['row_num']) - row_iter = self.results_list_liststore.get_iter(tree_path) - - self.results_list_liststore.set( - row_iter, - 3, - utils.shorten_string( - video_obj.nickname, - self.medium_string_max_len, - ), - ) - - if video_obj.duration is not None: - self.results_list_liststore.set( - row_iter, - 4, - utils.convert_seconds_to_string( - video_obj.duration, - ), - ) - - if video_obj.file_size: - self.results_list_liststore.set( - row_iter, - 5, - video_obj.get_file_size_string(), - ) - - if video_obj.upload_time: - self.results_list_liststore.set( - row_iter, - 6, - video_obj.get_upload_date_string(), - ) - - self.results_list_liststore.set(row_iter, 7, video_obj.dl_flag) - self.results_list_liststore.set(row_iter, 8, pixbuf) - - self.results_list_liststore.set( - row_iter, - 9, - utils.shorten_string( - video_obj.parent_obj.name, - self.medium_string_max_len, - ), - ) - - else: - - # File not found - - # If this was a simulated download, the key 'keep_description' - # won't exist in temp_dict - # For simulated downloads, we only check once (in case the - # video file already existed on the user's filesystem) - # For real downloads, we check again on the next call to this - # function - if 'keep_description' in temp_dict: - new_temp_list.append(temp_dict) - - # Any files that don't exist yet must be checked on the next call to - # this function - self.results_list_temp_list = new_temp_list - - - # (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 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 - - # 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 output_tab_add_page(self, summary_flag=False): - - """Called by self.output_tab_setup_pages(). - - Adds a new page to the Output Tab's notebook, and updates IVs. - - Args: - - 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 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() - - if not summary_flag: - label = Gtk.Label.new_with_mnemonic( - 'Thread #_' + str(self.output_page_count), - ) - else: - label = Gtk.Label.new_with_mnemonic('_Summary') - - self.output_notebook.append_page(tab, label) - tab.set_hexpand(True) - tab.set_vexpand(True) - tab.set_border_width(self.spacing_size) - - # 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) - - 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' \ - + '}' - ) - - textview = Gtk.TextView() - frame.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_editable(False) - textview.set_cursor_visible(False) - - context = textview.get_style_context() - context.add_provider(style_provider, 600) - - # 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' \ - + '}' - ) - - context = textview2.get_style_context() - context.add_provider(style_provider2, 600) - - # Set up auto-scrolling - textview.connect( - 'size-allocate', - self.output_tab_do_autoscroll, - scrolled, - ) - - # Make the page visible - self.show_all() - - # Update IVs - if not summary_flag: - self.output_textview_dict[self.output_page_count] = textview - else: - self.output_textview_dict[0] = textview - - - def output_tab_set_textview_css(self, css_string): - - """Called by self.output_tab_add_page(). - - 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: - - css_string (str): The CSS style to apply - - Returns: - - The Gtk.CssProvider created - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7287 output_tab_set_textview_css') - - 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 - ) - - return style_provider - - - 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. - - 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. - - 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 - default colour. - - 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(). - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7327 output_tab_write_stdout') - - self.output_tab_insert_list.extend( [page_num, msg, 'default'] ) - - - def output_tab_write_stderr(self, page_num, msg): - - """Called by various functions in downloads.py and info.py. - - 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. - - 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. - - 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(). - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7358 output_tab_write_stderr') - - self.output_tab_insert_list.extend( [page_num, msg, 'error_warning'] ) - - - def output_tab_write_system_cmd(self, page_num, msg): - - """Called by various functions in downloads.py, info.py and updates.py. - - 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. - - 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. - - 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(). - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7388 output_tab_write_system_cmd') - - self.output_tab_insert_list.extend( [page_num, msg, 'system_cmd'] ) - - - def output_tab_update_pages(self): - - """Can be called by anything. - - 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. - - 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). - - Other operations also call this function to display text added to - self.output_tab_insert_list. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7412 output_tab_update_pages') - - update_dict = {} - - if self.output_tab_insert_list: - - while self.output_tab_insert_list: - - 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) - - # 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 - - if msg_type != 'default': - - # 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) - - string = '' \ - + GObject.markup_escape_text(msg) + '\n' - - if msg_type == 'system_cmd': - - textbuffer.insert_markup( - textbuffer.get_end_iter(), - string.format( - self.output_tab_system_cmd_colour, - ), - -1, - ) - - else: - - # STDERR - textbuffer.insert_markup( - textbuffer.get_end_iter(), - string.format(self.output_tab_stderr_colour), - -1, - ) - - else: - - # STDOUT - textbuffer.insert( - textbuffer.get_end_iter(), - msg + '\n', - ) - - # Make the new output visible - for textview in update_dict.values(): - textview.show_all() - - - def output_tab_do_autoscroll(self, textview, rect, scrolled): - - """Called from a callback in self.output_tab_add_page(). - - 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: - - textview (Gtk.TextView): The textview to scroll - - rect (Gdk.Rectangle): Ignored - - scrolled (Gtk.ScrolledWindow): The scroller which contains the - textview - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7494 output_tab_do_autoscroll') - - adj = scrolled.get_vadjustment() - adj.set_value(adj.get_upper() - adj.get_page_size()) - - - def output_tab_scroll_visible_page(self, page_num): - - """Called by self.on_output_notebook_switch_page() and - .on_notebook_switch_page(). - - When the user switches between pages in the Output Tab, scroll the - visible textview to the bottom (otherwise it gets confusing). - - Args: - - page_num (int): The page to be scrolled, matching a key in - self.output_textview_dict - - """ - - 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] - - frame = textview.get_parent() - viewport = frame.get_parent() - scrolled = viewport.get_parent() - - adj = scrolled.get_vadjustment() - adj.set_value(adj.get_upper() - adj.get_page_size()) - - - def output_tab_reset_pages(self): - - """Called by mainapp.TartubeApp.download_manager_continue(), - .update_manager_start(), .refresh_manager_continue(), - .info_manager_start() and .tidy_manager_start(). - - At the start of a download/update/refresh/info/tidy operation, empty - the pages in the Output Tab (if allowed). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7540 output_tab_reset_pages') - - for textview in self.output_textview_dict.values(): - textbuffer = textview.get_buffer() - textbuffer.set_text('') - textview.show_all() - - - # (Errors Tab) - - - def errors_list_reset(self): - - """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 7560 errors_list_reset') - - # 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) - - self.tab_error_count = 0 - self.tab_warning_count = 0 - self.errors_list_refresh_label() - - - def errors_list_add_row(self, media_data_obj): - - """Called by downloads.DownloadWorker.run(). - - When a download job generates error and/or warning messages, this - function is called to display them in the Errors List. - - 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. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7590 errors_list_add_row') - - # 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')) - - if self.app_obj.operation_error_show_flag: - - for msg in media_data_obj.error_list: - - # Prepare the icons - pixbuf = self.pixbuf_dict['error_small'] - - 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)) - - # 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) - - # (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 - - if self.app_obj.operation_warning_show_flag: - - for msg in media_data_obj.warning_list: - - # Prepare the icons - pixbuf = self.pixbuf_dict['warning_small'] - - 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)) - - # 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) - - # (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 - - # Update the tab's label to show the number of warnings/errors visible - if self.visible_tab_num != 3: - self.errors_list_refresh_label() - - - def errors_list_add_system_error(self, error_code, msg): - - """Can be called by anything. The quickest way is to call - mainapp.TartubeApp.system_error(), which acts as a wrapper for this - function. - - Display a system error message in the Errors List. - - Args: - - error_code (int): An error code in the range 100-999 (see - the .system_error() function) - - msg (str): The system error message to display - - """ - - 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')) - - 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), - ) - - # 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) - - # (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 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: - - error_code (int): An error code in the range 100-999 (see - the .system_error() function) - - msg (str): The system warning message to display - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7757 errors_list_add_system_warning') - - 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')) - - 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), - ) - - # 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) - - # (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() - - - def errors_list_refresh_label(self): - - """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 7806 errors_list_refresh_label') - - text = '_Errors' - if self.tab_error_count: - text += ' (' + str(self.tab_error_count) + ')' - - text += ' / Warnings' - if self.tab_warning_count: - text += ' (' + str(self.tab_warning_count) + ')' - - self.errors_label.set_text_with_mnemonic(text) - - - # 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(). - - Adds a set of download options (handled by an - options.OptionsManager object) to 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 7839 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', - ) - - # 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(). - - Check the right-clicked 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 7879 on_video_index_check') - - 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_convert_container(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Converts a channel to a playlist, or a playlist to a channel. - - 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 7907 on_video_index_convert_container') - - 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_custom_dl(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Custom download the right-clicked 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 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', - ) - - # Start a custom download operation - self.app_obj.download_manager_start('custom', False, [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(). - - Deletes the channel, playlist or folder. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The clicked media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 7962 on_video_index_delete_container') - - self.app_obj.delete_container(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(). - - Set the media data object's flag to disable checking and downloading. - - 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 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) - - self.video_index_update_row_text(media_data_obj) - - - def on_video_index_download(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Download the right-clicked 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 8015 on_video_index_download') - - 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_drag_data_received(self, treeview, drag_context, x, y, \ - selection_data, info, timestamp): - - """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: - - 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 - - selection_data (Gtk.SelectionData): Data from the dragged row - - info (int): Ignored - - 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: - - # 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] - - # 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] - - if drag_name and dest_name: - - 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], - ) - - - 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: - - 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 8103 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(). - - 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 8131 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 8162 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 8186 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 8219 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 8240 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: - - 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 8267 on_video_index_mark_archived') - - 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 8298 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_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 bookmarked. - - 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 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) - - 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], - }, - ) - - - 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 bookmarked. - - 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 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: - - # 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], - ) - - 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', False, 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(). - - Mark all of the children of this channel, playlist or folder (and all - of their children, and so on) as favourite. - - 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 8439 on_video_index_mark_favourite') - - self.app_obj.mark_container_favourite( - media_data_obj, - True, - only_child_videos_flag, - ) - - - 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(). - - Mark all videos in this folder (and in any child channels, playlists - and folders) as not favourite. - - 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 8470 on_video_index_mark_not_favourite') - - self.app_obj.mark_container_favourite( - media_data_obj, - False, - only_child_videos_flag, - ) - - - 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 8502 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 8533 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 8559 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( - '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': ['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 8614 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( - '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': ['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 8669 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 8692 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(). - - Removes a set of download options (handled by an - options.OptionsManager object) from 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 8721 on_video_index_remove_options') - - if self.app_obj.current_manager_obj \ - or not media_data_obj.options_obj: - return self.app_obj.system_error( - 229, - 'Callback request denied due to current conditions', - ) - - # Remove download options from the media data object - self.app_obj.remove_download_options(media_data_obj) - - - def on_video_index_remove_videos(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Empties all child videos of a folder object, but doesn't remove any - child channel, playlist or folder objects. - - 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 8750 on_video_index_remove_videos') - - for child_obj in media_data_obj.child_list: - if isinstance(child_obj, media.Video): - self.app_obj.delete_video(child_obj) - - - def on_video_index_rename_location(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Renames a channel, playlist or folder. Also renames the corresponding - directory in Tartube's data directory. - - 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 8774 on_video_index_rename_location') - - self.app_obj.rename_container(media_data_obj) - - - def on_video_index_right_click(self, treeview, event): - - """Called from callback in self.video_index_reset(). - - When the user right-clicks an item in the Video Index, create a - context-sensitive popup menu. - - Args: - - treeview (Gtk.TreeView): The Video Index's treeview - - event (Gdk.EventButton): The event emitting the Gtk signal - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8795 on_video_index_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.video_index_sortmodel.get_iter(path) - if iter is not None: - self.video_index_popup_menu( - event, - self.video_index_sortmodel[iter][1], - ) - - - def on_video_index_selection_changed(self, selection): - - """Called from callback in self.video_index_reset(). - - Also called from callbacks in mainapp.TartubeApp.on_menu_test, - .on_button_switch_view() and .on_menu_add_video(). - - When the user clicks to select an item in the Video Index, call a - function to update the Video Catalogue. - - Args: - - selection (Gtk.TreeSelection): Data for the selected row - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 8837 on_video_index_selection_changed') - - (model, iter) = selection.get_selected() - if iter is None or not model.iter_is_valid(iter): - return - else: - name = model[iter][1] - - # Don't update the Video Catalogue during certain procedures, such as - # removing a row from the Video Index (in which case, the flag will - # be set) - if not self.ignore_video_index_select_flag: - - if iter is None: - self.video_index_current = None - self.video_index_current_priv_flag = False - self.video_catalogue_reset() - - else: - - # Update IVs - self.video_index_current = name - - dbid = self.app_obj.media_name_dict[name] - media_data_obj = self.app_obj.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and media_data_obj.priv_flag: - self.video_index_current_priv_flag = True - else: - self.video_index_current_priv_flag = False - - # Expand the tree beneath the selected line, if allowed - if self.app_obj.auto_expand_video_index_flag: - self.video_index_treeview.expand_to_path( - model.get_path(iter), - ) - - # Redraw the Video Catalogue, on the first page, and reset its - # scrollbars back to the top - self.video_catalogue_redraw_all(name, 1, True) - - - def on_video_index_set_destination(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Sets (or resets) the alternative download destination for the selected - channel, playlist or 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 8897 on_video_index_set_destination') - - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 230, - 'Cannot set the download destination of a video', - ) - - dialogue_win = SetDestinationDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - dbid = dialogue_win.choice - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - if dbid != media_data_obj.master_dbid: - media_data_obj.set_master_dbid(self.app_obj, dbid) - - # Update tooltips for this row - self.video_index_update_row_tooltip(media_data_obj) - - - def on_video_index_set_nickname(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Sets (or resets) the nickname for the selected channel, playlist or - 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 8938 on_video_index_set_nickname') - - if isinstance(media_data_obj, media.Video): - return self.app_obj.system_error( - 231, - 'Cannot set the nickname of a video', - ) - - dialogue_win = SetNicknameDialogue(self, media_data_obj) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window, before destroying it - nickname = dialogue_win.entry.get_text() - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # If nickname is an empty string, then the call to .set_nickname() - # resets the .nickname IV to match the .name IV - media_data_obj.set_nickname(nickname) - - # Update the name displayed in the Video Index - self.video_index_update_row_text(media_data_obj) - - - def on_video_index_show_destination(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens the sub-directory into which all files for the specified media - data object are downloaded (which might be the default sub-directory - for another media data object, if the media data object's .master_dbid - has been modified). - - 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 8982 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) - utils.open_file(path) - - - def on_video_index_show_location(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens the sub-directory into which all files for the specified media - data object are downloaded, by default (which might not be the actual - sub-directory, if the media data object's .master_dbid has been - modified). - - 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 9008 on_video_index_show_location') - - path = media_data_obj.get_default_dir(self.app_obj) - utils.open_file(path) - - - def on_video_index_show_properties(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens an edit window for the 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 9030 on_video_index_show_properties') - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 232, - 'Callback request denied due to current conditions', - ) - - # Open the edit window immediately - if isinstance(media_data_obj, media.Folder): - config.FolderEditWin(self.app_obj, media_data_obj) - else: - config.ChannelPlaylistEditWin(self.app_obj, media_data_obj) - - - def on_video_index_show_system_cmd(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Opens a dialogue window to show the system command that would be used - to download the clicked channel/playlist/folder. - - 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 9061 on_video_index_show_system_cmd') - - # Show the dialogue window - dialogue_win = SystemCmdDialogue(self, media_data_obj) - dialogue_win.run() - dialogue_win.destroy() - - - def on_video_index_tidy(self, menu_item, media_data_obj): - - """Called from a callback in self.video_index_popup_menu(). - - Perform a tidy operation on the right-clicked 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 9085 on_video_index_tidy') - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 233, - 'Callback request denied due to current conditions', - ) - - # Prompt the user to specify which actions should be applied to - # the media data object's directory - dialogue_win = TidyDialogue(self, media_data_obj) - response = dialogue_win.run() - - if response == Gtk.ResponseType.OK: - - # Retrieve user choices from the dialogue window - choices_dict = { - 'media_data_obj': media_data_obj, - 'corrupt_flag': dialogue_win.checkbutton.get_active(), - 'del_corrupt_flag': dialogue_win.checkbutton2.get_active(), - 'exist_flag': dialogue_win.checkbutton3.get_active(), - 'del_video_flag': dialogue_win.checkbutton4.get_active(), - 'del_others_flag': dialogue_win.checkbutton5.get_active(), - 'del_descrip_flag': dialogue_win.checkbutton6.get_active(), - 'del_json_flag': dialogue_win.checkbutton7.get_active(), - 'del_xml_flag': dialogue_win.checkbutton8.get_active(), - 'del_thumb_flag': dialogue_win.checkbutton9.get_active(), - 'del_archive_flag': dialogue_win.checkbutton10.get_active(), - } - - # Now destroy the window - dialogue_win.destroy() - - if response == Gtk.ResponseType.OK: - - # If nothing was selected, then there is nothing to do - # (Don't need to check 'del_others_flag' here) - if not choices_dict['corrupt_flag'] \ - and not choices_dict['exist_flag'] \ - and not choices_dict['del_video_flag'] \ - and not choices_dict['del_descrip_flag'] \ - and not choices_dict['del_json_flag'] \ - and not choices_dict['del_xml_flag'] \ - and not choices_dict['del_thumb_flag'] \ - and not choices_dict['del_archive_flag']: - return - - # Prompt the user for confirmation, before deleting any files - if choices_dict['del_corrupt_flag'] \ - or choices_dict['del_video_flag'] \ - or choices_dict['del_descrip_flag'] \ - or choices_dict['del_json_flag'] \ - or choices_dict['del_xml_flag'] \ - or choices_dict['del_thumb_flag'] \ - 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 - { - 'yes': 'tidy_manager_start', - # Specified options - 'data': choices_dict, - }, - ) - - else: - - # Start the tidy operation now - self.tidy_manager_start(choices_dict) - - - def on_video_catalogue_apply_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Adds a set of download options (handled by an - options.OptionsManager object) to the specified video object. - - 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 9176 on_video_catalogue_apply_options') - - if self.app_obj.current_manager_obj or media_data_obj.options_obj: - return self.app_obj.system_error( - 234, - 'Callback request denied due to current conditions', - ) - - # Apply download options to the media data object - media_data_obj.set_options_obj(options.OptionsManager()) - # Update the video catalogue to show the right icon - self.video_catalogue_update_row(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_catalogue_check(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Check the right-clicked media data object. - - 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 9212 on_video_catalogue_check') - - if self.app_obj.current_manager_obj: - 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] ) - - - def on_video_catalogue_check_multi(self, menu_item, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Check the right-clicked media data object(s). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9239 on_video_catalogue_check_multi') - - if self.app_obj.current_manager_obj: - 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() - - - 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 - - media_data_obj (media.Video): The clicked video object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9269 on_video_catalogue_custom_dl') - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 237, - '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_catalogue_custom_dl_multi(self, menu_item, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Custom download the right-clicked media data objects(s). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9296 on_video_catalogue_custom_dl_multi') - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 238, - 'Callback request denied due to current conditions', - ) - - # Start a download operation - self.app_obj.download_manager_start('custom', False, media_data_list) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_delete_video(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Deletes the right-clicked media data object. - - 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 9326 on_video_catalogue_delete_video') - - self.app_obj.delete_video(media_data_obj, True) - - - def on_video_catalogue_delete_video_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Deletes the right-clicked media data objects. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9347 on_video_catalogue_delete_video_multi') - - for media_data_obj in media_data_list: - self.app_obj.delete_video(media_data_obj, True) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_dl_and_watch(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Downloads a video and then opens it using the system's default media - player. - - 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 9372 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 - if not media_data_obj.dl_flag or not media_data_obj.source \ - or self.app_obj.update_manager_obj \ - or self.app_obj.refresh_manager_obj: - - # 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.app_obj.download_watch_videos( [media_data_obj] ) - - - def on_video_catalogue_dl_and_watch_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Download the videos and then open them using the system's default media - player. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9405 on_video_catalogue_dl_and_watch_multi') - - # Only download videos which have a source URL - mod_list = [] - for media_data_obj in media_data_list: - if media_data_obj.source: - mod_list.append(media_data_obj) - - # Can't download the videos if none have no source, or if an update/ - # refresh operation has started since the popup menu was created - if mod_list \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj: - - # Download the videos, and mark them to be opened in the system's - # default media player as soon as the download operation is - # complete - # If a download operation is already in progress, the videos are - # added to it - self.app_obj.download_watch_videos(mod_list) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_download(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Download the right-clicked media data object. - - 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 9445 on_video_catalogue_download') - - if self.app_obj.current_manager_obj: - 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] ) - - - def on_video_catalogue_download_multi(self, menu_item, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Download the right-clicked media data objects(s). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9472 on_video_catalogue_download_multi') - - if self.app_obj.current_manager_obj: - 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) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_edit_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Edit the download options (handled by an - options.OptionsManager object) for the specified video object. - - 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 9503 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( - 241, - '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_catalogue_enforce_check(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Set the video object's flag to force checking (disabling an actual - downloads). - - 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 9535 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, - # since media.Channel.dl_sim_flag or media.Playlist.dl_sim_flag - # applies instead) - if self.app_obj.current_manager_obj \ - or not isinstance(media_data_obj.parent_obj, media.Folder): - return self.app_obj.system_error( - 242, - '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_catalogue_update_row(media_data_obj) - - - def on_video_catalogue_fetch_formats(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Fetches a list of available video/audio formats for the specified - video, using an info operation. - - 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 9572 on_video_catalogue_fetch_formats') - - # Can't start an info operation if any type of operation has started - # since the popup menu was created - if media_data_obj.source \ - and not self.app_obj.current_manager_obj: - - # Fetch information about the video's available formats - self.app_obj.info_manager_start('formats', media_data_obj) - - - def on_video_catalogue_fetch_subs(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Fetches a list of available subtitles for the specified video, using an - info operation. - - 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 9599 on_video_catalogue_fetch_subs') - - # Can't start an info operation if any type of operation has started - # since the popup menu was created - if media_data_obj.source \ - and not self.app_obj.current_manager_obj: - - # Fetch information about the video's available subtitles - self.app_obj.info_manager_start('subs', 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(). - - Creates a media.Video object in the 'Temporary Videos' folder. The new - video object has the same source URL as the specified media_data_obj. - - 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 9626 on_video_catalogue_mark_temp_dl') - - # Can't mark the video for download if it has no source, or if an - # update/refresh/tidy operation has started since the popup menu was - # created - if media_data_obj.source \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj: - - # Create a new media.Video object in the 'Temporary Videos' folder - # (but don't download anything now) - self.app_obj.add_video( - self.app_obj.fixed_temp_folder, - media_data_obj.source, - ) - - - def on_video_catalogue_mark_temp_dl_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Creates new media.Video objects in the 'Temporary Videos' folder. The - new video objects have the same source URL as the video objects in the - specified media_data_list. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9662 on_video_catalogue_temp_dl_multi') - - # Only download videos which have a source URL - mod_list = [] - for media_data_obj in media_data_list: - if media_data_obj.source: - mod_list.append(media_data_obj) - - # Can't mark the videos for download if they have no source, or if an - # update/refresh/tidy operation has started since the popup menu was - # created - if mod_list \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj: - - for media_data_obj in mod_list: - - # Create a new media.Video object in the 'Temporary Videos' - # folder - new_media_data_obj = self.app_obj.add_video( - self.app_obj.fixed_temp_folder, - media_data_obj.source, - ) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_page_entry_activated(self, entry): - - """Called from a callback in self.setup_videos_tab(). - - Switches to a different page in the Video Catalogue (or re-inserts the - current page number, if the user typed an invalid page number). - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 9706 on_video_catalogue_page_entry_activated', - ) - - page_num = utils.strip_whitespace(entry.get_text()) - - if self.video_index_current is None \ - or not page_num.isdigit() \ - or int(page_num) < 1 \ - or int(page_num) > self.catalogue_toolbar_last_page: - # Invalid page number, so reinsert the number of the page that's - # actually visible - entry.set_text(str(self.catalogue_toolbar_current_page)) - - else: - # Switch to a different page - self.video_catalogue_redraw_all( - self.video_index_current, - int(page_num), - ) - - - def on_video_catalogue_re_download(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Re-downloads the right-clicked media data object. - - 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 9742 on_video_catalogue_re_download') - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 243, - 'Callback request denied due to current conditions', - ) - - # If the file exists, delete it (otherwise youtube-dl won't download - # anything) - # Don't even check media.Video.dl_flag: the file might exist, even if - # the flag has not been set - if media_data_obj.file_name: - - path = media_data_obj.get_actual_path(self.app_obj) - - if os.path.isfile(path): - os.remove(path) - - # No download operation will start, if the media.Video object is marked - # as downloaded - self.app_obj.mark_video_downloaded(media_data_obj, False) - - # 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. 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) - - # Now we're ready to start the download operation - self.app_obj.download_manager_start('real', False, [media_data_obj] ) - - - def on_video_catalogue_remove_options(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Removes a set of download options (handled by an - options.OptionsManager object) from the specified video object. - - 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 9793 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( - 244, - 'Callback request denied due to current conditions', - ) - - # Remove download options from the media data object - media_data_obj.set_options_obj(None) - # Update the video catalogue to show the right icon - self.video_catalogue_update_row(media_data_obj) - - - def on_video_catalogue_size_entry_activated(self, entry): - - """Called from a callback in self.setup_videos_tab(). - - Sets the page size, and redraws the Video Catalogue (with the first - page visible). - - Args: - - entry (Gtk.Entry): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 9822 on_video_catalogue_size_entry_activated', - ) - - size = utils.strip_whitespace(entry.get_text()) - - if size.isdigit(): - self.app_obj.set_catalogue_page_size(int(size)) - - # Need to completely redraw the video catalogue to take account of - # the new page size - if self.video_index_current is not None: - self.video_catalogue_redraw_all(self.video_index_current, 1) - - else: - # Invalid page size, so reinsert the size that's already visible - entry.set_text(str(self.catalogue_page_size)) - - - def on_video_catalogue_show_location(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Shows the actual sub-directory in which the specified video is stored - (which might be different from the default sub-directory, if the media - data object's .master_dbid has been modified). - - 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 9857 on_video_catalogue_show_location') - - parent_obj = media_data_obj.parent_obj - other_obj = self.app_obj.media_reg_dict[parent_obj.master_dbid] - path = other_obj.get_actual_dir(self.app_obj) - utils.open_file(path) - - - def on_video_catalogue_show_properties(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Opens an edit window for the video object. - - 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 9880 on_video_catalogue_show_properties') - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 245, - 'Callback request denied due to current conditions', - ) - - # Open the edit window immediately - config.VideoEditWin(self.app_obj, media_data_obj) - - - def on_video_catalogue_show_properties_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Opens an edit window for each video object. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 9909 on_video_catalogue_show_properties_multi', - ) - - if self.app_obj.current_manager_obj: - return self.app_obj.system_error( - 246, - 'Callback request denied due to current conditions', - ) - - # Open the edit window immediately - for media_data_obj in media_data_list: - config.VideoEditWin(self.app_obj, media_data_obj) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_show_system_cmd(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Opens a dialogue window to show the system command that would be used - to download the clicked video. - - 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 9942 on_video_catalogue_show_system_cmd') - - # Show the dialogue window - dialogue_win = SystemCmdDialogue(self, media_data_obj) - dialogue_win.run() - dialogue_win.destroy() - - - def on_video_catalogue_temp_dl(self, menu_item, media_data_obj, \ - watch_flag=False): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Creates a media.Video object in the 'Temporary Videos' folder. The new - video object has the same source URL as the specified media_data_obj. - - Downloads the video and optionally opens it using the system's default - media player. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_obj (media.Video): The clicked video object - - watch_flag (bool): If True, the video is opened using the system's - default media player, after being downloaded - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 9973 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 - if media_data_obj.source \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj: - - # Create a new media.Video object in the 'Temporary Videos' folder - new_media_data_obj = self.app_obj.add_video( - self.app_obj.fixed_temp_folder, - media_data_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.app_obj.download_watch_videos( - [new_media_data_obj], - watch_flag, - ) - - - def on_video_catalogue_temp_dl_multi(self, menu_item, - media_data_list, watch_flag=False): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Creates new media.Video objects in the 'Temporary Videos' folder. The - new video objects have the same source URL as the video objects in the - specified media_data_list. - - Downloads the videos and optionally opens them using the system's - default media player. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - watch_flag (bool): If True, the video is opened using the system's - default media player, after being downloaded - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10024 on_video_catalogue_temp_dl_multi') - - # Only download videos which have a source URL - mod_list = [] - for media_data_obj in media_data_list: - if media_data_obj.source: - mod_list.append(media_data_obj) - - # Can't download the videos if none have no source, or if an update/ - # refresh/tidy operation has started since the popup menu was created - ready_list = [] - if mod_list \ - and not self.app_obj.update_manager_obj \ - and not self.app_obj.refresh_manager_obj \ - and not self.app_obj.tidy_manager_obj: - - for media_data_obj in mod_list: - - # Create a new media.Video object in the 'Temporary Videos' - # folder - new_media_data_obj = self.app_obj.add_video( - self.app_obj.fixed_temp_folder, - media_data_obj.source, - ) - - if new_media_data_obj: - ready_list.append(new_media_data_obj) - - if ready_list: - - # Download the videos. If a download operation is already in - # progress, the videos are added to it - # Optionally open the videos in the system's default media player - self.app_obj.download_watch_videos(ready_list, watch_flag) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_test_dl(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Prompts the user to specify a URL and youtube-dl options. If the user - specifies one or both, launches an info operation to test youtube-dl - using the specified values. - - 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 10080 on_video_catalogue_test_dl') - - # Can't start an info operation if any type of operation has started - # since the popup menu was created - if not self.app_obj.current_manager_obj: - - # Prompt the user for what should be tested - dialogue_win = TestCmdDialogue(self, media_data_obj.source) - response = dialogue_win.run() - - # Retrieve user choices from the dialogue window... - source = dialogue_win.entry.get_text() - options_string = dialogue_win.textbuffer.get_text( - dialogue_win.textbuffer.get_start_iter(), - dialogue_win.textbuffer.get_end_iter(), - False, - ) - - # ...before destroying it - dialogue_win.destroy() - - # If the user specified either (or both) a URL and youtube-dl - # options, then we can proceed - if response == Gtk.ResponseType.OK \ - and (source != '' or options_string != ''): - - # Start the info operation, which issues the youtube-dl command - # with the specified options - self.app_obj.info_manager_start( - 'test_ytdl', - None, # No media.Video object in this case - source, # Use the source, if specified - options_string, # Use download options, if specified - ) - - - def on_video_catalogue_toggle_archived_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as archived or not archived. - - 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 10133 on_video_catalogue_toggle_archived_video', - ) - - if not media_data_obj.archive_flag: - media_data_obj.set_archive_flag(True) - else: - media_data_obj.set_archive_flag(False) - - self.video_catalogue_update_row(media_data_obj) - - - def on_video_catalogue_toggle_archived_video_multi(self, menu_item, - archived_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as archived or not archived. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - archived_flag (bool): True to mark the videos as archived, False to - mark the videos as not archived - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 10164 on_video_catalogue_toggle_archived_video_multi', - ) - - for media_data_obj in media_data_list: - media_data_obj.set_archive_flag(archived_flag) - self.video_catalogue_update_row(media_data_obj) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_toggle_bookmark_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as bookmarked or not bookmarked. - - 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 10192 on_video_catalogue_toggle_bookmark_video', - ) - - if not media_data_obj.bookmark_flag: - self.app_obj.mark_video_bookmark(media_data_obj, True) - else: - self.app_obj.mark_video_bookmark(media_data_obj, False) - - - def on_video_catalogue_toggle_bookmark_video_multi(self, menu_item, - bookmark_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as bookmarked or not bookmarked. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - bookmark_flag (bool): True to mark the videos as bookmarked, False - to mark the videos as not bookmarked - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 10221 on_video_catalogue_toggle_bookmark_video_multi', - ) - - for media_data_obj in media_data_list: - self.app_obj.mark_video_bookmark(media_data_obj, bookmark_flag) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_toggle_favourite_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as favourite or not favourite. - - 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 10248 on_video_catalogue_toggle_favourite_video', - ) - - if not media_data_obj.fav_flag: - self.app_obj.mark_video_favourite(media_data_obj, True) - else: - self.app_obj.mark_video_favourite(media_data_obj, False) - - - def on_video_catalogue_toggle_favourite_video_multi(self, menu_item, - fav_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as favourite or not favourite. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - fav_flag (bool): True to mark the videos as favourite, False to - mark the videos as not favourite - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 10277 on_video_catalogue_toggle_favourite_video_multi', - ) - - for media_data_obj in media_data_list: - self.app_obj.mark_video_favourite(media_data_obj, fav_flag) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_toggle_new_video(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as new (unwatched) or not new (watched). - - 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 10302 on_video_catalogue_toggle_new_video') - - if not media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, True) - else: - self.app_obj.mark_video_new(media_data_obj, False) - - - def on_video_catalogue_toggle_new_video_multi(self, menu_item, - new_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as new (unwatched) or not new (watched). - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - new_flag (bool): True to mark the videos as favourite, False to - mark the videos as not favourite - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 10330 on_video_catalogue_toggle_new_video_multi', - ) - - for media_data_obj in media_data_list: - self.app_obj.mark_video_new(media_data_obj, new_flag) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_toggle_waiting_video(self, menu_item, \ - media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Marks the video as in the waiting list or not in the waiting list. - - 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 10357 on_video_catalogue_toggle_waiting_video', - ) - - if not media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, True) - else: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_toggle_waiting_video_multi(self, menu_item, - waiting_flag, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Mark the videos as in the waiting list or not in the waiting list. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - waiting_flag (bool): True to mark the videos as in the waiting - list, False to mark the videos as not in the waiting list - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 10386 on_video_catalogue_toggle_waiting_video_multi', - ) - - for media_data_obj in media_data_list: - self.app_obj.mark_video_waiting(media_data_obj, waiting_flag) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_watch_hooktube(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Watch a YouTube video on HookTube. - - 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 10411 on_video_catalogue_watch_hooktube') - - # Launch the video - utils.open_file( - utils.convert_youtube_to_hooktube(media_data_obj.source), - ) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_watch_invidious(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Watch a YouTube video on Invidious. - - 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 10441 on_video_catalogue_watch_invidious') - - # Launch the video - utils.open_file( - utils.convert_youtube_to_invidious(media_data_obj.source), - ) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_watch_video(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Watch a video using the system's default media player, first checking - that a file actually exists. - - 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 10472 on_video_catalogue_watch_video') - - # Launch the video - self.app_obj.watch_video_in_player(media_data_obj) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_watch_video_multi(self, menu_item, media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Watch the videos using the system's default media player, first - checking that the files actually exist. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10501 on_video_catalogue_watch_video_multi') - - # Only watch videos which are marked as downloaded - for media_data_obj in media_data_list: - if media_data_obj.dl_flag: - - self.app_obj.watch_video_in_player(media_data_obj) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_video_catalogue_watch_website(self, menu_item, media_data_obj): - - """Called from a callback in self.video_catalogue_popup_menu(). - - Watch a video on its primary website. - - 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 10535 on_video_catalogue_watch_website') - - # Launch the video - utils.open_file(media_data_obj.source) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_video_catalogue_watch_website_multi(self, menu_item, - media_data_list): - - """Called from a callback in self.video_catalogue_multi_popup_menu(). - - Watch videos on their primary websites. - - Args: - - menu_item (Gtk.MenuItem): The clicked menu item - - media_data_list (list): List of one or more media.Video objects - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 10565 on_video_catalogue_watch_website_multi', - ) - - # Only watch videos which have a source URL - for media_data_obj in media_data_list: - if media_data_obj.source is not None: - - # Launch the video - utils.open_file(media_data_obj.source) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - # Standard de-selection of everything in the Video Catalogue - self.catalogue_listbox.unselect_all() - - - def on_progress_list_dl_last(self, menu_item, download_item_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Moves the selected media data object to the bottom of the - downloads.DownloadList, so it is assigned to the last available worker. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - download_item_obj (downloads.DownloadItem): The download item - object for the selected media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10603 on_progress_list_dl_last') - - # Check that, since the popup menu was created, the media data object - # hasn't been assigned a worker - for this_worker_obj in self.app_obj.download_manager_obj.worker_list: - if this_worker_obj.running_flag \ - and this_worker_obj.download_item_obj == download_item_obj \ - and this_worker_obj.video_downloader_obj is not None: - return - - # Assign this media data object to the last available worker - download_list_obj = self.app_obj.download_manager_obj.download_list_obj - download_list_obj.move_item_to_bottom(download_item_obj) - - # Change the row's icon to show that it will be checked/downloaded - # last - # (Because of the way the Progress List has been set up, borrowing from - # the design in youtube-dl-gui, reordering the rows in the list is - # not practial) - 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), - 2, - self.pixbuf_dict['arrow_down_small'], - ) - - - def on_progress_list_dl_next(self, menu_item, download_item_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Moves the selected media data object to the top of the - downloads.DownloadList, so it is assigned to the next available worker. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - download_item_obj (downloads.DownloadItem): The download item - object for the selected media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10650 on_progress_list_dl_next') - - # Check that, since the popup menu was created, the media data object - # hasn't been assigned a worker - for this_worker_obj in self.app_obj.download_manager_obj.worker_list: - if this_worker_obj.running_flag \ - and this_worker_obj.download_item_obj == download_item_obj \ - and this_worker_obj.video_downloader_obj is not None: - return - - # Assign this media data object to the next available worker - download_list_obj = self.app_obj.download_manager_obj.download_list_obj - download_list_obj.move_item_to_top(download_item_obj) - - # Change the row's icon to show that it will be checked/downloaded - # next - 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), - 2, - self.pixbuf_dict['arrow_up_small'], - ) - - - def on_progress_list_right_click(self, treeview, event): - - """Called from callback in self.setup_progress_tab(). - - When the user right-clicks an item in the Progress List, create a - context-sensitive popup menu. - - Args: - - treeview (Gtk.TreeView): The Progress List's treeview - - event (Gdk.EventButton): The event emitting the Gtk signal - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10693 on_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.progress_list_liststore.get_iter(path) - if iter is not None: - self.progress_list_popup_menu( - event, - self.progress_list_liststore[iter][0], - self.progress_list_liststore[iter][1], - ) - - - def on_progress_list_stop_all_soon(self, menu_item): - - """Called from a callback in self.progress_list_popup_menu(). - - Halts checking/downloading the selected media data object, after the - current video check/download has finished. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10733 on_progress_list_stop_soon') - - # Check that, since the popup menu was created, the download operation - # hasn't finished - if not self.app_obj.download_manager_obj: - # Do nothing - return - - # Tell the download manager to continue downloading the current videos - # (if any), and then stop - self.app_obj.download_manager_obj.stop_download_operation_soon() - - - def on_progress_list_stop_now(self, menu_item, download_item_obj, - worker_obj, video_downloader_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Halts checking/downloading the selected media data object. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - download_item_obj (downloads.DownloadItem): The download item - object for the selected media data object - - worker_obj (downloads.DownloadWorker): The worker currently - handling checking/downloading this media data object - - video_downloader_obj (downloads.VideoDownloader): The video - downloader handling checking/downloading this media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10769 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 - # data object - if not self.app_obj.download_manager_obj \ - or not worker_obj.running_flag \ - or worker_obj.download_item_obj != download_item_obj \ - or worker_obj.video_downloader_obj is None: - # Do nothing - return - - # Stop the video downloader (causing the worker to be assigned a new - # downloads.DownloadItem, if there are any left) - video_downloader_obj.stop() - - - def on_progress_list_stop_soon(self, menu_item, download_item_obj, - worker_obj, video_downloader_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Halts checking/downloading the selected media data object, after the - current video check/download has finished. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - download_item_obj (downloads.DownloadItem): The download item - object for the selected media data object - - worker_obj (downloads.DownloadWorker): The worker currently - handling checking/downloading this media data object - - video_downloader_obj (downloads.VideoDownloader): The video - downloader handling checking/downloading this media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10810 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 - # data object - if not self.app_obj.download_manager_obj \ - or not worker_obj.running_flag \ - or worker_obj.download_item_obj != download_item_obj \ - or worker_obj.video_downloader_obj is None: - # Do nothing - return - - # Tell the video downloader to stop after the current video check/ - # download has finished - video_downloader_obj.stop_soon() - - - def on_progress_list_watch_hooktube(self, menu_item, media_data_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Opens the clicked video, which is a YouTube video, on the HookTube - website. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The corresponding media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10843 on_progress_list_watch_hooktube') - - if isinstance(media_data_obj, media.Video): - - # Launch the video - utils.open_file( - utils.convert_youtube_to_hooktube(media_data_obj.source), - ) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_progress_list_watch_invidious(self, menu_item, media_data_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Opens the clicked video, which is a YouTube video, on the Invidious - website. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The corresponding media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10876 on_progress_list_watch_invidious') - - if isinstance(media_data_obj, media.Video): - - # Launch the video - utils.open_file( - utils.convert_youtube_to_invidious(media_data_obj.source), - ) - - # Mark the video as not new (having been watched) - if media_data_obj.new_flag: - self.app_obj.mark_video_new(media_data_obj, False) - # Remove the video from the waiting list (having been watched) - if media_data_obj.waiting_flag: - self.app_obj.mark_video_waiting(media_data_obj, False) - - - def on_progress_list_watch_website(self, menu_item, media_data_obj): - - """Called from a callback in self.progress_list_popup_menu(). - - Opens the clicked video's source URL in a web browser. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The corresponding media data object - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10908 on_progress_list_watch_website') - - if isinstance(media_data_obj, media.Video) \ - and media_data_obj.source: - - utils.open_file(media_data_obj.source) - - - def on_results_list_delete_video(self, menu_item, media_data_obj, path): - - """Called from a callback in self.results_list_popup_menu(). - - Deletes the video, and removes a row from the Results List. - - Args: - - menu_item (Gtk.MenuItem): The menu item that was clicked - - media_data_obj (media.Video): The video displayed on the clicked - row - - path (Gtk.TreePath): Path to the clicked row in the treeview - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 10934 on_results_list_delete_video') - - # Delete the video - self.app_obj.delete_video(media_data_obj, True) - - # Remove the row from the Results List - iter = self.results_list_liststore.get_iter(path) - self.results_list_liststore.remove(iter) - - - def on_results_list_right_click(self, treeview, event): - - """Called from callback in self.setup_progress_tab(). - - When the user right-clicks an item in the Results List, create 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 10960 on_results_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.results_list_liststore.get_iter(path) - if iter is not None: - self.results_list_popup_menu( - event, - path, - self.results_list_liststore[iter][0], - ) - - - def on_errors_list_clear(self, button): - - """Called from callback in self.setup_errors_tab(). - - In the Errors Tab, when the user clicks the 'Clear the list' button, - clear the Errors List. - - Args: - - button (Gtk.Button): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11000 on_errors_list_clear') - - self.errors_list_reset() - - - def on_bandwidth_spinbutton_changed(self, spinbutton): - - """Called from callback in self.setup_progress_tab(). - - In the Progress Tab, when the user sets the bandwidth limit, inform - mainapp.TartubeApp. The new setting is applied to the next download - job. - - Args: - - spinbutton (Gtk.SpinButton): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11020 on_bandwidth_spinbutton_changed') - - self.app_obj.set_bandwidth_default( - int(self.bandwidth_spinbutton.get_value()) - ) - - - def on_bandwidth_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_progress_tab(). - - In the Progress Tab, when the user turns the bandwidth 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 11042 on_bandwidth_checkbutton_changed') - - self.app_obj.set_bandwidth_apply_flag( - self.bandwidth_checkbutton.get_active(), - ) - - - def on_delete_event(self, widget, event): - - """Called from callback in self.setup_win(). - - If the user click-closes the window, close to the system tray (if - required), rather than closing the application. - - Args: - - widget (mainwin.MainWin): The main window - - event (Gdk.Event): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11065 on_delete_event') - - if self.app_obj.status_icon_obj \ - and self.app_obj.show_status_icon_flag \ - and self.app_obj.close_to_tray_flag \ - and self.is_visible(): - - # Close to the system tray - self.toggle_visibility() - return True - - else: - - # Allow the application to close as normal - return False - - - def on_hide_finished_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_progress_tab(). - - Toggles hiding finished rows in the Progress List. - - Args: - - checkbutton (Gtk.CheckButton) - The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11095 on_hide_finished_checkbutton_changed') - - self.app_obj.set_progress_list_hide_flag(checkbutton.get_active()) - - - def on_notebook_switch_page(self, notebook, box, page_num): - - """Called from callback in self.setup_notebook(). - - The Errors / Warnings tab shows the number of errors/warnings in its - tab label. When the user switches to this tab, reset the tab label. - - Args: - - notebook (Gtk.Notebook): The main window's notebook, providing - several tabs - - box (Gtk.Box) - The box in which the tab's widgets are placed - - page_num (int) - The number of the newly-visible tab (the Videos - Tab is number 0) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11120 on_notebook_switch_page') - - self.visible_tab_num = page_num - - if page_num == 2: - # Switching between tabs causes pages in the Output Tab to scroll - # to the top. Make sure they're all scrolled back to the bottom - - # Take into account range()... - page_count = self.output_page_count + 1 - # ...take into account the summary page, if present - if self.output_tab_summary_flag: - page_count += 1 - - 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: - # 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): - - """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 11159 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 11182 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()) - ) - - else: - - self.app_obj.set_num_worker_apply_flag(False) - - - def on_operation_error_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of operation error messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton) - The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 11210 on_operation_error_checkbutton_changed', - ) - - self.app_obj.set_operation_error_show_flag(checkbutton.get_active()) - - - def on_operation_warning_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of operation warning messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton) - The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 11230 on_operation_warning_checkbutton_changed', - ) - - self.app_obj.set_operation_warning_show_flag(checkbutton.get_active()) - - - def on_output_notebook_switch_page(self, notebook, box, page_num): - - """Called from callback in self.setup_output_tab(). - - When the user switches between pages in the Output Tab, scroll the - visible textview to the bottom (otherwise it gets confusing). - - Args: - - notebook (Gtk.Notebook): The Output Tab's notebook, providing - several pages - - box (Gtk.Box) - The box in which the page's widgets are placed - - page_num (int) - The number of the newly-visible page (the first - page is number 0) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11256 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) - - - def on_reverse_results_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_progress_tab(). - - Toggles reversing the order of the Results List. - - Args: - - checkbutton (Gtk.CheckButton) - The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time( - 'mwn 11276 on_reverse_results_checkbutton_changed', - ) - - self.app_obj.set_results_list_reverse_flag(checkbutton.get_active()) - - - def on_system_error_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of system error messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton) - The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11295 on_system_error_checkbutton_changed') - - self.app_obj.set_system_error_show_flag(checkbutton.get_active()) - - - def on_system_warning_checkbutton_changed(self, checkbutton): - - """Called from callback in self.setup_errors_tab(). - - Toggles display of system warning messages in the tab. - - Args: - - checkbutton (Gtk.CheckButton) - The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11313 on_system_warning_checkbutton_changed') - - self.app_obj.set_system_warning_show_flag(checkbutton.get_active()) - - - def on_window_drag_data_received(self, widget, context, x, y, data, info, - time): - - """Called from callback in self.setup_win(). - - This function is required for detecting when the user drags and drops - videos (for example, from a web browser) into the main window. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 11328 on_window_drag_data_received') - - 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' - parent_obj = None - if self.video_index_current is not None: - dbid \ - = self.app_obj.media_name_dict[self.video_index_current] - parent_obj = self.app_obj.media_reg_dict[dbid] - - if parent_obj.priv_flag: - parent_obj = None - - if not parent_obj: - parent_obj = self.app_obj.fixed_misc_folder - - # Split text into a list of lines and filter out invalid URLs - video_list = [] - duplicate_list = [] - for line in text.split('\n'): - - # Remove leading/trailing whitespace - line = utils.strip_whitespace(line) - - # Perform checks on the URL. If it passes, remove leading/ - # trailing whitespace - if utils.check_url(line): - video_list.append(utils.strip_whitespace(line)) - - # Check everything in the list against other media.Video - # objects with the same parent folder - for line in video_list: - if parent_obj.check_duplicate_video(line): - duplicate_list.append(line) - else: - self.app_obj.add_video(parent_obj, line) - - # In the Video Index, select the parent media data object, - # which updates both the Video Index and the Video - # Catalogue - self.video_index_select_row(parent_obj) - - # If any duplicates were found, inform the user - if duplicate_list: - - msg = 'The following videos are duplicates:' - for line in duplicate_list: - msg += '\n\n' + line - - self.app_obj.dialogue_manager_obj.show_msg_dialogue( - msg, - 'warning', - 'ok', - ) - - # Without this line, the user's cursor is permanently stuck in drag - # and drop mode - context.finish(True, False, time) - - - 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 11409 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 11431 on_video_res_checkbutton_changed') - - self.app_obj.set_video_res_apply_flag( - self.video_res_checkbutton.get_active(), - ) - - - # 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 11454 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 11482 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 11499 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 11533 __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 - - - # 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 11582 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) - - 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 11634 update_widgets') - - self.update_tooltips() - self.update_status_image() - self.update_video_name() - self.update_parent_name() - self.update_video_stats() - - - 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 11651 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 11670 update_status_image') - - # 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'], - ) - - - 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 11696 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 11750 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 11782 update_video_stats') - - if self.video_obj.duration is not None: - string = 'Duration: ' + utils.convert_seconds_to_string( - self.video_obj.duration, - True, - ) - - else: - 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' - - self.stats_label.set_markup(string) - - - # 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 11829 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 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 - - - # 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 - - - # 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 11940 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) - - 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 - vbox = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, - spacing=0, - ) - hbox.pack_start(vbox, False, False, 0) - - self.thumb_image = Gtk.Image() - vbox.pack_start(self.thumb_image, False, False, 0) - - # 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) - - # First row - video name - hbox2 = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, - spacing=0, - ) - vbox2.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) - vbox2.pack_start(self.descrip_label, True, True, 0) - self.descrip_label.connect( - 'activate-link', - self.on_click_descrip_label, - ) - - # 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) - - self.watch_label = Gtk.Label('Watch: ', xalign=0) - hbox3.pack_start(self.watch_label, False, False, 0) - - # 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, - ) - - # 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, - ) - - # 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, - ) - - # 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 - - # 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: - - 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) - - # 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, - ) - - # 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, - ) - - # 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, - ) - - # 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) - - 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, - ) - - # 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, - ) - - # 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, - ) - - # 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, - ) - - - 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 12197 update_widgets') - - 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 ( - 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() - - 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 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 12233 update_tooltips') - - 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, - ), - ) - - - def update_thumb_image(self): - - """Called by anything, but mainly called by self.update_widgets(). - - 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') - - # See if the video's thumbnail file has been downloaded - thumb_flag = False - if self.video_obj.file_name: - - # 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: - - # 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, - ), - ) - - # 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 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 12310 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.quite_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.quite_long_string_max_len, - ), - quote=True, - ) + '' - ) - - - def update_status_images(self): - - """Called by anything, but mainly called by self.update_widgets(). - - 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') - - # 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'], - ) - - # 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.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'], - ) - - elif self.video_obj.error_list: - - self.warning_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['error_small'], - ) - - self.error_image.clear() - - elif self.video_obj.warning_list: - - self.warning_image.set_from_pixbuf( - self.main_win_obj.pixbuf_dict['warning_small'], - ) - - self.error_image.clear() - - else: - - self.error_image.clear() - self.warning_image.clear() - - - def update_video_descrip(self): - - """Called by anything, but mainly called by self.update_widgets(). - - 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': - - # Show the first line of the video description, or all of it, - # depending on settings - if self.video_obj.short: - - # 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') - - if not self.expand_descrip_flag: - - string = html.escape( - utils.shorten_string( - line_list[0], - self.main_win_obj.very_long_string_max_len, - ), - quote=True, - ) - - if len(line_list) > 1: - self.descrip_label.set_markup( - 'More ' + string, - ) - else: - self.descrip_label.set_text(string) - - else: - - descrip = html.escape(self.video_obj.descrip, quote=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') - - else: - - # 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, - ) + '\'' - - if not self.video_obj.descrip: - self.descrip_label.set_text(string) - - elif not self.expand_descrip_flag: - - self.descrip_label.set_markup( - 'More ' + string, - ) - - else: - - descrip = html.escape(self.video_obj.descrip, quote=True) - self.descrip_label.set_markup( - 'Less ' + string + '\n' + descrip \ - + '\n', - ) - - - 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 12516 update_video_stats') - - if self.video_obj.duration is not None: - string = 'Duration: ' + utils.convert_seconds_to_string( - self.video_obj.duration, - True, - ) - - else: - 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' - - self.stats_label.set_markup(string) - - - def update_watch_player(self): - - """Called by anything, but mainly called by self.update_widgets(). - - 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') - - if 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: - - # Link clickable - self.watch_player_label.set_markup( - 'Download & watch', - ) - - else: - - # Link not clickable - self.watch_player_label.set_markup('Not downloaded') - - - def update_watch_web(self): - - """Called by anything, but mainly called by self.update_widgets(). - - 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') - - if self.video_obj.source: - - # For YouTube URLs, offer alternative links - source = self.video_obj.source - if utils.is_youtube(source): - - # Links clickable - self.watch_web_label.set_markup( - 'YouTube', - ) - - self.watch_hooktube_label.set_markup( - 'HookTube', - ) - - self.watch_invidious_label.set_markup( - 'Invidious', - ) - - else: - - self.watch_web_label.set_markup( - 'Website', - ) - - self.watch_hooktube_label.set_text('') - self.watch_invidious_label.set_text('') - - else: - - # Link not clickable - self.watch_web_label.set_markup('No weblink') - self.watch_hooktube_label.set_text('') - self.watch_invidious_label.set_text('') - - - def update_temp_labels(self): - - """Called by anything, but mainly called by self.update_widgets(). - - Updates the clickable Gtk.Label widget for temporary video downloads. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12651 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 = '' - - # (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', - ) - - self.temp_dl_label.set_markup( - 'Download', - ) - - self.temp_dl_watch_label.set_markup( - 'D/L and watch', - ) - - else: - - 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') - - - def update_marked_labels(self): - - """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 12693 update_marked_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 = '' - - # Archived/not archived - if not self.video_obj.archive_flag: - - self.marked_archive_label.set_markup( - 'Archived', - ) - - else: - - self.marked_archive_label.set_markup( - 'Archived', - ) - - # Bookmarked/not bookmarked - if not self.video_obj.bookmark_flag: - - self.marked_bookmark_label.set_markup( - 'Bookmarked', - ) - - else: - - self.marked_bookmark_label.set_markup( - 'Bookmarked', - ) - - # Favourite/not favourite - if not self.video_obj.fav_flag: - - self.marked_fav_label.set_markup( - 'Favourite', - ) - - else: - - self.marked_fav_label.set_markup( - 'Favourite', - ) - - # New/not new - if not self.video_obj.new_flag: - - self.marked_new_label.set_markup( - 'New', - ) - - else: - - self.marked_new_label.set_markup( - 'New', - ) - - # In waiting list/not in waiting list - if not self.video_obj.waiting_flag: - - self.marked_playlist_label.set_markup( - 'In waiting list', - ) - - else: - - self.marked_playlist_label.set_markup( - 'In waiting list', - ) - - - # Callback methods - - - def on_click_descrip_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. - - Args: - - label (Gtk.Label): The clicked widget - - uri (str): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12792 on_click_descrip_label') - - if not self.expand_descrip_flag: - self.expand_descrip_flag = True - else: - self.expand_descrip_flag = 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.descrip_label.set_markup('') - GObject.timeout_add(0, self.update_video_descrip) - - - def on_click_marked_archive_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as archived or not archived. - - 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 12826 on_click_marked_archive_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) - - # 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) - - return True - - - def on_click_marked_bookmark_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as bookmarked or not bookmarked. - - 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 12964 on_click_marked_bookmark_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, - ) - - else: - self.main_win_obj.app_obj.mark_video_bookmark( - 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_bookmark_label.set_markup('Not bookmarked') - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_fav_label(self, label, uri): - - """Called from callback in self.draw_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 - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 12909 on_click_marked_fav_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, - ) - - 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') - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_marked_new_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video as new or not new. - - 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 12954 on_click_marked_new_label') - - # 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) - - # 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) - - return True - - - def on_click_marked_waiting_list_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. - - 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 12992 on_click_marked_waiting_list_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, - ) - - else: - 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_playlist_label.set_markup('Not in waiting list') - - GObject.timeout_add(0, self.update_marked_labels) - - return True - - - def on_click_temp_dl_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Download the video into the 'Temporary Videos' folder. - - 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 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: - - # 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], - 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) - - return True - - - def on_click_temp_dl_watch_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Download the video into the 'Temporary Videos' folder. - - 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 13091 on_click_temp_dl_watch_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: - - # 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.temp_dl_watch_label.set_markup('D/L and watch') - GObject.timeout_add(0, self.update_temp_labels) - - return True - - - def on_click_temp_mark_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Mark the video for download into the 'Temporary Videos' folder. - - 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 13145 on_click_temp_mark_label') - - # 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.temp_mark_label.set_markup('Mark for download') - GObject.timeout_add(0, self.update_temp_labels) - - return True - - - def on_click_watch_hooktube_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a YouTube video on HookTube. - - 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 13188 on_click_watch_hooktube_label') - - # Launch the video - utils.open_file(uri) - - # 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_hooktube_label.set_markup('HookTube') - GObject.timeout_add(0, self.update_watch_web) - - return True - - - def on_click_watch_invidious_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a YouTube video on Invidious. - - 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 13232 on_click_watch_invidious_label') - - # Launch the video - utils.open_file(uri) - - # 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) - - return True - - - def on_click_watch_player_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a video using the system's default media player, first checking - that a file actually exists. - - 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 13277 on_click_watch_player_label') - - 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: - - # 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] ) - - else: - - # 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.watch_player_label.set_markup('Player') - GObject.timeout_add(0, self.update_watch_player) - - return True - - - def on_click_watch_web_label(self, label, uri): - - """Called from callback in self.draw_widgets(). - - Watch a video on its primary website. - - 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 13334 on_click_watch_web_label') - - # Launch the video - utils.open_file(uri) - - # 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') - - GObject.timeout_add(0, self.update_watch_web) - - return True - - - 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 13378 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 CatalogueRow(Gtk.ListBoxRow): - - """Called by MainWin.video_catalogue_redraw_all() and - .video_catalogue_insert_item(). - - Python class acting as a wrapper for Gtk.ListBoxRow, so that we can - retrieve the media.Video object displayed in each row. - - Args: - - video_obj (media.Video): The video object displayed on this row - - """ - - - # Standard class methods - - - def __init__(self, video_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13406 __init__') - - super(Gtk.ListBoxRow, self).__init__() - - # IV list - class objects - # ----------------------- - - self.video_obj = video_obj - - -class StatusIcon(Gtk.StatusIcon): - - """Called by mainapp.TartubeApp.start(). - - Python class acting as a wrapper for Gtk.StatusIcon. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13435 __init__') - - super(Gtk.StatusIcon, self).__init__() - - # IV list - class objects - # ----------------------- - # The main application - self.app_obj = app_obj - - - # IV list - other - # --------------- - # Flag set to True (by self.show_icon() ) when the status icon is - # actually visible - self.icon_visible_flag = False - - - # 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 13470 setup') - - # Display the default status icon, to start with... - self.update_icon() - # ...but the status icon isn't visible straight away - self.set_visible(False) - - # 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 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 13494 show_icon') - - if not self.icon_visible_flag: - self.icon_visible_flag = True - self.set_visible(True) - - - def hide_icon(self): - - """Can be called by anything. - - 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) - - - def update_icon(self): - - """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 13526 update_icon') - - 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'] - - self.set_from_file( - os.path.abspath( - os.path.join( - self.app_obj.main_win_obj.icon_dir_path, - 'status', - icon, - ), - ) - ) - - - # Callback class methods - - - # (Clicks on the status icon) - - - def on_button_press_event(self, widget, event_button): - - """Called from a callback in self.setup(). - - When the status icon is left-clicked, toggle the main window's - visibility. - - Args: - - widget (mainwin.StatusIcon): This object - - event_button (Gdk.EventButton): Ignored - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13577 on_button_press_event') - - if event_button.button == 1: - self.app_obj.main_win_obj.toggle_visibility() - return True - - else: - return False - - - def on_popup_menu(self, widget, button, time): - - """Called from a callback in self.setup(). - - When the status icon is right-clicked, open a popup men. - - Args: - - widget (mainwin.StatusIcon): This object - - button_type (int): Ignored - - time (int): Ignored - - """ - - 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) - - # 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) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # 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) - - # Separator - popup_menu.append(Gtk.SeparatorMenuItem()) - - # 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) - - - # (Menu item callbacks) - - - def on_check_menu_item(self, menu_item): - - """Called from a callback in self.popup_menu(). - - Starts the download manager. - - Args: - - menu_item (Gtk.MenuItem): The menu item clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13664 on_check_menu_item') - - if not self.app_obj.current_manager_obj: - self.app_obj.download_manager_start('sim') - - - def on_download_menu_item(self, menu_item): - - """Called from a callback in self.popup_menu(). - - Starts the download manager. - - Args: - - menu_item (Gtk.MenuItem): The menu item clicked - - """ - - 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') - - - def on_stop_menu_item(self, menu_item): - - """Called from a callback in self.popup_menu(). - - Halts the current download operation - - Args: - - menu_item (Gtk.MenuItem): The menu item clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13702 on_stop_menu_item') - - if self.app_obj.current_manager_obj: - - self.app_obj.set_operation_halted_flag(True) - - 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. - - Args: - - menu_item (Gtk.MenuItem): The menu item clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13733 on_quit_menu_item') - - self.app_obj.stop() - - -# (Dialogue window classes) - - -class AddChannelDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_channel(). - - Python class handling a dialogue window that adds a channel to the media - registry. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - suggest_parent_name (str): The name of the new channel's suggested - parent folder (which the user can change, if required), or None if - this dialogue window shouldn't suggest a parent folder - - dl_sim_flag (bool): True if the 'Don't download anything' radiobutton - should be made active immediately - - monitor_flag (bool): True if the 'Monitor the clipboard' checkbutton - should be selected immediately - - """ - - - # Standard class methods - - - 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__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - self.entry2 = None # Gtk.Entry - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.checkbutton = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # A list of media.Folders to display in the Gtk.ComboBox - self.folder_list = [] - # Set up IVs for clipboard monitoring, if required - self.clipboard_timer_id = None - self.clipboard_timer_time = 250 - self.clipboard_ignore_url = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Add channel', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - 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)', - ) - - 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') - grid.attach(label3, 0, 3, 2, 1) - - self.entry2 = Gtk.Entry() - grid.attach(self.entry2, 0, 4, 2, 1) - self.entry2.set_hexpand(True) - - # Drag-and-drop onto the entry inevitably inserts a URL in the - # middle of another URL. No way to prevent that, but we can disable - # drag-and-drop in the entry altogether, and instead handle it - # from the dialogue window itself - self.entry.drag_dest_unset() - self.entry2.drag_dest_unset() - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Separator - grid.attach(Gtk.HSeparator(), 0, 5, 2, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folder 'Temporary Videos' - # If a folder is selected in the Video Index, then it is the first one - # in the list. If not, 'Temporary Videos' is the first one in the - # list - for name, dbid in main_win_obj.app_obj.media_name_dict.items(): - media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and not media_data_obj.restrict_flag \ - and media_data_obj.get_depth() \ - < main_win_obj.app_obj.media_max_level \ - and ( - suggest_parent_name is None - or suggest_parent_name != media_data_obj.name - ): - self.folder_list.append(media_data_obj.name) - - self.folder_list.sort() - self.folder_list.insert(0, '') - self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name) - - 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') - grid.attach(label4, 0, 6, 2, 1) - - box = Gtk.Box() - grid.attach(box, 0, 7, 1, 1) - box.set_border_width(main_win_obj.spacing_size) - - image = Gtk.Image() - box.add(image) - image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small']) - - listmodel = Gtk.ListStore(str) - for item in self.folder_list: - listmodel.append([item]) - - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 1, 7, 1, 1) - combo.set_hexpand(True) - - cell = Gtk.CellRendererText() - combo.pack_start(cell, False) - combo.add_attribute(cell, 'text', 0) - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 8, 2, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - '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', - ) - 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.connect('toggled', self.on_checkbutton_toggled) - if monitor_flag: - - # Get the URL that would have been added to the Gtk.Entry, if we - # had not specified a True argument - self.clipboard_ignore_url \ - = utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - None, - None, - True, - ) - - self.checkbutton.set_active(True) - - # Paste in the contents of the clipboard (if it contains at least one - # valid URL) - if main_win_obj.app_obj.dialogue_copy_clipboard_flag \ - and not main_win_obj.app_obj.dialogue_keep_open_flag: - utils.add_links_to_entry_from_clipboard( - main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - ) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables clipboard monitoring. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 13973 on_checkbutton_toggled') - - if not checkbutton.get_active() \ - and self.clipboard_timer_id is not None: - - # Stop the timer - GObject.source_remove(self.clipboard_timer_id) - self.clipboard_timer_id = None - - elif checkbutton.get_active() and self.clipboard_timer_id is None: - - # Start the timer - self.clipboard_timer_id = GObject.timeout_add( - self.clipboard_timer_time, - self.clipboard_timer_callback, - ) - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14005 on_combo_changed') - - self.parent_name = self.folder_list[combo.get_active()] - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14019 on_window_drag_data_received') - - utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - - # (Callbacks) - - - def clipboard_timer_callback(self): - - """Called from a callback in self.on_checkbutton_toggled(). - - Periodically checks the system's clipboard, and adds any new URLs to - the dialogue window's entry. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14043 clipboard_timer_callback') - - utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - ) - - # Return 1 to keep the timer going - return 1 - - -class AddFolderDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_folder(). - - Python class handling a dialogue window that adds a folder to the media - registry. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - suggest_parent_name (str): The name of the new folder's suggested - parent folder (which the user can change, if required), or None if - this dialogue window shouldn't suggest a parent folder - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, suggest_parent_name=None): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14079 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - - - # IV list - other - # --------------- - # A list of media.Folders to display in the Gtk.ComboBox - self.folder_list = [] - # The media.Folder selected in the combobox - self.parent_name = None - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Add folder', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - 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 - # their contents) - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 2, 1) - self.entry.set_hexpand(True) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 2, 2, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folder 'Temporary Videos' - # If a folder is selected in the Video Index, then it is the first one - # in the list. If not, 'Temporary Videos' is the first one in the - # list - for name, dbid in main_win_obj.app_obj.media_name_dict.items(): - media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and not media_data_obj.restrict_flag \ - and media_data_obj.get_depth() \ - < main_win_obj.app_obj.media_max_level \ - and ( - suggest_parent_name is None - or suggest_parent_name != media_data_obj.name - ): - self.folder_list.append(media_data_obj.name) - - self.folder_list.sort() - self.folder_list.insert(0, '') - self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name) - - if suggest_parent_name is not None: - self.folder_list.insert(0, suggest_parent_name) - - # Store the combobox's selected item, so the calling function can - # retrieve it. - self.parent_name = self.folder_list[0] - - label4 = Gtk.Label( - '(Optional) Add this folder inside another folder', - ) - grid.attach(label4, 0, 3, 2, 1) - - box = Gtk.Box() - grid.attach(box, 0, 4, 1, 1) - box.set_border_width(main_win_obj.spacing_size) - - image = Gtk.Image() - box.add(image) - image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small']) - - listmodel = Gtk.ListStore(str) - for item in self.folder_list: - listmodel.append([item]) - - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 1, 4, 1, 1) - combo.set_hexpand(True) - - cell = Gtk.CellRendererText() - combo.pack_start(cell, False) - combo.add_attribute(cell, 'text', 0) - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 5, 2, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - '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', - ) - grid.attach(self.radiobutton2, 0, 7, 2, 1) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14231 on_combo_changed') - - self.parent_name = self.folder_list[combo.get_active()] - - -class AddPlaylistDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_playlist(). - - Python class handling a dialogue window that adds a playlist to the - media registry. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - suggest_parent_name (str): The name of the new playlist's suggested - parent folder (which the user can change, if required), or None if - this dialogue window shouldn't suggest a parent folder - - dl_sim_flag (bool): True if the 'Don't download anything' radiobutton - should be made active immediately - - monitor_flag (bool): True if the 'Monitor the clipboard' checkbutton - should be selected immediately - - """ - - - # Standard class methods - - - 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__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - self.entry2 = None # Gtk.Entry - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.checkbutton = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # A list of media.Folders to display in the Gtk.ComboBox - self.folder_list = [] - # Set up IVs for clipboard monitoring, if required - self.clipboard_timer_id = None - self.clipboard_timer_time = 250 - self.clipboard_ignore_url = None - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Add playlist', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - 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)', - ) - - 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') - grid.attach(label3, 0, 3, 2, 1) - - self.entry2 = Gtk.Entry() - grid.attach(self.entry2, 0, 4, 2, 1) - self.entry2.set_hexpand(True) - - # Drag-and-drop onto the entry inevitably inserts a URL in the - # middle of another URL. No way to prevent that, but we can disable - # drag-and-drop in the entry altogether, and instead handle it - # from the dialogue window itself - self.entry.drag_dest_unset() - self.entry2.drag_dest_unset() - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Separator - grid.attach(Gtk.HSeparator(), 0, 5, 2, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folder 'Temporary Videos' - # If a folder is selected in the Video Index, then it is the first one - # in the list. If not, 'Temporary Videos' is the first one in the - # list - for name, dbid in main_win_obj.app_obj.media_name_dict.items(): - media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and not media_data_obj.restrict_flag \ - and media_data_obj.get_depth() \ - < main_win_obj.app_obj.media_max_level \ - and ( - suggest_parent_name is None - or suggest_parent_name != media_data_obj.name - ): - self.folder_list.append(media_data_obj.name) - - self.folder_list.sort() - self.folder_list.insert(0, '') - self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name) - - 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') - grid.attach(label4, 0, 6, 2, 1) - - box = Gtk.Box() - grid.attach(box, 0, 7, 1, 1) - box.set_border_width(main_win_obj.spacing_size) - - image = Gtk.Image() - box.add(image) - image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small']) - - listmodel = Gtk.ListStore(str) - for item in self.folder_list: - listmodel.append([item]) - - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 1, 7, 1, 1) - combo.set_hexpand(True) - - cell = Gtk.CellRendererText() - combo.pack_start(cell, False) - combo.add_attribute(cell, 'text', 0) - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 8, 2, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - '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', - ) - 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.connect('toggled', self.on_checkbutton_toggled) - if monitor_flag: - - # Get the URL that would have been added to the Gtk.Entry, if we - # had not specified a True argument - self.clipboard_ignore_url \ - = utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - None, - None, - True, - ) - - self.checkbutton.set_active(True) - - # Paste in the contents of the clipboard (if it contains at least one - # valid URL) - if main_win_obj.app_obj.dialogue_copy_clipboard_flag \ - and not main_win_obj.app_obj.dialogue_keep_open_flag: - utils.add_links_to_entry_from_clipboard( - main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - ) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables clipboard monitoring. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14467 on_checkbutton_toggled') - - if not checkbutton.get_active() \ - and self.clipboard_timer_id is not None: - - # Stop the timer - GObject.source_remove(self.clipboard_timer_id) - self.clipboard_timer_id = None - - elif checkbutton.get_active() and self.clipboard_timer_id is None: - - # Start the timer - self.clipboard_timer_id = GObject.timeout_add( - self.clipboard_timer_time, - self.clipboard_timer_callback, - ) - - - def on_combo_changed(self, combo): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14499 on_combo_changed') - - self.parent_name = self.folder_list[combo.get_active()] - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14513 on_window_drag_data_received') - - utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - - # (Callbacks) - - - def clipboard_timer_callback(self): - - """Called from a callback in self.on_checkbutton_toggled(). - - Periodically checks the system's clipboard, and adds any new URLs to - the dialogue window's entry. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14537 clipboard_timer_callback') - - utils.add_links_to_entry_from_clipboard( - self.main_win_obj.app_obj, - self.entry2, - self.clipboard_ignore_url, - ) - - # Return 1 to keep the timer going - return 1 - - -class AddVideoDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_add_video(). - - Python class handling a dialogue window that adds invidual video(s) to - the media registry. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14589 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.textbuffer = None # Gtk.TextBuffer - self.mark_start = None # Gtk.TextMark - self.mark_end = None # Gtk.TextMark - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.checkbutton = None # Gtk.CheckButton - - - # IV list - other - # --------------- - # A list of media.Folders to display in the Gtk.ComboBox - self.folder_list = [] - # The media.Folder selected in the combobox - self.parent_name = None - # Set up IVs for clipboard monitoring, if required - self.clipboard_timer_id = None - self.clipboard_timer_time = 250 - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Add videos', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - 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' - - elif main_win_obj.app_obj.operation_convert_mode == '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' - - elif main_win_obj.app_obj.operation_convert_mode == 'disable': - text = 'Links containing multiple videos will not be downloaded' - + ' at all' - - label = Gtk.Label() - label.set_markup('' + text + '') - grid.attach(label, 0, 1, 2, 1) - - frame = Gtk.Frame() - grid.attach(frame, 0, 2, 2, 1) - - scrolledwindow = Gtk.ScrolledWindow() - frame.add(scrolledwindow) - # (Set enough vertical room for at several URLs) - scrolledwindow.set_size_request(-1, 150) - - textview = Gtk.TextView() - scrolledwindow.add(textview) - textview.set_hexpand(True) - self.textbuffer = textview.get_buffer() - - # Some callbacks will complain about invalid iterators, if we try to - # use Gtk.TextIters, so use Gtk.TextMarks instead - self.mark_start = self.textbuffer.create_mark( - 'mark_start', - self.textbuffer.get_start_iter(), - True, # Left gravity - ) - self.mark_end = self.textbuffer.create_mark( - 'mark_end', - self.textbuffer.get_end_iter(), - False, # Not left gravity - ) - # Drag-and-drop onto the textview inevitably inserts a URL in the - # middle of another URL. No way to prevent that, but we can disable - # drag-and-drop in the textview altogether, and instead handle it - # from the dialogue window itself -# textview.drag_dest_unset() - self.connect('drag-data-received', self.on_window_drag_data_received) - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_set_target_list(None) - self.drag_dest_add_text_targets() - - # Separator - grid.attach(Gtk.HSeparator(), 0, 3, 2, 1) - - # Prepare a list of folders to display in a combo. The list always - # includes the system folders 'Unsorted Videos' and 'Temporary - # Videos' - # If a folder is selected in the Video Index, then it is the first one - # in the list. If not, 'Unsorted Videos' is the first one in the - # list - folder_obj = None - # The selected item in the Video Index could be a channel, playlist or - # folder, but here we only pay attention to folders - selected = main_win_obj.video_index_current - if selected: - dbid = main_win_obj.app_obj.media_name_dict[selected] - container_obj = main_win_obj.app_obj.media_reg_dict[dbid] - if isinstance(container_obj, media.Folder) \ - and not container_obj.priv_flag: - folder_obj = container_obj - - for name, dbid in main_win_obj.app_obj.media_name_dict.items(): - media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid] - - if isinstance(media_data_obj, media.Folder) \ - and not media_data_obj.fixed_flag \ - and not media_data_obj.restrict_flag \ - and (folder_obj is None or media_data_obj != folder_obj): - self.folder_list.append(media_data_obj.name) - - self.folder_list.sort() - self.folder_list.insert(0, main_win_obj.app_obj.fixed_misc_folder.name) - self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name) - if folder_obj: - self.folder_list.insert(0, folder_obj.name) - - # Store the combobox's selected item, so the calling function can - # retrieve it. - self.parent_name = self.folder_list[0] - - label2 = Gtk.Label('Add the videos to this folder') - grid.attach(label2, 0, 4, 2, 1) - - box = Gtk.Box() - grid.attach(box, 0, 5, 1, 1) - box.set_border_width(main_win_obj.spacing_size) - - image = Gtk.Image() - box.add(image) - image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small']) - - listmodel = Gtk.ListStore(str) - for item in self.folder_list: - listmodel.append([item]) - - combo = Gtk.ComboBox.new_with_model(listmodel) - grid.attach(combo, 1, 5, 1, 1) - combo.set_hexpand(True) - - cell = Gtk.CellRendererText() - combo.pack_start(cell, False) - combo.add_attribute(cell, 'text', 0) - combo.set_active(0) - combo.connect('changed', self.on_combo_changed) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 6, 2, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - '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', - ) - 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.connect('toggled', self.on_checkbutton_toggled) - - # Paste in the contents of the clipboard (if it contains valid URLs) - if main_win_obj.app_obj.dialogue_copy_clipboard_flag: - utils.add_links_to_textview_from_clipboard( - main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - ) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - Enables/disables clipboard monitoring. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14793 on_checkbutton_toggled') - - if not checkbutton.get_active() \ - and self.clipboard_timer_id is not None: - - # Stop the timer - GObject.source_remove(self.clipboard_timer_id) - self.clipboard_timer_id = None - - elif checkbutton.get_active() and self.clipboard_timer_id is None: - - # Start the timer - self.clipboard_timer_id = GObject.timeout_add( - self.clipboard_timer_time, - self.clipboard_timer_callback, - ) - - - def on_combo_changed(self, combo): - - """Called a from callback in self.__init__(). - - Updates the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14825 on_combo_changed') - - self.parent_name = self.folder_list[combo.get_active()] - - - def on_window_drag_data_received(self, window, context, x, y, data, info, - time): - - """Called a from callback in self.__init__(). - - Handles drag-and-drop anywhere in the dialogue window. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14839 on_window_drag_data_received') - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - # Specify the drag-and-drop text, so the called function uses that, - # rather than the clipboard text - data.get_text(), - ) - - - # (Callbacks) - - - def clipboard_timer_callback(self): - - """Called from a callback in self.on_checkbutton_toggled(). - - Periodically checks the system's clipboard, and adds any new URLs to - the dialogue window's textview. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14864 clipboard_timer_callback') - - utils.add_links_to_textview_from_clipboard( - self.main_win_obj.app_obj, - self.textbuffer, - self.mark_start, - self.mark_end, - ) - - # Return 1 to keep the timer going - return 1 - - -class CalendarDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_button_find_date() and - config.OptionsEditWin.on_button_set_date_clicked(). - - Python class handling a dialogue window that prompts the user to choose a - date on a calendar - - Args: - - parent_win_obj (mainwin.MainWin): The parent window - - date (str): A date in the form YYYYMMDD. If set, that date is - selected in the calendar. If an empty string or None, no date is - selected - - """ - - - # Standard class methods - - - def __init__(self, parent_win_obj, date=None): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14902 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = parent_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.calendar = None # Gtk.Calendar - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Select a date', - parent_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(parent_win_obj.spacing_size) - grid.set_row_spacing(parent_win_obj.spacing_size) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.calendar = Gtk.Calendar.new() - grid.attach(self.calendar, 0, 0, 1, 1) - - # If the date was specified, it should be a string in the form YYYYMMDD - if date: - year = int(date[0:3]) - month = int(date[4:5]) - day = int(date[6:7]) - - if day >= 1 and day <= 31 and month >= 1 and month <= 12 \ - and year >=1: - self.calendar.select_month(month, year) - self.calendar.select_day(day) - - # Display the dialogue window - self.show_all() - - -class DeleteContainerDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.delete_container(). - - Python class handling a dialogue window that prompts the user for - confirmation, before removing a media.Channel, media.Playlist or - media.Folder object. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist or media.Folder): The - container media data object to be deleted - - empty_flag (bool): If True, the container media data object is to be - emptied, rather than being deleted - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj, empty_flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 14986 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.button = None # Gtk.Button - self.button2 = None # Gtk.Button - - # IV list - other - # --------------- - # Number of videos found in the container - self.video_count = 0 - - - # Code - # ---- - - # Prepare variables - pkg_string = __main__.__prettyname__ - media_type = media_data_obj.get_type() - if media_type == 'video': - return self.app_obj.system_error( - 248, - 'Dialogue window setup failed sanity check', - ) - - # Count the container object's children - total_count, self.video_count, channel_count, playlist_count, \ - folder_count = media_data_obj.count_descendants( [0, 0, 0, 0, 0] ) - - # Create the dialogue window - if not empty_flag: - title = 'Delete ' + media_type - else: - title = 'Empty ' + media_type - - Gtk.Dialog.__init__( - self, - title, - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - self.set_resizable(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label() - grid.attach(label, 0, 0, 1, 1) - label.set_markup('' + media_data_obj.name + '') - - # Separator - grid.attach(Gtk.HSeparator(), 0, 1, 1, 1) - - if not total_count: - - if media_type == 'folder': - - 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)', - ) - - grid.attach(label2, 0, 2, 1, 5) - label2.set_alignment(0, 0.5) - - else: - - label2 = Gtk.Label('This ' + media_type + ' contains:') - grid.attach(label2, 0, 2, 1, 1) - label2.set_alignment(0, 0.5) - - if folder_count == 1: - label_string = '1 folder' - else: - label_string = '' + str(folder_count) + ' folders' - - label3 = Gtk.Label() - grid.attach(label3, 0, 3, 1, 1) - label3.set_markup(label_string) - - if channel_count == 1: - label_string = '1 channel' - else: - label_string = '' + str(channel_count) + ' channels' - - label4 = Gtk.Label() - grid.attach(label4, 0, 4, 1, 1) - label4.set_markup(label_string) - - if playlist_count == 1: - label_string = '1 playlist' - else: - label_string = '' + str(playlist_count) + ' playlists' - - label5 = Gtk.Label() - grid.attach(label5, 0, 5, 1, 1) - label5.set_markup(label_string) - - if self.video_count == 1: - label_string = '1 video' - else: - label_string = '' + str(self.video_count) + ' videos' - - label6 = Gtk.Label() - grid.attach(label6, 0, 6, 1, 1) - label6.set_markup(label_string) - - # Separator - 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?', - ) - 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?', - ) - - 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', - ) - else: - self.button = Gtk.RadioButton.new_with_label_from_widget( - None, - 'Just empty the ' + media_type + ' in this list', - ) - - grid.attach(self.button, 0, 9, 1, 1) - - self.button2 = Gtk.RadioButton.new_from_widget(self.button) - self.button2.set_label( - 'Delete all files', - ) - grid.attach(self.button2, 0, 10, 1, 1) - - # Display the dialogue window - self.show_all() - - -class ExportDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.export_from_db(). - - Python class handling a dialogue window that prompts the user before - creating a database export. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - whole_flag (bool): True if the whole database is to be exported, False - if only part of the database is to be exported - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, whole_flag): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15185 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.checkbutton = None # Gtk.CheckButton - self.checkbutton2 = None # Gtk.CheckButton - self.checkbutton3 = None # Gtk.CheckButton - self.checkbutton4 = None # Gtk.CheckButton - self.checkbutton5 = None # Gtk.CheckButton - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Export from database', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - 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)' - 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)' - - label = Gtk.Label(msg) - grid.attach(label, 0, 0, 1, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 1, 1, 1) - - label = Gtk.Label('Choose what should be included:') - grid.attach(label, 0, 2, 1, 1) - - # (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_active(False) - - self.checkbutton2 = Gtk.CheckButton() - grid.attach(self.checkbutton2, 0, 4, 1, 1) - 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_active(True) - - self.checkbutton4 = Gtk.CheckButton() - grid.attach(self.checkbutton4, 0, 6, 1, 1) - self.checkbutton4.set_label('Preserve folder structure') - self.checkbutton4.set_active(True) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 7, 1, 1) - - self.checkbutton5 = Gtk.CheckButton() - grid.attach(self.checkbutton5, 0, 8, 1, 1) - self.checkbutton5.set_label('Export as plain text') - self.checkbutton5.set_active(False) - self.checkbutton5.connect('toggled', self.on_checkbutton_toggled) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from callback in self.__init__(). - - When the specified checkbutton is toggled, modify other widgets in the - dialogue window. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15298 on_checkbutton_toggled') - - if not checkbutton.get_active(): - self.checkbutton.set_sensitive(True) - self.checkbutton4.set_sensitive(True) - else: - self.checkbutton.set_active(False) - self.checkbutton.set_sensitive(False) - self.checkbutton4.set_active(False) - self.checkbutton4.set_sensitive(False) - - -class ImportDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.import_into_db(). - - Python class handling a dialogue window that prompts the user before - hanlding an export file, created by mainapp.TartubeApp.export_from_db(). - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - db_dict (dict): The imported data, a dictionary described in the - comments in mainapp.TartubeApp.export_from_db() - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, db_dict): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15333 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.treeview = None # Gtk.TreeView - self.liststore = None # Gtk.TreeView - self.checkbutton = None # Gtk.TreeView - self.checkbutton2 = None # Gtk.TreeView - - # IV list - other - # --------------- - # A flattened dictionary of media data objects - self.flat_db_dict = {} - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Import into database', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - self.set_default_size( - main_win_obj.app_obj.config_win_width, - main_win_obj.app_obj.config_win_height, - ) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label('Choose which items to import') - grid.attach(label, 0, 0, 4, 1) - - scrolled = Gtk.ScrolledWindow() - grid.attach(scrolled, 0, 1, 4, 1) - scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolled.set_hexpand(True) - scrolled.set_vexpand(True) - - frame = Gtk.Frame() - scrolled.add_with_viewport(frame) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.treeview = Gtk.TreeView() - frame.add(self.treeview) - self.treeview.set_can_focus(False) - - renderer_toggle = Gtk.CellRendererToggle() - renderer_toggle.connect('toggled', self.on_checkbutton_toggled) - column_toggle = Gtk.TreeViewColumn( - 'Import', - renderer_toggle, - active=0, - ) - self.treeview.append_column(column_toggle) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - column_pixbuf = Gtk.TreeViewColumn( - '', - renderer_pixbuf, - pixbuf=1, - ) - self.treeview.append_column(column_pixbuf) - - renderer_text = Gtk.CellRendererText() - column_text = Gtk.TreeViewColumn( - 'Name', - renderer_text, - text=2, - ) - self.treeview.append_column(column_text) - - renderer_text2 = Gtk.CellRendererText() - column_text2 = Gtk.TreeViewColumn( - 'hide', - renderer_text2, - text=3, - ) - column_text2.set_visible(False) - self.treeview.append_column(column_text2) - - self.liststore = Gtk.ListStore(bool, GdkPixbuf.Pixbuf, str, int) - self.treeview.set_model(self.liststore) - - self.checkbutton = Gtk.CheckButton() - grid.attach(self.checkbutton, 0, 2, 1, 1) - 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_active(False) - - 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') - grid.attach(button2, 3, 2, 1, 1) - button2.set_hexpand(False) - button2.connect('clicked', self.on_deselect_all_clicked) - - # The data is imported as a dictionary, perhaps preserving the original - # folder structure of the database, or perhaps not - # The 'db_dict' format is described in the comments in - # mainapp.TartubeApp.export_from_db() - # 'db_dict' contains mini-dictionaries, 'mini_dict', whose format is - # also described in that function. Each 'mini_dict' represents a - # single media data object - # - # Convert 'db_dict' to a list. Each item in the list is a 'mini_dict'. - # Each 'mini_dict' has some new key-value pairs (except those - # representing videos): - # - # - 'video_count': int (showing the number of videos the channel, - # playlist or folder contains) - # - 'display_name': str (the channel/playlist/folder name indented - # with extra whitespace (so the user can clearly see the folder - # structure) - # - 'import_flag': bool (True if this channel/playlist/folder should - # be imported, False if not) - converted_list = self.convert_to_list( db_dict, [] ) - - # Add a line to the textview for each channel, playlist and folder - for mini_dict in converted_list: - - pixbuf = main_win_obj.pixbuf_dict[mini_dict['type'] + '_small'] - text = mini_dict['display_name'] - if mini_dict['video_count'] == 1: - text += ' [ 1 video ]' - elif mini_dict['video_count']: - text += ' [ ' + str(mini_dict['video_count']) + ' videos ]' - - self.liststore.append( [True, pixbuf, text, mini_dict['dbid']] ) - - # Compile a dictionary, a flattened version of the original 'db_dict' - # (i.e. which the original database's folder structure removed) - # This new dictionary contains a single key-value pair for every - # channel, playlist and folder. Dictionary in the form: - # - # key: the channel/playlist/folder dbid - # value: the 'mini_dict' for that channel/playlist/folder - # - # If the channel/playlist/folder has any child videos, then its - # 'mini_dict' still has some child 'mini_dicts', one for each video - for mini_dict in converted_list: - self.flat_db_dict[mini_dict['dbid']] = mini_dict - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def convert_to_list(self, db_dict, converted_list, - parent_mini_dict=None, recursion_level=0): - - """Called by self.__init__(). Subsequently called by this function - recursively. - - Converts the imported 'db_dict' into a list, with each item in the - list being a 'mini_dict' (the format of both dictionaries is described - in the comments in mainapp.TartubeApp.export_from_db() ). - - Args: - - db_dict (dict): The dictionary described in self.export_from_db(); - if called from self.__init__(), the original imported - dictionary; if called recursively, a dictionary from somewhere - inside the original imported dictionary - - converted_list (list): The converted list so far; this function - adds more 'mini_dict' items to the list - - parent_mini_dict (dict): The contents of db_dict all represent - children of the channel/playlist/folder represent by this - dictionary - - recursion_level (int): The number of recursive calls to this - function (so far) - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15540 convert_to_list') - - # (Sorting function for the code immediately below) - def sort_dict_by_name(this_dict): - return this_dict['name'] - - # Deal with importable videos/channels/playlists/folders in - # alphabetical order - for mini_dict in sorted(db_dict.values(), key=sort_dict_by_name): - - if mini_dict['type'] == 'video': - - # Videos are not displayed in the treeview (but we count the - # number of videos in each channel/playlist/folder) - if parent_mini_dict: - parent_mini_dict['video_count'] += 1 - - else: - - # In the treeview, the channel/playlist/folder name is - # indented, so the user can see the folder structure - mini_dict['display_name'] = (' ' * 3 * recursion_level) \ - + mini_dict['name'] - - # Count the number of videos this channel/playlist/folder - # contains - mini_dict['video_count'] = 0 - - # Import everything, until the user chooses otherwise - mini_dict['import_flag'] = True - - # Add this channel/playlist/folder to the list visible in the - # textview - converted_list.append(mini_dict) - # Call this function to process any child videos/channels/ - # playlists/folders - converted_list = self.convert_to_list( - mini_dict['db_dict'], - converted_list, - mini_dict, - recursion_level + 1, - ) - - # Procedure complete - return converted_list - - - def on_checkbutton_toggled(self, checkbutton, path): - - """Called from a callback in self.__init__(). - - Respond when the user selects/deselects an item in the treeview. - - Args: - - checkbutton (Gtk.CheckButton): The widget clicked - - path (int): A number representing the widget's row - - """ - - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15603 on_checkbutton_toggled') - - # The user has clicked on the checkbutton widget, so toggle the widget - # itself - self.liststore[path][0] = not self.liststore[path][0] - - # Update the data to be returned (eventually) to the calling - # mainapp.TartubeApp.import_into_db() function - mini_dict = self.flat_db_dict[self.liststore[path][3]] - mini_dict['import_flag'] = self.liststore[path][0] - - - def on_select_all_clicked(self, button): - - """Called from a callback in self.__init__(). - - Mark all channels/playlists/folders to be imported. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15628 on_select_all_clicked') - - for path in range(0, len(self.liststore)): - self.liststore[path][0] = True - - for mini_dict in self.flat_db_dict.values(): - mini_dict['import_flag'] = True - - - def on_deselect_all_clicked(self, button): - - """Called from a callback in self.__init__(). - - Mark all channels/playlists/folders to be not imported. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15650 on_deselect_all_clicked') - - for path in range(0, len(self.liststore)): - self.liststore[path][0] = False - - for mini_dict in self.flat_db_dict.values(): - mini_dict['import_flag'] = False - - -class MountDriveDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.start() and .make_directory(). - - Python class handling a dialogue window that asks the user what to do, - if the drive containing Tartube's data directory is not mounted or is - unwriteable. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - unwriteable_flag (bool): True if the data directory is unwriteable; - False if the data directory is missing altogether - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, unwriteable_flag=False): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15683 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.radiobutton = None # Gtk.RadioButton - self.radiobutton2 = None # Gtk.RadioButton - self.combo = None # Gtk.ComboBox - self.radiobutton3 = None # Gtk.RadioButton - self.radiobutton4 = None # Gtk.RadioButton - self.radiobutton5 = None # Gtk.RadioButton - - - # IV list - other - # --------------- - # Flag set to True if the data directory specified by - # mainapp.TartubeApp.data_dir is now available - self.available_flag = False - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Mount drive', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid_width = 2 - - if os.name == 'nt': - folder = 'folder' - else: - folder = 'directory' - - label = Gtk.Label( - 'The ' + __main__.__prettyname__ + ' data ' + folder \ - + ' is set to:', - ) - grid.attach(label, 0, 0, grid_width, 1) - - label = Gtk.Label() - grid.attach(label, 0, 1, grid_width, 1) - label.set_markup( - '' \ - + utils.shorten_string(main_win_obj.app_obj.data_dir, 50) \ - + '', - ) - - if not unwriteable_flag: - label2 = Gtk.Label( - '...but this ' + folder + ' doesn\'t exist', - ) - else: - label2 = Gtk.Label( - '...but ' + __main__.__prettyname__ \ - + ' cannot write to this ' + folder, - ) - - grid.attach(label2, 0, 2, grid_width, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 3, grid_width, 1) - - self.radiobutton = Gtk.RadioButton.new_with_label_from_widget( - None, - 'I have mounted the drive, please try again', - ) - grid.attach(self.radiobutton, 0, 4, grid_width, 1) - - self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton, - 'Use this data ' + folder + ':', - ) - grid.attach(self.radiobutton2, 0, 5, grid_width, 1) - # signal_connect appears below - - store = Gtk.ListStore(str) - for item in self.main_win_obj.app_obj.data_dir_alt_list: - store.append([item]) - - self.combo = Gtk.ComboBox.new_with_model(store) - grid.attach(self.combo, 0, 6, grid_width, 1) - self.combo.set_hexpand(True) - renderer_text = Gtk.CellRendererText() - self.combo.pack_start(renderer_text, True) - self.combo.add_attribute(renderer_text, 'text', 0) - self.combo.set_entry_text_column(0) - self.combo.set_active(0) - self.combo.set_sensitive(False) - - # signal_connect from above - self.radiobutton2.connect( - 'toggled', - self.on_radiobutton_toggled, - ) - - self.radiobutton3 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton2, - 'Select a different data ' + folder, - ) - grid.attach(self.radiobutton3, 0, 7, grid_width, 1) - - self.radiobutton4 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton3, - 'Use the default data ' + folder, - ) - grid.attach(self.radiobutton4, 0, 8, grid_width, 1) - - self.radiobutton5 = Gtk.RadioButton.new_with_label_from_widget( - self.radiobutton4, - 'Shut down ' + __main__.__prettyname__, - ) - grid.attach(self.radiobutton5, 0, 9, grid_width, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 10, grid_width, 1) - - button = Gtk.Button.new_with_label('Cancel') - grid.attach(button, 0, 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.connect('clicked', self.on_ok_button_clicked) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - # (Callbacks) - - - def on_ok_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the OK button is clicked, perform the selected action. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15848 on_ok_button_clicked') - - if self.radiobutton.get_active(): - self.do_try_again() - - elif self.radiobutton2.get_active(): - - tree_iter = self.combo.get_active_iter() - model = self.combo.get_model() - path = model[tree_iter][0] - self.main_win_obj.app_obj.set_data_dir(path) - self.available_flag = True - self.destroy() - - elif self.radiobutton3.get_active(): - self.do_select_dir() - - elif self.radiobutton4.get_active(): - - self.main_win_obj.app_obj.reset_data_dir() - self.available_flag = True - self.destroy() - - elif self.radiobutton5.get_active(): - self.available_flag = False - self.destroy() - - - def on_cancel_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the Cancel button is clicked, shut down Tartube. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15889 on_cancel_button_clicked') - - self.available_flag = False - self.destroy() - - - def on_radiobutton_toggled(self, button): - - """Called from a callback in self.__init__(). - - When the radiobutton just above it is toggled, (de)sensitise the - combobox. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15909 on_radiobutton_toggled') - - if button.get_active(): - self.combo.set_sensitive(True) - else: - self.combo.set_sensitive(False) - - - # (Callback support functions) - - - def do_try_again(self): - - """Called by self.on_ok_button_clicked(). - - The user has selected 'I have mounted the drive, please try again'. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15928 do_try_again') - - app_obj = self.main_win_obj.app_obj - - if os.path.exists(app_obj.data_dir): - - # Data directory exists - self.available_flag = True - self.destroy() - - 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' \ - + ' different option', - 'error', - 'ok', - self, # Parent window is this window - ) - - mini_win.set_modal(True) - - - def do_select_dir(self): - - """Called by self.on_ok_button_clicked(). - - The user has selected 'Select a different data directory'. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15966 do_select_dir') - - if (self.main_win_obj.app_obj.prompt_user_for_data_dir()): - - # New data directory selected - self.available_flag = True - self.destroy() - - -class RemoveLockFileDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.start(). - - Python class handling a dialogue window that asks the user what to do, - if the database file can't be loaded because it's protected by a lockfile. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 15995 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - other - # --------------- - # Flag set to True if the lockfile should be removed - self.remove_flag = False - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Stale lockfile', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid_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\'', - ) - grid.attach(label, 0, 0, grid_width, 1) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 1, grid_width, 1) - - button = Gtk.Button.new_with_label( - 'Yes, I\'m sure', - ) - grid.attach(button, 0, 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', - ) - grid.attach(button2, 0, 3, 1, 1) - button2.set_hexpand(True) - button2.connect('clicked', self.on_no_button_clicked) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def on_yes_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the Yes button is clicked, set a flag for the calling function to - check, the close the window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16080 on_yes_button_clicked') - - self.remove_flag = True - self.destroy() - - - def on_no_button_clicked(self, button): - - """Called from a callback in self.__init__(). - - When the No button is clicked, set a flag for the calling function to - check, the close the window. - - Args: - - button (Gtk.Button): The widget clicked - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16100 on_no_button_clicked') - - self.remove_flag = False - self.destroy() - - -class RenameContainerDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.rename_container(). - - Python class handling a dialogue window that prompts the user to rename - a channel, playlist or folder. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist, media.Folder): The media - data object whose name is to be changed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16129 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - - - # Code - # ---- - - media_type = media_data_obj.get_type() - - Gtk.Dialog.__init__( - self, - 'Rename ' + media_type, - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - 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!', - ) - grid.attach(label, 0, 0, 1, 1) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 1, 1) - self.entry.set_text(media_data_obj.name) - - # Display the dialogue window - self.show_all() - - -class SetDestinationDialogue(Gtk.Dialog): - - """Called by MainWin.on_video_index_set_destination(). - - Python class handling a dialogue window that prompts the user to set the - alternative download destination for a channel, playlist or folder. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist, media.Folder): The media - data object whose download destination is to be changed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16208 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - other - # --------------- - # Store function arguments as IVs, so callback functions can retrieve - # them - self.main_win_obj = main_win_obj - self.media_data_obj = media_data_obj - # Store the user's choice as an IV, so the calling function can - # retrieve it - self.choice = media_data_obj.master_dbid - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Set download destination', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - if os.name == 'nt': - dir_name = 'folder' - else: - dir_name = 'directory' - - media_type = media_data_obj.get_type() - - 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', - ) - 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, - ) - 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 + ':') - grid.attach(radiobutton2, 0, 3, 1, 1) - # Signal connect appears below - - # Get a list of channels/playlists/folders - app_obj = main_win_obj.app_obj - dbid_list = list(app_obj.media_name_dict.values()) - - # If the alternative download destination selected by this window, the - # last time it was opened, has since been deleted, then reset the IV - # that stores it - prev_dbid = main_win_obj.previous_alt_dest_dbid - if prev_dbid is not None and not prev_dbid in app_obj.media_reg_dict: - prev_dbid = None - main_win_obj.set_previous_alt_dest_dbid(None) - - # From this list, filter out: - # - Any channel/playlist/folder which has an alternative download - # destination set (a media data object can't have an alternative - # destination, and be an alternative destination at the same - # time) - # - The most recently-selected alternative download destination, if - # any - # - media_data_obj itself - mod_dbid_list = [] - for this_dbid in dbid_list: - - this_obj = app_obj.media_reg_dict[this_dbid] - - if this_dbid != media_data_obj.dbid \ - and (prev_dbid is None or prev_dbid != this_dbid) \ - and this_obj.dbid == this_obj.master_dbid: - mod_dbid_list.append(this_dbid) - - # Sort the modified list... - name_list = [] - for this_dbid in mod_dbid_list: - this_obj = app_obj.media_reg_dict[this_dbid] - name_list.append(this_obj.name) - - name_list.sort(key=lambda x: x.lower()) - - # ...and then add the previous destination, and the media data object - # itself, at the top of it - name_list.insert(0, media_data_obj.name) - - if prev_dbid is not None: - prev_obj = app_obj.media_reg_dict[prev_dbid] - name_list.insert(0, prev_obj.name) - - # Add a combo - store = Gtk.ListStore(GdkPixbuf.Pixbuf, str) - - count = -1 - - for name in name_list: - dbid = app_obj.media_name_dict[name] - obj = app_obj.media_reg_dict[dbid] - - if isinstance(obj, media.Channel): - icon_name = 'channel_small' - elif isinstance(obj, media.Playlist): - icon_name = 'playlist_small' - else: - icon_name = 'folder_small' - - store.append( [main_win_obj.pixbuf_dict[icon_name], name] ) - - count += 1 - - combo = Gtk.ComboBox.new_with_model(store) - grid.attach(combo, 0, 4, 1, 1) - combo.set_hexpand(True) - - renderer_pixbuf = Gtk.CellRendererPixbuf() - combo.pack_start(renderer_pixbuf, False) - combo.add_attribute(renderer_pixbuf, 'pixbuf', 0) - - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, 'text', 1) - - combo.set_active(0) - # Signal connect appears below - - if media_data_obj.master_dbid == media_data_obj.dbid: - combo.set_sensitive(False) - else: - radiobutton2.set_active(True) - combo.set_sensitive(True) - - # Signal connects from above - radiobutton.connect( - 'toggled', - self.on_radiobutton_toggled, - combo, - ) - - radiobutton2.connect( - 'toggled', - self.on_radiobutton2_toggled, - combo, - ) - - combo.connect('changed', self.on_combo_changed, radiobutton2) - - # Display the dialogue window - self.show_all() - - - def on_combo_changed(self, combo, radiobutton2): - - """Called from callback in self.__init__(). - - Store the combobox's selected item, so the calling function can - retrieve it. - - Args: - - combo (Gtk.ComboBox): The clicked widget - - radiobutton2 (Gtk.RadioButton): Another widget to check - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16408 on_combo_changed') - - tree_iter = combo.get_active_iter() - model = combo.get_model() - pixbuf, name = model[tree_iter][:2] - - # (Allow for the possibility that the media data object might have - # been deleted, since the dialogue window opened) - if name in self.main_win_obj.app_obj.media_name_dict: - dbid = self.main_win_obj.app_obj.media_name_dict[name] - obj = self.main_win_obj.app_obj.media_reg_dict[dbid] - self.choice = obj.dbid - - if not radiobutton2.get_active(): - self.main_win_obj.set_previous_alt_dest_dbid(None) - else: - self.main_win_obj.set_previous_alt_dest_dbid(obj.dbid) - - - def on_radiobutton_toggled(self, radiobutton, combo): - - """Called from callback in self.__init__(). - - When the specified radiobutton is toggled, modify other widgets in the - dialogue window, and set self.choice (the value to be retrieved by the - calling function) - - Args: - - radiobutton (Gtk.RadioButton): The clicked widget - - combo (Gtk.ComboBox): The widget containing the user's choice - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16444 on_radiobutton_toggled') - - if radiobutton.get_active(): - combo.set_sensitive(False) - self.choice = self.media_data_obj.dbid - - self.main_win_obj.set_previous_alt_dest_dbid(None) - - - def on_radiobutton2_toggled(self, radiobutton2, combo): - - """Called from callback in self.__init__(). - - When the specified radiobutton is toggled, modify other widgets in the - dialogue window, and set self.choice (the value to be retrieved by the - calling function) - - Args: - - radiobutton2 (Gtk.RadioButton): The clicked widget - - combo (Gtk.ComboBox): The widget containing the user's choice - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16470 on_radiobutton2_toggled') - - if radiobutton2.get_active(): - combo.set_sensitive(True) - - tree_iter = combo.get_active_iter() - model = combo.get_model() - pixbuf, name = model[tree_iter][:2] - - # (Allow for the possibility that the media data object might have - # been deleted, since the dialogue window opened) - if name in self.main_win_obj.app_obj.media_name_dict: - dbid = self.main_win_obj.app_obj.media_name_dict[name] - obj = self.main_win_obj.app_obj.media_reg_dict[dbid] - self.choice = obj.dbid - - self.main_win_obj.set_previous_alt_dest_dbid(dbid) - - -class SetDirectoryDialogue_LinuxBSD(Gtk.Dialog): - - """Called by mainapp.TartubeApp.notify_user_of_data_dir(). - - Python class handling a dialogue window that prompts the user to set the - directory used as Tartube's data directory. - - Only used after a new installation on Linux/BSD. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - default_dir (str): The path to the default data directory, which is the - current value of mainapp.TartubeApp.data_dir - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, default_dir): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16514 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.button = None # Gtk.Button - self.button2 = None # Gtk.Button - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Welcome to ' + __main__.__prettyname__ + '!', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid.set_column_spacing(main_win_obj.spacing_size * 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' \ - + html.escape( - utils.tidy_up_long_string(default_dir, 50, True, True), - ) + '\n', - ) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.button = Gtk.RadioButton.new_with_label_from_widget( - None, - '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) - grid.attach(self.button2, 1, 2, 1, 1) - - # Display the dialogue window - self.show_all() - - -class SetDirectoryDialogue_MSWin(Gtk.Dialog): - - """Called by mainapp.TartubeApp.notify_user_of_data_dir(). - - Python class handling a dialogue window that prompts the user to set the - directory used as Tartube's data directory. - - Only used after a new installation on MS Windows. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16610 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Welcome to ' + __main__.__prettyname__ + '!', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(True) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid.set_column_spacing(main_win_obj.spacing_size * 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, - ), - 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, - ), - ] - - label = Gtk.Label(newline.join(line_list)) - grid.attach(label, 1, 0, 1, 1) - - # Display the dialogue window - self.show_all() - - -class SetNicknameDialogue(Gtk.Dialog): - - """Called by MainWin.on_video_index_set_nickname(). - - Python class handling a dialogue window that prompts the user to set the - nickname of a channel, playlist or folder. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist, media.Folder): The media - data object whose nickname is to be changed - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16705 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Set nickname', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - media_type = media_data_obj.get_type() - label = Gtk.Label( - 'Set the nickname for the ' + media_type + ' \'' \ - + media_data_obj.name \ - + '\'\n(or leave it blank to reset the nickname)', - ) - grid.attach(label, 0, 0, 1, 1) - - # (Store various widgets as IVs, so the calling function can retrieve - # their contents) - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 1, 1) - self.entry.set_text(media_data_obj.nickname) - - # Display the dialogue window - self.show_all() - - -class SystemCmdDialogue(Gtk.Dialog): - - """Called by MainWin.on_video_index_show_system_cmd() and - .on_video_catalogue_show_system_cmd(). - - Python class handling a dialogue window that shows the user the system - command that would be used in a download operation for a particular - media.Video, media.Channel, media.Playlist or media.Folder object. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object in question - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16785 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.textbuffer = None # Gtk.TextBuffer - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Show system command', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - (Gtk.STOCK_OK, Gtk.ResponseType.OK), - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - grid_width = 3 - - media_type = media_data_obj.get_type() - label = Gtk.Label( - utils.shorten_string( - utils.upper_case_first(media_type) + ': ' \ - + media_data_obj.name, - 50, - ), - ) - grid.attach(label, 0, 0, grid_width, 1) - - frame = Gtk.Frame() - grid.attach(frame, 0, 1, grid_width, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_size_request(400, 150) - - textview = Gtk.TextView() - scrolled.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_hexpand(False) - textview.set_editable(False) - - self.textbuffer = textview.get_buffer() - # Initialise the textbuffer's contents - self.update_textbuffer(media_data_obj) - - button = Gtk.Button('Update') - grid.attach(button, 0, 2, 1, 1) - button.set_hexpand(True) - button.connect( - 'clicked', - self.on_update_clicked, - media_data_obj, - ) - - button2 = Gtk.Button('Copy to clipboard') - grid.attach(button2, 1, 2, 1, 1) - button2.set_hexpand(True) - button2.connect( - 'clicked', - self.on_copy_clicked, - media_data_obj, - ) - - # Separator - grid.attach(Gtk.HSeparator(), 0, 3, 2, 1) - - # Display the dialogue window - self.show_all() - - - # Public class methods - - - def update_textbuffer(self, media_data_obj): - - """Called from self.__init__(). - - Initialises the specified textbuffer. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object whose system command is - displayed in this dialogue window - - Returns: - - A string containing the system command displayed, or an empty - string if the system command could not be generated - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16895 update_textbuffer') - - # Get the options.OptionsManager object that applies to this media - # data object - # (The manager might be specified by obj itself, or it might be - # specified by obj's parent, or we might use the default - # options.OptionsManager) - options_obj = utils.get_options_manager( - self.main_win_obj.app_obj, - media_data_obj, - ) - - # Generate the list of download options for this media data object - options_parser_obj = options.OptionsParser(self.main_win_obj.app_obj) - options_list = options_parser_obj.parse(media_data_obj, options_obj) - - # Obtain the system command used to download this media data object - cmd_list = utils.generate_system_cmd( - self.main_win_obj.app_obj, - media_data_obj, - options_list, - ) - - # Display it in the textbuffer - if cmd_list: - char = ' ' - system_cmd = char.join(cmd_list) - - else: - system_cmd = '' - - self.textbuffer.set_text(system_cmd) - return system_cmd - - - # (Callbacks) - - - def on_copy_clicked(self, button, media_data_obj): - - """Called from a callback in self.__init__(). - - Updates the contents of the textview, and copies the system command to - the clipboard. - - Args: - - button (Gtk.Button): The widget clicked - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object whose system command is - displayed in this dialogue window - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16951 on_copy_clicked') - - # Obtain the system command used to download this media data object, - # and display it in the textbuffer - system_cmd = self.update_textbuffer(media_data_obj) - - # Copy the system command to the clipboard - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clipboard.set_text(system_cmd, -1) - - - def on_update_clicked(self, button, media_data_obj): - - """Called from a callback in self.__init__(). - - Updates the contents of the textview. - - Args: - - button (Gtk.Button): The widget clicked - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object whose system command is - displayed in this dialogue window - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 16979 on_update_clicked') - - # Obtain the system command used to download this media data object, - # and display it in the textbuffer - self.update_textbuffer(media_data_obj) - - -class TestCmdDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_test_ytdl() and - MainWin.on_video_catalogue_test_dl() - - Python class handling a dialogue window that prompts the user for a - URL and youtube-dl options. If the user specifies one or both, they are - used in an info operation. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - source_url (str): If specified, this URL is added to the Gtk.Entry - automatically - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, source_url=None): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17011 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.entry = None # Gtk.Entry - self.textbuffer = None # Gtk.TextBuffer - - - # Code - # ---- - - Gtk.Dialog.__init__( - self, - 'Test youtube-dl', - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - label = Gtk.Label( - 'URL of the video to download (optional)' - ) - grid.attach(label, 0, 0, 1, 1) - - self.entry = Gtk.Entry() - grid.attach(self.entry, 0, 1, 1, 1) - self.entry.set_hexpand(True) - if source_url is not None: - self.entry.set_text(source_url) - - label2 = Gtk.Label( - 'youtube-dl command line options (optional)' - ) - grid.attach(label2, 0, 2, 1, 1) - - frame = Gtk.Frame() - grid.attach(frame, 0, 3, 1, 1) - - scrolled = Gtk.ScrolledWindow() - frame.add(scrolled) - scrolled.set_size_request(400, 150) - - textview = Gtk.TextView() - scrolled.add(textview) - textview.set_wrap_mode(Gtk.WrapMode.WORD) - textview.set_hexpand(False) - if source_url is not None: - # The calling function has already specified a URL, so move the - # cursor straight into the textview - textview.grab_focus() - - self.textbuffer = textview.get_buffer() - - # Display the dialogue window - self.show_all() - - -class TidyDialogue(Gtk.Dialog): - - """Called by mainapp.TartubeApp.on_menu_tidy_up() and - MainWin.on_video_index_tidy(). - - Python class handling a dialogue window that prompts the user for which - actions to perform during a tidy operation. If the user selects at least - one action, the calling function starts a tidy operation to apply them. - - Args: - - main_win_obj (mainwin.MainWin): The parent main window - - media_data_obj (media.Channel, media.Playlist or media.Folder): If - specified, only this media data object (and its children) are - tidied up - - """ - - - # Standard class methods - - - def __init__(self, main_win_obj, media_data_obj=None): - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17113 __init__') - - # IV list - class objects - # ----------------------- - # Tartube's main window - self.main_win_obj = main_win_obj - - - # IV list - Gtk widgets - # --------------------- - self.checkbutton = None # Gtk.CheckButton - self.checkbutton2 = None # Gtk.CheckButton - self.checkbutton3 = None # Gtk.CheckButton - self.checkbutton4 = None # Gtk.CheckButton - self.checkbutton5 = None # Gtk.CheckButton - self.checkbutton6 = None # Gtk.CheckButton - self.checkbutton7 = None # Gtk.CheckButton - self.checkbutton8 = None # Gtk.CheckButton - self.checkbutton9 = None # Gtk.CheckButton - self.checkbutton10 = None # Gtk.CheckButton - - - # Code - # ---- - - if media_data_obj is None: - title = 'Tidy up files' - elif isinstance(media_data_obj, media.Channel): - title = 'Tidy up channel' - elif isinstance(media_data_obj, media.Channel): - title = 'Tidy up playlist' - else: - title = 'Tidy up folder' - - Gtk.Dialog.__init__( - self, - title, - main_win_obj, - Gtk.DialogFlags.DESTROY_WITH_PARENT, - ( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK, - ) - ) - - self.set_modal(False) - - # Set up the dialogue window - box = self.get_content_area() - - grid = Gtk.Grid() - box.add(grid) - grid.set_border_width(main_win_obj.spacing_size) - grid.set_row_spacing(main_win_obj.spacing_size) - - # 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.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_sensitive(False) - - if not mainapp.HAVE_MOVIEPY_FLAG \ - or self.main_win_obj.app_obj.refresh_moviepy_timeout == 0: - self.checkbutton.set_sensitive(False) - self.checkbutton2.set_sensitive(False) - - self.checkbutton3 = Gtk.CheckButton() - grid.attach(self.checkbutton3, 0, 2, 1, 1) - 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)', - ) - 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', - ) - 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.checkbutton7 = Gtk.CheckButton() - grid.attach(self.checkbutton7, 1, 1, 1, 1) - 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.checkbutton9 = Gtk.CheckButton() - grid.attach(self.checkbutton9, 1, 3, 1, 1) - 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') - - # Bottom strip - - 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') - grid.attach(button, 1, 6, 1, 1) - button.set_hexpand(False) - button.connect('clicked', self.on_select_none_clicked) - - # Display the dialogue window - self.show_all() - - - def on_checkbutton_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - When the 'Check that videos are not corrupted' button is toggled, - update the 'Delete corrupted videos...' button. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17255 on_checkbutton_toggled') - - if not checkbutton.get_active(): - self.checkbutton2.set_active(False) - self.checkbutton2.set_sensitive(False) - - else: - self.checkbutton2.set_sensitive(True) - - - def on_checkbutton4_toggled(self, checkbutton): - - """Called from a callback in self.__init__(). - - When the 'Delete downloaded video files' button is toggled, update the - 'Also delete...' button. - - Args: - - checkbutton (Gtk.CheckButton): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17279 on_checkbutton4_toggled') - - if not checkbutton.get_active(): - self.checkbutton5.set_active(False) - self.checkbutton5.set_sensitive(False) - - else: - self.checkbutton5.set_sensitive(True) - - - def on_select_all_clicked(self, button): - - """Called from a callback in self.__init__(). - - Select all checkbuttons. - - Args: - - button (Gtk.Button): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17302 on_select_all_clicked') - - self.checkbutton.set_active(True) - self.checkbutton2.set_active(True) - self.checkbutton3.set_active(True) - self.checkbutton4.set_active(True) - self.checkbutton5.set_active(True) - self.checkbutton6.set_active(True) - self.checkbutton7.set_active(True) - self.checkbutton8.set_active(True) - self.checkbutton9.set_active(True) - self.checkbutton10.set_active(True) - - - def on_select_none_clicked(self, button): - - """Called from a callback in self.__init__(). - - Unselect all checkbuttons. - - Args: - - button (Gtk.Button): The clicked widget - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('mwn 17239 on_select_none_clicked') - - self.checkbutton.set_active(False) - self.checkbutton2.set_active(False) - self.checkbutton3.set_active(False) - self.checkbutton4.set_active(False) - self.checkbutton5.set_active(False) - self.checkbutton6.set_active(False) - self.checkbutton7.set_active(False) - self.checkbutton8.set_active(False) - self.checkbutton9.set_active(False) - self.checkbutton10.set_active(False) diff --git a/tartube/media.py b/tartube/media.py deleted file mode 100755 index c86dce7..0000000 --- a/tartube/media.py +++ /dev/null @@ -1,2504 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Media data classes.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import datetime -import functools -import os -import re -import time - - -# Import our modules -import mainapp -import utils - - -# Classes - - -class GenericMedia(object): - - """Base python class inherited by media.Video, media.Channel, - media.Playlist and media.Folder.""" - - - # Public class methods - - def get_type(self): - - if isinstance(self, Channel): - return 'channel' - elif isinstance(self, Playlist): - return 'playlist' - elif isinstance(self, Folder): - return 'folder' - else: - return 'video' - - - # Set accessors - - - def set_dl_sim_flag(self, flag): - - if flag: - self.dl_sim_flag = True - else: - self.dl_sim_flag = False - - - def set_error(self, msg): - - # The media.Folder object has no error/warning IVs (and shouldn't - # receive any error/warning messages) - if not isinstance(self, Folder): - self.error_list.append(msg) - - - def reset_error_warning(self): - - # The media.Folder object has no error/warning IVs (and shouldn't - # receive any error/warning messages) - if not isinstance(self, Folder): - self.error_list = [] - self.warning_list = [] - - - def set_fav_flag(self, flag): - - if flag: - self.fav_flag = True - else: - self.fav_flag = False - - - def set_nickname(self, nickname): - - if nickname is None or nickname == '': - self.nickname = self.name - else: - self.nickname = nickname - - - def set_options_obj(self, options_obj): - - self.options_obj = options_obj - - - def set_parent_obj(self, parent_obj): - - self.parent_obj = parent_obj - - - def set_warning(self, msg): - - # The media.Folder object has no error/warning IVs (and shouldn't - # receive any error/warning messages) - if not isinstance(self, Folder): - self.warning_list.append(msg) - - -class GenericContainer(GenericMedia): - - """Base python class inherited by media.Channel, media.Playlist and - media.Folder.""" - - - # Public class methods - - - def compile_all_containers(self, container_list): - - """Can be called by anything. Subsequently called by this function - recursively. - - Appends to the specified list this container, then calls all this - function recursively for all media.Channel, media.Playlist and - media.Folder objects, so they too can be added to the list. - - Args: - - container_list (list): A list of media.Channel, media.Playlist and - media.Folder objects - - Returns: - - The modified container_list - - """ - - container_list.append(self) - for child_obj in self.child_list: - - if not isinstance(child_obj, Video): - child_obj.compile_all_containers(container_list) - - return container_list - - - def compile_all_videos(self, video_list): - - """Can be called by anything. Subsequently called by this function - recursively. - - Appends to the specified list all child objects that are media.Video - objects, then calls this function recursively for all other child - objects, so they can add their children too. - - Args: - - video_list (list): A list of media.Video objects - - Returns: - - The modified video_list - - """ - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - video_list.append(child_obj) - else: - child_obj.compile_all_videos(video_list) - - return video_list - - - def count_descendants(self, count_list): - - """Can be called by anything. Subsequently called by this function - recursively. - - Counts the number of child objects, and then calls this function - recursively in those child objects to count their child objects. - - Args: - - count_list (list): A list representing the child objects counted - so far. List in the form - ( - total_count, video_count, channel_count, - playlist_count, folder_count, - ) - - Returns: - - The modified count_list - - """ - - for child_obj in self.child_list: - - count_list[0] += 1 - - if isinstance(child_obj, Video): - count_list[1] += 1 - else: - count_list = child_obj.count_descendants(count_list) - if isinstance(child_obj, Channel): - count_list[2] += 1 - elif isinstance(child_obj, Playlist): - count_list[3] += 1 - else: - count_list[4] += 1 - - return count_list - - - def del_child(self, child_obj): - - """Can be called by anything. - - Deletes a child object from self.child_list, first checking that it's - actually a child of this object. - - Args: - - child_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The child object to delete - - Returns: - - True if the child object was deleted, False if the specified object - was not a child of this object - - """ - - # Check this is really one of our children - if not child_obj in self.child_list: - return False - - else: - self.child_list.remove(child_obj) - - if isinstance(child_obj, Video): - self.vid_count -= 1 - - if child_obj.bookmark_flag: - self.bookmark_count -= 1 - - if child_obj.dl_flag: - self.dl_count -= 1 - - if child_obj.fav_flag: - self.fav_count -= 1 - - if child_obj.new_flag: - self.new_count -= 1 - - if child_obj.waiting_flag: - self.waiting_count -= 1 - - return True - - - def fetch_tooltip_text(self, app_obj, max_length): - - """Can be called by anything. - - Returns a string to be used as a tooltip for this channel, playlist or - folder. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - max_length (int or None): If specified, the maximum line length, in - characters - - Returns: - - Text containing the channel/playlist/folder directory path and - the source (except for folders), ready for display in a tooltip - - """ - - text = '#' + str(self.dbid) + ': ' + self.name + '\n\n' - - if not isinstance(self, Folder): - - text += 'Source:\n' - if self.source is None: - text += ' ' - else: - text += self.source - - text += '\n\n' - - text += 'Location:\n' - - location = self.get_default_dir(app_obj) - if location is None: - text += ' ' - 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 - - # Need to escape question marks or we'll get a markup error - text = re.sub('&', '&', text) - - # Apply a maximum line length, if required - if max_length is not None: - text = utils.tidy_up_long_descrip(text, max_length) - - return text - - - def find_matching_video(self, app_obj, name): - - """Can be called by anything. - - Checks all of this object's child objects, looking for a media.Video - object with a matching name. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - name (str): The name of the media.Video object to find - - Returns: - - The first matching media.Video object found, or None if no matching - videos are found. - - """ - - method = app_obj.match_method - first = app_obj.match_first_chars - ignore = app_obj.match_ignore_chars * -1 - - # Defend against two different of a name from the same video, one with - # punctuation marks stripped away, and double quotes converted to - # single quotes (thanks, YouTube!) by replacing those characters with - # whitespace - # (After extensive testing, this is the only regex sequence I could - # find that worked) - test_name = name[:] - - # Remove punctuation - test_name = re.sub(r'\W+', ' ', test_name, flags=re.UNICODE) - # Also need to replace underline characters - test_name = re.sub(r'[\_\s]+', ' ', test_name) - # Also need to remove leading/trailing whitespace, in case the original - # video name started/ended with a question mark or something like - # that - test_name = re.sub(r'^\s+', '', test_name) - test_name = re.sub(r'\s+$', '', test_name) - - for child_obj in self.child_list: - if isinstance(child_obj, Video): - - child_name = child_obj.name[:] - child_name = re.sub( - r'\W+', - ' ', - child_name, - flags=re.UNICODE, - ) - child_name = re.sub(r'[\_\s]+', ' ', child_name) - child_name = re.sub(r'^\s+', '', child_name) - child_name = re.sub(r'\s+$', '', child_name) - - if ( - method == 'exact_match' \ - and child_name == test_name - ) or ( - method == 'match_first' \ - and child_name[:first] == test_name[:first] - ) or ( - method == 'ignore_last' \ - and child_name[:ignore] == test_name[:ignore] - ): - return child_obj - - # No matches found - return None - - - def get_depth(self): - - """Can be called by anything. - - There is a limit to the depth of the media registry (a maximum number - of levels). - - This function finds the level occupied by this container object and - returns it. - - If this object has no parent, it is at level 1. If it has a parent - object, and the parent itself has no parent, this object is at level 2. - - Returns: - - The container object's level - - """ - - if self.parent_obj is None: - return 1 - - else: - level = 1 - parent_obj = self.parent_obj - - while parent_obj is not None: - level += 1 - parent_obj = parent_obj.parent_obj - - return level - - - def is_hidden(self): - - """Called by mainwin.MainWin.video_index_add_row() and - .video_index_select_row(). - - If this is a hidden media.Folder object, return True. - - If the parent media.Folder (or the parent's parent, and so on) is - hidden, return True. - - Otherwise, return False. (media.Channel and media.Playlist objects - can't be hidden directly.) - - Returns: - - True or False. - - """ - - if isinstance(self, Folder) and self.hidden_flag: - return True - - parent_obj = self.parent_obj - - while parent_obj: - if isinstance(parent_obj, Folder) and parent_obj.hidden_flag: - return True - else: - parent_obj = parent_obj.parent_obj - - return False - - - def prepare_export(self, include_video_flag, include_channel_flag, - include_playlist_flag): - - """Called by mainapp.TartubeApp.export_from_db(). Subsequently called - by this function recursively. - - Creates the dictionary, to be saved as a JSON file, described in the - comments to that function. This function is called when we want to - preserve the folder structure of the Tartube database. - - Args: - - include_video_flag (bool): If True, include videos. If False, don't - include them - - include_channel_flag (bool): If True, include channels (and their - videos, if allowed). If False, ignore them - - include_playlist_flag (bool): If True, include playlists (and their - videos, if allowed). If False, ignore them - - Returns: - - return_dict (dict): A dictionary described in the comments in the - calling function - - """ - - # 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' - - elif isinstance(self, Folder): - if self.fixed_flag: - return - else: - media_type = 'folder' - - # This dictionary contains values for the children of this object - db_dict = {} - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - - # (Don't bother exporting a video whose source URL is not - # known) - if include_video_flag and child_obj.source is not None: - - mini_dict = { - 'type': 'video', - 'dbid': child_obj.dbid, - 'name': child_obj.name, - 'nickname': None, - 'source': child_obj.source, - 'db_dict': {}, - } - - db_dict[child_obj.dbid] = mini_dict - - else: - - mini_dict = child_obj.prepare_export( - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - if mini_dict: - db_dict[child_obj.dbid] = mini_dict - - # This dictionary contains values for this object, and for the children - # of this object - return_dict = { - 'type': media_type, - 'dbid': self.dbid, - 'name': self.name, - 'nickname': self.nickname, - 'source': None, - 'db_dict': db_dict, - } - - if media_type != 'folder': - return_dict['source'] = self.source - - # Procedure complete - return return_dict - - - def prepare_flat_export(self, db_dict, include_video_flag, - include_channel_flag, include_playlist_flag): - - """Called by mainapp.TartubeApp.export_from_db(). Subsequently called - by this function recursively. - - Creates the dictionary, to be saved as a JSON file, described in the - comments to that function. This function is called when we don't want - to preserve the folder structure of the Tartube database. - - Args: - - db_dict (dict): The dictionary described in the comments in the - calling function - - include_video_flag (bool): If True, include videos. If False, don't - include them - - include_channel_flag (bool): If True, include channels (and their - videos, if allowed). If False, ignore them - - include_playlist_flag (bool): If True, include playlists (and their - videos, if allowed). If False, ignore them - - Returns: - - db_dict (dict): The modified dictionary - - """ - - # 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 - - 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' - - # Add values to the dictionary - if media_type == 'channel' or media_type == 'playlist': - - child_dict = {} - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - - # (Don't bother exporting a video whose source URL is not - # known) - if include_video_flag and child_obj.source is not None: - - child_mini_dict = { - 'type': 'video', - 'dbid': child_obj.dbid, - 'name': child_obj.name, - 'nickname': None, - 'source': child_obj.source, - 'db_dict': {}, - } - - child_dict[child_obj.dbid] = child_mini_dict - - else: - - db_dict = child_obj.prepare_flat_export( - db_dict, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - mini_dict = { - 'type': media_type, - 'dbid': self.dbid, - 'name': self.name, - 'nickname': self.nickname, - 'source': self.source, - 'db_dict': child_dict, - } - - db_dict[self.dbid] = mini_dict - - elif media_type == 'folder': - - for child_obj in self.child_list: - - if not isinstance(child_obj, Video): - - db_dict = child_obj.prepare_flat_export( - db_dict, - include_video_flag, - include_channel_flag, - include_playlist_flag, - ) - - # Procedure complete - return db_dict - - - def recalculate_counts(self): - - """Can be called by anything. - - Recalculates all count IVs. - """ - - self.vid_count = 0 - self.bookmark_count = 0 - self.dl_count = 0 - self.fav_count = 0 - self.new_count = 0 - self.waiting_count = 0 - - for child_obj in self.child_list: - - if isinstance(child_obj, Video): - self.vid_count += 1 - - if child_obj.bookmark_flag: - self.bookmark_count += 1 - - if child_obj.dl_flag: - self.dl_count += 1 - - if child_obj.fav_flag: - self.fav_count += 1 - - if child_obj.new_flag: - self.new_count += 1 - - if child_obj.waiting_flag: - self.waiting_count += 1 - - - # Set accessors - - - def reset_counts(self, vid_count, bookmark_count, dl_count, fav_count, - new_count, waiting_count): - - """Called by mainapp.TartubeApp.update_db(). - - When a database created by an earlier version of Tartube is loaded, - the calling function updates IVs as required. - - This function is called if this object's video counts need to be - changed. - """ - - self.vid_count = vid_count - self.bookmark_count = bookmark_count - self.dl_count = dl_count - self.fav_count = fav_count - self.new_count = new_count - self.waiting_count = waiting_count - - - def inc_bookmark_count(self): - - self.bookmark_count += 1 - - - def dec_bookmark_count(self): - - self.bookmark_count -= 1 - - - def inc_dl_count(self): - - self.dl_count += 1 - - - def dec_dl_count(self): - - self.dl_count -= 1 - - - def set_dl_disable_flag(self, flag): - - if flag: - self.dl_disable_flag = True - else: - self.dl_disable_flag = False - - - def inc_fav_count(self): - - self.fav_count += 1 - - - def dec_fav_count(self): - - self.fav_count -= 1 - - - def set_master_dbid(self, app_obj, dbid): - - if dbid == self.master_dbid: - # No change to the current value - return - - else: - - # Update the old alternative download destination - if self.master_dbid != self.dbid: - - # (If mainapp.TartubeApp.fix_integrity_db() is fixing an - # error, the old destination object might not exist) - if self.master_dbid in app_obj.media_reg_dict: - old_dest_obj = app_obj.media_reg_dict[self.master_dbid] - old_dest_obj.del_slave_dbid(self.dbid) - - # Update this object's IV - self.master_dbid = dbid - - if self.master_dbid != self.dbid: - - # Update the new alternative download destination - new_dest_obj = app_obj.media_reg_dict[self.master_dbid] - new_dest_obj.add_slave_dbid(self.dbid) - - - def inc_new_count(self): - - self.new_count += 1 - - - def dec_new_count(self): - - self.new_count -= 1 - - - def inc_waiting_count(self): - - self.waiting_count += 1 - - - def dec_waiting_count(self): - - self.waiting_count -= 1 - - - def add_slave_dbid(self, dbid): - - """Called by self.set_master_dbid() only.""" - - # (Failsafe: don't add the same value to self.slave_dbid_list) - match_flag = False - for slave_dbid in self.slave_dbid_list: - if slave_dbid == dbid: - match_flag = True - break - - if not match_flag: - self.slave_dbid_list.append(dbid) - - - def del_slave_dbid(self, dbid): - - """Called by mainapp.TartubeApp.fix_integrity_db() or by - self.set_master_dbid() only.""" - - new_list = [] - - for slave_dbid in self.slave_dbid_list: - if slave_dbid != dbid: - new_list.append(slave_dbid) - - self.slave_dbid_list = new_list.copy() - - - def set_name(self, name): - - # Update the nickname at the same time, if it has the same value as - # this object's name - if self.nickname == self.name: - self.nickname = name - - self.name = name - - - # Get accessors - - - def get_actual_dir(self, app_obj, new_name=None): - - """Can be called by anything. - - Fetches the full path to the sub-directory actually used by this - channel, playlist or folder. - - If self.dbid and self.master_dbid are the same, then files are - downloaded to the default location; the sub-directory belonging to the - channel/playlist/folder. In that case, this function returns the same - value as self.get_default_dir(). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. This function returns that sub-directory. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - Optional args: - - new_name (str): If specified, fetches the full path to the - sub-directory that would be used by this channel, playlist or - folder, if it were renamed to 'new_name'. If not specified, the - channel/playlist/folder's actual name is used - - Returns: - - The full path to the sub-directory - - """ - - if self.master_dbid != self.dbid: - - master_obj = app_obj.media_reg_dict[self.master_dbid] - return master_obj.get_default_dir(app_obj, new_name) - - else: - - return self.get_default_dir(app_obj, new_name) - - - def get_default_dir(self, app_obj, new_name=None): - - """Can be called by anything. - - Fetches the full path to the sub-directory used by this channel, - playlist or folder by default. - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. To get the actual download sub-directory, call - self.get_actual_dir(). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - Optional args: - - new_name (str): If specified, fetches the full path to the - sub-directory that would be used by this channel, playlist or - folder, if it were renamed to 'new_name'. If not specified, the - channel/playlist/folder's actual name is used - - Returns: - - The full path to the sub-directory - - """ - - if new_name is not None: - dir_list = [new_name] - else: - dir_list = [self.name] - - obj = self - while obj.parent_obj: - - obj = obj.parent_obj - dir_list.insert(0, obj.name) - - return os.path.abspath(os.path.join(app_obj.downloads_dir, *dir_list)) - - - def get_relative_actual_dir(self, app_obj, new_name=None): - - """Can be called by anything. - - Fetches the path to the sub-directory used by this channel, playlist or - folder, relative to mainapp.TartubeApp.downloads_dir. - - If self.dbid and self.master_dbid are the same, then files are - downloaded to the default location; the sub-directory belonging to the - channel/playlist/folder. In that case, this function returns the same - value as self.get_default_dir(). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. This function returns that sub-directory. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - new_name (str): If specified, fetches the relative path to the - sub-directory that would be used by this channel, playlist or - folder, if it were renamed to 'new_name'. If not specified, the - channel/playlist/folder's actual name is used - - Returns: - - The path to the sub-directory relative to - mainapp.TartubeApp.downloads_dir - - """ - - if self.master_dbid != self.dbid: - - master_obj = app_obj.media_reg_dict[self.master_dbid] - return master_obj.get_relative_default_dir(app_obj, new_name) - - else: - - return self.get_relative_default_dir(app_obj, new_name) - - - def get_relative_default_dir(self, new_name=None): - - """Can be called by anything. - - Fetches the path to the sub-directory used by this channel, playlist or - folder by default, relative to mainapp.TartubeApp.downloads_dir. - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. To get the actual download sub-directory, call - self.get_relative_actual_dir(). - - Args: - - new_name (str): If specified, fetches the relative path to the - sub-directory that would be used by this channel, playlist or - folder, if it were renamed to 'new_name'. If not specified, the - channel/playlist/folder's actual name is used - - Returns: - - The path to the sub-directory relative to - mainapp.TartubeApp.downloads_dir - - """ - - if new_name is not None: - dir_list = [new_name] - else: - dir_list = [self.name] - - obj = self - while obj.parent_obj: - - obj = obj.parent_obj - dir_list.insert(0, obj.name) - - return os.path.join(*dir_list) - - -class GenericRemoteContainer(GenericContainer): - - """Base python class inherited by media.Channel and media.Playlist.""" - - - # Public class methods - - - def add_child(self, child_obj, no_sort_flag=False): - - """Can be called by anything. - - Adds a child media data object, which must be a media.Video object. - - Args: - - child_obj (media.Video): The child object - - no_sort_flag (bool): True when the calling code wants to delay - sorting the parent container object, for some reason; False if - not - - """ - - # Only media.Video objects can be added to a channel or playlist as a - # child object. Also, check this is not already a child object - if isinstance(child_obj, Video) or child_obj in self.child_list: - - self.child_list.append(child_obj) - if not no_sort_flag: - self.sort_children() - - if isinstance(child_obj, Video): - self.vid_count += 1 - - - def do_sort(self, obj1, obj2): - - """Sorting function used by functools.cmp_to_key(), and called by - self.sort_children(). - - Sort videos by upload time, with the most recent video first. - - When downloading a channel or playlist, we assume that YouTube (etc) - supplies us with the most recent upload first. Therefore, when the - upload time is the same, sort by the order in youtube-dl fetches the - videos. - - Args: - - obj1, obj2 (media.Video) - Video objects being sorted - - Returns: - - -1 if obj1 comes first, 1 if obj2 comes first, 0 if they are equal - - """ - - # The video's index is not relevant unless sorting a playlist - if 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 - elif obj1.upload_time is not None and obj2.upload_time is not None: - if obj1.upload_time > obj2.upload_time: - return -1 - elif obj1.upload_time < obj2.upload_time: - return 1 - elif obj1.receive_time is not None \ - and obj2.receive_time is not None: - if obj1.receive_time < obj2.receive_time: - return -1 - elif obj1.receive_time > obj2.receive_time: - return 1 - else: - return 0 - else: - return 0 - - - def sort_children(self): - - """Can be called by anything. For example, called by self.add_child(). - - Sorts the child media.Video objects by upload time. - """ - - # Sort a copy of the list to prevent 'list modified during sort' - # errors - while True: - - copy_list = self.child_list.copy() - copy_list.sort(key=functools.cmp_to_key(self.do_sort)) - - if len(copy_list) == len(self.child_list): - self.child_list = copy_list.copy() - break - - - self.child_list.sort(key=functools.cmp_to_key(self.do_sort)) - - - # Set accessors - - - def clone_properties(self, other_obj): - - """Called by mainapp.TartubeApp.convert_remote_container() only. - - Copies properties from a media data object (about to be deleted) to - this media data object. - - Some properties are handled by the calling function; this function - handles the rest of them. - - Args: - - other_obj (media.Channel, media.Playlist): The object whose - properties should be copied - - """ - - self.options_obj = other_obj.options_obj - self.nickname = other_obj.nickname - self.source = other_obj.source - self.dl_sim_flag = other_obj.dl_sim_flag - self.dl_disable_flag = other_obj.dl_disable_flag - self.fav_flag = other_obj.fav_flag - - self.bookmark_count = other_obj.bookmark_count - self.dl_count = other_obj.dl_count - self.fav_count = other_obj.fav_count - self.new_count = other_obj.new_count - self.waiting_count = other_obj.waiting_count - - self.error_list = other_obj.error_list.copy() - self.warning_list = other_obj.warning_list.copy() - - - def set_source(self, source): - - self.source = source - - -class Video(GenericMedia): - - """Python class that handles an individual video. - - Args: - - dbid (int): A unique ID for this media data object - - name (str): The video name - - parent_obj (media.Channel, media.Playlist, media.Folder): The parent - media data object, if any - - options_obj (options.OptionsManager): The object specifying download - options for this video, if any - - no_sort_flag (bool): True when the calling code wants to delay sorting - the parent container object, for some reason; False if not - - """ - - - # Standard class methods - - - def __init__(self, dbid, name, parent_obj, options_obj=None, - no_sort_flag=False): - - # IV list - class objects - # ----------------------- - # The parent object (a media.Channel, media.Playlist or media.Folder - # object. All media.Video objects have a parent) - self.parent_obj = parent_obj - # The options.OptionsManager object that specifies how this video is - # downloaded (or None, if the parent's options.OptionsManager object - # should be used instead) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Unique media data object ID (an integer) - self.dbid = dbid - - # Video name - self.name = name - # Video nickname (displayed in the Video Catalogue) - # If the video's JSON data has been fetched, self.name matches the - # actual filename of the video, and self.nickname is the video's - # title - # (In practical terms, if the user has specified that the video - # filename should be in the format NAME + ID, then self.name will be - # in the format 'NAME + ID', and self.nickname will be in the format - # 'NAME') - # If the video's JSON data has not been fetched, self.name and - # self.nickname are the same - self.nickname = name - # Download source (a URL) - self.source = None - - # Flag set to True if Tartube should always simulate the download of - # video, or False if the downloads.DownloadManager object should - # decide whether to simulate, or not - self.dl_sim_flag = False - - # 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) - self.archive_flag = False - # Flag set to True if the video is marked as bookmarked, so that it - # appears in the 'Bookmarks' system folder - self.bookmark_flag = False - # Flag set to True if the video is marked a favourite. Upon download, - # it's marked as a favourite if the same IV in the parent channel, - # playlist or folder (also in the parent's parent, and so on) is True - self.fav_flag = False - # Flag set to True at the same time self.dl_sim_flag is set to True, - # showing that the video has been downloaded and not watched - self.new_flag = False - # Flag set to True if the video is marked add as added to the - # 'Waiting Videos' system folder - self.waiting_flag = False - - # The video's filename and extension - self.file_name = None - self.file_ext = None - - # Flag set to True once the file has been downloaded, and is confirmed - # to exist in Tartube's data directory - self.dl_flag = False - # The size of the video (in bytes) - self.file_size = None - # The video's upload time (in Unix time) - # YouTube (etc) only supplies a date, which Tartube then converts into - # seconds, so videos uploaded on the same day will have the same - # value for self.upload_time) - self.upload_time = None - # The time at which Tartube downloaded this video (in Unix time) - # When downloading a channel or playlist, we assume that YouTube (etc) - # supplies us with the most recent upload first - # Therefore, when sorting videos by time, if self.upload_time is the - # same (multiple videos were uploaded on the same day), then those - # videos are sorted with the lowest value of self.receive_time first - self.receive_time = None - # The video's duration (in integer seconds) - self.duration = None - # For videos in a channel or playlist (i.e. a media.Video object whose - # parent is a media.Channel or media.Playlist object), the video's - # index in the channel/playlist. (The server supplies an index even - # for a channel, and the user might want to convert a channel to a - # playlist) - # For videos whose parent is a media.Folder, the value remains as None - self.index = None - - # Video description. A string of any length, containing newline - # characters if necessary. (Set to None if the video description is - # not known) - self.descrip = None - # Video short description - the first line in self.descrip, limited to - # a certain number of characters (specifically, - # mainwin.MainWin.very_long_string_max_len) - self.short = None - - # List of error/warning messages generated the last time the video was - # checked or downloaded. Both set to empty lists if the video has - # never been checked or downloaded, or if there was no error/warning - # on the last check/download attempt - # NB If an error/warning message is generated when downloading a - # channel or playlist, the message is stored in the media.Channel - # or media.Playlist object instead - self.error_list = [] - self.warning_list = [] - - - # Code - # ---- - - # Update the parent - self.parent_obj.add_child(self, no_sort_flag) - - - # Public class methods - - - def ancestor_is_favourite(self): - - """Called by mainapp.TartubeApp.mark_video_downloaded(). - - Checks whether any ancestor channel, playlist or folder is marked as - favourite. - - Returns: - - True if the parent (or the parent's parent, and so on) is marked - favourite, False otherwise - - """ - - parent_obj = self.parent_obj - - while parent_obj: - if parent_obj.fav_flag: - return True - else: - parent_obj = parent_obj.parent_obj - - return False - - - def fetch_tooltip_text(self, app_obj, max_length=None): - - """Can be called by anything. - - Returns a string to be used as a tooltip for this video. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - max_length (int or None): If specified, the maximum line length, in - characters - - Returns: - - Text containing the video's file path and source, ready for display - in a tooltip - - """ - - text = '#' + str(self.dbid) + ': ' + self.name + '\n\n' - - 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' - - text += 'Source:\n' - if self.source is None: - text += ' ' - else: - text += self.source - - text += '\n\nFile:\n' - if self.file_name is None: - text += ' ' - else: - text += self.get_actual_path(app_obj) - - # Apply a maximum line length, if required - if max_length is not None: - text = utils.tidy_up_long_descrip(text, max_length) - - return text - - - def read_video_descrip(self, app_obj, max_length): - - """Can be called by anything. - - Reads the .description file, if it exists, and updates IVs. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - max_length (int): When storing the description in this object's - IVs, the maximum line length to use - - """ - - descrip_path = self.get_actual_path_by_ext(app_obj, '.description') - text = app_obj.file_manager_obj.load_text(descrip_path) - if text is not None: - self.set_video_descrip(text, max_length) - - - # Set accessors - - - def set_archive_flag(self, flag): - - if flag: - self.archive_flag = True - else: - self.archive_flag = False - - - def set_bookmark_flag(self, flag): - - if flag: - self.bookmark_flag = True - else: - self.bookmark_flag = False - - - def set_dl_flag(self, flag=False): - - self.dl_flag = flag - - if self.receive_time is None: - self.receive_time = int(time.time()) - - -# def set_dl_sim_flag(): # Inherited from GenericMedia - - - def set_duration(self, duration=None): - - if duration is not None: - if duration != int(duration): - self.duration = int(duration) + 1 - else: - self.duration = duration - - else: - self.duration = None - - - def set_file(self, filename, extension): - - self.file_name = filename - self.file_ext = extension - - - def set_file_size(self, size=None): - - self.file_size = size - - - def set_index(self, index): - - if index is None: - self.index = None - else: - self.index = int(index) - - - def set_mkv(self): - - """Called by mainapp.TartubeApp.update_video_when_file_found() and - refresh.RefreshManager.refresh_from_default_destination(). - - When the warning 'Requested formats are incompatible for merge and will - be merged into mkv' has been seen, the calling function has found an - .mkv file rather than the .mp4 file it was expecting. - - Update the IV. - """ - - self.file_ext = '.mkv' - - - def set_name(self, name): - - self.name = name - - - def set_new_flag(self, flag): - - if flag: - self.new_flag = True - else: - self.new_flag = False - - -# def set_options_obj(): # Inherited from GenericMedia - - - def set_receive_time(self): - - self.receive_time = int(time.time()) - - - def set_source(self, source): - - self.source = source - - - def set_upload_time(self, unix_time=None): - - self.upload_time = int(unix_time) - - - def set_video_descrip(self, descrip, max_length): - - """Can be caled by anything. - - Converts the video description into a list of lines, max_length - characters long (longer lines are split into shorter ones). - - Then uses the first line to set the short description, and uses all - lines to set the full description. - - Args: - - descrip (str): The video description - - max_length (int): A maximum line size - - """ - - if descrip: - - self.descrip = utils.tidy_up_long_descrip(descrip, max_length) - self.short = utils.shorten_string(descrip, max_length) - - else: - self.descrip = None - self.short = None - - - def set_waiting_flag(self, flag): - - if flag: - self.waiting_flag = True - else: - self.waiting_flag = False - - - # Get accessors - - - def get_actual_path(self, app_obj): - - """Can be called by anything. - - Returns the full path to the video file in its actual location. - - If self.dbid and self.master_dbid are the same, then files are - downloaded to the default location; the sub-directory belonging to the - channel/playlist/folder. In that case, this function returns the same - value as self.get_default_path(). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. This function returns a path to the file in that - sub-directory. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - return os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - self.file_name + self.file_ext, - ), - ) - - - def get_actual_path_by_ext(self, app_obj, ext): - - """Can be called by anything. - - Returns the full path to a file associated with the video; specifically - one with the same file name, but a different extension (for example, - the video's thumbnail file). - - If self.dbid and self.master_dbid are the same, then files are - downloaded to the default location; the sub-directory belonging to the - channel/playlist/folder. In that case, this function returns the same - value as self.get_default_path_by_ext(). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. This function returns a path to the file in that - sub-directory. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - ext (str): The extension, e.g. 'png' or '.png' - - """ - - # Add the full stop, if not supplied by the calling function - if not ext.find('.') == 0: - ext = '.' + ext - - return os.path.abspath( - os.path.join( - self.parent_obj.get_actual_dir(app_obj), - self.file_name + ext, - ), - ) - - - def get_default_path(self, app_obj): - - """Can be called by anything. - - Returns the full path to the video file in its default location. - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. To get the actual path to the video file, call - self.get_actual_path(). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - return os.path.abspath( - os.path.join( - self.parent_obj.get_default_dir(app_obj), - self.file_name + self.file_ext, - ), - ) - - - def get_default_path_by_ext(self, app_obj, ext): - - """Can be called by anything. - - Returns the full path to a file associated with the video; specifically - one with the same file name, but a different extension (for example, - the video's thumbnail file). - - If self.master_dbid is not the same as self.dbid, then files are - actually downloaded into the sub-directory used by another channel, - playlist or folder. To get the actual path to the associated file, call - self.get_actual_path_by_ext(). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - ext (str): The extension, e.g. 'png' or '.png' - - """ - - # Add the full stop, if not supplied by the calling function - if not ext.find('.') == 0: - ext = '.' + ext - - return os.path.abspath( - os.path.join( - self.parent_obj.get_default_dir(app_obj), - self.file_name + ext, - ), - ) - - - def get_file_size_string(self): - - """Can be called by anything. - - Converts self.file_size, in bytes, into a formatted string. - - Returns: - - The converted string, or None if self.file_size is not set - - """ - - if self.file_size: - return utils.format_bytes(self.file_size) - else: - return None - - - def get_receive_date_string(self): - - """Can be called by anything. - - A modified version of self.get_receive_time_string(), returning just - the date, not the date and the time. - - Returns: - - The formatted string, or None if self.receive_time is not set - - """ - - if self.receive_time: - timestamp = datetime.datetime.fromtimestamp(self.receive_time) - return timestamp.strftime('%Y-%m-%d') - else: - return None - - - def get_receive_time_string(self): - - """Can be called by anything. - - Converts self.upload_time, in Unix time, into a formatted string. - - Returns: - - The formatted string, or None if self.receive_time is not set - - """ - - if self.receive_time: - return str(datetime.datetime.fromtimestamp(self.receive_time)) - else: - return None - - - def get_upload_date_string(self, pretty_flag=False): - - """Can be called by anything. - - A modified version of self.get_upload_time_string(), returning just the - date, not the date and the time. - - Args: - - pretty_flag (bool): If True, the strings 'Today' and 'Yesterday' - are returned, when possible - - Returns: - - The formatted string, or None if self.upload_time is not set - - """ - - if not self.upload_time: - return None - - elif not pretty_flag: - timestamp = datetime.datetime.fromtimestamp(self.upload_time) - return timestamp.strftime('%Y-%m-%d') - - else: - today = datetime.date.today() - today_str = today.strftime('%y%m%d') - - yesterday = datetime.date.today() - datetime.timedelta(days=1) - yesterday_str = yesterday.strftime('%y%m%d') - - testday = datetime.datetime.fromtimestamp(self.upload_time) - testday_str = testday.strftime('%y%m%d') - - if testday_str == today_str: - return 'Today' - elif testday_str == yesterday_str: - return 'Yesterday' - else: - return testday.strftime('%Y-%m-%d') - - - def get_upload_time_string(self): - - """Can be called by anything. - - Converts self.upload_time, in Unix time, into a formatted string. - - Returns: - - The formatted string, or None if self.upload_time is not set - - """ - - if self.upload_time: - return str(datetime.datetime.fromtimestamp(self.upload_time)) - else: - return None - - -class Channel(GenericRemoteContainer): - - """Python class that handles a channel (e.g. on YouTube). - - Args: - - app_obj (mainapp.TartubeApp): The main application (not stored as an - IV) - - dbid (int): A unique ID for this media data object - - name (str) - The channel name - - parent_obj (media.Folder) - The parent media data object, if any - - options_obj (options.OptionsManager) - The object specifying download - options for this channel, if any - - """ - - - # Standard class methods - - - def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None): - - # IV list - class objects - # ----------------------- - # The parent object (a media.Folder object if this channel is - # downloaded into a particular sub-directory, or None otherwise) - self.parent_obj = parent_obj - # List of media.Video objects for this channel - self.child_list = [] - # The options.OptionsManager object that specifies how this channel is - # downloaded (or None, if the parent's options.OptionsManager object - # should be used instead) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Unique media data object ID (an integer) - self.dbid = dbid - - # Channel name - self.name = name - # Channel nickname (displayed in the Video Index; the same as .name, - # unless the user changes it) - self.nickname = name - # Download source (a URL) - self.source = None - - # Alternative download destination - the dbid of a channel, playlist or - # folder in whose directory videos, thumbnails (etc) are downloaded. - # By default, set to the dbid of this channel; but can be set to the - # dbid of any other channel/playlist/folder - # Used for: (1) adding a channel and its playlists to the Tartube - # database, so that duplicate videos don't exist on the user's - # filesystem, (2) tying together, for example, a YouTube and a - # BitChute account, so that duplicate videos don't exist on the - # user's filesystem - # NB A media data object can't have an alternative download destination - # and itself be the alternative download destination for another - # media data object; it must be one or the other (or neither) - self.master_dbid = dbid - # A list of dbids for any channel, playlist or folder that uses this - # channel as its alternative destination - self.slave_dbid_list = [] - - # Flag set to True if Tartube should always simulate the download of - # videos in this channel, or False if the downloads.DownloadManager - # object should decide whether to simulate, or not - self.dl_sim_flag = False - # Flag set to True if this channel should never be checked or - # downloaded - self.dl_disable_flag = False - # Flag set to True if this channel is marked as favourite, meaning - # that all child video objects are automatically marked as - # favourites - # (Child video objects will also be marked as favourite if one of this - # channel's ancestors are marked as favourite) - self.fav_flag = False - - # 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 - self.bookmark_count = 0 - self.dl_count = 0 - self.fav_count = 0 - self.new_count = 0 - self.waiting_count = 0 - - # List of error/warning messages generated the last time the channel - # was checked or downloaded. Both set to empty lists if the channel - # has never been checked or downloaded, or if there was no error/ - # warning on the last check/download attempt - # NB If an error/warning message is generated when downloading an - # individual video (not in a channel or playlist), the message is - # stored in the media.Video object - self.error_list = [] - self.warning_list = [] - - - # Code - # ---- - - # Update the parent (if any) - if self.parent_obj: - self.parent_obj.add_child(self) - - - # Public class methods - - -# def add_child(): # Inherited from GenericRemoteContainer - - -# def del_child(): # Inherited from GenericContainer - - -# def do_sort(): # Inherited from GenericRemoteContainer - - -# def sort_children(): # Inherited from GenericRemoteContainer - - - # Set accessors - - -# def reset_counts(): # Inherited from GenericContainer - - -# def set_dl_sim_flag(): # Inherited from GenericMedia - - -# def set_options_obj(): # Inherited from GenericMedia - - -# def set_source(): # Inherited from GenericRemoteContainer - - - # Get accessors - - -# def get_actual_dir(): # Inherited from GenericContainer - - -# def get_default_dir(): # Inherited from GenericContainer - - -# def get_relative_actual_dir(): # Inherited from GenericContainer - - -# def get_relative_default_dir(): # Inherited from GenericContainer - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass - - -class Playlist(GenericRemoteContainer): - - """Python class that handles a playlist (e.g. on YouTube). - - Args: - - app_obj (mainapp.TartubeApp): The main application (not stored as an - IV) - - dbid (int): A unique ID for this media data object - - name (str) - The playlist name - - parent_obj (media.Folder) - The parent media data object, if any - - options_obj (options.OptionsManager) - The object specifying download - options for this channel, if any - - """ - - - # Standard class methods - - - def __init__(self, app_obj, dbid, name, parent_obj=None, options_obj=None): - - # IV list - class objects - # ----------------------- - # The parent object (a media.Folder object if this playlist is - # downloaded into a particular sub-directory, or None otherwise) - self.parent_obj = parent_obj - # List of media.Video objects for this playlist - self.child_list = [] - # The options.OptionsManager object that specifies how this playlist - # is downloaded (or None, if the parent's options.OptionsManager - # object should be used instead) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Unique media data object ID (an integer) - self.dbid = dbid - - # Playlist name - self.name = name - # Playlist nickname (displayed in the Video Index; the same as .name, - # unless the user changes it) - self.nickname = name - # Download source (a URL) - self.source = None - - # Alternative download destination - the dbid of a channel, playlist or - # folder in whose directory videos, thumbnails (etc) are downloaded. - # By default, set to the dbid of this playlist; but can be set to the - # dbid of any other channel/playlist/folder - # Used for: (1) adding a channel and its playlists to the Tartube - # database, so that duplicate videos don't exist on the user's - # filesystem, (2) tying together, for example, a YouTube and a - # BitChute account, so that duplicate videos don't exist on the - # user's filesystem - # NB A media data object can't have an alternative download destination - # and itself be the alternative download destination for another - # media data object; it must be one or the other (or neither) - self.master_dbid = dbid - # A list of dbids for any channel, playlist or folder that uses this - # playlist as its alternative destination - self.slave_dbid_list = [] - - # Flag set to True if Tartube should always simulate the download of - # videos in this playlist, or False if the downloads.DownloadManager - # object should decide whether to simulate, or not - self.dl_sim_flag = False - # Flag set to True if this playlist should never be checked or - # downloaded - self.dl_disable_flag = False - # Flag set to True if this playlist is marked as favourite, meaning - # that all child video objects are automatically marked as - # favourites - # (Child video objects will also be marked as favourite if one of this - # playlist's ancestors are marked as favourite) - self.fav_flag = False - - # 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 - self.bookmark_count = 0 - self.dl_count = 0 - self.fav_count = 0 - self.new_count = 0 - self.waiting_count = 0 - - # List of error/warning messages generated the last time the channel - # was checked or downloaded. Both set to empty lists if the channel - # has never been checked or downloaded, or if there was no error/ - # warning on the last check/download attempt - # NB If an error/warning message is generated when downloading an - # individual video (not in a channel or playlist), the message is - # stored in the media.Video object - self.error_list = [] - self.warning_list = [] - - - # Code - # ---- - - # Update the parent (if any) - if self.parent_obj: - self.parent_obj.add_child(self) - - - # Public class methods - - -# def add_child(): # Inherited from GenericRemoteContainer - - -# def del_child(): # Inherited from GenericContainer - - -# def do_sort(): # Inherited from GenericRemoteContainer - - -# def sort_children(): # Inherited from GenericRemoteContainer - - - # Set accessors - - -# def reset_counts(): # Inherited from GenericContainer - - -# def set_dl_sim_flag(): # Inherited from GenericMedia - - -# def set_options_obj(): # Inherited from GenericMedia - - -# def set_source(): # Inherited from GenericRemoteContainer - - - # Get accessors - - -# def get_actual_dir(): # Inherited from GenericContainer - - -# def get_default_dir(): # Inherited from GenericContainer - - -# def get_relative_actual_dir(): # Inherited from GenericContainer - - -# def get_relative_default_dir(): # Inherited from GenericContainer - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass - - -class Folder(GenericContainer): - - """Python class that handles a sub-directory inside Tartube's data folder, - into which other media data objects (media.Video, media.Channel, - media.Playlist and other media.Folder objects) can be downloaded. - - Args: - - app_obj (mainapp.TartubeApp): The main application (not stored as an - IV) - - dbid (int): A unique ID for this media data object - - name (str) - The folder name - - parent_obj (media.Folder) - The parent media data object, if any - - options_obj (options.OptionsManager) - The object specifying download - options for this channel, if any - - fixed_flag (bool) - If True, this folder can't be deleted by the user - - priv_flag (bool) - If True, the user can't add anything to this folder, - because Tartube uses it for special purposes - - restrict_flag (bool) - If True, this folder cannot contain channels, - playlists and other folders (can only contain videos) - - temp_flag (bool) - If True, the folder's contents should be deleted - when Tartube shuts down (but the folder itself remains) - - """ - - - # Standard class methods - - - def __init__(self, app_obj, dbid, name, parent_obj=None, \ - options_obj=None, fixed_flag=False, priv_flag=False, restrict_flag=False, \ - temp_flag=False): - - # IV list - class objects - # ----------------------- - # The parent object (another media.Folder object, or None if no parent) - self.parent_obj = parent_obj - # List of media.Video, media.Channel, media.Playlist and media.Folder - # objects for which this object is the parent - self.child_list = [] - # The options.OptionsManager object that specifies how this channel is - # downloaded (or None, if the parent's options.OptionsManager object - # should be used instead) - self.options_obj = options_obj - - - # IV list - other - # --------------- - # Unique media data object ID (an integer) - self.dbid = dbid - - # Folder name - self.name = name - # Folder nickname (displayed in the Video Index; the same as .name, - # unless the user changes it). Note that the nickname of a fixed - # folder can't be changed - self.nickname = name - - # Alternative download destination - the dbid of a channel, playlist or - # folder in whose directory videos, thumbnails (etc) are downloaded. - # By default, set to the dbid of this folder; but can be set to the - # dbid of any other channel/playlist/folder - # Used for: (1) adding a channel and its playlists to the Tartube - # database, so that duplicate videos don't exist on the user's - # filesystem, (2) tying together, for example, a YouTube and a - # BitChute account, so that duplicate videos don't exist on the - # user's filesystem - # NB A media data object can't have an alternative download destination - # and itself be the alternative download destination for another - # media data object; it must be one or the other (or neither) - # NB Fixed folders cannot have an alternative download destination - self.master_dbid = dbid - # A list of dbids for any channel, playlist or folder that uses this - # folder as its alternative destination - self.slave_dbid_list = [] - - # Flag set to False if the folder can be deleted by the user, or True - # if it can't be deleted by the user - self.fixed_flag = fixed_flag - # Flag set to True to mark this as a private folder, meaning that the - # user can't add anything to it (because Tartube uses it for special - # purposes) - self.priv_flag = priv_flag - # Flag set to False if other channels, playlists and folders can be - # added as children of this folder, or True if only videos can be - # added as children of this folder - self.restrict_flag = restrict_flag - # Flag set to True for any folder whose contents should be deleted when - # Tartube shuts down (but the folder itself remains) - self.temp_flag = temp_flag - - # Flag set to True if Tartube should always simulate the download of - # videos in this folder, or False if the downloads.DownloadManager - # object should decide whether to simulate, or not - self.dl_sim_flag = False - # Flag set to True if this folder should never be checked or - # downloaded. If True, the setting applies to any descendant - # channels, playlists and folders - self.dl_disable_flag = False - # Flag set to True if this folder is hidden (not visible in the Video - # Index). Note that only folders can be hidden; channels and - # playlists cannot - self.hidden_flag = False - # Flag set to True if this folder is marked as favourite, meaning that - # any descendant video objects are automatically marked as favourites - # (but not descendant channels, playlists or folders) - # (Descendant video objects will also be marked as favourite if one of - # this folder's ancestors are marked as favourite) - self.fav_flag = False - - # 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 - self.bookmark_count = 0 - self.dl_count = 0 - self.fav_count = 0 - self.new_count = 0 - self.waiting_count = 0 - - - # Code - # ---- - - # Update the parent (if any) - if self.parent_obj: - self.parent_obj.add_child(self) - - - # Public class methods - - - def add_child(self, child_obj, no_sort_flag=False): - - """Can be called by anything. - - Adds a child media data object, which can be any type of media data - object (including another media.Folder object). - - Args: - - child_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The child object - - no_sort_flag (bool): If True, the child list is not sorted after - the new object has been added - - """ - - # Check this is not already a child object - if not child_obj in self.child_list: - - self.child_list.append(child_obj) - if not no_sort_flag: - self.sort_children() - - if isinstance(child_obj, Video): - self.vid_count += 1 - - - def check_duplicate_video(self, source): - - """Called by mainapp.TartubeApp.on_menu_add_video() and - mainwin.MainWin.on_window_drag_data_received(). - - When the user adds new videos using the 'Add Videos' dialogue window, - the calling function calls this function to check that the folder - doesn't contain a duplicate video (i.e., one whose source URL is the - same). - - Args: - - source (str): The video URL to check - - Returns: - - True if any of the child media.Video objects in this folder have - the same source URL; False otherwise - - """ - - for child_obj in self.child_list: - - if isinstance(child_obj, Video) \ - and child_obj.source is not None \ - and child_obj.source == source: - # Duplicate found - return True - - # No duplicate found - return False - - -# def del_child(): # Inherited from GenericContainer - - - def do_sort(self, obj1, obj2): - - """Sorting function used by functools.cmp_to_key(), and called by - self.sort_children(). - - Sorts the child media.Video, media.Channel, media.Playlist and - media.Folder objects. - - Firstly, sort by class - folders, channels/playlists, then videos. - - Within folders, channels and playlists, sort alphabetically. Within - videos, sort by upload time. - - Args: - - obj1, obj2 (media.Video, media.Channel, media.Playlist or - media.Folder) - Media data objects being sorted - - Returns: - - -1 if obj1 comes first, 1 if obj2 comes first, 0 if they are equal - - """ - - if str(obj1.__class__) == str(obj2.__class__) \ - or ( - isinstance(obj1, GenericRemoteContainer) \ - and isinstance(obj2, GenericRemoteContainer) - ): - if isinstance(obj1, Video): - - if obj1.upload_time is not None \ - and obj2.upload_time is not None: - if obj1.upload_time > obj2.upload_time: - return -1 - elif obj1.upload_time < obj2.upload_time: - return 1 - elif obj1.receive_time is not None \ - and obj2.receive_time is not None: - # In private folders (e.g. 'All Videos'), the most - # recently received video goes to the top of the list - if self.priv_flag: - if obj1.receive_time > obj2.receive_time: - return -1 - elif obj1.receive_time < obj2.receive_time: - return 1 - else: - return 0 - # ...but for everything else, the sorting algorithm is - # the same as for GenericRemoteContainer.do_sort(), - # in which we assume the website is sending us - # videos, newest first - else: - if obj1.receive_time < obj2.receive_time: - return -1 - elif obj1.receive_time > obj2.receive_time: - return 1 - else: - return 0 - else: - return 0 - else: - return 0 - else: - if obj1.name.lower() < obj2.name.lower(): - return -1 - elif obj1.name.lower() > obj2.name.lower(): - return 1 - else: - return 0 - else: - if isinstance(obj1, Folder): - return -1 - elif isinstance(obj2, Folder): - return 1 - elif isinstance(obj1, Channel) or isinstance(obj1, Playlist): - return -1 - elif isinstance(obj2, Channel) or isinstance(obj2, Playlist): - return 1 - else: - return 0 - - - def sort_children(self): - - """Can be called by anything. For example, called by self.add_child(). - - Sorts the child media.Video, media.Channel, media.Playlist and - media.Folder objects. - """ - - # v1.0.002: At the end of a download operation, I am seeing 'list - # modified during sort' errors. Not sure what the cause is, but we - # can prevent it by sorting a copy of the list, rather than the list - # itself. If the list itself is modified during the sort, sort it - # again - while True: - - copy_list = self.child_list.copy() - copy_list.sort(key=functools.cmp_to_key(self.do_sort)) - - if len(copy_list) == len(self.child_list): - self.child_list = copy_list.copy() - break - - - # Set accessors - - -# def reset_counts(): # Inherited from GenericContainer - - -# def set_dl_sim_flag(): # Inherited from GenericMedia - - - def set_hidden_flag(self, flag): - - if flag: - self.hidden_flag = True - else: - self.hidden_flag = False - - -# def set_options_obj(): # Inherited from GenericMedia - - - # Get accessors - - -# def get_actual_dir(): # Inherited from GenericContainer - - -# def get_default_dir(): # Inherited from GenericContainer - - -# def get_relative_actual_dir(): # Inherited from GenericContainer - - -# def get_relative_default_dir(): # Inherited from GenericContainer - - - def never_called_func(self): - - """Function that is never called, but which makes this class object - collapse neatly in my IDE.""" - - pass diff --git a/tartube/options.py b/tartube/options.py deleted file mode 100755 index e86f4b9..0000000 --- a/tartube/options.py +++ /dev/null @@ -1,1270 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Module that contains a class storing download options.""" - - -# Import Gtk modules -# ... - - -# Import other modules -import os - - -# Import our modules -import formats -import mainapp -import media -import utils - - -# Classes - - -class OptionsManager(object): - - """Called by mainapp.TartubeApp.OptionsManager(). - - Partially based on the OptionsManager class in youtube-dl-gui. - - This class handles settings for downloading media. Unlike youtube-dl-gui, - which has one group of download options applied to all downloads, this - object can be applied to any of the media data classes in media.py (so it - can be applied to a single video, or a whole channel, or generally to all - downloads). - - Tartube's options.OptionsManager implements a subset of options implemented - by the equivalent class in youtube-dl-gui. - - Options are listed here in the same order in which they appear in - youtube-dl's documentation. - - OPTIONS - - ignore_errors (bool): If True, youtube-dl will ignore the errors and - continue the download operation - - abort_on_error (bool): If True, youtube-dl will abord downloading - further playlist videos if an error occurs - - NETWORK OPTIONS - - proxy (str): Use the specified HTTP/HTTPS proxy - - socket_timeout (str): Time to wait before giving up, in seconds - - source_address (str): Client-side IP address to bind to - - force_ipv4 (str): Make all connections via IPv4 - - force_ipv6 (str): Make all connections via IPv6 - - GEO-RESTRICTION - - geo_verification_proxy (str): Use this proxy to verify the IP address - for some geo-restricted sites - - geo_bypass (bool): Bypass geographic restriction via faking - X-Forwarded-For HTTP header - - no_geo_bypass (bool): Do not bypass geographic restriction via faking - X-Forwarded-For HTTP header - - geo_bypass_country (str): Force bypass geographic restriction with - explicitly provided two-letter ISO 3166-2 country code - - geo_bypass_ip_block (str): Force bypass geographic restriction with - explicitly provided IP block in CIDR notation - - VIDEO SELECTION - - playlist_start (int): Playlist index to start downloading - - playlist_end (int): Playlist index to stop downloading - - max_downloads (int): Maximum number of video files to download from the - given playlist - - min_filesize (float): Minimum file size of the video file. If the video - file is smaller than the given size then youtube-dl will abort the - download operation - - max_filesize (float): Maximum file size of the video file. If the video - file is larger than the given size then youtube-dl will abort the - download operation - - date (str): Download only videos uploaded on this date (YYYYMMDD) - - date_before (str): Download only videos uploaded on or before this date - (YYYYMMDD) - - date_after (str): Download only videos uploaded on or after this date - (YYYYMMDD) - - min_views (int): Do not download any videos with fewer than this many - views - - max_views (int): Do not download any videos with more than this many - views - - match_filter (str): Generic video filter (see the comments in the - youtube-dl documentation). Tartube automatically adds quotes to - the beginning and end of the string - - age_limit (str): Download only videos suitable for the given age - - include_ads (bool): Download advertisements (experimental) - - DOWNLOAD OPTIONS - - limit_rate (str): Bandwidth limit, in bytes (example strings: 50K or - 4.2M) (not implemented by youtube-dl-gui) - NB: Can't be directly modified by user - - retries (int): Number of youtube-dl retries - - playlist_reverse (bool): When True, download playlist videos in reverse - order - - playlist_random (bool): When True, download playlist videos in random - order - - native_hls (bool): When True, youtube-dl will prefer the native HLS - (HTTP Live Streaming) implementation (rather than prefering FFmpeg, - which is at the current time the better downloader for general - compatibility) - - hls_prefer_ffmpeg (bool): When True, youtube-dl will prefer FFmpeg - (N.B. This should not be confused with the 'prefer_ffmpeg' option) - - external_downloader (str): Use the specified external downloaded. - youtube-dl currently supports the strings 'aria2c', 'avconv', - 'axel', 'curl', 'ffmpeg', 'httpie', 'wget' (use an empty string to - disable this option) - - external_arg_string (str): Arguments to pass to the external - downloader. Tartube automatically adds quotes to the beginning and - end of the string - - FILESYSTEM OPTIONS - - save_path (str): Path where youtube-dl should store the downloaded - file. The default is supplied by the media data object - NB: Can't be directly modified by user - - restrict_filenames (bool): If True, youtube-dl will restrict the - downloaded file's filename to ASCII characters only - - nomtime (bool): When True will not use the last-modified header to set - the file modification time (i.e., use the time at which the server - believes the resources was last modified) - - write_description (bool): If True, youtube-dl will write video - description to a .description file - - write_info (bool): If True, youtube-dl will write video metadata to an - .info.json file - - write_annotations (bool): If True, youtube-dl will write video - annotations to an .annotations.xml file - - THUMBNAIL IMAGES - - write_thumbnail (bool): If True youtube-dl will write thumbnail image - to disc - - VERBOSITY / SIMULATION OPTIONS - - youtube_dl_debug (bool): When True, will pass '-v' flag to youtube-dl - - WORKAROUNDS - - force_encoding (str): Force the specified encoding - - no_check_certificate (bool): If True, suppress HTTPS certificate - validation - - prefer_insecure (bool): If True, use an unencrypted connection to - retrieve information about the video. (Currently supported only for - YouTube) - - user_agent (str): Specify a custom user agent for youtube-dl - - referer (str): Specify a custom referer to use if the video access is - restricted to one domain - - VIDEO FORMAT OPTIONS - - 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'. - - 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 - - prefer_free_formats (bool): If True, prefer free video formats unless - one is specfied by video_format, etc - - yt_skip_dash (bool): If True, do not download DASh-related data with - YouTube videos - - merge_output_format (str): If a merge is required (e.g. - bestvideo+bestaudio), output to this container format. youtube-dl - supports the strings 'mkv', 'mp4', 'ogg', 'webm', 'flv' (or an - empty string to ignore this option) - - SUBTITLE OPTIONS - - write_subs (bool): If True, youtube-dl will try to download the - subtitles file for the given URL - - write_auto_subs (bool): If True, youtube-dl will try to download the - automatic subtitles file for the given URL - - write_all_subs (bool): If True, youtube-dl will try to download all the - the available subtitles files for the given URL - - subs_format (str): Subtitle format preference. youtube-dl supports - 'srt', 'ass', 'vtt', 'lrc' or combinations thereof, e.g. - 'ass/srt/best' - - subs_lang (str): Language of the subtitles file to download. Requires - the 'write_subs' option. Can not be set directly by the user; - instead, OptionsParser.parse() converts the option 'subs_lang_list' - to a string, and sets this option to that string - - AUTHENTIFICATION OPTIONS - - username (str): Username to login with - - password (str): Password to login with - - two_factor (str): Two-factor authentification code - - net_rc (bool): If True, use .netrc authentification data - - video_password (str): Video password for the given URL - - ADOBE PASS OPTIONS - - (none implemented) - - POST-PROCESSING OPTIONS - - extract_audio (bool): If True, youtube-dl will post-process the video - file - - audio_format (str): Audio format of the post-processed file. Available - values are 'mp3', 'wav', 'aac', 'm4a', 'vorbis', 'opus' & 'flac' - - audio_quality (str): Audio quality of the post-processed file. - Available values are '9', '5', '0'. The lowest the value the better - the quality - - recode_video (str): Encode the video to another format if necessary. - One of the strings 'avi', 'flv', 'mkv', 'mp4', 'ogg', 'webm', or an - empty string if disabled - - pp_args (str): Give these arguments to the postprocessor. Tartube - automatically adds quotes to the beginning and end of the string - - keep_video (bool): If True, youtube-dl will keep the video file after - post-processing it - - embed_subs (bool): If True, youtube-dl will merge the subtitles file - with the video (only for .mp4 files) - - embed_thumbnail (bool): When True will embed the thumbnail in the audio - file as cover art - - add_metadata (bool): When True will write metadata to the video file - - fixup_policy (str): Automatically correct known faults of the file. - The string can be 'never', 'warn', 'detect_or_worn' or an empty - string if disabled - - prefer_avconv (bool): Prefer AVConv over FFmpeg for running the - postprocessors - - prefer_ffmpeg (bool): Prefer FFmpeg over AVConv for running the - postprocessors - - YOUTUBE-DL-GUI OPTIONS (not passed to youtube-dl directly) - - [used to build the 'save_path' option] - - output_format (int): Option in the range 0-9, which is converted into - a youtube-dl output template using - formats.FILE_OUTPUT_CONVERT_DICT. If the value is 0, then the - custom 'output_template' is used instead - - 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: - '' (for bytes), 'k' (for kilobytes, etc), 'm', 'g', 't', 'p', - 'e', 'z', 'y' - - min_filesize_unit (str): Minimum file size unit. Available values - as above - - [in youtube-dl-gui, this was named 'cmd_args'] - - extra_cmd_string: String that contains extra youtube-dl options - separated by spaces. Components containing whitespace can be - enclosed within double quotes "..." - - TARTUBE OPTIONS (not passed to youtube-dl directly) - - keep_description (bool): - keep_info (bool): - keep_annotations (bool): - keep_thumbnail (bool): - During a download operation (not simulated, e.g. when the user - clicks the 'Download all' button), the video description/JSON/ - annotations/thumbnail files are downloaded only if - 'write_description', 'write_info', 'write_annotations' and/or - 'write_thumbnail' are True - - They are initially stored in the same sub-directory in which - Tartube will store the video - - If these options are True, they stay there; otherwise, they are - copied into the equivalent location in Tartube's temporary - directories. - - sim_keep_description (bool): - sim_keep_info (bool): - sim_keep_annotations (bool): - sim_keep_thumbnail (bool): - During a download operation (simulated, e.g. when the user clicks - the 'Check all' button), the video's JSON file is always loaded - into memory - - If 'write_description' and 'sim_description' are both true, the - description file is written directly to the sub-directory in which - Tartube would store the video - - If 'write_description' is true but 'keep_description' not, the - description file is written to the equivalent location in Tartube's - temporary directories. - - The same applies to the JSON, annotations and thumbnail files. - - use_fixed_folder (str or None): If not None, then all videos are - downloaded to one of Tartube's fixed folders (not including private - folders) - currently, that group consists of only 'Temporary - Videos' and 'Unsorted Videos'. The value should match the name of - the folder - - match_title_list (list): Download only matching titles (regex or - caseless sub-string). Each item in the list is passed to youtube-dl - as a separate --match-title argument - - reject_title_list (list): Skip download for any matching titles (regex - or caseless sub-string). Each item in the list is passed to - youtube-dl as a separate --reject-title argument - - subs_lang_list (list): List of language tags which are used to set - the 'subs_lang' option - - """ - - - # Standard class methods - - - def __init__(self): - - # IV list - other - # --------------- - # Dictionary of download options for youtube-dl, set by a call to - # self.reset_options - self.options_dict = {} - - - # Code - # ---- - - # Initialise youtube-dl options - self.reset_options() - - - # Public class methods - - - def clone_options(self, other_options_manager_obj): - - """Called by mainapp.TartubeApp.apply_download_options() and - .clone_general_options_manager(). - - Clones download options from the specified object into those object, - completely replacing this object's download options. - - Args: - - other_options_manager_obj (options.OptionsManager): The download - options object (usually the General Options Manager), from - which options will be cloned - - """ - - self.options_dict = other_options_manager_obj.options_dict.copy() - - - 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. - - youtube-dl won't download the specified formats properly, if audio - formats appear before video formats. Therefore, this function is called - to rearrange the list, putting all video formats above all audio - formats. - """ - - format_list = [ - self.options_dict['video_format'], - self.options_dict['second_video_format'], - self.options_dict['third_video_format'], - ] - video_list = [] - audio_list = [] - comb_list = [] - - for code in format_list: - - if code != '0': - - if formats.VIDEO_OPTION_TYPE_DICT[code] is False: - video_list.append(code) - else: - audio_list.append(code) - - 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' - - - def reset_options(self): - - """Called by self.__init__(). - - Resets (or initialises) self.options_dict to its default state. - """ - - self.options_dict = { - # OPTIONS - 'ignore_errors': True, - 'abort_on_error': False, - # NETWORK OPTIONS - 'proxy': '', - 'socket_timeout': '', - 'source_address': '', - 'force_ipv4': False, - 'force_ipv6': False, - # GEO-RESTRICTION - 'geo_verification_proxy': '', - 'geo_bypass': False, - 'no_geo_bypass': False, - 'geo_bypass_country': '', - 'geo_bypass_ip_block': '', - # VIDEO SELECTION - 'playlist_start': 1, - 'playlist_end': 0, - 'max_downloads': 0, - 'min_filesize': 0, - 'max_filesize': 0, - 'date': '', - 'date_before': '', - 'date_after': '', - 'min_views': 0, - 'max_views': 0, - 'match_filter': '', - 'age_limit': '', - 'include_ads': False, - # DOWNLOAD OPTIONS - 'limit_rate': '', # Can't be directly modified by user - 'retries': 10, - 'playlist_reverse': False, - 'playlist_random': False, - 'native_hls': True, - 'hls_prefer_ffmpeg': False, - 'external_downloader': '', - 'external_arg_string': '', - # FILESYSTEM OPTIONS - 'save_path': None, # Can't be directly modified by user - 'restrict_filenames': False, - 'nomtime': False, - 'write_description': True, - 'write_info': True, - 'write_annotations': True, - # THUMBNAIL IMAGES - 'write_thumbnail': True, - # VERBOSITY / SIMULATION OPTIONS - # (none implemented) - # WORKAROUNDS - 'force_encoding': '', - 'no_check_certificate': False, - 'prefer_insecure': False, - 'user_agent': '', - 'referer': '', - # VIDEO FORMAT OPTIONS - 'video_format': '0', - 'all_formats': False, - 'prefer_free_formats': False, - 'yt_skip_dash': False, - 'merge_output_format': '', - # SUBTITLE OPTIONS - 'write_subs': False, - 'write_auto_subs': False, - 'write_all_subs': False, - 'subs_format': '', - 'subs_lang': '', - # AUTHENTIFICATION OPTIONS - 'username': '', - 'password': '', - 'two_factor': '', - 'net_rc': False, - 'video_password': '', - # ADOBE PASS OPTIONS - # (none implemented) - # POST-PROCESSING OPTIONS - 'extract_audio': False, - 'audio_format': '', - 'audio_quality': '5', - 'recode_video': '', - 'pp_args': '', - 'keep_video': False, - 'embed_subs': False, - 'embed_thumbnail': False, - 'add_metadata': False, - 'fixup_policy': '', - 'prefer_avconv': False, - 'prefer_ffmpeg': False, - # 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' : '', - # TARTUBE OPTIONS - 'keep_description': False, - 'keep_info': False, - 'keep_annotations': False, - 'keep_thumbnail': True, - 'sim_keep_description': False, - 'sim_keep_info': False, - 'sim_keep_annotations': False, - 'sim_keep_thumbnail': True, - 'use_fixed_folder': None, - 'match_title_list': [], - 'reject_title_list': [], - 'subs_lang_list': [ 'en' ], - } - - -class OptionsParser(object): - - """Called by downloads.DownloadManager.__init__() and by - mainwin.SystemCmdDialogue.update_textbuffer(). - - This object converts the download options specified by an - options.OptionsManager object into a list of youtube-dl command line - options, whenever required. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - - # Standard class methods - - - def __init__(self, app_obj): - - # IV list - class objects - # ----------------------- - # The main application - self.app_obj = app_obj - - - # IV list - other - # --------------- - # List of options.OptionHolder objects, with their initial settings - # The options here are in the same order in which they appear in - # youtube-dl's options list - self.option_holder_list = [ - # OPTIONS - # -i, --ignore-errors - OptionHolder('ignore_errors', '-i', False), - # --abort-on-error - OptionHolder('abort_on_error', '--abort-on-error ', False), - # NETWORK OPTIONS - # --proxy URL - OptionHolder('proxy', '--proxy', ''), - OptionHolder('socket_timeout', '--socket-timeout', ''), - OptionHolder('source_address', '--source-address', ''), - OptionHolder('force_ipv4', '--force-ipv4', False), - OptionHolder('force_ipv6', '--force-ipv6', False), - # GEO-RESTRICTION - OptionHolder( - 'geo_verification_proxy', - '--geo-verification-proxy', - '', - ), - OptionHolder('geo_bypass', '--geo-bypass', False), - OptionHolder('no_geo_bypass', '--no-geo-bypass', False), - OptionHolder('geo_bypass_country', '--geo-bypass-country', ''), - OptionHolder('geo_bypass_ip_block', '--geo-bypass-ip-block', ''), - # VIDEO SELECTION - # --playlist-start NUMBER - OptionHolder('playlist_start', '--playlist-start', 1), - # --playlist-end NUMBER - OptionHolder('playlist_end', '--playlist-end', 0), - # --max-downloads NUMBER - OptionHolder('max_downloads', '--max-downloads', 0), - # --min-filesize SIZE - OptionHolder('min_filesize', '--min-filesize', 0), - # --max-filesize SIZE - OptionHolder('max_filesize', '--max-filesize', 0), - # --date DATE - OptionHolder('date', '--date', ''), - # --datebefore DATE - OptionHolder('date_before', '--datebefore', ''), - # --dateafter DATE - OptionHolder('date_after', '--dateafter', ''), - # --min-views COUNT - OptionHolder('min_views', '--min-views', 0), - # --max-views COUNT - OptionHolder('max_views', '--max-views', 0), - # --match-filter FILTER - OptionHolder('match_filter', '--match-filter', ''), - # --age-limit YEARS - OptionHolder('age_limit', '--age-limit', ''), - # --include-ads FILTER - OptionHolder('include_ads', '--include-ads', False), - # DOWNLOAD OPTIONS - # -r, --limit-rate RATE - OptionHolder('limit_rate', '-r', ''), - # -R, --retries RETRIES - OptionHolder('retries', '-R', 10), - # --playlist-reverse - OptionHolder('playlist_reverse', '--playlist-reverse', False), - # --playlist-random - OptionHolder('playlist_random', '--playlist-random', False), - # --hls-prefer-native - OptionHolder('native_hls', '--hls-prefer-native', False), - # --hls-prefer-ffmpeg - OptionHolder('hls_prefer_ffmpeg', '--hls-prefer-ffmpeg', False), - # --external-downloader COMMAND - OptionHolder('external_downloader', '--external-downloader', ''), - # --external-downloader-args ARGS - OptionHolder( - 'external_arg_string', - '--external-downloader-args', - '', - ), - # FILESYSTEM OPTIONS - # -o, --output TEMPLATE - OptionHolder('save_path', '-o', ''), - # --restrict-filenames - OptionHolder('restrict_filenames', '--restrict-filenames', False), - # --no-mtime - OptionHolder('nomtime', '--no-mtime', False), - # --write-description - OptionHolder('write_description', '--write-description', False), - # --write-info-json - OptionHolder('write_info', '--write-info-json', False), - # --write-annotations - OptionHolder('write_annotations', '--write-annotations', False), - # THUMBNAIL IMAGES - # --write-thumbnail - OptionHolder('write_thumbnail', '--write-thumbnail', False), - # VERBOSITY / SIMULATION OPTIONS - # (none implemented) - # WORKAROUNDS - # --encoding ENCODING - OptionHolder('force_encoding', '--encoding', ''), - # --no-check-certificate - OptionHolder( - 'no_check_certificate', - '--no-check-certificate', - False, - ), - # --prefer-insecure - OptionHolder('prefer_insecure', '--prefer-insecure ', False), - # --user-agent UA - OptionHolder('user_agent', '--user-agent', ''), - # --referer URL - OptionHolder('referer', '--referer', ''), - # VIDEO FORMAT OPTIONS - # -f, --format FORMAT - OptionHolder('video_format', '-f', '0'), - # --all-formats - OptionHolder('all_formats', '--all-formats', False), - # --prefer-free-formats - OptionHolder( - 'prefer_free_formats', - '--prefer-free-formats', - False, - ), - # --youtube-skip-dash-manifest - OptionHolder( - 'yt_skip_dash', - '--youtube-skip-dash-manifest', - False, - ), - # --merge-output-format FORMAT - OptionHolder('merge_output_format', '--merge-output-format', ''), - # SUBTITLE OPTIONS - # --write-sub - OptionHolder('write_subs', '--write-sub', False), - # --write-auto-sub - OptionHolder('write_auto_subs', '--write-auto-sub', False), - # --all-subs - OptionHolder('write_all_subs', '--all-subs', False), - # --sub-format FORMAT - OptionHolder('subs_format', '--sub-format', ''), - # --sub-lang LANGS. NB This '--sub-lang' string is not the one - # used as a switch by self.parse() - OptionHolder('subs_lang', '--sub-lang', '', ['write_subs']), - # AUTHENTIFICATION OPTIONS - # -u, --username USERNAME - OptionHolder('username', '-u', ''), - # -p, --password PASSWORD - OptionHolder('password', '-p', ''), - # -2, --twofactor TWOFACTOR - OptionHolder('two_factor', '--twofactor', ''), - # -n, --netrc - OptionHolder('net_rc', '--netrc', False), - # --video-password PASSWORD - OptionHolder('video_password', '--video-password', ''), - # ADOBE PASS OPTIONS - # (none implemented) - # POST-PROCESSING OPTIONS - # -x, --extract-audio - OptionHolder('extract_audio', '-x', False), - # --audio-format FORMAT - OptionHolder('audio_format', '--audio-format', ''), - # --audio-quality QUALITY - OptionHolder( - 'audio_quality', - '--audio-quality', - '5', - ['extract_audio'], - ), - # --recode-video FORMAT - OptionHolder('recode_video', '--recode-video', ''), - # --postprocessor-args ARGS - OptionHolder('pp_args', '--postprocessor-args', ''), - # -k, --keep-video - OptionHolder('keep_video', '-k', False), - # --embed-subs - OptionHolder( - 'embed_subs', - '--embed-subs', - False, - ['write_auto_subs', 'write_subs'], - ), - # --embed-thumbnail - OptionHolder('embed_thumbnail', '--embed-thumbnail', False), - # --add-metadata - OptionHolder('add_metadata', '--add-metadata', False), - # --fixup POLICY - OptionHolder('fixup_policy', '--fixup', ''), - # --prefer-avconv - OptionHolder('prefer_avconv', '--prefer-avconv', False), - # --prefer-ffmpeg - OptionHolder('prefer_ffmpeg', '--prefer-ffmpeg', False), - # 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', '', ''), - # TARTUBE OPTIONS (not given an options.OptionHolder object) -# OptionHolder('keep_description', '', False), -# OptionHolder('keep_info', '', False), -# OptionHolder('keep_annotations', '', False), -# OptionHolder('keep_thumbnail', '', False), -# OptionHolder('sim_keep_description', '', False), -# OptionHolder('sim_keep_info', '', False), -# OptionHolder('sim_keep_annotations', '', False), -# OptionHolder('sim_keep_thumbnail', '', False), -# OptionHolder('use_fixed_folder', '', None), -# OptionHolder('match_title_list', '', []), -# OptionHolder('reject_title_list', '', []), -# OptionHolder('subs_lang_list', '', []), - ] - - - # Public class methods - - - def parse(self, media_data_obj, options_manager_obj): - - """Called by downloads.DownloadWorker.prepare_download() and - mainwin.MainWin.update_textbuffer(). - - Converts the download options stored in the specified - options.OptionsManager object into a list of youtube-dl command line - options. - - Args: - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object being downloaded - - options_manager_obj (options.OptionsManager): The object containing - the download options for this media data object - - Returns: - - List of strings with all the youtube-dl command line options - - """ - - # Force youtube-dl's progress bar to be outputted as separate lines - options_list = ['--newline'] - - # 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) - # Set the 'min_filesize' and 'max_filesize' options - self.build_file_sizes(copy_dict) - # Set the 'limit_rate' option - self.build_limit_rate(copy_dict) - - # Parse basic youtube-dl command line options - for option_holder_obj in self.option_holder_list: - - # First deal with special cases... - if option_holder_obj.name == 'extract_audio': - if copy_dict['audio_format'] == '': - value = copy_dict[option_holder_obj.name] - - if value != option_holder_obj.default_value: - options_list.append(option_holder_obj.switch) - - elif option_holder_obj.name == 'audio_format': - value = copy_dict[option_holder_obj.name] - - if value != option_holder_obj.default_value: - options_list.append('-x') - options_list.append(option_holder_obj.switch) - options_list.append(utils.to_string(value)) - - # The '-x' / '--audio-quality' switch must precede the - # '--audio-quality' switch, if both are used - # Therefore, if the current value of the 'audio_quality' - # option is not the default value ('5'), then insert the - # '--audio-quality' switch into the options list right - # now - if copy_dict['audio_quality'] != '5': - options_list.append('--audio-quality') - options_list.append( - utils.to_string(copy_dict['audio_quality']), - ) - - elif option_holder_obj.name == 'audio_quality': - # If the '--audio-quality' switch was not added by the code - # block just above, then follow the standard procedure - if option_holder_obj.switch not in options_list: - if option_holder_obj.check_requirements(copy_dict): - value = copy_dict[option_holder_obj.name] - - if value != option_holder_obj.default_value: - options_list.append(option_holder_obj.switch) - options_list.append(utils.to_string(value)) - - elif option_holder_obj.name == 'match_filter' \ - or option_holder_obj.name == 'external_arg_string' \ - or option_holder_obj.name == 'pp_args': - value = copy_dict[option_holder_obj.name] - if value != '': - options_list.append(option_holder_obj.switch) - options_list.append('"' + utils.to_string(value) + '"') - - elif option_holder_obj.name == 'subs_lang_list': - # Convert the list to a comma-separated string, that the - # 'subs_lang' option can use - lang_list = copy_dict[option_holder_obj.name] - if lang_list: - - comma = ',' - options_list.append('--sub-lang') - options_list.append(comma.join(lang_list)) - - # For all other options, just check the value is valid - elif option_holder_obj.check_requirements(copy_dict): - value = copy_dict[option_holder_obj.name] - - if value != option_holder_obj.default_value: - options_list.append(option_holder_obj.switch) - - if not option_holder_obj.is_boolean(): - options_list.append(utils.to_string(value)) - - # Parse the 'extra_cmd_string' option, which can contain arguments - # inside double quotes "..." (arguments that can therefore contain - # whitespace) - parsed_list = utils.parse_ytdl_options(copy_dict['extra_cmd_string']) - for item in parsed_list: - options_list.append(item) - - # Parse the 'match_title_list' and 'reject_title_list' - for item in copy_dict['match_title_list']: - options_list.append('--match-title') - options_list.append(item) - - for item in copy_dict['reject_title_list']: - options_list.append('--reject-title') - options_list.append(item) - - # Parsing complete - return options_list - - - def build_file_sizes(self, copy_dict): - - """Called by self.parse(). - - Build the value of the 'min_filesize' and 'max_filesize' options and - store them in the options dictionary. - - Args: - - copy_dict (dict): Copy of the original options dictionary. - - """ - - if copy_dict['min_filesize']: - copy_dict['min_filesize'] = \ - utils.to_string(copy_dict['min_filesize']) + \ - copy_dict['min_filesize_unit'] - - if copy_dict['max_filesize']: - copy_dict['max_filesize'] = \ - utils.to_string(copy_dict['max_filesize']) + \ - copy_dict['max_filesize_unit'] - - - def build_limit_rate(self, copy_dict): - - """Called by self.parse(). - - Build the value of the 'limit_rate' option and store it in the options - dictionary. - - Args: - - copy_dict (dict): Copy of the original options dictionary. - - """ - - # Set the bandwidth limit (e.g. '50K') - if self.app_obj.bandwidth_apply_flag: - - # The bandwidth limit is divided equally between the workers - limit = int( - self.app_obj.bandwidth_default - / self.app_obj.num_worker_default - ) - - copy_dict['limit_rate'] = str(limit) + 'K' - - - def build_save_path(self, media_data_obj, copy_dict): - - """Called by self.parse(). - - Build the value of the 'save_path' option and store it in the options - dictionary. - - Args: - - 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. - - """ - - # 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: - - 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']] - # In the case of copy_dict['output_format'] = 0 - if template is None: - template = copy_dict['output_template'] - - copy_dict['save_path'] = os.path.abspath( - os.path.join(save_path, template), - ) - - - def build_video_format(self, copy_dict): - - """Called by self.parse(). - - Build the value of the 'video_format' option and store it in the - options dictionary. - - Args: - - copy_dict (dict): Copy of the original options dictionary. - - """ - - # 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') - # 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 - resolution_dict = formats.VIDEO_RESOLUTION_DICT.copy() - fps_dict = formats.VIDEO_FPS_DICT.copy() - - # If the progressive scan resolution is specified, it overrides all - # other video format options - height = None - fps = None - - if self.app_obj.video_res_apply_flag: - height = resolution_dict[self.app_obj.video_res_default] - # (Currently, formats.VIDEO_FPS_DICT only lists formats with 60fps) - 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']] - - 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']] - - 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 height is not None: - - # (Currently, formats.VIDEO_FPS_DICT only lists formats with 60fps) - if fps is None: - - # Use a youtube-dl argument in the form - # 'bestvideo[height<=?height]+bestaudio/best[height<=height]' - 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' - - # Not using a progressive scan resolution - elif copy_dict['video_format'] != '0' and \ - copy_dict['second_video_format'] != '0': - - if copy_dict['third_video_format'] != '0': - - copy_dict['video_format'] = copy_dict['video_format'] + '+' \ - + copy_dict['second_video_format'] + '+' \ - + copy_dict['third_video_format'] - - else: - copy_dict['video_format'] = copy_dict['video_format'] + '+' \ - + copy_dict['second_video_format'] - - -class OptionHolder(object): - - """Called from options.OptionsParser.__init__(). - - The options parser object converts the download options specified by an - options.OptionsManager object into a list of youtube-dl command line - options, whenever required. - - Each option has a name, a command line switch, a default value and an - optional list of requirements; they are stored together in an instance of - this object. - - Args: - - name (str): Option name. Must be a valid option name from the - optionsmanager.OptionsManager class (see the list in at the - beginning of the options.OptionsManager class) - - switch (str): The option command line switch. See - https://github.com/rg3/youtube-dl/#options - - default_value (any): The option default value. Must be the same type - as the corresponding option from the optionsmanager.OptionsManager - class. - - requirement_list (list): The requirements for the given option. This - argument is a list of strings with the name of all the options - that this specific option needs. If there are no requirements, the - IV is set to None. (For example 'subs_lang' needs the 'write_subs' - option to be enabled.) - - """ - - - # Standard class methods - - - def __init__(self, name, switch, default_value, requirement_list=None): - - # IV list - other - # --------------- - self.name = name - self.switch = switch - self.default_value = default_value - self.requirement_list = requirement_list - - - # Public class methods - - - def check_requirements(self, copy_dict): - - """Called by options.OptionsParser.parse(). - - Check if options required by another option are enabled, or not. - - Args: - - copy_dict (dict): Copy of the original options dictionary. - - Returns: - - True if any of the required options is enabled, otherwise returns - False. - - """ - - if not self.requirement_list: - return True - - return any([copy_dict[req] for req in self.requirement_list]) - - - def is_boolean(self): - - """Called by options.OptionsParser.parse(). - - Returns: - - True if the option is a boolean switch, otherwise returns False - - """ - - return type(self.default_value) is bool diff --git a/tartube/refresh.py b/tartube/refresh.py deleted file mode 100755 index 9b66eda..0000000 --- a/tartube/refresh.py +++ /dev/null @@ -1,612 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Refresh operation classes.""" - - -# Import Gtk modules -import gi -from gi.repository import GObject - - -# Import other modules -import os -import threading -import time - - -# Import our modules -import formats -import media -import utils - - -# Debugging flag (calls utils.debug_time at the start of every function) -DEBUG_FUNC_FLAG = False - - -# Classes - - -class RefreshManager(threading.Thread): - - """Called by mainapp.TartubeApp.refresh_manager_continue(). - - Python class to manage the refresh operation, in which the media registry - is checked against Tartube's data directory and updated as appropriate. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - init_obj (media.Channel, media.Playlist, media.Folder or None): If - specified, only this media data object is refreshed. If not - specified, the whole media data registry is refreshed. - - """ - - - # Standard class methods - - - def __init__(self, app_obj, init_obj=None): - - if DEBUG_FUNC_FLAG: - utils.debug_time('rop 71 __init__') - - super(RefreshManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media data object (channel, playlist or folder) to refresh, or - # None if the whole media data registry is to be refreshed - self.init_obj = init_obj - - - # IV list - other - # --------------- - # Flag set to False if self.stop_refresh_operation() is called, which - # halts the operation immediately - self.running_flag = True - - # The time at which the refresh operation began (in seconds since - # epoch) - self.start_time = int(time.time()) - # The time at which the refresh operation completed (in seconds since - # epoch) - self.stop_time = None - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # The number of media data objects refreshed so far... - self.job_count = 0 - # ...and the total number to refresh (these numbers are displayed in - # the progress bar in the Videos tab) - self.job_total = 0 - - # Total number of videos analysed - self.video_total_count = 0 - # Number of videos matched with a media.Video object in the database - self.video_match_count = 0 - # Number of videos not matched, and therefore given a new media.Video - # object - self.video_new_count = 0 - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Compiles a list of media data objects (channels, playlists and folders) - to refresh. If self.init_obj is not set, only that channel/playlist/ - folder (and its child channels/playlists/folders) are refreshed; - otherwise the whole media registry is refreshed. - - Then calls self.refresh_from_default_destination() for each item in the - list. - - Finally informs the main application that the refresh operation is - complete. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('rop 141 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', - ) - - else: - - media_type = self.init_obj.get_type() - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - 'Starting refresh operation, analysing ' + media_type \ - + ' \'' + self.init_obj.name + '\'', - ) - - # Compile a list of channels, playlists and folders to refresh (each - # one has their own sub-directory inside Tartube's data directory) - obj_list = [] - if self.init_obj: - # Add this channel/playlist/folder, and any child channels/ - # playlists/folders (but not videos, obviously) - obj_list = self.init_obj.compile_all_containers(obj_list) - else: - # Add all channels/playlists/folders in the database - for dbid in list(self.app_obj.media_name_dict.values()): - - obj = self.app_obj.media_reg_dict[dbid] - # Don't add private folders - if not isinstance(obj, media.Folder) or not obj.priv_flag: - obj_list.append(obj) - - self.job_total = len(obj_list) - - # Check each sub-directory in turn, updating the media data registry - # as we go - while self.running_flag and obj_list: - - obj = obj_list.pop(0) - - if obj.dbid == obj.master_dbid: - self.refresh_from_default_destination(obj) - else: - self.refresh_from_actual_destination(obj) - - # Pause a moment, before the next iteration of the loop (don't want - # to hog resources) - time.sleep(self.sleep_time) - - # Operation complete. Set the stop time - self.stop_time = int(time.time()) - - # Show a confirmation in the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - 'Refresh operation finished', - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' 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: ' \ - + str(self.video_match_count), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' New videos found and added to the database: ' \ - + str(self.video_new_count), - ) - - # Let the timer run for a few more seconds to prevent Gtk errors (for - # systems with Gtk < 3.24) - GObject.timeout_add( - 0, - self.app_obj.refresh_manager_halt_timer, - ) - - - def refresh_from_default_destination(self, media_data_obj): - - """Called by self.run(). - - Refreshes a single channel, playlist or folder, for which an - alternative download destination has not been set. - - If a file is missing in the channel/playlist/folder's sub-directory, - mark the video object as not downloaded. - - If unexpected video files exist in the sub-directory, create a new - media.Video object for them. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object to refresh - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('rop 249 refresh_from_default_destination') - - # Update the main window's progress bar - self.job_count += 1 - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - media_data_obj.name, - self.job_count, - self.job_total, - ) - - # Keep a running total of matched/new videos for this channel, playlist - # or folder - local_total_count = 0 - local_match_count = 0 - local_new_count = 0 - - # Update our progress in the Output Tab - if isinstance(media_data_obj, media.Channel): - string = 'Channel: ' - elif isinstance(media_data_obj, media.Playlist): - string = 'Playlist: ' - else: - string = 'Folder: ' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - string + media_data_obj.name, - ) - - # Get the sub-directory for this media data object - dir_path = media_data_obj.get_default_dir(self.app_obj) - - # Get a list of video files in the sub-directory - init_list = os.listdir(dir_path) - - # From this list, filter out files without a recognised file extension - # (.mp4, .webm, etc) - mod_list = [] - for relative_path in init_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - filename, ext = os.path.splitext(relative_path) - # (Remove the initial .) - ext = ext[1:] - if ext in formats.VIDEO_FORMAT_DICT: - - mod_list.append(relative_path) - - # From the new list, filter out duplicate filenames (e.g. if the list - # contains both 'my_video.mp4' and 'my_video.webm', filter out the - # second one, adding to a list of alternative files) - filter_list = [] - filter_dict = {} - alt_list = [] - for relative_path in mod_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - filename, ext = os.path.splitext(relative_path) - - if not filename in filter_dict: - filter_list.append(relative_path) - filter_dict[filename] = relative_path - - else: - alt_list.append(relative_path) - - # Now compile a dictionary of media.Video objects in this channel/ - # playlist/folder, so we can eliminate them one by one - check_dict = {} - for child_obj in media_data_obj.child_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - if isinstance(child_obj, media.Video) and child_obj.file_name: - - # Does the video file still exist? - this_file = child_obj.file_name + child_obj.file_ext - if child_obj.dl_flag and not this_file in init_list: - self.app_obj.mark_video_downloaded(child_obj, False) - else: - check_dict[child_obj.file_name] = child_obj - - # If this channel/playlist/folder is the alternative download - # destination for other channels/playlists/folders, compile a - # dicationary of their media.Video objects - # (If we find a video we weren't expecting, before creating a new - # media.Video object, we must first check it isn't one of them) - slave_dict = {} - for slave_dbid in media_data_obj.slave_dbid_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - slave_obj = self.app_obj.media_reg_dict[slave_dbid] - for child_obj in slave_obj.child_list: - - if isinstance(child_obj, media.Video) and child_obj.file_name: - slave_dict[child_obj.file_name] = child_obj - - # Now try to match each video file (in filter_list) with an existing - # media.Video object (in check_dict) - # If there is no match, and if the video file doesn't match a video - # in another channel/playlist/folder (for which this is the - # alternative download destination), then we can create a new - # media.Video object - for relative_path in filter_list: - - # (If self.stop_refresh_operation() has been called, give up - # immediately) - if not self.running_flag: - return - - filename, ext = os.path.splitext(relative_path) - - if self.app_obj.refresh_output_videos_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Checking: ' + filename, - ) - - if filename in check_dict: - - # File matched - self.video_total_count += 1 - local_total_count += 1 - self.video_match_count += 1 - local_match_count += 1 - - # If it is not marked as downloaded, we can mark it so now - child_obj = check_dict[filename] - if not child_obj.dl_flag: - self.app_obj.mark_video_downloaded(child_obj, True) - - # Make sure the stored extension is correct (e.g. if we've - # matched an existing .webm video file, with an expected - # .mp4 video file) - if child_obj.file_ext != ext: - child_relative_path \ - = child_obj.file_name + child_obj.file_ext - - if not child_relative_path in alt_list: - child_obj.set_file(filename, ext) - - # Eliminate this media.Video object; no other video file should - # match it - del check_dict[filename] - - # Update our progress in the Output Tab (if required) - if self.app_obj.refresh_output_videos_flag: - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Match: ' + child_obj.name, - ) - - elif filename not in slave_dict: - - # File didn't match a media.Video object - self.video_total_count += 1 - local_total_count += 1 - self.video_new_count += 1 - local_new_count += 1 - - # Display the list of non-matching videos, if required - if self.app_obj.refresh_output_videos_flag \ - and self.app_obj.refresh_output_verbose_flag: - - for failed_path in check_dict.keys(): - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Non-match: ' + failed_path, - ) - - # Create a new media.Video object - video_obj = self.app_obj.add_video(media_data_obj, None) - video_path = os.path.abspath( - os.path.join( - dir_path, - filter_dict[filename], - ) - ) - - # Set the new video object's IVs - filename, ext = os.path.splitext(filter_dict[filename]) - video_obj.set_name(filename) - video_obj.set_nickname(filename) - video_obj.set_file(filename, ext) - - if ext == '.mkv': - video_obj.set_mkv() - - video_obj.set_file_size( - os.path.getsize( - os.path.abspath( - os.path.join(dir_path, filter_dict[filename]), - ), - ), - ) - - # If the video's JSON file exists downloaded, we can extract - # video statistics from it - self.app_obj.update_video_from_json(video_obj) - - # For any of those statistics that haven't been set (because - # the JSON file was missing or didn't contain the right - # statistics), set them directly - self.app_obj.update_video_from_filesystem( - video_obj, - video_path, - ) - - # This call marks the video as downloaded, and also updates the - # Video Index and Video Catalogue (if required) - self.app_obj.mark_video_downloaded(video_obj, True) - - if self.app_obj.refresh_output_videos_flag: - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' New video: ' + video_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) \ - + ', new: ' + str(local_new_count), - ) - - - def refresh_from_actual_destination(self, media_data_obj): - - """Called by self.run(). - - A modified version of self.refresh_from_default_destination(). - Refreshes a single channel, playlist or folder, for which an - alternative download destination has been set. - - If a file is missing in the alternative download destination, mark the - video object as not downloaded. - - Don't check for unexpected video files in the alternative download - destination - we expect that they exist. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object to refresh - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('rop 516 refresh_from_actual_destination') - - # Update the main window's progress bar - self.job_count += 1 - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - media_data_obj.name, - self.job_count, - self.job_total, - ) - - # Keep a running total of matched videos for this channel, playlist or - # folder - local_total_count = 0 - local_match_count = 0 - # (No new media.Video objects are created) - local_missing_count = 0 - - # Update our progress in the Output Tab - if isinstance(media_data_obj, media.Channel): - string = 'Channel: ' - elif isinstance(media_data_obj, media.Playlist): - string = 'Playlist: ' - else: - string = 'Folder: ' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - string + media_data_obj.name, - ) - - # Get the alternative download destination - dir_path = media_data_obj.get_actual_dir(self.app_obj) - - # Get a list of video files in that sub-directory - init_list = os.listdir(dir_path) - - # Now check each media.Video object, to see if the video file still - # exists (or not) - for child_obj in media_data_obj.child_list: - - if isinstance(child_obj, media.Video) and child_obj.file_name: - - this_file = child_obj.file_name + child_obj.file_ext - if child_obj.dl_flag and not this_file in init_list: - - local_missing_count += 1 - - # Video doesn't exist, so mark it as not downloaded - self.app_obj.mark_video_downloaded(child_obj, False) - - # Update our progress in the Output Tab (if required) - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Missing: ' + child_obj.name, - ) - - elif not child_obj.dl_flag and this_file in init_list: - - self.video_total_count += 1 - local_total_count += 1 - self.video_match_count += 1 - local_match_count += 1 - - # Video exists, so mark it as downloaded (but don't mark it - # as new) - self.app_obj.mark_video_downloaded(child_obj, True, True) - - # Update our progress in the Output Tab (if required) - if self.app_obj.refresh_output_videos_flag: - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' 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), - ) - - - def stop_refresh_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Stops the refresh operation. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('rop 610 stop_refresh_operation') - - self.running_flag = False diff --git a/tartube/testing.py b/tartube/testing.py deleted file mode 100755 index 4c56362..0000000 --- a/tartube/testing.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Test code.""" - - -# Import Gtk modules -# ... - - -# Import other modules -# ... - - -# Import our modules -# ... - - -# Functions - - -def add_test_media(app_obj): - - """Called by mainapp.TartubeApp.on_menu_test(). - - Add a set of media data objects for testing. This function can only be - called if the debugging flags are set. - - Enable/disable various media objects by changing the 0s and 1s in the code - below. - - The videos, channels and playlists listed here have been chosen because - they are short. They have no connection to the Tartube developers. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - """ - - # Test videos - - if 1: - - if 1: - video = app_obj.add_video( - app_obj.fixed_misc_folder, - 'https://www.youtube.com/watch?v=668nUCeBHyY', - ) - video.set_name('Nature Beautiful short video 720p HD') - - if 1: - video2 = app_obj.add_video( - app_obj.fixed_misc_folder, - 'https://www.youtube.com/watch?v=MJXayNvM3_E', - ) - video2.set_name('2019 BMW K 1600 B Imperial Blue - Short Video') - - if 1: - video3 = app_obj.add_video( - app_obj.fixed_misc_folder, - 'https://www.youtube.com/watch?v=jypAVuatE5w', - ) - video3.set_name('our shortest dumb video') - - # Test channel - - if 1 and not 'Test channel' in app_obj.media_name_dict: - channel = app_obj.add_channel( - 'Test channel', - None, # No parent - 'https://www.youtube.com/channel/UCQqM9nXKbGaYFfl0mh6ShRA/' \ - + 'featured', - None, - ) - app_obj.main_win_obj.video_index_add_row(channel) - - # Test playlist - - if 1 and not 'Test playlist' in app_obj.media_name_dict: - playlist = app_obj.add_playlist( - 'Test playlist', - None, # No parent - 'https://www.youtube.com/watch?v=tPEE9ZwTmy0&list=' \ - + 'PLHJH2BlYG-EEBtw2y1njWpDukJSTs8Qqx', - None, - ) - app_obj.main_win_obj.video_index_add_row(playlist) - - # Test folder - - if 1: - - if 1 and not 'Test folder' in app_obj.media_name_dict: - folder = app_obj.add_folder( - 'Test folder', - None, # No parent - ) - app_obj.main_win_obj.video_index_add_row(folder) - - if 1 and not 'Test folder 2' in app_obj.media_name_dict: - folder2 = app_obj.add_folder( - 'Test folder 2', - None, # No parent - ) - app_obj.main_win_obj.video_index_add_row(folder2) - - if 1 and not 'Test folder 3' in app_obj.media_name_dict: - folder3 = app_obj.add_folder( - 'Test folder 3', - folder2, - ) - app_obj.main_win_obj.video_index_add_row(folder3) - - if 1 and not 'Test folder 4' in app_obj.media_name_dict: - folder4 = app_obj.add_folder( - 'Test folder 4', - folder2, - ) - app_obj.main_win_obj.video_index_add_row(folder4) diff --git a/tartube/tidy.py b/tartube/tidy.py deleted file mode 100755 index b355728..0000000 --- a/tartube/tidy.py +++ /dev/null @@ -1,1059 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Tidy operation classes.""" - - -# Import Gtk modules -import gi -from gi.repository import GObject - - -# Import other modules -try: - import moviepy.editor -except: - pass - -import os -import re -import threading -import time - - -# Import our modules -import formats -import media -import utils - - -# Debugging flag (calls utils.debug_time at the start of every function) -DEBUG_FUNC_FLAG = False - - -# Classes - - -class TidyManager(threading.Thread): - - """Called by mainapp.TartubeApp.tidy_manager_start(). - - Python class to manage the tidy operation, in which videos can be checked - for corruption and actually existing (or not), and various file types can - be deleted collectively. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - choices_dict (dict): A dictionary specifying the choices made by the - user in mainwin.TidyDialogue. The dictionary is in the following - format: - - media_data_obj: A media.Channel, media.Playlist or media.Folder - object, or None if all channels/playlists/folders are to be - tidied up. If specified, the cahnnel/playlist/folder and all of - its descendants are checked - - corrupt_flag: True if video files should be checked for corruption - - del_corrupt_flag: True if corrupted video files should be deleted - - exist_Flag: True if video files that should exist should be - checked, in case they don't (and vice-versa) - - del_video_flag: True if downloaded video files should be deleted - - del_others_flag: True if all video/audio files with the same name - should be deleted (as artefacts of post-processing with FFmpeg - or AVConv) - - del_descrip_flag: True if all description files should be deleted - - del_json_flag: True if all metadata (JSON) files should be deleted - - del_xml_flag: True if all annotation files should be deleted - - del_thumb_flag: True if all thumbnail files should be deleted - - del_archive_flag: True if all youtube-dl archive files should be - deleted - - """ - - - # Standard class methods - - - def __init__(self, app_obj, choices_dict): - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 107 __init__') - - super(TidyManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_obj - # The media data object (channel, playlist or folder) to be tidied up, - # or None if the whole data directory is to be tidied up - # If specified, the channel/playlist/folder and all of its descendants - # are checked - self.init_obj = choices_dict['media_data_obj'] - - - # IV list - other - # --------------- - # Flag set to False if self.stop_tidy_operation() is called, which - # halts the operation immediately - self.running_flag = True - - # The time at which the tidy operation began (in seconds since epoch) - self.start_time = int(time.time()) - # The time at which the tidy operation completed (in seconds since - # epoch) - self.stop_time = None - # The time (in seconds) between iterations of the loop in self.run() - self.sleep_time = 0.25 - - # Flags specifying which actions should be applied - # True if video files should be checked for corruption - self.corrupt_flag = choices_dict['corrupt_flag'] - # True if corrupted video files should be deleted - self.del_corrupt_flag = choices_dict['del_corrupt_flag'] - # True if video files that should exist should be checked, in case they - # don't (and vice-versa) - self.exist_flag = choices_dict['exist_flag'] - # True if downloaded video files should be deleted - self.del_video_flag = choices_dict['del_video_flag'] - # True if all video/audio files with the same name should be deleted - # (as artefacts of post-processing with FFmpeg or AVConv) - self.del_others_flag = choices_dict['del_others_flag'] - # True if all description files should be deleted - self.del_descrip_flag = choices_dict['del_descrip_flag'] - # True if all metadata (JSON) files should be deleted - self.del_json_flag = choices_dict['del_json_flag'] - # True if all annotation files should be deleted - self.del_xml_flag = choices_dict['del_xml_flag'] - # True if all thumbnail files should be deleted - self.del_thumb_flag = choices_dict['del_thumb_flag'] - # True if all youtube-dl archive files should be deleted - self.del_archive_flag = choices_dict['del_archive_flag'] - - # The number of media data objects whose directories have been tidied - # so far... - self.job_count = 0 - # ...and the total number to tidy (these numbers are displayed in the - # progress bar in the Videos tab) - self.job_total = 0 - - # Individual counts, updated as we go - self.video_corrupt_count = 0 - self.video_corrupt_deleted_count = 0 - self.video_exist_count = 0 - self.video_no_exist_count = 0 - self.video_deleted_count = 0 - self.other_deleted_count = 0 - self.descrip_deleted_count = 0 - self.json_deleted_count = 0 - self.xml_deleted_count = 0 - self.thumb_deleted_count = 0 - self.archive_deleted_count = 0 - - - # Code - # ---- - - # Let's get this party started! - self.start() - - - # Public class methods - - - def run(self): - - """Called as a result of self.__init__(). - - Compiles a list of media data objects (channels, playlists and folders) - to tidy up. If self.init_obj is not set, only that channel/playlist/ - folder (and its child channels/playlists/folders) are tidied up; - otherwise the whole data directory is tidied up. - - Then calls self.tidy_directory() for each item in the list. - - Finally informs the main application that the tidy operation is - complete. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 207 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', - ) - - else: - - media_type = self.init_obj.get_type() - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - 'Starting tidy operation, tidying up ' + media_type \ - + ' \'' + self.init_obj.name + '\'', - ) - - if self.corrupt_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Check videos are not corrupted: ' + text, - ) - - if self.corrupt_flag: - - if self.del_corrupt_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Delete corrupted videos: ' + text, - ) - - if self.exist_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Check videos do/don\'t exist: ' + text, - ) - - if self.del_video_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Delete all video files: ' + text, - ) - - if self.del_video_flag: - - if self.del_others_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Delete other video/audio files: ' + text, - ) - - if self.del_descrip_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Delete all description files: ' + text, - ) - - if self.del_json_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Delete all metadata (JSON) files: ' + text, - ) - - if self.del_xml_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Delete all annotation files: ' + text, - ) - - if self.del_thumb_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Delete all thumbnail files: ' + text, - ) - - if self.del_archive_flag: - text = 'YES' - else: - text = 'NO' - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Delete youtube-dl archive files: ' + text, - ) - - # Compile a list of channels, playlists and folders to tidy up (each - # one has their own sub-directory inside Tartube's data directory) - obj_list = [] - if self.init_obj: - # Add this channel/playlist/folder, and any child channels/ - # playlists/folders (but not videos, obviously) - obj_list = self.init_obj.compile_all_containers(obj_list) - else: - # Add all channels/playlists/folders in the database - for dbid in list(self.app_obj.media_name_dict.values()): - - obj = self.app_obj.media_reg_dict[dbid] - # Don't add private folders - if not isinstance(obj, media.Folder) or not obj.priv_flag: - obj_list.append(obj) - - self.job_total = len(obj_list) - - # Check each sub-directory in turn, updating the media data registry - # as we go - while self.running_flag and obj_list: - self.tidy_directory(obj_list.pop(0)) - - # Pause a moment, before the next iteration of the loop (don't want - # to hog resources) - time.sleep(self.sleep_time) - - # Operation complete. Set the stop time - self.stop_time = int(time.time()) - - # Show a confirmation in the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - 'Tidy operation finished', - ) - - if self.corrupt_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Corrupted videos found: ' \ - + str(self.video_corrupt_count), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Corrupted videos deleted: ' \ - + str(self.video_corrupt_deleted_count), - ) - - if self.exist_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' New video files detected: ' \ - + str(self.video_exist_count), - ) - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Missing video files detected: ' \ - + str(self.video_no_exist_count), - ) - - if self.del_video_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' 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: ' \ - + str(self.other_deleted_count), - ) - - if self.del_descrip_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Description files deleted: ' \ - + str(self.descrip_deleted_count), - ) - - if self.del_json_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Metadata (JSON) files deleted: ' \ - + str(self.json_deleted_count), - ) - - if self.del_xml_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Annotation files deleted: ' \ - + str(self.xml_deleted_count), - ) - - if self.del_thumb_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Thumbnail files deleted: ' \ - + str(self.thumb_deleted_count), - ) - - if self.del_archive_flag: - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' youtube-dl archive files deleted: ' \ - + str(self.archive_deleted_count), - ) - - # Let the timer run for a few more seconds to prevent Gtk errors (for - # systems with Gtk < 3.24) - GObject.timeout_add( - 0, - self.app_obj.tidy_manager_halt_timer, - ) - - - def tidy_directory(self, media_data_obj): - - """Called by self.run(). - - Tidy up the directory of a single channel, playlist or folder. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 470 tidy_directory') - - # Update the main window's progress bar - self.job_count += 1 - GObject.timeout_add( - 0, - self.app_obj.main_win_obj.update_progress_bar, - media_data_obj.name, - self.job_count, - self.job_total, - ) - - media_type = media_data_obj.get_type() - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - 'Checking ' + media_type + ' \'' + media_data_obj.name + '\'', - ) - - if self.corrupt_flag: - self.check_video_corrupt(media_data_obj) - - if self.exist_flag: - self.check_videos_exist(media_data_obj) - - if self.del_video_flag: - self.delete_video(media_data_obj) - - if self.del_descrip_flag: - self.delete_descrip(media_data_obj) - - if self.del_json_flag: - self.delete_json(media_data_obj) - - if self.del_xml_flag: - self.delete_xml(media_data_obj) - - if self.del_thumb_flag: - self.delete_thumb(media_data_obj) - - if self.del_archive_flag: - self.delete_archive(media_data_obj) - - - def check_video_corrupt(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - video are corrupted, don't delete them (let the user do that manually). - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 529 check_video_corrupt') - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None \ - and video_obj.dl_flag: - - video_path = video_obj.get_actual_path(self.app_obj) - - if os.path.isfile(video_path): - - # Code copied from - # mainapp.TartubeApp.update_video_from_filesystem() - # When the video file is corrupted, moviepy freezes - # indefinitely - # Instead, let's try placing the procedure inside a thread - # (unlike the original function, this one is never called - # if .refresh_moviepy_timeout is 0) - this_thread = threading.Thread( - target=self.call_moviepy, - args=(video_obj, video_path,), - ) - - this_thread.daemon = True - this_thread.start() - this_thread.join(self.app_obj.refresh_moviepy_timeout) - if this_thread.is_alive(): - - # moviepy timed out, so assume the video is corrupted - self.video_corrupt_count += 1 - - if self.del_corrupt_flag \ - and os.path.isfile(video_path): - - # Delete the corrupted file - os.remove(video_path) - - self.video_corrupt_deleted_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Deleted (possibly) corrupted video' - + ' file: \'' + video_obj.name + '\'', - ) - - self.app_obj.mark_video_downloaded( - video_obj, - False, - ) - - else: - - # Don't delete it - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Video file might be corrupt: \'' \ - + video_obj.name + '\'', - ) - - - def check_videos_exist(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - video should exist, but doesn't (or vice-versa), modify the media.Video - object's IVs accordingly. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 605 check_videos_exist') - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - video_path = video_obj.get_actual_path(self.app_obj) - - if not video_obj.dl_flag \ - and os.path.isfile(video_path): - - # File exists, but is marked as not downloaded - self.app_obj.mark_video_downloaded( - video_obj, - True, # Video is downloaded - True, # ...but don't mark it as new - ) - - self.video_exist_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Video file exists: \'' + video_obj.name + '\'', - ) - - elif video_obj.dl_flag \ - and not os.path.isfile(video_path): - - # File doesn't exist, but is marked as downloaded - self.app_obj.mark_video_downloaded( - video_obj, - False, # Video is not downloaded - ) - - self.video_no_exist_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Video file doesn\'t exist: \'' + video_obj.name \ - + '\'', - ) - - - def delete_video(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - video exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 663 delete_video') - - ext_list = formats.VIDEO_FORMAT_LIST.copy() - ext_list.extend(formats.AUDIO_FORMAT_LIST) - - for video_obj in media_data_obj.compile_all_videos( [] ): - - video_path = None - if video_obj.file_name is not None: - - video_path = video_obj.get_actual_path(self.app_obj) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - video_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - video_path, - ) - - if video_path is not None: - - if video_obj.dl_flag \ - and os.path.isfile(video_path): - - # Delete the downloaded video file - os.remove(video_path) - - # Mark the video as not downloaded - self.app_obj.mark_video_downloaded(video_obj, False) - - self.video_deleted_count += 1 - - if self.del_others_flag: - - # Also delete all video/audio files with the same name - # There might be thousands of files in the directory, so - # using os.walk() or something like that might be too - # expensive - # Also, post-processing might create various artefacts, all - # of which must be deleted - for ext in ext_list: - - other_path = video_obj.get_actual_path_by_ext( - self.app_obj, - ext, - ) - - if os.path.isfile(other_path): - os.remove(other_path) - - self.other_deleted_count += 1 - - # For an encore, delete all post-processing artefacts in the form - # VIDEO_NAME.fNNN.ext, where NNN is an integer and .ext is one of - # the video extensions specified by formats.VIDEO_FORMAT_LIST - # (.mkv, etc) - # (The previous code won't pick them up, but we can delete them all - # now.) - # (The alternative download destination, if set, is not affected.) - check_list = [] - search_path = media_data_obj.get_default_dir(self.app_obj) - - for (dir_path, dir_name_list, file_name_list) in os.walk(search_path): - check_list.extend(file_name_list) - - char = '|' - regex = '\.f\d+\.(' + char.join(formats.VIDEO_FORMAT_LIST) + ')$' - for check_path in check_list: - if re.search(regex, check_path): - - full_path = os.path.abspath( - os.path.join(search_path, check_path), - ) - - if os.path.isfile(full_path): - - os.remove(full_path) - self.other_deleted_count += 1 - - - def delete_descrip(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated description file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 762 delete_descrip') - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - descrip_path = video_obj.get_actual_path_by_ext( - self.app_obj, - '.description', - ) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - descrip_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - descrip_path, - ) - - if descrip_path is not None \ - and os.path.isfile(descrip_path): - - # Delete the description file - os.remove(descrip_path) - self.descrip_deleted_count += 1 - - - def delete_json(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated metadata (JSON) file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 807 delete_json') - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - json_path = video_obj.get_actual_path_by_ext( - self.app_obj, - '.info.json', - ) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - json_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - json_path, - ) - - if json_path is not None \ - and os.path.isfile(json_path): - - # Delete the metadata file - os.remove(json_path) - self.json_deleted_count += 1 - - - def delete_xml(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated annotation file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 852 delete_xml') - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - xml_path = video_obj.get_actual_path_by_ext( - self.app_obj, - '.annotations.xml', - ) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - xml_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - xml_path, - ) - - if xml_path is not None \ - and os.path.isfile(xml_path): - - # Delete the annotation file - os.remove(xml_path) - self.xml_deleted_count += 1 - - - def delete_thumb(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks all child videos of the specified media data object. If the - associated thumbnail file exists, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 897 delete_thumb') - - for video_obj in media_data_obj.compile_all_videos( [] ): - - if video_obj.file_name is not None: - - # Thumbnails might be in one of two locations - thumb_path = utils.find_thumbnail(self.app_obj, video_obj) - - # If the video's parent container has an alternative download - # destination set, we must check the corresponding media - # data object. If the latter also has a media.Video object - # matching this video, then this function returns None and - # nothing is deleted - if thumb_path is not None: - - thumb_path = self.check_video_in_actual_dir( - media_data_obj, - video_obj, - thumb_path, - ) - - if thumb_path is not None \ - and os.path.isfile(thumb_path): - - # Delete the thumbnail file - os.remove(thumb_path) - self.thumb_deleted_count += 1 - - - def delete_archive(self, media_data_obj): - - """Called by self.tidy_directory(). - - Checks the specified media data object's directory. If a youtube-dl - archive file is found there, delete it. - - Args: - - media_data_obj (media.Channel, media.Playlist or media.Folder): - The media data object whose directory must be tidied up - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 942 delete_archive') - - archive_path = os.path.abspath( - os.path.join( - media_data_obj.get_default_dir(self.app_obj), - 'ytdl-archive.txt', - ), - ) - - if os.path.isfile(archive_path): - - # Delete the archive file - os.remove(archive_path) - self.archive_deleted_count += 1 - - - def call_moviepy(self, video_obj, video_path): - - """Called by thread inside self.check_video_corrupt(). - - When we call moviepy.editor.VideoFileClip() on a corrupted video file, - moviepy freezes indefinitely. - - This function is called inside a thread, so a timeout of (by default) - ten seconds can be applied. - - Args: - - video_obj (media.Video): The video object being updated - - video_path (str): The path to the video file itself - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 977 call_moviepy') - - try: - clip = moviepy.editor.VideoFileClip(video_path) - - except: - self.video_corrupt_count += 1 - - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - ' Video file might be corrupt: \'' + video_obj.name + '\'', - ) - - - def check_video_in_actual_dir(self, container_obj, video_obj, file_path): - - """Called by self.delete_video(), .delete_descrip(), .delete_json(), - .delete_xml() and .delete_thumb(). - - If the video's parent container has an alternative download destination - set, we must check the corresponding media data object. If the latter - also has a media.Video object matching this video, then this function - returns None and nothing is deleted. Otherwise, the specified file_path - is returned, so it can be deleted. - - Args: - - container_obj (media.Channel, media.Playlist, media.Folder): A - channel, playlist or folder - - video_obj (media.Video): A video contained in that channel, - playlist or folder - - file_path (str): The path to a file which the calling function - wants to delete - - Returns: - - The specified file_path if it can be deleted, or None if it should - not be deleted - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 1021 check_video_in_actual_dir') - - if container_obj.dbid == container_obj.master_dbid: - - # No alternative download destination to check - return file_path - - else: - - # Get the channel/playlist/folder acting as container_obj's - # alternative download destination - master_obj = self.app_obj.media_reg_dict[container_obj.master_dbid] - - # Check its videos. Are there any videos with the same name? - for child_obj in master_obj.child_list: - - if child_obj.file_name is not None \ - and child_obj.file_name == video_obj.file_name: - - # Don't delete the file associated with this video - return None - - # There are no videos with the same name, so the file can be - # deleted - return file_path - - - def stop_tidy_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Stops the tidy operation. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('top 1057 stop_tidy_operation') - - self.running_flag = False diff --git a/tartube/updates.py b/tartube/updates.py deleted file mode 100755 index 4a9d1d8..0000000 --- a/tartube/updates.py +++ /dev/null @@ -1,558 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Update operation classes.""" - - -# Import Gtk modules -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import GObject - - -# Import other modules -import os -import queue -import re -import requests -import signal -import subprocess -import sys -import threading - - -# Import our modules -import downloads -import utils - - -# Debugging flag (calls utils.debug_time at the start of every function) -DEBUG_FUNC_FLAG = False - - -# Classes - - -class UpdateManager(threading.Thread): - - """Called by mainapp.TartubeApp.update_manager_start(). - - Python class to create a system child process, to do one of two jobs: - - 1. Install FFmpeg (on MS Windows only) - - 2. Install youtube-dl, or update it to its most recent version. - - 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 - - update_type (str): 'ffmpeg' to install FFmpeg (on MS Windows only), or - 'ytdl' to install/update youtube-dl - - """ - - - # Standard class methods - - - def __init__(self, app_obj, update_type): - - if DEBUG_FUNC_FLAG: - utils.debug_time('uop 81 __init__') - - super(UpdateManager, self).__init__() - - # IV list - class objects - # ----------------------- - # The mainapp.TartubeApp object - self.app_obj = app_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 = downloads.PipeReader(self.stdout_queue) - self.stderr_reader = downloads.PipeReader(self.stderr_queue) - - # The child process created by self.create_child_process() - self.child_process = None - - - # IV list - other - # --------------- - # 'ffmpeg' to install FFmpeg (on MS Windows only), or 'ytdl' to - # install/update youtube-dl - self.update_type = update_type - # Flag set to True if the update operation succeeds, False if it fails - self.success_flag = False - - # The youtube-dl version number as a string, if captured from the child - # process (e.g. '2019.07.02') - self.ytdl_version = None - - # (For debugging purposes, store any STDOUT/STDERR messages received; - # otherwise we would just set a flag if a STDERR message was - # received) - self.stdout_list = [] - self.stderr_list = [] - - - # 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('uop 141 run') - - if self.update_type == 'ffmpeg': - self.install_ffmpeg() - else: - self.install_ytdl() - - - def create_child_process(self, cmd_list): - - """Called by self.install_ffmpeg() or .install_ytdl(). - - Based on code from downloads.VideoDownloader.create_child_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. - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('uop 165 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, - ) - - 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') - - - def install_ffmpeg(self): - - """Called by self.run(). - - A modified version of self.install_ytdl, that installs FFmpeg on an - MS Windows system. - - Creates a child process to run the installation process. - - Reads from the child process STDOUT and STDERR, and calls the main - application with the result of the update (success or failure). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('uop 208 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', - ) - - # Create a new child process to install either the 64-bit or 32-bit - # version of FFmpeg, as appropriate - if sys.maxsize <= 2147483647: - binary = 'mingw-w64-i686-ffmpeg' - else: - binary = 'mingw-w64-x86_64-ffmpeg' - - self.create_child_process( - ['pacman', '-S', binary, '--noconfirm'], - ) - - # Show the system command in the Output Tab - space = ' ' - self.app_obj.main_win_obj.output_tab_write_system_cmd( - 1, - space.join( ['pacman', '-S', binary, '--noconfirm'] ), - ) - - # So that we can read from the child process STDOUT and STDERR, attach - # 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, - ) - - while self.is_child_process_alive(): - - # Read from the child process STDOUT, and convert into unicode for - # Python's convenience - while not self.stdout_queue.empty(): - - stdout = self.stdout_queue.get_nowait().rstrip() - stdout = stdout.decode('cp1252') - - if stdout: - - # Show command line output in the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - stdout, - ) - - # The child process has finished - while not self.stderr_queue.empty(): - - # Read from the child process STDERR queue (we don't need to read - # it in real time), and convert into unicode for python's - # convenience - stderr = self.stderr_queue.get_nowait().rstrip() - stderr = stderr.decode('cp1252') - - # Ignore pacman warning messages, e.g. 'warning: dependency cycle - # detected:' - if stderr and not re.match('warning\:', stderr): - - self.stderr_list.append(stderr) - - # Show command line output in the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - stderr, - ) - - # (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') - - elif self.child_process.returncode > 0: - self.stderr_list.append( - 'Child process exited with non-zero code: {}'.format( - self.child_process.returncode, - ) - ) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.update_manager_finished - if not self.stderr_list: - self.success_flag = True - - # Show a confirmation in the the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - 'Update operation finished', - ) - - # Let the timer run for a few more seconds to prevent Gtk errors (for - # systems with Gtk < 3.24) - GObject.timeout_add( - 0, - self.app_obj.update_manager_halt_timer, - ) - - - def install_ytdl(self): - - """Called by self.run(). - - Based on code from downloads.VideoDownloader.do_download(). - - Creates a child process to run the youtube-dl update. - - Reads from the child process STDOUT and STDERR, and calls the main - application with the result of the update (success or failure). - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('uop 328 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', - ) - - # Prepare the system command - - # The user can change the system command for updating youtube-dl, - # depending on how it was installed - # (For example, if youtube-dl was installed via pip, then it must be - # updated via pip) - cmd_list \ - = self.app_obj.ytdl_update_dict[self.app_obj.ytdl_update_current] - - # Convert a path beginning with ~ (not on MS Windows) - if os.name != 'nt': - cmd_list[0] = re.sub('^\~', os.path.expanduser('~'), cmd_list[0]) - - # Create a new child process using that command - self.create_child_process(cmd_list) - - # Show the system command in the Output Tab - space = ' ' - self.app_obj.main_win_obj.output_tab_write_system_cmd( - 1, - space.join(cmd_list), - ) - - # So that we can read from the child process STDOUT and STDERR, attach - # 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, - ) - - while self.is_child_process_alive(): - - # Read from the child process STDOUT, and convert into unicode for - # Python's convenience - while not self.stdout_queue.empty(): - - stdout = self.stdout_queue.get_nowait().rstrip() - if stdout: - - if os.name == 'nt': - stdout = stdout.decode('cp1252') - else: - stdout = stdout.decode('utf-8') - - # "It looks like you installed youtube-dl with a package - # manager, pip, setup.py or a tarball. Please use that to - # update." - if re.search('It looks like you installed', stdout): - self.stderr_list.append(stdout) - else: - # Try to intercept the new version number for - # youtube-dl - self.intercept_version_from_stdout(stdout) - self.stdout_list.append(stdout) - - # Show command line output in the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - stdout, - ) - - # The child process has finished - while not self.stderr_queue.empty(): - - # Read from the child process STDERR queue (we don't need to read - # it in real time), and convert into unicode for python's - # convenience - stderr = self.stderr_queue.get_nowait().rstrip() - if os.name == 'nt': - stderr = stderr.decode('cp1252') - else: - stderr = stderr.decode('utf-8') - - if stderr: - - # If the user has pip installed, rather than pip3, they will by - # now (mid-2019) be seeing a Python 2.7 deprecation warning. - # Ignore that message, if received - # If a newer version of pip is available, the user will see a - # 'You should consider upgrading' warning. Ignore that too, - # if received - if not re.search('DEPRECATION', stderr) \ - and not re.search('You are using pip version', stderr) \ - and not re.search('You should consider upgrading', stderr): - self.stderr_list.append(stderr) - - # Show command line output in the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - stderr, - ) - - # (Generate our own error messages for debugging purposes, in certain - # situations) - if self.child_process is None: - - msg = 'youtube-dl update did not start' - self.stderr_list.append(msg) - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - msg, - ) - - elif self.child_process.returncode > 0: - - msg = 'Child process exited with non-zero code: {}'.format( - self.child_process.returncode, - ) - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - msg, - ) - - # Operation complete. self.success_flag is checked by - # mainapp.TartubeApp.update_manager_finished - if not self.stderr_list: - self.success_flag = True - - # Show a confirmation in the the Output Tab - self.app_obj.main_win_obj.output_tab_write_stdout( - 1, - 'Update operation finished', - ) - - # Let the timer run for a few more seconds to prevent Gtk errors (for - # systems with Gtk < 3.24) - GObject.timeout_add( - 0, - self.app_obj.update_manager_halt_timer, - ) - - - def intercept_version_from_stdout(self, stdout): - - """Called by self.install_yt_dl() only. - - Check a STDOUT message, hoping to intercept the new youtube-dl version - number. - - Args: - - stdout (str): The STDOUT message - - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('uop 487 intercept_version_from_stdout') - - substring = re.search( - 'Requirement already up\-to\-date.*\(([\d\.]+)\)\s*$', - stdout, - ) - - if substring: - self.ytdl_version = substring.group(1) - - else: - substring = re.search( - 'Successfully installed youtube\-dl\-([\d\.]+)\s*$', - stdout, - ) - - if substring: - self.ytdl_version = substring.group(1) - - - def is_child_process_alive(self): - - """Called by self.install_ffmpeg(), .install_ytdl() and - .stop_update_operation(). - - Based on code from downloads.VideoDownloader.is_child_process_alive(). - - Called continuously during the self.run() 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('uop 524 is_child_process_alive') - - if self.child_process is None: - return False - - return self.child_process.poll() is None - - - def stop_update_operation(self): - - """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(), - .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item(). - - Based on code from downloads.VideoDownloader.stop(). - - Terminates the child process. - """ - - if DEBUG_FUNC_FLAG: - utils.debug_time('uop 543 stop_update_operation') - - 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) diff --git a/tartube/utils.py b/tartube/utils.py deleted file mode 100755 index 4d146c4..0000000 --- a/tartube/utils.py +++ /dev/null @@ -1,1192 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2019-2020 A S Lewis -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -"""Utility functions used by code copied from youtube-dl-gui.""" - - -# Import Gtk modules -from gi.repository import Gtk, Gdk - - -# Import other modules -import datetime -import locale -import math -import os -import re -import requests -import shutil -import subprocess -import sys -import textwrap - - -# Import our modules -import formats -import mainapp -import media - - -# Functions - - -def add_links_to_entry_from_clipboard(app_obj, entry, duplicate_text=None, -drag_drop_text=None, no_modify_flag=None): - - """Called by various functions in mainWin.AddChannelDialogue and - mainwin.AddPlaylistDialogue. - - Function to add valid URLs from the clipboard to a Gtk.Entry, ignoring - anything that is not a valid URL. - - A duplicate URL can be specified, when the dialogue window's clipboard - monitoring is turned on; it prevents this function adding the same URL - that was added the previous time. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - entry (Gtk.Entry): The entry to which valis URLs should be added. - Only the first valid URL is added, replacing any previous contents - (unless the URL matches the specified duplicate - - duplicate_text (str): If specified, ignore the clipboard contents, if - it matches this URL - - drag_drop_text (str): If specified, use this text and ignore the - clipboard - - no_modify_flag (bool): If True, the entry is not updated, instead, - the URL that would have been added to it is merely returned - - Returns: - - The URL added to the entry (or that would have been added to the entry) - or None if no valid and non-duplicate URL was found in the clipboard - - """ - - if drag_drop_text is None: - - # Get text from the system clipboard - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - cliptext = clipboard.wait_for_text() - - else: - - # Ignore the clipboard, and use the specified text - cliptext = drag_drop_text - - # Eliminate empty lines and any lines that are not valid URLs (we assume - # that it's one URL per line) - # Use the first valid line that doesn't match the duplicate (if specified) - if cliptext is not None and cliptext != Gdk.SELECTION_CLIPBOARD: - - for line in cliptext.split('\n'): - if check_url(line): - - line = strip_whitespace(line) - if re.search('\S', line) \ - and (duplicate_text is None or line != duplicate_text): - - if not no_modify_flag: - entry.set_text(line) - - return line - - # No valid and non-duplicate URL found - return None - - -def add_links_to_textview_from_clipboard(app_obj, textview, mark_start=None, -mark_end=None, drag_drop_text=None): - - """Called by mainwin.AddVideoDialogue.__init__(), - .on_window_drag_data_received() and .clipboard_timer_callback(). - - Function to add valid URLs from the clipboard to a Gtk.TextView, ignoring - anything that is not a valid URL, and ignoring duplicate URLs. - - If some text is supplied as an argument, uses that text rather than the - clipboard text - - Args: - - app_obj (mainapp.TartubeApp): The main application - - textview (Gtk.TextBuffer): The textview 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) - - drag_drop_text (str): If specified, use this text and ignore the - clipboard - - """ - - if drag_drop_text is None: - - # Get text from the system clipboard - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - cliptext = clipboard.wait_for_text() - - else: - - # Ignore the clipboard, and use the specified text - cliptext = drag_drop_text - - # Eliminate empty lines and any lines that are not valid URLs (we assume - # that it's one URL per line) - # At the same time, trim initial/final whitespace - valid_list = [] - if cliptext is not None and cliptext != Gdk.SELECTION_CLIPBOARD: - for line in cliptext.split('\n'): - if check_url(line): - - line = strip_whitespace(line) - if re.search('\S', line): - valid_list.append(line) - - if valid_list: - - # Some URLs survived the cull - - # Get the contents of the buffer - 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(), - # 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), - False, - ) - - # Remove any URLs that already exist in the buffer - line_list = buffer_text.split('\n') - mod_list = [] - for line in valid_list: - if not line in line_list: - mod_list.append(line) - - # Add any surviving URLs to the buffer, first adding a newline - # character, if the buffer doesn't end in one - if mod_list: - - if not re.search('\n\s*$', buffer_text) and buffer_text != '': - mod_list[0] = '\n' + mod_list[0] - - textview.insert( - textview.get_end_iter(), - str.join('\n', mod_list) + '\n', - ) - - -def check_url(url): - - """Can be called by anything. - - Checks for valid URLs. - - Args: - - url (str): The URL to check - - Returns: - - True if the URL is valid, False if invalid. - - """ - - prepared_request = requests.models.PreparedRequest() - try: - prepared_request.prepare_url(url, None) - - # The requests module allows a lot of URLs that are definitely not of - # interest to us - # This filter seems to catch most of the gibberish (although it's not - # perfect) - if re.search('^\S+\.\S', url) \ - or re.search('localhost', url): - return True - else: - return False - except: - return False - - -def convert_item(item, to_unicode=False): - - """Can be called by anything. - - Based on the convert_item() function in youtube-dl-gui. - - Convert item between 'unicode' and 'str'. - - Args: - - item (-): Can be any python item - - to_unicode (bool): When True it will convert all the 'str' types to - 'unicode'. When False it will convert all the 'unicode' types back - to 'str' - - Returns: - - The converted item - - """ - - if to_unicode and isinstance(item, str): - # Convert str to unicode - return item.decode(get_encoding(), 'ignore') - - if not to_unicode and isinstance(item, unicode): - # Convert unicode to str - return item.encode(get_encoding(), 'ignore') - - if hasattr(item, '__iter__'): - # Handle iterables - temp_list = [] - - for sub_item in item: - if isinstance(item, dict): - temp_list.append( - ( - convert_item(sub_item, to_unicode), - convert_item(item[sub_item], to_unicode), - ) - ) - else: - temp_list.append(convert_item(sub_item, to_unicode)) - - return type(item)(temp_list) - - return item - - -def convert_path_to_temp(app_obj, old_path, move_flag=False): - - """Can be called by anything. - - Converts a full path to a file that would be stored in Tartube's data - directory (mainapp.TartubeApp.downloads_dir) into the equivalent path in - Tartube's temporary directory (mainapp.TartubeApp.temp_dl_dir). - - Optionally moves a file from one location to the other. - - Regardless of whether the file is moved or not, creates the destination - sub-directory if it doesn't already exist, and deletes the destination file - if it already exists (both of which prevent exceptions being raised). - - Args: - - app_obj (mainapp.TartubeApp): The main application - - old_path (str): Full path to the existing file - - move_flag (bool): If True, the file is actually moved to the new - location - - Returns: - - new_path: The converted full file path - - """ - - data_dir_len = len(app_obj.downloads_dir) - - new_path = app_obj.temp_dl_dir + old_path[data_dir_len:] - new_dir, new_filename = os.path.split(new_path.strip("\"")) - - # The destination folder must exist, before moving files into it - if not os.path.exists(new_dir): - os.makedirs(new_dir) - - # On MS Windows, a file name new_path must not exist, or an exception will - # be raised - if os.path.isfile(new_path): - os.remove(new_path) - - # Move the file now, if the calling code requires that - if move_flag: - - # (On MSWin, can't do os.rename if the destination file already exists) - if os.path.isfile(new_path): - os.remove(new_path) - - # (os.rename sometimes fails on external hard drives; this is safer) - shutil.move(old_path, new_path) - - # Return the converted file path - return new_path - - -def convert_seconds_to_string(seconds, short_flag=False): - - """Can be called by anything. - - Converts a time in seconds into a formatted string. - - Args: - - seconds (int or float): The time to convert - - short_flag (bool): If True, show '05:15' rather than '0:05:15' - - Returns: - - The converted string, e.g. '05:12' or '16:05:12' - - """ - - # Round up fractional seconds - if seconds is not None: - if seconds != int(seconds): - seconds = int(seconds) + 1 - else: - seconds = 1 - - if short_flag and seconds < 3600: - - # When required, show 05:15 rather than 0:05:15 - minutes = int(seconds / 60) - seconds = int(seconds % 60) - - return '{:02d}:{:02d}'.format(minutes, seconds) - - else: - return str(datetime.timedelta(seconds=seconds)) - - -def convert_youtube_to_hooktube(url): - - """Can be called by anything. - - Converts a YouTube weblink to a HookTube weblink (but doesn't modify links - to other sites. - - Args: - - url (str): The weblink to convert - - Returns: - - The converted string - - """ - - if re.search(r'^https?:\/\/(www)+\.youtube\.com', url): - - url = re.sub( - r'youtube\.com', - 'hooktube.com', - url, - # Substitute first occurence only - 1, - ) - - return url - - -def convert_youtube_to_invidious(url): - - """Can be called by anything. - - Converts a YouTube weblink to an Invidious weblink (but doesn't modify - links to other sites. - - Args: - - url (str): The weblink to convert - - Returns: - - The converted string - - """ - - if re.search(r'^https?:\/\/(www)+\.youtube\.com', url): - - url = re.sub( - r'youtube\.com', - 'invidio.us', - url, - # Substitute first occurence only - 1, - ) - - return url - - -def debug_time(msg): - - """Called by all functions in downloads.py, info.py, mainapp.py, - mainwin.py, refresh.py, tidy.py and updates.py. - - Writes the current time, and the name of the calling function to STDOUT, - e.g. '2020-01-16 08:55:06 ap 91 __init__'. - - Args: - - msg (str): The message to write - - """ - - # Uncomment this code to display the time with microseconds -# print(str(datetime.datetime.now().time()) + ' ' + msg) - - # Uncomment this code to display the time without microseconds - dt = datetime.datetime.now() - print(str(dt.replace(microsecond=0)) + ' ' + msg) - - # Uncomment this code to display the message, without a timestamp -# print(msg) - - # This line makes my IDE collapse functions nicely - return - - -def disk_get_free_space(path, bytes_flag=False): - - """Can be called by anything. - - Returns the size of the disk on which a specified file/directory exists, - minus the used space on that disk. - - Args: - - path (str): Path to a file/directory on the disk, typically Tartube's - data directory - - bytes_flag (bool): True to return an integer value in MB, false to - return a value in bytes - - Returns: - - The free space in MB (or in bytes, if the flag is specified), or 0 if - the size can't be calculated for any reason - - """ - - try: - total_bytes, used_bytes, free_bytes = shutil.disk_usage( - os.path.realpath(path), - ) - - if not bytes_flag: - return int(free_bytes / 1000000) - else: - return free_bytes - - except: - return 0 - - -def disk_get_total_space(path, bytes_flag=False): - - """Can be called by anything. - - Returns the size of the disk on which a specified file/directory exists. - - Args: - - path (str): Path to a file/directory on the disk, typically Tartube's - data directory - - bytes_flag (bool): True to return an integer value in MB, false to - return a value in bytes - - Returns: - - The total size in MB (or in bytes, if the flag is specified) - - """ - - total_bytes, used_bytes, free_bytes = shutil.disk_usage( - os.path.realpath(path), - ) - - if not bytes_flag: - return int(total_bytes / 1000000) - else: - return total_bytes - - -def disk_get_used_space(path, bytes_flag=False): - - """Can be called by anything. - - Returns the size of the disk on which a specified file/directory exists, - minus the free space on that disk. - - Args: - - path (str): Path to a file/directory on the disk, typically Tartube's - data directory - - bytes_flag (bool): True to return an integer value in MB, false to - return a value in bytes - - Returns: - - The used space in MB (or in bytes, if the flag is specified) - - """ - - total_bytes, used_bytes, free_bytes = shutil.disk_usage( - os.path.realpath(path), - ) - - if not bytes_flag: - return int(used_bytes / 1000000) - else: - return used_bytes - - -def find_available_name(app_obj, old_name, min_value=2, max_value=9999): - - """Can be called by anything. - - mainapp.TartubeApp.media_name_dict stores the names of all media.Channel, - media.Playlist and media.Folder objects as keys. - - old_name is the name of an existing media data object. This function - slightly modifies the name, converting 'my_name' into 'my_name_N', where N - is the smallest positive integer for which the name is available. - - To preclude any possibility of infinite loops, the function will give up - after max_value attempts. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - old_name (str): The name which is already in use by a media data object - - min_value (str): The first name to try. 2 by default, so the first - name checked will be 'my_name_2' - - max_value (int): When to give up. 9999 by default, meaning that this - function will try everything up to 'my_name_9999' before giving up. - If set to -1, this function never gives up - - Returns: - - None on failure, the new name on success - - """ - - if max_value != -1: - - for n in range (min_value, max_value): - - new_name = old_name + '_' + str(n) - if not new_name in app_obj.media_name_dict: - return new_name - - # Failure - return None - - else: - - # Renaming is essential, for example, in calls from - # mainapp.TartubeApp.load_db(). Keep going indefinitely until an - # available name is found - n = 1 - while 1: - n += 1 - - new_name = old_name + '_' + str(n) - if not new_name in app_obj.media_name_dict: - return new_name - - -def find_thumbnail(app_obj, video_obj, temp_dir_flag=False): - - """Can be called by anything. - - No way to know which image format is used by all websites for their video - thumbnails, so look for the most common ones, and return the path to the - thumbnail file if one is found. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - video_obj (media.Video): The video object handling the downloaded video - - temp_dir_flag (bool): If True, this function will look in Tartube's - temporary data directory, if the thumbnail isn't found in the main - data directory - - Returns: - - path (str): The full path to the thumbnail file, or None - - """ - - for ext in ('.jpg', '.png', '.gif'): - - # Look in Tartube's permanent data directory - path = video_obj.get_actual_path_by_ext(app_obj, ext) - - if os.path.isfile(path): - return path - - elif temp_dir_flag: - - # Look in temporary data directory - data_dir_len = len(app_obj.downloads_dir) - - temp_path = app_obj.temp_dl_dir + path[data_dir_len:] - if os.path.isfile(temp_path): - return temp_path - - return None - - -def format_bytes(num_bytes): - - """Can be called by anything. - - Based on the format_bytes() function in youtube-dl-gui. - - Convert bytes into a formatted string, e.g. '23.5GiB'. - - Args: - - num_bytes (float): The number to convert - - Returns: - - The formatted string - - """ - - if num_bytes == 0.0: - exponent = 0 - else: - exponent = int(math.log(num_bytes, formats.KILO_SIZE)) - - suffix = formats.FILESIZE_METRIC_LIST[exponent] - output_value = num_bytes / (formats.KILO_SIZE ** exponent) - - return "%.2f%s" % (output_value, suffix) - - -def generate_system_cmd(app_obj, media_data_obj, options_list, -dl_sim_flag=False, divert_mode=None): - - """Called by downloads.VideoDownloader.do_download() and - mainwin.SystemCmdDialogue.update_textbuffer(). - - Based on YoutubeDLDownloader._get_cmd(). - - Prepare the system command that instructs youtube-dl to download the - specified media data object. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): The media data object to be downloaded - - options_list (list): A list of download options generated by a call to - options.OptionsParser.parse() - - dl_sim_flag (bool): True if a simulated download is to take place, - False if a real download is to take place - - 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 - source URL points to YouTube should be converted to HookTube or - Invidious (no conversion takes place for channels/playlists/ - folders) - - Returns: - - Python list that contains the system command to execute and its - arguments - - """ - - # Simulate the download, rather than actually downloading videos, if - # required - if dl_sim_flag: - options_list.append('--dump-json') - - # If actually downloading videos, create an archive file so that, if the - # user deletes the videos, youtube-dl won't try to download them again - elif app_obj.allow_ytdl_archive_flag: - - # (Create the archive file in the media data object's 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')), - ) - - # Show verbose output (youtube-dl debugging mode), if required - if app_obj.ytdl_write_verbose_flag: - options_list.append('--verbose') - - # Supply youtube-dl with the path to the ffmpeg/avconv binary, if the - # user has provided one - if app_obj.ffmpeg_path is not None: - options_list.append('--ffmpeg-location') - options_list.append('"' + app_obj.ffmpeg_path + '"') - - # Convert a YouTube URL to HookTube/Invidious, if required - source = media_data_obj.source - if isinstance(media_data_obj, media.Video) and divert_mode: - if divert_mode == 'hooktube': - source = convert_youtube_to_hooktube(source) - elif divert_mode == 'invidious': - source = convert_youtube_to_invidious(source) - - # Convert a path beginning with ~ (not on MS Windows) - ytdl_path = app_obj.ytdl_path - if os.name != 'nt': - ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path) - - # Set the list - cmd_list = [ytdl_path] + options_list + [source] - - return cmd_list - - -def get_encoding(): - - """Called by utils.convert_item(). - - Based on the get_encoding() function in youtube-dl-gui. - - Returns: - - The system encoding. - - """ - - try: - encoding = locale.getpreferredencoding() - 'TEST'.encode(encoding) - except: - encoding = 'UTF-8' - - return encoding - - -def get_options_manager(app_obj, media_data_obj): - - """Can be called by anything. Subsequently called by this function - recursively. - - Fetches the options.OptionsManager which applies to the specified media - data object. - - The media data object might specify its own options.OptionsManager, or - we might have to use the parent's, or the parent's parent's (and so - on). As a last resort, use General Options Manager. - - Args: - - app_obj (mainapp.TartubeApp): The main application - - media_data_obj (media.Video, media.Channel, media.Playlist, - media.Folder): A media data object - - Returns: - - The options.OptionsManager object that applies to the specified - media data object - - """ - - if media_data_obj.options_obj: - return media_data_obj.options_obj - elif media_data_obj.parent_obj: - return get_options_manager(app_obj, media_data_obj.parent_obj) - else: - return app_obj.general_options_obj - - -def is_youtube(url): - - """Can be called by anything. - - Checks whether a link is a YouTube link or not. - - Args: - - url (str): The weblink to check - - Returns: - - True if it's a YouTube link, False if not - - """ - - if re.search(r'^https?:\/\/(www)+\.youtube\.com', url): - return True - else: - return False - - -def open_file(uri): - - """Can be called by anything. - - Opens a file using the system's default software (e.g. open a media file in - the default media player; open a weblink in the default browser). - - Args: - - uri (str): The URI to open - - """ - - if sys.platform == "win32": - os.startfile(uri) - else: - opener ="open" if sys.platform == "darwin" else "xdg-open" - subprocess.call([opener, uri]) - - -def parse_ytdl_options(options_string): - - """Called by options.OptionsParser.parse() or info.InfoManager.run(). - - Parses the 'extra_cmd_string' option, which can contain arguments inside - double quotes "..." (arguments that can therefore contain whitespace) - - Args: - - options_string (str): A string containing various youtube-dl - download options, as described above - - Returns: - - A separated list of youtube-dl download options - - """ - - # Set a flag for an item beginning with double quotes, and reset it for an - # item ending in double quotes - quote_flag = False - # Temporary list to hold such quoted arguments - quote_list = [] - # Add options, one at a time, to a list - return_list = [] - - return_string = '' - for item in options_string.split(): - - quote_flag = (quote_flag or item[0] == "\"") - - if quote_flag: - quote_list.append(item) - else: - return_list.append(item) - - if quote_flag and item[-1] == "\"": - - # Special case mode is over - return_list.append(" ".join(quote_list)[1:-1]) - - quote_flag = False - quote_list = [] - - return return_list - - -def shorten_string(string, num_chars): - - """Can be called by anything. - - If string is longer than num_chars, truncates it and adds an ellipsis. - - Args: - - string (string): The string to convert - - num_chars (int): The maximum length of the desired string - - Returns: - - The converted string - - """ - - if string and len(string) > num_chars: - num_chars -= 3 - string = string[:num_chars] + '...' - - return string - - -def strip_whitespace(string): - - """Can be called by anything. - - Removes any leading/trailing whitespace from a string. - - Args: - - string (str): The string to convert - - Returns: - - The converted string - - """ - - if string: - string = re.sub(r'^\s+', '', string) - string = re.sub(r'\s+$', '', string) - - return string - - -def tidy_up_container_name(string, max_length): - - """Called by mainapp.TartubeApp.on_menu_add_channel(), - .on_menu_add_playlist() and .on_menu_add_folder(). - - Before creating a channel, playlist or folder, tidies up the name. - - Removes any leading/trailing whitespace. Reduces multiple whitespace - characters to a single space character. Applies a maximum length. - - Also replaces any forward/backward slashes with hyphens (if the user - specifies a name like 'Foo / Bar', that would create a directory on the - filesystem called .../Foo/Bar, which is definitely not what we want). - - Args: - - string (str): The string to convert - - max_length (int): The maximum length of the converted string (should be - mainapp.TartubeApp.container_name_max_len) - - Returns: - - The converted string - - """ - - if string: - - string = re.sub(r'^\s+', '', string) - string = re.sub(r'\s+$', '', string) - string = re.sub(r'\s+', ' ', string) - string = re.sub(r'[\/\\]', '-', string) - - return string[0:max_length] - - else: - - # Empty string - return string - - -def tidy_up_long_descrip(string, max_length=80): - - """Can be called by anything. - - A modified version of utils.tidy_up_long_string. In this case, the - specified string can contain any number of newline characters. We begin - by splitting that string into a list of lines. - - Then we split any line which is longer than the specified maximum length, - which gives us a (possibly longer) list of lines. - - Finally we recombine those lines into a single string, with lines joined by - newline characters. - - Args: - - string (str): The string to convert - - max_length (int): The maximum length of lines, before they are - recombined into a single string - - Returns: - - The converted string - - """ - - if string: - - line_list = [] - - for line in string.split('\n'): - - if line == '': - # Preserve empty lines - line_list.append('') - - else: - - new_list = textwrap.wrap( - line, - width=max_length, - # Don't split up URLs - break_long_words=False, - break_on_hyphens=False, - ) - - for mini_line in new_list: - line_list.append(mini_line) - - return '\n'.join(line_list) - - else: - - # Empty string - return string - - -def tidy_up_long_string(string, max_length=80, reduce_flag=True, -split_words_flag=False): - - """Can be called by anything. - - The specified string can contain any number of newline characters. - - Replaces newline characters with a single space character. - - Optionally reduces multiple whitespace characters and removes initial/ - final whitespace character(s). - - Then splits the string into a list of lines, each with the specified - maximum length. - - Finally recombines those lines into a single string, with lines joined by - newline characters. - - Args: - - string (str): The string to convert - - max_length (int): The maximum length of lines, before they are - recombined into a single string - - reduce_flag (bool): If True, initial and final whitespace is removed, - and multiple successive whitespace characters are reduced to a - single space character - - split_words_flag(bool): If True, the function will break words - (including hyphenated words) into smaller pieces, if necessary - - Returns: - - The converted string - - """ - - if string: - - string = re.sub(r'\r\n', ' ', string) - - if reduce_flag: - string = re.sub(r'^\s+', '', string) - string = re.sub(r'\s+$', '', string) - string = re.sub(r'\s+', ' ', string) - - line_list = [] - for line in string.split('\n'): - - if line == '': - # Preserve empty lines - line_list.append('') - - else: - new_list = textwrap.wrap( - line, - width=max_length, - # Don't split up URLs by default - break_long_words=split_words_flag, - break_on_hyphens=split_words_flag, - ) - - for mini_line in new_list: - line_list.append(mini_line) - - return '\n'.join(line_list) - - else: - - # Empty string - return string - - -def to_string(data): - - """Can be called by anything. - - Convert any data type to a string. - - Args: - - data (-): The data type - - Returns: - - The converted string - - """ - - return '%s' % data - - -def upper_case_first(string): - - """Can be called by anything. - - Args: - - string (str): The string to capitalise - - Returns: - - The converted string - - """ - - return string[0].upper() + string[1:] diff --git a/tartube/xdg_tartube.py b/tartube/xdg_tartube.py deleted file mode 100644 index 51e8ff4..0000000 --- a/tartube/xdg_tartube.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright © 2016-2019 Scott Stevenson -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all -# copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL -# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE -# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL -# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR -# PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -# -# Tartube v2.0.0 -# Imported into Tartube and renamed, so the Debian/RPM packagers don't confuse -# it with the standard xdg module. - -"""XDG Base Directory Specification variables. - -XDG_CACHE_HOME, XDG_CONFIG_HOME, and XDG_DATA_HOME are pathlib.Path -objects containing the value of the environment variable of the same -name, or the default defined in the specification if the environment -variable is unset or empty. - -XDG_CONFIG_DIRS and XDG_DATA_DIRS are lists of pathlib.Path objects -containing the value of the environment variable of the same name split -on colons, or the default defined in the specification if the -environment variable is unset or empty. - -XDG_RUNTIME_DIR is a pathlib.Path object containing the value of the -environment variable of the same name, or None if the environment -variable is not set. - -""" - -import os -from pathlib import Path -from typing import List, Optional - -__all__ = [ - "XDG_CACHE_HOME", - "XDG_CONFIG_DIRS", - "XDG_CONFIG_HOME", - "XDG_DATA_DIRS", - "XDG_DATA_HOME", - "XDG_RUNTIME_DIR", -] - -HOME = Path(os.path.expandvars("$HOME")) - - -def _path_from_env(variable: str, default: Path) -> Path: - """Read an environment variable as a path. - - The environment variable with the specified name is read, and its - value returned as a path. If the environment variable is not set, or - set to the empty string, the default value is returned. - - Parameters - ---------- - variable : str - Name of the environment variable. - default : Path - Default value. - - Returns - ------- - Path - Value from environment or default. - - """ - # TODO(srstevenson): Use assignment expression in Python 3.8. - value = os.environ.get(variable) - if value: - return Path(value) - return default - - -def _paths_from_env(variable: str, default: List[Path]) -> List[Path]: - """Read an environment variable as a list of paths. - - The environment variable with the specified name is read, and its - value split on colons and returned as a list of paths. If the - environment variable is not set, or set to the empty string, the - default value is returned. - - Parameters - ---------- - variable : str - Name of the environment variable. - default : List[Path] - Default value. - - Returns - ------- - List[Path] - Value from environment or default. - - """ - # TODO(srstevenson): Use assignment expression in Python 3.8. - value = os.environ.get(variable) - if value: - return [Path(path) for path in value.split(":")] - return default - - -XDG_CACHE_HOME = _path_from_env("XDG_CACHE_HOME", HOME / ".cache") - -XDG_CONFIG_DIRS = _paths_from_env("XDG_CONFIG_DIRS", [Path("/etc/xdg")]) - -XDG_CONFIG_HOME = _path_from_env("XDG_CONFIG_HOME", HOME / ".config") - -XDG_DATA_DIRS = _paths_from_env( - "XDG_DATA_DIRS", - [Path(path) for path in "/usr/local/share/:/usr/share/".split(":")], -) - -XDG_DATA_HOME = _path_from_env("XDG_DATA_HOME", HOME / ".local" / "share") - -try: - XDG_RUNTIME_DIR: Optional[Path] = Path(os.environ["XDG_RUNTIME_DIR"]) -except KeyError: - XDG_RUNTIME_DIR = None diff --git a/tartube_mswin.sh b/tartube_mswin.sh deleted file mode 100755 index 1723d9f..0000000 --- a/tartube_mswin.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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