mirror of
				https://github.com/chibicitiberiu/ytsm.git
				synced 2024-02-24 05:43:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			137 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			137 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import os
 | |
| import re
 | |
| from string import Template
 | |
| from threading import Lock
 | |
| 
 | |
| import youtube_dl
 | |
| 
 | |
| from YtManagerApp.models import Video
 | |
| from YtManagerApp.services.scheduler.job import Job
 | |
| 
 | |
| 
 | |
| 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'],
 | |
|             'merge_output_format': 'mp4',
 | |
|             '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:
 | |
|         """
 | |
|         from YtManagerApp.management.services import Services
 | |
|         Services.scheduler.add_job(DownloadVideoJob, args=[video, attempt])
 |