8     compat_subprocess_get_DEVNULL
, 
  16 class PostProcessor(object): 
  17     """Post Processor class. 
  19     PostProcessor objects can be added to downloaders with their 
  20     add_post_processor() method. When the downloader has finished a 
  21     successful download, it will take its internal chain of PostProcessors 
  22     and start calling the run() method on each one of them, first with 
  23     an initial argument and then with the returned value of the previous 
  26     The chain will be stopped if one of them ever returns None or the end 
  27     of the chain is reached. 
  29     PostProcessor objects follow a "mutual registration" process similar 
  30     to InfoExtractor objects. 
  35     def __init__(self
, downloader
=None): 
  36         self
._downloader 
= downloader
 
  38     def set_downloader(self
, downloader
): 
  39         """Sets the downloader for this PP.""" 
  40         self
._downloader 
= downloader
 
  42     def run(self
, information
): 
  43         """Run the PostProcessor. 
  45         The "information" argument is a dictionary like the ones 
  46         composed by InfoExtractors. The only difference is that this 
  47         one has an extra field called "filepath" that points to the 
  50         This method returns a tuple, the first element of which describes 
  51         whether the original file should be kept (i.e. not deleted - None for 
  52         no preference), and the second of which is the updated information. 
  54         In addition, this method may raise a PostProcessingError 
  55         exception if post processing fails. 
  57         return None, information 
# by default, keep file and do nothing 
  59 class FFmpegPostProcessorError(PostProcessingError
): 
  62 class AudioConversionError(PostProcessingError
): 
  65 class FFmpegPostProcessor(PostProcessor
): 
  66     def __init__(self
,downloader
=None): 
  67         PostProcessor
.__init
__(self
, downloader
) 
  68         self
._exes 
= self
.detect_executables() 
  71     def detect_executables(): 
  74                 subprocess
.Popen([exe
, '-version'], stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
).communicate() 
  78         programs 
= ['avprobe', 'avconv', 'ffmpeg', 'ffprobe'] 
  79         return dict((program
, executable(program
)) for program 
in programs
) 
  81     def run_ffmpeg_multiple_files(self
, input_paths
, out_path
, opts
): 
  82         if not self
._exes
['ffmpeg'] and not self
._exes
['avconv']: 
  83             raise FFmpegPostProcessorError(u
'ffmpeg or avconv not found. Please install one.') 
  86         for path 
in input_paths
: 
  87             files_cmd
.extend(['-i', encodeFilename(path
)]) 
  88         cmd 
= ([self
._exes
['avconv'] or self
._exes
['ffmpeg'], '-y'] + files_cmd
 
  90                [encodeFilename(self
._ffmpeg
_filename
_argument
(out_path
))]) 
  92         if self
._downloader
.params
.get('verbose', False): 
  93             self
._downloader
.to_screen(u
'[debug] ffmpeg command line: %s' % shell_quote(cmd
)) 
  94         p 
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
) 
  95         stdout
,stderr 
= p
.communicate() 
  97             stderr 
= stderr
.decode('utf-8', 'replace') 
  98             msg 
= stderr
.strip().split('\n')[-1] 
  99             raise FFmpegPostProcessorError(msg
) 
 101     def run_ffmpeg(self
, path
, out_path
, opts
): 
 102         self
.run_ffmpeg_multiple_files([path
], out_path
, opts
) 
 104     def _ffmpeg_filename_argument(self
, fn
): 
 105         # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details 
 106         if fn
.startswith(u
'-'): 
 110 class FFmpegExtractAudioPP(FFmpegPostProcessor
): 
 111     def __init__(self
, downloader
=None, preferredcodec
=None, preferredquality
=None, nopostoverwrites
=False): 
 112         FFmpegPostProcessor
.__init
__(self
, downloader
) 
 113         if preferredcodec 
is None: 
 114             preferredcodec 
= 'best' 
 115         self
._preferredcodec 
= preferredcodec
 
 116         self
._preferredquality 
= preferredquality
 
 117         self
._nopostoverwrites 
= nopostoverwrites
 
 119     def get_audio_codec(self
, path
): 
 120         if not self
._exes
['ffprobe'] and not self
._exes
['avprobe']: 
 121             raise PostProcessingError(u
'ffprobe or avprobe not found. Please install one.') 
 123             cmd 
= [self
._exes
['avprobe'] or self
._exes
['ffprobe'], '-show_streams', encodeFilename(self
._ffmpeg
_filename
_argument
(path
))] 
 124             handle 
= subprocess
.Popen(cmd
, stderr
=compat_subprocess_get_DEVNULL(), stdout
=subprocess
.PIPE
) 
 125             output 
= handle
.communicate()[0] 
 126             if handle
.wait() != 0: 
 128         except (IOError, OSError): 
 131         for line 
in output
.decode('ascii', 'ignore').split('\n'): 
 132             if line
.startswith('codec_name='): 
 133                 audio_codec 
= line
.split('=')[1].strip() 
 134             elif line
.strip() == 'codec_type=audio' and audio_codec 
is not None: 
 138     def run_ffmpeg(self
, path
, out_path
, codec
, more_opts
): 
 139         if not self
._exes
['ffmpeg'] and not self
._exes
['avconv']: 
 140             raise AudioConversionError('ffmpeg or avconv not found. Please install one.') 
 144             acodec_opts 
= ['-acodec', codec
] 
 145         opts 
= ['-vn'] + acodec_opts 
+ more_opts
 
 147             FFmpegPostProcessor
.run_ffmpeg(self
, path
, out_path
, opts
) 
 148         except FFmpegPostProcessorError 
as err
: 
 149             raise AudioConversionError(err
.msg
) 
 151     def run(self
, information
): 
 152         path 
= information
['filepath'] 
 154         filecodec 
= self
.get_audio_codec(path
) 
 155         if filecodec 
is None: 
 156             raise PostProcessingError(u
'WARNING: unable to obtain file audio codec with ffprobe') 
 159         if self
._preferredcodec 
== 'best' or self
._preferredcodec 
== filecodec 
or (self
._preferredcodec 
== 'm4a' and filecodec 
== 'aac'): 
 160             if filecodec 
== 'aac' and self
._preferredcodec 
in ['m4a', 'best']: 
 161                 # Lossless, but in another container 
 164                 more_opts 
= [self
._exes
['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc'] 
 165             elif filecodec 
in ['aac', 'mp3', 'vorbis', 'opus']: 
 166                 # Lossless if possible 
 168                 extension 
= filecodec
 
 169                 if filecodec 
== 'aac': 
 170                     more_opts 
= ['-f', 'adts'] 
 171                 if filecodec 
== 'vorbis': 
 175                 acodec 
= 'libmp3lame' 
 178                 if self
._preferredquality 
is not None: 
 179                     if int(self
._preferredquality
) < 10: 
 180                         more_opts 
+= [self
._exes
['avconv'] and '-q:a' or '-aq', self
._preferredquality
] 
 182                         more_opts 
+= [self
._exes
['avconv'] and '-b:a' or '-ab', self
._preferredquality 
+ 'k'] 
 184             # We convert the audio (lossy) 
 185             acodec 
= {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self
._preferredcodec
] 
 186             extension 
= self
._preferredcodec
 
 188             if self
._preferredquality 
is not None: 
 189                 # The opus codec doesn't support the -aq option 
 190                 if int(self
._preferredquality
) < 10 and extension 
!= 'opus': 
 191                     more_opts 
+= [self
._exes
['avconv'] and '-q:a' or '-aq', self
._preferredquality
] 
 193                     more_opts 
+= [self
._exes
['avconv'] and '-b:a' or '-ab', self
._preferredquality 
+ 'k'] 
 194             if self
._preferredcodec 
== 'aac': 
 195                 more_opts 
+= ['-f', 'adts'] 
 196             if self
._preferredcodec 
== 'm4a': 
 197                 more_opts 
+= [self
._exes
['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc'] 
 198             if self
._preferredcodec 
== 'vorbis': 
 200             if self
._preferredcodec 
== 'wav': 
 202                 more_opts 
+= ['-f', 'wav'] 
 204         prefix
, sep
, ext 
= path
.rpartition(u
'.') # not os.path.splitext, since the latter does not work on unicode in all setups 
 205         new_path 
= prefix 
+ sep 
+ extension
 
 207         # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly. 
 209             self
._nopostoverwrites 
= True 
 212             if self
._nopostoverwrites 
and os
.path
.exists(encodeFilename(new_path
)): 
 213                 self
._downloader
.to_screen(u
'[youtube] Post-process file %s exists, skipping' % new_path
) 
 215                 self
._downloader
.to_screen(u
'[' + (self
._exes
['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path
) 
 216                 self
.run_ffmpeg(path
, new_path
, acodec
, more_opts
) 
 218             etype
,e
,tb 
= sys
.exc_info() 
 219             if isinstance(e
, AudioConversionError
): 
 220                 msg 
= u
'audio conversion failed: ' + e
.msg
 
 222                 msg 
= u
'error running ' + (self
._exes
['avconv'] and 'avconv' or 'ffmpeg') 
 223             raise PostProcessingError(msg
) 
 225         # Try to update the date time for extracted audio file. 
 226         if information
.get('filetime') is not None: 
 228                 os
.utime(encodeFilename(new_path
), (time
.time(), information
['filetime'])) 
 230                 self
._downloader
.report_warning(u
'Cannot update utime of audio file') 
 232         information
['filepath'] = new_path
 
 233         return self
._nopostoverwrites
,information
 
 235 class FFmpegVideoConvertor(FFmpegPostProcessor
): 
 236     def __init__(self
, downloader
=None,preferedformat
=None): 
 237         super(FFmpegVideoConvertor
, self
).__init
__(downloader
) 
 238         self
._preferedformat
=preferedformat
 
 240     def run(self
, information
): 
 241         path 
= information
['filepath'] 
 242         prefix
, sep
, ext 
= path
.rpartition(u
'.') 
 243         outpath 
= prefix 
+ sep 
+ self
._preferedformat
 
 244         if information
['ext'] == self
._preferedformat
: 
 245             self
._downloader
.to_screen(u
'[ffmpeg] Not converting video file %s - already is in target format %s' % (path
, self
._preferedformat
)) 
 246             return True,information
 
 247         self
._downloader
.to_screen(u
'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information
['ext'], self
._preferedformat
) +outpath
) 
 248         self
.run_ffmpeg(path
, outpath
, []) 
 249         information
['filepath'] = outpath
 
 250         information
['format'] = self
._preferedformat
 
 251         information
['ext'] = self
._preferedformat
 
 252         return False,information
 
 255 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor
): 
 256     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt 
 444     def __init__(self
, downloader
=None, subtitlesformat
='srt'): 
 445         super(FFmpegEmbedSubtitlePP
, self
).__init
__(downloader
) 
 446         self
._subformat 
= subtitlesformat
 
 449     def _conver_lang_code(cls
, code
): 
 450         """Convert language code from ISO 639-1 to ISO 639-2/T""" 
 451         return cls
._lang
_map
.get(code
[:2]) 
 453     def run(self
, information
): 
 454         if information
['ext'] != u
'mp4': 
 455             self
._downloader
.to_screen(u
'[ffmpeg] Subtitles can only be embedded in mp4 files') 
 456             return True, information
 
 457         if not information
.get('subtitles'): 
 458             self
._downloader
.to_screen(u
'[ffmpeg] There aren\'t any subtitles to embed')  
 459             return True, information
 
 461         sub_langs 
= [key 
for key 
in information
['subtitles']] 
 462         filename 
= information
['filepath'] 
 463         input_files 
= [filename
] + [subtitles_filename(filename
, lang
, self
._subformat
) for lang 
in sub_langs
] 
 465         opts 
= ['-map', '0:0', '-map', '0:1', '-c:v', 'copy', '-c:a', 'copy'] 
 466         for (i
, lang
) in enumerate(sub_langs
): 
 467             opts
.extend(['-map', '%d:0' % (i
+1), '-c:s:%d' % i
, 'mov_text']) 
 468             lang_code 
= self
._conver
_lang
_code
(lang
) 
 469             if lang_code 
is not None: 
 470                 opts
.extend(['-metadata:s:s:%d' % i
, 'language=%s' % lang_code
]) 
 471         opts
.extend(['-f', 'mp4']) 
 473         temp_filename 
= filename 
+ u
'.temp' 
 474         self
._downloader
.to_screen(u
'[ffmpeg] Embedding subtitles in \'%s\'' % filename
) 
 475         self
.run_ffmpeg_multiple_files(input_files
, temp_filename
, opts
) 
 476         os
.remove(encodeFilename(filename
)) 
 477         os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
)) 
 479         return True, information
 
 482 class FFmpegMetadataPP(FFmpegPostProcessor
): 
 485         if info
.get('title') is not None: 
 486             metadata
['title'] = info
['title'] 
 487         if info
.get('upload_date') is not None: 
 488             metadata
['date'] = info
['upload_date'] 
 489         if info
.get('uploader') is not None: 
 490             metadata
['artist'] = info
['uploader'] 
 491         elif info
.get('uploader_id') is not None: 
 492             metadata
['artist'] = info
['uploader_id'] 
 495             self
._downloader
.to_screen(u
'[ffmpeg] There isn\'t any metadata to add') 
 498         filename 
= info
['filepath'] 
 499         ext 
= os
.path
.splitext(filename
)[1][1:] 
 500         temp_filename 
= filename 
+ u
'.temp' 
 502         options 
= ['-c', 'copy'] 
 503         for (name
, value
) in metadata
.items(): 
 504             options
.extend(['-metadata', '%s=%s' % (name
, value
)]) 
 505         options
.extend(['-f', ext
]) 
 507         self
._downloader
.to_screen(u
'[ffmpeg] Adding metadata to \'%s\'' % filename
) 
 508         self
.run_ffmpeg(filename
, temp_filename
, options
) 
 509         os
.remove(encodeFilename(filename
)) 
 510         os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
))