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
))