better naming for the sub-modules
[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
64         def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, keepvideo=False):
65                 PostProcessor.__init__(self, downloader)
66                 if preferredcodec is None:
67                         preferredcodec = 'best'
68                 self._preferredcodec = preferredcodec
69                 self._preferredquality = preferredquality
70                 self._keepvideo = keepvideo
71
72         @staticmethod
73         def get_audio_codec(path):
74                 try:
75                         cmd = ['ffprobe', '-show_streams', '--', encodeFilename(path)]
76                         handle = subprocess.Popen(cmd, stderr=file(os.path.devnull, 'w'), stdout=subprocess.PIPE)
77                         output = handle.communicate()[0]
78                         if handle.wait() != 0:
79                                 return None
80                 except (IOError, OSError):
81                         return None
82                 audio_codec = None
83                 for line in output.split('\n'):
84                         if line.startswith('codec_name='):
85                                 audio_codec = line.split('=')[1].strip()
86                         elif line.strip() == 'codec_type=audio' and audio_codec is not None:
87                                 return audio_codec
88                 return None
89
90         @staticmethod
91         def run_ffmpeg(path, out_path, codec, more_opts):
92                 if codec is None:
93                         acodec_opts = []
94                 else:
95                         acodec_opts = ['-acodec', codec]
96                 cmd = ['ffmpeg', '-y', '-i', encodeFilename(path), '-vn'] + acodec_opts + more_opts + ['--', encodeFilename(out_path)]
97                 try:
98                         p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
99                         stdout,stderr = p.communicate()
100                 except (IOError, OSError):
101                         e = sys.exc_info()[1]
102                         if isinstance(e, OSError) and e.errno == 2:
103                                 raise AudioConversionError('ffmpeg not found. Please install ffmpeg.')
104                         else:
105                                 raise e
106                 if p.returncode != 0:
107                         msg = stderr.strip().split('\n')[-1]
108                         raise AudioConversionError(msg)
109
110         def run(self, information):
111                 path = information['filepath']
112
113                 filecodec = self.get_audio_codec(path)
114                 if filecodec is None:
115                         self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe')
116                         return None
117
118                 more_opts = []
119                 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
120                         if self._preferredcodec == 'm4a' and filecodec == 'aac':
121                                 # Lossless, but in another container
122                                 acodec = 'copy'
123                                 extension = self._preferredcodec
124                                 more_opts = ['-absf', 'aac_adtstoasc']
125                         elif filecodec in ['aac', 'mp3', 'vorbis']:
126                                 # Lossless if possible
127                                 acodec = 'copy'
128                                 extension = filecodec
129                                 if filecodec == 'aac':
130                                         more_opts = ['-f', 'adts']
131                                 if filecodec == 'vorbis':
132                                         extension = 'ogg'
133                         else:
134                                 # MP3 otherwise.
135                                 acodec = 'libmp3lame'
136                                 extension = 'mp3'
137                                 more_opts = []
138                                 if self._preferredquality is not None:
139                                         more_opts += ['-ab', self._preferredquality]
140                 else:
141                         # We convert the audio (lossy)
142                         acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
143                         extension = self._preferredcodec
144                         more_opts = []
145                         if self._preferredquality is not None:
146                                 more_opts += ['-ab', self._preferredquality]
147                         if self._preferredcodec == 'aac':
148                                 more_opts += ['-f', 'adts']
149                         if self._preferredcodec == 'm4a':
150                                 more_opts += ['-absf', 'aac_adtstoasc']
151                         if self._preferredcodec == 'vorbis':
152                                 extension = 'ogg'
153                         if self._preferredcodec == 'wav':
154                                 extension = 'wav'
155                                 more_opts += ['-f', 'wav']
156
157                 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
158                 new_path = prefix + sep + extension
159                 self._downloader.to_screen(u'[ffmpeg] Destination: ' + new_path)
160                 try:
161                         self.run_ffmpeg(path, new_path, acodec, more_opts)
162                 except:
163                         etype,e,tb = sys.exc_info()
164                         if isinstance(e, AudioConversionError):
165                                 self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message)
166                         else:
167                                 self._downloader.to_stderr(u'ERROR: error running ffmpeg')
168                         return None
169
170                 # Try to update the date time for extracted audio file.
171                 if information.get('filetime') is not None:
172                         try:
173                                 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
174                         except:
175                                 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
176
177                 if not self._keepvideo:
178                         try:
179                                 os.remove(encodeFilename(path))
180                         except (IOError, OSError):
181                                 self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file')
182                                 return None
183
184                 information['filepath'] = new_path
185                 return information