]> jfr.im git - yt-dlp.git/blame - youtube_dl/PostProcessor.py
Convert all tabs to 4 spaces (PEP8)
[yt-dlp.git] / youtube_dl / PostProcessor.py
CommitLineData
d77c3dfd
FV
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4import os
5import subprocess
6import sys
7import time
8
d11d05d0 9from utils import *
d77c3dfd
FV
10
11
12class 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
58class AudioConversionError(BaseException):
59ae15a5
PH
59 def __init__(self, message):
60 self.message = message
d77c3dfd
FV
61
62class 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