Convert all tabs to 4 spaces (PEP8)
[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