apparently the -n option is available only in ffmpeg
[youtube-dl] / youtube_dl / PostProcessor.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from __future__ import absolute_import
5
6 import os
7 import subprocess
8 import sys
9 import time
10
11 from .utils import *
12
13
14 class PostProcessor(object):
15     """Post Processor class.
16
17     PostProcessor objects can be added to downloaders with their
18     add_post_processor() method. When the downloader has finished a
19     successful download, it will take its internal chain of PostProcessors
20     and start calling the run() method on each one of them, first with
21     an initial argument and then with the returned value of the previous
22     PostProcessor.
23
24     The chain will be stopped if one of them ever returns None or the end
25     of the chain is reached.
26
27     PostProcessor objects follow a "mutual registration" process similar
28     to InfoExtractor objects.
29     """
30
31     _downloader = None
32
33     def __init__(self, downloader=None):
34         self._downloader = downloader
35
36     def set_downloader(self, downloader):
37         """Sets the downloader for this PP."""
38         self._downloader = downloader
39
40     def run(self, information):
41         """Run the PostProcessor.
42
43         The "information" argument is a dictionary like the ones
44         composed by InfoExtractors. The only difference is that this
45         one has an extra field called "filepath" that points to the
46         downloaded file.
47
48         When this method returns None, the postprocessing chain is
49         stopped. However, this method may return an information
50         dictionary that will be passed to the next postprocessing
51         object in the chain. It can be the one it received after
52         changing some fields.
53
54         In addition, this method may raise a PostProcessingError
55         exception that will be taken into account by the downloader
56         it was called from.
57         """
58         return information # by default, do nothing
59
60 class AudioConversionError(BaseException):
61     def __init__(self, message):
62         self.message = message
63
64 class FFmpegExtractAudioPP(PostProcessor):
65     def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, keepvideo=False, nopostoverwrites=False):
66         PostProcessor.__init__(self, downloader)
67         if preferredcodec is None:
68             preferredcodec = 'best'
69         self._preferredcodec = preferredcodec
70         self._preferredquality = preferredquality
71         self._keepvideo = keepvideo
72         self._nopostoverwrites = nopostoverwrites
73         self._exes = self.detect_executables()
74
75     @staticmethod
76     def detect_executables():
77         def executable(exe):
78             try:
79                 subprocess.Popen([exe, '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
80             except OSError:
81                 return False
82             return exe
83         programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
84         return dict((program, executable(program)) for program in programs)
85
86     def get_audio_codec(self, path):
87         if not self._exes['ffprobe'] and not self._exes['avprobe']: return None
88         try:
89             cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', '--', encodeFilename(path)]
90             handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
91             output = handle.communicate()[0]
92             if handle.wait() != 0:
93                 return None
94         except (IOError, OSError):
95             return None
96         audio_codec = None
97         for line in output.decode('ascii', 'ignore').split('\n'):
98             if line.startswith('codec_name='):
99                 audio_codec = line.split('=')[1].strip()
100             elif line.strip() == 'codec_type=audio' and audio_codec is not None:
101                 return audio_codec
102         return None
103
104     def run_ffmpeg(self, path, out_path, codec, more_opts):
105         if not self._exes['ffmpeg'] and not self._exes['avconv']:
106             raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
107         if codec is None:
108             acodec_opts = []
109         else:
110             acodec_opts = ['-acodec', codec]
111         if self._nopostoverwrites and self._exes['ffmpeg']:
112             overwrite_opts = '-n'
113         else:
114             overwrite_opts = '-y'
115         cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], overwrite_opts, '-i', encodeFilename(path), '-vn']
116                + acodec_opts + more_opts +
117                ['--', encodeFilename(out_path)])
118         p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
119         stdout,stderr = p.communicate()
120         if p.returncode != 0:
121             msg = stderr.strip().split('\n')[-1]
122             raise AudioConversionError(msg)
123
124     def run(self, information):
125         path = information['filepath']
126
127         filecodec = self.get_audio_codec(path)
128         if filecodec is None:
129             self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe')
130             return None
131
132         more_opts = []
133         if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
134             if self._preferredcodec == 'm4a' and filecodec == 'aac':
135                 # Lossless, but in another container
136                 acodec = 'copy'
137                 extension = self._preferredcodec
138                 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
139             elif filecodec in ['aac', 'mp3', 'vorbis']:
140                 # Lossless if possible
141                 acodec = 'copy'
142                 extension = filecodec
143                 if filecodec == 'aac':
144                     more_opts = ['-f', 'adts']
145                 if filecodec == 'vorbis':
146                     extension = 'ogg'
147             else:
148                 # MP3 otherwise.
149                 acodec = 'libmp3lame'
150                 extension = 'mp3'
151                 more_opts = []
152                 if self._preferredquality is not None:
153                     if int(self._preferredquality) < 10:
154                         more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
155                     else:
156                         more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
157         else:
158             # We convert the audio (lossy)
159             acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
160             extension = self._preferredcodec
161             more_opts = []
162             if self._preferredquality is not None:
163                 if int(self._preferredquality) < 10:
164                     more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
165                 else:
166                     more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
167             if self._preferredcodec == 'aac':
168                 more_opts += ['-f', 'adts']
169             if self._preferredcodec == 'm4a':
170                 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
171             if self._preferredcodec == 'vorbis':
172                 extension = 'ogg'
173             if self._preferredcodec == 'wav':
174                 extension = 'wav'
175                 more_opts += ['-f', 'wav']
176
177         prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
178         new_path = prefix + sep + extension
179         self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
180         try:
181             self.run_ffmpeg(path, new_path, acodec, more_opts)
182         except:
183             etype,e,tb = sys.exc_info()
184             if isinstance(e, AudioConversionError):
185                 self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message)
186             else:
187                 self._downloader.to_stderr(u'ERROR: error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg'))
188             return None
189
190         # Try to update the date time for extracted audio file.
191         if information.get('filetime') is not None:
192             try:
193                 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
194             except:
195                 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
196
197         if not self._keepvideo:
198             try:
199                 os.remove(encodeFilename(path))
200             except (IOError, OSError):
201                 self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file')
202                 return None
203
204         information['filepath'] = new_path
205         return information