diff --git a/youtube_dl/YoutubeDL.py b/youtube_dl/YoutubeDL.py index f3666573a..6edf54713 100755 --- a/youtube_dl/YoutubeDL.py +++ b/youtube_dl/YoutubeDL.py @@ -59,7 +59,7 @@ from .utils import ( ) from .extractor import get_info_extractor, gen_extractors from .downloader import get_suitable_downloader -from .postprocessor import FFmpegMergerPP +from .postprocessor import FFmpegMergerPP, FFmpegConcatPP from .version import __version__ @@ -645,6 +645,32 @@ class YoutubeDL(object): extra_info=extra) playlist_results.append(entry_result) ie_result['entries'] = playlist_results + + if download and not(self.params.get('simulate', False)) and \ + self.params.get('concat', False): + downloaded = [] + concat = FFmpegConcatPP(self) + if not concat._get_executable(): + postprocessors = [] + self.report_warning('You have requested multiple ' + 'formats but ffmpeg or avconv are not installed.' + ' The formats won\'t be merged') + else: + postprocessors = [concat] + for f in ie_result['entries']: + new_info = dict(ie_result) + new_info.update(f) + if 'ext' in new_info: ie_result['ext'] = new_info['ext'] + if 'id' in new_info: ie_result['id'] = new_info['id'] + fname = self.prepare_filename(new_info) + downloaded.append(fname) + + filename = self.prepare_filename(ie_result) + + ie_result['__postprocessors'] = postprocessors + ie_result['__files_to_merge'] = downloaded + self.post_process(filename, ie_result) + return ie_result elif result_type == 'compat_list': def _fixup(r): diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 1d8cf9a09..aa2f3bc83 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -500,6 +500,8 @@ def parseOpts(overrideArguments=None): help='ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default 5)') postproc.add_option('--recode-video', metavar='FORMAT', dest='recodevideo', default=None, help='Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm)') + postproc.add_option('--concat', action='store_true', dest='concat', + help='Attempt to concatenate multiple videos into one file') postproc.add_option('-k', '--keep-video', action='store_true', dest='keepvideo', default=False, help='keeps the video file on disk after the post-processing; the video is erased by default') postproc.add_option('--no-post-overwrites', action='store_true', dest='nopostoverwrites', default=False, @@ -518,6 +520,7 @@ def parseOpts(overrideArguments=None): help='Prefer ffmpeg over avconv for running the postprocessors') + parser.add_option_group(general) parser.add_option_group(selection) parser.add_option_group(downloader) @@ -791,6 +794,7 @@ def _real_main(argv=None): 'bidi_workaround': opts.bidi_workaround, 'debug_printtraffic': opts.debug_printtraffic, 'prefer_ffmpeg': opts.prefer_ffmpeg, + 'concat': opts.concat, 'include_ads': opts.include_ads, 'default_search': opts.default_search, 'youtube_include_dash_manifest': opts.youtube_include_dash_manifest, diff --git a/youtube_dl/postprocessor/__init__.py b/youtube_dl/postprocessor/__init__.py index 08e6ddd00..d49f5ae3b 100644 --- a/youtube_dl/postprocessor/__init__.py +++ b/youtube_dl/postprocessor/__init__.py @@ -1,6 +1,7 @@ from .atomicparsley import AtomicParsleyPP from .ffmpeg import ( + FFmpegConcatPP, FFmpegAudioFixPP, FFmpegMergerPP, FFmpegMetadataPP, @@ -12,6 +13,7 @@ from .xattrpp import XAttrMetadataPP __all__ = [ 'AtomicParsleyPP', + 'FFmpegConcatPP', 'FFmpegAudioFixPP', 'FFmpegMergerPP', 'FFmpegMetadataPP', diff --git a/youtube_dl/postprocessor/ffmpeg.py b/youtube_dl/postprocessor/ffmpeg.py index 602e370f4..08c9e8f79 100644 --- a/youtube_dl/postprocessor/ffmpeg.py +++ b/youtube_dl/postprocessor/ffmpeg.py @@ -487,6 +487,35 @@ class FFmpegMergerPP(FFmpegPostProcessor): self.run_ffmpeg_multiple_files(info['__files_to_merge'], filename, args) return True, info +class FFmpegConcatPP(FFmpegPostProcessor): + def run(self, info): + filename = info['filepath'] + args = ['-f', 'concat', '-i', '-', '-c', 'copy'] + self._downloader.to_screen(u'[ffmpeg] Concatenating files into "%s"' % filename) + + if not self._get_executable(): + raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.') + + cmd = ([self._get_executable(), '-y'] + args + + [encodeFilename(self._ffmpeg_filename_argument(filename), True)]) + files = info['__files_to_merge'] + + if self._downloader.params.get('verbose', False): + self._downloader.to_screen(u'[debug] ffmpeg command line: %s' % shell_quote(cmd)) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + + files_cmd = u'' + for path in files: + encoded_path = encodeFilename(path, True) + files_cmd += u'file \'%s\'\n' % path + stdout, stderr = p.communicate(input=files_cmd) + if p.returncode != 0: + stderr = stderr.decode('utf-8', 'replace') + msg = stderr.strip().split('\n')[-1] + raise FFmpegPostProcessorError(msg) + + for path in files: + os.remove(encodeFilename(path)) class FFmpegAudioFixPP(FFmpegPostProcessor): def run(self, info):