From 111957c3505022d1fa0848563a1ad93c23c6726c Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Thu, 16 Jan 2025 14:22:51 +0530 Subject: [PATCH] (feat): added embded caption downloading support --- pytubepp/config.py | 1 + pytubepp/download.py | 34 ++++-- pytubepp/main.py | 233 +++++++++++++++++++++++++++++++++------- pytubepp/postprocess.py | 64 ++++++++--- 4 files changed, 272 insertions(+), 60 deletions(-) diff --git a/pytubepp/config.py b/pytubepp/config.py index 8083b66..345633f 100644 --- a/pytubepp/config.py +++ b/pytubepp/config.py @@ -14,6 +14,7 @@ def get_download_folder(): DEFAULT_CONFIG = { 'downloadDIR': get_download_folder(), 'defaultStream': 'max', + 'defaultCaption': 'none', } def get_temporary_directory(): diff --git a/pytubepp/download.py b/pytubepp/download.py index 2ada86c..67a923c 100644 --- a/pytubepp/download.py +++ b/pytubepp/download.py @@ -1,13 +1,13 @@ from tqdm import tqdm from .config import get_temporary_directory, load_config -from .utils import get_unique_filename -import os, re, requests, shutil, sys, random +from .utils import get_unique_filename, postprocess_cleanup +import os, re, requests, shutil, sys, random, ffmpy userConfig = load_config() downloadDIR = userConfig['downloadDIR'] tempDIR = get_temporary_directory() -def download_progressive(stream, itag, title, resolution, file_extention, tempDIR=tempDIR, downloadDIR=downloadDIR): +def download_progressive(stream, itag, title, resolution, file_extention, captions, caption_code=None, tempDIR=tempDIR, downloadDIR=downloadDIR): global total_filesize, progress_bar selected_vdo = stream.get_by_itag(itag) total_filesize = selected_vdo.filesize @@ -15,11 +15,31 @@ def download_progressive(stream, itag, title, resolution, file_extention, tempDI random_filename = str(random.randint(1000000000, 9999999999)) filename = random_filename + '_vdo.' + file_extention output_temp_file = os.path.join(tempDIR, filename) - output_file = os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '.' + file_extention)) + output_file = os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '.' + file_extention)) if not caption_code else os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '_' + caption_code + '.' + file_extention)) selected_vdo.download(output_path=tempDIR, filename=filename) - print('Processing...') - shutil.move(output_temp_file, output_file) - print('Done! 🎉') + + if caption_code: + print(f'Downloading Caption ({caption_code})...') + caption = captions[caption_code] + caption_file = os.path.join(tempDIR, random_filename + '_cap.srt') + caption.save_captions(caption_file) + print('Processing...') + devnull = open(os.devnull, 'w') + output_temp_file_with_subs = os.path.join(tempDIR, random_filename + '_merged.' + file_extention) + ff = ffmpy.FFmpeg( + inputs={output_temp_file: None}, + outputs={output_temp_file_with_subs: ['-i', caption_file, '-c', 'copy', '-c:s', 'mov_text', '-metadata:s:s:0', f'language={caption_code}', '-metadata:s:s:0', f'title={caption_code}', '-metadata:s:s:0', f'handler_name={caption_code}']} + ) + ff.run(stdout=devnull, stderr=devnull) + devnull.close() + + shutil.move(output_temp_file_with_subs, output_file) + postprocess_cleanup(tempDIR, ['_vdo.' + file_extention, '_cap.srt', '_merged.' + file_extention], random_filename) + print('Done! 🎉') + else: + print('Processing...') + shutil.move(output_temp_file, output_file) + print('Done! 🎉') def download_nonprogressive(stream, itag_vdo, itag_ado, file_extention, output_path): global total_filesize, progress_bar diff --git a/pytubepp/main.py b/pytubepp/main.py index c8ed77f..f7bcc9c 100644 --- a/pytubepp/main.py +++ b/pytubepp/main.py @@ -13,6 +13,7 @@ class YouTubeDownloader: self.temp_dir = get_temporary_directory() self.config_dir = appdirs.user_config_dir('pytubepp') self.default_stream = self.user_config['defaultStream'] + self.default_caption = self.user_config['defaultCaption'] self.version = get_version() # Video attributes @@ -50,6 +51,7 @@ class YouTubeDownloader: self.thumbnail = self.video.thumbnail_url self.views = str(self.video.views) self.stream = self.video.streams + self.captions = self.video.captions # Find maximum resolution for res in self.stream_resolutions.keys(): @@ -144,7 +146,7 @@ class YouTubeDownloader: print('Sorry, No video streams found....!!!') sys.exit() - print(f'\nTitle: {self.video.title}\nAuthor: {self.author}\nPublished On: {self.video.publish_date.strftime("%d/%m/%Y")}\nDuration: {self.video.length}\nViews: {self.views}\n') + print(f'\nTitle: {self.video.title}\nAuthor: {self.author}\nPublished On: {self.video.publish_date.strftime("%d/%m/%Y")}\nDuration: {f"{self.video.length//3600:02}:{(self.video.length%3600)//60:02}:{self.video.length%60:02}" if self.video.length >= 3600 else f"{(self.video.length%3600)//60:02}:{self.video.length%60:02}"}\nViews: {self.views}\nCaptions: {[caption.code for caption in self.captions.keys()] or "Unavailable"}\n') print(tabulate(table, headers=['Stream', 'Alias (for -s flag)', 'Format', 'Size', 'FrameRate', 'V-Codec', 'A-Codec', 'V-BitRate', 'A-BitRate'])) print('\n') else: @@ -188,6 +190,7 @@ class YouTubeDownloader: 'published_on': self.video.publish_date.strftime('%d/%m/%Y'), 'duration': self.video.length, 'streams': streams_list, + 'captions': [caption.code for caption in self.captions.keys()] or None } print(json.dumps(output, indent=4 if prettify else None)) @@ -206,6 +209,13 @@ class YouTubeDownloader: else: print('\nInvalid video link! Please enter a valid video url...!!') return [] + + def get_allowed_captions(self, link): + if self.set_video_info(link): + return self.captions.keys() + else: + print('\nInvalid video link! Please enter a valid video url...!!') + return [] def print_short_info(self, chosen_stream): resolution_map = { @@ -222,30 +232,35 @@ class YouTubeDownloader: } print(f'\nTitle: {self.title}\nSelected Stream: {resolution_map.get(chosen_stream, "Unknown")}\n') - def download_stream(self, link, chosen_stream): + def download_stream(self, link, chosen_stream, chosen_caption=None): if self.set_video_info(link): - self.print_short_info(chosen_stream) allowed_streams = self.get_allowed_streams(link) + allowed_captions = self.get_allowed_captions(link) + + if chosen_caption and (chosen_caption not in allowed_captions): + print('\nInvalid caption code or caption not available! Please choose a different caption...!! (use -i to see available captions)') + sys.exit() if chosen_stream in allowed_streams: + self.print_short_info(chosen_stream) if chosen_stream in ['360', '360p']: - download_progressive(self.stream, 18, self.title, '360p', 'mp4') + download_progressive(self.stream, 18, self.title, '360p', 'mp4', self.captions, chosen_caption) elif chosen_stream in ['1080', '1080p', 'fhd']: - self._handle_1080p_download() + self._handle_1080p_download(chosen_caption) elif chosen_stream in ['720', '720p', 'hd']: - self._handle_720p_download() + self._handle_720p_download(chosen_caption) elif chosen_stream in ['480', '480p']: - merge_audio_video(self.title, '480p', 'mp4', download_nonprogressive(self.stream, 135, 140, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '480p', 'mp4', download_nonprogressive(self.stream, 135, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) elif chosen_stream in ['240', '240p']: - merge_audio_video(self.title, '240p', 'mp4', download_nonprogressive(self.stream, 133, 139, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '240p', 'mp4', download_nonprogressive(self.stream, 133, 139, 'mp4', self.temp_dir), self.captions, chosen_caption) elif chosen_stream in ['144', '144p']: - merge_audio_video(self.title, '144p', 'mp4', download_nonprogressive(self.stream, 160, 139, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '144p', 'mp4', download_nonprogressive(self.stream, 160, 139, 'mp4', self.temp_dir), self.captions, chosen_caption) elif chosen_stream in ['4320', '4320p', '8k']: - self._handle_4320p_download() + self._handle_4320p_download(chosen_caption) elif chosen_stream in ['2160', '2160p', '4k']: - self._handle_2160p_download() + self._handle_2160p_download(chosen_caption) elif chosen_stream in ['1440', '1440p', '2k']: - self._handle_1440p_download() + self._handle_1440p_download(chosen_caption) elif chosen_stream == 'mp3': convert_to_mp3(self.title, self.thumbnail, download_audio(self.stream, 140, self.temp_dir), self.author, self.video.title, self.author) else: @@ -253,39 +268,43 @@ class YouTubeDownloader: else: print('\nInvalid video link! Please enter a valid video url...!!') - def _handle_4320p_download(self): + def _handle_4320p_download(self, chosen_caption=None): if self.stream.get_by_itag(702): - merge_audio_video(self.title, '8k', 'mp4', download_nonprogressive(self.stream, 702, 140, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '8k', 'mp4', download_nonprogressive(self.stream, 702, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) elif self.stream.get_by_itag(571): - merge_audio_video(self.title, '8k', 'mp4', download_nonprogressive(self.stream, 571, 140, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '8k', 'mp4', download_nonprogressive(self.stream, 571, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) - def _handle_2160p_download(self): + def _handle_2160p_download(self, chosen_caption=None): if self.stream.get_by_itag(701): - merge_audio_video(self.title, '4k', 'mp4', download_nonprogressive(self.stream, 701, 140, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '4k', 'mp4', download_nonprogressive(self.stream, 701, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) elif self.stream.get_by_itag(315): - merge_audio_video(self.title, '4k', 'webm', download_nonprogressive(self.stream, 315, 251, 'webm', self.temp_dir)) + merge_audio_video(self.title, '4k', 'webm', download_nonprogressive(self.stream, 315, 251, 'webm', self.temp_dir), self.captions, chosen_caption) elif self.stream.get_by_itag(313): - merge_audio_video(self.title, '4k', 'webm', download_nonprogressive(self.stream, 313, 251, 'webm', self.temp_dir)) + merge_audio_video(self.title, '4k', 'webm', download_nonprogressive(self.stream, 313, 251, 'webm', self.temp_dir), self.captions, chosen_caption) - def _handle_1440p_download(self): + def _handle_1440p_download(self, chosen_caption=None): if self.stream.get_by_itag(700): - merge_audio_video(self.title, '2k', 'mp4', download_nonprogressive(self.stream, 700, 140, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '2k', 'mp4', download_nonprogressive(self.stream, 700, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) elif self.stream.get_by_itag(308): - merge_audio_video(self.title, '2k', 'webm', download_nonprogressive(self.stream, 308, 251, 'webm', self.temp_dir)) + merge_audio_video(self.title, '2k', 'webm', download_nonprogressive(self.stream, 308, 251, 'webm', self.temp_dir), self.captions, chosen_caption) elif self.stream.get_by_itag(271): - merge_audio_video(self.title, '2k', 'webm', download_nonprogressive(self.stream, 271, 251, 'webm', self.temp_dir)) + merge_audio_video(self.title, '2k', 'webm', download_nonprogressive(self.stream, 271, 251, 'webm', self.temp_dir), self.captions, chosen_caption) - def _handle_1080p_download(self): + def _handle_1080p_download(self, chosen_caption=None): if self.stream.get_by_itag(699): - merge_audio_video(self.title, '1080p', 'mp4', download_nonprogressive(self.stream, 699, 140, 'mp4', self.temp_dir)) - else: - merge_audio_video(self.title, '1080p', 'mp4', download_nonprogressive(self.stream, 137, 140, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '1080p', 'mp4', download_nonprogressive(self.stream, 699, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) + elif self.stream.get_by_itag(299): + merge_audio_video(self.title, '1080p', 'mp4', download_nonprogressive(self.stream, 299, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) + elif self.stream.get_by_itag(137): + merge_audio_video(self.title, '1080p', 'mp4', download_nonprogressive(self.stream, 137, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) - def _handle_720p_download(self): + def _handle_720p_download(self, chosen_caption=None): if self.stream.get_by_itag(698): - merge_audio_video(self.title, '720p', 'mp4', download_nonprogressive(self.stream, 698, 140, 'mp4', self.temp_dir)) - else: - merge_audio_video(self.title, '720p', 'mp4', download_nonprogressive(self.stream, 136, 140, 'mp4', self.temp_dir)) + merge_audio_video(self.title, '720p', 'mp4', download_nonprogressive(self.stream, 698, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) + elif self.stream.get_by_itag(298): + merge_audio_video(self.title, '720p', 'mp4', download_nonprogressive(self.stream, 298, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) + elif self.stream.get_by_itag(136): + merge_audio_video(self.title, '720p', 'mp4', download_nonprogressive(self.stream, 136, 140, 'mp4', self.temp_dir), self.captions, chosen_caption) def main(): downloader = YouTubeDownloader() @@ -294,7 +313,9 @@ def main(): parser.add_argument('url', nargs='?', default=None, help='url of the youtube video') parser.add_argument('-df', '--download-folder', default=argparse.SUPPRESS, help='set custom download folder path (default: ~/Downloads/Pytube Downloads) [arg eg: "/path/to/folder"]') parser.add_argument('-ds', '--default-stream', default=argparse.SUPPRESS, help='set default download stream (default: max) [available arguments: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p, 4320p, mp3, max]') + parser.add_argument('-dc', '--default-caption', default=argparse.SUPPRESS, help='set default caption (default: none) [available arguments: all language codes, none]') parser.add_argument('-s', '--stream', default=argparse.SUPPRESS, help='choose download stream for the current video (default: your chosen --default-stream) [available arguments: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p, 4320p, 144, 240, 360, 480, 720, 1080, 1440, 2160, 4320, mp3, hd, fhd, 2k, 4k, 8k]') + parser.add_argument('-c', '--caption', default=argparse.SUPPRESS, help='choose caption to embed for the current video (default: none)') parser.add_argument('-i', '--show-info', action='store_true', help='show video info (title, author, views and available_streams)') parser.add_argument('-ri', '--raw-info', action='store_true', help='show video info in raw json format') parser.add_argument('-jp', '--json-prettify', action='store_true', help='show json in prettified indented view') @@ -319,6 +340,8 @@ def main(): print('\nVideo url supplied! igonering -df flag...!!') if hasattr(args, 'default_stream'): print('\nVideo url supplied! ignoreing -ds flag...!!') + if hasattr(args, 'default_caption'): + print('\nVideo url supplied! ignoreing -dc flag...!!') if args.reset_default: print('\nVideo url supplied! ignoreing -r flag...!!') if args.clear_temp: @@ -335,14 +358,68 @@ def main(): print('\nMissing flag! -jp flag must be used with a flag which returns json data...!! (eg: -ri)') # Handle download cases - if hasattr(args, 'stream'): - downloader.download_stream(args.url, args.stream) - elif not any([args.show_info, args.raw_info, args.json_prettify]): # If no info flags are set + if hasattr(args, 'stream') and hasattr(args, 'caption'): if downloader.set_video_info(args.url): - if downloader.default_stream == 'max' and downloader.maxres: - downloader.download_stream(args.url, downloader.maxres) - elif (downloader.default_stream == 'mp3' and downloader.stream.get_by_itag(140)) or (downloader.default_stream != 'max' and downloader.stream.filter(res=downloader.default_stream)): - downloader.download_stream(args.url, downloader.default_stream) + if args.caption not in downloader.captions.keys(): + print('\nInvalid caption code or caption not available! Please choose a different caption...!! (use -i to see available captions)') + sys.exit() + elif args.stream == 'mp3' and downloader.stream.get_by_itag(140): + print(f'\nYou have chosen to download mp3 stream! ( Captioning audio files is not supported )') + answer = input('Do you still want to continue downloading ? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you still want to continue downloading ? [yes/no]\n') + if answer in ['yes', 'y']: + downloader.download_stream(args.url, args.stream) + else: + print('Download cancelled! exiting...!!') + else: + downloader.download_stream(args.url, args.stream, args.caption) + elif hasattr(args, 'stream'): + if downloader.set_video_info(args.url): + if downloader.default_caption == 'none': + downloader.download_stream(args.url, args.stream) + elif args.stream == 'mp3' and downloader.stream.get_by_itag(140): + print(f'\nYou have chosen to download mp3 stream! ( Captioning audio files is not supported )') + answer = input('Do you still want to continue downloading ? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you still want to continue downloading ? [yes/no]\n') + if answer in ['yes', 'y']: + downloader.download_stream(args.url, args.stream) + else: + print('Download cancelled! exiting...!!') + elif downloader.default_caption in downloader.captions.keys(): + downloader.download_stream(args.url, args.stream, downloader.default_caption) + else: + print(f'\nDefault caption not available! ( Default: {downloader.default_caption} | Available: {[caption.code for caption in downloader.captions.keys()] or "Nothing"} )') + answer = input('Do you still want to continue downloading without caption? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you still want to continue downloading without caption? [yes/no]\n') + if answer in ['yes', 'y']: + downloader.download_stream(args.url, args.stream) + else: + print('Download cancelled! exiting...!!') + elif hasattr(args, 'caption'): + if downloader.set_video_info(args.url): + if args.caption not in downloader.captions.keys(): + print('\nInvalid caption code or caption not available! Please choose a different caption...!! (use -i to see available captions)') + sys.exit() + elif downloader.default_stream == 'max' and downloader.maxres: + downloader.download_stream(args.url, downloader.maxres, args.caption) + elif downloader.default_stream == 'mp3' and downloader.stream.get_by_itag(140): + print(f'\nDefault stream set to mp3! ( Captioning audio files is not supported )') + answer = input('Do you still want to continue downloading ? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you still want to continue downloading ? [yes/no]\n') + if answer in ['yes', 'y']: + downloader.download_stream(args.url, downloader.default_stream) + else: + print('Download cancelled! exiting...!!') + elif downloader.default_stream != 'max' and downloader.stream.filter(res=downloader.default_stream): + downloader.download_stream(args.url, downloader.default_stream, args.caption) else: if downloader.maxres: print(f'\nDefault stream not available! ( Default: {downloader.default_stream} | Available: {downloader.maxres} )') @@ -350,10 +427,76 @@ def main(): while answer not in ['yes', 'y', 'no', 'n']: print('Invalid answer! try again...!! answer with: [yes/y/no/n]') answer = input('Do you want to download the maximum available stream ? [yes/no]\n') + if answer in ['yes', 'y']: + downloader.download_stream(args.url, downloader.maxres, args.caption) + else: + print('Download cancelled! exiting...!!') + else: + print('Sorry, No downloadable video stream found....!!!') + elif not any([args.show_info, args.raw_info, args.json_prettify]): # If no info flags are set + if downloader.set_video_info(args.url): + if downloader.default_stream == 'max' and downloader.maxres: + if downloader.default_caption == 'none': + downloader.download_stream(args.url, downloader.maxres) + elif downloader.default_caption in downloader.captions.keys(): + downloader.download_stream(args.url, downloader.maxres, downloader.default_caption) + else: + print(f'\nDefault caption not available! ( Default: {downloader.default_caption} | Available: {[caption.code for caption in downloader.captions.keys()] or "Nothing"} )') + answer = input('Do you still want to continue downloading without caption? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you still want to continue downloading without caption? [yes/no]\n') if answer in ['yes', 'y']: downloader.download_stream(args.url, downloader.maxres) else: print('Download cancelled! exiting...!!') + elif (downloader.default_stream == 'mp3' and downloader.stream.get_by_itag(140)) or (downloader.default_stream != 'max' and downloader.stream.filter(res=downloader.default_stream)): + if downloader.default_caption == 'none': + downloader.download_stream(args.url, downloader.default_stream) + elif downloader.default_stream == 'mp3' and downloader.stream.get_by_itag(140): + print(f'\nDefault stream set to mp3! ( Captioning audio files is not supported )') + answer = input('Do you still want to continue downloading ? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you still want to continue downloading ? [yes/no]\n') + if answer in ['yes', 'y']: + downloader.download_stream(args.url, downloader.default_stream) + else: + print('Download cancelled! exiting...!!') + elif downloader.default_caption in downloader.captions.keys(): + downloader.download_stream(args.url, downloader.default_stream, downloader.default_caption) + else: + print(f'\nDefault caption not available! ( Default: {downloader.default_caption} | Available: {[caption.code for caption in downloader.captions.keys()] or "Nothing"} )') + answer = input('Do you still want to continue downloading without caption? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you still want to continue downloading without caption? [yes/no]\n') + if answer in ['yes', 'y']: + downloader.download_stream(args.url, downloader.default_stream) + else: + print('Download cancelled! exiting...!!') + else: + if downloader.maxres: + print(f'\nDefault stream not available! ( Default: {downloader.default_stream} | Available: {downloader.maxres} )') + answer = input('Do you want to download the maximum available stream ? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you want to download the maximum available stream ? [yes/no]\n') + if answer in ['yes', 'y']: + if downloader.default_caption == 'none': + downloader.download_stream(args.url, downloader.maxres) + elif downloader.default_caption in downloader.captions.keys(): + downloader.download_stream(args.url, downloader.maxres, downloader.default_caption) + else: + print(f'\nDefault caption not available! ( Default: {downloader.default_caption} | Available: {[caption.code for caption in downloader.captions.keys()] or "Nothing"} )') + answer = input('Do you still want to continue downloading without caption? [yes/no]\n') + while answer not in ['yes', 'y', 'no', 'n']: + print('Invalid answer! try again...!! answer with: [yes/y/no/n]') + answer = input('Do you still want to continue downloading without caption? [yes/no]\n') + if answer in ['yes', 'y']: + downloader.download_stream(args.url, downloader.maxres) + else: + print('Download cancelled! exiting...!!') else: print('Sorry, No downloadable video stream found....!!!') else: @@ -377,7 +520,17 @@ def main(): print('\nInvalid default stream! Please enter a valid stream...!! (use -h to see available default_stream arguments)') else: print('\nDefault stream is the same! Not updating...!!') - + + if hasattr(args, 'default_caption'): + if args.default_caption != downloader.default_caption: + if not all(c.isalpha() or c in '.-' for c in args.default_caption) or len(args.default_caption) > 10: + print('\nInvalid caption code! Only a-z, A-Z, dash (-) and dot (.) are allowed with maximum 10 characters...!!') + else: + update_config('defaultCaption', args.default_caption) + print(f'\nDefault caption updated to: {args.default_caption}') + else: + print('\nDefault caption is the same! Not updating...!!') + if args.reset_default: reset_config() @@ -385,7 +538,7 @@ def main(): clear_temp_files() if args.show_config: - print(f'\ndownloadDIR: {downloader.download_dir}\ntempDIR: {downloader.temp_dir}\nconfigDIR: {downloader.config_dir}\ndefaultStream: {downloader.default_stream}\n') + print(f'\ntempDIR: {downloader.temp_dir} (Unchangeable) \nconfigDIR: {downloader.config_dir} (Unchangeable)\ndownloadDIR: {downloader.download_dir}\ndefaultStream: {downloader.default_stream}\ndefaultCaption: {downloader.default_caption}\n') if args.version: print(f'pytubepp {downloader.version}') diff --git a/pytubepp/postprocess.py b/pytubepp/postprocess.py index 5d6e02d..c2729de 100644 --- a/pytubepp/postprocess.py +++ b/pytubepp/postprocess.py @@ -8,24 +8,62 @@ userConfig = load_config() downloadDIR = userConfig['downloadDIR'] tempDIR = get_temporary_directory() -def merge_audio_video(title, resolution, file_extention, random_filename, tempDIR=tempDIR, downloadDIR=downloadDIR): +def merge_audio_video(title, resolution, file_extention, random_filename, captions, caption_code=None, tempDIR=tempDIR, downloadDIR=downloadDIR): video_file = os.path.join(tempDIR, random_filename + '_vdo.' + file_extention) audio_file = os.path.join(tempDIR, random_filename + '_ado.' + file_extention) output_temp_file = os.path.join(tempDIR, random_filename + '_merged.' + file_extention) - output_file = os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '.' + file_extention)) - - input_params = {video_file: None, audio_file: None} - output_params = {output_temp_file: ['-c:v', 'copy', '-c:a', 'copy']} + output_file = os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '.' + file_extention)) if not caption_code else os.path.join(downloadDIR, get_unique_filename(title + '_' + resolution + '_' + caption_code + '.' + file_extention)) - print('Processing...') - devnull = open(os.devnull, 'w') - ff = ffmpy.FFmpeg(inputs=input_params, outputs=output_params) - ff.run(stdout=devnull, stderr=devnull) - devnull.close() + if caption_code: + print(f'Downloading Caption ({caption_code})...') + caption = captions[caption_code] + srt_file = os.path.join(tempDIR, random_filename + '_cap.srt') + caption.save_captions(srt_file) + vtt_file = os.path.join(tempDIR, random_filename + '_cap.vtt') - shutil.move(output_temp_file, output_file) - postprocess_cleanup(tempDIR, ['_vdo.' + file_extention, '_ado.' + file_extention, '_merged.' + file_extention], random_filename) - print('Done! 🎉') + print('Processing...') + if file_extention == 'webm': + devnull = open(os.devnull, 'w') + ff_convert = ffmpy.FFmpeg( + inputs={srt_file: None}, + outputs={vtt_file: None} + ) + ff_convert.run(stdout=devnull, stderr=devnull) + subtitle_file = vtt_file + subtitle_codec = 'webvtt' + else: + subtitle_file = srt_file + subtitle_codec = 'mov_text' + + input_params = {video_file: None, audio_file: None} + output_params = {output_temp_file: ['-i', subtitle_file, '-c:v', 'copy', '-c:a', 'copy', + '-c:s', subtitle_codec, '-metadata:s:s:0', f'language={caption_code}', + '-metadata:s:s:0', f'title={caption_code}', '-metadata:s:s:0', f'handler_name={caption_code}']} + + devnull = open(os.devnull, 'w') + ff = ffmpy.FFmpeg(inputs=input_params, outputs=output_params) + ff.run(stdout=devnull, stderr=devnull) + devnull.close() + + shutil.move(output_temp_file, output_file) + cleanup_files = ['_vdo.' + file_extention, '_ado.' + file_extention, '_cap.srt', '_merged.' + file_extention] + if file_extention == 'webm': + cleanup_files.append('_cap.vtt') + postprocess_cleanup(tempDIR, cleanup_files, random_filename) + print('Done! 🎉') + else: + input_params = {video_file: None, audio_file: None} + output_params = {output_temp_file: ['-c:v', 'copy', '-c:a', 'copy']} + + print('Processing...') + devnull = open(os.devnull, 'w') + ff = ffmpy.FFmpeg(inputs=input_params, outputs=output_params) + ff.run(stdout=devnull, stderr=devnull) + devnull.close() + + shutil.move(output_temp_file, output_file) + postprocess_cleanup(tempDIR, ['_vdo.' + file_extention, '_ado.' + file_extention, '_merged.' + file_extention], random_filename) + print('Done! 🎉') def convert_to_mp3(title, thumbnail_url, random_filename, mp3_artist='Unknown', mp3_title='Unknown', mp3_album='Unknown', tempDIR=tempDIR, downloadDIR=downloadDIR): image_file = os.path.join(tempDIR, random_filename + '_thumbnail.jpg')