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 }