Merge pull request #432 from cryzed/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                 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                                         if int(self._preferredquality) < 10:
146                                                 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
147                                         else:
148                                                 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality]
149                 else:
150                         # We convert the audio (lossy)
151                         acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
152                         extension = self._preferredcodec
153                         more_opts = []
154                         if self._preferredquality is not None:
155                                 if int(self._preferredquality) < 10:
156                                         more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
157                                 else:
158                                         more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality]
159                         if self._preferredcodec == 'aac':
160                                 more_opts += ['-f', 'adts']
161                         if self._preferredcodec == 'm4a':
162                                 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
163                         if self._preferredcodec == 'vorbis':
164                                 extension = 'ogg'
165                         if self._preferredcodec == 'wav':
166                                 extension = 'wav'
167                                 more_opts += ['-f', 'wav']
168
169                 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
170                 new_path = prefix + sep + extension
171                 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
172                 try:
173                         self.run_ffmpeg(path, new_path, acodec, more_opts)
174                 except:
175                         etype,e,tb = sys.exc_info()
176                         if isinstance(e, AudioConversionError):
177                                 self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message)
178                         else:
179                                 self._downloader.to_stderr(u'ERROR: error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg'))
180                         return None
181
182                 # Try to update the date time for extracted audio file.
183                 if information.get('filetime') is not None:
184                         try:
185                                 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
186                         except:
187                                 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
188
189                 if not self._keepvideo:
190                         try:
191                                 os.remove(encodeFilename(path))
192                         except (IOError, OSError):
193                                 self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file')
194                                 return None
195
196                 information['filepath'] = new_path
197                 return information