--recode-video option (Closes #18)
authorPhilipp Hagemeister <phihag@phihag.de>
Sat, 12 Jan 2013 14:07:59 +0000 (15:07 +0100)
committerPhilipp Hagemeister <phihag@phihag.de>
Sat, 12 Jan 2013 14:09:09 +0000 (15:09 +0100)
youtube_dl/FileDownloader.py
youtube_dl/PostProcessor.py
youtube_dl/__init__.py
youtube_dl/utils.py

index be9e4918ec0983eaf8e188a46acf090ff8deb4de..f0879a675b5f83ea95f1066c1f8ffcb8fff163eb 100644 (file)
@@ -81,6 +81,7 @@ class FileDownloader(object):
     writesubtitles:    Write the video subtitles to a .srt file
     subtitleslang:     Language of the subtitles to download
     test:              Download only first bytes to test the downloader.
     writesubtitles:    Write the video subtitles to a .srt file
     subtitleslang:     Language of the subtitles to download
     test:              Download only first bytes to test the downloader.
+    keepvideo:         Keep the video file after post-processing
     """
 
     params = None
     """
 
     params = None
@@ -529,13 +530,27 @@ class FileDownloader(object):
         return self._download_retcode
 
     def post_process(self, filename, ie_info):
         return self._download_retcode
 
     def post_process(self, filename, ie_info):
-        """Run the postprocessing chain on the given file."""
+        """Run all the postprocessors on the given file."""
         info = dict(ie_info)
         info['filepath'] = filename
         info = dict(ie_info)
         info['filepath'] = filename
+        keep_video = None
         for pp in self._pps:
         for pp in self._pps:
-            info = pp.run(info)
-            if info is None:
-                break
+            try:
+                keep_video_wish,new_info = pp.run(info)
+                if keep_video_wish is not None:
+                    if keep_video_wish:
+                        keep_video = keep_video_wish
+                    elif keep_video is None:
+                        # No clear decision yet, let IE decide
+                        keep_video = keep_video_wish
+            except PostProcessingError as e:
+                self.to_stderr(u'ERROR: ' + e.msg)
+        if not keep_video and not self.params.get('keepvideo', False):
+            try:
+                self.to_stderr(u'Deleting original file %s (pass -k to keep)' % filename)
+                os.remove(encodeFilename(filename))
+            except (IOError, OSError):
+                self.to_stderr(u'WARNING: Unable to remove downloaded video file')
 
     def _download_with_rtmpdump(self, filename, url, player_url, page_url):
         self.report_destination(filename)
 
     def _download_with_rtmpdump(self, filename, url, player_url, page_url):
         self.report_destination(filename)
index 3d10937312205884d7627f541031f7621248cb49..545b6992b4c41c8cc59c260f6eb9faa21e14eb91 100644 (file)
@@ -45,25 +45,20 @@ class PostProcessor(object):
         one has an extra field called "filepath" that points to the
         downloaded file.
 
         one has an extra field called "filepath" that points to the
         downloaded file.
 
-        When this method returns None, the postprocessing chain is
-        stopped. However, this method may return an information
-        dictionary that will be passed to the next postprocessing
-        object in the chain. It can be the one it received after
-        changing some fields.
+        This method returns a tuple, the first element of which describes
+        whether the original file should be kept (i.e. not deleted - None for
+        no preference), and the second of which is the updated information.
 
         In addition, this method may raise a PostProcessingError
 
         In addition, this method may raise a PostProcessingError
-        exception that will be taken into account by the downloader
-        it was called from.
+        exception if post processing fails.
         """
         """
-        return information # by default, do nothing
+        return None, information # by default, keep file and do nothing
 
 
-class FFmpegPostProcessorError(BaseException):
-    def __init__(self, message):
-        self.message = message
+class FFmpegPostProcessorError(PostProcessingError):
+    pass
 
 
-class AudioConversionError(BaseException):
-    def __init__(self, message):
-        self.message = message
+class AudioConversionError(PostProcessingError):
+    pass
 
 class FFmpegPostProcessor(PostProcessor):
     def __init__(self,downloader=None):
 
 class FFmpegPostProcessor(PostProcessor):
     def __init__(self,downloader=None):
@@ -83,7 +78,7 @@ class FFmpegPostProcessor(PostProcessor):
 
     def run_ffmpeg(self, path, out_path, opts):
         if not self._exes['ffmpeg'] and not self._exes['avconv']:
 
     def run_ffmpeg(self, path, out_path, opts):
         if not self._exes['ffmpeg'] and not self._exes['avconv']:
-            raise FFmpegPostProcessorError('ffmpeg or avconv not found. Please install one.')
+            raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.')
         cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path)]
                + opts +
                [encodeFilename(self._ffmpeg_filename_argument(out_path))])
         cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path)]
                + opts +
                [encodeFilename(self._ffmpeg_filename_argument(out_path))])
@@ -91,7 +86,7 @@ class FFmpegPostProcessor(PostProcessor):
         stdout,stderr = p.communicate()
         if p.returncode != 0:
             msg = stderr.strip().split('\n')[-1]
         stdout,stderr = p.communicate()
         if p.returncode != 0:
             msg = stderr.strip().split('\n')[-1]
-            raise FFmpegPostProcessorError(msg)
+            raise FFmpegPostProcessorError(msg.decode('utf-8', 'replace'))
 
     def _ffmpeg_filename_argument(self, fn):
         # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
 
     def _ffmpeg_filename_argument(self, fn):
         # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
@@ -100,13 +95,12 @@ class FFmpegPostProcessor(PostProcessor):
         return fn
 
 class FFmpegExtractAudioPP(FFmpegPostProcessor):
         return fn
 
 class FFmpegExtractAudioPP(FFmpegPostProcessor):
-    def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, keepvideo=False, nopostoverwrites=False):
+    def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
         FFmpegPostProcessor.__init__(self, downloader)
         if preferredcodec is None:
             preferredcodec = 'best'
         self._preferredcodec = preferredcodec
         self._preferredquality = preferredquality
         FFmpegPostProcessor.__init__(self, downloader)
         if preferredcodec is None:
             preferredcodec = 'best'
         self._preferredcodec = preferredcodec
         self._preferredquality = preferredquality
-        self._keepvideo = keepvideo
         self._nopostoverwrites = nopostoverwrites
 
     def get_audio_codec(self, path):
         self._nopostoverwrites = nopostoverwrites
 
     def get_audio_codec(self, path):
@@ -145,8 +139,7 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
 
         filecodec = self.get_audio_codec(path)
         if filecodec is None:
 
         filecodec = self.get_audio_codec(path)
         if filecodec is None:
-            self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe')
-            return None
+            raise PostProcessingError(u'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'):
 
         more_opts = []
         if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
@@ -204,10 +197,10 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
         except:
             etype,e,tb = sys.exc_info()
             if isinstance(e, AudioConversionError):
         except:
             etype,e,tb = sys.exc_info()
             if isinstance(e, AudioConversionError):
-                self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message)
+                msg = u'audio conversion failed: ' + e.message
             else:
             else:
-                self._downloader.to_stderr(u'ERROR: error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg'))
-            return None
+                msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
+            raise PostProcessingError(msg)
 
         # Try to update the date time for extracted audio file.
         if information.get('filetime') is not None:
 
         # Try to update the date time for extracted audio file.
         if information.get('filetime') is not None:
@@ -216,29 +209,24 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
             except:
                 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
 
             except:
                 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
 
-        if not self._keepvideo:
-            try:
-                os.remove(encodeFilename(path))
-            except (IOError, OSError):
-                self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file')
-                return None
-
         information['filepath'] = new_path
         information['filepath'] = new_path
-        return information
+        return False,information
 
 class FFmpegVideoConvertor(FFmpegPostProcessor):
     def __init__(self, downloader=None,preferedformat=None):
 
 class FFmpegVideoConvertor(FFmpegPostProcessor):
     def __init__(self, downloader=None,preferedformat=None):
-        FFmpegPostProcessor.__init__(self,downloader)
+        super(FFmpegVideoConvertor, self).__init__(downloader)
         self._preferedformat=preferedformat
 
     def run(self, information):
         path = information['filepath']
         prefix, sep, ext = path.rpartition(u'.')
         outpath = prefix + sep + self._preferedformat
         self._preferedformat=preferedformat
 
     def run(self, information):
         path = information['filepath']
         prefix, sep, ext = path.rpartition(u'.')
         outpath = prefix + sep + self._preferedformat
-        if not self._preferedformat or information['format'] == self._preferedformat:
-            return information
-        self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['format'], self._preferedformat) +outpath)
+        if information['ext'] == self._preferedformat:
+            self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
+            return True,information
+        self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
         self.run_ffmpeg(path, outpath, [])
         information['filepath'] = outpath
         information['format'] = self._preferedformat
         self.run_ffmpeg(path, outpath, [])
         information['filepath'] = outpath
         information['format'] = self._preferedformat
-        return information
+        information['ext'] = self._preferedformat
+        return False,information
index 95add9eb1102d669983f6647adc6ffb9d7ad58d9..ae12128b96fdba83cedc78857ccf246700681057 100644 (file)
@@ -175,7 +175,6 @@ def parseOpts():
             action='store', dest='subtitleslang', metavar='LANG',
             help='language of the closed captions to download (optional) use IETF language tags like \'en\'')
 
             action='store', dest='subtitleslang', metavar='LANG',
             help='language of the closed captions to download (optional) use IETF language tags like \'en\'')
 
-
     verbosity.add_option('-q', '--quiet',
             action='store_true', dest='quiet', help='activates quiet mode', default=False)
     verbosity.add_option('-s', '--simulate',
     verbosity.add_option('-q', '--quiet',
             action='store_true', dest='quiet', help='activates quiet mode', default=False)
     verbosity.add_option('-s', '--simulate',
@@ -251,6 +250,8 @@ def parseOpts():
             help='"best", "aac", "vorbis", "mp3", "m4a", "opus", or "wav"; best by default')
     postproc.add_option('--audio-quality', metavar='QUALITY', dest='audioquality', default='5',
             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)')
             help='"best", "aac", "vorbis", "mp3", "m4a", "opus", or "wav"; best by default')
     postproc.add_option('--audio-quality', metavar='QUALITY', dest='audioquality', default='5',
             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('-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,
     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,
@@ -380,6 +381,9 @@ def _real_main():
         opts.audioquality = opts.audioquality.strip('k').strip('K')
         if not opts.audioquality.isdigit():
             parser.error(u'invalid audio quality specified')
         opts.audioquality = opts.audioquality.strip('k').strip('K')
         if not opts.audioquality.isdigit():
             parser.error(u'invalid audio quality specified')
+    if opts.recodevideo is not None:
+        if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg']:
+            parser.error(u'invalid video recode format specified')
 
     if sys.version_info < (3,):
         # In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
 
     if sys.version_info < (3,):
         # In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
@@ -436,6 +440,7 @@ def _real_main():
         'prefer_free_formats': opts.prefer_free_formats,
         'verbose': opts.verbose,
         'test': opts.test,
         'prefer_free_formats': opts.prefer_free_formats,
         'verbose': opts.verbose,
         'test': opts.test,
+        'keepvideo': opts.keepvideo,
         })
 
     if opts.verbose:
         })
 
     if opts.verbose:
@@ -457,7 +462,9 @@ def _real_main():
 
     # PostProcessors
     if opts.extractaudio:
 
     # PostProcessors
     if opts.extractaudio:
-        fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, keepvideo=opts.keepvideo, nopostoverwrites=opts.nopostoverwrites))
+        fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, nopostoverwrites=opts.nopostoverwrites))
+    if opts.recodevideo:
+        fd.add_post_processor(FFmpegVideoConvertor(preferedformat=opts.recodevideo))
 
     # Maybe do nothing
     if len(all_urls) < 1:
 
     # Maybe do nothing
     if len(all_urls) < 1:
index 8f856ee8c073dd9095f2b62b1567eabdb321c117..0e37390a25d483fb9152b782431974391739f6fd 100644 (file)
@@ -450,7 +450,8 @@ class PostProcessingError(Exception):
     This exception may be raised by PostProcessor's .run() method to
     indicate an error in the postprocessing task.
     """
     This exception may be raised by PostProcessor's .run() method to
     indicate an error in the postprocessing task.
     """
-    pass
+    def __init__(self, msg):
+        self.msg = msg
 
 class MaxDownloadsReached(Exception):
     """ --max-downloads limit has been reached. """
 
 class MaxDownloadsReached(Exception):
     """ --max-downloads limit has been reached. """