Add files via upload

This commit is contained in:
A S Lewis 2019-06-03 10:57:30 +01:00 committed by GitHub
parent 424ccecc6b
commit 8cca9e28d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 899 additions and 971 deletions

View File

@ -10,6 +10,12 @@ 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>`__.
Screenshots
-----------
.. image:: screenshots/tartube.png
:alt: Tartube screenshot
Why should I use Tartube?
-------------------------
@ -20,8 +26,7 @@ Why should I use Tartube?
- 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
conceal videos which challenge their preferred political views. Tartube won't do any of those things
- Tartube can, in some circumstances, see videos that are
region-blocked and age-restricted
@ -50,7 +55,7 @@ Install from source
1. Download & extract the source
2. Change directory into the Tartube directory
3. Run ``python setup.py install``
3. Run ``python3 setup.py install``
Install using PyPI
~~~~~~~~~~~~~~~~~~
@ -67,7 +72,7 @@ Run without installing
1. Download & extract the source
2. Change directory into the Tartube directory
3. Run 'python tartube.py'
3. Run 'python3 tartube.py'
Frequently-Asked Questions
--------------------------

View File

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

@ -98,6 +98,8 @@ class DownloadManager(threading.Thread):
def __init__(self, app_obj, force_sim_flag, download_list_obj):
print('dl 101 __init__')
super(DownloadManager, self).__init__()
# IV list - class objects
@ -172,6 +174,8 @@ class DownloadManager(threading.Thread):
download operation is complete.
"""
print('dl 177 run')
# Perform the download operation until there is nothing left to
# download, or until something has called
# self.stop_download_operation()
@ -267,6 +271,8 @@ class DownloadManager(threading.Thread):
"""
print('dl 274 change_worker_count')
# How many workers do we have already?
current = len(self.worker_list)
# If this object hasn't set up its worker pool yet, let the setup code
@ -319,9 +325,11 @@ class DownloadManager(threading.Thread):
True if all downloads.DownloadWorker objects have finished their
jobs, otherwise returns False
"""
print('dl 331 check_workers_all_finished')
for worker_obj in self.worker_list:
if not worker_obj.available_flag:
return False
@ -339,9 +347,11 @@ class DownloadManager(threading.Thread):
The first available downloads.DownloadWorker, or None if there are
no available workers.
"""
print('dl 353 get_available_worker')
for worker_obj in self.worker_list:
if worker_obj.available_flag:
return worker_obj
@ -362,6 +372,8 @@ class DownloadManager(threading.Thread):
"""
print('dl 375 remove_worker')
new_list = []
for other_obj in self.worker_list:
@ -382,6 +394,8 @@ class DownloadManager(threading.Thread):
loop, the downloads.DownloadWorker objects are cleaned up.
"""
print('dl 397 stop_download_operation')
self.running_flag = False
@ -411,6 +425,8 @@ class DownloadWorker(threading.Thread):
def __init__(self, download_manager_obj):
print('dl 428 __init__')
super(DownloadWorker, self).__init__()
# IV list - class objects
@ -465,6 +481,8 @@ class DownloadWorker(threading.Thread):
create a new downloads.VideoDownloader object and wait for the result.
"""
print('dl 484 run')
# Import the main application (for convenience)
app_obj = self.download_manager_obj.app_obj
@ -534,6 +552,8 @@ class DownloadWorker(threading.Thread):
Tidy up IVs and stop any child processes.
"""
print('dl 555 close')
self.running_flag = False
if self.video_downloader_obj:
self.video_downloader_obj.stop()
@ -552,9 +572,11 @@ class DownloadWorker(threading.Thread):
download_item_obj (downloads.DownloadItem): The download item
object describing the URL from which youtube-dl should download
video(s).
"""
print('dl 578 prepare_download')
self.download_item_obj = download_item_obj
self.options_manager_obj = download_item_obj.options_manager_obj
self.options_list = self.download_manager_obj.options_parser_obj.parse(
@ -569,6 +591,8 @@ class DownloadWorker(threading.Thread):
"""Called by downloads.DownloadManager.change_worker_count()."""
print('dl 594 set_doomed_flag')
self.doomed_flag = flag
@ -595,6 +619,8 @@ class DownloadWorker(threading.Thread):
"""
print('dl 622 data_callback')
app_obj = self.download_manager_obj.app_obj
app_obj.main_win_obj.progress_list_receive_dl_stats(
self.download_item_obj,
@ -635,6 +661,8 @@ class DownloadList(object):
def __init__(self, app_obj, media_data_obj):
print('dl 664 __init__')
# IV list - class objects
# -----------------------
self.app_obj = app_obj
@ -711,6 +739,8 @@ class DownloadList(object):
"""
print('dl 742 change_item_stage')
self.download_item_dict[dbid].stage = new_stage
@ -743,6 +773,8 @@ class DownloadList(object):
"""
print('dl 776 create_item')
# Get the options.OptionsManager object that applies to this media
# data object
# (The manager might be specified by obj itself, or it might be
@ -805,9 +837,11 @@ class DownloadList(object):
The next downloads.DownloadItem object, or None if there are none
left.
"""
print('dl 843 fetch_next_item')
for dbid in self.download_item_list:
this_item = self.download_item_dict[dbid]
@ -841,6 +875,8 @@ class DownloadList(object):
"""
print('dl 878 get_options_manager')
if media_data_obj.options_obj:
return media_data_obj.options_obj
elif media_data_obj.parent_obj:
@ -877,6 +913,8 @@ class DownloadItem(object):
def __init__(self, dbid, media_data_obj, options_manager_obj):
print('dl 916 __init__')
# IV list - class objects
# -----------------------
# The media data object to be downloaded
@ -965,6 +1003,8 @@ class VideoDownloader(object):
def __init__(self, download_manager_obj, download_worker_obj, \
download_item_obj):
print('dl 1006 __init__')
# IV list - class objects
# -----------------------
# The downloads.DownloadManager object handling the entire download
@ -1059,126 +1099,6 @@ class VideoDownloader(object):
# Public class methods
def OLDdo_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():
stdout = self.stdout_queue.get_nowait().rstrip()
stdout = utils.convert_item(stdout, to_unicode=True)
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
stderr = self.stderr_queue.get_nowait().rstrip()
stderr = utils.convert_item(stderr, to_unicode=True)
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 do_download(self):
"""Called by downloads.DownloadWorker.run().
@ -1194,6 +1114,8 @@ class VideoDownloader(object):
"""
print('dl 1117 do_download')
# Import the main application (for convenience)
app_obj = self.download_manager_obj.app_obj
@ -1257,11 +1179,11 @@ class VideoDownloader(object):
# 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)
# # (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)
@ -1311,6 +1233,8 @@ class VideoDownloader(object):
Destructor function for this object.
"""
print('dl 1236 close')
# Tell the PipeReader objects to shut down, thus joining their threads
self.stdout_reader.join()
self.stderr_reader.join()
@ -1337,6 +1261,8 @@ class VideoDownloader(object):
"""
print('dl 1264 confirm_new_video')
if not self.video_num in self.video_check_dict:
self.video_check_dict[self.video_num] = filename
@ -1386,6 +1312,8 @@ class VideoDownloader(object):
"""
print('dl 1315 confirm_old_video')
# Create shortcut variables (for convenience)
app_obj = self.download_manager_obj.app_obj
media_data_obj = self.download_item_obj.media_data_obj
@ -1435,206 +1363,6 @@ class VideoDownloader(object):
)
def OLDconfirm_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):
fh = open(descrip_path, 'w')
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 confirm_sim_video(self, json_dict):
"""Called by self.extract_stdout_data().
@ -1649,7 +1377,9 @@ class VideoDownloader(object):
"""
# IMport the main application (for convenience)
print('dl 1380 confirm_sim_video')
# Import the main application (for convenience)
app_obj = self.download_manager_obj.app_obj
# From the JSON dictionary, extract the data we need
@ -1859,6 +1589,8 @@ class VideoDownloader(object):
"""
print('dl 1592 create_child_process')
info = preexec = None
if os.name == 'nt':
# Hide the child process window that MS Windows helpfully creates
@ -1911,6 +1643,8 @@ class VideoDownloader(object):
"""
print('dl 1646 extract_filename')
path, fullname = os.path.split(input_data.strip("\""))
filename, extension = os.path.splitext(fullname)
@ -1956,6 +1690,8 @@ class VideoDownloader(object):
"""
print('dl 1693 extract_stdout_data')
# Initialise the dictionary with default key-value pairs for the main
# window to display, to be overwritten (if possible) with new key-
# value pairs as this function interprets the STDOUT message
@ -1981,7 +1717,7 @@ class VideoDownloader(object):
# Extract the data
stdout_list[0] = stdout_list[0].lstrip('\r')
if stdout_list[0] == '[download]':
dl_stat_dict['status'] = constants.ACTIVE_STAGE_DOWNLOAD
# Get path, filename and extension
@ -2043,7 +1779,7 @@ class VideoDownloader(object):
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
@ -2058,7 +1794,7 @@ 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
@ -2104,7 +1840,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:
@ -2130,12 +1866,12 @@ class VideoDownloader(object):
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'] = constants.ACTIVE_STAGE_PRE_PROCESS
@ -2155,13 +1891,15 @@ class VideoDownloader(object):
(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
"""
print('dl 1901 extract_stdout_status')
if 'status' in dl_stat_dict:
if dl_stat_dict['status'] == constants.COMPLETED_STAGE_ALREADY:
self.set_return_code(self.ALREADY)
@ -2182,11 +1920,13 @@ class VideoDownloader(object):
youtube-dl.
Returns:
Python list that contains the system command to execute.
"""
print('dl 1928 get_system_cmd')
options_list = self.download_worker_obj.options_list
# Simulate the download, rather than actually downloading videos, if
@ -2220,6 +1960,8 @@ class VideoDownloader(object):
"""
print('dl 1963 is_child_process_alive')
if self.child_process is None:
return False
@ -2236,7 +1978,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:
@ -2245,6 +1987,8 @@ class VideoDownloader(object):
"""
print('dl 1990 is_warning')
return stderr.split(':')[0] == 'WARNING'
@ -2264,6 +2008,8 @@ class VideoDownloader(object):
The new key-value pairs are used to update the main window.
"""
print('dl 2011 last_data_callback')
dl_stat_dict = {}
if self.return_code == self.OK:
@ -2308,11 +2054,13 @@ 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
"""
print('dl 2062 set_return_code')
if code >= self.return_code:
self.return_code = code
@ -2325,6 +2073,8 @@ class VideoDownloader(object):
self.STOPPED.
"""
print('dl 2076 stop')
if self.is_child_process_alive():
if os.name == 'nt':
@ -2371,6 +2121,8 @@ class PipeReader(threading.Thread):
def __init__(self, queue):
print('dl 2124 __init__')
super(PipeReader, self).__init__()
# IV list - other
@ -2398,34 +2150,6 @@ class PipeReader(threading.Thread):
# Public class methods
def OLDOLDrun(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:
for line in iter(self.file_descriptor.readline, str('')):
if str('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 run(self):
"""Called as a result of self.__init__().
@ -2433,6 +2157,8 @@ class PipeReader(threading.Thread):
Reads from STDOUT or STERR using the attached filed descriptor.
"""
print('dl 2160 run')
# Use this flag so that the loop can ignore FFmpeg error messsages
# (because the parent VideoDownloader object shouldn't use that as a
# serious error)
@ -2472,9 +2198,11 @@ class PipeReader(threading.Thread):
Args:
filedesc (filehandle): The open filehandle for STDOUT or STDERR
"""
print('dl 2204 attach_file_descriptor')
self.file_descriptor = filedesc
@ -2488,9 +2216,11 @@ class PipeReader(threading.Thread):
Args:
timeout (-): No calling code sets a timeout
"""
print('dl 2222 join')
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):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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,40 +401,6 @@ 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
@ -448,13 +414,13 @@ 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
"""
# # Convert Python2 to Python3
@ -659,7 +625,7 @@ class Video(GenericMedia):
favourite.
Returns:
True if the parent (or the parent's parent, and so on) is marked
favourite, False otherwise
@ -688,7 +654,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(
@ -813,7 +779,7 @@ class Video(GenericMedia):
max_length (int): A maximum line size
"""
if descrip:
self.descrip = utils.tidy_up_long_descrip(descrip, max_length)
@ -836,7 +802,7 @@ class Video(GenericMedia):
Returns:
The converted string, or None if self.file_size is not set
"""
if self.file_size:
@ -855,7 +821,7 @@ class Video(GenericMedia):
Returns:
The formatted string, or None if self.receive_time is not set
"""
if self.receive_time:
@ -874,7 +840,7 @@ class Video(GenericMedia):
Returns:
The formatted string, or None if self.receive_time is not set
"""
if self.receive_time:
@ -893,7 +859,7 @@ class Video(GenericMedia):
Returns:
The formatted string, or None if self.upload_time is not set
"""
if self.upload_time:
@ -912,7 +878,7 @@ class Video(GenericMedia):
Returns:
The formatted string, or None if self.upload_time is not set
"""
if self.upload_time:
@ -1326,70 +1292,6 @@ class Folder(GenericContainer):
# def del_child(): # Inherited from GenericContainer
def OLDdo_sort(self, obj1, obj2):
"""Sorting function used by functools.cmp_to_key(), and called by
self.sort_children().
Sorts the child media.Video, media.Channel, media.Playlist and
media.Folder objects.
Firstly, sort by class - folders, channels/playlists, then videos.
Within folders, channels and playlists, sort alphabetically. Within
videos, sort by upload time.
Args:
obj1, obj2 (media.Video, media.Channel, media.Playlist or
media.Folder) - Media data objects being sorted
Returns:
-1 if obj1 comes first, 1 if obj2 comes first, 0 if they are equal
"""
if str(obj1.__class__) == str(obj2.__class__) \
or (
isinstance(obj1, GenericRemoteContainer) \
and isinstance(obj2, GenericRemoteContainer)
):
if isinstance(obj1, Video):
if obj1.upload_time < 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 do_sort(self, obj1, obj2):
"""Sorting function used by functools.cmp_to_key(), and called by
@ -1404,14 +1306,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__) \

View File

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

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

@ -104,90 +104,6 @@ class UpdateManager(threading.Thread):
# Public class methods
def OLDrun(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():
stdout = self.stdout_queue.get_nowait().rstrip()
stdout = utils.convert_item(stdout, to_unicode=True)
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
stderr = self.stderr_queue.get_nowait().rstrip()
stderr = utils.convert_item(stderr, to_unicode=True)
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 run(self):
@ -202,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
@ -251,11 +167,11 @@ class UpdateManager(threading.Thread):
# 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)
# # (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)
@ -351,7 +267,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

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

BIN
screenshots/tartube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

@ -35,11 +35,11 @@ import setuptools
# Setup
setuptools.setup(
name='tartube',
version='0.1.007',
version='0.1.015',
description='GUI front-end for youtube-dl',
# 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',
long_description_content_type='text/markdown',
author='A S Lewis',
author_email='aslewis@cpan.org',
url='https://github.com/axcore/tartube',

View File

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