]>
Commit | Line | Data |
---|---|---|
d77c3dfd FV |
1 | #!/usr/bin/env python |
2 | # -*- coding: utf-8 -*- | |
3 | ||
4 | import os | |
5 | import subprocess | |
6 | import sys | |
7 | import time | |
8 | ||
d11d05d0 | 9 | from utils import * |
d77c3dfd FV |
10 | |
11 | ||
12 | class PostProcessor(object): | |
59ae15a5 | 13 | """Post Processor class. |
d77c3dfd | 14 | |
59ae15a5 PH |
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 | |
20 | PostProcessor. | |
d77c3dfd | 21 | |
59ae15a5 PH |
22 | The chain will be stopped if one of them ever returns None or the end |
23 | of the chain is reached. | |
d77c3dfd | 24 | |
59ae15a5 PH |
25 | PostProcessor objects follow a "mutual registration" process similar |
26 | to InfoExtractor objects. | |
27 | """ | |
d77c3dfd | 28 | |
59ae15a5 | 29 | _downloader = None |
d77c3dfd | 30 | |
59ae15a5 PH |
31 | def __init__(self, downloader=None): |
32 | self._downloader = downloader | |
d77c3dfd | 33 | |
59ae15a5 PH |
34 | def set_downloader(self, downloader): |
35 | """Sets the downloader for this PP.""" | |
36 | self._downloader = downloader | |
d77c3dfd | 37 | |
59ae15a5 PH |
38 | def run(self, information): |
39 | """Run the PostProcessor. | |
d77c3dfd | 40 | |
59ae15a5 PH |
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 | |
44 | downloaded file. | |
d77c3dfd | 45 | |
59ae15a5 PH |
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 | |
50 | changing some fields. | |
d77c3dfd | 51 | |
59ae15a5 PH |
52 | In addition, this method may raise a PostProcessingError |
53 | exception that will be taken into account by the downloader | |
54 | it was called from. | |
55 | """ | |
56 | return information # by default, do nothing | |
d77c3dfd FV |
57 | |
58 | class AudioConversionError(BaseException): | |
59ae15a5 PH |
59 | def __init__(self, message): |
60 | self.message = message | |
d77c3dfd FV |
61 | |
62 | class FFmpegExtractAudioPP(PostProcessor): | |
59ae15a5 PH |
63 | def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, keepvideo=False): |
64 | PostProcessor.__init__(self, downloader) | |
65 | if preferredcodec is None: | |
66 | preferredcodec = 'best' | |
67 | self._preferredcodec = preferredcodec | |
68 | self._preferredquality = preferredquality | |
69 | self._keepvideo = keepvideo | |
70 | self._exes = self.detect_executables() | |
71 | ||
72 | @staticmethod | |
73 | def detect_executables(): | |
74 | def executable(exe): | |
75 | try: | |
76 | subprocess.Popen([exe, '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() | |
77 | except OSError: | |
78 | return False | |
79 | return exe | |
80 | programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe'] | |
81 | return dict((program, executable(program)) for program in programs) | |
82 | ||
83 | def get_audio_codec(self, path): | |
84 | if not self._exes['ffprobe'] and not self._exes['avprobe']: return None | |
85 | try: | |
86 | cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', '--', encodeFilename(path)] | |
87 | handle = subprocess.Popen(cmd, stderr=file(os.path.devnull, 'w'), stdout=subprocess.PIPE) | |
88 | output = handle.communicate()[0] | |
89 | if handle.wait() != 0: | |
90 | return None | |
91 | except (IOError, OSError): | |
92 | return None | |
93 | audio_codec = None | |
94 | for line in output.split('\n'): | |
95 | if line.startswith('codec_name='): | |
96 | audio_codec = line.split('=')[1].strip() | |
97 | elif line.strip() == 'codec_type=audio' and audio_codec is not None: | |
98 | return audio_codec | |
99 | return None | |
100 | ||
101 | def run_ffmpeg(self, path, out_path, codec, more_opts): | |
102 | if not self._exes['ffmpeg'] and not self._exes['avconv']: | |
103 | raise AudioConversionError('ffmpeg or avconv not found. Please install one.') | |
104 | if codec is None: | |
105 | acodec_opts = [] | |
106 | else: | |
107 | acodec_opts = ['-acodec', codec] | |
108 | cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path), '-vn'] | |
109 | + acodec_opts + more_opts + | |
110 | ['--', encodeFilename(out_path)]) | |
111 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
112 | stdout,stderr = p.communicate() | |
113 | if p.returncode != 0: | |
114 | msg = stderr.strip().split('\n')[-1] | |
115 | raise AudioConversionError(msg) | |
116 | ||
117 | def run(self, information): | |
118 | path = information['filepath'] | |
119 | ||
120 | filecodec = self.get_audio_codec(path) | |
121 | if filecodec is None: | |
122 | self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe') | |
123 | return None | |
124 | ||
125 | more_opts = [] | |
126 | if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'): | |
127 | if self._preferredcodec == 'm4a' and filecodec == 'aac': | |
128 | # Lossless, but in another container | |
129 | acodec = 'copy' | |
130 | extension = self._preferredcodec | |
131 | more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc'] | |
132 | elif filecodec in ['aac', 'mp3', 'vorbis']: | |
133 | # Lossless if possible | |
134 | acodec = 'copy' | |
135 | extension = filecodec | |
136 | if filecodec == 'aac': | |
137 | more_opts = ['-f', 'adts'] | |
138 | if filecodec == 'vorbis': | |
139 | extension = 'ogg' | |
140 | else: | |
141 | # MP3 otherwise. | |
142 | acodec = 'libmp3lame' | |
143 | extension = 'mp3' | |
144 | more_opts = [] | |
145 | if self._preferredquality is not None: | |
146 | if int(self._preferredquality) < 10: | |
147 | more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality] | |
148 | else: | |
149 | more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k'] | |
150 | else: | |
151 | # We convert the audio (lossy) | |
152 | acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec] | |
153 | extension = self._preferredcodec | |
154 | more_opts = [] | |
155 | if self._preferredquality is not None: | |
156 | if int(self._preferredquality) < 10: | |
157 | more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality] | |
158 | else: | |
159 | more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k'] | |
160 | if self._preferredcodec == 'aac': | |
161 | more_opts += ['-f', 'adts'] | |
162 | if self._preferredcodec == 'm4a': | |
163 | more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc'] | |
164 | if self._preferredcodec == 'vorbis': | |
165 | extension = 'ogg' | |
166 | if self._preferredcodec == 'wav': | |
167 | extension = 'wav' | |
168 | more_opts += ['-f', 'wav'] | |
169 | ||
170 | prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups | |
171 | new_path = prefix + sep + extension | |
172 | self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path) | |
173 | try: | |
174 | self.run_ffmpeg(path, new_path, acodec, more_opts) | |
175 | except: | |
176 | etype,e,tb = sys.exc_info() | |
177 | if isinstance(e, AudioConversionError): | |
178 | self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message) | |
179 | else: | |
180 | self._downloader.to_stderr(u'ERROR: error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')) | |
181 | return None | |
182 | ||
183 | # Try to update the date time for extracted audio file. | |
184 | if information.get('filetime') is not None: | |
185 | try: | |
186 | os.utime(encodeFilename(new_path), (time.time(), information['filetime'])) | |
187 | except: | |
188 | self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file') | |
189 | ||
190 | if not self._keepvideo: | |
191 | try: | |
192 | os.remove(encodeFilename(path)) | |
193 | except (IOError, OSError): | |
194 | self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file') | |
195 | return None | |
196 | ||
197 | information['filepath'] = new_path | |
198 | return information |