Add files via upload

master
A S Lewis 2019-05-28 14:48:25 +01:00 committed by GitHub
parent cdaa2ba53c
commit c1916c395a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1247 additions and 371 deletions

254
README.rst Normal file
View File

@ -0,0 +1,254 @@
Tartube
=======
Tartube is a GUI front-end for `youtube-dl <https://youtube-dl.org/>`__,
partly based on
`youtube-dl-gui <https://mrs0m30n3.github.io/youtube-dl-gui/>`__ and
written in Python 3 / Gtk 3.
Tartube is **alpha software**. It crashes a lot. If you find this
frustrating, find a solution and then `send it to
me <https://github.com/axcore/tartube/issues>`__.
Why should I use Tartube?
-------------------------
- You can download individual videos, and even whole channels and
playlists, from hundreds of different websites
- You can fetch information about those videos, channels and playlists,
without actually downloading anything
- Tartube will organise your videos into convenient folders
- Certain popular video websites manipulate search results, repeatedly
unsubscribe people from their favourite channels and/or deliberately
conceal videos which challenge the Californian political consensus.
Tartube won't do any of those things
- Tartube can, in some circumstances, see videos that are
region-blocked and age-restricted
Requirements
------------
- A working installation of `youtube-dl <https://youtube-dl.org/>`__
- `Python 3+ <https://www.python.org/downloads>`__
- `Gtk 3+ <https://python-gtk-3-tutorial.readthedocs.io/en/latest/>`__
- `Python validators module <https://pypi.org/project/validators/>`__
optional, but recommended
- `Python moviepy module <https://pypi.org/project/moviepy/>`__
optional
Downloads
---------
- `Source <http://tartube.sourceforge.io/>`__ from sourceforge.io
- `Source <https://github.com/axcore/tarbue>`__ from github
Installation
------------
Install from source
~~~~~~~~~~~~~~~~~~~
1. Download & extract the source
2. Change directory into the Tartube directory
3. Run ``python setup.py install``
Install using PyPI
~~~~~~~~~~~~~~~~~~
1. Run ``pip install tartube``
Install using MS Windows Installer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There is no installer for MS Windows yet.
Run without installing
~~~~~~~~~~~~~~~~~~~~~~
1. Download & extract the source
2. Change directory into the Tartube directory
3. Run 'python tartube.py'
Frequently-Asked Questions
--------------------------
**Q: I can't install Tartube!**
A: I have no experience of writing in Python and I'm still working it
out for myself. Contact me on the `Github
issues <https://github.com/axcore/tartube/issues>`__ page if you can do
better than me.
**Q: I can't run Tartube!**
A: See above.
**Q: Tartube doesn't work properly!**
A: See above.
**Q: Tartube keeps crashing!**
A: See above.
**Q: How do I use Tartube?**
A: Assuming that Tartube is installed and running correctly, then you
should start by checking that youtube-dl is also installed and running
correctly.
- Click **Operations > Update youtube-dl**
There are several locations on your filesystem where youtube-dl might
have been installed. If the update operation fails, you should try
modifying Tartube's settings.
- Click **Edit > System preferences...**
- Click the **youtube-dl** tab
- Try changing the setting **'Actual path to use during
download/update/refresh operations'**
- Try changing the setting **'Shell command for update operations'**
- Try the update operation again
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.
When you start Tartube, there are five folders already visible. You
can't remove any of these folders (but you can hide them, if you want).
Videos saved to the 'Temporary Videos' folder are deleted when Tartube
shuts down.
Once you've finished adding videos, channels, playlists and folders,
there are four things Tartube can do:
- **'Check'** - Fetch information about videos, but don't download them
- **'Download'** - Actually download the videos. If you have disabled
downloads for a particular item, Tartube will just fetch information
about it instead
- **'Update'** - Updates youtube-dl, as described above
- **'Refresh'** - Examines your filesystem. If you have manually copied
any videos into Tartube's data directory, those videos are added to
Tartube's database
**Protip:** Do an **'Update'** operation before you do a **'Check'** or
**'Download'** operation
**Protip:** Do a **'Check'** operation before you do **'Refresh'**
operation
youtube-dl offers a large number of download options. This is how to set
them.
- Click **Edit > General download options...**
Any changes you make in the new window aren't actually applied until you
click the **'Apply'** or **'OK'** buttons.
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.
For example, suppose you have added these folders and channels:
::
Comedy folder
CollegeHumor channel
PewDiePie channel
Politics folder
Liberal folder
The Young Turks channel
Conservative folder
Joe Rogan channel
Mark Dice channel
The general download options apply to all of these channels. Now,
suppose you apply some download options to the Politics folder:
- Right-click the folder, and select **Apply download options...**
Tartube's database now looks something like this:
::
Comedy folder
CollegeHumor channel
PewDiePie channel
++Politics folder
Liberal folder
The Young Turks channel
Conservative folder
Joe Rogan channel
Mark Dice channel
The new download options (marked ++) apply to *everything* inside the
Politics folder - The Young Turks, Joe Rogan and Mark Dice.
Now, suppose you add another set of download options (marked @@) to the
Conservative folder:
::
Comedy folder
CollegeHumor channel
PewDiePie channel
**Politics folder
Liberal folder
The Young Turks channel
@@Conservative folder
Joe Rogan channel
Mark Dice channel
These new download options only apply to Joe Rogan and Mark Dice. They
don't apply to The Young Turks, which are still using the *previous* set
of download options. They don't apply to CollegeHumor or PewDiePie,
which are still using the *default* download options.
Future plans
------------
- Fix the endless crashes, somehow
- Support for multiple databases (so you can store videos on two
external hard drives at the same time)
- Add download scheduling
- Add video archiving
- Allow selection of multiple videos in the catalogue, so the same
action can be applied to all of them at the same time
- Tie channels and playlists together, so that they won't both download
the same video
- Add tooltips for everything
- Add more youtube-dl options
Known issues
------------
- Tartube crashes continuously and often
- Alphabetic sorting of channels/playlists/folders doesn't always work
as intended, due to an unresolved Gtk issue
- Channels/playlists/folder selection does not always work as intended,
due to an unresolved Gtk issue
- Users can type in comboboxes, but this should not be possible
Contributing
------------
- Report a bug: Use the Github
`issues <https://github.com/axcore/tartube/issues>`__ page
Authors
-------
See the `AUTHORS <AUTHORS>`__ file.
License
-------
Tartube is licensed under the `GNU General Public License
v3.0 <https://www.gnu.org/licenses/gpl-3.0.en.html>`__.
✨🍰✨

View File

@ -1 +1 @@
name = "tartube"

1
docs/empty.md Normal file
View File

@ -0,0 +1 @@
#Tartube

View File

@ -31,11 +31,11 @@ import os
# Import our modules # Import our modules
import constants
import __main__ import __main__
import mainapp from . import constants
import media from . import mainapp
import utils from . import media
from . import utils
# Classes # Classes
@ -179,7 +179,7 @@ class GenericConfigWin(Gtk.Window):
also_self (an object inheriting from config.GenericConfigWin): also_self (an object inheriting from config.GenericConfigWin):
another copy of self another copy of self
""" """
self.app_obj.main_win_obj.del_child_window(self) self.app_obj.main_win_obj.del_child_window(self)
@ -825,9 +825,9 @@ class GenericEditWin(GenericConfigWin):
showing their original values. showing their original values.
Args: Args:
button (Gtk.Button): The widget clicked button (Gtk.Button): The widget clicked
""" """
# Destroy the window # Destroy the window
@ -841,7 +841,7 @@ class GenericEditWin(GenericConfigWin):
Destroys any changes made by the user and then closes the window. Destroys any changes made by the user and then closes the window.
Args: Args:
button (Gtk.Button): The widget clicked button (Gtk.Button): The widget clicked
""" """
@ -861,7 +861,7 @@ class GenericEditWin(GenericConfigWin):
showing their original values. showing their original values.
Args: Args:
button (Gtk.Button): The widget clicked button (Gtk.Button): The widget clicked
""" """
@ -891,11 +891,11 @@ class GenericEditWin(GenericConfigWin):
selected, False if not. selected, False if not.
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
prop (string): The attribute in self.edit_obj to modify prop (string): The attribute in self.edit_obj to modify
""" """
if not checkbutton.get_active(): if not checkbutton.get_active():
@ -911,11 +911,11 @@ class GenericEditWin(GenericConfigWin):
Temporarily stores the contents of the widget in self.edit_dict. Temporarily stores the contents of the widget in self.edit_dict.
Args: Args:
combo (Gtk.ComboBox): The widget clicked combo (Gtk.ComboBox): The widget clicked
prop (string): The attribute in self.edit_obj to modify prop (string): The attribute in self.edit_obj to modify
""" """
tree_iter = combo.get_active_iter() tree_iter = combo.get_active_iter()
@ -931,11 +931,11 @@ class GenericEditWin(GenericConfigWin):
value, and stores the later in self.edit_dict. value, and stores the later in self.edit_dict.
Args: Args:
combo (Gtk.ComboBox): The widget clicked combo (Gtk.ComboBox): The widget clicked
prop (string): The attribute in self.edit_obj to modify prop (string): The attribute in self.edit_obj to modify
""" """
tree_iter = combo.get_active_iter() tree_iter = combo.get_active_iter()
@ -950,11 +950,11 @@ class GenericEditWin(GenericConfigWin):
Temporarily stores the contents of the widget in self.edit_dict. Temporarily stores the contents of the widget in self.edit_dict.
Args: Args:
entry (Gtk.Entry): The widget clicked entry (Gtk.Entry): The widget clicked
prop (string): The attribute in self.edit_obj to modify prop (string): The attribute in self.edit_obj to modify
""" """
self.edit_dict[prop] = entry.get_text() self.edit_dict[prop] = entry.get_text()
@ -968,13 +968,13 @@ class GenericEditWin(GenericConfigWin):
(from those in the group) is the selected one. (from those in the group) is the selected one.
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
prop (string): The attribute in self.edit_obj to modify prop (string): The attribute in self.edit_obj to modify
value (-): The attribute's new value value (-): The attribute's new value
""" """
if radiobutton.get_active(): if radiobutton.get_active():
@ -988,11 +988,11 @@ class GenericEditWin(GenericConfigWin):
Temporarily stores the contents of the widget in self.edit_dict. Temporarily stores the contents of the widget in self.edit_dict.
Args: Args:
spinbutton (Gtk.SpinkButton): The widget clicked spinbutton (Gtk.SpinkButton): The widget clicked
prop (string): The attribute in self.edit_obj to modify prop (string): The attribute in self.edit_obj to modify
""" """
self.edit_dict[prop] = int(spinbutton.get_value()) self.edit_dict[prop] = int(spinbutton.get_value())
@ -1005,11 +1005,11 @@ class GenericEditWin(GenericConfigWin):
Temporarily stores the contents of the widget in self.edit_dict. Temporarily stores the contents of the widget in self.edit_dict.
Args: Args:
textbuffer (Gtk.TextBuffer): The widget modified textbuffer (Gtk.TextBuffer): The widget modified
prop (string): The attribute in self.edit_obj to modify prop (string): The attribute in self.edit_obj to modify
""" """
self.edit_dict[prop] = textbuffer.get_text( self.edit_dict[prop] = textbuffer.get_text(
@ -1030,9 +1030,9 @@ class GenericEditWin(GenericConfigWin):
Apply download options to the media data object. Apply download options to the media data object.
Args: Args:
button (Gtk.Button): The widget clicked button (Gtk.Button): The widget clicked
""" """
if self.edit_obj.options_obj: if self.edit_obj.options_obj:
@ -1056,9 +1056,9 @@ class GenericEditWin(GenericConfigWin):
Edit download options for the media data object. Edit download options for the media data object.
Args: Args:
button (Gtk.Button): The widget clicked button (Gtk.Button): The widget clicked
""" """
if not self.edit_obj.options_obj: if not self.edit_obj.options_obj:
@ -1082,9 +1082,9 @@ class GenericEditWin(GenericConfigWin):
Remove download options from the media data object. Remove download options from the media data object.
Args: Args:
button (Gtk.Button): The widget clicked button (Gtk.Button): The widget clicked
""" """
if not self.edit_obj.options_obj: if not self.edit_obj.options_obj:
@ -1567,7 +1567,7 @@ class OptionsEditWin(GenericEditWin):
Returns: Returns:
The original or modified value of that attribute. The original or modified value of that attribute.
""" """
if name in self.edit_dict: if name in self.edit_dict:
@ -2452,7 +2452,7 @@ class OptionsEditWin(GenericEditWin):
button (Gtk.Button): The widget clicked button (Gtk.Button): The widget clicked
entry (Gtk.Entry): Another widget to be modified by this function entry (Gtk.Entry): Another widget to be modified by this function
combo (Gtk.ComboBox): Another widget to be modified by this combo (Gtk.ComboBox): Another widget to be modified by this
function function
@ -2489,7 +2489,7 @@ class OptionsEditWin(GenericEditWin):
combo (Gtk.ComboBox): The widget clicked combo (Gtk.ComboBox): The widget clicked
entry (Gtk.Entry): Another widget to be modified by this function entry (Gtk.Entry): Another widget to be modified by this function
""" """
tree_iter = combo.get_active_iter() tree_iter = combo.get_active_iter()
@ -2514,7 +2514,7 @@ class OptionsEditWin(GenericEditWin):
Args: Args:
entry (Gtk.Entry): The widget clicked entry (Gtk.Entry): The widget clicked
""" """
# Only set 'output_template' when option 3 is selected, which is when # Only set 'output_template' when option 3 is selected, which is when
@ -2540,7 +2540,7 @@ class OptionsEditWin(GenericEditWin):
other_liststore (Gtk.ListStore): Another widget to be modified by other_liststore (Gtk.ListStore): Another widget to be modified by
this function this function
""" """
selection = treeview.get_selection() selection = treeview.get_selection()
@ -2592,7 +2592,7 @@ class OptionsEditWin(GenericEditWin):
treeview (Gtk.TreeView): Another widget to be modified by this treeview (Gtk.TreeView): Another widget to be modified by this
function function
""" """
selection = treeview.get_selection() selection = treeview.get_selection()
@ -2647,11 +2647,11 @@ class OptionsEditWin(GenericEditWin):
add_button, up_button, down_button (Gtk.Button): Other widgets to add_button, up_button, down_button (Gtk.Button): Other widgets to
be modified by this function be modified by this function
treeview (Gtk.TreeView): Another widget to be modified by this treeview (Gtk.TreeView): Another widget to be modified by this
function function
""" """
selection = treeview.get_selection() selection = treeview.get_selection()
@ -2702,7 +2702,7 @@ class OptionsEditWin(GenericEditWin):
treeview (Gtk.TreeView): Another widget to be modified by this treeview (Gtk.TreeView): Another widget to be modified by this
function function
""" """
selection = treeview.get_selection() selection = treeview.get_selection()
@ -2758,7 +2758,7 @@ class OptionsEditWin(GenericEditWin):
function function
prop (string): The attribute in self.edit_obj to modify prop (string): The attribute in self.edit_obj to modify
""" """
if radiobutton.get_active(): if radiobutton.get_active():
@ -4361,7 +4361,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
other_flag = self.app_obj.main_win_obj.checkbutton2.get_active() other_flag = self.app_obj.main_win_obj.checkbutton2.get_active()
@ -4398,7 +4398,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
redraw_flag = False redraw_flag = False
@ -4430,7 +4430,7 @@ class SystemPrefWin(GenericPrefWin):
button (Gtk.Button): The widget clicked button (Gtk.Button): The widget clicked
entry (Gtk.Entry): Another widget to be modified by this function entry (Gtk.Entry): Another widget to be modified by this function
""" """
dialogue_win = Gtk.FileChooserDialog( dialogue_win = Gtk.FileChooserDialog(
@ -4488,7 +4488,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
radiobutton (Gtk.RadioButton): The widget clicked radiobutton (Gtk.RadioButton): The widget clicked
""" """
default_val = self.app_obj.match_default_chars default_val = self.app_obj.match_default_chars
@ -4526,7 +4526,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
spinbutton (Gtk.SpinButton): The widget clicked spinbutton (Gtk.SpinButton): The widget clicked
""" """
if spinbutton == self.spinbutton: if spinbutton == self.spinbutton:
@ -4544,7 +4544,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
if checkbutton.get_active() \ if checkbutton.get_active() \
@ -4565,7 +4565,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
if checkbutton.get_active() and not self.app_obj.operation_save_flag: if checkbutton.get_active() and not self.app_obj.operation_save_flag:
@ -4584,7 +4584,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
if checkbutton.get_active() \ if checkbutton.get_active() \
@ -4605,7 +4605,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
if checkbutton.get_active() \ if checkbutton.get_active() \
@ -4615,7 +4615,7 @@ class SystemPrefWin(GenericPrefWin):
and self.app_obj.ytdl_write_stdout_flag: and self.app_obj.ytdl_write_stdout_flag:
self.app_obj.set_ytdl_write_stdout_flag(False) self.app_obj.set_ytdl_write_stdout_flag(False)
def on_update_combo_changed(self, combo): def on_update_combo_changed(self, combo):
"""Called from a callback in self.setup_ytdl_tab(). """Called from a callback in self.setup_ytdl_tab().
@ -4626,7 +4626,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
combo (Gtk.ComboBox): The widget clicked combo (Gtk.ComboBox): The widget clicked
""" """
tree_iter = combo.get_active_iter() tree_iter = combo.get_active_iter()
@ -4643,7 +4643,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
if checkbutton.get_active() \ if checkbutton.get_active() \
@ -4663,7 +4663,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
if checkbutton.get_active() \ if checkbutton.get_active() \
@ -4685,7 +4685,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
checkbutton (Gtk.CheckButton): The widget clicked checkbutton (Gtk.CheckButton): The widget clicked
""" """
other_flag = self.app_obj.main_win_obj.checkbutton.get_active() other_flag = self.app_obj.main_win_obj.checkbutton.get_active()
@ -4707,7 +4707,7 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
spinbutton (Gtk.SpinButton): The widget clicked spinbutton (Gtk.SpinButton): The widget clicked
""" """
self.app_obj.main_win_obj.spinbutton.set_value(spinbutton.get_value()) self.app_obj.main_win_obj.spinbutton.set_value(spinbutton.get_value())
@ -4723,10 +4723,10 @@ class SystemPrefWin(GenericPrefWin):
Args: Args:
combo (Gtk.ComboBox): The widget clicked combo (Gtk.ComboBox): The widget clicked
""" """
tree_iter = combo.get_active_iter() tree_iter = combo.get_active_iter()
model = combo.get_model() model = combo.get_model()
self.app_obj.set_ytdl_path(model[tree_iter][1]) self.app_obj.set_ytdl_path(model[tree_iter][1])

View File

@ -49,8 +49,8 @@ FILESIZE_METRIC_LIST = [
# Main stages of the download operation # Main stages of the download operation
MAIN_STAGE_QUEUED = 'Queued' MAIN_STAGE_QUEUED = 'Queued'
MAIN_STAGE_ACTIVE = 'Active' MAIN_STAGE_ACTIVE = 'Active'
MAIN_STAGE_PAUSED = 'Paused' MAIN_STAGE_PAUSED = 'Paused' # (not actually used)
MAIN_STAGE_COMPLETED = 'Completed' MAIN_STAGE_COMPLETED = 'Completed' # (not actually used)
MAIN_STAGE_ERROR = 'Error' MAIN_STAGE_ERROR = 'Error'
# Sub-stages of the 'Active' stage # Sub-stages of the 'Active' stage
ACTIVE_STAGE_PRE_PROCESS = 'Pre-processing' ACTIVE_STAGE_PRE_PROCESS = 'Pre-processing'

View File

@ -31,7 +31,7 @@ import datetime
import json import json
import signal import signal
import os import os
import Queue import queue
import requests import requests
import subprocess import subprocess
import sys import sys
@ -40,16 +40,11 @@ import time
# Import our modules # Import our modules
from constants import MAIN_STAGE_QUEUED, MAIN_STAGE_ACTIVE, \ from . import constants
MAIN_STAGE_PAUSED, MAIN_STAGE_COMPLETED, MAIN_STAGE_ERROR, \ from . import mainapp
ACTIVE_STAGE_PRE_PROCESS, ACTIVE_STAGE_DOWNLOAD, ACTIVE_STAGE_POST_PROCESS, \ from . import media
ACTIVE_STAGE_CHECKING, COMPLETED_STAGE_FINISHED, COMPLETED_STAGE_WARNING, \ from . import options
COMPLETED_STAGE_ALREADY, ERROR_STAGE_ERROR, ERROR_STAGE_STOPPED, \ from . import utils
ERROR_STAGE_ABORT
import mainapp
import media
import options
import utils
# Decorator to add thread synchronisation to some functions in the # Decorator to add thread synchronisation to some functions in the
@ -184,8 +179,8 @@ class DownloadManager(threading.Thread):
download_item_obj = self.download_list_obj.fetch_next_item() download_item_obj = self.download_list_obj.fetch_next_item()
# Exit this loop when there are no more downloads.DownloadItem # Exit this loop when there are no more downloads.DownloadItem
# objects whose .status is MAIN_STAGE_QUEUED, and when all # objects whose .status is constants.MAIN_STAGE_QUEUED, and when
# workers have finished their downloads # all workers have finished their downloads
# Otherwise, wait for an available downloads.DownloadWorker, and # Otherwise, wait for an available downloads.DownloadWorker, and
# then assign the next downloads.DownloadItem to it # then assign the next downloads.DownloadItem to it
if not download_item_obj: if not download_item_obj:
@ -210,7 +205,7 @@ class DownloadManager(threading.Thread):
# downloads.DownloadItem # downloads.DownloadItem
self.download_list_obj.change_item_stage( self.download_list_obj.change_item_stage(
download_item_obj.dbid, download_item_obj.dbid,
MAIN_STAGE_ACTIVE, constants.MAIN_STAGE_ACTIVE,
) )
# Update the main window's progress bar # Update the main window's progress bar
self.job_count += 1 self.job_count += 1
@ -324,7 +319,7 @@ class DownloadManager(threading.Thread):
True if all downloads.DownloadWorker objects have finished their True if all downloads.DownloadWorker objects have finished their
jobs, otherwise returns False jobs, otherwise returns False
""" """
for worker_obj in self.worker_list: for worker_obj in self.worker_list:
@ -344,7 +339,7 @@ class DownloadManager(threading.Thread):
The first available downloads.DownloadWorker, or None if there are The first available downloads.DownloadWorker, or None if there are
no available workers. no available workers.
""" """
for worker_obj in self.worker_list: for worker_obj in self.worker_list:
@ -557,7 +552,7 @@ class DownloadWorker(threading.Thread):
download_item_obj (downloads.DownloadItem): The download item download_item_obj (downloads.DownloadItem): The download item
object describing the URL from which youtube-dl should download object describing the URL from which youtube-dl should download
video(s). video(s).
""" """
self.download_item_obj = download_item_obj self.download_item_obj = download_item_obj
@ -712,7 +707,7 @@ class DownloadList(object):
dbid (int): The specified item's .dbid dbid (int): The specified item's .dbid
new_stage: The new download stage, one of the values imported from new_stage: The new download stage, one of the values imported from
constants.py (e.g. MAIN_STAGE_QUEUED) constants.py (e.g. constants.MAIN_STAGE_QUEUED)
""" """
@ -810,14 +805,14 @@ class DownloadList(object):
The next downloads.DownloadItem object, or None if there are none The next downloads.DownloadItem object, or None if there are none
left. left.
""" """
for dbid in self.download_item_list: for dbid in self.download_item_list:
this_item = self.download_item_dict[dbid] this_item = self.download_item_dict[dbid]
# Don't return an item that's marked as MAIN_STAGE_ACTIVE # Don't return an item that's marked as constants.MAIN_STAGE_ACTIVE
if this_item.stage == MAIN_STAGE_QUEUED: if this_item.stage == constants.MAIN_STAGE_QUEUED:
return this_item return this_item
return None return None
@ -895,7 +890,7 @@ class DownloadItem(object):
# A unique ID for this object # A unique ID for this object
self.dbid = dbid self.dbid = dbid
# The current download stage # The current download stage
self.stage = MAIN_STAGE_QUEUED self.stage = constants.MAIN_STAGE_QUEUED
class VideoDownloader(object): class VideoDownloader(object):
@ -984,8 +979,8 @@ class VideoDownloader(object):
# This object reads from the child process STDOUT and STDERR in an # This object reads from the child process STDOUT and STDERR in an
# asynchronous way # asynchronous way
# Standard Python synchronised queue classes # Standard Python synchronised queue classes
self.stdout_queue = Queue.Queue() self.stdout_queue = queue.Queue()
self.stderr_queue = Queue.Queue() self.stderr_queue = queue.Queue()
# The downloads.PipeReader objects created to handle reading from the # The downloads.PipeReader objects created to handle reading from the
# pipes # pipes
self.stdout_reader = PipeReader(self.stdout_queue) self.stdout_reader = PipeReader(self.stdout_queue)
@ -1064,7 +1059,7 @@ class VideoDownloader(object):
# Public class methods # Public class methods
def do_download(self): def OLDdo_download(self):
"""Called by downloads.DownloadWorker.run(). """Called by downloads.DownloadWorker.run().
@ -1123,8 +1118,9 @@ class VideoDownloader(object):
# standard format, specified in the comments for # standard format, specified in the comments for
# self.extract_stdout_data() # self.extract_stdout_data()
dl_stat_dict = self.extract_stdout_data(stdout) dl_stat_dict = self.extract_stdout_data(stdout)
# If the job's status is COMPLETED_STAGE_ALREADY or # If the job's status is constants.COMPLETED_STAGE_ALREADY
# ERROR_STAGE_ABORT, set our self.return_code IV # or constants.ERROR_STAGE_ABORT, set our
# self.return_code IV
self.extract_stdout_status(dl_stat_dict) self.extract_stdout_status(dl_stat_dict)
# Pass the dictionary on to self.download_worker_obj so the # Pass the dictionary on to self.download_worker_obj so the
# main window can be updated # main window can be updated
@ -1183,6 +1179,130 @@ class VideoDownloader(object):
# Pass the result back to the parent downloads.DownloadWorker object # Pass the result back to the parent downloads.DownloadWorker object
return self.return_code return self.return_code
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)
"""
# 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()
# Prepare a system command...
cmd_list = self.get_system_cmd()
# ...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():
# Read from the child process STDOUT, and convert into unicode for
# Python's convenience
while not self.stdout_queue.empty():
# # (Convert Python2 to Python3)
# stdout = self.stdout_queue.get_nowait().rstrip()
# stdout = utils.convert_item(stdout, to_unicode=True)
stdout = self.stdout_queue.get_nowait().rstrip().decode('utf-8')
if stdout:
# 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 constants.COMPLETED_STAGE_ALREADY
# or constants.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)
if (app_obj.ytdl_write_stdout_flag):
print(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
# # (Convert Python2 to Python3)
# stderr = self.stderr_queue.get_nowait().rstrip()
# stderr = utils.convert_item(stderr, to_unicode=True)
stderr = self.stderr_queue.get_nowait().rstrip().decode('utf-8')
if self.is_warning(stderr):
self.set_return_code(self.WARNING)
self.download_item_obj.media_data_obj.set_warning(stderr)
else:
self.set_return_code(self.ERROR)
self.download_item_obj.media_data_obj.set_error(stderr)
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 cant 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)
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 close(self): def close(self):
@ -1315,7 +1435,7 @@ class VideoDownloader(object):
) )
def confirm_sim_video(self, json_dict): def OLDconfirm_sim_video(self, json_dict):
"""Called by self.extract_stdout_data(). """Called by self.extract_stdout_data().
@ -1515,6 +1635,208 @@ class VideoDownloader(object):
# Update the main window # Update the main window
app_obj.announce_video_download(self.download_item_obj, video_obj) app_obj.announce_video_download(self.download_item_obj, video_obj)
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
"""
# IMport the main application (for convenience)
app_obj = self.download_manager_obj.app_obj
# 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:
return app_obj.system_error(
302,
'Missing filename in JSON data',
)
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.strftime('%s')
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 isinstance(media_data_obj, media.Video):
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:
# No matching media.Video object found, so create a new one
video_obj = app_obj.create_video_from_download(
self.download_item_obj,
path,
filename,
extension,
)
# Update its IVs with the JSON information we extracted
if name is not None:
video_obj.set_name(name)
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.long_string_max_len,
)
# Only save the playlist index when this video is actually stored
# inside a media.Playlist object
if isinstance(video_obj.parent_obj, media.Playlist) \
and playlist_index is not None:
video_obj.set_index(playlist_index)
else:
# If the 'Add videos' button was used, the path/filename/extension
# won't be set yet
if not video_obj.file_dir and full_path:
video_obj.set_file(path, filename, extension)
# Update any video object IVs that are not set
if video_obj.name == app_obj.default_video_name \
and name is not None:
video_obj.set_name(name)
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.long_string_max_len,
)
# Only save the playlist index when this video is actually stored
# inside a media.Playlist object
if not video_obj.index \
and isinstance(video_obj.parent_obj, media.Playlist) \
and playlist_index is not None:
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.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):
# # Convert Python2 to Python3
# fh = open(descrip_path, 'w')
fh = open(descrip_path, 'wb')
fh.write(descrip.encode('utf-8'))
fh.close()
if options_dict['write_info']:
json_path = 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):
with open(json_path, 'w') as outfile:
json.dump(json_dict, outfile, indent=4)
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 = os.path.join(
video_obj.file_dir,
video_obj.file_name + 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)
with open(thumb_path, 'wb') as outfile:
outfile.write(request_obj.content)
# Update the main window
app_obj.announce_video_download(self.download_item_obj, video_obj)
def create_child_process(self, cmd_list): def create_child_process(self, cmd_list):
@ -1659,8 +1981,8 @@ class VideoDownloader(object):
# Extract the data # Extract the data
stdout_list[0] = stdout_list[0].lstrip('\r') stdout_list[0] = stdout_list[0].lstrip('\r')
if stdout_list[0] == '[download]': if stdout_list[0] == '[download]':
dl_stat_dict['status'] = ACTIVE_STAGE_DOWNLOAD dl_stat_dict['status'] = constants.ACTIVE_STAGE_DOWNLOAD
# Get path, filename and extension # Get path, filename and extension
if stdout_list[1] == 'Destination:': if stdout_list[1] == 'Destination:':
@ -1705,7 +2027,7 @@ class VideoDownloader(object):
# Get file already downloaded status # Get file already downloaded status
if stdout_list[-1] == 'downloaded': if stdout_list[-1] == 'downloaded':
dl_stat_dict['status'] = COMPLETED_STAGE_ALREADY dl_stat_dict['status'] = constants.COMPLETED_STAGE_ALREADY
path, filename, extension = self.extract_filename( path, filename, extension = self.extract_filename(
' '.join(stdout_with_spaces_list[1:-4]), ' '.join(stdout_with_spaces_list[1:-4]),
) )
@ -1718,14 +2040,14 @@ class VideoDownloader(object):
# Get filesize abort status # Get filesize abort status
if stdout_list[-1] == 'Aborting.': if stdout_list[-1] == 'Aborting.':
dl_stat_dict['status'] = ERROR_STAGE_ABORT dl_stat_dict['status'] = constants.ERROR_STAGE_ABORT
elif stdout_list[0] == '[hlsnative]': elif stdout_list[0] == '[hlsnative]':
# Get information from the native HLS extractor (see # Get information from the native HLS extractor (see
# https://github.com/rg3/youtube-dl/blob/master/youtube_dl/ # https://github.com/rg3/youtube-dl/blob/master/youtube_dl/
# downloader/hls.py#L54 # downloader/hls.py#L54
dl_stat_dict['status'] = ACTIVE_STAGE_DOWNLOAD dl_stat_dict['status'] = constants.ACTIVE_STAGE_DOWNLOAD
if len(stdout_list) == 7: if len(stdout_list) == 7:
segment_no = float(stdout_list[6]) segment_no = float(stdout_list[6])
@ -1736,12 +2058,12 @@ class VideoDownloader(object):
dl_stat_dict['percent'] = percent dl_stat_dict['percent'] = percent
elif stdout_list[0] == '[ffmpeg]': elif stdout_list[0] == '[ffmpeg]':
# Using FFmpeg, not the the native HLS extractor # Using FFmpeg, not the the native HLS extractor
# A successful video download is announced in one of several ways. # A successful video download is announced in one of several ways.
# Use the first announcement to update self.video_check_dict, and # Use the first announcement to update self.video_check_dict, and
# ignore subsequent announcements # ignore subsequent announcements
dl_stat_dict['status'] = ACTIVE_STAGE_POST_PROCESS dl_stat_dict['status'] = constants.ACTIVE_STAGE_POST_PROCESS
# Get the final file extension after the merging process has # Get the final file extension after the merging process has
# completed # completed
@ -1782,7 +2104,7 @@ class VideoDownloader(object):
self.confirm_new_video(path, filename, extension) self.confirm_new_video(path, filename, extension)
elif stdout_list[0][0] == '{': elif stdout_list[0][0] == '{':
# JSON data, the result of a simulated download. Convert to a # JSON data, the result of a simulated download. Convert to a
# python dictionary # python dictionary
if self.dl_sim_flag: if self.dl_sim_flag:
@ -1805,17 +2127,17 @@ class VideoDownloader(object):
self.video_total += 1 self.video_total += 1
dl_stat_dict['playlist_size'] = self.video_total dl_stat_dict['playlist_size'] = self.video_total
dl_stat_dict['status'] = ACTIVE_STAGE_CHECKING dl_stat_dict['status'] = constants.ACTIVE_STAGE_CHECKING
elif stdout_list[0][0] != '[' or stdout_list[0] == '[debug]': elif stdout_list[0][0] != '[' or stdout_list[0] == '[debug]':
# (Just ignore this output) # (Just ignore this output)
return dl_stat_dict return dl_stat_dict
else: else:
# The download has started # The download has started
dl_stat_dict['status'] = ACTIVE_STAGE_PRE_PROCESS dl_stat_dict['status'] = constants.ACTIVE_STAGE_PRE_PROCESS
return dl_stat_dict return dl_stat_dict
@ -1827,12 +2149,13 @@ class VideoDownloader(object):
Based on YoutubeDLDownloader._extract_info(). Based on YoutubeDLDownloader._extract_info().
If the job's status is COMPLETED_STAGE_ALREADY or ERROR_STAGE_ABORT, If the job's status is constants.COMPLETED_STAGE_ALREADY or
translate that into a new value for the return code, and then use that constants.ERROR_STAGE_ABORT, translate that into a new value for the
value to actually set self.return_code (which halts the download). return code, and then use that value to actually set self.return_code
(which halts the download).
Args: Args:
dl_stat_dict (dict): The Python dictionary returned by the call to dl_stat_dict (dict): The Python dictionary returned by the call to
self.extract_stdout_data(), in the standard form described by self.extract_stdout_data(), in the standard form described by
the comments for that function the comments for that function
@ -1840,11 +2163,11 @@ class VideoDownloader(object):
""" """
if 'status' in dl_stat_dict: if 'status' in dl_stat_dict:
if dl_stat_dict['status'] == COMPLETED_STAGE_ALREADY: if dl_stat_dict['status'] == constants.COMPLETED_STAGE_ALREADY:
self.set_return_code(self.ALREADY) self.set_return_code(self.ALREADY)
dl_stat_dict['status'] = None dl_stat_dict['status'] = None
if dl_stat_dict['status'] == ERROR_STAGE_ABORT: if dl_stat_dict['status'] == constants.ERROR_STAGE_ABORT:
self.set_return_code(self.FILESIZE_ABORT) self.set_return_code(self.FILESIZE_ABORT)
dl_stat_dict['status'] = None dl_stat_dict['status'] = None
@ -1859,7 +2182,7 @@ class VideoDownloader(object):
youtube-dl. youtube-dl.
Returns: Returns:
Python list that contains the system command to execute. Python list that contains the system command to execute.
""" """
@ -1913,7 +2236,7 @@ class VideoDownloader(object):
checks the STERR message to see if it's an error or just a warning. checks the STERR message to see if it's an error or just a warning.
Args: Args:
stderr (string): A message from the child process STDERR. stderr (string): A message from the child process STDERR.
Returns: Returns:
@ -1944,23 +2267,23 @@ class VideoDownloader(object):
dl_stat_dict = {} dl_stat_dict = {}
if self.return_code == self.OK: if self.return_code == self.OK:
dl_stat_dict['status'] = COMPLETED_STAGE_FINISHED dl_stat_dict['status'] = constants.COMPLETED_STAGE_FINISHED
elif self.return_code == self.ERROR: elif self.return_code == self.ERROR:
dl_stat_dict['status'] = MAIN_STAGE_ERROR dl_stat_dict['status'] = constants.MAIN_STAGE_ERROR
dl_stat_dict['eta'] = '' dl_stat_dict['eta'] = ''
dl_stat_dict['speed'] = '' dl_stat_dict['speed'] = ''
elif self.return_code == self.WARNING: elif self.return_code == self.WARNING:
dl_stat_dict['status'] = COMPLETED_STAGE_WARNING dl_stat_dict['status'] = constants.COMPLETED_STAGE_WARNING
dl_stat_dict['eta'] = '' dl_stat_dict['eta'] = ''
dl_stat_dict['speed'] = '' dl_stat_dict['speed'] = ''
elif self.return_code == self.STOPPED: elif self.return_code == self.STOPPED:
dl_stat_dict['status'] = ERROR_STAGE_STOPPED dl_stat_dict['status'] = constants.ERROR_STAGE_STOPPED
dl_stat_dict['eta'] = '' dl_stat_dict['eta'] = ''
dl_stat_dict['speed'] = '' dl_stat_dict['speed'] = ''
elif self.return_code == self.ALREADY: elif self.return_code == self.ALREADY:
dl_stat_dict['status'] = COMPLETED_STAGE_ALREADY dl_stat_dict['status'] = constants.COMPLETED_STAGE_ALREADY
else: else:
dl_stat_dict['status'] = ERROR_STAGE_ABORT dl_stat_dict['status'] = constants.ERROR_STAGE_ABORT
# Use some empty values in dl_stat_dict so that the Progress Tab # Use some empty values in dl_stat_dict so that the Progress Tab
# doesn't show arbitrary data from the last file downloaded # doesn't show arbitrary data from the last file downloaded
@ -1985,7 +2308,7 @@ class VideoDownloader(object):
is higher in the hierarchy of return codes than the current value. is higher in the hierarchy of return codes than the current value.
Args: Args:
code (int): A return code in the range 0-5 code (int): A return code in the range 0-5
""" """
@ -2033,7 +2356,7 @@ class PipeReader(threading.Thread):
process pipes in an asynchronous way. process pipes in an asynchronous way.
Args: Args:
queue (Queue.Queue): Python queue to store the output of the child queue (queue.Queue): Python queue to store the output of the child
process. process.
Warnings: Warnings:
@ -2075,7 +2398,7 @@ class PipeReader(threading.Thread):
# Public class methods # Public class methods
def run(self): def OLDOLDrun(self):
"""Called as a result of self.__init__(). """Called as a result of self.__init__().
@ -2103,6 +2426,42 @@ class PipeReader(threading.Thread):
time.sleep(self.sleep_time) time.sleep(self.sleep_time)
def run(self):
"""Called as a result of self.__init__().
Reads from STDOUT or STERR using the attached filed descriptor.
"""
# 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:
# # Convert Python2 to Python3 - the for loop no longer
# # terminates, but produces endless <b''> strings instead
# for line in iter(self.file_descriptor.readline, str('')):
for line in iter(self.file_descriptor.readline, str('')):
if line == b'':
break
# # Convert Python2 to Python3
# if str('ffmpeg version') in line:
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): def attach_file_descriptor(self, filedesc):
@ -2113,9 +2472,9 @@ class PipeReader(threading.Thread):
Args: Args:
filedesc (filehandle): The open filehandle for STDOUT or STDERR filedesc (filehandle): The open filehandle for STDOUT or STDERR
""" """
self.file_descriptor = filedesc self.file_descriptor = filedesc
@ -2129,10 +2488,9 @@ class PipeReader(threading.Thread):
Args: Args:
timeout (-): No calling code sets a timeout timeout (-): No calling code sets a timeout
""" """
self.running_flag = False self.running_flag = False
super(PipeReader, self).join(timeout) super(PipeReader, self).join(timeout)

View File

@ -57,7 +57,7 @@ class FileManager(threading.Thread):
# Public class methods # Public class methods
def load_json(self, full_path): def load_json(self, full_path):
"""Can be called by anything. """Can be called by anything.
@ -66,7 +66,7 @@ class FileManager(threading.Thread):
dictionary and returns the dictionary. dictionary and returns the dictionary.
Args: Args:
full_path (string): The full path to the JSON file full_path (string): The full path to the JSON file
Returns: Returns:
@ -95,20 +95,20 @@ class FileManager(threading.Thread):
Args: Args:
full_path (string): The full path to the text file full_path (string): The full path to the text file
Returns: Returns:
The contents of the text file as a string, or or None if the file The contents of the text file as a string, or or None if the file
is missing or can't be loaded is missing or can't be loaded
""" """
if not os.path.isfile(full_path): if not os.path.isfile(full_path):
return None return None
with open(full_path, 'r') as text_file: with open(full_path, 'r') as text_file:
text = text_file.read() text = text_file.read()
return text return text
@ -129,7 +129,7 @@ class FileManager(threading.Thread):
Returns: Returns:
A GdkPixbuf, or None if the file is missing or can't be loaded A GdkPixbuf, or None if the file is missing or can't be loaded
""" """
if not os.path.isfile(full_path): if not os.path.isfile(full_path):

View File

@ -51,17 +51,17 @@ except:
# Import our modules # Import our modules
import config
import downloads
import files
import __main__ import __main__
import mainwin from . import config
import media from . import downloads
import options from . import files
import refresh from . import mainwin
import testing from . import media
import updates from . import options
import utils from . import refresh
from . import testing
from . import updates
from . import utils
# Classes # Classes
@ -415,30 +415,34 @@ class TartubeApp(Gtk.Application):
# Debugging flags (can only be set by editing the source code) # Debugging flags (can only be set by editing the source code)
# Delete the config file and the contents of Tartube's data directory # Delete the config file and the contents of Tartube's data directory
# on startup # on startup
self.debug_delete_data_flag = False # self.debug_delete_data_flag = False
self.debug_delete_data_flag = True
# In the main window's menu, show a menu item for adding a set of # In the main window's menu, show a menu item for adding a set of
# media data objects for testing # media data objects for testing
self.debug_test_media_menu_flag = False # self.debug_test_media_menu_flag = False
self.debug_test_media_menu_flag = True
# In the main window's toolbar, show a toolbar item for adding a set of # In the main window's toolbar, show a toolbar item for adding a set of
# media data objects for testing # media data objects for testing
self.debug_test_media_toolbar_flag = False self.debug_test_media_toolbar_flag = False
# Show an dialogue window with 'Tartube is already running!' if the # Show an dialogue window with 'Tartube is already running!' if the
# user tries to open a second instance of Tartube # user tries to open a second instance of Tartube
self.debug_warn_multiple_flag = False # self.debug_warn_multiple_flag = False
self.debug_warn_multiple_flag = True
# Open the main window in the top-left corner of the desktop # Open the main window in the top-left corner of the desktop
self.debug_open_top_left_flag = False # self.debug_open_top_left_flag = False
self.debug_open_top_left_flag = True
# Automatically open the system preferences window on startup # Automatically open the system preferences window on startup
self.debug_open_pref_win_flag = False self.debug_open_pref_win_flag = False
# For Tartube developers who don't want to manually change # For Tartube developers who don't want to manually change
# self.ytdl_path and self.ytdl_update_current on every startup # self.ytdl_path and self.ytdl_update_current on every startup
# (assuming that self.debug_delete_data_flag is True), modify those # (assuming that self.debug_delete_data_flag is True), modify those
# IVs # IVs
self.debug_modify_ytdl_flag = False # self.debug_modify_ytdl_flag = False
self.debug_ytdl_path = None # self.debug_ytdl_path = None
self.debug_ytdl_update_current = None # self.debug_ytdl_update_current = None
# self.debug_modify_ytdl_flag = True self.debug_modify_ytdl_flag = True
# self.debug_ytdl_path = 'youtube-dl' self.debug_ytdl_path = 'youtube-dl'
# self.debug_ytdl_update_current = 'Update using pip' self.debug_ytdl_update_current = 'Update using pip'
def do_startup(self): def do_startup(self):
@ -694,7 +698,7 @@ class TartubeApp(Gtk.Application):
self.show_msg_dialogue( self.show_msg_dialogue(
utils.upper_case_first(__main__.__packagename__) \ utils.upper_case_first(__main__.__packagename__) \
+ ' is already running!', + ' is already running!',
False, # Not modal False, # Not modal
'warning', 'warning',
'ok', 'ok',
) )
@ -870,7 +874,7 @@ class TartubeApp(Gtk.Application):
'There is ' + string + ' operation in progress.\n' \ 'There is ' + string + ' operation in progress.\n' \
+ 'Are you sure you want to quit ' \ + 'Are you sure you want to quit ' \
+ utils.upper_case_first(__main__.__packagename__) + '?', + utils.upper_case_first(__main__.__packagename__) + '?',
True, # Modal True, # Modal
'question', 'question',
'yes-no', 'yes-no',
) )
@ -925,7 +929,7 @@ class TartubeApp(Gtk.Application):
200-299: mainwin.py (in use: 201-234) 200-299: mainwin.py (in use: 201-234)
300-399: downloads.py (in use: 301-303) 300-399: downloads.py (in use: 301-303)
400-499: config.py (in use: 401-404) 400-499: config.py (in use: 401-404)
""" """
if self.main_win_obj: if self.main_win_obj:
@ -1445,13 +1449,13 @@ class TartubeApp(Gtk.Application):
Args: Args:
msg (string): The message to display msg (string): The message to display
""" """
if self.main_win_obj: if self.main_win_obj:
self.show_msg_dialogue( self.show_msg_dialogue(
msg, msg,
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -1565,7 +1569,7 @@ class TartubeApp(Gtk.Application):
return self.show_msg_dialogue( return self.show_msg_dialogue(
'A download operation cannot start\nif one or more' \ 'A download operation cannot start\nif one or more' \
+ ' configuration\nwindows are still open', + ' configuration\nwindows are still open',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -1601,7 +1605,7 @@ class TartubeApp(Gtk.Application):
return self.show_msg_dialogue( return self.show_msg_dialogue(
msg, msg,
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -1736,7 +1740,7 @@ class TartubeApp(Gtk.Application):
return self.show_msg_dialogue( return self.show_msg_dialogue(
'An update operation cannot start\nif one or more' \ 'An update operation cannot start\nif one or more' \
+ ' configuration\nwindows are still open', + ' configuration\nwindows are still open',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -1763,7 +1767,7 @@ class TartubeApp(Gtk.Application):
success_flag (True or False): True if the update operation success_flag (True or False): True if the update operation
succeeded, False if not succeeded, False if not
""" """
# Any code can check whether a download/update/refresh operation is in # Any code can check whether a download/update/refresh operation is in
@ -1792,7 +1796,7 @@ class TartubeApp(Gtk.Application):
self.show_msg_dialogue( self.show_msg_dialogue(
msg, msg,
False, # Not modal False, # Not modal
'info', 'info',
'ok', 'ok',
) )
@ -1854,7 +1858,7 @@ class TartubeApp(Gtk.Application):
return self.show_msg_dialogue( return self.show_msg_dialogue(
'A refresh operation cannot start\nif one or more' \ 'A refresh operation cannot start\nif one or more' \
+ ' configuration\nwindows are still open', + ' configuration\nwindows are still open',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -1904,7 +1908,7 @@ class TartubeApp(Gtk.Application):
self.show_msg_dialogue( self.show_msg_dialogue(
msg, msg,
False, # Not modal False, # Not modal
'info', 'info',
'ok', 'ok',
) )
@ -2299,7 +2303,7 @@ class TartubeApp(Gtk.Application):
Returns: Returns:
The new media.Channel object The new media.Channel object
""" """
# Channels can only be placed inside an unrestricted media.Folder # Channels can only be placed inside an unrestricted media.Folder
@ -2546,7 +2550,7 @@ class TartubeApp(Gtk.Application):
+ 'to the top level of ' \ + 'to the top level of ' \
+ utils.upper_case_first(__main__.__packagename__) \ + utils.upper_case_first(__main__.__packagename__) \
+ '\'s data directory', + '\'s data directory',
False, # Not modal False, # Not modal
'question', 'question',
'yes-no', 'yes-no',
) )
@ -2606,7 +2610,7 @@ class TartubeApp(Gtk.Application):
return self.show_msg_dialogue( return self.show_msg_dialogue(
'Channels, playlists and folders can\nonly be dragged into' \ 'Channels, playlists and folders can\nonly be dragged into' \
+ ' a folder', + ' a folder',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -2616,7 +2620,7 @@ class TartubeApp(Gtk.Application):
return self.show_msg_dialogue( return self.show_msg_dialogue(
'The fixed folder \'' + dest_obj.name \ 'The fixed folder \'' + dest_obj.name \
+ '\'\ncannot be moved (but it can still\nbe hidden)', + '\'\ncannot be moved (but it can still\nbe hidden)',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -2626,7 +2630,7 @@ class TartubeApp(Gtk.Application):
return self.show_msg_dialogue( return self.show_msg_dialogue(
'The folder \'' + dest_obj.name \ 'The folder \'' + dest_obj.name \
+ '\'\ncan only contain videos', + '\'\ncan only contain videos',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -2655,7 +2659,7 @@ class TartubeApp(Gtk.Application):
+ 'This procedure will move all downloaded files\n' \ + 'This procedure will move all downloaded files\n' \
+ 'to the new location' \ + 'to the new location' \
+ temp_string, + temp_string,
False, # Not modal False, # Not modal
'question', 'question',
'yes-no', 'yes-no',
) )
@ -2810,7 +2814,7 @@ class TartubeApp(Gtk.Application):
response2 = self.show_msg_dialogue( response2 = self.show_msg_dialogue(
'Are you SURE you want to delete files?\nThis procedure' \ 'Are you SURE you want to delete files?\nThis procedure' \
' cannot be reversed!', ' cannot be reversed!',
True, # Modal True, # Modal
'question', 'question',
'yes-no', 'yes-no',
) )
@ -2874,7 +2878,7 @@ class TartubeApp(Gtk.Application):
no_update_index_flag (True or False): False if the Video Index no_update_index_flag (True or False): False if the Video Index
should not be updated, because the calling function wants to do should not be updated, because the calling function wants to do
that itself. that itself.
""" """
# (List of Video Index rows to update, at the end of this function) # (List of Video Index rows to update, at the end of this function)
@ -2962,7 +2966,7 @@ class TartubeApp(Gtk.Application):
Marks a video object as downloaded (i.e. the video file exists on the Marks a video object as downloaded (i.e. the video file exists on the
user's filesystem) or not downloaded. user's filesystem) or not downloaded.
The video object's .dl_flag IV is updated. The video object's .dl_flag IV is updated.
Args: Args:
@ -2971,7 +2975,7 @@ class TartubeApp(Gtk.Application):
flag (True or False): True to mark the video as downloaded, False flag (True or False): True to mark the video as downloaded, False
to mark it as not downloaded. to mark it as not downloaded.
""" """
# (List of Video Index rows to update, at the end of this function) # (List of Video Index rows to update, at the end of this function)
@ -3048,7 +3052,7 @@ class TartubeApp(Gtk.Application):
"""Can be called by anything. """Can be called by anything.
Marks a video object as favourite or not favourite. Marks a video object as favourite or not favourite.
The video object's .fav_flag IV is updated. The video object's .fav_flag IV is updated.
Args: Args:
@ -3061,7 +3065,7 @@ class TartubeApp(Gtk.Application):
no_update_index_flag (True or False): False if the Video Index no_update_index_flag (True or False): False if the Video Index
should not be updated, because the calling function wants to do should not be updated, because the calling function wants to do
that itself. that itself.
""" """
# (List of Video Index rows to update, at the end of this function) # (List of Video Index rows to update, at the end of this function)
@ -3299,7 +3303,7 @@ class TartubeApp(Gtk.Application):
media_data_obj (media.Video, media.Channel, media.Playlist or media_data_obj (media.Video, media.Channel, media.Playlist or
media.Folder): The media data object to which the download media.Folder): The media data object to which the download
options are applied. options are applied.
""" """
if self.current_manager_obj \ if self.current_manager_obj \
@ -3334,7 +3338,7 @@ class TartubeApp(Gtk.Application):
media_data_obj (media.Video, media.Channel, media.Playlist or media_data_obj (media.Video, media.Channel, media.Playlist or
media.Folder): The media data object from which the download media.Folder): The media data object from which the download
options are removed. options are removed.
""" """
if self.current_manager_obj or not media_data_obj.options_obj: if self.current_manager_obj or not media_data_obj.options_obj:
@ -3378,7 +3382,7 @@ class TartubeApp(Gtk.Application):
'The video file is missing from ' \ 'The video file is missing from ' \
+ utils.upper_case_first(__main__.__packagename__) \ + utils.upper_case_first(__main__.__packagename__) \
+ '\'s\ndata directory (try downloading the\nvideo again!', + '\'s\ndata directory (try downloading the\nvideo again!',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -3466,9 +3470,9 @@ class TartubeApp(Gtk.Application):
# (Download operation timer) # (Download operation timer)
def timer_callback(self):
def timer_callback(self):
"""Called by gobject timer created by self.download_manager_start(). """Called by gobject timer created by self.download_manager_start().
@ -3483,7 +3487,7 @@ class TartubeApp(Gtk.Application):
Returns: Returns:
1 to keep the timer going, or None to halt it 1 to keep the timer going, or None to halt it
""" """
if self.timer_check_time is None: if self.timer_check_time is None:
@ -3502,16 +3506,16 @@ class TartubeApp(Gtk.Application):
# Not all downloaded files confirmed to exist yet, so return 1 # Not all downloaded files confirmed to exist yet, so return 1
# to keep the timer going a little longer # to keep the timer going a little longer
return 1 return 1
# The download operation has finished. The call to # The download operation has finished. The call to
# self.download_manager_finished() destroys the timer # self.download_manager_finished() destroys the timer
self.download_manager_finished() self.download_manager_finished()
# (Menu item and toolbar button callbacks) # (Menu item and toolbar button callbacks)
def on_button_stop_operation(self, action, par):
def on_button_stop_operation(self, action, par):
"""Called from a callback in self.do_startup(). """Called from a callback in self.do_startup().
@ -3522,7 +3526,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
self.operation_halted_flag = True self.operation_halted_flag = True
@ -3535,7 +3539,7 @@ class TartubeApp(Gtk.Application):
self.refresh_manager_obj.stop_refresh_operation() self.refresh_manager_obj.stop_refresh_operation()
def on_button_switch_view(self, action, par): def on_button_switch_view(self, action, par):
"""Called from a callback in self.do_startup(). """Called from a callback in self.do_startup().
@ -3546,7 +3550,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
if not self.complex_catalogue_flag: if not self.complex_catalogue_flag:
@ -3561,7 +3565,7 @@ class TartubeApp(Gtk.Application):
self.main_win_obj.video_index_current, self.main_win_obj.video_index_current,
) )
def on_menu_about(self, action, par): def on_menu_about(self, action, par):
"""Called from a callback in self.do_startup(). """Called from a callback in self.do_startup().
@ -3573,7 +3577,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
dialogue_win = Gtk.AboutDialog() dialogue_win = Gtk.AboutDialog()
@ -3610,7 +3614,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
action.destroy() action.destroy()
@ -3654,7 +3658,7 @@ class TartubeApp(Gtk.Application):
self.show_msg_dialogue( self.show_msg_dialogue(
'You must give the channel a name', 'You must give the channel a name',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -3667,7 +3671,7 @@ class TartubeApp(Gtk.Application):
): ):
self.show_msg_dialogue( self.show_msg_dialogue(
'You must enter a valid URL', 'You must enter a valid URL',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -3712,7 +3716,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
dialogue_win = mainwin.AddFolderDialogue(self.main_win_obj) dialogue_win = mainwin.AddFolderDialogue(self.main_win_obj)
@ -3736,7 +3740,7 @@ class TartubeApp(Gtk.Application):
self.show_msg_dialogue( self.show_msg_dialogue(
'You must give the folder a name', 'You must give the folder a name',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -3776,7 +3780,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
dialogue_win = mainwin.AddPlaylistDialogue(self.main_win_obj) dialogue_win = mainwin.AddPlaylistDialogue(self.main_win_obj)
@ -3802,7 +3806,7 @@ class TartubeApp(Gtk.Application):
self.show_msg_dialogue( self.show_msg_dialogue(
'You must give the playlist a name', 'You must give the playlist a name',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -3815,7 +3819,7 @@ class TartubeApp(Gtk.Application):
): ):
self.show_msg_dialogue( self.show_msg_dialogue(
'You must enter a valid URL', 'You must enter a valid URL',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -3860,7 +3864,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
dialogue_win = mainwin.AddVideoDialogue(self.main_win_obj) dialogue_win = mainwin.AddVideoDialogue(self.main_win_obj)
@ -3918,7 +3922,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
self.download_manager_start(True) self.download_manager_start(True)
@ -3935,7 +3939,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
self.download_manager_start(False) self.download_manager_start(False)
@ -3952,7 +3956,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
config.OptionsEditWin(self, self.general_options_obj, None) config.OptionsEditWin(self, self.general_options_obj, None)
@ -3969,7 +3973,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
self.refresh_manager_start() self.refresh_manager_start()
@ -3986,7 +3990,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
self.save_db() self.save_db()
@ -3997,7 +4001,7 @@ class TartubeApp(Gtk.Application):
self.show_msg_dialogue( self.show_msg_dialogue(
'Database saved', 'Database saved',
False, # Not modal False, # Not modal
'info', 'info',
'ok', 'ok',
) )
@ -4014,7 +4018,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
for name in self.media_name_dict: for name in self.media_name_dict:
@ -4038,7 +4042,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
config.SystemPrefWin(self) config.SystemPrefWin(self)
@ -4056,7 +4060,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
# Add media data objects for testing: videos, channels, playlists and/ # Add media data objects for testing: videos, channels, playlists and/
@ -4090,7 +4094,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
self.update_manager_start() self.update_manager_start()
@ -4107,7 +4111,7 @@ class TartubeApp(Gtk.Application):
action (Gio.SimpleAction): Object generated by Gio action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored par (None): Ignored
""" """
self.stop() self.stop()
@ -4145,7 +4149,7 @@ class TartubeApp(Gtk.Application):
self.show_msg_dialogue( self.show_msg_dialogue(
'There is already a ' + string + ' with that name\n' \ 'There is already a ' + string + ' with that name\n' \
+ '(so please choose a different name)', + '(so please choose a different name)',
False, # Not modal False, # Not modal
'error', 'error',
'ok', 'ok',
) )
@ -4219,7 +4223,7 @@ class TartubeApp(Gtk.Application):
Applies or releases the simultaneous download limit. If a download Applies or releases the simultaneous download limit. If a download
operation is in progress, the new setting is applied to the next operation is in progress, the new setting is applied to the next
download job. download job.
""" """
if not flag: if not flag:

View File

@ -40,13 +40,14 @@ import time
# Import our modules # Import our modules
from . import config
from . import constants
#from . import __main__
import __main__ import __main__
import config from . import mainapp
import constants from . import media
import mainapp from . import options
import media from . import utils
import options
import utils
# Classes # Classes
@ -1255,7 +1256,7 @@ class MainWin(Gtk.ApplicationWindow):
Returns: Returns:
-1 if row_iter1 comes before row_iter2, 1 if row_iter2 comes before -1 if row_iter1 comes before row_iter2, 1 if row_iter2 comes before
row_iter1, 0 if their order should not be changed row_iter1, 0 if their order should not be changed
""" """
# If auto-sorting is disabled temporarily, we can prevent the list # If auto-sorting is disabled temporarily, we can prevent the list
@ -1315,7 +1316,7 @@ class MainWin(Gtk.ApplicationWindow):
return 0 return 0
def video_catalogue_auto_sort(self, row1, row2, data, notify): def OLDvideo_catalogue_auto_sort(self, row1, row2, data, notify):
"""Sorting function created by self.videos_tab. """Sorting function created by self.videos_tab.
@ -1332,7 +1333,7 @@ class MainWin(Gtk.ApplicationWindow):
Returns: Returns:
-1 if row1 comes before row2, 1 if row2 comes before row1, 0 if -1 if row1 comes before row2, 1 if row2 comes before row1, 0 if
their order should not be changed their order should not be changed
""" """
@ -1359,6 +1360,64 @@ class MainWin(Gtk.ApplicationWindow):
else: else:
return -1 return -1
def video_catalogue_auto_sort(self, row1, row2, data, notify):
"""Sorting function created by self.videos_tab.
Automatically sorts rows in the Video Catalogue.
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
"""
# Get the media.Video objects displayed on each row
obj1 = row1.video_obj
obj2 = row2.video_obj
# Sort videos by playlist index (if set), then by upload time, and then
# by receive (download) time
if obj1.index is not None and obj2.index is not None:
if obj1.index < obj2.index:
return -1
else:
return 1
# # Convert Python2 to Python3
# elif obj1.upload_time < obj2.upload_time:
# return 1
# elif obj1.upload_time == obj2.upload_time:
# if obj1.receive_time < obj2.receive_time:
# return -1
# elif obj1.receive_time == obj2.receive_time:
# return 0
# 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:
if obj1.receive_time < obj2.receive_time:
return -1
elif obj1.receive_time == obj2.receive_time:
return 0
else:
return 1
else:
return -1
else:
return 0
# (Video Index) # (Video Index)
@ -1565,11 +1624,11 @@ class MainWin(Gtk.ApplicationWindow):
Also called by callbacks in mainapp.TartubeApp.on_menu_add_channel(), Also called by callbacks in mainapp.TartubeApp.on_menu_add_channel(),
.cb on_menu_add_folder() and cb on_menu_add_playlist(). .cb on_menu_add_folder() and cb on_menu_add_playlist().
Adds a row to the Video Index. Adds a row to the Video Index.
Args: Args:
media_data_obj (media.Video, media.Channel, media.Playlist, media_data_obj (media.Video, media.Channel, media.Playlist,
media.Folder): The media data object for this row media.Folder): The media data object for this row
@ -1659,10 +1718,10 @@ class MainWin(Gtk.ApplicationWindow):
Removes a row from the Video Index. Removes a row from the Video Index.
Args: Args:
media_data_obj (media.Video, media.Channel, media.Playlist, media_data_obj (media.Video, media.Channel, media.Playlist,
media.Folder): The media data object for this row media.Folder): The media data object for this row
""" """
# Videos can't be shown in the Video Index # Videos can't be shown in the Video Index
@ -1753,7 +1812,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Folder): media_data_obj (media.Channel, media.Playlist or media.Folder):
The media data object whose row should be updated The media data object whose row should be updated
""" """
# Videos can't be shown in the Video Index # Videos can't be shown in the Video Index
@ -1770,7 +1829,7 @@ class MainWin(Gtk.ApplicationWindow):
# Update the treeview row # Update the treeview row
tree_ref = self.video_index_row_dict[media_data_obj.name] tree_ref = self.video_index_row_dict[media_data_obj.name]
model = tree_ref.get_model() model = tree_ref.get_model()
tree_path = tree_ref.get_path() tree_path = tree_ref.get_path()
tree_iter = model.get_iter(tree_path) tree_iter = model.get_iter(tree_path)
model.set(tree_iter, 2, self.videx_index_get_icon(media_data_obj)) model.set(tree_iter, 2, self.videx_index_get_icon(media_data_obj))
@ -1794,9 +1853,9 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Folder): media_data_obj (media.Channel, media.Playlist or media.Folder):
The media data object whose row should be updated The media data object whose row should be updated
""" """
# Videos can't be shown in the Video Index # Videos can't be shown in the Video Index
if isinstance(media_data_obj, media.Video): if isinstance(media_data_obj, media.Video):
return self.app_obj.system_error( return self.app_obj.system_error(
@ -1811,7 +1870,7 @@ class MainWin(Gtk.ApplicationWindow):
# Update the treeview row # Update the treeview row
tree_ref = self.video_index_row_dict[media_data_obj.name] tree_ref = self.video_index_row_dict[media_data_obj.name]
model = tree_ref.get_model() model = tree_ref.get_model()
tree_path = tree_ref.get_path() tree_path = tree_ref.get_path()
tree_iter = model.get_iter(tree_path) tree_iter = model.get_iter(tree_path)
model.set(tree_iter, 3, self.video_index_get_text(media_data_obj)) model.set(tree_iter, 3, self.video_index_get_text(media_data_obj))
@ -1931,7 +1990,7 @@ class MainWin(Gtk.ApplicationWindow):
A string. A string.
""" """
text = utils.shorten_string( text = utils.shorten_string(
media_data_obj.name, media_data_obj.name,
@ -2320,7 +2379,7 @@ class MainWin(Gtk.ApplicationWindow):
"""Called from callbacks in self.on_video_index_selection_changed(), """Called from callbacks in self.on_video_index_selection_changed(),
mainapp.TartubeApp.on_button_switch_view(), mainapp.TartubeApp.on_button_switch_view(),
.on_menu_add_video() and on_menu_test(). .on_menu_add_video() and on_menu_test().
When the user clicks on a media data object in the Video Index (a 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 channel, playlist or folder), this function is called to replace the
contents of the Video Catalogue with all the video objects stored as contents of the Video Catalogue with all the video objects stored as
@ -2339,7 +2398,7 @@ class MainWin(Gtk.ApplicationWindow):
each video. each video.
Args: Args:
name (string): The selected media data object's name; one of the name (string): The selected media data object's name; one of the
keys in self.media_name_dict keys in self.media_name_dict
@ -2405,11 +2464,11 @@ class MainWin(Gtk.ApplicationWindow):
"""Called by self.results_list_update_row and a callback in """Called by self.results_list_update_row and a callback in
self.on_video_catalogue_enforce_check(). self.on_video_catalogue_enforce_check().
Also called by mainapp.TartubeApp.create_video_from_download(), Also called by mainapp.TartubeApp.create_video_from_download(),
.announce_video_download(), .mark_video_new() and .announce_video_download(), .mark_video_new() and
.mark_video_favourite(). .mark_video_favourite().
This function is called with a media.Video object. If that video is This function is called with a media.Video object. If that video is
already visible in the Video Catalogue, updates the corresponding already visible in the Video Catalogue, updates the corresponding
mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem (which mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem (which
@ -2499,7 +2558,7 @@ class MainWin(Gtk.ApplicationWindow):
This function is called with a media.Video object. If that video is This function is called with a media.Video object. If that video is
already visible in the Video Catalogue, removes the corresponding already visible in the Video Catalogue, removes the corresponding
mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem . mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem .
Args: Args:
video_obj (media.Video) - The video to remove video_obj (media.Video) - The video to remove
@ -2845,7 +2904,7 @@ class MainWin(Gtk.ApplicationWindow):
Progress List. Progress List.
Args: Args:
download_list_obj (downloads.DownloadList): The download list download_list_obj (downloads.DownloadList): The download list
object that has just been created object that has just been created
@ -3038,7 +3097,7 @@ class MainWin(Gtk.ApplicationWindow):
str, str,
) )
self.results_list_treeview.set_model(self.results_list_liststore) self.results_list_treeview.set_model(self.results_list_liststore)
# Reset IVs # Reset IVs
self.results_list_row_count = 0 self.results_list_row_count = 0
self.results_list_temp_list = [] self.results_list_temp_list = []
@ -3276,7 +3335,7 @@ class MainWin(Gtk.ApplicationWindow):
) )
else: else:
# File not found # File not found
# If this was a simulated download, the key 'keep_description' # If this was a simulated download, the key 'keep_description'
@ -3511,9 +3570,9 @@ class MainWin(Gtk.ApplicationWindow):
page_num (int) - The number of the newly-visible tab (the Videos page_num (int) - The number of the newly-visible tab (the Videos
Tab is number 0) Tab is number 0)
""" """
self.visible_tab_num = page_num self.visible_tab_num = page_num
if page_num == 2: if page_num == 2:
@ -3535,7 +3594,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
if self.app_obj.current_manager_obj \ if self.app_obj.current_manager_obj \
@ -3572,7 +3631,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -3597,7 +3656,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
self.app_obj.delete_container(media_data_obj) self.app_obj.delete_container(media_data_obj)
@ -3615,7 +3674,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -3649,9 +3708,9 @@ class MainWin(Gtk.ApplicationWindow):
info (int): Ignored info (int): Ignored
timestamp (int): Ignored timestamp (int): Ignored
""" """
# Must override the usual Gtk handler # Must override the usual Gtk handler
treeview.stop_emission('drag_data_received') treeview.stop_emission('drag_data_received')
@ -3697,7 +3756,7 @@ class MainWin(Gtk.ApplicationWindow):
x, y (int): Cell coordinates in the treeview x, y (int): Cell coordinates in the treeview
time (int): A timestamp time (int): A timestamp
""" """
# Must override the usual Gtk handler # Must override the usual Gtk handler
@ -3722,7 +3781,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
if self.app_obj.current_manager_obj or not media_data_obj.options_obj: if self.app_obj.current_manager_obj or not media_data_obj.options_obj:
@ -3752,7 +3811,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -3782,7 +3841,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
self.app_obj.mark_container_favourite(media_data_obj, True) self.app_obj.mark_container_favourite(media_data_obj, True)
@ -3801,7 +3860,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
self.app_obj.mark_container_favourite(media_data_obj, False) self.app_obj.mark_container_favourite(media_data_obj, False)
@ -3819,7 +3878,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
self.app_obj.mark_folder_hidden(media_data_obj, True) self.app_obj.mark_folder_hidden(media_data_obj, True)
@ -3839,7 +3898,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
# Special arrangements for private folders # Special arrangements for private folders
@ -3884,7 +3943,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
# Special arrangements for private folders # Special arrangements for private folders
@ -3926,7 +3985,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
self.app_obj.move_container_to_top(media_data_obj) self.app_obj.move_container_to_top(media_data_obj)
@ -3939,14 +3998,14 @@ class MainWin(Gtk.ApplicationWindow):
Refresh the right-clicked media data object, checking the corresponding Refresh the right-clicked media data object, checking the corresponding
directory on the user's filesystem against video objects in the directory on the user's filesystem against video objects in the
database. database.
Args: Args:
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -3972,7 +4031,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
if self.app_obj.current_manager_obj \ if self.app_obj.current_manager_obj \
@ -3998,9 +4057,9 @@ class MainWin(Gtk.ApplicationWindow):
treeview (Gtk.TreeView): The Video Index's treeview treeview (Gtk.TreeView): The Video Index's treeview
event (Gdk.EventButton): The event emitting the Gtk signal event (Gdk.EventButton): The event emitting the Gtk signal
""" """
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
# If the user right-clicked on empty space, the call to # If the user right-clicked on empty space, the call to
@ -4038,7 +4097,7 @@ class MainWin(Gtk.ApplicationWindow):
selection (Gtk.TreeSelection): Data for the selected row selection (Gtk.TreeSelection): Data for the selected row
""" """
(model, iter) = selection.get_selected() (model, iter) = selection.get_selected()
# Don't update the Video Catalogue during certain proecudres, such as # Don't update the Video Catalogue during certain proecudres, such as
@ -4067,7 +4126,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
path = media_data_obj.get_dir(self.app_obj) path = media_data_obj.get_dir(self.app_obj)
@ -4086,7 +4145,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj (media.Channel, media.Playlist or media.Channel): media_data_obj (media.Channel, media.Playlist or media.Channel):
The clicked media data object The clicked media data object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -4114,7 +4173,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if self.app_obj.current_manager_obj or media_data_obj.options_obj: if self.app_obj.current_manager_obj or media_data_obj.options_obj:
@ -4127,7 +4186,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj.set_options_obj(options.OptionsManager()) media_data_obj.set_options_obj(options.OptionsManager())
# Update the video catalogue to show the right icon # Update the video catalogue to show the right icon
self.video_catalogue_update_row(media_data_obj) self.video_catalogue_update_row(media_data_obj)
# Open an edit window to show the options immediately # Open an edit window to show the options immediately
config.OptionsEditWin( config.OptionsEditWin(
self.app_obj, self.app_obj,
@ -4147,7 +4206,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -4181,7 +4240,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -4206,7 +4265,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if self.app_obj.current_manager_obj or not media_data_obj.options_obj: if self.app_obj.current_manager_obj or not media_data_obj.options_obj:
@ -4235,7 +4294,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
# (Don't allow the user to change the setting of # (Don't allow the user to change the setting of
@ -4268,7 +4327,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -4311,7 +4370,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if self.app_obj.current_manager_obj or not media_data_obj.options_obj: if self.app_obj.current_manager_obj or not media_data_obj.options_obj:
@ -4324,7 +4383,7 @@ class MainWin(Gtk.ApplicationWindow):
media_data_obj.set_options_obj(None) media_data_obj.set_options_obj(None)
# Update the video catalogue to show the right icon # Update the video catalogue to show the right icon
self.video_catalogue_update_row(media_data_obj) self.video_catalogue_update_row(media_data_obj)
def on_video_catalogue_show_properties(self, menu_item, media_data_obj): def on_video_catalogue_show_properties(self, menu_item, media_data_obj):
@ -4337,7 +4396,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if self.app_obj.current_manager_obj: if self.app_obj.current_manager_obj:
@ -4362,7 +4421,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if not media_data_obj.fav_flag: if not media_data_obj.fav_flag:
@ -4382,7 +4441,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
if not media_data_obj.new_flag: if not media_data_obj.new_flag:
@ -4402,7 +4461,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
# Launch the video # Launch the video
@ -4427,7 +4486,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
# Launch the video # Launch the video
@ -4449,7 +4508,7 @@ class MainWin(Gtk.ApplicationWindow):
menu_item (Gtk.MenuItem): The clicked menu item menu_item (Gtk.MenuItem): The clicked menu item
media_data_obj (media.Video) - The clicked video object media_data_obj (media.Video) - The clicked video object
""" """
# Launch the video # Launch the video
@ -4460,7 +4519,7 @@ class MainWin(Gtk.ApplicationWindow):
self.app_obj.mark_video_new(media_data_obj, False) self.app_obj.mark_video_new(media_data_obj, False)
def on_spinbutton_changed(self, spinbutton): def on_spinbutton_changed(self, spinbutton):
"""Called from callback in self.setup_progress_tab(). """Called from callback in self.setup_progress_tab().
@ -4491,7 +4550,7 @@ class MainWin(Gtk.ApplicationWindow):
Args: Args:
checkbutton (Gtk.CheckButton) - The clicked widget checkbutton (Gtk.CheckButton) - The clicked widget
""" """
if self.checkbutton.get_active(): if self.checkbutton.get_active():
@ -4502,7 +4561,7 @@ class MainWin(Gtk.ApplicationWindow):
) )
else: else:
self.app_obj.set_num_worker_apply_flag(False) self.app_obj.set_num_worker_apply_flag(False)
@ -4517,7 +4576,7 @@ class MainWin(Gtk.ApplicationWindow):
Args: Args:
spinbutton (Gtk.SpinButton): The clicked widget spinbutton (Gtk.SpinButton): The clicked widget
""" """
self.app_obj.set_bandwidth_default( self.app_obj.set_bandwidth_default(
@ -4536,7 +4595,7 @@ class MainWin(Gtk.ApplicationWindow):
Args: Args:
checkbutton (Gtk.CheckButton): The clicked widget checkbutton (Gtk.CheckButton): The clicked widget
""" """
self.app_obj.set_bandwidth_apply_flag(self.checkbutton2.get_active()) self.app_obj.set_bandwidth_apply_flag(self.checkbutton2.get_active())
@ -4552,7 +4611,7 @@ class MainWin(Gtk.ApplicationWindow):
Args: Args:
button (Gtk.Button): The clicked widget button (Gtk.Button): The clicked widget
""" """
self.errors_list_reset() self.errors_list_reset()
@ -4656,7 +4715,7 @@ class SimpleCatalogueItem(object):
# Public class methods # Public class methods
def draw_widgets(self, catalogue_row): def draw_widgets(self, catalogue_row):
@ -4808,7 +4867,7 @@ class SimpleCatalogueItem(object):
event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the
signal emitted by the click signal emitted by the click
""" """
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
@ -4881,7 +4940,7 @@ class ComplexCatalogueItem(object):
# Public class methods # Public class methods
def draw_widgets(self, catalogue_row): def draw_widgets(self, catalogue_row):
"""Called by mainwin.MainWin.video_catalogue_redraw_all() and """Called by mainwin.MainWin.video_catalogue_redraw_all() and
@ -5308,7 +5367,7 @@ class ComplexCatalogueItem(object):
event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the
signal emitted by the click signal emitted by the click
""" """
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
@ -5354,7 +5413,7 @@ class ComplexCatalogueItem(object):
Returns: Returns:
True to show the action has been handled True to show the action has been handled
""" """
# Launch the video # Launch the video
@ -5383,7 +5442,7 @@ class ComplexCatalogueItem(object):
Returns: Returns:
True to show the action has been handled True to show the action has been handled
""" """
# Launch the video # Launch the video
@ -5411,7 +5470,7 @@ class ComplexCatalogueItem(object):
Returns: Returns:
True to show the action has been handled True to show the action has been handled
""" """
# Launch the video # Launch the video
@ -5564,7 +5623,7 @@ class AddVideoDialogue(Gtk.Dialog):
# Public class methods # Public class methods
def on_combo_changed(self, combo): def on_combo_changed(self, combo):
"""Called from callback in self.__init__(). """Called from callback in self.__init__().
@ -5575,7 +5634,7 @@ class AddVideoDialogue(Gtk.Dialog):
Args: Args:
combo (Gtk.ComboBox): The clicked widget combo (Gtk.ComboBox): The clicked widget
""" """
self.parent_name = self.folder_list[combo.get_active()] self.parent_name = self.folder_list[combo.get_active()]
@ -5700,7 +5759,7 @@ class AddChannelDialogue(Gtk.Dialog):
# Public class methods # Public class methods
def on_combo_changed(self, combo): def on_combo_changed(self, combo):
"""Called from callback in self.__init__(). """Called from callback in self.__init__().
@ -5711,7 +5770,7 @@ class AddChannelDialogue(Gtk.Dialog):
Args: Args:
combo (Gtk.ComboBox): The clicked widget combo (Gtk.ComboBox): The clicked widget
""" """
self.parent_name = self.folder_list[combo.get_active()] self.parent_name = self.folder_list[combo.get_active()]
@ -5836,7 +5895,7 @@ class AddPlaylistDialogue(Gtk.Dialog):
# Public class methods # Public class methods
def on_combo_changed(self, combo): def on_combo_changed(self, combo):
"""Called from callback in self.__init__(). """Called from callback in self.__init__().
@ -5847,7 +5906,7 @@ class AddPlaylistDialogue(Gtk.Dialog):
Args: Args:
combo (Gtk.ComboBox): The clicked widget combo (Gtk.ComboBox): The clicked widget
""" """
self.parent_name = self.folder_list[combo.get_active()] self.parent_name = self.folder_list[combo.get_active()]
@ -5958,7 +6017,7 @@ class AddFolderDialogue(Gtk.Dialog):
# Public class methods # Public class methods
def on_combo_changed(self, combo): def on_combo_changed(self, combo):
"""Called from callback in self.__init__(). """Called from callback in self.__init__().
@ -5969,7 +6028,7 @@ class AddFolderDialogue(Gtk.Dialog):
Args: Args:
combo (Gtk.ComboBox): The clicked widget combo (Gtk.ComboBox): The clicked widget
""" """
self.parent_name = self.folder_list[combo.get_active()] self.parent_name = self.folder_list[combo.get_active()]

View File

@ -26,14 +26,14 @@
# Import other modules # Import other modules
import datetime import datetime
import functools
import os import os
import time
# Import our modules # Import our modules
import functools from . import mainapp
import mainapp from . import utils
import time
import utils
# Classes # Classes
@ -121,7 +121,7 @@ class GenericContainer(GenericMedia):
video_list (list): A list of media.Video objects video_list (list): A list of media.Video objects
Returns: Returns:
The modified video_list The modified video_list
""" """
@ -139,7 +139,7 @@ class GenericContainer(GenericMedia):
def count_descendants(self, count_list): def count_descendants(self, count_list):
"""Can be called by anything. Currently called by """Can be called by anything. Currently called by
mainwin.DeleteContainerDialogue.__init__(), and then again by this mainwin.DeleteContainerDialogue.__init__(), and then again by this
function recursively. function recursively.
Counts the number of child objects, and then calls this function Counts the number of child objects, and then calls this function
@ -155,7 +155,7 @@ class GenericContainer(GenericMedia):
) )
Returns: Returns:
The modified count_list The modified count_list
""" """
@ -196,7 +196,7 @@ class GenericContainer(GenericMedia):
was not a child of this object was not a child of this object
""" """
# Check this is really one of our children # Check this is really one of our children
index = self.find_child_index(child_obj) index = self.find_child_index(child_obj)
if index is None: if index is None:
@ -227,7 +227,7 @@ class GenericContainer(GenericMedia):
An integer describing the position in self.child_list, or None of An integer describing the position in self.child_list, or None of
the child object is not found in self.child_list the child object is not found in self.child_list
""" """
try: try:
@ -252,7 +252,7 @@ class GenericContainer(GenericMedia):
Returns: Returns:
The container object's level The container object's level
""" """
if self.parent_obj is None: if self.parent_obj is None:
@ -283,7 +283,7 @@ class GenericContainer(GenericMedia):
can't be hidden directly.) can't be hidden directly.)
Returns: Returns:
True or False. True or False.
""" """
@ -357,7 +357,7 @@ class GenericContainer(GenericMedia):
Returns: Returns:
The full path to the directory The full path to the directory
""" """
dir_list = [self.name] dir_list = [self.name]
@ -401,6 +401,40 @@ class GenericRemoteContainer(GenericContainer):
self.vid_count += 1 self.vid_count += 1
def OLDdo_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
"""
if obj1.upload_time < obj2.upload_time:
return 1
elif obj1.upload_time == obj2.upload_time:
if obj1.receive_time < obj2.receive_time:
return -1
elif obj1.receive_time == obj2.receive_time:
return 0
else:
return 1
else:
return -1
def do_sort(self, obj1, obj2): def do_sort(self, obj1, obj2):
"""Sorting function used by functools.cmp_to_key(), and called by """Sorting function used by functools.cmp_to_key(), and called by
@ -414,16 +448,21 @@ class GenericRemoteContainer(GenericContainer):
videos. videos.
Args: Args:
obj1, obj2 (media.Video) - Video objects being sorted obj1, obj2 (media.Video) - Video objects being sorted
Returns: Returns:
-1 if obj1 comes first, 1 if obj2 comes first, 0 if they are equal -1 if obj1 comes first, 1 if obj2 comes first, 0 if they are equal
""" """
if obj1.upload_time < obj2.upload_time: # # Convert Python2 to Python3
# if obj1.upload_time < obj2.upload_time:
# return 1
if obj1.upload_time is None or obj2.upload_time is None:
return 0
elif obj1.upload_time < obj2.upload_time:
return 1 return 1
elif obj1.upload_time == obj2.upload_time: elif obj1.upload_time == obj2.upload_time:
if obj1.receive_time < obj2.receive_time: if obj1.receive_time < obj2.receive_time:
@ -620,7 +659,7 @@ class Video(GenericMedia):
favourite. favourite.
Returns: Returns:
True if the parent (or the parent's parent, and so on) is marked True if the parent (or the parent's parent, and so on) is marked
favourite, False otherwise favourite, False otherwise
@ -649,7 +688,7 @@ class Video(GenericMedia):
max_length (int): When storing the description in this object's max_length (int): When storing the description in this object's
IVs, the maximum line length to use IVs, the maximum line length to use
""" """
descrip_path = os.path.join( descrip_path = os.path.join(
@ -774,7 +813,7 @@ class Video(GenericMedia):
max_length (int): A maximum line size max_length (int): A maximum line size
""" """
if descrip: if descrip:
self.descrip = utils.tidy_up_long_descrip(descrip, max_length) self.descrip = utils.tidy_up_long_descrip(descrip, max_length)
@ -797,7 +836,7 @@ class Video(GenericMedia):
Returns: Returns:
The converted string, or None if self.file_size is not set The converted string, or None if self.file_size is not set
""" """
if self.file_size: if self.file_size:
@ -816,7 +855,7 @@ class Video(GenericMedia):
Returns: Returns:
The formatted string, or None if self.receive_time is not set The formatted string, or None if self.receive_time is not set
""" """
if self.receive_time: if self.receive_time:
@ -835,7 +874,7 @@ class Video(GenericMedia):
Returns: Returns:
The formatted string, or None if self.receive_time is not set The formatted string, or None if self.receive_time is not set
""" """
if self.receive_time: if self.receive_time:
@ -854,7 +893,7 @@ class Video(GenericMedia):
Returns: Returns:
The formatted string, or None if self.upload_time is not set The formatted string, or None if self.upload_time is not set
""" """
if self.upload_time: if self.upload_time:
@ -873,7 +912,7 @@ class Video(GenericMedia):
Returns: Returns:
The formatted string, or None if self.upload_time is not set The formatted string, or None if self.upload_time is not set
""" """
if self.upload_time: if self.upload_time:
@ -1287,7 +1326,7 @@ class Folder(GenericContainer):
# def del_child(): # Inherited from GenericContainer # def del_child(): # Inherited from GenericContainer
def do_sort(self, obj1, obj2): def OLDdo_sort(self, obj1, obj2):
"""Sorting function used by functools.cmp_to_key(), and called by """Sorting function used by functools.cmp_to_key(), and called by
self.sort_children(). self.sort_children().
@ -1301,14 +1340,14 @@ class Folder(GenericContainer):
videos, sort by upload time. videos, sort by upload time.
Args: Args:
obj1, obj2 (media.Video, media.Channel, media.Playlist or obj1, obj2 (media.Video, media.Channel, media.Playlist or
media.Folder) - Media data objects being sorted media.Folder) - Media data objects being sorted
Returns: Returns:
-1 if obj1 comes first, 1 if obj2 comes first, 0 if they are equal -1 if obj1 comes first, 1 if obj2 comes first, 0 if they are equal
""" """
if str(obj1.__class__) == str(obj2.__class__) \ if str(obj1.__class__) == str(obj2.__class__) \
@ -1351,6 +1390,75 @@ class Folder(GenericContainer):
else: else:
return 0 return 0
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):
# # Convert Python2 to Python3
# if obj1.upload_time < obj2.upload_time:
# return 1
if obj1.upload_time is None or obj2.upload_time is None:
return 0
elif obj1.upload_time < obj2.upload_time:
return 1
elif obj1.upload_time == obj2.upload_time:
if obj1.receive_time < obj2.receive_time:
return -1
elif obj1.receive_time == obj2.receive_time:
return 0
else:
return 1
else:
return -1
else:
if obj1.name.lower() < obj2.name.lower():
return -1
elif obj1.name.lower() == obj2.name.lower():
return 0
else:
return 1
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 find_child_index(): # Inherited from GenericContainer # def find_child_index(): # Inherited from GenericContainer

View File

@ -29,10 +29,10 @@ import os
# Import our modules # Import our modules
import constants from . import constants
import mainapp from . import mainapp
import media from . import media
import utils from . import utils
# Classes # Classes
@ -539,7 +539,7 @@ class OptionsParser(object):
taken from options.OptionsManager.options_dict taken from options.OptionsManager.options_dict
Returns: Returns:
List of strings with all the youtube-dl command line options List of strings with all the youtube-dl command line options
""" """
@ -822,11 +822,11 @@ class OptionHolder(object):
Check if options required by another option are enabled, or not. Check if options required by another option are enabled, or not.
Args: Args:
copy_dict (dict): Copy of the original options dictionary. copy_dict (dict): Copy of the original options dictionary.
Returns: Returns:
True if any of the required options is enabled, otherwise returns True if any of the required options is enabled, otherwise returns
False. False.
@ -845,7 +845,7 @@ class OptionHolder(object):
Returns: Returns:
True if the option is a boolean switch, otherwise returns False True if the option is a boolean switch, otherwise returns False
""" """
return type(self.default_value) is bool return type(self.default_value) is bool

View File

@ -25,13 +25,13 @@
# Import other modules # Import other modules
import constants
import os import os
import threading import threading
# Import our modules # Import our modules
import media from . import constants
from . import media
# Classes # Classes
@ -80,7 +80,7 @@ class RefreshManager(threading.Thread):
# Public class methods # Public class methods
def run(self): def run(self):
"""Called by mainapp.TartubeApp.refresh_manager_start(). """Called by mainapp.TartubeApp.refresh_manager_start().

View File

@ -51,7 +51,7 @@ def add_test_media(app_obj):
Args: Args:
app_obj (mainapp.TartubeApp): The main application app_obj (mainapp.TartubeApp): The main application
""" """
# Test videos # Test videos

View File

@ -26,7 +26,8 @@
# Import other modules # Import other modules
import os import os
import Queue import queue
import re
import requests import requests
import subprocess import subprocess
import sys import sys
@ -34,9 +35,8 @@ import threading
# Import our modules # Import our modules
import downloads from . import downloads
import re from . import utils
import utils
# Classes # Classes
@ -74,8 +74,8 @@ class UpdateManager(threading.Thread):
# This object reads from the child process STDOUT and STDERR in an # This object reads from the child process STDOUT and STDERR in an
# asynchronous way # asynchronous way
# Standard Python synchronised queue classes # Standard Python synchronised queue classes
self.stdout_queue = Queue.Queue() self.stdout_queue = queue.Queue()
self.stderr_queue = Queue.Queue() self.stderr_queue = queue.Queue()
# The downloads.PipeReader objects created to handle reading from the # The downloads.PipeReader objects created to handle reading from the
# pipes # pipes
self.stdout_reader = downloads.PipeReader(self.stdout_queue) self.stdout_reader = downloads.PipeReader(self.stdout_queue)
@ -104,8 +104,8 @@ class UpdateManager(threading.Thread):
# Public class methods # Public class methods
def run(self): def OLDrun(self):
"""Called as a result of self.__init__(). """Called as a result of self.__init__().
@ -118,7 +118,7 @@ class UpdateManager(threading.Thread):
""" """
# Prepare the system command # Prepare the system command
# The user can change the system command for updating youtube-dl, # The user can change the system command for updating youtube-dl,
# depending on how it was installed # depending on how it was installed
# (For example, if youtube-dl was installed via pip, then it must be # (For example, if youtube-dl was installed via pip, then it must be
@ -189,6 +189,94 @@ class UpdateManager(threading.Thread):
else: else:
self.app_obj.update_manager_finished(True) self.app_obj.update_manager_finished(True)
def run(self):
"""Called as a result of self.__init__().
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).
"""
# 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]
# 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 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():
# # (Convert Python2 to Python3)
# stdout = self.stdout_queue.get_nowait().rstrip()
# stdout = utils.convert_item(stdout, to_unicode=True)
stdout = self.stdout_queue.get_nowait().rstrip().decode('utf-8')
if stdout:
# "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:
self.stdout_list.append(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
# # (Convert Python2 to Python3)
# stderr = self.stderr_queue.get_nowait().rstrip()
# stderr = utils.convert_item(stderr, to_unicode=True)
stderr = self.stderr_queue.get_nowait().rstrip().decode('utf-8')
if stderr:
self.stderr_list.append(stderr)
# (Generate our own error messages for debugging purposes, in certain
# situations)
if self.child_process is None:
self.stderr_list.append('Download 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; inform the main application of success or failure
if self.stderr_list:
self.app_obj.update_manager_finished(False)
else:
self.app_obj.update_manager_finished(True)
def create_child_process(self, cmd_list): def create_child_process(self, cmd_list):
@ -263,7 +351,7 @@ class UpdateManager(threading.Thread):
"""Called by mainapp.TartubeApp.on_button_stop_operation(), .stop() and """Called by mainapp.TartubeApp.on_button_stop_operation(), .stop() and
a callback in .on_button_stop_operation(). a callback in .on_button_stop_operation().
Based on code from downloads.VideoDownloader.stop(). Based on code from downloads.VideoDownloader.stop().
Terminates the child process. Terminates the child process.

View File

@ -36,10 +36,10 @@ import textwrap
# Import our modules # Import our modules
import constants from . import constants
import mainapp from . import mainapp
if mainapp.HAVE_VALIDATORS_FLAG: if mainapp.HAVE_VALIDATORS_FLAG:
import validators from . import validators
# Functions # Functions
@ -90,7 +90,7 @@ def convert_item(item, to_unicode=False):
Convert item between 'unicode' and 'str'. Convert item between 'unicode' and 'str'.
Args: Args:
item (-): Can be any python item. item (-): Can be any python item.
to_unicode (boolean): When True it will convert all the 'str' types to_unicode (boolean): When True it will convert all the 'str' types
@ -98,7 +98,7 @@ def convert_item(item, to_unicode=False):
back to 'str'. back to 'str'.
Returns: Returns:
The converted item The converted item
""" """
@ -310,7 +310,7 @@ def format_bytes(num_bytes):
Returns: Returns:
The formatted string The formatted string
""" """
if num_bytes == 0.0: if num_bytes == 0.0:
@ -332,7 +332,7 @@ def get_encoding():
Returns: Returns:
The system encoding. The system encoding.
""" """
try: try:
@ -354,7 +354,7 @@ def open_file(uri):
Args: Args:
uri (string): The URI to open uri (string): The URI to open
""" """
if sys.platform == "win32": if sys.platform == "win32":
@ -378,7 +378,7 @@ def remove_shortcuts(path):
Returns: Returns:
The converted path The converted path
""" """
return path.replace('~', os.path.expanduser('~')) return path.replace('~', os.path.expanduser('~'))
@ -399,7 +399,7 @@ def shorten_string(string, num_chars):
Returns: Returns:
The converted string The converted string
""" """
if string and len(string) > num_chars: if string and len(string) > num_chars:
@ -423,7 +423,7 @@ def to_string(data):
Returns: Returns:
The converted string The converted string
""" """
return '%s' % data return '%s' % data
@ -441,7 +441,7 @@ def upper_case_first(string):
Returns: Returns:
The converted string The converted string
""" """
return string[0].upper() + string[1:] return string[0].upper() + string[1:]

View File

@ -21,27 +21,30 @@
# Import modules # Import modules
from setuptools import setup, find_packages import setuptools
# Import documents # Import documents
with open('README.rst') as f: #with open('README.rst', 'r') as f:
readme = f.read() # long_description = f.read()
#
with open('LICENSE') as f: #with open('LICENSE') as f:
license = f.read() # license = f.read()
# Setup # Setup
setup( setuptools.setup(
name='tartube', name='tartube',
version='0.1.000', version='0.1.007',
description='GUI front-end for youtube-dl', description='GUI front-end for youtube-dl',
long_description=readme, # long_description=long_description,
long_description="""Tartube is a GUI front-end for youtube-dl, partly based on youtube-dl-gui and written in Python 3 / Gtk 3""",
long_description_content_type='text/markdown',
author='A S Lewis', author='A S Lewis',
author_email='aslewis@cpan.org', author_email='aslewis@cpan.org',
url='https://github.com/axcore/tartube', url='https://github.com/axcore/tartube',
license=license, # license=license,
packages=find_packages(exclude=('tests', 'docs')) license="""GPL3+""",
packages=setuptools.find_packages()
) )

View File

@ -34,8 +34,8 @@ from lib import mainapp
# 'Global' variables # 'Global' variables
__packagename__ = 'tartube' __packagename__ = 'tartube'
__version__ = '0.1.000' __version__ = '0.1.007'
__date__ = '27 May 2019' __date__ = '28 May 2019'
__copyright__ = 'Copyright \xc2\xa9 2019 A S Lewis' __copyright__ = 'Copyright \xc2\xa9 2019 A S Lewis'
__license__ = """ __license__ = """
Copyright \xc2\xa9 2019 A S Lewis. Copyright \xc2\xa9 2019 A S Lewis.

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@