]> Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/PostProcessor.py
fddf58606015b92cc21a9f89818c90852c365e83
[youtubedl] / youtube_dl / PostProcessor.py
1 import os
2 import subprocess
3 import sys
4 import time
5
6 from .utils import *
7
8
9 class PostProcessor(object):
10 """Post Processor class.
11
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
17 PostProcessor.
18
19 The chain will be stopped if one of them ever returns None or the end
20 of the chain is reached.
21
22 PostProcessor objects follow a "mutual registration" process similar
23 to InfoExtractor objects.
24 """
25
26 _downloader = None
27
28 def __init__(self, downloader=None):
29 self._downloader = downloader
30
31 def set_downloader(self, downloader):
32 """Sets the downloader for this PP."""
33 self._downloader = downloader
34
35 def run(self, information):
36 """Run the PostProcessor.
37
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
41 downloaded file.
42
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.
46
47 In addition, this method may raise a PostProcessingError
48 exception if post processing fails.
49 """
50 return None, information # by default, keep file and do nothing
51
52 class FFmpegPostProcessorError(PostProcessingError):
53 pass
54
55 class AudioConversionError(PostProcessingError):
56 pass
57
58 class FFmpegPostProcessor(PostProcessor):
59 def __init__(self,downloader=None):
60 PostProcessor.__init__(self, downloader)
61 self._exes = self.detect_executables()
62
63 @staticmethod
64 def detect_executables():
65 def executable(exe):
66 try:
67 subprocess.Popen([exe, '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
68 except OSError:
69 return False
70 return exe
71 programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
72 return dict((program, executable(program)) for program in programs)
73
74 def run_ffmpeg(self, path, 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.')
77 cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path)]
78 + opts +
79 [encodeFilename(self._ffmpeg_filename_argument(out_path))])
80 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
81 stdout,stderr = p.communicate()
82 if p.returncode != 0:
83 stderr = stderr.decode('utf-8', 'replace')
84 msg = stderr.strip().split('\n')[-1]
85 raise FFmpegPostProcessorError(msg)
86
87 def _ffmpeg_filename_argument(self, fn):
88 # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
89 if fn.startswith(u'-'):
90 return u'./' + fn
91 return fn
92
93 class FFmpegExtractAudioPP(FFmpegPostProcessor):
94 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
95 FFmpegPostProcessor.__init__(self, downloader)
96 if preferredcodec is None:
97 preferredcodec = 'best'
98 self._preferredcodec = preferredcodec
99 self._preferredquality = preferredquality
100 self._nopostoverwrites = nopostoverwrites
101
102 def get_audio_codec(self, path):
103 if not self._exes['ffprobe'] and not self._exes['avprobe']:
104 raise PostProcessingError(u'ffprobe or avprobe not found. Please install one.')
105 try:
106 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
107 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
108 output = handle.communicate()[0]
109 if handle.wait() != 0:
110 return None
111 except (IOError, OSError):
112 return None
113 audio_codec = None
114 for line in output.decode('ascii', 'ignore').split('\n'):
115 if line.startswith('codec_name='):
116 audio_codec = line.split('=')[1].strip()
117 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
118 return audio_codec
119 return None
120
121 def run_ffmpeg(self, path, out_path, codec, more_opts):
122 if not self._exes['ffmpeg'] and not self._exes['avconv']:
123 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
124 if codec is None:
125 acodec_opts = []
126 else:
127 acodec_opts = ['-acodec', codec]
128 opts = ['-vn'] + acodec_opts + more_opts
129 try:
130 FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
131 except FFmpegPostProcessorError as err:
132 raise AudioConversionError(err.message)
133
134 def run(self, information):
135 path = information['filepath']
136
137 filecodec = self.get_audio_codec(path)
138 if filecodec is None:
139 raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
140
141 more_opts = []
142 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
143 if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
144 # Lossless, but in another container
145 acodec = 'copy'
146 extension = 'm4a'
147 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
148 elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
149 # Lossless if possible
150 acodec = 'copy'
151 extension = filecodec
152 if filecodec == 'aac':
153 more_opts = ['-f', 'adts']
154 if filecodec == 'vorbis':
155 extension = 'ogg'
156 else:
157 # MP3 otherwise.
158 acodec = 'libmp3lame'
159 extension = 'mp3'
160 more_opts = []
161 if self._preferredquality is not None:
162 if int(self._preferredquality) < 10:
163 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
164 else:
165 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
166 else:
167 # We convert the audio (lossy)
168 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
169 extension = self._preferredcodec
170 more_opts = []
171 if self._preferredquality is not None:
172 if int(self._preferredquality) < 10:
173 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
174 else:
175 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
176 if self._preferredcodec == 'aac':
177 more_opts += ['-f', 'adts']
178 if self._preferredcodec == 'm4a':
179 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
180 if self._preferredcodec == 'vorbis':
181 extension = 'ogg'
182 if self._preferredcodec == 'wav':
183 extension = 'wav'
184 more_opts += ['-f', 'wav']
185
186 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
187 new_path = prefix + sep + extension
188
189 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
190 if new_path == path:
191 self._nopostoverwrites = True
192
193 try:
194 if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
195 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
196 else:
197 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
198 self.run_ffmpeg(path, new_path, acodec, more_opts)
199 except:
200 etype,e,tb = sys.exc_info()
201 if isinstance(e, AudioConversionError):
202 msg = u'audio conversion failed: ' + e.message
203 else:
204 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
205 raise PostProcessingError(msg)
206
207 # Try to update the date time for extracted audio file.
208 if information.get('filetime') is not None:
209 try:
210 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
211 except:
212 self._downloader.report_warning(u'Cannot update utime of audio file')
213
214 information['filepath'] = new_path
215 return self._nopostoverwrites,information
216
217 class FFmpegVideoConvertor(FFmpegPostProcessor):
218 def __init__(self, downloader=None,preferedformat=None):
219 super(FFmpegVideoConvertor, self).__init__(downloader)
220 self._preferedformat=preferedformat
221
222 def run(self, information):
223 path = information['filepath']
224 prefix, sep, ext = path.rpartition(u'.')
225 outpath = prefix + sep + self._preferedformat
226 if information['ext'] == self._preferedformat:
227 self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
228 return True,information
229 self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
230 self.run_ffmpeg(path, outpath, [])
231 information['filepath'] = outpath
232 information['format'] = self._preferedformat
233 information['ext'] = self._preferedformat
234 return False,information