]>
Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/PostProcessor.py
8868b37af6455523bad0fbf9a6702fd37e45d024
   2 # -*- coding: utf-8 -*- 
   4 from __future__ 
import absolute_import
 
  14 class PostProcessor(object): 
  15     """Post Processor class. 
  17     PostProcessor objects can be added to downloaders with their 
  18     add_post_processor() method. When the downloader has finished a 
  19     successful download, it will take its internal chain of PostProcessors 
  20     and start calling the run() method on each one of them, first with 
  21     an initial argument and then with the returned value of the previous 
  24     The chain will be stopped if one of them ever returns None or the end 
  25     of the chain is reached. 
  27     PostProcessor objects follow a "mutual registration" process similar 
  28     to InfoExtractor objects. 
  33     def __init__(self
, downloader
=None): 
  34         self
._downloader 
= downloader
 
  36     def set_downloader(self
, downloader
): 
  37         """Sets the downloader for this PP.""" 
  38         self
._downloader 
= downloader
 
  40     def run(self
, information
): 
  41         """Run the PostProcessor. 
  43         The "information" argument is a dictionary like the ones 
  44         composed by InfoExtractors. The only difference is that this 
  45         one has an extra field called "filepath" that points to the 
  48         This method returns a tuple, the first element of which describes 
  49         whether the original file should be kept (i.e. not deleted - None for 
  50         no preference), and the second of which is the updated information. 
  52         In addition, this method may raise a PostProcessingError 
  53         exception if post processing fails. 
  55         return None, information 
# by default, keep file and do nothing 
  57 class FFmpegPostProcessorError(PostProcessingError
): 
  60 class AudioConversionError(PostProcessingError
): 
  63 class FFmpegPostProcessor(PostProcessor
): 
  64     def __init__(self
,downloader
=None): 
  65         PostProcessor
.__init
__(self
, downloader
) 
  66         self
._exes 
= self
.detect_executables() 
  69     def detect_executables(): 
  72                 subprocess
.Popen([exe
, '-version'], stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
).communicate() 
  76         programs 
= ['avprobe', 'avconv', 'ffmpeg', 'ffprobe'] 
  77         return dict((program
, executable(program
)) for program 
in programs
) 
  79     def run_ffmpeg(self
, path
, out_path
, opts
): 
  80         if not self
._exes
['ffmpeg'] and not self
._exes
['avconv']: 
  81             raise FFmpegPostProcessorError(u
'ffmpeg or avconv not found. Please install one.') 
  82         cmd 
= ([self
._exes
['avconv'] or self
._exes
['ffmpeg'], '-y', '-i', encodeFilename(path
)] 
  84                [encodeFilename(self
._ffmpeg
_filename
_argument
(out_path
))]) 
  85         p 
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
) 
  86         stdout
,stderr 
= p
.communicate() 
  88             stderr 
= stderr
.decode('utf-8', 'replace') 
  89             msg 
= stderr
.strip().split('\n')[-1] 
  90             raise FFmpegPostProcessorError(msg
) 
  92     def _ffmpeg_filename_argument(self
, fn
): 
  93         # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details 
  94         if fn
.startswith(u
'-'): 
  98 class FFmpegExtractAudioPP(FFmpegPostProcessor
): 
  99     def __init__(self
, downloader
=None, preferredcodec
=None, preferredquality
=None, nopostoverwrites
=False): 
 100         FFmpegPostProcessor
.__init
__(self
, downloader
) 
 101         if preferredcodec 
is None: 
 102             preferredcodec 
= 'best' 
 103         self
._preferredcodec 
= preferredcodec
 
 104         self
._preferredquality 
= preferredquality
 
 105         self
._nopostoverwrites 
= nopostoverwrites
 
 107     def get_audio_codec(self
, path
): 
 108         if not self
._exes
['ffprobe'] and not self
._exes
['avprobe']: return None 
 110             cmd 
= [self
._exes
['avprobe'] or self
._exes
['ffprobe'], '-show_streams', encodeFilename(self
._ffmpeg
_filename
_argument
(path
))] 
 111             handle 
= subprocess
.Popen(cmd
, stderr
=compat_subprocess_get_DEVNULL(), stdout
=subprocess
.PIPE
) 
 112             output 
= handle
.communicate()[0] 
 113             if handle
.wait() != 0: 
 115         except (IOError, OSError): 
 118         for line 
in output
.decode('ascii', 'ignore').split('\n'): 
 119             if line
.startswith('codec_name='): 
 120                 audio_codec 
= line
.split('=')[1].strip() 
 121             elif line
.strip() == 'codec_type=audio' and audio_codec 
is not None: 
 125     def run_ffmpeg(self
, path
, out_path
, codec
, more_opts
): 
 126         if not self
._exes
['ffmpeg'] and not self
._exes
['avconv']: 
 127             raise AudioConversionError('ffmpeg or avconv not found. Please install one.') 
 131             acodec_opts 
= ['-acodec', codec
] 
 132         opts 
= ['-vn'] + acodec_opts 
+ more_opts
 
 134             FFmpegPostProcessor
.run_ffmpeg(self
, path
, out_path
, opts
) 
 135         except FFmpegPostProcessorError 
as err
: 
 136             raise AudioConversionError(err
.message
) 
 138     def run(self
, information
): 
 139         path 
= information
['filepath'] 
 141         filecodec 
= self
.get_audio_codec(path
) 
 142         if filecodec 
is None: 
 143             raise PostProcessingError(u
'WARNING: unable to obtain file audio codec with ffprobe') 
 146         if self
._preferredcodec 
== 'best' or self
._preferredcodec 
== filecodec 
or (self
._preferredcodec 
== 'm4a' and filecodec 
== 'aac'): 
 147             if filecodec 
== 'aac' and self
._preferredcodec 
in ['m4a', 'best']: 
 148                 # Lossless, but in another container 
 151                 more_opts 
= [self
._exes
['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc'] 
 152             elif filecodec 
in ['aac', 'mp3', 'vorbis', 'opus']: 
 153                 # Lossless if possible 
 155                 extension 
= filecodec
 
 156                 if filecodec 
== 'aac': 
 157                     more_opts 
= ['-f', 'adts'] 
 158                 if filecodec 
== 'vorbis': 
 162                 acodec 
= 'libmp3lame' 
 165                 if self
._preferredquality 
is not None: 
 166                     if int(self
._preferredquality
) < 10: 
 167                         more_opts 
+= [self
._exes
['avconv'] and '-q:a' or '-aq', self
._preferredquality
] 
 169                         more_opts 
+= [self
._exes
['avconv'] and '-b:a' or '-ab', self
._preferredquality 
+ 'k'] 
 171             # We convert the audio (lossy) 
 172             acodec 
= {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self
._preferredcodec
] 
 173             extension 
= self
._preferredcodec
 
 175             if self
._preferredquality 
is not None: 
 176                 if int(self
._preferredquality
) < 10: 
 177                     more_opts 
+= [self
._exes
['avconv'] and '-q:a' or '-aq', self
._preferredquality
] 
 179                     more_opts 
+= [self
._exes
['avconv'] and '-b:a' or '-ab', self
._preferredquality 
+ 'k'] 
 180             if self
._preferredcodec 
== 'aac': 
 181                 more_opts 
+= ['-f', 'adts'] 
 182             if self
._preferredcodec 
== 'm4a': 
 183                 more_opts 
+= [self
._exes
['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc'] 
 184             if self
._preferredcodec 
== 'vorbis': 
 186             if self
._preferredcodec 
== 'wav': 
 188                 more_opts 
+= ['-f', 'wav'] 
 190         prefix
, sep
, ext 
= path
.rpartition(u
'.') # not os.path.splitext, since the latter does not work on unicode in all setups 
 191         new_path 
= prefix 
+ sep 
+ extension
 
 193         # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly. 
 195             self
._nopostoverwrites 
= True 
 198             if self
._nopostoverwrites 
and os
.path
.exists(encodeFilename(new_path
)): 
 199                 self
._downloader
.to_screen(u
'[youtube] Post-process file %s exists, skipping' % new_path
) 
 201                 self
._downloader
.to_screen(u
'[' + (self
._exes
['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path
) 
 202                 self
.run_ffmpeg(path
, new_path
, acodec
, more_opts
) 
 204             etype
,e
,tb 
= sys
.exc_info() 
 205             if isinstance(e
, AudioConversionError
): 
 206                 msg 
= u
'audio conversion failed: ' + e
.message
 
 208                 msg 
= u
'error running ' + (self
._exes
['avconv'] and 'avconv' or 'ffmpeg') 
 209             raise PostProcessingError(msg
) 
 211         # Try to update the date time for extracted audio file. 
 212         if information
.get('filetime') is not None: 
 214                 os
.utime(encodeFilename(new_path
), (time
.time(), information
['filetime'])) 
 216                 self
._downloader
.to_stderr(u
'WARNING: Cannot update utime of audio file') 
 218         information
['filepath'] = new_path
 
 219         return self
._nopostoverwrites
,information
 
 221 class FFmpegVideoConvertor(FFmpegPostProcessor
): 
 222     def __init__(self
, downloader
=None,preferedformat
=None): 
 223         super(FFmpegVideoConvertor
, self
).__init
__(downloader
) 
 224         self
._preferedformat
=preferedformat
 
 226     def run(self
, information
): 
 227         path 
= information
['filepath'] 
 228         prefix
, sep
, ext 
= path
.rpartition(u
'.') 
 229         outpath 
= prefix 
+ sep 
+ self
._preferedformat
 
 230         if information
['ext'] == self
._preferedformat
: 
 231             self
._downloader
.to_screen(u
'[ffmpeg] Not converting video file %s - already is in target format %s' % (path
, self
._preferedformat
)) 
 232             return True,information
 
 233         self
._downloader
.to_screen(u
'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information
['ext'], self
._preferedformat
) +outpath
) 
 234         self
.run_ffmpeg(path
, outpath
, []) 
 235         information
['filepath'] = outpath
 
 236         information
['format'] = self
._preferedformat
 
 237         information
['ext'] = self
._preferedformat
 
 238         return False,information