Implemented manual subtitles translation using Google Translate

pull/1236/head
morpheus65535 4 years ago
parent 6e3f4bf804
commit 49c6e8b3fb

@ -27,7 +27,7 @@ from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_s
list_missing_subtitles, list_missing_subtitles_movies list_missing_subtitles, list_missing_subtitles_movies
from utils import history_log, history_log_movie, blacklist_log, blacklist_delete, blacklist_delete_all, \ from utils import history_log, history_log_movie, blacklist_log, blacklist_delete, blacklist_delete_all, \
blacklist_log_movie, blacklist_delete_movie, blacklist_delete_all_movie, get_sonarr_version, get_radarr_version, \ blacklist_log_movie, blacklist_delete_movie, blacklist_delete_all_movie, get_sonarr_version, get_radarr_version, \
delete_subtitles, subtitles_apply_mods delete_subtitles, subtitles_apply_mods, translate_subtitles_file
from get_providers import get_providers, get_providers_auth, list_throttled_providers, reset_throttled_providers, \ from get_providers import get_providers, get_providers_auth, list_throttled_providers, reset_throttled_providers, \
get_throttled_providers, set_throttled_providers get_throttled_providers, set_throttled_providers
from event_handler import event_stream from event_handler import event_stream
@ -870,7 +870,8 @@ class EpisodesTools(Resource):
subs[0] = {"name": language_from_alpha2(subtitle[0]), subs[0] = {"name": language_from_alpha2(subtitle[0]),
"code2": subtitle[0], "code2": subtitle[0],
"code3": alpha3_from_alpha2(subtitle[0]), "code3": alpha3_from_alpha2(subtitle[0]),
"forced": True if len(subtitle) > 1 else False} "forced": True if subs[0].endswith(':forced') else False,
"hi": True if subs[0].endswith(':hi') else False}
episode_external_subtitles.append({'language': subs[0], episode_external_subtitles.append({'language': subs[0],
'path': path_mappings.path_replace(subs[1]), 'path': path_mappings.path_replace(subs[1]),
'filename': os.path.basename(subs[1]), 'filename': os.path.basename(subs[1]),
@ -1932,6 +1933,29 @@ class SubMods(Resource):
return '', 200 return '', 200
class SubTranslate(Resource):
@authenticate
def post(self):
video_path = request.form.get('videoPath')
media_type = request.form.get('mediaType')
subtitles_path = request.form.get('subtitlesPath')
dest_language = request.form.get('language')
forced = True if request.form.get('forced') == 'true' else False
hi = True if request.form.get('hi') == 'true' else False
result = translate_subtitles_file(video_path=video_path, source_srt_file=subtitles_path, to_lang=dest_language,
forced=forced, hi=hi)
if result:
if media_type == 'series':
store_subtitles(path_mappings.path_replace_reverse(video_path), video_path)
else:
store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path)
return '', 200
else:
return '', 500
class BrowseBazarrFS(Resource): class BrowseBazarrFS(Resource):
@authenticate @authenticate
def get(self): def get(self):
@ -2036,6 +2060,7 @@ api.add_resource(BlacklistMovieSubtitlesRemoveAll, '/blacklist_movie_subtitles_r
api.add_resource(SyncSubtitles, '/sync_subtitles') api.add_resource(SyncSubtitles, '/sync_subtitles')
api.add_resource(SubMods, '/sub_mods') api.add_resource(SubMods, '/sub_mods')
api.add_resource(SubTranslate, '/sub_translate')
api.add_resource(BrowseBazarrFS, '/browse_bazarr_filesystem') api.add_resource(BrowseBazarrFS, '/browse_bazarr_filesystem')
api.add_resource(BrowseSonarrFS, '/browse_sonarr_filesystem') api.add_resource(BrowseSonarrFS, '/browse_sonarr_filesystem')

@ -5,6 +5,7 @@ import time
import platform import platform
import logging import logging
import requests import requests
import pysubs2
from whichcraft import which from whichcraft import which
from get_args import args from get_args import args
@ -15,8 +16,10 @@ from get_languages import alpha2_from_alpha3, language_from_alpha3, alpha3_from_
from helper import path_mappings from helper import path_mappings
from list_subtitles import store_subtitles, store_subtitles_movie from list_subtitles import store_subtitles, store_subtitles_movie
from subliminal_patch.subtitle import Subtitle from subliminal_patch.subtitle import Subtitle
from subliminal_patch.core import get_subtitle_path
from subzero.language import Language from subzero.language import Language
from subliminal import region as subliminal_cache_region from subliminal import region as subliminal_cache_region
from deep_translator import GoogleTranslator
import datetime import datetime
import glob import glob
@ -284,3 +287,51 @@ def subtitles_apply_mods(language, subtitle_path, mods):
f.write(content) f.write(content)
def translate_subtitles_file(video_path, source_srt_file, to_lang, forced, hi):
lang_obj = Language(to_lang)
if forced:
lang_obj = Language.rebuild(lang_obj, forced=True)
if hi:
lang_obj = Language.rebuild(lang_obj, hi=True)
logging.debug('BAZARR is translating in {0} this subtitles {1}'.format(lang_obj, source_srt_file))
max_characters = 5000
dest_srt_file = get_subtitle_path(video_path, language=lang_obj, extension='.srt', forced_tag=forced, hi_tag=hi)
subs = pysubs2.load(source_srt_file, encoding='utf-8')
lines_list = [x.plaintext for x in subs]
joined_lines_str = '\n\n\n'.join(lines_list)
logging.debug('BAZARR splitting subtitles into {} characters blocks'.format(max_characters))
lines_block_list = []
translated_lines_list = []
while len(joined_lines_str):
partial_lines_str = joined_lines_str[:max_characters]
if len(joined_lines_str) > max_characters:
new_partial_lines_str = partial_lines_str.rsplit('\n\n\n', 1)[0]
else:
new_partial_lines_str = partial_lines_str
lines_block_list.append(new_partial_lines_str)
joined_lines_str = joined_lines_str.replace(new_partial_lines_str, '')
logging.debug('BAZARR is sending {} blocks to Google Translate'.format(len(lines_block_list)))
for block_str in lines_block_list:
try:
translated_partial_srt_text = GoogleTranslator(source='auto',
target=lang_obj.basename).translate(text=block_str)
except:
return False
else:
translated_partial_srt_list = translated_partial_srt_text.split('\n\n\n')
translated_lines_list += translated_partial_srt_list
logging.debug('BAZARR saving translated subtitles to {}'.format(dest_srt_file))
for i, line in enumerate(subs):
line.plaintext = translated_lines_list[i]
subs.save(dest_srt_file)
return True

@ -0,0 +1,25 @@
"""Top-level package for deep_translator."""
from .google_trans import GoogleTranslator
from .pons import PonsTranslator
from .linguee import LingueeTranslator
from .mymemory import MyMemoryTranslator
from .yandex import YandexTranslator
from .qcri import QCRI
from .deepl import DeepL
from .detection import single_detection, batch_detection
__author__ = """Nidhal Baccouri"""
__email__ = 'nidhalbacc@gmail.com'
__version__ = '1.3.2'
__all__ = [GoogleTranslator,
PonsTranslator,
LingueeTranslator,
MyMemoryTranslator,
YandexTranslator,
QCRI,
DeepL,
single_detection,
batch_detection]

@ -0,0 +1,52 @@
"""Console script for deep_translator."""
import argparse
import sys
from .google_trans import GoogleTranslator
from .mymemory import MyMemoryTranslator
from .pons import PonsTranslator
from .linguee import LingueeTranslator
def translate(args):
"""
function used to provide translations from the parsed terminal arguments
@param args: parsed terminal arguments
@return: None
"""
translator = None
if args.translator == 'google':
translator = GoogleTranslator(source=args.source, target=args.target)
elif args.translator == 'pons':
translator = PonsTranslator(source=args.source, target=args.target)
elif args.translator == 'linguee':
translator = LingueeTranslator(source=args.source, target=args.target)
elif args.translator == 'mymemory':
translator = MyMemoryTranslator(source=args.source, target=args.target)
else:
print("given translator is not supported. Please use a supported translator from the deep_translator tool")
res = translator.translate(args.text)
print(" | Translation from {} to {} |".format(args.source, args.target))
print("Translated text: \n {}".format(res))
def main():
"""
function responsible for parsing terminal arguments and provide them for further use in the translation process
"""
parser = argparse.ArgumentParser()
parser.add_argument('--translator', '-trans',
default='google', type=str, help="name of the translator you want to use")
parser.add_argument('--source', '-src', type=str, help="source language to translate from", required=True)
parser.add_argument('--target', '-tg', type=str, help="target language to translate to", required=True)
parser.add_argument('--text', '-txt', type=str, help="text you want to translate", required=True)
args = parser.parse_args()
translate(args)
# sys.exit()
if __name__ == "__main__":
main()

@ -0,0 +1,11 @@
"""
configuration object that holds data about the language detection api
"""
config = {
"url": 'https://ws.detectlanguage.com/0.2/detect',
"headers": {
'User-Agent': 'Detect Language API Python Client 1.4.0',
'Authorization': 'Bearer {}',
}
}

@ -0,0 +1,183 @@
BASE_URLS = {
"GOOGLE_TRANSLATE": "https://translate.google.com/m",
"PONS": "https://en.pons.com/translate/",
"YANDEX": "https://translate.yandex.net/api/{version}/tr.json/{endpoint}",
"LINGUEE": "https://www.linguee.com/",
"MYMEMORY": "http://api.mymemory.translated.net/get",
"QCRI": "https://mt.qcri.org/api/v1/{endpoint}?",
"DEEPL": "https://api.deepl.com/{version}/"
}
GOOGLE_CODES_TO_LANGUAGES = {
'af': 'afrikaans',
'sq': 'albanian',
'am': 'amharic',
'ar': 'arabic',
'hy': 'armenian',
'az': 'azerbaijani',
'eu': 'basque',
'be': 'belarusian',
'bn': 'bengali',
'bs': 'bosnian',
'bg': 'bulgarian',
'ca': 'catalan',
'ceb': 'cebuano',
'ny': 'chichewa',
'zh-cn': 'chinese (simplified)',
'zh-tw': 'chinese (traditional)',
'co': 'corsican',
'hr': 'croatian',
'cs': 'czech',
'da': 'danish',
'nl': 'dutch',
'en': 'english',
'eo': 'esperanto',
'et': 'estonian',
'tl': 'filipino',
'fi': 'finnish',
'fr': 'french',
'fy': 'frisian',
'gl': 'galician',
'ka': 'georgian',
'de': 'german',
'el': 'greek',
'gu': 'gujarati',
'ht': 'haitian creole',
'ha': 'hausa',
'haw': 'hawaiian',
'iw': 'hebrew',
'hi': 'hindi',
'hmn': 'hmong',
'hu': 'hungarian',
'is': 'icelandic',
'ig': 'igbo',
'id': 'indonesian',
'ga': 'irish',
'it': 'italian',
'ja': 'japanese',
'jw': 'javanese',
'kn': 'kannada',
'kk': 'kazakh',
'km': 'khmer',
'ko': 'korean',
'ku': 'kurdish (kurmanji)',
'ky': 'kyrgyz',
'lo': 'lao',
'la': 'latin',
'lv': 'latvian',
'lt': 'lithuanian',
'lb': 'luxembourgish',
'mk': 'macedonian',
'mg': 'malagasy',
'ms': 'malay',
'ml': 'malayalam',
'mt': 'maltese',
'mi': 'maori',
'mr': 'marathi',
'mn': 'mongolian',
'my': 'myanmar (burmese)',
'ne': 'nepali',
'no': 'norwegian',
'ps': 'pashto',
'fa': 'persian',
'pl': 'polish',
'pt': 'portuguese',
'pa': 'punjabi',
'ro': 'romanian',
'ru': 'russian',
'sm': 'samoan',
'gd': 'scots gaelic',
'sr': 'serbian',
'st': 'sesotho',
'sn': 'shona',
'sd': 'sindhi',
'si': 'sinhala',
'sk': 'slovak',
'sl': 'slovenian',
'so': 'somali',
'es': 'spanish',
'su': 'sundanese',
'sw': 'swahili',
'sv': 'swedish',
'tg': 'tajik',
'ta': 'tamil',
'te': 'telugu',
'th': 'thai',
'tr': 'turkish',
'uk': 'ukrainian',
'ur': 'urdu',
'uz': 'uzbek',
'vi': 'vietnamese',
'cy': 'welsh',
'xh': 'xhosa',
'yi': 'yiddish',
'yo': 'yoruba',
'zu': 'zulu',
'fil': 'Filipino',
'he': 'Hebrew'
}
GOOGLE_LANGUAGES_TO_CODES = {v: k for k, v in GOOGLE_CODES_TO_LANGUAGES.items()}
PONS_CODES_TO_LANGUAGES = {
'ar': 'arabic',
'bg': 'bulgarian',
'zh-cn': 'chinese',
'cs': 'czech',
'da': 'danish',
'nl': 'dutch',
'en': 'english',
'fr': 'french',
'de': 'german',
'el': 'greek',
'hu': 'hungarian',
'it': 'italian',
'la': 'latin',
'no': 'norwegian',
'pl': 'polish',
'pt': 'portuguese',
'ru': 'russian',
'sl': 'slovenian',
'es': 'spanish',
'sv': 'swedish',
'tr': 'turkish',
'elv': 'elvish'
}
PONS_LANGUAGES_TO_CODES = {v: k for k, v in PONS_CODES_TO_LANGUAGES.items()}
LINGUEE_LANGUAGES_TO_CODES = {
"maltese": "mt",
"english": "en",
"german": "de",
"bulgarian": "bg",
"polish": "pl",
"portuguese": "pt",
"hungarian": "hu",
"romanian": "ro",
"russian": "ru",
#"serbian": "sr",
"dutch": "nl",
"slovakian": "sk",
"greek": "el",
"slovenian": "sl",
"danish": "da",
"italian": "it",
"spanish": "es",
"finnish": "fi",
"chinese": "zh",
"french": "fr",
#"croatian": "hr",
"czech": "cs",
"laotian": "lo",
"swedish": "sv",
"latvian": "lv",
"estonian": "et",
"japanese": "ja"
}
LINGUEE_CODE_TO_LANGUAGE = {v: k for k, v in LINGUEE_LANGUAGES_TO_CODES.items()}
# "72e9e2cc7c992db4dcbdd6fb9f91a0d1"

@ -0,0 +1,59 @@
import requests
from requests.utils import requote_uri
from deep_translator.constants import BASE_URLS
from deep_translator.exceptions import (RequestError,
ServerException, TranslationNotFound, TooManyRequests)
class DeepL(object):
"""
class that wraps functions, which use the DeepL translator under the hood to translate word(s)
"""
def __init__(self, api_key=None):
"""
@param api_key: your DeepL api key. Get one here: https://www.deepl.com/docs-api/accessing-the-api/
"""
if not api_key:
raise ServerException(401)
self.version = 'v2'
self.api_key = api_key
self.__base_url = BASE_URLS.get("DEEPL").format(version=self.version)
def translate(self, source, target, text):
params = {
"auth_key": self.api_key,
"target_lang": target,
"source_lang": source,
"text": text
}
try:
response = requests.get(self.__base_url, params=params)
except ConnectionError:
raise ServerException(503)
else:
if response.status_code != 200:
ServerException(response.status_code)
else:
res = response.json()
if not res:
raise TranslationNotFound(text)
return res
def translate_batch(self, source, target, batch):
"""
translate a batch of texts
@param source: source language
@param target: target language
@param batch: list of texts to translate
@return: list of translations
"""
return [self.translate(source, target, text) for text in batch]
if __name__ == '__main__':
d = DeepL(api_key="key")
print(d)

@ -0,0 +1,76 @@
"""
language detection API
"""
import requests
from deep_translator.configs import config
from requests.exceptions import HTTPError
def get_request_body(text, api_key, *args):
"""
send a request and return the response body parsed as dictionary
@param text: target text that you want to detect its language
@type text: str
@type api_key: str
@param api_key: your private API key
"""
if not api_key:
raise Exception("you need to get an API_KEY for this to work. "
"Get one for free here: https://detectlanguage.com/documentation")
if not text:
raise Exception("Please provide an input text")
else:
try:
headers = config['headers']
headers['Authorization'] = headers['Authorization'].format(api_key)
response = requests.post(config['url'],
json={'q': text},
headers=headers)
body = response.json().get('data')
return body
except HTTPError as e:
print("Error occured while requesting from server: ", e.args)
raise e
def single_detection(text, api_key=None, detailed=False, *args, **kwargs):
"""
function responsible for detecting the language from a text
@param text: target text that you want to detect its language
@type text: str
@type api_key: str
@param api_key: your private API key
@param detailed: set to True if you want to get detailed information about the detection process
"""
body = get_request_body(text, api_key)
detections = body.get('detections')
if detailed:
return detections[0]
lang = detections[0].get('language', None)
if lang:
return lang
def batch_detection(text_list, api_key, detailed=False, *args):
"""
function responsible for detecting the language from a text
@param text_list: target batch that you want to detect its language
@param api_key: your private API key
@param detailed: set to True if you want to get detailed information about the detection process
"""
body = get_request_body(text_list, api_key)
detections = body.get('detections')
res = [obj[0] for obj in detections]
if detailed:
return res
else:
return [obj['language'] for obj in res]

@ -0,0 +1,113 @@
class BaseError(Exception):
"""
base error structure class
"""
def __init__(self, val, message):
"""
@param val: actual value
@param message: message shown to the user
"""
self.val = val
self.message = message
super().__init__()
def __str__(self):
return "{} --> {}".format(self.val, self.message)
class LanguageNotSupportedException(BaseError):
"""
exception thrown if the user uses a language that is not supported by the deep_translator
"""
def __init__(self, val, message="There is no support for the chosen language"):
super().__init__(val, message)
class NotValidPayload(BaseError):
"""
exception thrown if the user enters an invalid payload
"""
def __init__(self,
val,
message='text must be a valid text with maximum 5000 character, otherwise it cannot be translated'):
super(NotValidPayload, self).__init__(val, message)
class TranslationNotFound(BaseError):
"""
exception thrown if no translation was found for the text provided by the user
"""
def __init__(self,
val,
message='No translation was found using the current translator. Try another translator?'):
super(TranslationNotFound, self).__init__(val, message)
class ElementNotFoundInGetRequest(BaseError):
"""
exception thrown if the html element was not found in the body parsed by beautifulsoup
"""
def __init__(self,
val,
message='Required element was not found in the API response'):
super(ElementNotFoundInGetRequest, self).__init__(val, message)
class NotValidLength(BaseError):
"""
exception thrown if the provided text exceed the length limit of the translator
"""
def __init__(self, val, min_chars, max_chars):
message = "Text length need to be between {} and {} characters".format(min_chars, max_chars)
super(NotValidLength, self).__init__(val, message)
class RequestError(Exception):
"""
exception thrown if an error occured during the request call, e.g a connection problem.
"""
def __init__(self, message="Request exception can happen due to an api connection error. "
"Please check your connection and try again"):
self.message = message
def __str__(self):
return self.message
class TooManyRequests(Exception):
"""
exception thrown if an error occured during the request call, e.g a connection problem.
"""
def __init__(self, message="Server Error: You made too many requests to the server. According to google, you are allowed to make 5 requests per second and up to 200k requests per day. You can wait and try again later or you can try the translate_batch function"):
self.message = message
def __str__(self):
return self.message
class ServerException(Exception):
"""
Default YandexTranslate exception from the official website
"""
errors = {
401: "ERR_KEY_INVALID",
402: "ERR_KEY_BLOCKED",
403: "ERR_DAILY_REQ_LIMIT_EXCEEDED",
404: "ERR_DAILY_CHAR_LIMIT_EXCEEDED",
413: "ERR_TEXT_TOO_LONG",
422: "ERR_UNPROCESSABLE_TEXT",
501: "ERR_LANG_NOT_SUPPORTED",
503: "ERR_SERVICE_NOT_AVAIBLE",
}
def __init__(self, status_code, *args):
message = self.errors.get(status_code, "API server error")
super(ServerException, self).__init__(message, *args)

@ -0,0 +1,173 @@
"""
google translator API
"""
from deep_translator.constants import BASE_URLS, GOOGLE_LANGUAGES_TO_CODES
from deep_translator.exceptions import TooManyRequests, LanguageNotSupportedException, TranslationNotFound, NotValidPayload, RequestError
from deep_translator.parent import BaseTranslator
from bs4 import BeautifulSoup
import requests
from time import sleep
import warnings
import logging
class GoogleTranslator(BaseTranslator):
"""
class that wraps functions, which use google translate under the hood to translate text(s)
"""
_languages = GOOGLE_LANGUAGES_TO_CODES
supported_languages = list(_languages.keys())
def __init__(self, source="auto", target="en"):
"""
@param source: source language to translate from
@param target: target language to translate to
"""
self.__base_url = BASE_URLS.get("GOOGLE_TRANSLATE")
if self.is_language_supported(source, target):
self._source, self._target = self._map_language_to_code(source.lower(), target.lower())
super(GoogleTranslator, self).__init__(base_url=self.__base_url,
source=self._source,
target=self._target,
element_tag='div',
element_query={"class": "t0"},
payload_key='q', # key of text in the url
hl=self._target,
sl=self._source)
self._alt_element_query = {"class": "result-container"}
@staticmethod
def get_supported_languages(as_dict=False):
"""
return the supported languages by the google translator
@param as_dict: if True, the languages will be returned as a dictionary mapping languages to their abbreviations
@return: list or dict
"""
return GoogleTranslator.supported_languages if not as_dict else GoogleTranslator._languages
def _map_language_to_code(self, *languages):
"""
map language to its corresponding code (abbreviation) if the language was passed by its full name by the user
@param languages: list of languages
@return: mapped value of the language or raise an exception if the language is not supported
"""
for language in languages:
if language in self._languages.values() or language == 'auto':
yield language
elif language in self._languages.keys():
yield self._languages[language]
else:
raise LanguageNotSupportedException(language)
def is_language_supported(self, *languages):
"""
check if the language is supported by the translator
@param languages: list of languages
@return: bool or raise an Exception
"""
for lang in languages:
if lang != 'auto' and lang not in self._languages.keys():
if lang != 'auto' and lang not in self._languages.values():
raise LanguageNotSupportedException(lang)
return True
def translate(self, text, **kwargs):
"""
function that uses google translate to translate a text
@param text: desired text to translate
@return: str: translated text
"""
if self._validate_payload(text):
text = text.strip()
if self.payload_key:
self._url_params[self.payload_key] = text
response = requests.get(self.__base_url,
params=self._url_params, headers ={'User-agent': 'your bot 0.1'})
if response.status_code == 429:
raise TooManyRequests()
if response.status_code != 200:
# print("status code", response.status_code)
raise RequestError()
soup = BeautifulSoup(response.text, 'html.parser')
element = soup.find(self._element_tag, self._element_query)
if not element:
element = soup.find(self._element_tag, self._alt_element_query)
if not element:
raise TranslationNotFound(text)
return element.get_text(strip=True)
def translate_file(self, path, **kwargs):
"""
translate directly from file
@param path: path to the target file
@type path: str
@param kwargs: additional args
@return: str
"""
try:
with open(path) as f:
text = f.read()
return self.translate(text=text)
except Exception as e:
raise e
def translate_sentences(self, sentences=None, **kwargs):
"""
translate many sentences together. This makes sense if you have sentences with different languages
and you want to translate all to unified language. This is handy because it detects
automatically the language of each sentence and then translate it.
@param sentences: list of sentences to translate
@return: list of all translated sentences
"""
warnings.warn("deprecated. Use the translate_batch function instead", DeprecationWarning, stacklevel=2)
logging.warning("deprecated. Use the translate_batch function instead")
if not sentences:
raise NotValidPayload(sentences)
translated_sentences = []
try:
for sentence in sentences:
translated = self.translate(text=sentence)
translated_sentences.append(translated)
return translated_sentences
except Exception as e:
raise e
def translate_batch(self, batch=None):
"""
translate a list of texts
@param batch: list of texts you want to translate
@return: list of translations
"""
if not batch:
raise Exception("Enter your text list that you want to translate")
arr = []
for text in batch:
translated = self.translate(text)
arr.append(translated)
sleep(2)
return arr
# if __name__ == '__main__':
# for _ in range(10):
# txt = GoogleTranslator(source="en", target="ar").translate("Hello how are you")
# print("text: ", txt)

@ -0,0 +1,130 @@
"""
linguee translator API
"""
from deep_translator.constants import BASE_URLS, LINGUEE_LANGUAGES_TO_CODES, LINGUEE_CODE_TO_LANGUAGE
from deep_translator.exceptions import (LanguageNotSupportedException,
TranslationNotFound,
NotValidPayload,
ElementNotFoundInGetRequest,
RequestError,
TooManyRequests)
from deep_translator.parent import BaseTranslator
from bs4 import BeautifulSoup
import requests
from requests.utils import requote_uri
class LingueeTranslator(BaseTranslator):
"""
class that wraps functions, which use the linguee translator under the hood to translate word(s)
"""
_languages = LINGUEE_LANGUAGES_TO_CODES
supported_languages = list(_languages.keys())
def __init__(self, source, target="en"):
"""
@param source: source language to translate from
@param target: target language to translate to
"""
self.__base_url = BASE_URLS.get("LINGUEE")
if self.is_language_supported(source, target):
self._source, self._target = self._map_language_to_code(source.lower(), target.lower())
super().__init__(base_url=self.__base_url,
source=self._source,
target=self._target,
element_tag='a',
element_query={'class': 'dictLink featured'},
payload_key=None, # key of text in the url
)
@staticmethod
def get_supported_languages(as_dict=False):
"""
return the supported languages by the linguee translator
@param as_dict: if True, the languages will be returned as a dictionary mapping languages to their abbreviations
@return: list or dict
"""
return LingueeTranslator.supported_languages if not as_dict else LingueeTranslator._languages
def _map_language_to_code(self, *languages, **kwargs):
"""
map language to its corresponding code (abbreviation) if the language was passed by its full name by the user
@param languages: list of languages
@return: mapped value of the language or raise an exception if the language is not supported
"""
for language in languages:
if language in self._languages.values():
yield LINGUEE_CODE_TO_LANGUAGE[language]
elif language in self._languages.keys():
yield language
else:
raise LanguageNotSupportedException(language)
def is_language_supported(self, *languages, **kwargs):
"""
check if the language is supported by the translator
@param languages: list of languages
@return: bool or raise an Exception
"""
for lang in languages:
if lang not in self._languages.keys():
if lang not in self._languages.values():
raise LanguageNotSupportedException(lang)
return True
def translate(self, word, return_all=False, **kwargs):
"""
function that uses linguee to translate a word
@param word: word to translate
@type word: str
@param return_all: set to True to return all synonym of the translated word
@type return_all: bool
@return: str: translated word
"""
if self._validate_payload(word, max_chars=50):
# %s-%s/translation/%s.html
url = "{}{}-{}/translation/{}.html".format(self.__base_url, self._source, self._target, word)
url = requote_uri(url)
response = requests.get(url)
if response.status_code == 429:
raise TooManyRequests()
if response.status_code != 200:
raise RequestError()
soup = BeautifulSoup(response.text, 'html.parser')
elements = soup.find_all(self._element_tag, self._element_query)
if not elements:
raise ElementNotFoundInGetRequest(elements)
filtered_elements = []
for el in elements:
try:
pronoun = el.find('span', {'class': 'placeholder'}).get_text(strip=True)
except AttributeError:
pronoun = ''
filtered_elements.append(el.get_text(strip=True).replace(pronoun, ''))
if not filtered_elements:
raise TranslationNotFound(word)
return filtered_elements if return_all else filtered_elements[0]
def translate_words(self, words, **kwargs):
"""
translate a batch of words together by providing them in a list
@param words: list of words you want to translate
@param kwargs: additional args
@return: list of translated words
"""
if not words:
raise NotValidPayload(words)
translated_words = []
for word in words:
translated_words.append(self.translate(payload=word))
return translated_words

@ -0,0 +1,174 @@
"""
mymemory translator API
"""
import logging
import warnings
from deep_translator.constants import BASE_URLS, GOOGLE_LANGUAGES_TO_CODES
from deep_translator.exceptions import (NotValidPayload,
TranslationNotFound,
LanguageNotSupportedException,
RequestError,
TooManyRequests)
from deep_translator.parent import BaseTranslator
import requests
from time import sleep
class MyMemoryTranslator(BaseTranslator):
"""
class that uses the mymemory translator to translate texts
"""
_languages = GOOGLE_LANGUAGES_TO_CODES
supported_languages = list(_languages.keys())
def __init__(self, source="auto", target="en", **kwargs):
"""
@param source: source language to translate from
@param target: target language to translate to
"""
self.__base_url = BASE_URLS.get("MYMEMORY")
if self.is_language_supported(source, target):
self._source, self._target = self._map_language_to_code(source.lower(), target.lower())
self._source = self._source if self._source != 'auto' else 'Lao'
self.email = kwargs.get('email', None)
super(MyMemoryTranslator, self).__init__(base_url=self.__base_url,
source=self._source,
target=self._target,
payload_key='q',
langpair='{}|{}'.format(self._source, self._target))
@staticmethod
def get_supported_languages(as_dict=False):
"""
return the supported languages by the mymemory translator
@param as_dict: if True, the languages will be returned as a dictionary mapping languages to their abbreviations
@return: list or dict
"""
return MyMemoryTranslator.supported_languages if not as_dict else MyMemoryTranslator._languages
def _map_language_to_code(self, *languages):
"""
map language to its corresponding code (abbreviation) if the language was passed by its full name by the user
@param languages: list of languages
@return: mapped value of the language or raise an exception if the language is not supported
"""
for language in languages:
if language in self._languages.values() or language == 'auto':
yield language
elif language in self._languages.keys():
yield self._languages[language]
else:
raise LanguageNotSupportedException(language)
def is_language_supported(self, *languages):
"""
check if the language is supported by the translator
@param languages: list of languages
@return: bool or raise an Exception
"""
for lang in languages:
if lang != 'auto' and lang not in self._languages.keys():
if lang != 'auto' and lang not in self._languages.values():
raise LanguageNotSupportedException(lang)
return True
def translate(self, text, return_all=False, **kwargs):
"""
function that uses the mymemory translator to translate a text
@param text: desired text to translate
@type text: str
@param return_all: set to True to return all synonym/similars of the translated text
@return: str or list
"""
if self._validate_payload(text, max_chars=500):
text = text.strip()
if self.payload_key:
self._url_params[self.payload_key] = text
if self.email:
self._url_params['de'] = self.email
response = requests.get(self.__base_url,
params=self._url_params,
headers=self.headers)
if response.status_code == 429:
raise TooManyRequests()
if response.status_code != 200:
raise RequestError()
data = response.json()
if not data:
TranslationNotFound(text)
translation = data.get('responseData').get('translatedText')
if translation:
return translation
elif not translation:
all_matches = data.get('matches')
matches = (match['translation'] for match in all_matches)
next_match = next(matches)
return next_match if not return_all else list(all_matches)
def translate_sentences(self, sentences=None, **kwargs):
"""
translate many sentences together. This makes sense if you have sentences with different languages
and you want to translate all to unified language. This is handy because it detects
automatically the language of each sentence and then translate it.
@param sentences: list of sentences to translate
@return: list of all translated sentences
"""
warn_msg = "deprecated. Use the translate_batch function instead"
warnings.warn(warn_msg, DeprecationWarning, stacklevel=2)
logging.warning(warn_msg)
if not sentences:
raise NotValidPayload(sentences)
translated_sentences = []
try:
for sentence in sentences:
translated = self.translate(text=sentence, **kwargs)
translated_sentences.append(translated)
return translated_sentences
except Exception as e:
raise e
def translate_file(self, path, **kwargs):
"""
translate directly from file
@param path: path to the target file
@type path: str
@param kwargs: additional args
@return: str
"""
try:
with open(path) as f:
text = f.read()
return self.translate(text=text)
except Exception as e:
raise e
def translate_batch(self, batch=None):
"""
translate a list of texts
@param batch: list of texts you want to translate
@return: list of translations
"""
if not batch:
raise Exception("Enter your text list that you want to translate")
arr = []
for text in batch:
translated = self.translate(text)
arr.append(translated)
sleep(2)
return arr

@ -0,0 +1,71 @@
"""parent translator class"""
from deep_translator.exceptions import NotValidPayload, NotValidLength
from abc import ABC, abstractmethod
class BaseTranslator(ABC):
"""
Abstract class that serve as a parent translator for other different translators
"""
def __init__(self,
base_url=None,
source="auto",
target="en",
payload_key=None,
element_tag=None,
element_query=None,
**url_params):
"""
@param source: source language to translate from
@param target: target language to translate to
"""
self.__base_url = base_url
self._source = source
self._target = target
self._url_params = url_params
self._element_tag = element_tag
self._element_query = element_query
self.payload_key = payload_key
self.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) '
'AppleWebit/535.19'
'(KHTML, like Gecko) Chrome/18.0.1025.168 Safari/535.19'}
super(BaseTranslator, self).__init__()
@staticmethod
def _validate_payload(payload, min_chars=1, max_chars=5000):
"""
validate the target text to translate
@param payload: text to translate
@return: bool
"""
if not payload or not isinstance(payload, str):
raise NotValidPayload(payload)
if not BaseTranslator.__check_length(payload, min_chars, max_chars):
raise NotValidLength(payload, min_chars, max_chars)
return True
@staticmethod
def __check_length(payload, min_chars, max_chars):
"""
check length of the provided target text to translate
@param payload: text to translate
@param min_chars: minimum characters allowed
@param max_chars: maximum characters allowed
@return: bool
"""
return True if min_chars < len(payload) < max_chars else False
@abstractmethod
def translate(self, text, **kwargs):
"""
translate a text using a translator under the hood and return the translated text
@param text: text to translate
@param kwargs: additional arguments
@return: str
"""
return NotImplemented('You need to implement the translate method!')

@ -0,0 +1,136 @@
"""
pons translator API
"""
from bs4 import BeautifulSoup
import requests
from deep_translator.constants import BASE_URLS, PONS_LANGUAGES_TO_CODES, PONS_CODES_TO_LANGUAGES
from deep_translator.exceptions import (LanguageNotSupportedException,
TranslationNotFound,
NotValidPayload,
ElementNotFoundInGetRequest,
RequestError,
TooManyRequests)
from deep_translator.parent import BaseTranslator
from requests.utils import requote_uri
class PonsTranslator(BaseTranslator):
"""
class that uses PONS translator to translate words
"""
_languages = PONS_LANGUAGES_TO_CODES
supported_languages = list(_languages.keys())
def __init__(self, source, target="english"):
"""
@param source: source language to translate from
@param target: target language to translate to
"""
self.__base_url = BASE_URLS.get("PONS")
if self.is_language_supported(source, target):
self._source, self._target = self._map_language_to_code(source, target)
super().__init__(base_url=self.__base_url,
source=self._source,
target=self._target,
payload_key=None,
element_tag='div',
element_query={"class": "target"}
)
@staticmethod
def get_supported_languages(as_dict=False):
"""
return the supported languages by the linguee translator
@param as_dict: if True, the languages will be returned as a dictionary mapping languages to their abbreviations
@return: list or dict
"""
return PonsTranslator.supported_languages if not as_dict else PonsTranslator._languages
def _map_language_to_code(self, *languages, **kwargs):
"""
map language to its corresponding code (abbreviation) if the language was passed by its full name by the user
@param languages: list of languages
@return: mapped value of the language or raise an exception if the language is not supported
"""
for language in languages:
if language in self._languages.values():
yield PONS_CODES_TO_LANGUAGES[language]
elif language in self._languages.keys():
yield language
else:
raise LanguageNotSupportedException(language)
def is_language_supported(self, *languages, **kwargs):
"""
check if the language is supported by the translator
@param languages: list of languages
@return: bool or raise an Exception
"""
for lang in languages:
if lang not in self._languages.keys():
if lang not in self._languages.values():
raise LanguageNotSupportedException(lang)
return True
def translate(self, word, return_all=False, **kwargs):
"""
function that uses PONS to translate a word
@param word: word to translate
@type word: str
@param return_all: set to True to return all synonym of the translated word
@type return_all: bool
@return: str: translated word
"""
if self._validate_payload(word, max_chars=50):
url = "{}{}-{}/{}".format(self.__base_url, self._source, self._target, word)
url = requote_uri(url)
response = requests.get(url)
if response.status_code == 429:
raise TooManyRequests()
if response.status_code != 200:
raise RequestError()
soup = BeautifulSoup(response.text, 'html.parser')
elements = soup.findAll(self._element_tag, self._element_query)
if not elements:
raise ElementNotFoundInGetRequest(word)
filtered_elements = []
for el in elements:
temp = ''
for e in el.findAll('a'):
if e.parent.name == 'div':
if e and "/translate/{}-{}/".format(self._target, self._source) in e.get('href'):
temp += e.get_text() + ' '
filtered_elements.append(temp)
if not filtered_elements:
raise ElementNotFoundInGetRequest(word)
word_list = [word for word in filtered_elements if word and len(word) > 1]
if not word_list:
raise TranslationNotFound(word)
return word_list if return_all else word_list[0]
def translate_words(self, words, **kwargs):
"""
translate a batch of words together by providing them in a list
@param words: list of words you want to translate
@param kwargs: additional args
@return: list of translated words
"""
if not words:
raise NotValidPayload(words)
translated_words = []
for word in words:
translated_words.append(self.translate(payload=word))
return translated_words

@ -0,0 +1,91 @@
import requests
from requests.utils import requote_uri
from deep_translator.constants import BASE_URLS
from deep_translator.exceptions import (RequestError,
ServerException, TranslationNotFound, TooManyRequests)
class QCRI(object):
"""
class that wraps functions, which use the QRCI translator under the hood to translate word(s)
"""
def __init__(self, api_key=None):
"""
@param api_key: your qrci api key. Get one for free here https://mt.qcri.org/api/v1/ref
"""
if not api_key:
raise ServerException(401)
self.__base_url = BASE_URLS.get("QCRI")
self.api_key = api_key
self.api_endpoints = {
"get_languages": "getLanguagePairs",
"get_domains": "getDomains",
"translate": "translate",
}
self.params = {
"key": self.api_key
}
def _get(self, endpoint, params=None, return_text=True):
if not params:
params = self.params
try:
res = requests.get(self.__base_url.format(endpoint=self.api_endpoints[endpoint]), params=params)
return res.text if return_text else res
except Exception as e:
raise e
def get_supported_languages(self):
pairs = self._get("get_languages")
return pairs
@property
def languages(self):
return self.get_supported_languages()
def get_domains(self):
domains = self._get("get_domains")
return domains
@property
def domains(self):
return self.get_domains()
def translate(self, source, target, domain, text):
params = {
"key": self.api_key,
"langpair": "{}-{}".format(source, target),
"domain": domain,
"text": text
}
try:
response = self._get("translate", params=params, return_text=False)
except ConnectionError:
raise ServerException(503)
else:
if response.status_code != 200:
ServerException(response.status_code)
else:
res = response.json()
translation = res["translatedText"]
if not translation:
raise TranslationNotFound(text)
return translation
def translate_batch(self, source, target, domain, batch):
"""
translate a batch of texts
@param source: source language
@param target: target language
@param batch: list of texts to translate
@return: list of translations
"""
return [self.translate(source, target, domain, text) for text in batch]

@ -0,0 +1 @@
"""Unit test package for deep_translator."""

@ -0,0 +1,57 @@
#!/usr/bin/env python
"""Tests for `deep_translator` package."""
import pytest
from deep_translator import exceptions, GoogleTranslator
@pytest.fixture
def google_translator():
"""Sample pytest fixture.
See more at: http://doc.pytest.org/en/latest/fixture.html
"""
return GoogleTranslator(target='en')
def test_content(google_translator):
"""Sample pytest test function with the pytest fixture as an argument."""
# from bs4 import BeautifulSoup
# assert 'GitHub' in BeautifulSoup(response.content).title.string
assert google_translator.translate(text='좋은') == "good"
def test_inputs():
with pytest.raises(exceptions.LanguageNotSupportedException):
GoogleTranslator(source="", target="")
with pytest.raises(exceptions.LanguageNotSupportedException):
GoogleTranslator(source="auto", target="nothing")
# test abbreviations and languages
g1 = GoogleTranslator("en", "fr")
g2 = GoogleTranslator("english", "french")
assert g1._source == g2._source
assert g1._target == g2._target
def test_payload(google_translator):
with pytest.raises(exceptions.NotValidPayload):
google_translator.translate(text="")
with pytest.raises(exceptions.NotValidPayload):
google_translator.translate(text=123)
with pytest.raises(exceptions.NotValidPayload):
google_translator.translate(text={})
with pytest.raises(exceptions.NotValidPayload):
google_translator.translate(text=[])
with pytest.raises(exceptions.NotValidLength):
google_translator.translate("a"*5001)
#for _ in range(1):
#assert google_translator.translate(text='좋은') == "good"

@ -0,0 +1,49 @@
#!/usr/bin/env python
"""Tests for `deep_translator` package."""
import pytest
from deep_translator import exceptions, LingueeTranslator
@pytest.fixture
def linguee():
return LingueeTranslator(source="english", target='french')
def test_content(linguee):
"""Sample pytest test function with the pytest fixture as an argument."""
# from bs4 import BeautifulSoup
# assert 'GitHub' in BeautifulSoup(response.content).title.string
assert linguee.translate(word='good') is not None
def test_inputs():
with pytest.raises(exceptions.LanguageNotSupportedException):
LingueeTranslator(source="", target="")
with pytest.raises(exceptions.LanguageNotSupportedException):
LingueeTranslator(source="auto", target="nothing")
l1 = LingueeTranslator("en", "fr")
l2 = LingueeTranslator("english", "french")
assert l1._source == l2._source
assert l1._target == l2._target
def test_payload(linguee):
with pytest.raises(exceptions.NotValidPayload):
linguee.translate("")
with pytest.raises(exceptions.NotValidPayload):
linguee.translate(123)
with pytest.raises(exceptions.NotValidPayload):
linguee.translate({})
with pytest.raises(exceptions.NotValidPayload):
linguee.translate([])
with pytest.raises(exceptions.NotValidLength):
linguee.translate("a"*51)

@ -0,0 +1,48 @@
#!/usr/bin/env python
"""Tests for `deep_translator` package."""
import pytest
from deep_translator import exceptions, MyMemoryTranslator
@pytest.fixture
def mymemory():
return MyMemoryTranslator(source="en", target='fr')
def test_content(mymemory):
"""Sample pytest test function with the pytest fixture as an argument."""
# from bs4 import BeautifulSoup
# assert 'GitHub' in BeautifulSoup(response.content).title.string
assert mymemory.translate(text='good') is not None
def test_inputs():
with pytest.raises(exceptions.LanguageNotSupportedException):
MyMemoryTranslator(source="", target="")
with pytest.raises(exceptions.LanguageNotSupportedException):
MyMemoryTranslator(source="auto", target="nothing")
m1 = MyMemoryTranslator("en", "fr")
m2 = MyMemoryTranslator("english", "french")
assert m1._source == m2._source
assert m1._target == m2._target
def test_payload(mymemory):
with pytest.raises(exceptions.NotValidPayload):
mymemory.translate(text="")
with pytest.raises(exceptions.NotValidPayload):
mymemory.translate(text=123)
with pytest.raises(exceptions.NotValidPayload):
mymemory.translate(text={})
with pytest.raises(exceptions.NotValidPayload):
mymemory.translate(text=[])
with pytest.raises(exceptions.NotValidLength):
mymemory.translate(text="a"*501)

@ -0,0 +1,48 @@
#!/usr/bin/env python
"""Tests for `deep_translator` package."""
import pytest
from deep_translator import exceptions, PonsTranslator
@pytest.fixture
def pons():
return PonsTranslator(source="english", target='french')
def test_content(pons):
"""Sample pytest test function with the pytest fixture as an argument."""
# from bs4 import BeautifulSoup
# assert 'GitHub' in BeautifulSoup(response.content).title.string
assert pons.translate(word='good') is not None
def test_inputs():
with pytest.raises(exceptions.LanguageNotSupportedException):
PonsTranslator(source="", target="")
with pytest.raises(exceptions.LanguageNotSupportedException):
PonsTranslator(source="auto", target="nothing")
l1 = PonsTranslator("en", "fr")
l2 = PonsTranslator("english", "french")
assert l1._source == l2._source
assert l1._target == l2._target
def test_payload(pons):
with pytest.raises(exceptions.NotValidPayload):
pons.translate("")
with pytest.raises(exceptions.NotValidPayload):
pons.translate(123)
with pytest.raises(exceptions.NotValidPayload):
pons.translate({})
with pytest.raises(exceptions.NotValidPayload):
pons.translate([])
with pytest.raises(exceptions.NotValidLength):
pons.translate("a" * 51)

@ -0,0 +1,3 @@
"""
utilities
"""

@ -0,0 +1,132 @@
"""
Yandex translator API
"""
import requests
from requests import exceptions
from deep_translator.constants import BASE_URLS
from deep_translator.exceptions import (RequestError,
ServerException, TranslationNotFound, TooManyRequests)
class YandexTranslator(object):
"""
class that wraps functions, which use the yandex translator under the hood to translate word(s)
"""
def __init__(self, api_key=None):
"""
@param api_key: your yandex api key
"""
if not api_key:
raise ServerException(401)
self.__base_url = BASE_URLS.get("YANDEX")
self.api_key = api_key
self.api_version = "v1.5"
self.api_endpoints = {
"langs": "getLangs",
"detect": "detect",
"translate": "translate",
}
def get_supported_languages(self):
return set(x.split("-")[0] for x in self.dirs)
@property
def languages(self):
return self.get_supported_languages()
@property
def dirs(self, proxies=None):
try:
url = self.__base_url.format(version=self.api_version, endpoint="getLangs")
print("url: ", url)
response = requests.get(url, params={"key": self.api_key}, proxies=proxies)
except requests.exceptions.ConnectionError:
raise ServerException(503)
else:
data = response.json()
if response.status_code != 200:
raise ServerException(response.status_code)
return data.get("dirs")
def detect(self, text, proxies=None):
response = None
params = {
"text": text,
"format": "plain",
"key": self.api_key,
}
try:
url = self.__base_url.format(version=self.api_version, endpoint="detect")
response = requests.post(url, data=params, proxies=proxies)
except RequestError:
raise
except ConnectionError:
raise ServerException(503)
except ValueError:
raise ServerException(response.status_code)
else:
response = response.json()
language = response['lang']
status_code = response['code']
if status_code != 200:
raise RequestError()
elif not language:
raise ServerException(501)
return language
def translate(self, source, target, text, proxies=None):
params = {
"text": text,
"format": "plain",
"lang": target if source == "auto" else "{}-{}".format(source, target),
"key": self.api_key
}
try:
url = self.__base_url.format(version=self.api_version, endpoint="translate")
response = requests.post(url, data=params, proxies=proxies)
except ConnectionError:
raise ServerException(503)
else:
response = response.json()
if response['code'] == 429:
raise TooManyRequests()
if response['code'] != 200:
raise ServerException(response['code'])
if not response['text']:
raise TranslationNotFound()
return response['text']
def translate_file(self, source, target, path):
"""
translate from a file
@param source: source language
@param target: target language
@param path: path to file
@return: translated text
"""
try:
with open(path) as f:
text = f.read()
return self.translate(source, target, text)
except Exception as e:
raise e
def translate_batch(self, source, target, batch):
"""
translate a batch of texts
@param source: source language
@param target: target language
@param batch: list of texts to translate
@return: list of translations
"""
return [self.translate(source, target, text) for text in batch]

@ -44,6 +44,10 @@
.dtrg-start { .dtrg-start {
cursor: pointer; cursor: pointer;
} }
.dropdown-menu li.disabled {
cursor: not-allowed;
}
</style> </style>
{% endblock page_head %} {% endblock page_head %}
@ -428,6 +432,42 @@
</div> </div>
</div> </div>
<div id="episodeSubtitleTranslateModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Translate Subtitles</h5><br>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form class="form" name="subtitles_translate_form" id="subtitles_translate_form">
<div class="modal-body">
<div class="container-fluid">
<div class="row">
<div class="col text-right">
<b>Language Name</b>
</div>
<div class="form-group col">
<select class="form-control selectpicker" id="translate_language_select">
</select>
</div>
</div>
<input type="hidden" id="translate_data_path" value="" />
<input type="hidden" id="translate_data_videopath" value="" />
<input type="hidden" id="translate_data_forced" value="" />
<input type="hidden" id="translate_data_hi" value="" />
</div>
</div>
<div class="modal-footer">
<span id="subtitles_translate_save_button_span"><button type="submit" id="subtitles_translate_save_button" class="btn btn-info">Translate</button></span>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
<div id="episodeSubtitleModFpsModal" class="modal" tabindex="-1" role="dialog"> <div id="episodeSubtitleModFpsModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
@ -1571,6 +1611,7 @@
tools += '<a href="" class="subtitles_mod_color badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Adds color to your subtitles"><i class="fa fa-paint-brush"></i></a> '; tools += '<a href="" class="subtitles_mod_color badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Adds color to your subtitles"><i class="fa fa-paint-brush"></i></a> ';
tools += '<a href="" class="subtitles_mod_fps badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Change Frame Rate"><i class="fa fa-film"></i></a> '; tools += '<a href="" class="subtitles_mod_fps badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Change Frame Rate"><i class="fa fa-film"></i></a> ';
tools += '<a href="" class="subtitles_mod_offset badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Adjust all times (show earlier/later)"><i class="fa fa-clock"></i></a> '; tools += '<a href="" class="subtitles_mod_offset badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Adjust all times (show earlier/later)"><i class="fa fa-clock"></i></a> ';
tools += '<a href="" class="subtitles_translate badge badge-secondary" data-forced="' + data.language.forced + '" data-hi="' + data.language.hi + '" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-videopath="' + data.videopath + '" data-toggle="tooltip" data-placement="right" title = "Translate your subtitles"><i class="fa fa-language"></i></a> ';
return tools; return tools;
} }
@ -1689,6 +1730,30 @@
}); });
}); });
$('#episode_tools_result').on('click', '.subtitles_translate', function (e) {
e.preventDefault();
$('#translate_data_language').val($(this).attr("data-language"))
$('#translate_data_path').val($(this).attr("data-path"))
$('#translate_data_videopath').val($(this).attr("data-videopath"))
$('#translate_data_forced').val($(this).attr("data-forced"))
$('#translate_data_hi').val($(this).attr("data-hi"))
$('#translate_language_select')
.empty();
$.each(enabledLanguages, function (i, item) {
$('#translate_language_select')
.append('<option value="' + item.code3 + '"' + ((item.code2 in GOOGLE_CODES_TO_LANGUAGES) ? '' : ' data-subtext="Unsupported" disabled') + '>' + item.name + '</option>');
});
$("#translate_language_select").selectpicker("refresh");
$('#episodeToolsModal').modal('hide');
$('#episodeSubtitleTranslateModal')
.modal({
focus: false
});
});
$('#subtitles_mod_fps_form').on('submit', function (e) { $('#subtitles_mod_fps_form').on('submit', function (e) {
e.preventDefault(); e.preventDefault();
@ -1713,6 +1778,33 @@
}); });
}); });
$('#subtitles_translate_form').on('submit', function (e) {
e.preventDefault();
const values = {
language: $('#translate_language_select').val(),
subtitlesPath: $('#translate_data_path').val(),
videoPath: $('#translate_data_videopath').val(),
forced: $('#translate_data_forced').val(),
hi: $('#translate_data_hi').val(),
mediaType: 'series',
};
$.ajax({
url: "{{ url_for('api.subtranslate') }}",
type: "POST",
dataType: "json",
data: values,
beforeSend: function () {
$('#subtitles_translate_save_button').html('<div class="spinner-border spinner-border-sm" role="status"><span class="sr-only">Loading...</span></div>');
},
complete: function () {
$('#episodeSubtitleTranslateModal').modal('hide');
}
});
});
$('#episodeSubtitleModFpsModal').on('hidden.bs.modal', function (e) { $('#episodeSubtitleModFpsModal').on('hidden.bs.modal', function (e) {
$('#subtitles_mod_fps_save_button_span').html('<button type="submit" id="subtitles_mod_fps_save_button" class="btn btn-info">Save</button>'); $('#subtitles_mod_fps_save_button_span').html('<button type="submit" id="subtitles_mod_fps_save_button" class="btn btn-info">Save</button>');
}); });
@ -1766,6 +1858,10 @@
$('#subtitles_mod_offset_save_button_span').html('<button type="submit" id="subtitles_mod_offset_save_button" class="btn btn-info">Save</button>'); $('#subtitles_mod_offset_save_button_span').html('<button type="submit" id="subtitles_mod_offset_save_button" class="btn btn-info">Save</button>');
}); });
$('#episodeSubtitleTranslateModal').on('hidden.bs.modal', function (e) {
$('#subtitles_translate_save_button_span').html('<button type="submit" id="subtitles_translate_save_button" class="btn btn-info">Translate</button>');
});
}); });
function episodesDetailsRefresh() { function episodesDetailsRefresh() {
@ -1854,5 +1950,114 @@
} }
}); });
} }
GOOGLE_CODES_TO_LANGUAGES = {
'af': 'afrikaans',
'sq': 'albanian',
'am': 'amharic',
'ar': 'arabic',
'hy': 'armenian',
'az': 'azerbaijani',
'eu': 'basque',
'be': 'belarusian',
'bn': 'bengali',
'bs': 'bosnian',
'bg': 'bulgarian',
'ca': 'catalan',
'ceb': 'cebuano',
'ny': 'chichewa',
'zh-cn': 'chinese (simplified)',
'zh-tw': 'chinese (traditional)',
'co': 'corsican',
'hr': 'croatian',
'cs': 'czech',
'da': 'danish',
'nl': 'dutch',
'en': 'english',
'eo': 'esperanto',
'et': 'estonian',
'tl': 'filipino',
'fi': 'finnish',
'fr': 'french',
'fy': 'frisian',
'gl': 'galician',
'ka': 'georgian',
'de': 'german',
'el': 'greek',
'gu': 'gujarati',
'ht': 'haitian creole',
'ha': 'hausa',
'haw': 'hawaiian',
'iw': 'hebrew',
'hi': 'hindi',
'hmn': 'hmong',
'hu': 'hungarian',
'is': 'icelandic',
'ig': 'igbo',
'id': 'indonesian',
'ga': 'irish',
'it': 'italian',
'ja': 'japanese',
'jw': 'javanese',
'kn': 'kannada',
'kk': 'kazakh',
'km': 'khmer',
'ko': 'korean',
'ku': 'kurdish (kurmanji)',
'ky': 'kyrgyz',
'lo': 'lao',
'la': 'latin',
'lv': 'latvian',
'lt': 'lithuanian',
'lb': 'luxembourgish',
'mk': 'macedonian',
'mg': 'malagasy',
'ms': 'malay',
'ml': 'malayalam',
'mt': 'maltese',
'mi': 'maori',
'mr': 'marathi',
'mn': 'mongolian',
'my': 'myanmar (burmese)',
'ne': 'nepali',
'no': 'norwegian',
'ps': 'pashto',
'fa': 'persian',
'pl': 'polish',
'pt': 'portuguese',
'pa': 'punjabi',
'ro': 'romanian',
'ru': 'russian',
'sm': 'samoan',
'gd': 'scots gaelic',
'sr': 'serbian',
'st': 'sesotho',
'sn': 'shona',
'sd': 'sindhi',
'si': 'sinhala',
'sk': 'slovak',
'sl': 'slovenian',
'so': 'somali',
'es': 'spanish',
'su': 'sundanese',
'sw': 'swahili',
'sv': 'swedish',
'tg': 'tajik',
'ta': 'tamil',
'te': 'telugu',
'th': 'thai',
'tr': 'turkish',
'uk': 'ukrainian',
'ur': 'urdu',
'uz': 'uzbek',
'vi': 'vietnamese',
'cy': 'welsh',
'xh': 'xhosa',
'yi': 'yiddish',
'yo': 'yoruba',
'zu': 'zulu',
'fil': 'Filipino',
'he': 'Hebrew'
};
</script> </script>
{% endblock tail %} {% endblock tail %}

@ -40,6 +40,10 @@
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
} }
.dropdown-menu li.disabled {
cursor: not-allowed;
}
</style> </style>
{% endblock page_head %} {% endblock page_head %}
@ -372,6 +376,42 @@
</div> </div>
</div> </div>
<div id="movieSubtitleTranslateModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Translate Subtitles</h5><br>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form class="form" name="subtitles_translate_form" id="subtitles_translate_form">
<div class="modal-body">
<div class="container-fluid">
<div class="row">
<div class="col text-right">
<b>Language Name</b>
</div>
<div class="form-group col">
<select class="form-control selectpicker" id="translate_language_select">
</select>
</div>
</div>
<input type="hidden" id="translate_data_path" value="" />
<input type="hidden" id="translate_data_videopath" value="" />
<input type="hidden" id="translate_data_forced" value="" />
<input type="hidden" id="translate_data_hi" value="" />
</div>
</div>
<div class="modal-footer">
<span id="subtitles_translate_save_button_span"><button type="submit" id="subtitles_translate_save_button" class="btn btn-info">Translate</button></span>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
<div id="movieSubtitleModFpsModal" class="modal" tabindex="-1" role="dialog"> <div id="movieSubtitleModFpsModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
@ -971,6 +1011,7 @@
tools += '<a href="" class="subtitles_mod_color badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Adds color to your subtitles"><i class="fa fa-paint-brush"></i></a> '; tools += '<a href="" class="subtitles_mod_color badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Adds color to your subtitles"><i class="fa fa-paint-brush"></i></a> ';
tools += '<a href="" class="subtitles_mod_fps badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Change Frame Rate"><i class="fa fa-film"></i></a> '; tools += '<a href="" class="subtitles_mod_fps badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Change Frame Rate"><i class="fa fa-film"></i></a> ';
tools += '<a href="" class="subtitles_mod_offset badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Adjust all times (show earlier/later)"><i class="fa fa-clock"></i></a> '; tools += '<a href="" class="subtitles_mod_offset badge badge-secondary" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-toggle="tooltip" data-placement="right" title = "Adjust all times (show earlier/later)"><i class="fa fa-clock"></i></a> ';
tools += '<a href="" class="subtitles_translate badge badge-secondary" data-forced="' + data.language.forced + '" data-hi="' + data.language.hi + '" data-language="' + data.language.code3 + '" data-path="' + data.path + '" data-videopath="' + data.videopath + '" data-toggle="tooltip" data-placement="right" title = "Translate your subtitles"><i class="fa fa-language"></i></a> ';
return tools; return tools;
} }
@ -1088,6 +1129,30 @@
}); });
}); });
$('#movie_tools_result').on('click', '.subtitles_translate', function (e) {
e.preventDefault();
$('#translate_data_language').val($(this).attr("data-language"))
$('#translate_data_path').val($(this).attr("data-path"))
$('#translate_data_videopath').val($(this).attr("data-videopath"))
$('#translate_data_forced').val($(this).attr("data-forced"))
$('#translate_data_hi').val($(this).attr("data-hi"))
$('#translate_language_select')
.empty();
$.each(enabledLanguages, function (i, item) {
$('#translate_language_select')
.append('<option value="' + item.code3 + '"' + ((item.code2 in GOOGLE_CODES_TO_LANGUAGES) ? '' : 'data-subtext="Unsupported" disabled') + '>' + item.name + '</option>');
});
$("#translate_language_select").selectpicker("refresh");
$('#movieToolsModal').modal('hide');
$('#movieSubtitleTranslateModal')
.modal({
focus: false
});
});
$('#subtitles_mod_fps_form').on('submit', function (e) { $('#subtitles_mod_fps_form').on('submit', function (e) {
e.preventDefault(); e.preventDefault();
@ -1112,10 +1177,41 @@
}); });
}); });
$('#subtitles_translate_form').on('submit', function (e) {
e.preventDefault();
const values = {
language: $('#translate_language_select').val(),
subtitlesPath: $('#translate_data_path').val(),
videoPath: $('#translate_data_videopath').val(),
forced: $('#translate_data_forced').val(),
hi: $('#translate_data_hi').val(),
mediaType: 'movie',
};
$.ajax({
url: "{{ url_for('api.subtranslate') }}",
type: "POST",
dataType: "json",
data: values,
beforeSend: function () {
$('#subtitles_translate_save_button').html('<div class="spinner-border spinner-border-sm" role="status"><span class="sr-only">Loading...</span></div>');
},
complete: function () {
$('#movieSubtitleTranslateModal').modal('hide');
}
});
});
$('#movieSubtitleModFpsModal').on('hidden.bs.modal', function (e) { $('#movieSubtitleModFpsModal').on('hidden.bs.modal', function (e) {
$('#subtitles_mod_fps_save_button_span').html('<button type="submit" id="subtitles_mod_fps_save_button" class="btn btn-info">Save</button>'); $('#subtitles_mod_fps_save_button_span').html('<button type="submit" id="subtitles_mod_fps_save_button" class="btn btn-info">Save</button>');
}); });
$('#movieSubtitleTranslateModal').on('hidden.bs.modal', function (e) {
$('#subtitles_translate_save_button_span').html('<button type="submit" id="subtitles_translate_save_button" class="btn btn-info">Translate</button>');
});
$('#movie_tools_result').on('click', '.subtitles_mod_offset', function (e) { $('#movie_tools_result').on('click', '.subtitles_mod_offset', function (e) {
e.preventDefault(); e.preventDefault();
@ -1303,5 +1399,114 @@
} }
}); });
} }
GOOGLE_CODES_TO_LANGUAGES = {
'af': 'afrikaans',
'sq': 'albanian',
'am': 'amharic',
'ar': 'arabic',
'hy': 'armenian',
'az': 'azerbaijani',
'eu': 'basque',
'be': 'belarusian',
'bn': 'bengali',
'bs': 'bosnian',
'bg': 'bulgarian',
'ca': 'catalan',
'ceb': 'cebuano',
'ny': 'chichewa',
'zh-cn': 'chinese (simplified)',
'zh-tw': 'chinese (traditional)',
'co': 'corsican',
'hr': 'croatian',
'cs': 'czech',
'da': 'danish',
'nl': 'dutch',
'en': 'english',
'eo': 'esperanto',
'et': 'estonian',
'tl': 'filipino',
'fi': 'finnish',
'fr': 'french',
'fy': 'frisian',
'gl': 'galician',
'ka': 'georgian',
'de': 'german',
'el': 'greek',
'gu': 'gujarati',
'ht': 'haitian creole',
'ha': 'hausa',
'haw': 'hawaiian',
'iw': 'hebrew',
'hi': 'hindi',
'hmn': 'hmong',
'hu': 'hungarian',
'is': 'icelandic',
'ig': 'igbo',
'id': 'indonesian',
'ga': 'irish',
'it': 'italian',
'ja': 'japanese',
'jw': 'javanese',
'kn': 'kannada',
'kk': 'kazakh',
'km': 'khmer',
'ko': 'korean',
'ku': 'kurdish (kurmanji)',
'ky': 'kyrgyz',
'lo': 'lao',
'la': 'latin',
'lv': 'latvian',
'lt': 'lithuanian',
'lb': 'luxembourgish',
'mk': 'macedonian',
'mg': 'malagasy',
'ms': 'malay',
'ml': 'malayalam',
'mt': 'maltese',
'mi': 'maori',
'mr': 'marathi',
'mn': 'mongolian',
'my': 'myanmar (burmese)',
'ne': 'nepali',
'no': 'norwegian',
'ps': 'pashto',
'fa': 'persian',
'pl': 'polish',
'pt': 'portuguese',
'pa': 'punjabi',
'ro': 'romanian',
'ru': 'russian',
'sm': 'samoan',
'gd': 'scots gaelic',
'sr': 'serbian',
'st': 'sesotho',
'sn': 'shona',
'sd': 'sindhi',
'si': 'sinhala',
'sk': 'slovak',
'sl': 'slovenian',
'so': 'somali',
'es': 'spanish',
'su': 'sundanese',
'sw': 'swahili',
'sv': 'swedish',
'tg': 'tajik',
'ta': 'tamil',
'te': 'telugu',
'th': 'thai',
'tr': 'turkish',
'uk': 'ukrainian',
'ur': 'urdu',
'uz': 'uzbek',
'vi': 'vietnamese',
'cy': 'welsh',
'xh': 'xhosa',
'yi': 'yiddish',
'yo': 'yoruba',
'zu': 'zulu',
'fil': 'Filipino',
'he': 'Hebrew'
};
</script> </script>
{% endblock tail %} {% endblock tail %}

Loading…
Cancel
Save