2 # -*- coding: utf-8 -*-
12 class PostProcessor(object):
13 """Post Processor class.
15 PostProcessor objects can be added to downloaders with their
16 add_post_processor() method. When the downloader has finished a
17 successful download, it will take its internal chain of PostProcessors
18 and start calling the run() method on each one of them, first with
19 an initial argument and then with the returned value of the previous
22 The chain will be stopped if one of them ever returns None or the end
23 of the chain is reached.
25 PostProcessor objects follow a "mutual registration" process similar
26 to InfoExtractor objects.
31 def __init__(self, downloader=None):
32 self._downloader = downloader
34 def set_downloader(self, downloader):
35 """Sets the downloader for this PP."""
36 self._downloader = downloader
38 def run(self, information):
39 """Run the PostProcessor.
41 The "information" argument is a dictionary like the ones
42 composed by InfoExtractors. The only difference is that this
43 one has an extra field called "filepath" that points to the
46 When this method returns None, the postprocessing chain is
47 stopped. However, this method may return an information
48 dictionary that will be passed to the next postprocessing
49 object in the chain. It can be the one it received after
52 In addition, this method may raise a PostProcessingError
53 exception that will be taken into account by the downloader
56 return information # by default, do nothing
58 class AudioConversionError(BaseException):
59 def __init__(self, message):
60 self.message = message
62 class FFmpegExtractAudioPP(PostProcessor):
64 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, keepvideo=False):
65 PostProcessor.__init__(self, downloader)
66 if preferredcodec is None:
67 preferredcodec = 'best'
68 self._preferredcodec = preferredcodec
69 self._preferredquality = preferredquality
70 self._keepvideo = keepvideo
73 def get_audio_codec(path):
75 cmd = ['ffprobe', '-show_streams', '--', encodeFilename(path)]
76 handle = subprocess.Popen(cmd, stderr=file(os.path.devnull, 'w'), stdout=subprocess.PIPE)
77 output = handle.communicate()[0]
78 if handle.wait() != 0:
80 except (IOError, OSError):
83 for line in output.split('\n'):
84 if line.startswith('codec_name='):
85 audio_codec = line.split('=')[1].strip()
86 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
91 def run_ffmpeg(path, out_path, codec, more_opts):
95 acodec_opts = ['-acodec', codec]
96 cmd = ['ffmpeg', '-y', '-i', encodeFilename(path), '-vn'] + acodec_opts + more_opts + ['--', encodeFilename(out_path)]
98 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
99 stdout,stderr = p.communicate()
100 except (IOError, OSError):
101 e = sys.exc_info()[1]
102 if isinstance(e, OSError) and e.errno == 2:
103 raise AudioConversionError('ffmpeg not found. Please install ffmpeg.')
106 if p.returncode != 0:
107 msg = stderr.strip().split('\n')[-1]
108 raise AudioConversionError(msg)
110 def run(self, information):
111 path = information['filepath']
113 filecodec = self.get_audio_codec(path)
114 if filecodec is None:
115 self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe')
119 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
120 if self._preferredcodec == 'm4a' and filecodec == 'aac':
121 # Lossless, but in another container
123 extension = self._preferredcodec
124 more_opts = ['-absf', 'aac_adtstoasc']
125 elif filecodec in ['aac', 'mp3', 'vorbis']:
126 # Lossless if possible
128 extension = filecodec
129 if filecodec == 'aac':
130 more_opts = ['-f', 'adts']
131 if filecodec == 'vorbis':
135 acodec = 'libmp3lame'
138 if self._preferredquality is not None:
139 more_opts += ['-ab', self._preferredquality]
141 # We convert the audio (lossy)
142 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
143 extension = self._preferredcodec
145 if self._preferredquality is not None:
146 more_opts += ['-ab', self._preferredquality]
147 if self._preferredcodec == 'aac':
148 more_opts += ['-f', 'adts']
149 if self._preferredcodec == 'm4a':
150 more_opts += ['-absf', 'aac_adtstoasc']
151 if self._preferredcodec == 'vorbis':
153 if self._preferredcodec == 'wav':
155 more_opts += ['-f', 'wav']
157 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
158 new_path = prefix + sep + extension
159 self._downloader.to_screen(u'[ffmpeg] Destination: ' + new_path)
161 self.run_ffmpeg(path, new_path, acodec, more_opts)
163 etype,e,tb = sys.exc_info()
164 if isinstance(e, AudioConversionError):
165 self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message)
167 self._downloader.to_stderr(u'ERROR: error running ffmpeg')
170 # Try to update the date time for extracted audio file.
171 if information.get('filetime') is not None:
173 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
175 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
177 if not self._keepvideo:
179 os.remove(encodeFilename(path))
180 except (IOError, OSError):
181 self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file')
184 information['filepath'] = new_path