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 }