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

@ -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
@ -322,6 +328,8 @@ class DownloadManager(threading.Thread):
"""
print('dl 331 check_workers_all_finished')
for worker_obj in self.worker_list:
if not worker_obj.available_flag:
return False
@ -342,6 +350,8 @@ class DownloadManager(threading.Thread):
"""
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()
@ -555,6 +575,8 @@ class DownloadWorker(threading.Thread):
"""
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
@ -808,6 +840,8 @@ class DownloadList(object):
"""
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
@ -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
@ -2162,6 +1898,8 @@ class VideoDownloader(object):
"""
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)
@ -2187,6 +1925,8 @@ class VideoDownloader(object):
"""
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
@ -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:
@ -2313,6 +2059,8 @@ class VideoDownloader(object):
"""
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)
@ -2475,6 +2201,8 @@ class PipeReader(threading.Thread):
"""
print('dl 2204 attach_file_descriptor')
self.file_descriptor = filedesc
@ -2491,6 +2219,8 @@ class PipeReader(threading.Thread):
"""
print('dl 2222 join')
self.running_flag = False
super(PipeReader, self).join(timeout)

View File

@ -77,6 +77,8 @@ class TartubeApp(Gtk.Application):
def __init__(self, *args, **kwargs):
print('ap 80 __init__')
super(TartubeApp, self).__init__(
*args,
application_id=__main__.__app_id__,
@ -350,7 +352,8 @@ class TartubeApp(Gtk.Application):
self.operation_save_flag = True
# Flag set to True if a dialogue window should be shown at the end of
# each download/update/refresh operation
self.operation_dialogue_flag = True
# self.operation_dialogue_flag = True
self.operation_dialogue_flag = False
# Flag set to True if self.update_video_from_filesystem() should get
# the video duration, if not already known, using the moviepy.editor
# module (which may be slow)
@ -366,7 +369,8 @@ class TartubeApp(Gtk.Application):
# and then waits for the results, an increase in this number is
# applied to a download operation immediately, but a decrease is not
# applied until one of the download jobs has finished
self.num_worker_default = 2
# self.num_worker_default = 2
self.num_worker_default = 1
# (Absoute minimum and maximum values)
self.num_worker_max = 10
self.num_worker_min = 1
@ -415,40 +419,38 @@ 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 = True
self.debug_delete_data_flag = False
# In the main window's menu, show a menu item for adding a set of
# media data objects for testing
# self.debug_test_media_menu_flag = False
self.debug_test_media_menu_flag = True
self.debug_test_media_menu_flag = False
# In the main window's toolbar, show a toolbar item for adding a set of
# media data objects for testing
self.debug_test_media_toolbar_flag = False
# Show an dialogue window with 'Tartube is already running!' if the
# user tries to open a second instance of Tartube
# self.debug_warn_multiple_flag = False
self.debug_warn_multiple_flag = True
self.debug_warn_multiple_flag = False
# Open the main window in the top-left corner of the desktop
# self.debug_open_top_left_flag = False
self.debug_open_top_left_flag = True
self.debug_open_top_left_flag = False
# 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):
"""Gio.Application standard function."""
print('ap 454 do_startup')
GObject.threads_init()
Gtk.Application.do_startup(self)
@ -681,6 +683,8 @@ class TartubeApp(Gtk.Application):
"""Gio.Application standard function."""
print('ap 684 do_activate')
# Only allow a single main window (raise any existing main windows)
if not self.main_win_obj:
self.start()
@ -712,6 +716,8 @@ class TartubeApp(Gtk.Application):
handled by self.stop().
"""
print('ap 721 do_shutdown')
# Don't prompt the user before halting a download/update/refresh
# operation, as we would do in calls to self.stop()
if self.download_manager_obj:
@ -735,6 +741,8 @@ class TartubeApp(Gtk.Application):
Performs general initialisation.
"""
print('ap 746 start')
# Delete Tartube's config file and data directory, if the debugging
# flag is set
if self.debug_delete_data_flag:
@ -859,6 +867,8 @@ class TartubeApp(Gtk.Application):
self.do_shutdown().
"""
print('ap 872 stop')
# If a download/update/refresh operation is in progress, get
# confirmation before stopping
if self.current_manager_obj:
@ -932,6 +942,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 947 system_error')
if self.main_win_obj:
self.main_win_obj.errors_list_add_system_error(error_code, msg)
else:
@ -950,6 +962,8 @@ class TartubeApp(Gtk.Application):
loading/saving.
"""
print('ap 967 load_config')
# Sanity check
if self.current_manager_obj \
or not os.path.isfile(self.config_file_name) \
@ -1049,6 +1063,8 @@ class TartubeApp(Gtk.Application):
loading/saving.
"""
print('ap 1068 save_config')
# Sanity check
if self.current_manager_obj or self.disable_load_save_flag:
return
@ -1118,6 +1134,8 @@ class TartubeApp(Gtk.Application):
loading/saving.
"""
print('ap 1139 load_db')
# Sanity check
path = os.path.join(self.data_dir, self.db_file_name)
if self.current_manager_obj \
@ -1204,6 +1222,8 @@ class TartubeApp(Gtk.Application):
loading/saving.
"""
print('ap 1227 save_db')
# Sanity check
if self.current_manager_obj or self.disable_load_save_flag:
return
@ -1285,6 +1305,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 1310 switch_db')
# Sanity check
if self.current_manager_obj or self.disable_load_save_flag:
return
@ -1351,7 +1373,10 @@ class TartubeApp(Gtk.Application):
"""Called by self.switch_db().
Resets media registry IVs, so that a new Tartube database file can be
created."""
created.
"""
print('ap 1381 reset_db')
# Reset IVs to their default states
self.general_options_obj = options.OptionsManager()
@ -1378,6 +1403,8 @@ class TartubeApp(Gtk.Application):
destroyed by the user.
"""
print('ap 1408 create_system_folders')
self.fixed_all_folder = self.add_folder(
'All Videos',
None, # No parent folder
@ -1434,6 +1461,8 @@ class TartubeApp(Gtk.Application):
window's menu item.
"""
print('ap 1466 disable_load_save')
self.disable_load_save_flag = True
self.main_win_obj.save_db_menu_item.set_sensitive(False)
@ -1452,6 +1481,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 1486 file_error_dialogue')
if self.main_win_obj:
self.show_msg_dialogue(
msg,
@ -1472,6 +1503,8 @@ class TartubeApp(Gtk.Application):
'Temporary Videos' folder. (The folders themselves are not deleted).
"""
print('ap 1508 delete_temp_folders')
for name in self.media_name_dict:
dbid = self.media_name_dict[name]
@ -1514,6 +1547,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 1552 convert_version')
num_list = version.split('.')
if len(num_list) != 3:
return None
@ -1556,6 +1591,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 1596 download_manager_start')
if self.current_manager_obj:
# Download, update or refresh operation already in progress
return self.system_error(
@ -1656,6 +1693,8 @@ class TartubeApp(Gtk.Application):
continue running for a few seconds more.
"""
print('ap 1698 download_manager_halt_timer')
if self.timer_id:
self.timer_check_time = time.time() + self.timer_final_time
@ -1668,6 +1707,8 @@ class TartubeApp(Gtk.Application):
widgets.
"""
print('ap 1712 download_manager_finished')
# Get the time taken by the download operation, so we can convert it
# into a nice string below (e.g. '05:15')
time_num = int(
@ -1727,6 +1768,8 @@ class TartubeApp(Gtk.Application):
self.update_manager_finished() is called.
"""
print('ap 1773 update_manager_start')
if self.current_manager_obj:
# Download, update or refresh operation already in progress
return self.system_error(
@ -1770,6 +1813,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 1818 update_manager_finished')
# Any code can check whether a download/update/refresh operation is in
# progress, or not, by checking this IV
self.current_manager_obj = None
@ -1838,6 +1883,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 1888 refresh_manager_start')
if self.current_manager_obj:
# Download, update or refresh operation already in progress
return self.system_error(
@ -1884,6 +1931,8 @@ class TartubeApp(Gtk.Application):
widgets.
"""
print('ap 1936 refresh_manager_finished')
# Any code can check whether a download/update/refresh operation is in
# progress, or not, by checking this IV
self.current_manager_obj = None
@ -1948,6 +1997,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2002 create_video_from_download')
# The downloads.DownloadItem handles a download for a video, a channel
# or a playlist
media_data_obj = download_item_obj.media_data_obj
@ -2019,6 +2070,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2075 announce_video_download')
# If the video's parent media data object (a channel, playlist or
# folder) is selected in the Video Index, update the Video Catalogue
# for the downloaded video
@ -2070,6 +2123,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2128 update_video_when_file_found')
# Only set the .name IV if the video is currently unnamed
if video_obj.name == self.default_video_name:
video_obj.set_name(video_obj.file_name)
@ -2150,6 +2205,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2210 update_video_from_json')
json_path = os.path.join(
video_obj.file_dir,
video_obj.file_name + '.info.json',
@ -2195,6 +2252,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2257 update_video_from_filesystem')
if video_obj.upload_time is None:
video_obj.set_upload_time(os.path.getmtime(video_path))
@ -2238,6 +2297,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2302 add_video')
# Videos can't be placed inside other videos
if parent_obj and isinstance(parent_obj, media.Video):
return self.system_error(
@ -2306,6 +2367,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2372 add_channel')
# Channels can only be placed inside an unrestricted media.Folder
# object (if they have a parent at all)
if parent_obj \
@ -2384,6 +2447,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2452 add_playlist')
# Playlists can only be place inside an unrestricted media.Folder
# object (if they have a parent at all)
if parent_obj \
@ -2460,6 +2525,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2530 add_folder')
# Folders can only be placed inside an unrestricted media.Folder object
# (if they have a parent at all)
if parent_obj \
@ -2527,6 +2594,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2599 move_container_to_top')
# Do some basic checks
if media_data_obj is None or isinstance(media_data_obj, media.Video) \
or self.current_manager_obj or not media_data_obj.parent_obj:
@ -2591,6 +2660,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2665 move_container')
# Do some basic checks
if source_obj is None or isinstance(source_obj, media.Video) \
or dest_obj is None or isinstance(dest_obj, media.Video) \
@ -2711,6 +2782,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2787 delete_video')
if not isinstance(video_obj, media.Video):
return self.system_error(
115,
@ -2769,6 +2842,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2847 delete_container')
# Check this isn't a video or a fixed folder (which cannot be removed)
if isinstance(media_data_obj, media.Video) \
or (
@ -2881,6 +2956,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 2961 mark_video_new')
# (List of Video Index rows to update, at the end of this function)
update_list = [self.fixed_new_folder]
if not no_update_index_flag:
@ -2978,6 +3055,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3060 mark_video_downloaded')
# (List of Video Index rows to update, at the end of this function)
update_list = [video_obj.parent_obj, self.fixed_all_folder]
@ -3068,6 +3147,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3152 mark_video_favourite')
# (List of Video Index rows to update, at the end of this function)
update_list = [self.fixed_fav_folder]
if not no_update_index_flag:
@ -3170,6 +3251,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3256 mark_folder_hidden')
if not isinstance(folder_obj, media.Folder):
return self.system_error(
120,
@ -3226,6 +3309,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3314 mark_container_favourite')
if isinstance(media_data_obj, media.Video):
return self.system_error(
121,
@ -3306,6 +3391,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3396 apply_download_options')
if self.current_manager_obj \
or media_data_obj.options_obj\
or (
@ -3341,6 +3428,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3433 remove_download_options')
if self.current_manager_obj or not media_data_obj.options_obj:
return self.system_error(
123,
@ -3371,6 +3460,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3465 watch_video_in_player')
path = os.path.join(
video_obj.file_dir,
video_obj.file_name + video_obj.file_ext,
@ -3421,6 +3512,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3517 show_msg_dialogue')
# Prepare arguments
main_win_obj = self.main_win_obj
if not main_win_obj:
@ -3490,6 +3583,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3588 timer_callback')
if self.timer_check_time is None:
self.main_win_obj.progress_list_display_dl_stats()
self.main_win_obj.results_list_update_row()
@ -3529,6 +3624,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3629 on_button_stop_operation')
self.operation_halted_flag = True
if self.download_manager_obj:
@ -3553,6 +3650,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3655 on_button_switch_view')
if not self.complex_catalogue_flag:
self.complex_catalogue_flag = True
else:
@ -3580,6 +3679,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3684 on_menu_about')
dialogue_win = Gtk.AboutDialog()
dialogue_win.set_transient_for(self.main_win_obj)
dialogue_win.set_destroy_with_parent(True)
@ -3617,6 +3718,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3723 on_menu_about_close')
action.destroy()
@ -3635,6 +3738,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3743 on_menu_add_channel')
dialogue_win = mainwin.AddChannelDialogue(self.main_win_obj)
response = dialogue_win.run()
@ -3719,6 +3824,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3829 on_menu_add_folder')
dialogue_win = mainwin.AddFolderDialogue(self.main_win_obj)
response = dialogue_win.run()
@ -3783,6 +3890,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3895 on_menu_add_playlist')
dialogue_win = mainwin.AddPlaylistDialogue(self.main_win_obj)
response = dialogue_win.run()
@ -3867,6 +3976,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 3981 on_menu_add_video')
dialogue_win = mainwin.AddVideoDialogue(self.main_win_obj)
response = dialogue_win.run()
@ -3925,6 +4036,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4041 on_menu_check_all')
self.download_manager_start(True)
@ -3942,6 +4055,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4060 on_menu_download_all')
self.download_manager_start(False)
@ -3959,6 +4074,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4079 on_menu_general_options')
config.OptionsEditWin(self, self.general_options_obj, None)
@ -3976,6 +4093,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4098 on_menu_refresh_db')
self.refresh_manager_start()
@ -3993,6 +4112,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4117 on_menu_save_db')
self.save_db()
# Show a dialogue window for confirmation (unless file load/save has
@ -4021,6 +4142,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4147 on_menu_show_hidden')
for name in self.media_name_dict:
dbid = self.media_name_dict[name]
@ -4045,6 +4168,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4173 on_menu_system_preferences')
config.SystemPrefWin(self)
@ -4063,6 +4188,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4193 on_menu_test')
# Add media data objects for testing: videos, channels, playlists and/
# or folders
testing.add_test_media(self)
@ -4097,6 +4224,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4229 on_menu_update_ytdl')
self.update_manager_start()
@ -4114,6 +4243,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4248 on_menu_quit')
self.stop()
@ -4135,6 +4266,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4271 reject_media_name')
# Get the existing media data object with this name
dbid = self.media_name_dict[name]
media_data_obj = self.media_reg_dict[dbid]
@ -4171,6 +4304,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4309 set_bandwidth_default')
if value < self.bandwidth_min or value > self.bandwidth_max:
return self.system_error(
124,
@ -4188,6 +4323,8 @@ class TartubeApp(Gtk.Application):
progress, the new setting is applied to the next download job.
"""
print('ap 4328 set_bandwidth_apply_flag')
if not flag:
self.bandwidth_apply_flag = False
else:
@ -4196,6 +4333,8 @@ class TartubeApp(Gtk.Application):
def set_complex_index_flag(self, flag):
print('ap 4338 set_complex_index_flag')
if not flag:
self.complex_index = False
else:
@ -4204,16 +4343,22 @@ class TartubeApp(Gtk.Application):
def set_match_first_chars(self, num_chars):
print('ap 4348 set_match_first_chars')
self.match_first_chars = num_chars
def set_match_ignore_chars(self, num_chars):
print('ap 4355 set_match_ignore_chars')
self.match_ignore_chars = num_chars
def set_match_method(self, method):
print('ap 4362 set_match_method')
self.match_method = method
@ -4226,6 +4371,8 @@ class TartubeApp(Gtk.Application):
download job.
"""
print('ap 4376 set_num_worker_apply_flag')
if not flag:
self.bandwidth_apply_flag = False
else:
@ -4247,6 +4394,8 @@ class TartubeApp(Gtk.Application):
"""
print('ap 4399 set_num_worker_default')
if value < self.num_worker_min or value > self.num_worker_max:
return self.system_error(
125,
@ -4262,6 +4411,8 @@ class TartubeApp(Gtk.Application):
def set_operation_auto_update_flag(self, flag):
print('ap 4416 set_operation_auto_update_flag')
if not flag:
self.operation_auto_update_flag = False
else:
@ -4270,6 +4421,8 @@ class TartubeApp(Gtk.Application):
def set_operation_dialogue_flag(self, flag):
print('ap 4426 set_operation_dialogue_flag')
if not flag:
self.operation_dialogue_flag = False
else:
@ -4278,6 +4431,8 @@ class TartubeApp(Gtk.Application):
def set_operation_save_flag(self, flag):
print('ap 4436 set_operation_save_flag')
if not flag:
self.operation_save_flag = False
else:
@ -4286,6 +4441,8 @@ class TartubeApp(Gtk.Application):
def set_use_module_moviepy_flag(self, flag):
print('ap 4446 set_use_module_moviepy_flag')
if not flag:
self.use_module_moviepy_flag = False
else:
@ -4294,6 +4451,8 @@ class TartubeApp(Gtk.Application):
def set_use_module_moviepy_flag(self, flag):
print('ap 4456 set_use_module_moviepy_flag')
if not flag:
self.use_module_validators_flag = False
else:
@ -4302,16 +4461,22 @@ class TartubeApp(Gtk.Application):
def set_ytdl_path(self, path):
print('ap 4466 set_ytdl_path')
self.ytdl_path = path
def set_ytdl_update_current(self, string):
print('ap 4473 set_ytdl_update_current')
self.ytdl_update_current = string
def set_ytdl_write_stderr_flag(self, flag):
print('ap 4480 set_ytdl_write_stderr_flag')
if not flag:
self.ytdl_write_stderr_flag = False
else:
@ -4320,6 +4485,8 @@ class TartubeApp(Gtk.Application):
def set_ytdl_write_stdout_flag(self, flag):
print('ap 4490 set_ytdl_write_stdout_flag')
if not flag:
self.ytdl_write_stdout_flag = False
else:
@ -4328,6 +4495,8 @@ class TartubeApp(Gtk.Application):
def set_ytdl_write_verbose_flag(self, flag):
print('ap 4500 set_ytdl_write_verbose_flag')
if not flag:
self.ytdl_write_verbose_flag = False
else:

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -105,90 +105,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):
"""Called as a result of self.__init__().

BIN
screenshots/tartube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

@ -35,7 +35,7 @@ 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""",

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.