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