File indexing completed on 2024-12-22 04:40:17

0001 /*
0002     SPDX-FileCopyrightText: 2023 Mladen Milinkovic <max@smoothware.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 #include "deeplengine.h"
0007 
0008 #include "appglobal.h"
0009 #include "application.h"
0010 #include "helpers/common.h"
0011 #include "scconfig.h"
0012 
0013 #include <QByteArray>
0014 #include <QDebug>
0015 #include <QFile>
0016 #include <QJsonArray>
0017 #include <QJsonDocument>
0018 #include <QJsonObject>
0019 #include <QNetworkAccessManager>
0020 #include <QNetworkReply>
0021 #include <QNetworkRequest>
0022 #include <QUrlQuery>
0023 
0024 #include <KLocalizedString>
0025 #include <KMessageBox>
0026 
0027 #include <memory>
0028 #include <type_traits>
0029 
0030 #include <openssl/bio.h>
0031 #include <openssl/evp.h>
0032 #include <openssl/pem.h>
0033 
0034 using namespace SubtitleComposer;
0035 
0036 DeepLEngine::DeepLEngine(QObject *parent)
0037     : TranslateEngine(parent),
0038       m_netManager(new QNetworkAccessManager(this)),
0039       m_ui(new Ui::DeepLEngine)
0040 {
0041 }
0042 
0043 DeepLEngine::~DeepLEngine()
0044 {
0045     delete m_ui;
0046 }
0047 
0048 static void
0049 showError(QNetworkReply *res)
0050 {
0051     QString errText;
0052     const QJsonDocument doc = QJsonDocument::fromJson(res->readAll());
0053     if(doc.isObject()) {
0054         const QJsonObject err = doc[$("error")].toObject();
0055         if(!err.empty()) {
0056             errText = i18n("Remote service error %1 %2\n%3",
0057                             err[$("code")].toInt(),
0058                             err[$("status")].toString(),
0059                             err[$("message")].toString());
0060         }
0061     }
0062     if(errText.isEmpty()) {
0063         const int httpCode = res->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
0064         if(httpCode) {
0065             errText = i18n("HTTP Error %1 - %2\n%3",
0066                     httpCode,
0067                     res->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(),
0068                     QString(res->readAll()));
0069         } else {
0070             errText = i18n("Network Error %1\n%2",
0071                     int(res->error()),
0072                     QString(res->readAll()));
0073         }
0074     }
0075     qWarning() << "DeepL Engine Error" << errText;
0076     KMessageBox::error(app()->mainWindow(), errText, i18n("DeepL Engine Error"));
0077 }
0078 
0079 void
0080 DeepLEngine::settings(QWidget *widget)
0081 {
0082     m_ui->setupUi(widget);
0083     m_ui->authKey->setText(SCConfig::dltAuthKey());
0084     m_ui->apiDomain->setText(SCConfig::dltApiDomain());
0085 
0086     connect(m_ui->btnConnect, &QPushButton::clicked, this, [&](){
0087         SCConfig::setDltAuthKey(m_ui->authKey->text());
0088         SCConfig::setDltApiDomain(m_ui->apiDomain->text());
0089         languagesUpdate();
0090     });
0091 
0092     m_ui->grpSettings->setEnabled(false);
0093     emit engineReady(false);
0094 
0095     if(!SCConfig::dltAuthKey().isEmpty())
0096         languagesUpdate();
0097 }
0098 
0099 bool
0100 DeepLEngine::languagesUpdate()
0101 {
0102     QNetworkRequest request;
0103     QNetworkReply *res;
0104 
0105     request.setUrl($("https://%1/v2/languages?type=source").arg(SCConfig::dltApiDomain()));
0106     request.setRawHeader("Authorization", QByteArray("DeepL-Auth-Key ") + SCConfig::dltAuthKey().toUtf8());
0107     res = m_netManager->get(request);
0108     connect(res, &QNetworkReply::finished, this, &DeepLEngine::languagesUpdated);
0109 
0110     request.setUrl($("https://%1/v2/languages?type=target").arg(SCConfig::dltApiDomain()));
0111     request.setRawHeader("Authorization", QByteArray("DeepL-Auth-Key ") + SCConfig::dltAuthKey().toUtf8());
0112     res = m_netManager->get(request);
0113     connect(res, &QNetworkReply::finished, this, &DeepLEngine::languagesUpdated);
0114 
0115     return true;
0116 }
0117 
0118 void
0119 DeepLEngine::languagesUpdated()
0120 {
0121     QNetworkReply *res = qobject_cast<QNetworkReply *>(sender());
0122     const bool isSource = res->request().url().query().endsWith($("=source"));
0123     res->deleteLater();
0124 
0125     if(res->error() == QNetworkReply::NoError) {
0126         if(isSource) {
0127             m_ui->langSource->clear();
0128             m_ui->langSource->addItem(i18n("Autodetect Language"), QString());
0129         } else {
0130             m_ui->langTranslation->clear();
0131         }
0132 
0133         const QJsonDocument doc = QJsonDocument::fromJson(res->readAll());
0134         const QJsonArray langs = doc.array();
0135         for(auto it = langs.cbegin(); it != langs.cend(); ++it) {
0136             const QJsonObject o = it->toObject();
0137             const QString langCode = o.value($("language")).toString();
0138             const QString langTitle = o.value($("name")).toString()/*QLocale(langCode).nativeLanguageName()*/;
0139             const QString ttl = langCode % $(" - ") % langTitle;
0140             if(isSource) {
0141                 m_ui->langSource->addItem(ttl, langCode);
0142                 if(langCode == SCConfig::dltLangSource())
0143                     m_ui->langSource->setCurrentIndex(m_ui->langSource->count() - 1);
0144             } else {
0145                 m_ui->langTranslation->addItem(ttl, langCode);
0146                 if(langCode == SCConfig::dltLangTrans())
0147                     m_ui->langTranslation->setCurrentIndex(m_ui->langTranslation->count() - 1);
0148             }
0149         }
0150         m_ui->grpSettings->setEnabled(true);
0151         emit engineReady(true);
0152     } else {
0153         showError(res);
0154     }
0155 }
0156 
0157 void
0158 DeepLEngine::translate(QVector<QString> &textLines)
0159 {
0160     SCConfig::setDltLangSource(m_ui->langSource->currentData().toString());
0161     SCConfig::setDltLangTrans(m_ui->langTranslation->currentData().toString());
0162 
0163     QNetworkRequest request($("https://%1/v2/translate").arg(SCConfig::dltApiDomain()));
0164     request.setRawHeader("Authorization", QByteArray("DeepL-Auth-Key ") + SCConfig::dltAuthKey().toUtf8());
0165     request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
0166 
0167     QUrlQuery commonPost;
0168     if(!SCConfig::dltLangSource().isEmpty())
0169         commonPost.addQueryItem($("source_lang"), SCConfig::dltLangSource());
0170     commonPost.addQueryItem($("target_lang"), SCConfig::dltLangTrans());
0171     const QByteArray commonPostData = commonPost.query(QUrl::FullyEncoded).toUtf8();
0172 
0173     int *reqCount = new int;
0174     auto translateDone = [=](){
0175         if(--(*reqCount) == 0) {
0176             delete reqCount;
0177             emit translated();
0178         }
0179     };
0180 
0181     int line = 0;
0182     *reqCount = 1;
0183     while(line != textLines.size()) {
0184         // Request body size must not exceed 128 KiB (128 ยท 1024 bytes)
0185         constexpr const int sizeLimit = 128 << 10;
0186         const int off = line;
0187 
0188         QByteArray postData = commonPostData;
0189         for(;;) {
0190             static const char key[] = "&text=";
0191             const QByteArray val = QUrl::toPercentEncoding(textLines.at(line));
0192             const QString &ln = textLines.at(line);
0193             if(postData.size() + sizeof(key) - 1 + ln.size() >= sizeLimit)
0194                 break;
0195             postData.append(key, sizeof(key) - 1).append(val);
0196             if(++line == textLines.size())
0197                 break;
0198         }
0199 
0200         (*reqCount)++;
0201         QNetworkReply *res = m_netManager->post(QNetworkRequest(request), postData);
0202         connect(res, &QNetworkReply::finished, this, [=, &textLines](){
0203             res->deleteLater();
0204             if(res->error() == QNetworkReply::NoError) {
0205                 const QJsonDocument doc = QJsonDocument::fromJson(res->readAll());
0206                 const QJsonArray tta = doc[$("translations")].toArray();
0207                 for(int i = 0, n = tta.size(); i < n; i++)
0208                     textLines[off + i] = tta.at(i).toObject().value($("text")).toString();
0209             } else {
0210                 showError(res);
0211             }
0212             translateDone();
0213         });
0214     }
0215     translateDone();
0216 }