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.models import Video
|
2019-08-14 14:14:16 +00:00
|
|
|
from YtManagerApp.scheduler import Job, scheduler
|
|
|
|
|
|
|
|
|
|
|
|
class DownloadVideoJob(Job):
|
|
|
|
name = "DownloadVideoJob"
|
|
|
|
__lock = Lock()
|
|
|
|
|
|
|
|
def __init__(self, job_execution, video: Video, attempt: int = 1):
|
|
|
|
super().__init__(job_execution)
|
|
|
|
self.__video = video
|
|
|
|
self.__attempt = attempt
|
|
|
|
self.__log_youtube_dl = self.log.getChild('youtube_dl')
|
|
|
|
|
|
|
|
def get_description(self):
|
|
|
|
ret = "Downloading video " + self.__video.name
|
|
|
|
if self.__attempt > 1:
|
|
|
|
ret += f" (attempt {self.__attempt})"
|
|
|
|
return ret
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
# 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.
|
|
|
|
self.__lock.acquire()
|
|
|
|
|
|
|
|
try:
|
|
|
|
user = self.__video.subscription.user
|
|
|
|
max_attempts = user.preferences['max_download_attempts']
|
|
|
|
|
|
|
|
youtube_dl_params, output_path = self.__build_youtube_dl_params(self.__video)
|
|
|
|
with youtube_dl.YoutubeDL(youtube_dl_params) as yt:
|
|
|
|
ret = yt.download(["https://www.youtube.com/watch?v=" + self.__video.video_id])
|
|
|
|
|
|
|
|
self.log.info('Download finished with code %d', ret)
|
|
|
|
|
|
|
|
if ret == 0:
|
|
|
|
self.__video.downloaded_path = output_path
|
|
|
|
self.__video.save()
|
|
|
|
self.log.info('Video %d [%s %s] downloaded successfully!', self.__video.id, self.__video.video_id, self.__video.name)
|
|
|
|
|
|
|
|
elif self.__attempt <= max_attempts:
|
|
|
|
self.log.warning('Re-enqueueing video (attempt %d/%d)', self.__attempt, max_attempts)
|
|
|
|
DownloadVideoJob.schedule(self.__video, self.__attempt + 1)
|
|
|
|
|
|
|
|
else:
|
|
|
|
self.log.error('Multiple attempts to download video %d [%s %s] failed!', self.__video.id, self.__video.video_id,
|
|
|
|
self.__video.name)
|
|
|
|
self.__video.downloaded_path = ''
|
|
|
|
self.__video.save()
|
|
|
|
|
|
|
|
finally:
|
|
|
|
self.__lock.release()
|
|
|
|
|
|
|
|
def __build_youtube_dl_params(self, video: Video):
|
|
|
|
|
|
|
|
sub = video.subscription
|
|
|
|
user = sub.user
|
|
|
|
|
|
|
|
# resolve path
|
|
|
|
download_path = user.preferences['download_path']
|
|
|
|
|
|
|
|
template_dict = self.__build_template_dict(video)
|
|
|
|
output_pattern = Template(user.preferences['download_file_pattern']).safe_substitute(template_dict)
|
|
|
|
|
|
|
|
output_path = os.path.join(download_path, output_pattern)
|
|
|
|
output_path = os.path.normpath(output_path)
|
|
|
|
|
|
|
|
youtube_dl_params = {
|
|
|
|
'logger': self.__log_youtube_dl,
|
|
|
|
'format': user.preferences['download_format'],
|
|
|
|
'outtmpl': output_path,
|
|
|
|
'writethumbnail': True,
|
|
|
|
'writedescription': True,
|
|
|
|
'writesubtitles': user.preferences['download_subtitles'],
|
|
|
|
'writeautomaticsub': user.preferences['download_autogenerated_subtitles'],
|
|
|
|
'allsubtitles': user.preferences['download_subtitles_all'],
|
|
|
|
'postprocessors': [
|
|
|
|
{
|
|
|
|
'key': 'FFmpegMetadata'
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
sub_langs = user.preferences['download_subtitles_langs'].split(',')
|
|
|
|
sub_langs = [i.strip() for i in sub_langs]
|
|
|
|
if len(sub_langs) > 0:
|
|
|
|
youtube_dl_params['subtitleslangs'] = sub_langs
|
|
|
|
|
|
|
|
sub_format = user.preferences['download_subtitles_format']
|
|
|
|
if len(sub_format) > 0:
|
|
|
|
youtube_dl_params['subtitlesformat'] = sub_format
|
|
|
|
|
|
|
|
return youtube_dl_params, output_path
|
|
|
|
|
|
|
|
def __build_template_dict(self, video: Video):
|
|
|
|
return {
|
|
|
|
'channel': video.subscription.channel_name,
|
|
|
|
'channel_id': video.subscription.channel_id,
|
|
|
|
'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,
|
|
|
|
}
|
|
|
|
|
|
|
|
def __get_valid_path(self, 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
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def schedule(video: Video, attempt: int = 1):
|
|
|
|
"""
|
|
|
|
Schedules to download video immediately
|
|
|
|
:param video:
|
|
|
|
:param attempt:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
scheduler.add_job(DownloadVideoJob, args=[video, attempt])
|