]> Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/PostProcessor.py
debian/control: Allow use of libav-tools for transcoding instead of ffmpeg.
[youtubedl] / youtube_dl / PostProcessor.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import os
5 import subprocess
6 import sys
7 import time
8
9 from utils import *
10
11
12 class PostProcessor(object):
13 """Post Processor class.
14
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.
21
22 The chain will be stopped if one of them ever returns None or the end
23 of the chain is reached.
24
25 PostProcessor objects follow a "mutual registration" process similar
26 to InfoExtractor objects.
27 """
28
29 _downloader = None
30
31 def __init__(self, downloader=None):
32 self._downloader = downloader
33
34 def set_downloader(self, downloader):
35 """Sets the downloader for this PP."""
36 self._downloader = downloader
37
38 def run(self, information):
39 """Run the PostProcessor.
40
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.
45
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.
51
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
57
58 class AudioConversionError(BaseException):
59 def __init__(self, message):
60 self.message = message
61
62 class FFmpegExtractAudioPP(PostProcessor):
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 available = {'avprobe' : False, 'avconv' : False, 'ffmpeg' : False, 'ffprobe' : False}
75 for path in os.environ["PATH"].split(os.pathsep):
76 for program in available.keys():
77 exe_file = os.path.join(path, program)
78 if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
79 available[program] = exe_file
80 return available
81
82 def get_audio_codec(self, path):
83 if not self._exes['ffprobe'] and not self._exes['avprobe']: return None
84 try:
85 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', '--', encodeFilename(path)]
86 handle = subprocess.Popen(cmd, stderr=file(os.path.devnull, 'w'), stdout=subprocess.PIPE)
87 output = handle.communicate()[0]
88 if handle.wait() != 0:
89 return None
90 except (IOError, OSError):
91 return None
92 audio_codec = None
93 for line in output.split('\n'):
94 if line.startswith('codec_name='):
95 audio_codec = line.split('=')[1].strip()
96 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
97 return audio_codec
98 return None
99
100 def run_ffmpeg(self, path, out_path, codec, more_opts):
101 if not self._exes['ffmpeg'] and not self._exes['avconv']:
102 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
103 if codec is None:
104 acodec_opts = []
105 else:
106 acodec_opts = ['-acodec', codec]
107 cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path), '-vn']
108 + acodec_opts + more_opts +
109 ['--', encodeFilename(out_path)])
110 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
111 stdout,stderr = p.communicate()
112 if p.returncode != 0:
113 msg = stderr.strip().split('\n')[-1]
114 raise AudioConversionError(msg)
115
116 def run(self, information):
117 path = information['filepath']
118
119 filecodec = self.get_audio_codec(path)
120 if filecodec is None:
121 self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe')
122 return None
123
124 more_opts = []
125 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
126 if self._preferredcodec == 'm4a' and filecodec == 'aac':
127 # Lossless, but in another container
128 acodec = 'copy'
129 extension = self._preferredcodec
130 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
131 elif filecodec in ['aac', 'mp3', 'vorbis']:
132 # Lossless if possible
133 acodec = 'copy'
134 extension = filecodec
135 if filecodec == 'aac':
136 more_opts = ['-f', 'adts']
137 if filecodec == 'vorbis':
138 extension = 'ogg'
139 else:
140 # MP3 otherwise.
141 acodec = 'libmp3lame'
142 extension = 'mp3'
143 more_opts = []
144 if self._preferredquality is not None:
145 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality]
146 else:
147 # We convert the audio (lossy)
148 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
149 extension = self._preferredcodec
150 more_opts = []
151 if self._preferredquality is not None:
152 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality]
153 if self._preferredcodec == 'aac':
154 more_opts += ['-f', 'adts']
155 if self._preferredcodec == 'm4a':
156 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
157 if self._preferredcodec == 'vorbis':
158 extension = 'ogg'
159 if self._preferredcodec == 'wav':
160 extension = 'wav'
161 more_opts += ['-f', 'wav']
162
163 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
164 new_path = prefix + sep + extension
165 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
166 try:
167 self.run_ffmpeg(path, new_path, acodec, more_opts)
168 except:
169 etype,e,tb = sys.exc_info()
170 if isinstance(e, AudioConversionError):
171 self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message)
172 else:
173 self._downloader.to_stderr(u'ERROR: error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg'))
174 return None
175
176 # Try to update the date time for extracted audio file.
177 if information.get('filetime') is not None:
178 try:
179 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
180 except:
181 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
182
183 if not self._keepvideo:
184 try:
185 os.remove(encodeFilename(path))
186 except (IOError, OSError):
187 self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file')
188 return None
189
190 information['filepath'] = new_path
191 return information