From dc179db221331a6e63c7e1e7bd569fc07b0841ec Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Sat, 13 Feb 2021 20:09:31 +1100 Subject: [PATCH] Add concat-playlist option --- youtube_dl/YoutubeDL.py | 16 +++++++ youtube_dl/__init__.py | 1 + youtube_dl/options.py | 5 +++ youtube_dl/postprocessor/__init__.py | 2 + youtube_dl/postprocessor/embedthumbnail.py | 2 +- youtube_dl/postprocessor/ffmpeg.py | 52 ++++++++++++++++------ 6 files changed, 64 insertions(+), 14 deletions(-) diff --git a/youtube_dl/YoutubeDL.py b/youtube_dl/YoutubeDL.py index ecac31f7a..2f2d47387 100755 --- a/youtube_dl/YoutubeDL.py +++ b/youtube_dl/YoutubeDL.py @@ -104,6 +104,7 @@ from .postprocessor import ( FFmpegFixupM4aPP, FFmpegFixupStretchedPP, FFmpegMergerPP, + FFmpegConcatPP, FFmpegPostProcessor, get_postprocessor, ) @@ -1058,6 +1059,20 @@ class YoutubeDL(object): entry_result = self.__process_iterable_entry(entry, download, extra) # TODO: skip failed (empty) entries? playlist_results.append(entry_result) + + if self.params.get('concat_playlist', False): + concat_pp = FFmpegConcatPP(self) + ie_result['__postprocessors'] = [concat_pp] + ie_result['__files_to_concat'] = list(map(lambda e: e['_filename'], entries)) + + filename_wo_ext = self.prepare_filename(ie_result) + + # Ensure filename always has a correct extension for successful merge + ie_result['ext'] = self.params.get('merge_output_format') or entries[0]['ext'] + ie_result['_filename'] = filename = '%s.%s' % (filename_wo_ext, ie_result['ext']) + + self.post_process(filename, ie_result) + ie_result['entries'] = playlist_results self.to_screen('[download] Finished downloading playlist: %s' % playlist) return ie_result @@ -1677,6 +1692,7 @@ class YoutubeDL(object): new_info = dict(info_dict) new_info.update(format) self.process_info(new_info) + format.update(new_info) # We update the info dict with the best quality format (backwards compatibility) info_dict.update(formats_to_download[-1]) return info_dict diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index e1bd67919..023ffcd8a 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -409,6 +409,7 @@ def _real_main(argv=None): 'youtube_include_dash_manifest': opts.youtube_include_dash_manifest, 'encoding': opts.encoding, 'extract_flat': opts.extract_flat, + 'concat_playlist': opts.concat_playlist, 'mark_watched': opts.mark_watched, 'merge_output_format': opts.merge_output_format, 'postprocessors': postprocessors, diff --git a/youtube_dl/options.py b/youtube_dl/options.py index 241cf110f..6731292bf 100644 --- a/youtube_dl/options.py +++ b/youtube_dl/options.py @@ -185,6 +185,11 @@ def parseOpts(overrideArguments=None): action='store_const', dest='extract_flat', const='in_playlist', default=False, help='Do not extract the videos of a playlist, only list them.') + general.add_option( + '--concat-playlist', + action='store_true', dest='concat_playlist', default=False, + help='Concatenate all videos in a playlist into a single video file. Useful for services which split up full ' + 'episodes into multiple segments.') general.add_option( '--mark-watched', action='store_true', dest='mark_watched', default=False, diff --git a/youtube_dl/postprocessor/__init__.py b/youtube_dl/postprocessor/__init__.py index 3ea518399..631b48400 100644 --- a/youtube_dl/postprocessor/__init__.py +++ b/youtube_dl/postprocessor/__init__.py @@ -9,6 +9,7 @@ from .ffmpeg import ( FFmpegFixupM3u8PP, FFmpegFixupM4aPP, FFmpegMergerPP, + FFmpegConcatPP, FFmpegMetadataPP, FFmpegVideoConvertorPP, FFmpegSubtitlesConvertorPP, @@ -31,6 +32,7 @@ __all__ = [ 'FFmpegFixupM4aPP', 'FFmpegFixupStretchedPP', 'FFmpegMergerPP', + 'FFmpegConcatPP', 'FFmpegMetadataPP', 'FFmpegPostProcessor', 'FFmpegSubtitlesConvertorPP', diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index 5a3359588..782fab4fc 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -81,7 +81,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor): self._downloader.to_screen('[ffmpeg] Adding thumbnail to "%s"' % filename) - self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options) + self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options, []) if not self._already_have_thumbnail: os.remove(encodeFilename(thumbnail_filename)) diff --git a/youtube_dl/postprocessor/ffmpeg.py b/youtube_dl/postprocessor/ffmpeg.py index 5f7298345..d3a27225d 100644 --- a/youtube_dl/postprocessor/ffmpeg.py +++ b/youtube_dl/postprocessor/ffmpeg.py @@ -6,7 +6,6 @@ import subprocess import time import re - from .common import AudioConversionError, PostProcessor from ..utils import ( @@ -23,7 +22,6 @@ from ..utils import ( replace_extension, ) - EXT_TO_OUT_FORMATS = { 'aac': 'adts', 'flac': 'flac', @@ -203,7 +201,7 @@ class FFmpegPostProcessor(PostProcessor): return mobj.group(1) return None - def run_ffmpeg_multiple_files(self, input_paths, out_path, opts): + def run_ffmpeg_multiple_files(self, input_paths, out_path, opts, file_opts): self.check_version() oldest_mtime = min( @@ -214,6 +212,7 @@ class FFmpegPostProcessor(PostProcessor): files_cmd = [] for path in input_paths: files_cmd.extend([ + *file_opts, encodeArgument('-i'), encodeFilename(self._ffmpeg_filename_argument(path), True) ]) @@ -235,8 +234,8 @@ class FFmpegPostProcessor(PostProcessor): raise FFmpegPostProcessorError(msg) self.try_utime(out_path, oldest_mtime, oldest_mtime) - def run_ffmpeg(self, path, out_path, opts): - self.run_ffmpeg_multiple_files([path], out_path, opts) + def run_ffmpeg(self, path, out_path, opts, file_opts): + self.run_ffmpeg_multiple_files([path], out_path, opts, file_opts) def _ffmpeg_filename_argument(self, fn): # Always use 'file:' because the filename may contain ':' (ffmpeg @@ -274,7 +273,8 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor): raise PostProcessingError('WARNING: unable to obtain file audio codec with ffprobe') more_opts = [] - if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'): + if self._preferredcodec == 'best' or self._preferredcodec == filecodec or ( + self._preferredcodec == 'm4a' and filecodec == 'aac'): if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']: # Lossless, but in another container acodec = 'copy' @@ -319,7 +319,8 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor): extension = 'wav' more_opts += ['-f', 'wav'] - prefix, sep, ext = path.rpartition('.') # not os.path.splitext, since the latter does not work on unicode in all setups + prefix, sep, ext = path.rpartition( + '.') # not os.path.splitext, since the latter does not work on unicode in all setups new_path = prefix + sep + extension information['filepath'] = new_path @@ -357,14 +358,16 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor): def run(self, information): path = information['filepath'] if information['ext'] == self._preferedformat: - self._downloader.to_screen('[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat)) + self._downloader.to_screen( + '[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat)) return [], information options = [] if self._preferedformat == 'avi': options.extend(['-c:v', 'libxvid', '-vtag', 'XVID']) prefix, sep, ext = path.rpartition('.') outpath = prefix + sep + self._preferedformat - self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath) + self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % ( + information['ext'], self._preferedformat) + outpath) self.run_ffmpeg(path, outpath, options) information['filepath'] = outpath information['format'] = self._preferedformat @@ -423,7 +426,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): temp_filename = prepend_extension(filename, 'temp') self._downloader.to_screen('[ffmpeg] Embedding subtitles in \'%s\'' % filename) - self.run_ffmpeg_multiple_files(input_files, temp_filename, opts) + self.run_ffmpeg_multiple_files(input_files, temp_filename, opts, []) os.remove(encodeFilename(filename)) os.rename(encodeFilename(temp_filename), encodeFilename(filename)) @@ -506,7 +509,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor): options.extend(['-map_metadata', '1']) self._downloader.to_screen('[ffmpeg] Adding metadata to \'%s\'' % filename) - self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options) + self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options, []) if chapters: os.remove(metadata_filename) os.remove(encodeFilename(filename)) @@ -520,7 +523,7 @@ class FFmpegMergerPP(FFmpegPostProcessor): temp_filename = prepend_extension(filename, 'temp') args = ['-c', 'copy', '-map', '0:v:0', '-map', '1:a:0'] self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename) - self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args) + self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args, []) os.rename(encodeFilename(temp_filename), encodeFilename(filename)) return info['__files_to_merge'], info @@ -542,6 +545,29 @@ class FFmpegMergerPP(FFmpegPostProcessor): return True +class FFmpegConcatPP(FFmpegPostProcessor): + def run(self, info): + filename = info['filepath'] + + list_filename = prepend_extension(filename, 'list') + temp_filename = prepend_extension(filename, 'temp') + + with open(list_filename, 'wt') as f: + f.write('ffconcat version 1.0\n') + for file in info['__files_to_concat']: + f.write("file '%s'\n" % self._ffmpeg_filename_argument(file)) + + file_opts = ['-f', 'concat', '-safe', '0'] + opts = ['-c', 'copy'] + self._downloader.to_screen('[ffmpeg] Concatenating files into "%s"' % filename) + self.run_ffmpeg(list_filename, temp_filename, opts, file_opts) + os.rename(encodeFilename(temp_filename), encodeFilename(filename)) + return info['__files_to_concat'], info + + def can_merge(self): + return True + + class FFmpegFixupStretchedPP(FFmpegPostProcessor): def run(self, info): stretched_ratio = info.get('stretched_ratio') @@ -587,7 +613,7 @@ class FFmpegFixupM3u8PP(FFmpegPostProcessor): options = ['-c', 'copy', '-f', 'mp4', '-bsf:a', 'aac_adtstoasc'] self._downloader.to_screen('[ffmpeg] Fixing malformed AAC bitstream in "%s"' % filename) - self.run_ffmpeg(filename, temp_filename, options) + self.run_ffmpeg(filename, temp_filename, options, []) os.remove(encodeFilename(filename)) os.rename(encodeFilename(temp_filename), encodeFilename(filename))