Add files via upload

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

254
README.rst Normal file
View File

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

View File

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

1
docs/empty.md Normal file
View File

@ -0,0 +1 @@
#Tartube

View File

@ -31,11 +31,11 @@ import os
# Import our modules
import 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])

View File

@ -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'

View File

@ -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)

View File

@ -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):

View File

@ -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:

View File

@ -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()]

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -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.

View File

@ -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:]

View File

@ -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()
)

View File

@ -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.

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@