File indexing completed on 2024-05-12 05:09:34
0001 /*************************************************************************** 0002 Copyright (C) 2005-2009 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 "gcstarpluginfetcher.h" 0026 #include "gcstarthread.h" 0027 #include "fetchmanager.h" 0028 #include "../collection.h" 0029 #include "../entry.h" 0030 #include "../translators/gcstarimporter.h" 0031 #include "../gui/combobox.h" 0032 #include "../gui/collectiontypecombo.h" 0033 #include "../utils/cursorsaver.h" 0034 #include "../core/filehandler.h" 0035 #include "../utils/guiproxy.h" 0036 #include "../tellico_debug.h" 0037 0038 #include <KConfigGroup> 0039 #include <KProcess> 0040 #include <KAcceleratorManager> 0041 #include <KShell> 0042 #include <KFilterDev> 0043 #include <KCompressionDevice> 0044 #include <KTar> 0045 #include <KLocalizedString> 0046 #include <karchive_version.h> 0047 0048 #include <QTemporaryDir> 0049 #include <QDir> 0050 #include <QLabel> 0051 #include <QShowEvent> 0052 #include <QGridLayout> 0053 #include <QBuffer> 0054 #include <QStandardPaths> 0055 0056 using namespace Tellico; 0057 using Tellico::Fetch::GCstarPluginFetcher; 0058 0059 GCstarPluginFetcher::CollectionPlugins GCstarPluginFetcher::collectionPlugins; 0060 GCstarPluginFetcher::PluginParse GCstarPluginFetcher::pluginParse = NotYet; 0061 0062 //static 0063 GCstarPluginFetcher::PluginList GCstarPluginFetcher::plugins(int collType_) { 0064 if(!collectionPlugins.contains(collType_)) { 0065 GUI::CursorSaver cs; 0066 QString gcstar = QStandardPaths::findExecutable(QStringLiteral("gcstar")); 0067 0068 if(pluginParse == NotYet) { 0069 KProcess proc; 0070 proc.setProgram(gcstar, QStringList() << QStringLiteral("--version")); 0071 proc.setOutputChannelMode(KProcess::OnlyStdoutChannel); 0072 // wait 5 seconds at most, just a sanity thing, never want to block completely 0073 if(proc.execute(5000) > -1) { 0074 QString output = QString::fromLocal8Bit(proc.readAllStandardOutput()); 0075 if(!output.isEmpty()) { 0076 // always going to be x.y[.z] ? 0077 static const QRegularExpression versionRx(QLatin1String("(\\d+)\\.(\\d+)(?:\\.(\\d+))?")); 0078 QRegularExpressionMatch m = versionRx.match(output); 0079 if(m.hasMatch()) { 0080 int x = m.captured(1).toInt(); 0081 int y = m.captured(2).toInt(); 0082 int z = m.captured(3).toInt(); // ok to be empty 0083 myDebug() << QStringLiteral("found %1.%2.%3").arg(x).arg(y).arg(z); 0084 // --list-plugins argument was added for 1.3 release 0085 pluginParse = (x >= 1 && y >=3) ? New : Old; 0086 } 0087 } 0088 } 0089 // if still zero, then we should use old in future 0090 if(pluginParse == NotYet) { 0091 pluginParse = Old; 0092 } 0093 } 0094 0095 if(pluginParse == New) { 0096 readPluginsNew(collType_, gcstar); 0097 } else { 0098 readPluginsOld(collType_, gcstar); 0099 } 0100 } 0101 0102 return collectionPlugins.contains(collType_) ? collectionPlugins.value(collType_) : GCstarPluginFetcher::PluginList(); 0103 } 0104 0105 void GCstarPluginFetcher::readPluginsNew(int collType_, const QString& gcstar_) { 0106 PluginList plugins; 0107 0108 const QString gcstarCollection = gcstarType(collType_); 0109 if(gcstarCollection.isEmpty()) { 0110 collectionPlugins.insert(collType_, plugins); 0111 return; 0112 } 0113 0114 QStringList args; 0115 args << QStringLiteral("--execute") 0116 << QStringLiteral("--list-plugins") 0117 << QStringLiteral("--collection") 0118 << gcstarCollection; 0119 0120 KProcess proc; 0121 proc.setProgram(gcstar_, args); 0122 proc.setOutputChannelMode(KProcess::OnlyStdoutChannel); 0123 if(proc.execute() < 0) { 0124 myWarning() << "can't start"; 0125 return; 0126 } 0127 0128 bool hasName = false; 0129 PluginInfo info; 0130 QTextStream stream(&proc); 0131 for(QString line = stream.readLine(); !stream.atEnd(); line = stream.readLine()) { 0132 if(line.isEmpty()) { 0133 if(hasName) { 0134 plugins << info; 0135 } 0136 hasName = false; 0137 info.clear(); 0138 } else { 0139 // authors have \t at beginning 0140 line = line.trimmed(); 0141 if(!hasName) { 0142 info.insert(QStringLiteral("name"), line); 0143 hasName = true; 0144 } else { 0145 info.insert(QStringLiteral("author"), line); 0146 } 0147 // myDebug() << line; 0148 } 0149 } 0150 0151 collectionPlugins.insert(collType_, plugins); 0152 } 0153 0154 void GCstarPluginFetcher::readPluginsOld(int collType_, const QString& gcstar_) { 0155 QDir dir(gcstar_, QStringLiteral("GC*.pm")); 0156 dir.cd(QStringLiteral("../../lib/gcstar/GCPlugins/")); 0157 0158 static const QRegularExpression rx(QLatin1String("get(Name|Author|Lang)\\s*\\{\\s*return\\s+['\"](.+?)['\"]")); 0159 0160 PluginList plugins; 0161 0162 const QString dirName = gcstarType(collType_); 0163 if(dirName.isEmpty()) { 0164 collectionPlugins.insert(collType_, plugins); 0165 return; 0166 } 0167 0168 foreach(const QString& file, dir.entryList()) { 0169 QUrl u = QUrl::fromLocalFile(dir.filePath(file)); 0170 PluginInfo info; 0171 QString text = FileHandler::readTextFile(u); 0172 QRegularExpressionMatchIterator i = rx.globalMatch(text); 0173 while(i.hasNext()) { 0174 QRegularExpressionMatch match = i.next(); 0175 info.insert(match.captured(1).toLower(), match.captured(2)); 0176 } 0177 // only add if it has a name 0178 if(info.contains(QStringLiteral("name"))) { 0179 plugins << info; 0180 } 0181 } 0182 // inserting empty list is ok 0183 collectionPlugins.insert(collType_, plugins); 0184 } 0185 0186 QString GCstarPluginFetcher::gcstarType(int collType_) { 0187 switch(collType_) { 0188 case Data::Collection::Book: return QStringLiteral("GCbooks"); 0189 case Data::Collection::Video: return QStringLiteral("GCfilms"); 0190 case Data::Collection::Album: return QStringLiteral("GCmusics"); 0191 case Data::Collection::ComicBook: return QStringLiteral("GCcomics"); 0192 case Data::Collection::Wine: return QStringLiteral("GCwines"); 0193 case Data::Collection::Coin: return QStringLiteral("GCcoins"); 0194 case Data::Collection::Stamp: return QStringLiteral("GCstamps"); 0195 case Data::Collection::Game: return QStringLiteral("GCgames"); 0196 case Data::Collection::BoardGame: return QStringLiteral("GCboardgames"); 0197 default: break; 0198 } 0199 return QString(); 0200 } 0201 0202 GCstarPluginFetcher::GCstarPluginFetcher(QObject* parent_) : Fetcher(parent_), 0203 m_started(false), m_collType(-1), m_thread(nullptr) { 0204 } 0205 0206 GCstarPluginFetcher::~GCstarPluginFetcher() { 0207 if(m_thread) { 0208 if(m_thread->isRunning()) { 0209 m_thread->terminate(); 0210 m_thread->wait(); 0211 } 0212 delete m_thread; 0213 } 0214 } 0215 0216 QString GCstarPluginFetcher::source() const { 0217 return m_name; 0218 } 0219 0220 bool GCstarPluginFetcher::canFetch(int type_) const { 0221 return m_collType == -1 ? false : m_collType == type_; 0222 } 0223 0224 void GCstarPluginFetcher::readConfigHook(const KConfigGroup& config_) { 0225 m_collType = config_.readEntry("CollectionType", -1); 0226 m_plugin = config_.readEntry("Plugin"); 0227 } 0228 0229 void GCstarPluginFetcher::search() { 0230 m_started = true; 0231 if(m_plugin.isEmpty() || m_collType == -1) { 0232 myWarning() << "no plugin information!"; 0233 myDebug() << m_collType << m_plugin; 0234 stop(); 0235 return; 0236 } 0237 0238 m_data.clear(); 0239 0240 const QString gcstar = QStandardPaths::findExecutable(QStringLiteral("gcstar")); 0241 if(gcstar.isEmpty()) { 0242 myWarning() << "gcstar not found!"; 0243 stop(); 0244 return; 0245 } 0246 0247 QStringList args; 0248 args << QStringLiteral("--execute") 0249 << QStringLiteral("--collection") << gcstarType(m_collType) 0250 << QStringLiteral("--export") << QStringLiteral("TarGz") 0251 << QStringLiteral("--exportprefs") << QStringLiteral("collection=>/tmp/test.gcs,file=>/tmp/test1.tar.gz") 0252 << QStringLiteral("--website") << m_plugin 0253 << QStringLiteral("--download") << KShell::quoteArg(request().value()); 0254 myLog() << args; 0255 0256 m_thread = new GCstarThread(this); 0257 m_thread->setProgram(gcstar, args); 0258 connect(m_thread, &GCstarThread::standardOutput, this, &GCstarPluginFetcher::slotData); 0259 connect(m_thread, &GCstarThread::standardError, this, &GCstarPluginFetcher::slotError); 0260 connect(m_thread, &QThread::finished, this, &GCstarPluginFetcher::slotProcessExited); 0261 m_thread->start(); 0262 } 0263 0264 void GCstarPluginFetcher::stop() { 0265 if(!m_started) { 0266 return; 0267 } 0268 if(m_thread) { 0269 if(m_thread->isRunning()) { 0270 m_thread->terminate(); 0271 m_thread->wait(); 0272 } 0273 delete m_thread; 0274 m_thread = nullptr; 0275 } 0276 m_data.clear(); 0277 m_started = false; 0278 m_errors.clear(); 0279 emit signalDone(this); 0280 } 0281 0282 void GCstarPluginFetcher::slotData(const QByteArray& data_) { 0283 m_data.append(data_); 0284 } 0285 0286 void GCstarPluginFetcher::slotError(const QByteArray& data_) { 0287 QString msg = QString::fromLocal8Bit(data_); 0288 msg.prepend(source() + QLatin1String(": ")); 0289 myDebug() << msg; 0290 m_errors << msg; 0291 } 0292 0293 void GCstarPluginFetcher::slotProcessExited() { 0294 // if stop() is called and the thread terminated 0295 // the finished() signal will still fire 0296 if(!m_started) { 0297 return; 0298 } 0299 0300 if(!m_errors.isEmpty()) { 0301 message(m_errors.join(QLatin1String("\n")), MessageHandler::Warning); 0302 } 0303 0304 if(m_data.isEmpty()) { 0305 myDebug() << source() << ": no data"; 0306 stop(); 0307 return; 0308 } 0309 0310 QBuffer filterBuffer(&m_data); 0311 #if KARCHIVE_VERSION >= QT_VERSION_CHECK(5,85,0) 0312 auto compressionType = KCompressionDevice::compressionTypeForMimeType(QStringLiteral("application/x-gzip")); 0313 #else 0314 auto compressionType = KFilterDev::compressionTypeForMimeType(QStringLiteral("application/x-gzip")); 0315 #endif 0316 KCompressionDevice filter(&filterBuffer, false, compressionType); 0317 if(!filter.open(QIODevice::ReadOnly)) { 0318 myWarning() << "unable to open gzip filter"; 0319 stop(); 0320 return; 0321 } 0322 0323 QByteArray tarData = filter.readAll(); 0324 QBuffer buffer(&tarData); 0325 0326 KTar tar(&buffer); 0327 if(!tar.open(QIODevice::ReadOnly)) { 0328 myWarning() << "unable to open tar file"; 0329 stop(); 0330 return; 0331 } 0332 0333 const KArchiveDirectory* dir = tar.directory(); 0334 if(!dir) { 0335 myWarning() << "unable to open tar directory"; 0336 stop(); 0337 return; 0338 } 0339 0340 QTemporaryDir tempDir; 0341 dir->copyTo(tempDir.path()); 0342 0343 // KDE seems to have a bug (#252821) for gcstar files where the images are not in the images/ directory 0344 foreach(const QString& filename, dir->entries()) { 0345 if(dir->entry(filename)->isFile() && filename != QLatin1String("collection.gcs")) { 0346 const KArchiveFile* f = static_cast<const KArchiveFile*>(dir->entry(filename)); 0347 f->copyTo(tempDir.path() + QLatin1String("/images")); 0348 } 0349 } 0350 0351 QUrl gcsUrl = QUrl::fromLocalFile(tempDir.path()); 0352 gcsUrl = gcsUrl.adjusted(QUrl::StripTrailingSlash); 0353 gcsUrl.setPath(gcsUrl.path() + QLatin1String("/collection.gcs")); 0354 0355 Import::GCstarImporter imp(gcsUrl); 0356 imp.setHasRelativeImageLinks(true); 0357 0358 Data::CollPtr coll = imp.collection(); 0359 if(!coll) { 0360 if(!imp.statusMessage().isEmpty()) { 0361 message(imp.statusMessage(), MessageHandler::Status); 0362 } 0363 myWarning() << "no collection pointer"; 0364 stop(); 0365 return; 0366 } 0367 0368 foreach(Data::EntryPtr entry, coll->entries()) { 0369 FetchResult* r = new FetchResult(this, entry); 0370 m_entries.insert(r->uid, entry); 0371 emit signalResultFound(r); 0372 if(!m_started) { 0373 return; 0374 } 0375 } 0376 stop(); // be sure to call this 0377 } 0378 0379 Tellico::Data::EntryPtr GCstarPluginFetcher::fetchEntryHook(uint uid_) { 0380 return m_entries[uid_]; 0381 } 0382 0383 Tellico::Fetch::FetchRequest GCstarPluginFetcher::updateRequest(Data::EntryPtr entry_) { 0384 // ry searching for title and rely on Collection::sameEntry() to figure things out 0385 QString t = entry_->field(QStringLiteral("title")); 0386 if(!t.isEmpty()) { 0387 return FetchRequest(Fetch::Title, t); 0388 } 0389 return FetchRequest(); 0390 } 0391 0392 Tellico::Fetch::ConfigWidget* GCstarPluginFetcher::configWidget(QWidget* parent_) const { 0393 return new GCstarPluginFetcher::ConfigWidget(parent_, this); 0394 } 0395 0396 QString GCstarPluginFetcher::defaultName() { 0397 return i18n("GCstar Plugin"); 0398 } 0399 0400 QString GCstarPluginFetcher::defaultIcon() { 0401 return QStringLiteral("gcstar"); 0402 } 0403 0404 GCstarPluginFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GCstarPluginFetcher* fetcher_/*=0*/) 0405 : Fetch::ConfigWidget(parent_), m_needPluginList(true) { 0406 QGridLayout* l = new QGridLayout(optionsWidget()); 0407 l->setSpacing(4); 0408 l->setColumnStretch(1, 10); 0409 0410 int row = -1; 0411 0412 QLabel* label = new QLabel(i18n("Collection &type:"), optionsWidget()); 0413 l->addWidget(label, ++row, 0); 0414 m_collCombo = new GUI::CollectionTypeCombo(optionsWidget()); 0415 void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; 0416 connect(m_collCombo, activatedInt, this, &ConfigWidget::slotSetModified); 0417 connect(m_collCombo, activatedInt, this, &ConfigWidget::slotTypeChanged); 0418 l->addWidget(m_collCombo, row, 1, 1, 3); 0419 QString w = i18n("Set the collection type of the data returned from the plugin."); 0420 label->setWhatsThis(w); 0421 m_collCombo->setWhatsThis(w); 0422 label->setBuddy(m_collCombo); 0423 0424 label = new QLabel(i18n("&Plugin: "), optionsWidget()); 0425 l->addWidget(label, ++row, 0); 0426 m_pluginCombo = new GUI::ComboBox(optionsWidget()); 0427 connect(m_pluginCombo, activatedInt, this, &ConfigWidget::slotSetModified); 0428 connect(m_pluginCombo, activatedInt, this, &ConfigWidget::slotPluginChanged); 0429 l->addWidget(m_pluginCombo, row, 1, 1, 3); 0430 w = i18n("Select the GCstar plugin used for the data source."); 0431 label->setWhatsThis(w); 0432 m_pluginCombo->setWhatsThis(w); 0433 label->setBuddy(m_pluginCombo); 0434 0435 label = new QLabel(i18n("Author: "), optionsWidget()); 0436 l->addWidget(label, ++row, 0); 0437 m_authorLabel = new QLabel(optionsWidget()); 0438 l->addWidget(m_authorLabel, row, 1); 0439 0440 if(fetcher_) { 0441 if(fetcher_->m_collType > -1) { 0442 m_collCombo->setCurrentType(fetcher_->m_collType); 0443 } else { 0444 m_collCombo->setCurrentType(fetcher_->collectionType()); 0445 } 0446 m_originalPluginName = fetcher_->m_plugin; 0447 } else { 0448 // default to Book for now 0449 m_collCombo->setCurrentType(Data::Collection::Book); 0450 } 0451 0452 KAcceleratorManager::manage(optionsWidget()); 0453 } 0454 0455 GCstarPluginFetcher::ConfigWidget::~ConfigWidget() { 0456 } 0457 0458 void GCstarPluginFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 0459 config_.writeEntry("CollectionType", m_collCombo->currentType()); 0460 config_.writeEntry("Plugin", m_pluginCombo->currentText()); 0461 } 0462 0463 QString GCstarPluginFetcher::ConfigWidget::preferredName() const { 0464 QString plugin = m_pluginCombo->currentText(); 0465 return plugin.isEmpty() ? plugin : QLatin1String("GCstar - ") + plugin; 0466 } 0467 0468 void GCstarPluginFetcher::ConfigWidget::slotTypeChanged() { 0469 int collType = m_collCombo->currentType(); 0470 m_pluginCombo->clear(); 0471 QStringList pluginNames; 0472 GCstarPluginFetcher::PluginList list = GCstarPluginFetcher::plugins(collType); 0473 foreach(const GCstarPluginFetcher::PluginInfo& info, list) { 0474 pluginNames << info.value(QStringLiteral("name")).toString(); 0475 m_pluginCombo->addItem(pluginNames.last(), info); 0476 } 0477 slotPluginChanged(); 0478 emit signalName(preferredName()); 0479 } 0480 0481 void GCstarPluginFetcher::ConfigWidget::slotPluginChanged() { 0482 PluginInfo info = m_pluginCombo->currentData().toHash(); 0483 m_authorLabel->setText(info[QStringLiteral("author")].toString()); 0484 emit signalName(preferredName()); 0485 } 0486 0487 void GCstarPluginFetcher::ConfigWidget::showEvent(QShowEvent*) { 0488 if(m_needPluginList) { 0489 m_needPluginList = false; 0490 slotTypeChanged(); // update plugin combo box 0491 if(!m_originalPluginName.isEmpty()) { 0492 m_pluginCombo->setEditText(m_originalPluginName); 0493 slotPluginChanged(); 0494 } 0495 } 0496 }