PageRenderTime 56ms CodeModel.GetById 2ms app.highlight 35ms RepoModel.GetById 14ms app.codeStats 0ms

/jws.py

https://bitbucket.org/opensourcecoders/jws
Python | 463 lines | 423 code | 16 blank | 24 comment | 4 complexity | a183385eb7818d292f80a02d3f2c245a MD5 | raw file
  1#!/usr/bin/env python
  2# -*- coding: utf-8 -*-
  3#    Just Wanna Say - Say what you types using google translate engine
  4#    Copyright (C) 2011 Thomaz de Oliveira dos Reis
  5#    Copyright (C) 2011 Dirley Rodrigues
  6#
  7#    This program is free software: you can redistribute it and/or modify
  8#    it under the terms of the GNU General Public License as published by
  9#    the Free Software Foundation, either version 2 of the License, or
 10#    (at your option) any later version.
 11#
 12#    This program is distributed in the hope that it will be useful,
 13#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 14#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15#    GNU General Public License for more details.
 16#
 17#    You should have received a copy of the GNU General Public License
 18#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 19
 20import ctypes
 21import hashlib
 22import optparse
 23import os
 24import sys
 25import tempfile
 26import time
 27import urllib
 28import urllib2
 29
 30
 31class Loader(object):
 32    def load(self, text, language):
 33        raise NotImplementedError
 34
 35
 36class Storage(object):
 37    def store(self, identifier, fp):
 38        raise NotImplementedError
 39
 40    def retrieve(self, identifier):
 41        """ Should raise ``Exception`` if not found.
 42        TODO: specialized exception """
 43        raise NotImplementedError
 44
 45    def release(self, identifier):
 46        """ Release a identified file if it was stored here.
 47        TODO: specialized exception """
 48        raise NotImplementedError
 49
 50
 51class Backend(object):
 52    named_file_required = None
 53
 54    def __init__(self, *args):
 55        pass
 56
 57    def play(self, fp):
 58        raise NotImplementedError
 59   
 60    @staticmethod
 61    def available():
 62        """ Subclasses must extend this method to tell the caller if they are
 63        available for use or not. """
 64        return True
 65
 66    @classmethod
 67    def name(cls):
 68        return cls.__name__[:-len('_backend')]
 69
 70    @classmethod
 71    def info(cls):
 72        return cls.__doc__.strip()
 73
 74    @classmethod
 75    def availability_info(cls):
 76        doc = cls.available.__doc__
 77        if not doc is Backend.available.__doc__:
 78            return doc.strip()
 79
 80class DefaultLoader(Loader):
 81    def load(self, text, language):
 82        """ ``text`` must be unicode. ``language`` doesn't. """
 83        data = {
 84            'tl': language,
 85            'q': text.encode(sys.stdin.encoding or 'utf-8'),
 86        }
 87
 88        url = 'http://translate.google.com/translate_tts?' + urllib.urlencode(data)
 89        request = urllib2.Request(url)
 90        request.add_header('User-Agent', 'Mozilla/5.0')
 91
 92        webfile = urllib2.urlopen(request)
 93        return webfile
 94
 95
 96class TempfileStorage(Storage):
 97    fmap = {}
 98
 99    def store(self, identifier, fp):
100        tf = open(tempfile.mktemp(suffix='.mp3'), 'wb')
101        tf.write(fp.read())
102        tf.close()
103
104        self.fmap[identifier] = tf.name
105
106        return self.retrieve(identifier)
107
108    def retrieve(self, identifier):
109        fp = None
110        fname = self.fmap.get(identifier)
111        if fname is not None:
112            try:
113                fp = open(fname, 'rb')
114            except OSError:
115                pass
116        if fp is None:
117            raise Exception('Not found')
118        return fp
119
120    def release(self, identifier):
121        fname = self.fmap.get(identifier)
122        if fname is not None:
123            os.unlink(fname)
124
125class appkit_backend(Backend):
126    """ An Apple's AppKit powered backend. """
127    named_file_required = True
128
129    def play(self, fp):
130        from AppKit import NSSound
131        sound = NSSound.alloc()
132        sound.initWithContentsOfFile_byReference_(fp.name, True)
133        sound.play()
134        while sound.isPlaying():
135          time.sleep(1)
136
137    @classmethod
138    def available(cls):
139        """ Requires Apple AppKit available on MacOS X. """
140        if not hasattr(cls,"_available"):
141            try:
142                import AppKit
143            except ImportError:
144                cls._available = False
145            else:
146                cls._available = True 
147
148        return cls._available
149
150class stdout_backend(Backend):
151    """ A backend that prints the output to stdout. """
152    def play(self, fp):
153        print fp.read()
154
155
156class external_backend(Backend):
157    """ A backend that uses a external program to play the audio. """
158    named_file_required = True
159
160    def __init__(self, command=""):
161        if not command:
162           command = self.autodetect_external_program()
163        self.command = command
164    
165    @staticmethod
166    def autodetect_external_program():
167        external_programs = (
168            ('mpg123', 'mplayer %s >/dev/null 2>&1'),
169            ('playsound', 'playsound %s >/dev/null 2>&1'),
170            ('mplayer', 'mplayer %s >/dev/null 2>&1'),
171        )
172        def is_exe(fpath):
173            return os.path.exists(fpath) and os.access(fpath, os.X_OK)
174
175        for program, command in external_programs:
176            for path in os.environ['PATH'].split(os.pathsep):
177                if is_exe(os.path.join(path, program)):
178                    return command
179
180    def play(self, fp):
181        command = self.command
182        if not '%s' in self.command:
183            command = self.command + ' %s'
184        os.system(command % (fp.name,))
185
186
187class defaultapp_backend(external_backend):
188    """ Try to use your default application as backend. """
189    def __init__(self, *args):
190        cmd = {'darwin': 'open %s',
191               'win32': 'cmd /c "start %s"',
192               'linux2': 'xdg-open %s'}.get(sys.platform)
193        super(defaultapp_backend, self).__init__(cmd)
194
195
196class pyaudio_backend(Backend):
197    """ A PortAudio and PyMAD powered backend. """
198
199    def play(self, fp):
200        import mad, pyaudio
201
202        mf = mad.MadFile(fp)
203
204        # open stream
205        p = pyaudio.PyAudio()
206        stream = p.open(format=p.get_format_from_width(pyaudio.paInt32),
207                channels=2, rate=mf.samplerate(), output=True)
208
209        data = mf.read()
210        while data != None:
211            stream.write(data)
212            data = mf.read()
213
214        stream.close()
215        p.terminate()
216
217    @classmethod
218    def available(cls):
219        """ Requires PyMad (http://spacepants.org/src/pymad/) and PyAudio (http://people.csail.mit.edu/hubert/pyaudio/). """
220        if not hasattr(cls,"_available"):
221            try:
222                import mad, pyaudio
223            except ImportError:
224                cls._available = False
225            else:
226                cls._available = True 
227
228        return cls._available
229
230class ao_backend(Backend):
231    """A LibAO and PyMAD powered backend. """
232
233    def __init__(self, backend=None):
234        self.backend = backend
235
236    def play(self, fp):
237        import mad, ao
238
239        backend = self.backend
240        if backend is None:
241            import sys
242            backend = {
243                'darwin': 'macosx',
244                'win32': 'wmm',
245                'linux2': 'alsa'
246            }.get(sys.platform)
247
248        if backend is None:
249            raise Exception("Can't guess a usable libao baceknd."
250                            "If you know a backend that may work on your system then"
251                            "you can specify it using the backend options parameter.")
252
253        mf = mad.MadFile(fp)
254        dev = ao.AudioDevice(backend)
255
256        while True:
257            buf = mf.read()
258            if buf is None:
259                break
260            dev.play(buf, len(buf))
261
262    @classmethod
263    def available(cls):
264        """ Requires PyMad (http://spacepants.org/src/pymad/) and PyAO (http://ekyo.nerim.net/software/pyogg/). """
265        if not hasattr(cls,"_available"):
266            try:
267                import mad,ao
268            except ImportError:
269                cls._available = False
270            else:
271                cls._available = True 
272
273        return cls._available
274
275class Win32AudioClip(object):
276    """ A simple win32 audio clip. Inspired by mp3play (http://pypi.python.org/pypi/mp3play/) """
277
278    def __init__(self, fname):
279        self._alias = 'mp3_%s' % (id(self),)
280        self._mci = ctypes.windll.winmm.mciSendStringA
281        self._send(r'open "%s" alias %s' % (fname, self._alias))
282        self._send('set %s time format milliseconds' % (self._alias,))
283        length = self._send('status %s length' % (self._alias,))
284        self._length = int(length)
285
286    def _send(self, command):
287        buf = ctypes.c_buffer(255)
288        ret = self._mci(str(command), buf, 254, 0)
289        if ret:
290            err = int(ret)
291            err_buf = ctypes.c_buffer(255)
292            self._mci(err, err_buf, 254)
293            raise RuntimeError("Error %d: %s" % (err, err_buf.vale))
294        else:
295            return buf.value
296
297    def play(self):
298        self._send('play %s from %d to %d' % (self._alias, 0, self._length))
299
300    def isplaying(self):
301        return 'playing' == self._send('status %s mode' % self._alias)
302
303    def __del__(self):
304        self._send('close %s' % (self._alias,))
305
306
307class win32_backend(Backend):
308    """ The simplest backend available for Windows XP """
309    named_file_required = True
310
311    def play(self, fp):
312        clip = Win32AudioClip(fp.name)
313        clip.play()
314        while clip.isplaying():
315            time.sleep(1)
316
317    @classmethod
318    def available(cls):
319        """ Runs on Windows XP or newer (?) """
320        return sys.platform == 'win32'
321
322
323def autodetect_backend():
324    # test for win32 backend
325    if win32_backend.available():
326        return win32_backend()
327
328    # test for appkit
329    if appkit_backend.available():
330        return appkit_backend()
331
332    # test for pyaudio
333    if pyaudio_backend.available():
334        return pyaudio_backend()
335
336    # test for external programs
337    cmd = external_backend.autodetect_external_program()
338    if cmd is not None:
339        return external_backend(cmd)
340
341    # test for ao
342    if ao_backend.available():
343        return ao_backend()
344
345    # using default app as backend
346    print (u'No backend was found. Trying to play'
347           u' using your default application')
348    return defaultapp_backend()
349
350
351def installed_backends():
352    """ Return installed backends, classified in available or unavailable. """
353    # TODO: return in the order they are autodetected
354    def recursive_backends(classes):
355        result = classes
356        for cls in classes:
357            result += recursive_backends(cls.__subclasses__())
358        return result
359        
360    backends = recursive_backends(Backend.__subclasses__())
361    available = tuple(cls for cls in backends if cls.available())
362    unavailable = tuple(cls for cls in backends if not cls.available())
363    return available, unavailable
364
365
366def backends_help(extended=True):
367    available, unavailable = installed_backends()
368
369    print u'Available backends:'
370    for backend in available:
371        print '%s %s' % (backend.name().ljust(20), backend.info())
372
373    if not extended: return
374
375    print
376    print u'Unavailable backends:'
377    for backend in unavailable:
378        print '%s %s \n %s' % (backend.name().ljust(20), backend.info(), " "*20+backend.availability_info())
379
380    print
381    print (u'To use the unavailable backends you must first supply their dependencies')
382
383
384def main():
385    about= (u"Just Wanna Say [Version 2.1]  Copyright (C) 2011 Thomaz Reis and Dirley Rodrigues"
386           u"\nThis program comes with ABSOLUTELY NO WARRANTY;"
387           u"\nThis is free software, and you are welcome to redistribute it under certain conditions;") 
388
389    usage = 'usage: %prog [options] [phrases]'
390    option_list = [
391        optparse.make_option('-h', '--help', action='store_true',
392            dest='help', default=False, help=u'Show this help.'),
393        optparse.make_option('--list-backends', action='store_true',
394            dest='list_backends', default=False,
395            help=u'List all installed backends.'),
396        optparse.make_option('-l', '--language', action='store',
397            type='string', dest='language', default='pt',
398            help=u'Change the input language.'),
399        optparse.make_option('-b', '--backend', action='store',
400            type='string', dest='backend', default=None,
401            help=u'Specify the audio output mean.'),
402        optparse.make_option('-o', '--backend-options', action='store',
403            type='string', dest='backend_options', default=None,
404            help=u'Options to be passed to the backend.'), 
405    ]
406    parser = optparse.OptionParser(usage=usage, option_list=option_list, add_help_option=False)
407
408    options, arguments = parser.parse_args()
409
410    if options.help:
411        print about
412        parser.print_help()
413        print
414        backends_help(options.list_backends)
415        return
416    elif options.list_backends:
417        print about
418        print
419        backends_help(True)
420        return
421    elif not arguments:
422        print about
423        print
424        print u'No arguments specified. Please, try -h or --help for usage information.'
425        return
426
427    if options.backend is not None:
428        backend = globals().get('%s_backend' % (options.backend.lower(),))(options.backend_options)
429    elif options.backend_options is not None:
430        print u'You specified backend options but no backend.'
431        return
432    else:
433        backend = autodetect_backend()
434
435    text = u' '.join([i.decode(sys.stdin.encoding or 'utf-8') for i in arguments])
436
437    #Just Wanna Have Fun :)
438    if text == "Does JWS has any easter eggs?":
439       if options.language == "pt":
440           text = u"Infelizmente nĂŁo tem nenhum ister ĂŠgui nesse programa."
441       else:
442           text = u"Unfortunately there is no easter egg in this program."
443           
444    loader = DefaultLoader()
445    lfp = loader.load(text, options.language)
446
447    if backend.named_file_required:
448        identifier = hashlib.md5(':'.join([options.language, text.encode('utf-8')])).hexdigest()
449
450        storage = TempfileStorage()
451        fp = storage.store(identifier, lfp)
452        lfp.close()
453
454        backend.play(fp)
455
456        fp.close()
457    else:
458        backend.play(lfp)
459        lfp.close()
460
461
462if __name__ == '__main__':
463    main()