Repository: pndurette/gTTS

Issue:
gTTS slow on generating file
Hi,
I've used this library for almost 2 years. I runt a python script on a RPi3 and now when It start generating mp3 file from TTS conversion it locks for aroung 1 minute or more and no output file is generated. This behaviour seems to be randomic. Is there any issue between gTTS and Google Translation system?

Discussion:

Patch:
diff --git a/README.md b/README.md
index 09433aa..822c289 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,6 @@ Write spoken `mp3` data to a file, a file-like object (bytestring) for further a
 
 -   Customizable speech-specific sentence tokenizer that allows for unlimited lengths of text to be read, all while keeping proper intonation, abbreviations, decimals and more;
 -   Customizable text pre-processors which can, for example, provide pronunciation corrections;
--   Automatic retrieval of supported languages.
 
 ### Installation
 
diff --git a/docs/cli.rst b/docs/cli.rst
index ea5255a..39745a2 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -16,10 +16,6 @@ List available languages::
 
    $ gtts-cli --all
 
-List available languages (Italian names)::
-
-   $ gtts-cli --tld it --all
-
 Read 'hello' to ``hello.mp3``::
 
    $ gtts-cli 'hello' --output hello.mp3
diff --git a/gtts/cli.py b/gtts/cli.py
index 6d90ac6..f7c3f82 100644
--- a/gtts/cli.py
+++ b/gtts/cli.py
@@ -60,14 +60,12 @@ def validate_text(ctx, param, text):
 def validate_lang(ctx, param, lang):
     """Validation callback for the <lang> option.
     Ensures <lang> is a supported language unless the <nocheck> flag is set
-    Uses <tld> to fetch languages from other domains
     """
     if ctx.params['nocheck']:
         return lang
 
     try:
-        tld = ctx.params['tld']
-        if lang not in tts_langs(tld):
+        if lang not in tts_langs():
             raise click.UsageError(
                 "'%s' not in list of supported languages.\n"
                 "Use --all to list languages or "
@@ -92,14 +90,7 @@ def print_languages(ctx, param, value):
         return
 
     try:
-        tld = ctx.params['tld']
-    except KeyError:
-        # Either --tld was used after --all or not at all
-        # Default to the 'com' tld
-        tld = 'com'
-
-    try:
-        langs = tts_langs(tld)
+        langs = tts_langs()
         langs_str_list = sorted("{}: {}".format(k, langs[k]) for k in langs)
         click.echo('  ' + '\n  '.join(langs_str_list))
     except RuntimeError as e:  # pragma: no cover
@@ -167,8 +158,7 @@ def set_debug(ctx, param, debug):
     is_eager=True,
     expose_value=False,
     callback=print_languages,
-    help="Print all documented available IETF language tags and exit. "
-         "Use --tld beforehand to use an alternate domain")
+    help="Print all documented available IETF language tags and exit.")
 @click.option(
     '--debug',
     default=False,
diff --git a/gtts/lang.py b/gtts/lang.py
index 4cb8a40..089c84c 100644
--- a/gtts/lang.py
+++ b/gtts/lang.py
@@ -1,9 +1,5 @@
 # -*- coding: utf-8 -*-
-from gtts.utils import _translate_url
-from bs4 import BeautifulSoup
-import requests
 import logging
-import re
 
 __all__ = ['tts_langs']
 
@@ -12,14 +8,9 @@ log = logging.getLogger(__name__)
 log.addHandler(logging.NullHandler())
 
 
-def tts_langs(tld="com"):
+def tts_langs():
     """Languages Google Text-to-Speech supports.
 
-    Args:
-        tld (string): Top-level domain for the Google Translate host
-            to fetch languages from. i.e `https://translate.google.<tld>`.
-            Default is ``com``.
-
     Returns:
         dict: A dictionary of the type `{ '<lang>': '<name>'}`
 
@@ -29,60 +20,86 @@ def tts_langs(tld="com"):
 
     The dictionary returned combines languages from two origins:
 
-    - Languages fetched automatically from Google Translate
+    - Languages fetched from Google Translate
     - Languages that are undocumented variations that were observed to work and
       present different dialects or accents.
 
     """
-    try:
-        langs = dict()
-        log.debug("Fetching with '{}' tld".format(tld))
-        langs.update(_fetch_langs(tld))
-        langs.update(_extra_langs())
-        log.debug("langs: {}".format(langs))
-        return langs
-    except Exception as e:
-        raise RuntimeError("Unable to get language list: {}".format(str(e)))
-
-
-def _fetch_langs(tld="com"):
-    """Fetch (scrape) languages from Google Translate.
-
-    Google Translate loads a JavaScript Array of 'languages codes' that can
-    be spoken. We intersect this list with all the languages Google Translate
-    provides to get the ones that support text-to-speech.
-
-    Args:
-        tld (string): Top-level domain for the Google Translate host
-            to fetch languages from. i.e `https://translate.google.<tld>`.
-            The language names obtained will be in a language locale of the TLD
-            (e.g. ``tld=fr`` will retrieve the French names of the languages).
-            Default is ``com``.
-
-    Returns:
-        dict: A dictionnary of languages from Google Translate
+    langs = dict()
+    langs.update(_main_langs())
+    langs.update(_extra_langs())
+    log.debug("langs: {}".format(langs))
+    return langs
 
-    """
 
-    # Load JavaScript
-    js_contents = requests.get('https://ssl.gstatic.com/inputtools/js/ln/17/en.js').text
+def _main_langs():
+    """Define the main languages.
 
-    # Approximately extract TTS-enabled language codes
-    # RegEx pattern search because minified variables can change.
-    # Extra garbage will be dealt with later as we keep languages only.
-    # In: "[...]Fv={af:1,ar:1,[...],zh:1,"zh-cn":1,"zh-tw":1}[...]"
-    # Out: ['is', '12', [...], 'af', 'ar', [...], 'zh', 'zh-cn', 'zh-tw']
-    pattern = r'[{,\"](\w{2}|\w{2}-\w{2,3})(?=:1|\":1)'
-    tts_langs = re.findall(pattern, js_contents)
+    Returns:
+        dict: A dictionnary of the main languages extracted from
+            Google Translate.
 
-    # Build lang. dict. from main page (JavaScript object populating lang. menu)
-    # Filtering with the TTS-enabled languages
-    # In: "{code:'auto',name:'Detect language'},{code:'af',name:'Afrikaans'},[...]"
-    # re.findall: [('auto', 'Detect language'), ('af', 'Afrikaans'), [...]]
-    # Out: {'af': 'Afrikaans', [...]}
-    trans_pattern = r"{code:'(?P<lang>.+?[^'])',name:'(?P<name>.+?[^'])'}"
-    trans_langs = re.findall(trans_pattern, page.text)
-    return {lang: name for lang, name in trans_langs if lang in tts_langs}
+    """
+    return {
+        'af': 'Afrikaans',
+        'ar': 'Arabic',
+        'bn': 'Bengali',
+        'bs': 'Bosnian',
+        'ca': 'Catalan',
+        'cs': 'Czech',
+        'cy': 'Welsh',
+        'da': 'Danish',
+        'de': 'German',
+        'el': 'Greek',
+        'en': 'English',
+        'eo': 'Esperanto',
+        'es': 'Spanish',
+        'et': 'Estonian',
+        'fi': 'Finnish',
+        'fr': 'French',
+        'gu': 'Gujarati',
+        'hi': 'Hindi',
+        'hr': 'Croatian',
+        'hu': 'Hungarian',
+        'hy': 'Armenian',
+        'id': 'Indonesian',
+        'is': 'Icelandic',
+        'it': 'Italian',
+        'ja': 'Japanese',
+        'jw': 'Javanese',
+        'km': 'Khmer',
+        'kn': 'Kannada',
+        'ko': 'Korean',
+        'la': 'Latin',
+        'lv': 'Latvian',
+        'mk': 'Macedonian',
+        'ml': 'Malayalam',
+        'mr': 'Marathi',
+        'my': 'Myanmar (Burmese)',
+        'ne': 'Nepali',
+        'nl': 'Dutch',
+        'no': 'Norwegian',
+        'pl': 'Polish',
+        'pt': 'Portuguese',
+        'ro': 'Romanian',
+        'ru': 'Russian',
+        'si': 'Sinhala',
+        'sk': 'Slovak',
+        'sq': 'Albanian',
+        'sr': 'Serbian',
+        'su': 'Sundanese',
+        'sv': 'Swedish',
+        'sw': 'Swahili',
+        'ta': 'Tamil',
+        'te': 'Telugu',
+        'th': 'Thai',
+        'tl': 'Filipino',
+        'tr': 'Turkish',
+        'uk': 'Ukrainian',
+        'ur': 'Urdu',
+        'vi': 'Vietnamese',
+        'zh-CN': 'Chinese'
+    }
 
 
 def _extra_langs():
@@ -91,7 +108,7 @@ def _extra_langs():
     Returns:
         dict: A dictionnary of extra languages manually defined.
 
-            Variations of the ones fetched by `_fetch_langs`,
+            Variations of the ones fetched by `_main_langs`,
             observed to provide different dialects or accents or
             just simply accepted by the Google Translate Text-to-Speech API.
 
diff --git a/gtts/tts.py b/gtts/tts.py
index c722444..bdad2a4 100644
--- a/gtts/tts.py
+++ b/gtts/tts.py
@@ -132,7 +132,7 @@ class gTTS:
         # Language
         if lang_check:
             try:
-                langs = tts_langs(self.tld)
+                langs = tts_langs()
                 if lang.lower() not in langs:
                     raise ValueError("Language not supported: %s" % lang)
             except RuntimeError as e:
@@ -288,7 +288,9 @@ class gTTS:
                             decoded = base64.b64decode(as_bytes)
                             fp.write(decoded)
                         else:
-                            raise gTTSError("No audio stream in response")
+                            # Request successful, good response,
+                            # no audio stream in response
+                            raise gTTSError(tts=self, response=r)
                 log.debug("part-%i written to %s", idx, fp)
             except (AttributeError, TypeError) as e:
                 raise TypeError(
@@ -348,9 +350,9 @@ class gTTSError(Exception):
 
             if status == 403:
                 cause = "Bad token or upstream API changes"
-            elif status == 404 and not tts.lang_check:
-                cause = "Unsupported language '%s'" % self.tts.lang
+            elif status == 200 and not tts.lang_check:
+                cause = "No audio stream in response. Unsupported language '%s'" % self.tts.lang
             elif status >= 500:
                 cause = "Uptream API error. Try again later."
 
-        return "{}. Probable cause: {}".format(premise, cause)
+        return "{}. Probable cause: {}".format(premise, cause)
\ No newline at end of file


Test patch:
diff --git a/gtts/tests/test_cli.py b/gtts/tests/test_cli.py
index b652180..b801bda 100644
--- a/gtts/tests/test_cli.py
+++ b/gtts/tests/test_cli.py
@@ -77,16 +77,6 @@ def test_all():
     assert re.match(r"^(?:\s{2}(\w{2}|\w{2}-\w{2}): .+\n?)+$", result.output)
     assert result.exit_code == 0
 
-@pytest.mark.net
-def test_all_tld():
-    """Option <all> should return a list of languages"""
-    result = runner(['--tld', 'it', '--all'])
-
-    # Top-level domain set to 'it', language outputs should be Italian
-
-    assert "en: Inglese" in result.output
-    assert result.exit_code == 0
-
 
 # <lang> tests
 @pytest.mark.net
@@ -108,7 +98,7 @@ def test_lang_nocheck():
 
     assert 'lang: xx' in log
     assert 'lang_check: False' in log
-    assert "Probable cause: Unsupported language 'xx'" in result.output
+    assert "Unsupported language 'xx'" in result.output
     assert result.exit_code != 0
 
 # Param set tests
diff --git a/gtts/tests/test_lang.py b/gtts/tests/test_lang.py
index 3a094ca..564c596 100644
--- a/gtts/tests/test_lang.py
+++ b/gtts/tests/test_lang.py
@@ -1,35 +1,24 @@
 # -*- coding: utf-8 -*-
 import pytest
-from gtts.lang import tts_langs, _fetch_langs, _extra_langs
+from gtts.lang import tts_langs, _main_langs, _extra_langs
 
 
 """Test language list downloading"""
 
 
 @pytest.mark.net
-def test_fetch_langs():
+def test_main_langs():
     """Fetch languages successfully"""
     # Downloaded Languages
     # Safe to assume 'en' (english) will always be there
-    scraped_langs = _fetch_langs()
+    scraped_langs = _main_langs()
     assert 'en' in scraped_langs
 
-    # Scraping garbage
-    assert 'Detect language' not in scraped_langs
-    assert '—' not in scraped_langs
-
     # Add-in Languages
     all_langs = tts_langs()
     extra_langs = _extra_langs()
     assert len(all_langs) == len(scraped_langs) + len(extra_langs)
 
 
-@pytest.mark.net
-def test_fetch_langs_exception():
-    """Raise RuntimeError on language fetch exception"""
-    with pytest.raises(RuntimeError):
-        tts_langs(tld="invalid")
-
-
 if __name__ == '__main__':
     pytest.main(['-x', __file__])
diff --git a/gtts/tests/test_tts.py b/gtts/tests/test_tts.py
index c042b13..fa67c1b 100644
--- a/gtts/tests/test_tts.py
+++ b/gtts/tests/test_tts.py
@@ -5,11 +5,11 @@ from mock import Mock
 from six.moves import urllib
 
 from gtts.tts import gTTS, gTTSError
-from gtts.lang import _fetch_langs, _extra_langs
+from gtts.lang import _main_langs, _extra_langs
 
 # Testing all languages takes some time.
 # Set TEST_LANGS envvar to choose languages to test.
-#  * 'fetch': Languages fetched from the Web
+#  * 'main': Languages extracted from the Web
 #  * 'extra': Languagee set in Languages.EXTRA_LANGS
 #  * 'all': All of the above
 #  * <csv>: Languages tags list to test
@@ -26,10 +26,10 @@ ex.: { 'environ' : ['en', 'fr'] }
 """
 env = os.environ.get('TEST_LANGS')
 if not env or env == 'all':
-    langs = _fetch_langs()
+    langs = _main_langs()
     langs.update(_extra_langs())
-elif env == 'fetch':
-    langs = _fetch_langs()
+elif env == 'main':
+    langs = _main_langs()
 elif env == 'extra':
     langs = _extra_langs()
 else:
@@ -50,8 +50,8 @@ def test_TTS(tmp_path, lang):
         tts = gTTS(text=text, lang=lang, slow=slow)
         tts.save(filename)
 
-        # Check if files created is > 2k
-        assert filename.stat().st_size > 2000
+        # Check if files created is > 1.5
+        assert filename.stat().st_size > 1500
 
 
 @pytest.mark.net
@@ -115,6 +115,7 @@ def test_get_urls():
     assert r.netloc == 'translate.google.com'
     assert r.path == '/_/TranslateWebserverUi/data/batchexecute'
 
+
 @pytest.mark.net
 def test_get_bodies():
     """get request bodies list"""
@@ -152,11 +153,11 @@ def test_infer_msg():
     error403 = gTTSError(tts=tts403, response=response403)
     assert error403.msg == "403 (aaa) from TTS API. Probable cause: Bad token or upstream API changes"
 
-    # 404 (and not lang_check)
-    tts404 = Mock(lang='xx', lang_check=False)
-    response404 = Mock(status_code=404, reason='bbb')
-    error404 = gTTSError(tts=tts404, response=response404)
-    assert error404.msg == "404 (bbb) from TTS API. Probable cause: Unsupported language 'xx'"
+    # 200 (and not lang_check)
+    tts200 = Mock(lang='xx', lang_check=False)
+    response404 = Mock(status_code=200, reason='bbb')
+    error200 = gTTSError(tts=tts200, response=response404)
+    assert error200.msg == "200 (bbb) from TTS API. Probable cause: No audio stream in response. Unsupported language 'xx'"
 
     # >= 500
     tts500 = Mock()