Add files via upload
parent
cdaa2ba53c
commit
c1916c395a
|
@ -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>`__.
|
||||
|
||||
✨🍰✨
|
|
@ -1 +1 @@
|
|||
|
||||
name = "tartube"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
#Tartube
|
112
lib/config.py
112
lib/config.py
|
@ -31,11 +31,11 @@ import os
|
|||
|
||||
|
||||
# Import our modules
|
||||
import constants
|
||||
import __main__
|
||||
import mainapp
|
||||
import media
|
||||
import utils
|
||||
from . import constants
|
||||
from . import mainapp
|
||||
from . import media
|
||||
from . import utils
|
||||
|
||||
|
||||
# Classes
|
||||
|
@ -179,7 +179,7 @@ class GenericConfigWin(Gtk.Window):
|
|||
|
||||
also_self (an object inheriting from config.GenericConfigWin):
|
||||
another copy of self
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.app_obj.main_win_obj.del_child_window(self)
|
||||
|
@ -825,9 +825,9 @@ class GenericEditWin(GenericConfigWin):
|
|||
showing their original values.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
button (Gtk.Button): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Destroy the window
|
||||
|
@ -841,7 +841,7 @@ class GenericEditWin(GenericConfigWin):
|
|||
Destroys any changes made by the user and then closes the window.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
button (Gtk.Button): The widget clicked
|
||||
|
||||
"""
|
||||
|
@ -861,7 +861,7 @@ class GenericEditWin(GenericConfigWin):
|
|||
showing their original values.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
button (Gtk.Button): The widget clicked
|
||||
|
||||
"""
|
||||
|
@ -891,11 +891,11 @@ class GenericEditWin(GenericConfigWin):
|
|||
selected, False if not.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
prop (string): The attribute in self.edit_obj to modify
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if not checkbutton.get_active():
|
||||
|
@ -911,11 +911,11 @@ class GenericEditWin(GenericConfigWin):
|
|||
Temporarily stores the contents of the widget in self.edit_dict.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
combo (Gtk.ComboBox): The widget clicked
|
||||
|
||||
prop (string): The attribute in self.edit_obj to modify
|
||||
|
||||
|
||||
"""
|
||||
|
||||
tree_iter = combo.get_active_iter()
|
||||
|
@ -931,11 +931,11 @@ class GenericEditWin(GenericConfigWin):
|
|||
value, and stores the later in self.edit_dict.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
combo (Gtk.ComboBox): The widget clicked
|
||||
|
||||
prop (string): The attribute in self.edit_obj to modify
|
||||
|
||||
|
||||
"""
|
||||
|
||||
tree_iter = combo.get_active_iter()
|
||||
|
@ -950,11 +950,11 @@ class GenericEditWin(GenericConfigWin):
|
|||
Temporarily stores the contents of the widget in self.edit_dict.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
entry (Gtk.Entry): The widget clicked
|
||||
|
||||
prop (string): The attribute in self.edit_obj to modify
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.edit_dict[prop] = entry.get_text()
|
||||
|
@ -968,13 +968,13 @@ class GenericEditWin(GenericConfigWin):
|
|||
(from those in the group) is the selected one.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
prop (string): The attribute in self.edit_obj to modify
|
||||
|
||||
value (-): The attribute's new value
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if radiobutton.get_active():
|
||||
|
@ -988,11 +988,11 @@ class GenericEditWin(GenericConfigWin):
|
|||
Temporarily stores the contents of the widget in self.edit_dict.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
spinbutton (Gtk.SpinkButton): The widget clicked
|
||||
|
||||
prop (string): The attribute in self.edit_obj to modify
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
textbuffer (Gtk.TextBuffer): The widget modified
|
||||
|
||||
prop (string): The attribute in self.edit_obj to modify
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.edit_dict[prop] = textbuffer.get_text(
|
||||
|
@ -1030,9 +1030,9 @@ class GenericEditWin(GenericConfigWin):
|
|||
Apply download options to the media data object.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
button (Gtk.Button): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.edit_obj.options_obj:
|
||||
|
@ -1056,9 +1056,9 @@ class GenericEditWin(GenericConfigWin):
|
|||
Edit download options for the media data object.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
button (Gtk.Button): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if not self.edit_obj.options_obj:
|
||||
|
@ -1082,9 +1082,9 @@ class GenericEditWin(GenericConfigWin):
|
|||
Remove download options from the media data object.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
button (Gtk.Button): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if not self.edit_obj.options_obj:
|
||||
|
@ -1567,7 +1567,7 @@ class OptionsEditWin(GenericEditWin):
|
|||
Returns:
|
||||
|
||||
The original or modified value of that attribute.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if name in self.edit_dict:
|
||||
|
@ -2452,7 +2452,7 @@ class OptionsEditWin(GenericEditWin):
|
|||
button (Gtk.Button): The widget clicked
|
||||
|
||||
entry (Gtk.Entry): Another widget to be modified by this function
|
||||
|
||||
|
||||
combo (Gtk.ComboBox): Another widget to be modified by this
|
||||
function
|
||||
|
||||
|
@ -2489,7 +2489,7 @@ class OptionsEditWin(GenericEditWin):
|
|||
combo (Gtk.ComboBox): The widget clicked
|
||||
|
||||
entry (Gtk.Entry): Another widget to be modified by this function
|
||||
|
||||
|
||||
"""
|
||||
|
||||
tree_iter = combo.get_active_iter()
|
||||
|
@ -2514,7 +2514,7 @@ class OptionsEditWin(GenericEditWin):
|
|||
Args:
|
||||
|
||||
entry (Gtk.Entry): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# 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
|
||||
this function
|
||||
|
||||
|
||||
"""
|
||||
|
||||
selection = treeview.get_selection()
|
||||
|
@ -2592,7 +2592,7 @@ class OptionsEditWin(GenericEditWin):
|
|||
|
||||
treeview (Gtk.TreeView): Another widget to be modified by this
|
||||
function
|
||||
|
||||
|
||||
"""
|
||||
|
||||
selection = treeview.get_selection()
|
||||
|
@ -2647,11 +2647,11 @@ class OptionsEditWin(GenericEditWin):
|
|||
|
||||
add_button, up_button, down_button (Gtk.Button): Other widgets to
|
||||
be modified by this function
|
||||
|
||||
|
||||
|
||||
treeview (Gtk.TreeView): Another widget to be modified by this
|
||||
function
|
||||
|
||||
|
||||
"""
|
||||
|
||||
selection = treeview.get_selection()
|
||||
|
@ -2702,7 +2702,7 @@ class OptionsEditWin(GenericEditWin):
|
|||
|
||||
treeview (Gtk.TreeView): Another widget to be modified by this
|
||||
function
|
||||
|
||||
|
||||
"""
|
||||
|
||||
selection = treeview.get_selection()
|
||||
|
@ -2758,7 +2758,7 @@ class OptionsEditWin(GenericEditWin):
|
|||
function
|
||||
|
||||
prop (string): The attribute in self.edit_obj to modify
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if radiobutton.get_active():
|
||||
|
@ -4361,7 +4361,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
other_flag = self.app_obj.main_win_obj.checkbutton2.get_active()
|
||||
|
@ -4398,7 +4398,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
redraw_flag = False
|
||||
|
@ -4430,7 +4430,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
button (Gtk.Button): The widget clicked
|
||||
|
||||
entry (Gtk.Entry): Another widget to be modified by this function
|
||||
|
||||
|
||||
"""
|
||||
|
||||
dialogue_win = Gtk.FileChooserDialog(
|
||||
|
@ -4488,7 +4488,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
radiobutton (Gtk.RadioButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
default_val = self.app_obj.match_default_chars
|
||||
|
@ -4526,7 +4526,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
spinbutton (Gtk.SpinButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if spinbutton == self.spinbutton:
|
||||
|
@ -4544,7 +4544,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if checkbutton.get_active() \
|
||||
|
@ -4565,7 +4565,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if checkbutton.get_active() and not self.app_obj.operation_save_flag:
|
||||
|
@ -4584,7 +4584,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if checkbutton.get_active() \
|
||||
|
@ -4605,7 +4605,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if checkbutton.get_active() \
|
||||
|
@ -4615,7 +4615,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
and self.app_obj.ytdl_write_stdout_flag:
|
||||
self.app_obj.set_ytdl_write_stdout_flag(False)
|
||||
|
||||
|
||||
|
||||
def on_update_combo_changed(self, combo):
|
||||
|
||||
"""Called from a callback in self.setup_ytdl_tab().
|
||||
|
@ -4626,7 +4626,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
combo (Gtk.ComboBox): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
tree_iter = combo.get_active_iter()
|
||||
|
@ -4643,7 +4643,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if checkbutton.get_active() \
|
||||
|
@ -4663,7 +4663,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if checkbutton.get_active() \
|
||||
|
@ -4685,7 +4685,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
other_flag = self.app_obj.main_win_obj.checkbutton.get_active()
|
||||
|
@ -4707,7 +4707,7 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
spinbutton (Gtk.SpinButton): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.app_obj.main_win_obj.spinbutton.set_value(spinbutton.get_value())
|
||||
|
@ -4723,10 +4723,10 @@ class SystemPrefWin(GenericPrefWin):
|
|||
Args:
|
||||
|
||||
combo (Gtk.ComboBox): The widget clicked
|
||||
|
||||
|
||||
"""
|
||||
|
||||
tree_iter = combo.get_active_iter()
|
||||
model = combo.get_model()
|
||||
self.app_obj.set_ytdl_path(model[tree_iter][1])
|
||||
|
||||
|
||||
|
|
|
@ -49,8 +49,8 @@ FILESIZE_METRIC_LIST = [
|
|||
# Main stages of the download operation
|
||||
MAIN_STAGE_QUEUED = 'Queued'
|
||||
MAIN_STAGE_ACTIVE = 'Active'
|
||||
MAIN_STAGE_PAUSED = 'Paused'
|
||||
MAIN_STAGE_COMPLETED = 'Completed'
|
||||
MAIN_STAGE_PAUSED = 'Paused' # (not actually used)
|
||||
MAIN_STAGE_COMPLETED = 'Completed' # (not actually used)
|
||||
MAIN_STAGE_ERROR = 'Error'
|
||||
# Sub-stages of the 'Active' stage
|
||||
ACTIVE_STAGE_PRE_PROCESS = 'Pre-processing'
|
||||
|
|
482
lib/downloads.py
482
lib/downloads.py
|
@ -31,7 +31,7 @@ import datetime
|
|||
import json
|
||||
import signal
|
||||
import os
|
||||
import Queue
|
||||
import queue
|
||||
import requests
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -40,16 +40,11 @@ import time
|
|||
|
||||
|
||||
# Import our modules
|
||||
from constants import MAIN_STAGE_QUEUED, MAIN_STAGE_ACTIVE, \
|
||||
MAIN_STAGE_PAUSED, MAIN_STAGE_COMPLETED, MAIN_STAGE_ERROR, \
|
||||
ACTIVE_STAGE_PRE_PROCESS, ACTIVE_STAGE_DOWNLOAD, ACTIVE_STAGE_POST_PROCESS, \
|
||||
ACTIVE_STAGE_CHECKING, COMPLETED_STAGE_FINISHED, COMPLETED_STAGE_WARNING, \
|
||||
COMPLETED_STAGE_ALREADY, ERROR_STAGE_ERROR, ERROR_STAGE_STOPPED, \
|
||||
ERROR_STAGE_ABORT
|
||||
import mainapp
|
||||
import media
|
||||
import options
|
||||
import utils
|
||||
from . import constants
|
||||
from . import mainapp
|
||||
from . import media
|
||||
from . import options
|
||||
from . import utils
|
||||
|
||||
|
||||
# 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()
|
||||
|
||||
# Exit this loop when there are no more downloads.DownloadItem
|
||||
# objects whose .status is MAIN_STAGE_QUEUED, and when all
|
||||
# workers have finished their downloads
|
||||
# objects whose .status is constants.MAIN_STAGE_QUEUED, and when
|
||||
# all workers have finished their downloads
|
||||
# Otherwise, wait for an available downloads.DownloadWorker, and
|
||||
# then assign the next downloads.DownloadItem to it
|
||||
if not download_item_obj:
|
||||
|
@ -210,7 +205,7 @@ class DownloadManager(threading.Thread):
|
|||
# downloads.DownloadItem
|
||||
self.download_list_obj.change_item_stage(
|
||||
download_item_obj.dbid,
|
||||
MAIN_STAGE_ACTIVE,
|
||||
constants.MAIN_STAGE_ACTIVE,
|
||||
)
|
||||
# Update the main window's progress bar
|
||||
self.job_count += 1
|
||||
|
@ -324,7 +319,7 @@ class DownloadManager(threading.Thread):
|
|||
|
||||
True if all downloads.DownloadWorker objects have finished their
|
||||
jobs, otherwise returns False
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
no available workers.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
for worker_obj in self.worker_list:
|
||||
|
@ -557,7 +552,7 @@ class DownloadWorker(threading.Thread):
|
|||
download_item_obj (downloads.DownloadItem): The download item
|
||||
object describing the URL from which youtube-dl should download
|
||||
video(s).
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.download_item_obj = download_item_obj
|
||||
|
@ -712,7 +707,7 @@ class DownloadList(object):
|
|||
dbid (int): The specified item's .dbid
|
||||
|
||||
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
|
||||
left.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
for dbid in self.download_item_list:
|
||||
this_item = self.download_item_dict[dbid]
|
||||
|
||||
# Don't return an item that's marked as MAIN_STAGE_ACTIVE
|
||||
if this_item.stage == MAIN_STAGE_QUEUED:
|
||||
# Don't return an item that's marked as constants.MAIN_STAGE_ACTIVE
|
||||
if this_item.stage == constants.MAIN_STAGE_QUEUED:
|
||||
return this_item
|
||||
|
||||
return None
|
||||
|
@ -895,7 +890,7 @@ class DownloadItem(object):
|
|||
# A unique ID for this object
|
||||
self.dbid = dbid
|
||||
# The current download stage
|
||||
self.stage = MAIN_STAGE_QUEUED
|
||||
self.stage = constants.MAIN_STAGE_QUEUED
|
||||
|
||||
|
||||
class VideoDownloader(object):
|
||||
|
@ -984,8 +979,8 @@ class VideoDownloader(object):
|
|||
# This object reads from the child process STDOUT and STDERR in an
|
||||
# asynchronous way
|
||||
# Standard Python synchronised queue classes
|
||||
self.stdout_queue = Queue.Queue()
|
||||
self.stderr_queue = Queue.Queue()
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stderr_queue = queue.Queue()
|
||||
# The downloads.PipeReader objects created to handle reading from the
|
||||
# pipes
|
||||
self.stdout_reader = PipeReader(self.stdout_queue)
|
||||
|
@ -1064,7 +1059,7 @@ class VideoDownloader(object):
|
|||
# Public class methods
|
||||
|
||||
|
||||
def do_download(self):
|
||||
def OLDdo_download(self):
|
||||
|
||||
"""Called by downloads.DownloadWorker.run().
|
||||
|
||||
|
@ -1123,8 +1118,9 @@ class VideoDownloader(object):
|
|||
# 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 COMPLETED_STAGE_ALREADY or
|
||||
# ERROR_STAGE_ABORT, set our self.return_code IV
|
||||
# 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
|
||||
|
@ -1183,6 +1179,130 @@ class VideoDownloader(object):
|
|||
# Pass the result back to the parent downloads.DownloadWorker object
|
||||
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):
|
||||
|
||||
|
@ -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().
|
||||
|
||||
|
@ -1515,6 +1635,208 @@ class VideoDownloader(object):
|
|||
# Update the main window
|
||||
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):
|
||||
|
||||
|
@ -1659,8 +1981,8 @@ class VideoDownloader(object):
|
|||
# Extract the data
|
||||
stdout_list[0] = stdout_list[0].lstrip('\r')
|
||||
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
|
||||
if stdout_list[1] == 'Destination:':
|
||||
|
@ -1705,7 +2027,7 @@ class VideoDownloader(object):
|
|||
|
||||
# Get file already downloaded status
|
||||
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(
|
||||
' '.join(stdout_with_spaces_list[1:-4]),
|
||||
)
|
||||
|
@ -1718,14 +2040,14 @@ class VideoDownloader(object):
|
|||
|
||||
# Get filesize abort status
|
||||
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]':
|
||||
|
||||
|
||||
# Get information from the native HLS extractor (see
|
||||
# https://github.com/rg3/youtube-dl/blob/master/youtube_dl/
|
||||
# downloader/hls.py#L54
|
||||
dl_stat_dict['status'] = ACTIVE_STAGE_DOWNLOAD
|
||||
dl_stat_dict['status'] = constants.ACTIVE_STAGE_DOWNLOAD
|
||||
|
||||
if len(stdout_list) == 7:
|
||||
segment_no = float(stdout_list[6])
|
||||
|
@ -1736,12 +2058,12 @@ class VideoDownloader(object):
|
|||
dl_stat_dict['percent'] = percent
|
||||
|
||||
elif stdout_list[0] == '[ffmpeg]':
|
||||
|
||||
|
||||
# Using FFmpeg, not the the native HLS extractor
|
||||
# A successful video download is announced in one of several ways.
|
||||
# Use the first announcement to update self.video_check_dict, and
|
||||
# ignore subsequent announcements
|
||||
dl_stat_dict['status'] = ACTIVE_STAGE_POST_PROCESS
|
||||
dl_stat_dict['status'] = constants.ACTIVE_STAGE_POST_PROCESS
|
||||
|
||||
# Get the final file extension after the merging process has
|
||||
# completed
|
||||
|
@ -1782,7 +2104,7 @@ class VideoDownloader(object):
|
|||
self.confirm_new_video(path, filename, extension)
|
||||
|
||||
elif stdout_list[0][0] == '{':
|
||||
|
||||
|
||||
# JSON data, the result of a simulated download. Convert to a
|
||||
# python dictionary
|
||||
if self.dl_sim_flag:
|
||||
|
@ -1805,17 +2127,17 @@ class VideoDownloader(object):
|
|||
self.video_total += 1
|
||||
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]':
|
||||
|
||||
|
||||
# (Just ignore this output)
|
||||
return dl_stat_dict
|
||||
|
||||
else:
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -1827,12 +2149,13 @@ class VideoDownloader(object):
|
|||
|
||||
Based on YoutubeDLDownloader._extract_info().
|
||||
|
||||
If the job's status is COMPLETED_STAGE_ALREADY or ERROR_STAGE_ABORT,
|
||||
translate that into a new value for the return code, and then use that
|
||||
value to actually set self.return_code (which halts the download).
|
||||
If the job's status is constants.COMPLETED_STAGE_ALREADY or
|
||||
constants.ERROR_STAGE_ABORT, translate that into a new value for the
|
||||
return code, and then use that value to actually set self.return_code
|
||||
(which halts the download).
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
dl_stat_dict (dict): The Python dictionary returned by the call to
|
||||
self.extract_stdout_data(), in the standard form described by
|
||||
the comments for that function
|
||||
|
@ -1840,11 +2163,11 @@ class VideoDownloader(object):
|
|||
"""
|
||||
|
||||
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)
|
||||
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)
|
||||
dl_stat_dict['status'] = None
|
||||
|
||||
|
@ -1859,7 +2182,7 @@ class VideoDownloader(object):
|
|||
youtube-dl.
|
||||
|
||||
Returns:
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
stderr (string): A message from the child process STDERR.
|
||||
|
||||
Returns:
|
||||
|
@ -1944,23 +2267,23 @@ class VideoDownloader(object):
|
|||
dl_stat_dict = {}
|
||||
|
||||
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:
|
||||
dl_stat_dict['status'] = MAIN_STAGE_ERROR
|
||||
dl_stat_dict['status'] = constants.MAIN_STAGE_ERROR
|
||||
dl_stat_dict['eta'] = ''
|
||||
dl_stat_dict['speed'] = ''
|
||||
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['speed'] = ''
|
||||
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['speed'] = ''
|
||||
elif self.return_code == self.ALREADY:
|
||||
dl_stat_dict['status'] = COMPLETED_STAGE_ALREADY
|
||||
dl_stat_dict['status'] = constants.COMPLETED_STAGE_ALREADY
|
||||
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
|
||||
# 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.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
code (int): A return code in the range 0-5
|
||||
|
||||
"""
|
||||
|
@ -2033,7 +2356,7 @@ class PipeReader(threading.Thread):
|
|||
process pipes in an asynchronous way.
|
||||
|
||||
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.
|
||||
|
||||
Warnings:
|
||||
|
@ -2075,7 +2398,7 @@ class PipeReader(threading.Thread):
|
|||
# Public class methods
|
||||
|
||||
|
||||
def run(self):
|
||||
def OLDOLDrun(self):
|
||||
|
||||
"""Called as a result of self.__init__().
|
||||
|
||||
|
@ -2103,6 +2426,42 @@ class PipeReader(threading.Thread):
|
|||
|
||||
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):
|
||||
|
||||
|
@ -2113,9 +2472,9 @@ class PipeReader(threading.Thread):
|
|||
Args:
|
||||
|
||||
filedesc (filehandle): The open filehandle for STDOUT or STDERR
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
self.file_descriptor = filedesc
|
||||
|
||||
|
||||
|
@ -2129,10 +2488,9 @@ class PipeReader(threading.Thread):
|
|||
Args:
|
||||
|
||||
timeout (-): No calling code sets a timeout
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.running_flag = False
|
||||
super(PipeReader, self).join(timeout)
|
||||
|
||||
|
||||
|
|
14
lib/files.py
14
lib/files.py
|
@ -57,7 +57,7 @@ class FileManager(threading.Thread):
|
|||
|
||||
# Public class methods
|
||||
|
||||
|
||||
|
||||
def load_json(self, full_path):
|
||||
|
||||
"""Can be called by anything.
|
||||
|
@ -66,7 +66,7 @@ class FileManager(threading.Thread):
|
|||
dictionary and returns the dictionary.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
full_path (string): The full path to the JSON file
|
||||
|
||||
Returns:
|
||||
|
@ -95,20 +95,20 @@ class FileManager(threading.Thread):
|
|||
Args:
|
||||
|
||||
full_path (string): The full path to the text file
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
The contents of the text file as a string, or or None if the file
|
||||
is missing or can't be loaded
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if not os.path.isfile(full_path):
|
||||
return None
|
||||
|
||||
|
||||
with open(full_path, 'r') as text_file:
|
||||
text = text_file.read()
|
||||
|
||||
|
||||
return text
|
||||
|
||||
|
||||
|
@ -129,7 +129,7 @@ class FileManager(threading.Thread):
|
|||
Returns:
|
||||
|
||||
A GdkPixbuf, or None if the file is missing or can't be loaded
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if not os.path.isfile(full_path):
|
||||
|
|
164
lib/mainapp.py
164
lib/mainapp.py
|
@ -51,17 +51,17 @@ except:
|
|||
|
||||
|
||||
# Import our modules
|
||||
import config
|
||||
import downloads
|
||||
import files
|
||||
import __main__
|
||||
import mainwin
|
||||
import media
|
||||
import options
|
||||
import refresh
|
||||
import testing
|
||||
import updates
|
||||
import utils
|
||||
from . import config
|
||||
from . import downloads
|
||||
from . import files
|
||||
from . import mainwin
|
||||
from . import media
|
||||
from . import options
|
||||
from . import refresh
|
||||
from . import testing
|
||||
from . import updates
|
||||
from . import utils
|
||||
|
||||
|
||||
# Classes
|
||||
|
@ -415,30 +415,34 @@ class TartubeApp(Gtk.Application):
|
|||
# Debugging flags (can only be set by editing the source code)
|
||||
# Delete the config file and the contents of Tartube's data directory
|
||||
# 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
|
||||
# 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
|
||||
# media data objects for testing
|
||||
self.debug_test_media_toolbar_flag = False
|
||||
# Show an dialogue window with 'Tartube is already running!' if the
|
||||
# user tries to open a second instance of Tartube
|
||||
self.debug_warn_multiple_flag = False
|
||||
# self.debug_warn_multiple_flag = False
|
||||
self.debug_warn_multiple_flag = True
|
||||
# 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
|
||||
self.debug_open_pref_win_flag = False
|
||||
# For Tartube developers who don't want to manually change
|
||||
# self.ytdl_path and self.ytdl_update_current on every startup
|
||||
# (assuming that self.debug_delete_data_flag is True), modify those
|
||||
# IVs
|
||||
self.debug_modify_ytdl_flag = False
|
||||
self.debug_ytdl_path = None
|
||||
self.debug_ytdl_update_current = None
|
||||
# self.debug_modify_ytdl_flag = True
|
||||
# self.debug_ytdl_path = 'youtube-dl'
|
||||
# self.debug_ytdl_update_current = 'Update using pip'
|
||||
# self.debug_modify_ytdl_flag = False
|
||||
# self.debug_ytdl_path = None
|
||||
# self.debug_ytdl_update_current = None
|
||||
self.debug_modify_ytdl_flag = True
|
||||
self.debug_ytdl_path = 'youtube-dl'
|
||||
self.debug_ytdl_update_current = 'Update using pip'
|
||||
|
||||
|
||||
def do_startup(self):
|
||||
|
@ -694,7 +698,7 @@ class TartubeApp(Gtk.Application):
|
|||
self.show_msg_dialogue(
|
||||
utils.upper_case_first(__main__.__packagename__) \
|
||||
+ ' is already running!',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'warning',
|
||||
'ok',
|
||||
)
|
||||
|
@ -870,7 +874,7 @@ class TartubeApp(Gtk.Application):
|
|||
'There is ' + string + ' operation in progress.\n' \
|
||||
+ 'Are you sure you want to quit ' \
|
||||
+ utils.upper_case_first(__main__.__packagename__) + '?',
|
||||
True, # Modal
|
||||
True, # Modal
|
||||
'question',
|
||||
'yes-no',
|
||||
)
|
||||
|
@ -925,7 +929,7 @@ class TartubeApp(Gtk.Application):
|
|||
200-299: mainwin.py (in use: 201-234)
|
||||
300-399: downloads.py (in use: 301-303)
|
||||
400-499: config.py (in use: 401-404)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.main_win_obj:
|
||||
|
@ -1445,13 +1449,13 @@ class TartubeApp(Gtk.Application):
|
|||
Args:
|
||||
|
||||
msg (string): The message to display
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.main_win_obj:
|
||||
self.show_msg_dialogue(
|
||||
msg,
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -1565,7 +1569,7 @@ class TartubeApp(Gtk.Application):
|
|||
return self.show_msg_dialogue(
|
||||
'A download operation cannot start\nif one or more' \
|
||||
+ ' configuration\nwindows are still open',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -1601,7 +1605,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
return self.show_msg_dialogue(
|
||||
msg,
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -1736,7 +1740,7 @@ class TartubeApp(Gtk.Application):
|
|||
return self.show_msg_dialogue(
|
||||
'An update operation cannot start\nif one or more' \
|
||||
+ ' configuration\nwindows are still open',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -1763,7 +1767,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
success_flag (True or False): True if the update operation
|
||||
succeeded, False if not
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Any code can check whether a download/update/refresh operation is in
|
||||
|
@ -1792,7 +1796,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
self.show_msg_dialogue(
|
||||
msg,
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'info',
|
||||
'ok',
|
||||
)
|
||||
|
@ -1854,7 +1858,7 @@ class TartubeApp(Gtk.Application):
|
|||
return self.show_msg_dialogue(
|
||||
'A refresh operation cannot start\nif one or more' \
|
||||
+ ' configuration\nwindows are still open',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -1904,7 +1908,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
self.show_msg_dialogue(
|
||||
msg,
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'info',
|
||||
'ok',
|
||||
)
|
||||
|
@ -2299,7 +2303,7 @@ class TartubeApp(Gtk.Application):
|
|||
Returns:
|
||||
|
||||
The new media.Channel object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Channels can only be placed inside an unrestricted media.Folder
|
||||
|
@ -2546,7 +2550,7 @@ class TartubeApp(Gtk.Application):
|
|||
+ 'to the top level of ' \
|
||||
+ utils.upper_case_first(__main__.__packagename__) \
|
||||
+ '\'s data directory',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'question',
|
||||
'yes-no',
|
||||
)
|
||||
|
@ -2606,7 +2610,7 @@ class TartubeApp(Gtk.Application):
|
|||
return self.show_msg_dialogue(
|
||||
'Channels, playlists and folders can\nonly be dragged into' \
|
||||
+ ' a folder',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -2616,7 +2620,7 @@ class TartubeApp(Gtk.Application):
|
|||
return self.show_msg_dialogue(
|
||||
'The fixed folder \'' + dest_obj.name \
|
||||
+ '\'\ncannot be moved (but it can still\nbe hidden)',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -2626,7 +2630,7 @@ class TartubeApp(Gtk.Application):
|
|||
return self.show_msg_dialogue(
|
||||
'The folder \'' + dest_obj.name \
|
||||
+ '\'\ncan only contain videos',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -2655,7 +2659,7 @@ class TartubeApp(Gtk.Application):
|
|||
+ 'This procedure will move all downloaded files\n' \
|
||||
+ 'to the new location' \
|
||||
+ temp_string,
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'question',
|
||||
'yes-no',
|
||||
)
|
||||
|
@ -2810,7 +2814,7 @@ class TartubeApp(Gtk.Application):
|
|||
response2 = self.show_msg_dialogue(
|
||||
'Are you SURE you want to delete files?\nThis procedure' \
|
||||
' cannot be reversed!',
|
||||
True, # Modal
|
||||
True, # Modal
|
||||
'question',
|
||||
'yes-no',
|
||||
)
|
||||
|
@ -2874,7 +2878,7 @@ class TartubeApp(Gtk.Application):
|
|||
no_update_index_flag (True or False): False if the Video Index
|
||||
should not be updated, because the calling function wants to do
|
||||
that itself.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# (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
|
||||
user's filesystem) or not downloaded.
|
||||
|
||||
|
||||
The video object's .dl_flag IV is updated.
|
||||
|
||||
Args:
|
||||
|
@ -2971,7 +2975,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
flag (True or False): True to mark the video as downloaded, False
|
||||
to mark it as not downloaded.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# (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.
|
||||
|
||||
Marks a video object as favourite or not favourite.
|
||||
|
||||
|
||||
The video object's .fav_flag IV is updated.
|
||||
|
||||
Args:
|
||||
|
@ -3061,7 +3065,7 @@ class TartubeApp(Gtk.Application):
|
|||
no_update_index_flag (True or False): False if the Video Index
|
||||
should not be updated, because the calling function wants to do
|
||||
that itself.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# (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.Folder): The media data object to which the download
|
||||
options are applied.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.current_manager_obj \
|
||||
|
@ -3334,7 +3338,7 @@ class TartubeApp(Gtk.Application):
|
|||
media_data_obj (media.Video, media.Channel, media.Playlist or
|
||||
media.Folder): The media data object from which the download
|
||||
options are removed.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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 ' \
|
||||
+ utils.upper_case_first(__main__.__packagename__) \
|
||||
+ '\'s\ndata directory (try downloading the\nvideo again!',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -3466,9 +3470,9 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
|
||||
# (Download operation timer)
|
||||
|
||||
|
||||
|
||||
def timer_callback(self):
|
||||
def timer_callback(self):
|
||||
|
||||
"""Called by gobject timer created by self.download_manager_start().
|
||||
|
||||
|
@ -3483,7 +3487,7 @@ class TartubeApp(Gtk.Application):
|
|||
Returns:
|
||||
|
||||
1 to keep the timer going, or None to halt it
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
# to keep the timer going a little longer
|
||||
return 1
|
||||
|
||||
|
||||
# The download operation has finished. The call to
|
||||
# self.download_manager_finished() destroys the timer
|
||||
self.download_manager_finished()
|
||||
|
||||
|
||||
# (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().
|
||||
|
||||
|
@ -3522,7 +3526,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.operation_halted_flag = True
|
||||
|
@ -3535,7 +3539,7 @@ class TartubeApp(Gtk.Application):
|
|||
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().
|
||||
|
||||
|
@ -3546,7 +3550,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if not self.complex_catalogue_flag:
|
||||
|
@ -3561,7 +3565,7 @@ class TartubeApp(Gtk.Application):
|
|||
self.main_win_obj.video_index_current,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def on_menu_about(self, action, par):
|
||||
|
||||
"""Called from a callback in self.do_startup().
|
||||
|
@ -3573,7 +3577,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
dialogue_win = Gtk.AboutDialog()
|
||||
|
@ -3610,7 +3614,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
action.destroy()
|
||||
|
@ -3654,7 +3658,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
self.show_msg_dialogue(
|
||||
'You must give the channel a name',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -3667,7 +3671,7 @@ class TartubeApp(Gtk.Application):
|
|||
):
|
||||
self.show_msg_dialogue(
|
||||
'You must enter a valid URL',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -3712,7 +3716,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
dialogue_win = mainwin.AddFolderDialogue(self.main_win_obj)
|
||||
|
@ -3736,7 +3740,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
self.show_msg_dialogue(
|
||||
'You must give the folder a name',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -3776,7 +3780,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
dialogue_win = mainwin.AddPlaylistDialogue(self.main_win_obj)
|
||||
|
@ -3802,7 +3806,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
self.show_msg_dialogue(
|
||||
'You must give the playlist a name',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -3815,7 +3819,7 @@ class TartubeApp(Gtk.Application):
|
|||
):
|
||||
self.show_msg_dialogue(
|
||||
'You must enter a valid URL',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -3860,7 +3864,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
dialogue_win = mainwin.AddVideoDialogue(self.main_win_obj)
|
||||
|
@ -3918,7 +3922,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.download_manager_start(True)
|
||||
|
@ -3935,7 +3939,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.download_manager_start(False)
|
||||
|
@ -3952,7 +3956,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
config.OptionsEditWin(self, self.general_options_obj, None)
|
||||
|
@ -3969,7 +3973,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.refresh_manager_start()
|
||||
|
@ -3986,7 +3990,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.save_db()
|
||||
|
@ -3997,7 +4001,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
self.show_msg_dialogue(
|
||||
'Database saved',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'info',
|
||||
'ok',
|
||||
)
|
||||
|
@ -4014,7 +4018,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
for name in self.media_name_dict:
|
||||
|
@ -4038,7 +4042,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
config.SystemPrefWin(self)
|
||||
|
@ -4056,7 +4060,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# 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
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.update_manager_start()
|
||||
|
@ -4107,7 +4111,7 @@ class TartubeApp(Gtk.Application):
|
|||
action (Gio.SimpleAction): Object generated by Gio
|
||||
|
||||
par (None): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.stop()
|
||||
|
@ -4145,7 +4149,7 @@ class TartubeApp(Gtk.Application):
|
|||
self.show_msg_dialogue(
|
||||
'There is already a ' + string + ' with that name\n' \
|
||||
+ '(so please choose a different name)',
|
||||
False, # Not modal
|
||||
False, # Not modal
|
||||
'error',
|
||||
'ok',
|
||||
)
|
||||
|
@ -4219,7 +4223,7 @@ class TartubeApp(Gtk.Application):
|
|||
|
||||
Applies or releases the simultaneous download limit. If a download
|
||||
operation is in progress, the new setting is applied to the next
|
||||
download job.
|
||||
download job.
|
||||
"""
|
||||
|
||||
if not flag:
|
||||
|
|
235
lib/mainwin.py
235
lib/mainwin.py
|
@ -40,13 +40,14 @@ import time
|
|||
|
||||
|
||||
# Import our modules
|
||||
from . import config
|
||||
from . import constants
|
||||
#from . import __main__
|
||||
import __main__
|
||||
import config
|
||||
import constants
|
||||
import mainapp
|
||||
import media
|
||||
import options
|
||||
import utils
|
||||
from . import mainapp
|
||||
from . import media
|
||||
from . import options
|
||||
from . import utils
|
||||
|
||||
|
||||
# Classes
|
||||
|
@ -1255,7 +1256,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
Returns:
|
||||
-1 if row_iter1 comes before row_iter2, 1 if row_iter2 comes before
|
||||
row_iter1, 0 if their order should not be changed
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# If auto-sorting is disabled temporarily, we can prevent the list
|
||||
|
@ -1315,7 +1316,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
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.
|
||||
|
||||
|
@ -1332,7 +1333,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
Returns:
|
||||
-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:
|
||||
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)
|
||||
|
||||
|
@ -1565,11 +1624,11 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
Also called by callbacks in mainapp.TartubeApp.on_menu_add_channel(),
|
||||
.cb on_menu_add_folder() and cb on_menu_add_playlist().
|
||||
|
||||
|
||||
Adds a row to the Video Index.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
media_data_obj (media.Video, media.Channel, media.Playlist,
|
||||
media.Folder): The media data object for this row
|
||||
|
||||
|
@ -1659,10 +1718,10 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
Removes a row from the Video Index.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
media_data_obj (media.Video, media.Channel, media.Playlist,
|
||||
media.Folder): The media data object for this row
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# 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):
|
||||
The media data object whose row should be updated
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Videos can't be shown in the Video Index
|
||||
|
@ -1770,7 +1829,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
# Update the treeview row
|
||||
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_iter = model.get_iter(tree_path)
|
||||
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):
|
||||
The media data object whose row should be updated
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Videos can't be shown in the Video Index
|
||||
if isinstance(media_data_obj, media.Video):
|
||||
return self.app_obj.system_error(
|
||||
|
@ -1811,7 +1870,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
# Update the treeview row
|
||||
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_iter = model.get_iter(tree_path)
|
||||
model.set(tree_iter, 3, self.video_index_get_text(media_data_obj))
|
||||
|
@ -1931,7 +1990,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
A string.
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
text = utils.shorten_string(
|
||||
media_data_obj.name,
|
||||
|
@ -2320,7 +2379,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
"""Called from callbacks in self.on_video_index_selection_changed(),
|
||||
mainapp.TartubeApp.on_button_switch_view(),
|
||||
.on_menu_add_video() and on_menu_test().
|
||||
|
||||
|
||||
When the user clicks on a media data object in the Video Index (a
|
||||
channel, playlist or folder), this function is called to replace the
|
||||
contents of the Video Catalogue with all the video objects stored as
|
||||
|
@ -2339,7 +2398,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
each video.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
name (string): The selected media data object's name; one of the
|
||||
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
|
||||
self.on_video_catalogue_enforce_check().
|
||||
|
||||
|
||||
Also called by mainapp.TartubeApp.create_video_from_download(),
|
||||
.announce_video_download(), .mark_video_new() and
|
||||
.mark_video_favourite().
|
||||
|
||||
|
||||
This function is called with a media.Video object. If that video is
|
||||
already visible in the Video Catalogue, updates the corresponding
|
||||
mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem (which
|
||||
|
@ -2499,7 +2558,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
This function is called with a media.Video object. If that video is
|
||||
already visible in the Video Catalogue, removes the corresponding
|
||||
mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem .
|
||||
|
||||
|
||||
Args:
|
||||
|
||||
video_obj (media.Video) - The video to remove
|
||||
|
@ -2845,7 +2904,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
Progress List.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
download_list_obj (downloads.DownloadList): The download list
|
||||
object that has just been created
|
||||
|
||||
|
@ -3038,7 +3097,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
str,
|
||||
)
|
||||
self.results_list_treeview.set_model(self.results_list_liststore)
|
||||
|
||||
|
||||
# Reset IVs
|
||||
self.results_list_row_count = 0
|
||||
self.results_list_temp_list = []
|
||||
|
@ -3276,7 +3335,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
)
|
||||
|
||||
else:
|
||||
|
||||
|
||||
# File not found
|
||||
|
||||
# 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
|
||||
Tab is number 0)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
self.visible_tab_num = page_num
|
||||
|
||||
if page_num == 2:
|
||||
|
@ -3535,7 +3594,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.app_obj.current_manager_obj:
|
||||
|
@ -3649,9 +3708,9 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
info (int): Ignored
|
||||
|
||||
timestamp (int): Ignored
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Must override the usual Gtk handler
|
||||
treeview.stop_emission('drag_data_received')
|
||||
|
||||
|
@ -3697,7 +3756,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
x, y (int): Cell coordinates in the treeview
|
||||
|
||||
time (int): A timestamp
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Must override the usual Gtk handler
|
||||
|
@ -3722,7 +3781,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Special arrangements for private folders
|
||||
|
@ -3884,7 +3943,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Special arrangements for private folders
|
||||
|
@ -3926,7 +3985,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
directory on the user's filesystem against video objects in the
|
||||
database.
|
||||
|
||||
|
||||
Args:
|
||||
|
||||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.app_obj.current_manager_obj:
|
||||
|
@ -3972,7 +4031,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
|
||||
media_data_obj (media.Channel, media.Playlist or media.Channel):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.app_obj.current_manager_obj \
|
||||
|
@ -3998,9 +4057,9 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
treeview (Gtk.TreeView): The Video Index's treeview
|
||||
|
||||
event (Gdk.EventButton): The event emitting the Gtk signal
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
|
||||
|
||||
# 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
|
||||
"""
|
||||
|
||||
|
||||
(model, iter) = selection.get_selected()
|
||||
|
||||
# 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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
The clicked media data object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.app_obj.current_manager_obj:
|
||||
|
@ -4114,7 +4173,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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())
|
||||
# Update the video catalogue to show the right icon
|
||||
self.video_catalogue_update_row(media_data_obj)
|
||||
|
||||
|
||||
# Open an edit window to show the options immediately
|
||||
config.OptionsEditWin(
|
||||
self.app_obj,
|
||||
|
@ -4147,7 +4206,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.app_obj.current_manager_obj:
|
||||
|
@ -4181,7 +4240,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.app_obj.current_manager_obj:
|
||||
|
@ -4206,7 +4265,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# (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
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.app_obj.current_manager_obj:
|
||||
|
@ -4311,7 +4370,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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)
|
||||
# Update the video catalogue to show the right icon
|
||||
self.video_catalogue_update_row(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
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.app_obj.current_manager_obj:
|
||||
|
@ -4362,7 +4421,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if not media_data_obj.fav_flag:
|
||||
|
@ -4382,7 +4441,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if not media_data_obj.new_flag:
|
||||
|
@ -4402,7 +4461,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Launch the video
|
||||
|
@ -4427,7 +4486,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Launch the video
|
||||
|
@ -4449,7 +4508,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
menu_item (Gtk.MenuItem): The clicked menu item
|
||||
|
||||
media_data_obj (media.Video) - The clicked video object
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Launch the video
|
||||
|
@ -4460,7 +4519,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
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().
|
||||
|
||||
|
@ -4491,7 +4550,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton) - The clicked widget
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.checkbutton.get_active():
|
||||
|
@ -4502,7 +4561,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
)
|
||||
|
||||
else:
|
||||
|
||||
|
||||
self.app_obj.set_num_worker_apply_flag(False)
|
||||
|
||||
|
||||
|
@ -4517,7 +4576,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
Args:
|
||||
|
||||
spinbutton (Gtk.SpinButton): The clicked widget
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.app_obj.set_bandwidth_default(
|
||||
|
@ -4536,7 +4595,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
Args:
|
||||
|
||||
checkbutton (Gtk.CheckButton): The clicked widget
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.app_obj.set_bandwidth_apply_flag(self.checkbutton2.get_active())
|
||||
|
@ -4552,7 +4611,7 @@ class MainWin(Gtk.ApplicationWindow):
|
|||
Args:
|
||||
|
||||
button (Gtk.Button): The clicked widget
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.errors_list_reset()
|
||||
|
@ -4656,7 +4715,7 @@ class SimpleCatalogueItem(object):
|
|||
|
||||
|
||||
# Public class methods
|
||||
|
||||
|
||||
|
||||
def draw_widgets(self, catalogue_row):
|
||||
|
||||
|
@ -4808,7 +4867,7 @@ class SimpleCatalogueItem(object):
|
|||
|
||||
event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the
|
||||
signal emitted by the click
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
|
||||
|
@ -4881,7 +4940,7 @@ class ComplexCatalogueItem(object):
|
|||
|
||||
# Public class methods
|
||||
|
||||
|
||||
|
||||
def draw_widgets(self, catalogue_row):
|
||||
|
||||
"""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
|
||||
signal emitted by the click
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
|
||||
|
@ -5354,7 +5413,7 @@ class ComplexCatalogueItem(object):
|
|||
Returns:
|
||||
|
||||
True to show the action has been handled
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Launch the video
|
||||
|
@ -5383,7 +5442,7 @@ class ComplexCatalogueItem(object):
|
|||
Returns:
|
||||
|
||||
True to show the action has been handled
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Launch the video
|
||||
|
@ -5411,7 +5470,7 @@ class ComplexCatalogueItem(object):
|
|||
Returns:
|
||||
|
||||
True to show the action has been handled
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Launch the video
|
||||
|
@ -5564,7 +5623,7 @@ class AddVideoDialogue(Gtk.Dialog):
|
|||
|
||||
# Public class methods
|
||||
|
||||
|
||||
|
||||
def on_combo_changed(self, combo):
|
||||
|
||||
"""Called from callback in self.__init__().
|
||||
|
@ -5575,7 +5634,7 @@ class AddVideoDialogue(Gtk.Dialog):
|
|||
Args:
|
||||
|
||||
combo (Gtk.ComboBox): The clicked widget
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.parent_name = self.folder_list[combo.get_active()]
|
||||
|
@ -5700,7 +5759,7 @@ class AddChannelDialogue(Gtk.Dialog):
|
|||
|
||||
# Public class methods
|
||||
|
||||
|
||||
|
||||
def on_combo_changed(self, combo):
|
||||
|
||||
"""Called from callback in self.__init__().
|
||||
|
@ -5711,7 +5770,7 @@ class AddChannelDialogue(Gtk.Dialog):
|
|||
Args:
|
||||
|
||||
combo (Gtk.ComboBox): The clicked widget
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.parent_name = self.folder_list[combo.get_active()]
|
||||
|
@ -5836,7 +5895,7 @@ class AddPlaylistDialogue(Gtk.Dialog):
|
|||
|
||||
# Public class methods
|
||||
|
||||
|
||||
|
||||
def on_combo_changed(self, combo):
|
||||
|
||||
"""Called from callback in self.__init__().
|
||||
|
@ -5847,7 +5906,7 @@ class AddPlaylistDialogue(Gtk.Dialog):
|
|||
Args:
|
||||
|
||||
combo (Gtk.ComboBox): The clicked widget
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.parent_name = self.folder_list[combo.get_active()]
|
||||
|
@ -5958,7 +6017,7 @@ class AddFolderDialogue(Gtk.Dialog):
|
|||
|
||||
# Public class methods
|
||||
|
||||
|
||||
|
||||
def on_combo_changed(self, combo):
|
||||
|
||||
"""Called from callback in self.__init__().
|
||||
|
@ -5969,7 +6028,7 @@ class AddFolderDialogue(Gtk.Dialog):
|
|||
Args:
|
||||
|
||||
combo (Gtk.ComboBox): The clicked widget
|
||||
|
||||
|
||||
"""
|
||||
|
||||
self.parent_name = self.folder_list[combo.get_active()]
|
||||
|
|
164
lib/media.py
164
lib/media.py
|
@ -26,14 +26,14 @@
|
|||
|
||||
# Import other modules
|
||||
import datetime
|
||||
import functools
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
# Import our modules
|
||||
import functools
|
||||
import mainapp
|
||||
import time
|
||||
import utils
|
||||
from . import mainapp
|
||||
from . import utils
|
||||
|
||||
|
||||
# Classes
|
||||
|
@ -121,7 +121,7 @@ class GenericContainer(GenericMedia):
|
|||
video_list (list): A list of media.Video objects
|
||||
|
||||
Returns:
|
||||
|
||||
|
||||
The modified video_list
|
||||
|
||||
"""
|
||||
|
@ -139,7 +139,7 @@ class GenericContainer(GenericMedia):
|
|||
def count_descendants(self, count_list):
|
||||
|
||||
"""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.
|
||||
|
||||
Counts the number of child objects, and then calls this function
|
||||
|
@ -155,7 +155,7 @@ class GenericContainer(GenericMedia):
|
|||
)
|
||||
|
||||
Returns:
|
||||
|
||||
|
||||
The modified count_list
|
||||
|
||||
"""
|
||||
|
@ -196,7 +196,7 @@ class GenericContainer(GenericMedia):
|
|||
was not a child of this object
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Check this is really one of our children
|
||||
index = self.find_child_index(child_obj)
|
||||
if index is None:
|
||||
|
@ -227,7 +227,7 @@ class GenericContainer(GenericMedia):
|
|||
|
||||
An integer describing the position in self.child_list, or None of
|
||||
the child object is not found in self.child_list
|
||||
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
|
@ -252,7 +252,7 @@ class GenericContainer(GenericMedia):
|
|||
Returns:
|
||||
|
||||
The container object's level
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.parent_obj is None:
|
||||
|
@ -283,7 +283,7 @@ class GenericContainer(GenericMedia):
|
|||
can't be hidden directly.)
|
||||
|
||||
Returns:
|
||||
|
||||
|
||||
True or False.
|
||||
|
||||
"""
|
||||
|
@ -357,7 +357,7 @@ class GenericContainer(GenericMedia):
|
|||
Returns:
|
||||
|
||||
The full path to the directory
|
||||
|
||||
|
||||
"""
|
||||
|
||||
dir_list = [self.name]
|
||||
|
@ -401,6 +401,40 @@ class GenericRemoteContainer(GenericContainer):
|
|||
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):
|
||||
|
||||
"""Sorting function used by functools.cmp_to_key(), and called by
|
||||
|
@ -414,16 +448,21 @@ class GenericRemoteContainer(GenericContainer):
|
|||
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:
|
||||
# # 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:
|
||||
|
@ -620,7 +659,7 @@ class Video(GenericMedia):
|
|||
favourite.
|
||||
|
||||
Returns:
|
||||
|
||||
|
||||
True if the parent (or the parent's parent, and so on) is marked
|
||||
favourite, False otherwise
|
||||
|
||||
|
@ -649,7 +688,7 @@ class Video(GenericMedia):
|
|||
|
||||
max_length (int): When storing the description in this object's
|
||||
IVs, the maximum line length to use
|
||||
|
||||
|
||||
"""
|
||||
|
||||
descrip_path = os.path.join(
|
||||
|
@ -774,7 +813,7 @@ class Video(GenericMedia):
|
|||
max_length (int): A maximum line size
|
||||
|
||||
"""
|
||||
|
||||
|
||||
if descrip:
|
||||
|
||||
self.descrip = utils.tidy_up_long_descrip(descrip, max_length)
|
||||
|
@ -797,7 +836,7 @@ class Video(GenericMedia):
|
|||
Returns:
|
||||
|
||||
The converted string, or None if self.file_size is not set
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.file_size:
|
||||
|
@ -816,7 +855,7 @@ class Video(GenericMedia):
|
|||
Returns:
|
||||
|
||||
The formatted string, or None if self.receive_time is not set
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.receive_time:
|
||||
|
@ -835,7 +874,7 @@ class Video(GenericMedia):
|
|||
Returns:
|
||||
|
||||
The formatted string, or None if self.receive_time is not set
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.receive_time:
|
||||
|
@ -854,7 +893,7 @@ class Video(GenericMedia):
|
|||
Returns:
|
||||
|
||||
The formatted string, or None if self.upload_time is not set
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.upload_time:
|
||||
|
@ -873,7 +912,7 @@ class Video(GenericMedia):
|
|||
Returns:
|
||||
|
||||
The formatted string, or None if self.upload_time is not set
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if self.upload_time:
|
||||
|
@ -1287,7 +1326,7 @@ class Folder(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
|
||||
self.sort_children().
|
||||
|
@ -1301,14 +1340,14 @@ class Folder(GenericContainer):
|
|||
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__) \
|
||||
|
@ -1351,6 +1390,75 @@ class Folder(GenericContainer):
|
|||
else:
|
||||
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
|
||||
|
||||
|
|
|
@ -29,10 +29,10 @@ import os
|
|||
|
||||
|
||||
# Import our modules
|
||||
import constants
|
||||
import mainapp
|
||||
import media
|
||||
import utils
|
||||
from . import constants
|
||||
from . import mainapp
|
||||
from . import media
|
||||
from . import utils
|
||||
|
||||
|
||||
# Classes
|
||||
|
@ -539,7 +539,7 @@ class OptionsParser(object):
|
|||
taken from options.OptionsManager.options_dict
|
||||
|
||||
Returns:
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
copy_dict (dict): Copy of the original options dictionary.
|
||||
|
||||
Returns:
|
||||
|
||||
|
||||
True if any of the required options is enabled, otherwise returns
|
||||
False.
|
||||
|
||||
|
@ -845,7 +845,7 @@ class OptionHolder(object):
|
|||
Returns:
|
||||
|
||||
True if the option is a boolean switch, otherwise returns False
|
||||
|
||||
|
||||
"""
|
||||
|
||||
return type(self.default_value) is bool
|
||||
|
|
|
@ -25,13 +25,13 @@
|
|||
|
||||
|
||||
# Import other modules
|
||||
import constants
|
||||
import os
|
||||
import threading
|
||||
|
||||
|
||||
# Import our modules
|
||||
import media
|
||||
from . import constants
|
||||
from . import media
|
||||
|
||||
|
||||
# Classes
|
||||
|
@ -80,7 +80,7 @@ class RefreshManager(threading.Thread):
|
|||
|
||||
# Public class methods
|
||||
|
||||
|
||||
|
||||
def run(self):
|
||||
|
||||
"""Called by mainapp.TartubeApp.refresh_manager_start().
|
||||
|
|
|
@ -51,7 +51,7 @@ def add_test_media(app_obj):
|
|||
Args:
|
||||
|
||||
app_obj (mainapp.TartubeApp): The main application
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Test videos
|
||||
|
|
108
lib/updates.py
108
lib/updates.py
|
@ -26,7 +26,8 @@
|
|||
|
||||
# Import other modules
|
||||
import os
|
||||
import Queue
|
||||
import queue
|
||||
import re
|
||||
import requests
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -34,9 +35,8 @@ import threading
|
|||
|
||||
|
||||
# Import our modules
|
||||
import downloads
|
||||
import re
|
||||
import utils
|
||||
from . import downloads
|
||||
from . import utils
|
||||
|
||||
|
||||
# Classes
|
||||
|
@ -74,8 +74,8 @@ class UpdateManager(threading.Thread):
|
|||
# This object reads from the child process STDOUT and STDERR in an
|
||||
# asynchronous way
|
||||
# Standard Python synchronised queue classes
|
||||
self.stdout_queue = Queue.Queue()
|
||||
self.stderr_queue = Queue.Queue()
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stderr_queue = queue.Queue()
|
||||
# The downloads.PipeReader objects created to handle reading from the
|
||||
# pipes
|
||||
self.stdout_reader = downloads.PipeReader(self.stdout_queue)
|
||||
|
@ -104,8 +104,8 @@ class UpdateManager(threading.Thread):
|
|||
|
||||
# Public class methods
|
||||
|
||||
|
||||
def run(self):
|
||||
|
||||
def OLDrun(self):
|
||||
|
||||
"""Called as a result of self.__init__().
|
||||
|
||||
|
@ -118,7 +118,7 @@ class UpdateManager(threading.Thread):
|
|||
"""
|
||||
|
||||
# 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
|
||||
|
@ -189,6 +189,94 @@ class UpdateManager(threading.Thread):
|
|||
else:
|
||||
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):
|
||||
|
||||
|
@ -263,7 +351,7 @@ class UpdateManager(threading.Thread):
|
|||
|
||||
"""Called by mainapp.TartubeApp.on_button_stop_operation(), .stop() and
|
||||
a callback in .on_button_stop_operation().
|
||||
|
||||
|
||||
Based on code from downloads.VideoDownloader.stop().
|
||||
|
||||
Terminates the child process.
|
||||
|
|
24
lib/utils.py
24
lib/utils.py
|
@ -36,10 +36,10 @@ import textwrap
|
|||
|
||||
|
||||
# Import our modules
|
||||
import constants
|
||||
import mainapp
|
||||
from . import constants
|
||||
from . import mainapp
|
||||
if mainapp.HAVE_VALIDATORS_FLAG:
|
||||
import validators
|
||||
from . import validators
|
||||
|
||||
|
||||
# Functions
|
||||
|
@ -90,7 +90,7 @@ def convert_item(item, to_unicode=False):
|
|||
Convert item between 'unicode' and 'str'.
|
||||
|
||||
Args:
|
||||
|
||||
|
||||
item (-): Can be any python item.
|
||||
|
||||
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'.
|
||||
|
||||
Returns:
|
||||
|
||||
|
||||
The converted item
|
||||
|
||||
"""
|
||||
|
@ -310,7 +310,7 @@ def format_bytes(num_bytes):
|
|||
Returns:
|
||||
|
||||
The formatted string
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if num_bytes == 0.0:
|
||||
|
@ -332,7 +332,7 @@ def get_encoding():
|
|||
Returns:
|
||||
|
||||
The system encoding.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
|
@ -354,7 +354,7 @@ def open_file(uri):
|
|||
Args:
|
||||
|
||||
uri (string): The URI to open
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if sys.platform == "win32":
|
||||
|
@ -378,7 +378,7 @@ def remove_shortcuts(path):
|
|||
Returns:
|
||||
|
||||
The converted path
|
||||
|
||||
|
||||
"""
|
||||
|
||||
return path.replace('~', os.path.expanduser('~'))
|
||||
|
@ -399,7 +399,7 @@ def shorten_string(string, num_chars):
|
|||
Returns:
|
||||
|
||||
The converted string
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if string and len(string) > num_chars:
|
||||
|
@ -423,7 +423,7 @@ def to_string(data):
|
|||
Returns:
|
||||
|
||||
The converted string
|
||||
|
||||
|
||||
"""
|
||||
|
||||
return '%s' % data
|
||||
|
@ -441,7 +441,7 @@ def upper_case_first(string):
|
|||
Returns:
|
||||
|
||||
The converted string
|
||||
|
||||
|
||||
"""
|
||||
|
||||
return string[0].upper() + string[1:]
|
||||
|
|
25
setup.py
25
setup.py
|
@ -21,27 +21,30 @@
|
|||
|
||||
|
||||
# Import modules
|
||||
from setuptools import setup, find_packages
|
||||
import setuptools
|
||||
|
||||
|
||||
# Import documents
|
||||
with open('README.rst') as f:
|
||||
readme = f.read()
|
||||
|
||||
with open('LICENSE') as f:
|
||||
license = f.read()
|
||||
#with open('README.rst', 'r') as f:
|
||||
# long_description = f.read()
|
||||
#
|
||||
#with open('LICENSE') as f:
|
||||
# license = f.read()
|
||||
|
||||
|
||||
# Setup
|
||||
setup(
|
||||
setuptools.setup(
|
||||
name='tartube',
|
||||
version='0.1.000',
|
||||
version='0.1.007',
|
||||
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_email='aslewis@cpan.org',
|
||||
url='https://github.com/axcore/tartube',
|
||||
license=license,
|
||||
packages=find_packages(exclude=('tests', 'docs'))
|
||||
# license=license,
|
||||
license="""GPL3+""",
|
||||
packages=setuptools.find_packages()
|
||||
)
|
||||
|
||||
|
|
|
@ -34,8 +34,8 @@ from lib import mainapp
|
|||
|
||||
# 'Global' variables
|
||||
__packagename__ = 'tartube'
|
||||
__version__ = '0.1.000'
|
||||
__date__ = '27 May 2019'
|
||||
__version__ = '0.1.007'
|
||||
__date__ = '28 May 2019'
|
||||
__copyright__ = 'Copyright \xc2\xa9 2019 A S Lewis'
|
||||
__license__ = """
|
||||
Copyright \xc2\xa9 2019 A S Lewis.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
|
Loading…
Reference in New Issue