]>
Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/PostProcessor.py
9 class PostProcessor(object):
10 """Post Processor class.
12 PostProcessor objects can be added to downloaders with their
13 add_post_processor() method. When the downloader has finished a
14 successful download, it will take its internal chain of PostProcessors
15 and start calling the run() method on each one of them, first with
16 an initial argument and then with the returned value of the previous
19 The chain will be stopped if one of them ever returns None or the end
20 of the chain is reached.
22 PostProcessor objects follow a "mutual registration" process similar
23 to InfoExtractor objects.
28 def __init__(self
, downloader
=None):
29 self
._downloader
= downloader
31 def set_downloader(self
, downloader
):
32 """Sets the downloader for this PP."""
33 self
._downloader
= downloader
35 def run(self
, information
):
36 """Run the PostProcessor.
38 The "information" argument is a dictionary like the ones
39 composed by InfoExtractors. The only difference is that this
40 one has an extra field called "filepath" that points to the
43 This method returns a tuple, the first element of which describes
44 whether the original file should be kept (i.e. not deleted - None for
45 no preference), and the second of which is the updated information.
47 In addition, this method may raise a PostProcessingError
48 exception if post processing fails.
50 return None, information
# by default, keep file and do nothing
52 class FFmpegPostProcessorError(PostProcessingError
):
55 class AudioConversionError(PostProcessingError
):
58 class FFmpegPostProcessor(PostProcessor
):
59 def __init__(self
,downloader
=None):
60 PostProcessor
.__init
__(self
, downloader
)
61 self
._exes
= self
.detect_executables()
64 def detect_executables():
67 subprocess
.Popen([exe
, '-version'], stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
).communicate()
71 programs
= ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
72 return dict((program
, executable(program
)) for program
in programs
)
74 def run_ffmpeg_multiple_files(self
, input_paths
, out_path
, opts
):
75 if not self
._exes
['ffmpeg'] and not self
._exes
['avconv']:
76 raise FFmpegPostProcessorError(u
'ffmpeg or avconv not found. Please install one.')
79 for path
in input_paths
:
80 files_cmd
.extend(['-i', encodeFilename(path
)])
81 cmd
= ([self
._exes
['avconv'] or self
._exes
['ffmpeg'], '-y'] + files_cmd
83 [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 run_ffmpeg(self
, path
, out_path
, opts
):
93 self
.run_ffmpeg_multiple_files([path
], out_path
, opts
)
95 def _ffmpeg_filename_argument(self
, fn
):
96 # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
97 if fn
.startswith(u
'-'):
101 class FFmpegExtractAudioPP(FFmpegPostProcessor
):
102 def __init__(self
, downloader
=None, preferredcodec
=None, preferredquality
=None, nopostoverwrites
=False):
103 FFmpegPostProcessor
.__init
__(self
, downloader
)
104 if preferredcodec
is None:
105 preferredcodec
= 'best'
106 self
._preferredcodec
= preferredcodec
107 self
._preferredquality
= preferredquality
108 self
._nopostoverwrites
= nopostoverwrites
110 def get_audio_codec(self
, path
):
111 if not self
._exes
['ffprobe'] and not self
._exes
['avprobe']:
112 raise PostProcessingError(u
'ffprobe or avprobe not found. Please install one.')
114 cmd
= [self
._exes
['avprobe'] or self
._exes
['ffprobe'], '-show_streams', encodeFilename(self
._ffmpeg
_filename
_argument
(path
))]
115 handle
= subprocess
.Popen(cmd
, stderr
=compat_subprocess_get_DEVNULL(), stdout
=subprocess
.PIPE
)
116 output
= handle
.communicate()[0]
117 if handle
.wait() != 0:
119 except (IOError, OSError):
122 for line
in output
.decode('ascii', 'ignore').split('\n'):
123 if line
.startswith('codec_name='):
124 audio_codec
= line
.split('=')[1].strip()
125 elif line
.strip() == 'codec_type=audio' and audio_codec
is not None:
129 def run_ffmpeg(self
, path
, out_path
, codec
, more_opts
):
130 if not self
._exes
['ffmpeg'] and not self
._exes
['avconv']:
131 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
135 acodec_opts
= ['-acodec', codec
]
136 opts
= ['-vn'] + acodec_opts
+ more_opts
138 FFmpegPostProcessor
.run_ffmpeg(self
, path
, out_path
, opts
)
139 except FFmpegPostProcessorError
as err
:
140 raise AudioConversionError(err
.msg
)
142 def run(self
, information
):
143 path
= information
['filepath']
145 filecodec
= self
.get_audio_codec(path
)
146 if filecodec
is None:
147 raise PostProcessingError(u
'WARNING: unable to obtain file audio codec with ffprobe')
150 if self
._preferredcodec
== 'best' or self
._preferredcodec
== filecodec
or (self
._preferredcodec
== 'm4a' and filecodec
== 'aac'):
151 if filecodec
== 'aac' and self
._preferredcodec
in ['m4a', 'best']:
152 # Lossless, but in another container
155 more_opts
= [self
._exes
['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
156 elif filecodec
in ['aac', 'mp3', 'vorbis', 'opus']:
157 # Lossless if possible
159 extension
= filecodec
160 if filecodec
== 'aac':
161 more_opts
= ['-f', 'adts']
162 if filecodec
== 'vorbis':
166 acodec
= 'libmp3lame'
169 if self
._preferredquality
is not None:
170 if int(self
._preferredquality
) < 10:
171 more_opts
+= [self
._exes
['avconv'] and '-q:a' or '-aq', self
._preferredquality
]
173 more_opts
+= [self
._exes
['avconv'] and '-b:a' or '-ab', self
._preferredquality
+ 'k']
175 # We convert the audio (lossy)
176 acodec
= {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self
._preferredcodec
]
177 extension
= self
._preferredcodec
179 if self
._preferredquality
is not None:
180 if int(self
._preferredquality
) < 10:
181 more_opts
+= [self
._exes
['avconv'] and '-q:a' or '-aq', self
._preferredquality
]
183 more_opts
+= [self
._exes
['avconv'] and '-b:a' or '-ab', self
._preferredquality
+ 'k']
184 if self
._preferredcodec
== 'aac':
185 more_opts
+= ['-f', 'adts']
186 if self
._preferredcodec
== 'm4a':
187 more_opts
+= [self
._exes
['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
188 if self
._preferredcodec
== 'vorbis':
190 if self
._preferredcodec
== 'wav':
192 more_opts
+= ['-f', 'wav']
194 prefix
, sep
, ext
= path
.rpartition(u
'.') # not os.path.splitext, since the latter does not work on unicode in all setups
195 new_path
= prefix
+ sep
+ extension
197 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
199 self
._nopostoverwrites
= True
202 if self
._nopostoverwrites
and os
.path
.exists(encodeFilename(new_path
)):
203 self
._downloader
.to_screen(u
'[youtube] Post-process file %s exists, skipping' % new_path
)
205 self
._downloader
.to_screen(u
'[' + (self
._exes
['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path
)
206 self
.run_ffmpeg(path
, new_path
, acodec
, more_opts
)
208 etype
,e
,tb
= sys
.exc_info()
209 if isinstance(e
, AudioConversionError
):
210 msg
= u
'audio conversion failed: ' + e
.msg
212 msg
= u
'error running ' + (self
._exes
['avconv'] and 'avconv' or 'ffmpeg')
213 raise PostProcessingError(msg
)
215 # Try to update the date time for extracted audio file.
216 if information
.get('filetime') is not None:
218 os
.utime(encodeFilename(new_path
), (time
.time(), information
['filetime']))
220 self
._downloader
.report_warning(u
'Cannot update utime of audio file')
222 information
['filepath'] = new_path
223 return self
._nopostoverwrites
,information
225 class FFmpegVideoConvertor(FFmpegPostProcessor
):
226 def __init__(self
, downloader
=None,preferedformat
=None):
227 super(FFmpegVideoConvertor
, self
).__init
__(downloader
)
228 self
._preferedformat
=preferedformat
230 def run(self
, information
):
231 path
= information
['filepath']
232 prefix
, sep
, ext
= path
.rpartition(u
'.')
233 outpath
= prefix
+ sep
+ self
._preferedformat
234 if information
['ext'] == self
._preferedformat
:
235 self
._downloader
.to_screen(u
'[ffmpeg] Not converting video file %s - already is in target format %s' % (path
, self
._preferedformat
))
236 return True,information
237 self
._downloader
.to_screen(u
'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information
['ext'], self
._preferedformat
) +outpath
)
238 self
.run_ffmpeg(path
, outpath
, [])
239 information
['filepath'] = outpath
240 information
['format'] = self
._preferedformat
241 information
['ext'] = self
._preferedformat
242 return False,information
245 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor
):
246 # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
434 def __init__(self
, downloader
=None, subtitlesformat
='srt'):
435 super(FFmpegEmbedSubtitlePP
, self
).__init
__(downloader
)
436 self
._subformat
= subtitlesformat
439 def _conver_lang_code(cls
, code
):
440 """Convert language code from ISO 639-1 to ISO 639-2/T"""
441 return cls
._lang
_map
.get(code
[:2])
443 def run(self
, information
):
444 if information
['ext'] != u
'mp4':
445 self
._downloader
.to_screen(u
'[ffmpeg] Subtitles can only be embedded in mp4 files')
446 return True, information
447 if not information
.get('subtitles'):
448 self
._downloader
.to_screen(u
'[ffmpeg] There aren\'t any subtitles to embed')
449 return True, information
451 sub_langs
= [key
for key
in information
['subtitles']]
452 filename
= information
['filepath']
453 input_files
= [filename
] + [subtitles_filename(filename
, lang
, self
._subformat
) for lang
in sub_langs
]
455 opts
= ['-map', '0:0', '-map', '0:1', '-c:v', 'copy', '-c:a', 'copy']
456 for (i
, lang
) in enumerate(sub_langs
):
457 opts
.extend(['-map', '%d:0' % (i
+1), '-c:s:%d' % i
, 'mov_text'])
458 lang_code
= self
._conver
_lang
_code
(lang
)
459 if lang_code
is not None:
460 opts
.extend(['-metadata:s:s:%d' % i
, 'language=%s' % lang_code
])
461 opts
.extend(['-f', 'mp4'])
463 temp_filename
= filename
+ u
'.temp'
464 self
._downloader
.to_screen(u
'[ffmpeg] Embedding subtitles in \'%s\'' % filename
)
465 self
.run_ffmpeg_multiple_files(input_files
, temp_filename
, opts
)
466 os
.remove(encodeFilename(filename
))
467 os
.rename(encodeFilename(temp_filename
), encodeFilename(filename
))
469 return True, information