Merge remote-tracking branch 'Asido/master'
[youtube-dl] / 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                 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