File indexing completed on 2024-05-12 05:10:08

0001 /***************************************************************************
0002     Copyright (C) 2014 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 "boardgamegeekimporter.h"
0026 #include "../collections/boardgamecollection.h"
0027 #include "xslthandler.h"
0028 #include "tellicoimporter.h"
0029 #include "../core/filehandler.h"
0030 #include "../utils/datafileregistry.h"
0031 #include "../tellico_debug.h"
0032 
0033 #include <KSharedConfig>
0034 #include <KConfigGroup>
0035 #include <KLocalizedString>
0036 
0037 #include <QLineEdit>
0038 #include <QVBoxLayout>
0039 #include <QFormLayout>
0040 #include <QGroupBox>
0041 #include <QCheckBox>
0042 #include <QDomDocument>
0043 #include <QFile>
0044 #include <QApplication>
0045 #include <QUrlQuery>
0046 #include <QThread>
0047 #include <QElapsedTimer>
0048 
0049 namespace {
0050   static const char* BGG_THING_URL  = "http://boardgamegeek.com/xmlapi2/thing";
0051   static const char* BGG_COLLECTION_URL = "http://boardgamegeek.com/xmlapi2/collection";
0052   static int BGG_STEPSIZE = 25;
0053 }
0054 
0055 using Tellico::Import::BoardGameGeekImporter;
0056 
0057 BoardGameGeekImporter::BoardGameGeekImporter() : Import::Importer(), m_cancelled(false), m_widget(nullptr)
0058     , m_userEdit(nullptr), m_checkOwned(nullptr) {
0059   QString xsltFile = DataFileRegistry::self()->locate(QStringLiteral("boardgamegeek2tellico.xsl"));
0060   if(!xsltFile.isEmpty()) {
0061     m_xsltURL = QUrl::fromLocalFile(xsltFile);
0062   } else {
0063     myWarning() << "unable to find boardgamegeek2tellico.xsl!";
0064   }
0065 
0066   KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ImportOptions - BoardGameGeek"));
0067   m_user = config.readEntry("User ID");
0068   m_ownedOnly = config.readEntry("Owned", false);
0069 }
0070 
0071 bool BoardGameGeekImporter::canImport(int type) const {
0072   return type == Data::Collection::BoardGame;
0073 }
0074 
0075 Tellico::Data::CollPtr BoardGameGeekImporter::collection() {
0076   if(m_coll) {
0077     return m_coll;
0078   }
0079 
0080   if(m_xsltURL.isEmpty() || !m_xsltURL.isValid()) {
0081     setStatusMessage(i18n("A valid XSLT file is needed to import the file."));
0082     return Data::CollPtr();
0083   }
0084 
0085   if(!m_widget) {
0086     myWarning() << "no widget!";
0087     return Data::CollPtr();
0088   }
0089 
0090   m_user = m_userEdit->text().trimmed();
0091   if(m_user.isEmpty()) {
0092     setStatusMessage(i18n("A valid user ID must be entered."));
0093     return Data::CollPtr();
0094   }
0095 
0096   XSLTHandler handler(m_xsltURL);
0097   if(!handler.isValid()) {
0098     setStatusMessage(i18n("Tellico encountered an error in XSLT processing."));
0099     return Data::CollPtr();
0100   }
0101 
0102   m_ownedOnly = m_checkOwned->isChecked();
0103 
0104   // first get the bgg id list
0105   QUrl u(QString::fromLatin1(BGG_COLLECTION_URL));
0106   QUrlQuery q;
0107   q.addQueryItem(QStringLiteral("username"), m_user);
0108   q.addQueryItem(QStringLiteral("subtype"), QStringLiteral("boardgame"));
0109   q.addQueryItem(QStringLiteral("brief"), QStringLiteral("1"));
0110   if(m_ownedOnly) {
0111     q.addQueryItem(QStringLiteral("own"), QStringLiteral("1"));
0112   }
0113   u.setQuery(q);
0114 
0115   QStringList idList;
0116   QDomDocument dom = FileHandler::readXMLDocument(u, false, true);
0117   // could return HTTP 202 while the caching system generates the file
0118   // see https://boardgamegeek.com/thread/1188687/export-collections-has-been-updated-xmlapi-develop
0119   // also has a root node of message. Try 5 times, waiting by 2 seconds each time
0120   bool hasMessage = dom.documentElement().tagName() == QStringLiteral("message");
0121   for(int loopCount = 0; hasMessage && loopCount < 5; ++loopCount) {
0122     // wait 2 seconds and try again
0123     QElapsedTimer timer;
0124     timer.start();
0125     while(timer.elapsed() < 2000) {
0126       QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
0127       QThread::msleep(500);
0128     }
0129     dom = FileHandler::readXMLDocument(u, false, true);
0130     hasMessage = dom.documentElement().tagName() == QStringLiteral("message");
0131   }
0132 
0133   if(hasMessage) {
0134     myDebug() << "BoardGameGeekImporter still has message and no collection";
0135 #if 0
0136     myWarning() << "Remove debug from boardgamegeekimporter.cpp";
0137     QFile f(QStringLiteral("/tmp/bgg-message.xml"));
0138     if(f.open(QIODevice::WriteOnly)) {
0139       QTextStream t(&f);
0140       t.setCodec("UTF-8");
0141       t << dom.toString();
0142     }
0143     f.close();
0144 #endif
0145   }
0146 
0147   QDomNodeList items = dom.documentElement().elementsByTagName(QStringLiteral("item"));
0148   for(int i = 0; i < items.count(); ++i) {
0149     if(!items.at(i).isElement()) {
0150       continue;
0151     }
0152     const QString id = items.at(i).toElement().attribute(QStringLiteral("objectid"));
0153     if(!id.isEmpty()) {
0154       idList += id;
0155     }
0156   }
0157 
0158   if(idList.isEmpty()) {
0159     myLog() << "No items found in BGG collection";
0160     return Data::CollPtr();
0161   }
0162 
0163   const bool showProgress = options() & ImportProgress;
0164   if(showProgress) {
0165     // use 10 as the amount for reading the ids
0166     emit signalTotalSteps(this, 10 + 100);
0167     emit signalProgress(this, 10);
0168   }
0169 
0170   m_coll = new Data::BoardGameCollection(true);
0171 
0172   for(int j = 0; j < idList.size() && !m_cancelled; j += BGG_STEPSIZE) {
0173     QStringList ids;
0174     const int maxSize = qMin(j+BGG_STEPSIZE, idList.size());
0175     for(int k = j; k < maxSize; ++k) {
0176       ids += idList.at(k);
0177     }
0178 
0179 #if 0
0180     const QString xmlData = text(ids);
0181     myWarning() << "Remove debug from boardgamegeekimporter.cpp";
0182     QFile f(QStringLiteral("/tmp/test.xml"));
0183     if(f.open(QIODevice::WriteOnly)) {
0184       QTextStream t(&f);
0185       t.setCodec("UTF-8");
0186       t << xmlData;
0187     }
0188     f.close();
0189 #endif
0190 
0191     QString str = handler.applyStylesheet(text(ids));
0192 //    QString str = handler.applyStylesheet(xmlData);
0193     //  myDebug() << str;
0194 #if 0
0195     myWarning() << "Remove debug2 from boardgamegeekimporter.cpp";
0196     QFile f2(QStringLiteral("/tmp/test.tc"));
0197     if(f2.open(QIODevice::WriteOnly)) {
0198       QTextStream t(&f2);
0199       t.setCodec("UTF-8");
0200       t << str;
0201     }
0202     f2.close();
0203 #endif
0204 
0205     Import::TellicoImporter imp(str);
0206     imp.setOptions(imp.options() ^ Import::ImportShowImageErrors);
0207     Data::CollPtr c = imp.collection();
0208     if(!c) {
0209       continue;
0210     }
0211     // assume we always want the 3 extra fields defined in boardgamegeek2tellico.xsl
0212     if(!m_coll->hasField(QStringLiteral("bggid"))) {
0213       m_coll->addField(Data::FieldPtr(new Data::Field(*c->fieldByName(QStringLiteral("bggid")))));
0214       m_coll->addField(Data::FieldPtr(new Data::Field(*c->fieldByName(QStringLiteral("boardgamegeek-link")))));
0215       Data::FieldPtr f(new Data::Field(*c->fieldByName(QStringLiteral("artist"))));
0216       // also, let's assume that the artist field title should be illustrator instead of musician
0217       f->setTitle(i18nc("Comic Book Illustrator", "Artist"));
0218       m_coll->addField(f);
0219     }
0220     m_coll->addEntries(c->entries());
0221     setStatusMessage(imp.statusMessage());
0222 
0223     if(showProgress) {
0224       emit signalProgress(this, 10 + 100*maxSize/idList.size());
0225       qApp->processEvents();
0226     }
0227   }
0228 
0229   KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ImportOptions - BoardGameGeek"));
0230   config.writeEntry("User ID", m_user);
0231   config.writeEntry("Owned", m_ownedOnly);
0232 
0233   if(m_cancelled) {
0234     m_coll = Data::CollPtr();
0235   }
0236   return m_coll;
0237 }
0238 
0239 QWidget* BoardGameGeekImporter::widget(QWidget* parent_) {
0240   if(m_widget) {
0241     return m_widget;
0242   }
0243   m_widget = new QWidget(parent_);
0244   QVBoxLayout* l = new QVBoxLayout(m_widget);
0245 
0246   QGroupBox* gbox = new QGroupBox(i18n("BoardGameGeek Options"), m_widget);
0247   QFormLayout* lay = new QFormLayout(gbox);
0248 
0249   m_userEdit = new QLineEdit(gbox);
0250   m_userEdit->setText(m_user);
0251 
0252   m_checkOwned = new QCheckBox(i18n("Import owned items only"), gbox);
0253   m_checkOwned->setChecked(m_ownedOnly);
0254 
0255   lay->addRow(i18n("User ID:"), m_userEdit);
0256   lay->addRow(m_checkOwned);
0257 
0258   l->addWidget(gbox);
0259   l->addStretch(1);
0260 
0261   return m_widget;
0262 }
0263 
0264 QString BoardGameGeekImporter::text(const QStringList& idList_) const {
0265 //  myDebug() << idList_;
0266   QUrl u(QString::fromLatin1(BGG_THING_URL));
0267   QUrlQuery q;
0268   q.addQueryItem(QStringLiteral("id"), idList_.join(QLatin1String(",")));
0269   q.addQueryItem(QStringLiteral("type"), QStringLiteral("boardgame,boardgameexpansion"));
0270   u.setQuery(q);
0271 //  myDebug() << u;
0272   return FileHandler::readTextFile(u, true, true);
0273 }
0274 
0275 void BoardGameGeekImporter::slotCancel() {
0276   m_cancelled = true;
0277 }