File indexing completed on 2024-05-12 16:46:02
0001 /*************************************************************************** 0002 Copyright (C) 2013-2019 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include <config.h> 0026 #include "vndbfetcher.h" 0027 #include "../collections/gamecollection.h" 0028 #include "../images/imagefactory.h" 0029 #include "../utils/guiproxy.h" 0030 #include "../utils/string_utils.h" 0031 #include "../entry.h" 0032 #include "../fieldformat.h" 0033 #include "../core/filehandler.h" 0034 #include "../tellico_debug.h" 0035 0036 #include <KLocalizedString> 0037 0038 #include <QTcpSocket> 0039 #include <QLabel> 0040 #include <QFile> 0041 #include <QTextStream> 0042 #include <QGridLayout> 0043 #include <QTextCodec> 0044 #include <QJsonDocument> 0045 #include <QJsonObject> 0046 #include <QJsonValue> 0047 #include <QRegularExpression> 0048 #include <QTimer> 0049 0050 namespace { 0051 static const char* VNDB_HOSTNAME = "api.vndb.org"; 0052 static const int VNDB_PORT = 19534; 0053 } 0054 0055 using namespace Tellico; 0056 using Tellico::Fetch::VNDBFetcher; 0057 0058 VNDBFetcher::VNDBFetcher(QObject* parent_) 0059 : Fetcher(parent_), m_started(false), m_socket(nullptr), m_isConnected(false), m_state(PreLogin) { 0060 } 0061 0062 VNDBFetcher::~VNDBFetcher() { 0063 if(m_socket && m_isConnected) { 0064 m_socket->disconnectFromHost(); 0065 } 0066 delete m_socket; 0067 m_socket = nullptr; 0068 } 0069 0070 QString VNDBFetcher::source() const { 0071 return m_name.isEmpty() ? defaultName() : m_name; 0072 } 0073 0074 bool VNDBFetcher::canSearch(Fetch::FetchKey k) const { 0075 return k == Title; 0076 } 0077 0078 bool VNDBFetcher::canFetch(int type) const { 0079 return type == Data::Collection::Game; 0080 } 0081 0082 void VNDBFetcher::readConfigHook(const KConfigGroup&) { 0083 } 0084 0085 void VNDBFetcher::search() { 0086 m_started = true; 0087 m_data.clear(); 0088 0089 if(!m_socket) { 0090 m_socket = new QTcpSocket(this); 0091 QObject::connect(m_socket, &QTcpSocket::readyRead, this, &VNDBFetcher::slotRead); 0092 QObject::connect(m_socket, &QTcpSocket::stateChanged, this, &VNDBFetcher::slotState); 0093 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) 0094 // see https://wiki.qt.io/New_Signal_Slot_Syntax#Overload for why this is necessary 0095 void (QTcpSocket::* errorSignal)(QAbstractSocket::SocketError) = &QAbstractSocket::error; 0096 QObject::connect(m_socket, errorSignal, this, &VNDBFetcher::slotError); 0097 #else 0098 QObject::connect(m_socket, &QTcpSocket::errorOccurred, this, &VNDBFetcher::slotError); 0099 #endif 0100 } 0101 if(!m_isConnected) { 0102 m_socket->connectToHost(QLatin1String(VNDB_HOSTNAME), VNDB_PORT); 0103 } 0104 0105 //the client ver only wants digits, I think? 0106 QString clientVersion(QStringLiteral(TELLICO_VERSION)); 0107 clientVersion.remove(QRegularExpression(QStringLiteral("[^0-9.]"))); 0108 0109 QByteArray login = "login {" 0110 "\"protocol\":1," 0111 "\"client\":\"Tellico\"," 0112 "\"clientver\": \""; 0113 login += clientVersion.toUtf8() + "\"}"; 0114 login.append(0x04); 0115 if(m_socket->waitForConnected()) { 0116 // myDebug() << login; 0117 m_socket->write(login); 0118 m_socket->waitForReadyRead(5000); 0119 if(m_state == PreLogin) { 0120 // login did not work 0121 stop(); 0122 return; 0123 } 0124 } 0125 0126 QByteArray get("get vn basic,details "); 0127 switch(request().key()) { 0128 case Title: 0129 get += "(search ~ \"" + request().value().toUtf8() + "\")"; 0130 break; 0131 0132 default: 0133 myWarning() << "key not recognized:" << request().key(); 0134 stop(); 0135 return; 0136 } 0137 0138 m_state = GetVN; 0139 // myDebug() << get << m_state; 0140 get.append(0x04); 0141 m_socket->write(get); 0142 } 0143 0144 void VNDBFetcher::stop() { 0145 if(!m_started) { 0146 return; 0147 } 0148 m_started = false; 0149 emit signalDone(this); 0150 } 0151 0152 Tellico::Data::EntryPtr VNDBFetcher::fetchEntryHook(uint uid_) { 0153 Data::EntryPtr entry = m_entries.value(uid_); 0154 if(!entry) { 0155 myWarning() << "no entry in dict"; 0156 return Data::EntryPtr(); 0157 } 0158 0159 // clear past data 0160 m_data.clear(); 0161 // don't need anything for the "details" group 0162 QByteArray get("get release basic,producers "); 0163 get += "(vn = " + entry->field(QStringLiteral("vn-id")).toLatin1() 0164 + " and patch = false and type = \"complete\")"; 0165 0166 m_state = GetRelease; 0167 // myDebug() << get << m_state; 0168 get.append(0x04); 0169 m_socket->write(get); 0170 // 5 sec is long enough? 0171 m_socket->waitForReadyRead(5000); 0172 0173 if(verifyData()) { 0174 parseReleaseResults(entry); 0175 } 0176 0177 // image might still be a URL 0178 const QString image_id = entry->field(QStringLiteral("cover")); 0179 if(image_id.contains(QLatin1Char('/'))) { 0180 const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); 0181 if(id.isEmpty()) { 0182 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0183 } 0184 // empty image ID is ok 0185 entry->setField(QStringLiteral("cover"), id); 0186 } 0187 // clear the placeholder fields 0188 entry->setField(QStringLiteral("vn-id"), QString()); 0189 0190 if(m_socket->isValid()) { 0191 m_socket->disconnectFromHost(); 0192 m_state = PreLogin; 0193 } 0194 0195 return entry; 0196 } 0197 0198 Tellico::Fetch::FetchRequest VNDBFetcher::updateRequest(Data::EntryPtr entry_) { 0199 const QString title = entry_->field(QStringLiteral("title")); 0200 if(!title.isEmpty()) { 0201 return FetchRequest(Title, title); 0202 } 0203 return FetchRequest(); 0204 } 0205 0206 bool VNDBFetcher::verifyData() { 0207 if(m_data.isEmpty()) { 0208 myDebug() << "no data"; 0209 return false; 0210 } 0211 0212 // remove the last hex character 0213 m_data.chop(1); 0214 0215 if(m_data.startsWith("error")) { //krazy:exclude=strings 0216 QJsonDocument doc = QJsonDocument::fromJson(m_data.mid(5)); 0217 QVariantMap result = doc.object().toVariantMap(); 0218 if(result.contains(QStringLiteral("msg"))) { 0219 myDebug() << "Data error:" << result.value(QStringLiteral("msg")).toString(); 0220 message(result.value(QStringLiteral("msg")).toString(), MessageHandler::Error); 0221 } 0222 return false; 0223 } 0224 0225 // myDebug() << m_data; 0226 if(m_state == PreLogin) { 0227 if(m_data.startsWith("ok")) { //krazy:exclude=strings 0228 m_state = PostLogin; 0229 m_data.clear(); // reset data buffer 0230 return true; 0231 } 0232 // some sort of error 0233 myDebug() << "Failed to login properly..."; 0234 return false; 0235 } 0236 0237 if(!m_data.startsWith("results")) { //krazy:exclude=strings 0238 myDebug() << "Expecting results and didn't get them!"; 0239 return false; 0240 } 0241 0242 // chop off "results" 0243 m_data = m_data.mid(7); 0244 return true; 0245 } 0246 0247 void VNDBFetcher::parseVNResults() { 0248 if(!verifyData()) { 0249 return; 0250 } 0251 #if 0 0252 myWarning() << "Remove debug from vndbfetcher.cpp"; 0253 QFile f(QString::fromLatin1("/tmp/vndbtest.json")); 0254 if(f.open(QIODevice::WriteOnly)) { 0255 QTextStream t(&f); 0256 t.setCodec("UTF-8"); 0257 t << m_data; 0258 } 0259 f.close(); 0260 #endif 0261 0262 QJsonParseError jsonError; 0263 QJsonDocument doc = QJsonDocument::fromJson(m_data, &jsonError); 0264 if(doc.isNull()) { 0265 myDebug() << "null JSON document:" << jsonError.errorString(); 0266 message(jsonError.errorString(), MessageHandler::Error); 0267 stop(); 0268 return; 0269 } 0270 QVariantMap topResultMap = doc.object().toVariantMap(); 0271 QVariantList resultList = topResultMap.value(QStringLiteral("items")).toList(); 0272 if(resultList.isEmpty()) { 0273 myDebug() << "no results"; 0274 stop(); 0275 return; 0276 } 0277 0278 Data::CollPtr coll(new Data::GameCollection(true)); 0279 // placeholder for vn id, to be removed later 0280 Data::FieldPtr f1(new Data::Field(QStringLiteral("vn-id"), QString(), Data::Field::Number)); 0281 coll->addField(f1); 0282 // add new fields 0283 if(optionalFields().contains(QStringLiteral("origtitle"))) { 0284 Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); 0285 f->setFormatType(FieldFormat::FormatTitle); 0286 coll->addField(f); 0287 } 0288 if(optionalFields().contains(QStringLiteral("alias"))) { 0289 Data::FieldPtr f(new Data::Field(QStringLiteral("alias"), i18n("Alias"))); 0290 f->setFlags(Data::Field::AllowMultiple); 0291 f->setFormatType(FieldFormat::FormatTitle); 0292 coll->addField(f); 0293 } 0294 0295 QVariantMap resultMap; 0296 foreach(const QVariant& result, resultList) { 0297 // be sure to check that the fetcher has not been stopped 0298 // crashes can occur if not 0299 if(!m_started) { 0300 break; 0301 } 0302 resultMap = result.toMap(); 0303 0304 Data::EntryPtr entry(new Data::Entry(coll)); 0305 entry->setField(QStringLiteral("title"), mapValue(resultMap, "title")); 0306 entry->setField(QStringLiteral("vn-id"), mapValue(resultMap, "id")); 0307 entry->setField(QStringLiteral("year"), mapValue(resultMap, "released").left(4)); 0308 entry->setField(QStringLiteral("genre"), i18n("Visual Novel")); 0309 entry->setField(QStringLiteral("description"), mapValue(resultMap, "description")); 0310 entry->setField(QStringLiteral("cover"), mapValue(resultMap, "image")); 0311 if(optionalFields().contains(QStringLiteral("origtitle"))) { 0312 entry->setField(QStringLiteral("origtitle"), mapValue(resultMap, "original")); 0313 } 0314 if(optionalFields().contains(QStringLiteral("alias"))) { 0315 const QString aliases = mapValue(resultMap, "aliases"); 0316 entry->setField(QStringLiteral("alias"), aliases.split(QStringLiteral("\n")).join(FieldFormat::delimiterString())); 0317 } 0318 0319 FetchResult* r = new FetchResult(this, entry); 0320 m_entries.insert(r->uid, entry); 0321 emit signalResultFound(r); 0322 } 0323 0324 // m_start = m_entries.count(); 0325 // m_hasMoreResults = m_start <= m_total; 0326 m_hasMoreResults = false; // for now, no continued searches 0327 stop(); 0328 } 0329 0330 void VNDBFetcher::parseReleaseResults(Data::EntryPtr entry_) { 0331 #if 0 0332 myWarning() << "Remove debug2 from vndbfetcher.cpp"; 0333 QFile f(QString::fromLatin1("/tmp/vndb-release.json")); 0334 if(f.open(QIODevice::WriteOnly)) { 0335 QTextStream t(&f); 0336 t.setCodec("UTF-8"); 0337 t << m_data; 0338 } 0339 f.close(); 0340 #endif 0341 0342 QJsonParseError jsonError; 0343 QJsonDocument doc = QJsonDocument::fromJson(m_data, &jsonError); 0344 if(doc.isNull()) { 0345 myDebug() << "null JSON document:" << jsonError.errorString(); 0346 message(jsonError.errorString(), MessageHandler::Error); 0347 return; 0348 } 0349 QVariantMap topResultMap = doc.object().toVariantMap(); 0350 QVariantList resultList = topResultMap.value(QStringLiteral("items")).toList(); 0351 if(resultList.isEmpty()) { 0352 myDebug() << "no results"; 0353 return; 0354 } 0355 0356 // only work on the first release item 0357 const QVariantMap resultMap = resultList.at(0).toMap(); 0358 0359 QStringList pubs, devs; 0360 QVariantList producerList = resultMap.value(QStringLiteral("producers")).toList(); 0361 foreach(const QVariant& producerObject, producerList) { 0362 const QVariantMap producerMap = producerObject.toMap(); 0363 if(producerMap.value(QStringLiteral("publisher")) == true) { 0364 pubs += mapValue(producerMap, "name"); 0365 } 0366 if(producerMap.value(QStringLiteral("developer")) == true) { 0367 devs += mapValue(producerMap, "name"); 0368 } 0369 } 0370 entry_->setField(QStringLiteral("publisher"), pubs.join(FieldFormat::delimiterString())); 0371 entry_->setField(QStringLiteral("developer"), devs.join(FieldFormat::delimiterString())); 0372 0373 // update release year 0374 entry_->setField(QStringLiteral("year"), mapValue(resultMap, "released").left(4)); 0375 } 0376 0377 void VNDBFetcher::slotState() { 0378 if(!m_socket) { 0379 return; 0380 } 0381 if(m_socket->state() == QAbstractSocket::ConnectedState) { 0382 m_isConnected = true; 0383 } else if(m_socket->state() == QAbstractSocket::UnconnectedState) { 0384 m_isConnected = false; 0385 } 0386 } 0387 0388 void VNDBFetcher::slotError() { 0389 if(!m_socket) { 0390 return; 0391 } 0392 myDebug() << "Socket error:" << m_socket->errorString(); 0393 } 0394 0395 void VNDBFetcher::slotRead() { 0396 m_data += m_socket->readAll(); 0397 0398 // check the last character, if it's not the hex character, continue waiting 0399 if(m_socket->atEnd() && m_data.endsWith(0x04)) { 0400 switch(m_state) { 0401 case PreLogin: 0402 verifyData(); 0403 break; 0404 case GetVN: 0405 // necessary for passing unit test 0406 QTimer::singleShot(0, this, &VNDBFetcher::parseVNResults); 0407 break; 0408 case GetRelease: 0409 // handled asynch 0410 break; 0411 case PostLogin: 0412 default: 0413 myDebug() << "Unexpected state in slotRead"; 0414 } 0415 } 0416 } 0417 0418 Tellico::Fetch::ConfigWidget* VNDBFetcher::configWidget(QWidget* parent_) const { 0419 return new VNDBFetcher::ConfigWidget(parent_, this); 0420 } 0421 0422 QString VNDBFetcher::defaultName() { 0423 return QStringLiteral("The Visual Novel Database"); // no translation 0424 } 0425 0426 QString VNDBFetcher::defaultIcon() { 0427 return favIcon("https://vndb.org"); 0428 } 0429 0430 Tellico::StringHash VNDBFetcher::allOptionalFields() { 0431 StringHash hash; 0432 hash[QStringLiteral("origtitle")] = i18n("Original Title"); 0433 hash[QStringLiteral("alias")] = i18n("Alias"); 0434 return hash; 0435 } 0436 0437 VNDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const VNDBFetcher* fetcher_) 0438 : Fetch::ConfigWidget(parent_) { 0439 QVBoxLayout* l = new QVBoxLayout(optionsWidget()); 0440 l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); 0441 l->addStretch(); 0442 0443 // now add additional fields widget 0444 addFieldsWidget(VNDBFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 0445 } 0446 0447 void VNDBFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { 0448 } 0449 0450 QString VNDBFetcher::ConfigWidget::preferredName() const { 0451 return VNDBFetcher::defaultName(); 0452 }