2018-10-10 22:43:50 +00:00
|
|
|
import logging
|
2018-12-09 23:15:10 +00:00
|
|
|
import os
|
2018-10-20 22:20:31 +00:00
|
|
|
import re
|
2018-12-09 23:15:10 +00:00
|
|
|
from string import Template
|
2018-11-02 00:50:16 +00:00
|
|
|
from threading import Lock
|
2018-10-10 22:43:50 +00:00
|
|
|
|
2018-12-09 23:15:10 +00:00
|
|
|
import youtube_dl
|
|
|
|
|
|
|
|
from YtManagerApp import scheduler
|
|
|
|
from YtManagerApp.models import Video
|
|
|
|
|
2018-10-10 22:43:50 +00:00
|
|
|
log = logging.getLogger('video_downloader')
|
|
|
|
log_youtube_dl = log.getChild('youtube_dl')
|
|
|
|
|
2018-11-02 00:50:16 +00:00
|
|
|
_lock = Lock()
|
|
|
|
|
2018-10-10 22:43:50 +00:00
|
|
|
|
2018-10-20 22:20:31 +00:00
|
|
|
def __get_valid_path(path):
|
|
|
|
"""
|
|
|
|
Normalizes string, converts to lowercase, removes non-alpha characters,
|
|
|
|
and converts spaces to hyphens.
|
|
|
|
"""
|
|
|
|
import unicodedata
|
|
|
|
value = unicodedata.normalize('NFKD', path).encode('ascii', 'ignore').decode('ascii')
|
|
|
|
value = re.sub('[:"*]', '', value).strip()
|
|
|
|
value = re.sub('[?<>|]', '#', value)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2018-12-09 23:15:10 +00:00
|
|
|
def __build_template_dict(video: Video):
|
|
|
|
return {
|
2018-10-29 16:52:09 +00:00
|
|
|
'channel': video.subscription.channel_name,
|
|
|
|
'channel_id': video.subscription.channel_id,
|
2018-10-10 22:43:50 +00:00
|
|
|
'playlist': video.subscription.name,
|
|
|
|
'playlist_id': video.subscription.playlist_id,
|
|
|
|
'playlist_index': "{:03d}".format(1 + video.playlist_index),
|
|
|
|
'title': video.name,
|
|
|
|
'id': video.video_id,
|
|
|
|
}
|
|
|
|
|
2018-12-09 23:15:10 +00:00
|
|
|
|
|
|
|
def __build_youtube_dl_params(video: Video):
|
|
|
|
|
|
|
|
sub = video.subscription
|
|
|
|
user = sub.user
|
|
|
|
|
|
|
|
# resolve path
|
|
|
|
download_path = user.preferences['download_path']
|
|
|
|
|
|
|
|
template_dict = __build_template_dict(video)
|
|
|
|
output_pattern = Template(user.preferences['download_file_pattern']).safe_substitute(template_dict)
|
2018-10-10 22:43:50 +00:00
|
|
|
|
|
|
|
output_path = os.path.join(download_path, output_pattern)
|
|
|
|
output_path = os.path.normpath(output_path)
|
|
|
|
|
|
|
|
youtube_dl_params = {
|
|
|
|
'logger': log_youtube_dl,
|
2018-12-09 23:15:10 +00:00
|
|
|
'format': user.preferences['download_format'],
|
2018-10-10 22:43:50 +00:00
|
|
|
'outtmpl': output_path,
|
|
|
|
'writethumbnail': True,
|
|
|
|
'writedescription': True,
|
2018-12-29 18:43:39 +00:00
|
|
|
'writesubtitles': user.preferences['download_subtitles'],
|
|
|
|
'writeautomaticsub': user.preferences['download_autogenerated_subtitles'],
|
|
|
|
'allsubtitles': user.preferences['download_subtitles_all'],
|
2018-10-10 22:43:50 +00:00
|
|
|
'postprocessors': [
|
|
|
|
{
|
2018-10-20 22:20:31 +00:00
|
|
|
'key': 'FFmpegMetadata'
|
2018-10-10 22:43:50 +00:00
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2018-12-29 18:43:39 +00:00
|
|
|
sub_langs = user.preferences['download_subtitles_langs'].split(',')
|
2018-10-10 22:43:50 +00:00
|
|
|
sub_langs = [i.strip() for i in sub_langs]
|
|
|
|
if len(sub_langs) > 0:
|
|
|
|
youtube_dl_params['subtitleslangs'] = sub_langs
|
|
|
|
|
2018-12-29 18:43:39 +00:00
|
|
|
sub_format = user.preferences['download_subtitles_format']
|
2018-10-10 22:43:50 +00:00
|
|
|
if len(sub_format) > 0:
|
|
|
|
youtube_dl_params['subtitlesformat'] = sub_format
|
|
|
|
|
|
|
|
return youtube_dl_params, output_path
|
|
|
|
|
|
|
|
|
|
|
|
def download_video(video: Video, attempt: int = 1):
|
|
|
|
|
2018-12-09 23:15:10 +00:00
|
|
|
user = video.subscription.user
|
|
|
|
|
2018-10-10 22:43:50 +00:00
|
|
|
log.info('Downloading video %d [%s %s]', video.id, video.video_id, video.name)
|
|
|
|
|
2018-11-02 00:50:16 +00:00
|
|
|
# Issue: if multiple videos are downloaded at the same time, a race condition appears in the mkdirs() call that
|
|
|
|
# youtube-dl makes, which causes it to fail with the error 'Cannot create folder - file already exists'.
|
|
|
|
# For now, allow a single download instance.
|
|
|
|
_lock.acquire()
|
|
|
|
|
|
|
|
try:
|
2018-12-29 18:43:39 +00:00
|
|
|
max_attempts = user.preferences['max_download_attempts']
|
2018-11-02 00:50:16 +00:00
|
|
|
|
|
|
|
youtube_dl_params, output_path = __build_youtube_dl_params(video)
|
|
|
|
with youtube_dl.YoutubeDL(youtube_dl_params) as yt:
|
|
|
|
ret = yt.download(["https://www.youtube.com/watch?v=" + video.video_id])
|
2018-10-10 22:43:50 +00:00
|
|
|
|
2018-11-02 00:50:16 +00:00
|
|
|
log.info('Download finished with code %d', ret)
|
2018-10-10 22:43:50 +00:00
|
|
|
|
2018-11-02 00:50:16 +00:00
|
|
|
if ret == 0:
|
|
|
|
video.downloaded_path = output_path
|
|
|
|
video.save()
|
|
|
|
log.info('Video %d [%s %s] downloaded successfully!', video.id, video.video_id, video.name)
|
2018-10-10 22:43:50 +00:00
|
|
|
|
2018-11-02 00:50:16 +00:00
|
|
|
elif attempt <= max_attempts:
|
|
|
|
log.warning('Re-enqueueing video (attempt %d/%d)', attempt, max_attempts)
|
|
|
|
__schedule_download_video(video, attempt + 1)
|
2018-10-10 22:43:50 +00:00
|
|
|
|
2018-11-02 00:50:16 +00:00
|
|
|
else:
|
|
|
|
log.error('Multiple attempts to download video %d [%s %s] failed!', video.id, video.video_id, video.name)
|
|
|
|
video.downloaded_path = ''
|
|
|
|
video.save()
|
2018-10-10 22:43:50 +00:00
|
|
|
|
2018-11-02 00:50:16 +00:00
|
|
|
finally:
|
|
|
|
_lock.release()
|
2018-10-10 22:43:50 +00:00
|
|
|
|
|
|
|
|
2018-10-29 16:52:09 +00:00
|
|
|
def __schedule_download_video(video: Video, attempt=1):
|
|
|
|
job = scheduler.scheduler.add_job(download_video, args=[video, attempt])
|
|
|
|
log.info('Scheduled download video job video=(%s), attempt=%d, job=%s', video, attempt, job.id)
|
|
|
|
|
|
|
|
|
2018-10-10 22:43:50 +00:00
|
|
|
def schedule_download_video(video: Video):
|
|
|
|
"""
|
|
|
|
Schedules a download video job to run immediately.
|
|
|
|
:param video:
|
|
|
|
:return:
|
|
|
|
"""
|
2018-10-29 16:52:09 +00:00
|
|
|
__schedule_download_video(video)
|