]> jfr.im git - yt-dlp.git/blame - youtube_dl/PostProcessor.py
Added "min-filesize" and "max-filesize" options
[yt-dlp.git] / youtube_dl / PostProcessor.py
CommitLineData
d77c3dfd
FV
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
9e8056d5
PH
4from __future__ import absolute_import
5
d77c3dfd
FV
6import os
7import subprocess
8import sys
9import time
10
9e8056d5 11from .utils import *
d77c3dfd
FV
12
13
14class PostProcessor(object):
59ae15a5 15 """Post Processor class.
d77c3dfd 16
59ae15a5
PH
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.
d77c3dfd 23
59ae15a5
PH
24 The chain will be stopped if one of them ever returns None or the end
25 of the chain is reached.
d77c3dfd 26
59ae15a5
PH
27 PostProcessor objects follow a "mutual registration" process similar
28 to InfoExtractor objects.
29 """
d77c3dfd 30
59ae15a5 31 _downloader = None
d77c3dfd 32
59ae15a5
PH
33 def __init__(self, downloader=None):
34 self._downloader = downloader
d77c3dfd 35
59ae15a5
PH
36 def set_downloader(self, downloader):
37 """Sets the downloader for this PP."""
38 self._downloader = downloader
d77c3dfd 39
59ae15a5
PH
40 def run(self, information):
41 """Run the PostProcessor.
d77c3dfd 42
59ae15a5
PH
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.
d77c3dfd 47
7851b379
PH
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.
d77c3dfd 51
59ae15a5 52 In addition, this method may raise a PostProcessingError
7851b379 53 exception if post processing fails.
59ae15a5 54 """
7851b379 55 return None, information # by default, keep file and do nothing
d77c3dfd 56
7851b379
PH
57class FFmpegPostProcessorError(PostProcessingError):
58 pass
67d0c25e 59
7851b379
PH
60class AudioConversionError(PostProcessingError):
61 pass
d77c3dfd 62
67d0c25e
JMF
63class FFmpegPostProcessor(PostProcessor):
64 def __init__(self,downloader=None):
59ae15a5 65 PostProcessor.__init__(self, downloader)
59ae15a5
PH
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
67d0c25e
JMF
79 def run_ffmpeg(self, path, out_path, opts):
80 if not self._exes['ffmpeg'] and not self._exes['avconv']:
7851b379 81 raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.')
67d0c25e
JMF
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]
7851b379 89 raise FFmpegPostProcessorError(msg.decode('utf-8', 'replace'))
67d0c25e
JMF
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
97class FFmpegExtractAudioPP(FFmpegPostProcessor):
7851b379 98 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
67d0c25e
JMF
99 FFmpegPostProcessor.__init__(self, downloader)
100 if preferredcodec is None:
101 preferredcodec = 'best'
102 self._preferredcodec = preferredcodec
103 self._preferredquality = preferredquality
67d0c25e
JMF
104 self._nopostoverwrites = nopostoverwrites
105
59ae15a5
PH
106 def get_audio_codec(self, path):
107 if not self._exes['ffprobe'] and not self._exes['avprobe']: return None
108 try:
712e86b9 109 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
5910e210 110 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
59ae15a5
PH
111 output = handle.communicate()[0]
112 if handle.wait() != 0:
113 return None
114 except (IOError, OSError):
115 return None
116 audio_codec = None
5910e210 117 for line in output.decode('ascii', 'ignore').split('\n'):
59ae15a5
PH
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']:
0c007432 126 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
59ae15a5
PH
127 if codec is None:
128 acodec_opts = []
129 else:
130 acodec_opts = ['-acodec', codec]
67d0c25e
JMF
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)
59ae15a5
PH
136
137 def run(self, information):
138 path = information['filepath']
139
140 filecodec = self.get_audio_codec(path)
141 if filecodec is None:
7851b379 142 raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
59ae15a5
PH
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']
510e6f6d 151 elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
59ae15a5
PH
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)
510e6f6d 171 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
59ae15a5
PH
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
59ae15a5 191 try:
b7298b6e
BPG
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)
59ae15a5
PH
197 except:
198 etype,e,tb = sys.exc_info()
199 if isinstance(e, AudioConversionError):
7851b379 200 msg = u'audio conversion failed: ' + e.message
59ae15a5 201 else:
7851b379
PH
202 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
203 raise PostProcessingError(msg)
59ae15a5
PH
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
59ae15a5 212 information['filepath'] = new_path
7851b379 213 return False,information
712e86b9 214
67d0c25e
JMF
215class FFmpegVideoConvertor(FFmpegPostProcessor):
216 def __init__(self, downloader=None,preferedformat=None):
7851b379 217 super(FFmpegVideoConvertor, self).__init__(downloader)
67d0c25e 218 self._preferedformat=preferedformat
712e86b9 219
67d0c25e
JMF
220 def run(self, information):
221 path = information['filepath']
222 prefix, sep, ext = path.rpartition(u'.')
223 outpath = prefix + sep + self._preferedformat
7851b379
PH
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)
67d0c25e
JMF
228 self.run_ffmpeg(path, outpath, [])
229 information['filepath'] = outpath
230 information['format'] = self._preferedformat
7851b379
PH
231 information['ext'] = self._preferedformat
232 return False,information