]> jfr.im git - yt-dlp.git/blob - youtube_dl/PostProcessor.py
--recode-video option (Closes #18)
[yt-dlp.git] / 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 msg = stderr.strip().split('\n')[-1]
89 raise FFmpegPostProcessorError(msg.decode('utf-8', 'replace'))
90
91 def _ffmpeg_filename_argument(self, fn):
92 # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
93 if fn.startswith(u'-'):
94 return u'./' + fn
95 return fn
96
97 class FFmpegExtractAudioPP(FFmpegPostProcessor):
98 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
99 FFmpegPostProcessor.__init__(self, downloader)
100 if preferredcodec is None:
101 preferredcodec = 'best'
102 self._preferredcodec = preferredcodec
103 self._preferredquality = preferredquality
104 self._nopostoverwrites = nopostoverwrites
105
106 def get_audio_codec(self, path):
107 if not self._exes['ffprobe'] and not self._exes['avprobe']: return None
108 try:
109 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
110 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
111 output = handle.communicate()[0]
112 if handle.wait() != 0:
113 return None
114 except (IOError, OSError):
115 return None
116 audio_codec = None
117 for line in output.decode('ascii', 'ignore').split('\n'):
118 if line.startswith('codec_name='):
119 audio_codec = line.split('=')[1].strip()
120 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
121 return audio_codec
122 return None
123
124 def run_ffmpeg(self, path, out_path, codec, more_opts):
125 if not self._exes['ffmpeg'] and not self._exes['avconv']:
126 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
127 if codec is None:
128 acodec_opts = []
129 else:
130 acodec_opts = ['-acodec', codec]
131 opts = ['-vn'] + acodec_opts + more_opts
132 try:
133 FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
134 except FFmpegPostProcessorError as err:
135 raise AudioConversionError(err.message)
136
137 def run(self, information):
138 path = information['filepath']
139
140 filecodec = self.get_audio_codec(path)
141 if filecodec is None:
142 raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
143
144 more_opts = []
145 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
146 if self._preferredcodec == 'm4a' and filecodec == 'aac':
147 # Lossless, but in another container
148 acodec = 'copy'
149 extension = self._preferredcodec
150 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
151 elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
152 # Lossless if possible
153 acodec = 'copy'
154 extension = filecodec
155 if filecodec == 'aac':
156 more_opts = ['-f', 'adts']
157 if filecodec == 'vorbis':
158 extension = 'ogg'
159 else:
160 # MP3 otherwise.
161 acodec = 'libmp3lame'
162 extension = 'mp3'
163 more_opts = []
164 if self._preferredquality is not None:
165 if int(self._preferredquality) < 10:
166 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
167 else:
168 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
169 else:
170 # We convert the audio (lossy)
171 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
172 extension = self._preferredcodec
173 more_opts = []
174 if self._preferredquality is not None:
175 if int(self._preferredquality) < 10:
176 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
177 else:
178 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
179 if self._preferredcodec == 'aac':
180 more_opts += ['-f', 'adts']
181 if self._preferredcodec == 'm4a':
182 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
183 if self._preferredcodec == 'vorbis':
184 extension = 'ogg'
185 if self._preferredcodec == 'wav':
186 extension = 'wav'
187 more_opts += ['-f', 'wav']
188
189 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
190 new_path = prefix + sep + extension
191 try:
192 if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
193 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
194 else:
195 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
196 self.run_ffmpeg(path, new_path, acodec, more_opts)
197 except:
198 etype,e,tb = sys.exc_info()
199 if isinstance(e, AudioConversionError):
200 msg = u'audio conversion failed: ' + e.message
201 else:
202 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
203 raise PostProcessingError(msg)
204
205 # Try to update the date time for extracted audio file.
206 if information.get('filetime') is not None:
207 try:
208 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
209 except:
210 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
211
212 information['filepath'] = new_path
213 return False,information
214
215 class FFmpegVideoConvertor(FFmpegPostProcessor):
216 def __init__(self, downloader=None,preferedformat=None):
217 super(FFmpegVideoConvertor, self).__init__(downloader)
218 self._preferedformat=preferedformat
219
220 def run(self, information):
221 path = information['filepath']
222 prefix, sep, ext = path.rpartition(u'.')
223 outpath = prefix + sep + self._preferedformat
224 if information['ext'] == self._preferedformat:
225 self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
226 return True,information
227 self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
228 self.run_ffmpeg(path, outpath, [])
229 information['filepath'] = outpath
230 information['format'] = self._preferedformat
231 information['ext'] = self._preferedformat
232 return False,information