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

0001 /***************************************************************************
0002     Copyright (C) 2003-2020 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 "alexandriaexporter.h"
0026 #include "../collection.h"
0027 #include "../fieldformat.h"
0028 #include "../images/imagefactory.h"
0029 #include "../images/image.h"
0030 #include "../utils/string_utils.h"
0031 #include "../progressmanager.h"
0032 #include "../utils/guiproxy.h"
0033 #include "../tellico_debug.h"
0034 
0035 #include <KLocalizedString>
0036 #include <KMessageBox>
0037 
0038 #include <QDir>
0039 #include <QTextStream>
0040 #include <QApplication>
0041 
0042 namespace {
0043   static const int ALEXANDRIA_MAX_SIZE_SMALL = 60;
0044   static const int ALEXANDRIA_MAX_SIZE_MEDIUM = 140;
0045 }
0046 
0047 using namespace Tellico;
0048 using Tellico::Export::AlexandriaExporter;
0049 
0050 AlexandriaExporter::AlexandriaExporter(Data::CollPtr coll_) : Exporter(coll_) {
0051 }
0052 
0053 QString AlexandriaExporter::escapeText(const QString& str_) {
0054   // no control characters at all which is more strict than XML (which is what string_utils::removeControlCharacters has)
0055   QString result;
0056   result.reserve(str_.size());
0057   for(int i = 0; i < str_.size(); ++i) {
0058     const ushort c = str_.at(i).unicode();
0059     if((c > 0x1F && c < 0x7F) || (c > 0xA0)) {
0060       result += str_.at(i);
0061     }
0062   }
0063   result.replace(QLatin1String("\""), QLatin1String("\\\""));
0064   return result;
0065 }
0066 
0067 QString AlexandriaExporter::formatString() const {
0068   return QStringLiteral("Alexandria");
0069 }
0070 
0071 bool AlexandriaExporter::exec() {
0072   Data::CollPtr coll = collection();
0073   if(!coll || (coll->type() != Data::Collection::Book && coll->type() != Data::Collection::Bibtex)) {
0074     myLog() << "bad collection";
0075     return false;
0076   }
0077 
0078   const QString alexDirName = QStringLiteral(".alexandria");
0079 
0080   // create if necessary
0081   const QUrl u = url();
0082   QDir libraryDir;
0083   if(u.isEmpty()) {
0084     libraryDir = QDir::home();
0085   } else {
0086     if(u.isLocalFile()) {
0087       if(!libraryDir.cd(u.toLocalFile())) {
0088         myWarning() << "can't change to directory:" << u.path();
0089         return false;
0090       }
0091     } else {
0092       myWarning() << "can't write to remote directory";
0093       return false;
0094     }
0095   }
0096 
0097   if(!libraryDir.cd(alexDirName)) {
0098     if(!libraryDir.mkdir(alexDirName) || !libraryDir.cd(alexDirName)) {
0099       myLog() << "can't locate directory";
0100       return false;
0101     }
0102   }
0103 
0104   // the collection title is the name of the directory to create
0105   if(libraryDir.cd(coll->title())) {
0106     int ret = KMessageBox::warningContinueCancel(GUI::Proxy::widget(),
0107                                                  i18n("<qt>An Alexandria library called <i>%1</i> already exists. "
0108                                                       "Any existing books in that library could be overwritten.</qt>",
0109                                                       coll->title()));
0110     if(ret == KMessageBox::Cancel) {
0111       return false;
0112     }
0113   } else if(!libraryDir.mkdir(coll->title()) || !libraryDir.cd(coll->title())) {
0114     return false; // could not create and cd to the dir
0115   }
0116 
0117   ProgressItem& item = ProgressManager::self()->newProgressItem(this, QString(), false);
0118   item.setTotalSteps(entries().count());
0119   ProgressItem::Done done(this);
0120   const uint stepSize = qMax(1, entries().count()/100);
0121   const bool showProgress = options() & ExportProgress;
0122 
0123   bool success = true;
0124   uint j = 0;
0125   foreach(const Data::EntryPtr& entry, entries()) {
0126     success &= writeFile(libraryDir, entry);
0127     if(showProgress && j%stepSize == 0) {
0128       item.setProgress(j);
0129       qApp->processEvents();
0130     }
0131     ++j;
0132   }
0133   return success;
0134 }
0135 
0136 // this isn't true YAML export, of course
0137 // everything is put between quotes except for the rating, just to be sure it's interpreted as a string
0138 bool AlexandriaExporter::writeFile(const QDir& dir_, Tellico::Data::EntryPtr entry_) {
0139   // the filename is the isbn without dashes, followed by .yaml
0140   QString isbn = entry_->field(QStringLiteral("isbn"));
0141   if(isbn.isEmpty()) {
0142     return false; // can't write it since Alexandria uses isbn as name of file
0143   }
0144   isbn.remove(QLatin1Char('-')); // remove dashes
0145 
0146   QFile file(dir_.absolutePath() + QDir::separator() + isbn + QLatin1String(".yaml"));
0147   if(!file.open(QIODevice::WriteOnly)) {
0148     return false;
0149   }
0150 
0151   // do we format?
0152   FieldFormat::Request format = (options() & Export::ExportFormatted ?
0153                                                 FieldFormat::ForceFormat :
0154                                                 FieldFormat::AsIsFormat);
0155 
0156   QTextStream ts(&file);
0157   // alexandria uses utf-8 all the time
0158   ts.setCodec("UTF-8");
0159   ts << "--- !ruby/object:Alexandria::Book\n";
0160   ts << "authors:\n";
0161   QStringList authors = FieldFormat::splitValue(entry_->formattedField(QStringLiteral("author"), format));
0162   for(QStringList::Iterator it = authors.begin(); it != authors.end(); ++it) {
0163     ts << "  - " << escapeText(*it) << "\n";
0164   }
0165   // Alexandria crashes when no authors, and uses n/a when none
0166   if(authors.count() == 0) {
0167     ts << "  - n/a\n";
0168   }
0169 
0170   QString tmp = entry_->title(format);
0171   ts << "title: \"" << escapeText(tmp) << "\"\n";
0172 
0173   // Alexandria refers to the binding as the edition
0174   tmp = entry_->formattedField(QStringLiteral("binding"), format);
0175   ts << "edition: \"" << escapeText(tmp) << "\"\n";
0176 
0177   // sometimes Alexandria interprets the isbn as a number instead of a string
0178   // I have no idea how to debug ruby, so err on safe side and add quotes
0179   ts << "isbn: \"" << isbn << "\"\n";
0180 
0181   static const QRegularExpression rx(QLatin1String("<br/?>"), QRegularExpression::CaseInsensitiveOption);
0182   tmp = entry_->formattedField(QStringLiteral("comments"), format);
0183   tmp.replace(rx, QStringLiteral("\n"));
0184   ts << "notes: |-\n";
0185   foreach(const QString& line, tmp.split(QLatin1Char('\n'))) {
0186     ts << "  " << escapeText(line) << "\n";
0187   }
0188 
0189   tmp = entry_->formattedField(QStringLiteral("publisher"), format);
0190   // publisher uses n/a when empty
0191   ts << "publisher: \"" << (tmp.isEmpty() ? QStringLiteral("n/a") : escapeText(tmp)) << "\"\n";
0192 
0193   tmp = entry_->formattedField(QStringLiteral("pub_year"), format);
0194   if(!tmp.isEmpty()) {
0195     ts << "publishing_year: \"" << escapeText(tmp) << "\"\n";
0196   }
0197 
0198   tmp = entry_->field(QStringLiteral("rating"));
0199   bool ok;
0200   int rating = Tellico::toUInt(tmp, &ok);
0201   if(ok) {
0202     ts << "rating: " << rating << "\n";
0203   }
0204 
0205   tmp = entry_->field(QStringLiteral("read"));
0206   if(!tmp.isEmpty()) {
0207     ts << "redd: true\n";
0208   }
0209 
0210   file.close();
0211 
0212   QString cover = entry_->field(QStringLiteral("cover"));
0213   if(cover.isEmpty() || !(options() & Export::ExportImages)) {
0214     return true; // all done
0215   }
0216 
0217   QImage img1(ImageFactory::imageById(cover));
0218   QImage img2;
0219   QString filename = dir_.absolutePath() + QDir::separator() + isbn;
0220   if(img1.height() > ALEXANDRIA_MAX_SIZE_SMALL) {
0221     if(img1.height() > ALEXANDRIA_MAX_SIZE_MEDIUM) { // limit maximum size
0222       img1 = img1.scaled(ALEXANDRIA_MAX_SIZE_MEDIUM, ALEXANDRIA_MAX_SIZE_MEDIUM, Qt::KeepAspectRatio);
0223     }
0224     img2 = img1.scaled(ALEXANDRIA_MAX_SIZE_SMALL, ALEXANDRIA_MAX_SIZE_SMALL, Qt::KeepAspectRatio);
0225   } else {
0226     // img2 is the small image
0227     img2 = img1;
0228     img1 = QImage();
0229   }
0230   return (img1.isNull() || img1.save(filename + QLatin1String("_medium.jpg"), "JPEG"))
0231       && img2.save(filename + QLatin1String("_small.jpg"), "JPEG");
0232 }